Skip to content

Commit

Permalink
Add support for replacing custom fonts in svgs with links to the fonts
Browse files Browse the repository at this point in the history
- the replacement font needs to be specified in the element config as either name and url or the whole font face definition
  • Loading branch information
JiriLojda committed Feb 27, 2024
1 parent e949943 commit 71cf868
Show file tree
Hide file tree
Showing 5 changed files with 120 additions and 19 deletions.
22 changes: 20 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,27 @@ https://github.com/JiriLojda/integration-diagrams-net/blob/8f9d0d62ae2efed67da74

## Value is too large for Kontent.ai with a custom font used in the diagram.

When using the `previewImageFormat: "svg"` and a custom font in the diagram, diagrams.net includes the whole font in the data-url for preview.
When using the `"previewImageFormat": { "format": "svg" }` and a custom font in the diagram, diagrams.net includes the whole font in the data-url for preview.
This makes it (and the value as the data-url is saved as well) too large.
To avoid the problem, set `previewImageFormat: "png"` in your configuration. Png's don't have this problem, but are usually bigger so the svg is the default.
To avoid the problem, you can do one of the following:
* Set `"previewImageFormat": { "format": "png" }` in your configuration. PNG's don't have this problem, but are usually bigger and don't scale so the SVG is the default.
* Make the integration replace the custom font in the SVG with your font's url. You will need to provide the url in the configuration. Please, keep in mind that SVGs with links to external sources won't load the source in the `<img />` tag. You will need to use the `<object />` tag to display such an SVG.
```jsonc
{
"previewImageFormat": {
"format": "svg"
"customFontConfigType": "nameAndUrl",
"fontName": "<your font name>",
"fontUrl": "<your font url>"
},
// or
"previewImageFormat": {
"format": "svg"
"customFontConfigType": "fontFaceDefinition",
"fontFaceDefinition": "@font-face { font-name: 'your-font-name'; src: 'your-font-url'; }" // this allows more flexibility, you can have multiple @font-face definitions and custom font-face properties
}
}
```

# Contributing

Expand Down
48 changes: 36 additions & 12 deletions src/IntegrationApp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -95,18 +95,42 @@ export const IntegrationApp: FC = () => {
Delete diagram
</Button>
</div>
<img
alt="Preview of the current diagram"
height={value.dimensions.height}
width={value.dimensions.width}
src={value.dataUrl}
onClick={editorWindow ? focusEditor : editDiagram}
style={{
border: config?.previewBorder ? `${config.previewBorder.color} solid ${config.previewBorder.weight}px` : undefined,
gridArea: "preview",
cursor: "pointer",
}}
/>
{config?.previewImageFormat?.format === "svg" && config.previewImageFormat.customFont
? (
<div
onClick={editorWindow ? focusEditor : editDiagram}
style={{
border: config?.previewBorder ? `${config.previewBorder.color} solid ${config.previewBorder.weight}px` : undefined,
gridArea: "preview",
cursor: "pointer",
}}
>
<object
data={value.dataUrl}
type="image/svg+xml"
height={value.dimensions.height}
width={value.dimensions.width}
style={{ pointerEvents: "none" }} // we must handle click in the parent div as click events are not triggered from the object element
>
Preview of the current diagram
</object>
</div>
)
: (
<img
alt="Preview of the current diagram"
height={value.dimensions.height}
width={value.dimensions.width}
src={value.dataUrl}
onClick={editorWindow ? focusEditor : editDiagram}
style={{
border: config?.previewBorder ? `${config.previewBorder.color} solid ${config.previewBorder.weight}px` : undefined,
gridArea: "preview",
cursor: "pointer",
}}
/>
)
}
</div>
)
: (
Expand Down
10 changes: 9 additions & 1 deletion src/constants/readmeSnippets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,15 @@ export const exampleConfiguration: Required<Config> = {
color: "#000000", // border color
weight: 1, // border width
},
previewImageFormat: "png", // one of "svg" or "png". Set this to png when you use custom font as diagrams.net includes the font in the generated preview data-url which makes it too large.
previewImageFormat: {
format: "png", // one of "svg" or "png". Set this to png when you use custom font as diagrams.net includes the font in the generated preview data-url which makes it too large.
// customFont: { // this can only be used with format: "svg"
// customFontConfigType: "nameAndUrl", // alternatively this can also be "fontFaceDefinition"
// fontUrl: "<url to our custom font>", // this must only be used with customFontConfigType: "nameAndUrl"
// fontName: "<name of our custom font, this must be used inside the svg elements>", // this must only be used with customFontConfigType: "nameAndUrl"
// // fontFaceDefinition: "<font face definition>", // this must only be used with customFontConfigType: "fontFaceDefinition"
// }
},
configuration: { // diagrams.net configuration, see https://www.diagrams.net/doc/faq/configure-diagram-editor for available keys
colorNames: {
"000000": "Our color",
Expand Down
33 changes: 31 additions & 2 deletions src/handleDiagramsEvent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export const handleDiagramsEvent = ({ config, editorWindowOrigin, editorWindow,
const sendExportMessage = () => {
postMessage({
action: "export",
format: config?.previewImageFormat,
format: config?.previewImageFormat?.format,
});
};

Expand Down Expand Up @@ -66,9 +66,10 @@ export const handleDiagramsEvent = ({ config, editorWindowOrigin, editorWindow,
return;
}
case "export": {
const svgStyleDef = config?.previewImageFormat?.format === "svg" ? createSvgStyleDef(config.previewImageFormat) : null;
setValue({
xml: data.xml,
dataUrl: data.data,
dataUrl: svgStyleDef ? replaceStyleDef(data.data, svgStyleDef) : data.data,
dimensions: {
width: Math.ceil(data.bounds.width),
height: Math.ceil(data.bounds.height),
Expand All @@ -90,7 +91,35 @@ export const handleDiagramsEvent = ({ config, editorWindowOrigin, editorWindow,
return;
}
}
};

const createSvgStyleDef = (config: Config["previewImageFormat"] & { format: "svg" }) => {
switch (config.customFont?.customFontConfigType) {
case undefined:
return null;
case "nameAndUrl":
return `@font-face { font-family: "${config.customFont.fontName}"; src: url("${config.customFont.fontUrl}"); }`;
case "fontFaceDefinition":
return config.customFont.fontFaceDefinition;
default:
throw new Error(`Unknown customFontConfigType "${(config.customFont as any).customFontConfigType}"`);
}
};

const replaceStyleDef = (dataUrl: string, newStyleDef: string): string => {
const dataUrlPrefix = "data:image/svg+xml;base64,";
const inputBase64 = dataUrl.replace(dataUrlPrefix, "");
const inputBase64Bytes = Uint8Array.from(atob(inputBase64), m => m?.codePointAt(0) ?? 0);
const decodedSvg = new TextDecoder().decode(inputBase64Bytes);

// replace the style tag
const svgWithReplacedStyleDef = decodedSvg.replace(/<defs><style type="text\/css">.+<\/style><\/defs>/, `<defs><style type="text/css">${newStyleDef}</style></defs>`);

const resultBytes = new TextEncoder().encode(svgWithReplacedStyleDef);
const resultBase64 = btoa(String.fromCodePoint(...resultBytes));

return dataUrlPrefix + resultBase64;
};

type ExportMessage = Readonly<{
action: "export";
Expand Down
26 changes: 24 additions & 2 deletions src/useCustomElementContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,21 @@ export type Config = Readonly<{
color: string;
weight: number;
}>;
previewImageFormat?: "svg" | "png"; // svg is the default
previewImageFormat?: PngImageFormatConfig | SvgImageFormatConfig; // svg is the default
configuration?: Readonly<Record<string, unknown>>;
}>;

type PngImageFormatConfig = Readonly<{ format: "png" }>;

type SvgImageFormatConfig = Readonly<{
format: "svg";
customFont?: SvgFontUrlConfig | SvgFontFaceDefinitionConfig;
}>;

type SvgFontUrlConfig = Readonly<{ customFontConfigType: "nameAndUrl"; fontName: string; fontUrl: string }>;

type SvgFontFaceDefinitionConfig = Readonly<{ customFontConfigType: "fontFaceDefinition"; fontFaceDefinition: string }>;

type Params = Readonly<{
heightPadding: number;
emptyHeight: number;
Expand Down Expand Up @@ -71,12 +82,23 @@ export const useCustomElementContext = ({ heightPadding, emptyHeight }: Params)
}
};

const isPngFormatConfig: (v: unknown) => v is PngImageFormatConfig = tg.ObjectOf({ format: tg.ValueOf(["png"]) });

const isSvgFontUrlConfig: (v: unknown) => v is SvgFontUrlConfig = tg.ObjectOf({ customFontConfigType: tg.ValueOf(["nameAndUrl"]), fontName: tg.isString, fontUrl: tg.isString });

const isSvgFontFaceDefinitionConfig: (v: unknown) => v is SvgFontFaceDefinitionConfig = tg.ObjectOf({ customFontConfigType: tg.ValueOf(["fontFaceDefinition"]), fontFaceDefinition: tg.isString });

const isSvgFormatConfig: (v: unknown) => v is SvgImageFormatConfig = tg.ObjectOf({
format: tg.ValueOf(["svg"]),
customFont: tg.OptionalOf(tg.OneOf([isSvgFontUrlConfig, isSvgFontFaceDefinitionConfig])),
});

const isStrictlyConfig: (v: unknown) => v is Config = tg.ObjectOf({
previewBorder: tg.OptionalOf(tg.ObjectOf({
color: tg.isString,
weight: tg.isNumber,
})),
previewImageFormat: tg.ValueOf(["svg", "png"] as const),
previewImageFormat: tg.OptionalOf(tg.OneOf([isPngFormatConfig, isSvgFormatConfig])),
configuration: tg.OptionalOf(tg.isObject),
});

Expand Down

0 comments on commit 71cf868

Please sign in to comment.