Skip to content

Commit 71cf868

Browse files
committed
Add support for replacing custom fonts in svgs with links to the fonts
- the replacement font needs to be specified in the element config as either name and url or the whole font face definition
1 parent e949943 commit 71cf868

File tree

5 files changed

+120
-19
lines changed

5 files changed

+120
-19
lines changed

README.md

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,9 +52,27 @@ https://github.com/JiriLojda/integration-diagrams-net/blob/8f9d0d62ae2efed67da74
5252

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

55-
When using the `previewImageFormat: "svg"` and a custom font in the diagram, diagrams.net includes the whole font in the data-url for preview.
55+
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.
5656
This makes it (and the value as the data-url is saved as well) too large.
57-
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.
57+
To avoid the problem, you can do one of the following:
58+
* 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.
59+
* 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.
60+
```jsonc
61+
{
62+
"previewImageFormat": {
63+
"format": "svg"
64+
"customFontConfigType": "nameAndUrl",
65+
"fontName": "<your font name>",
66+
"fontUrl": "<your font url>"
67+
},
68+
// or
69+
"previewImageFormat": {
70+
"format": "svg"
71+
"customFontConfigType": "fontFaceDefinition",
72+
"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
73+
}
74+
}
75+
```
5876

5977
# Contributing
6078

src/IntegrationApp.tsx

Lines changed: 36 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -95,18 +95,42 @@ export const IntegrationApp: FC = () => {
9595
Delete diagram
9696
</Button>
9797
</div>
98-
<img
99-
alt="Preview of the current diagram"
100-
height={value.dimensions.height}
101-
width={value.dimensions.width}
102-
src={value.dataUrl}
103-
onClick={editorWindow ? focusEditor : editDiagram}
104-
style={{
105-
border: config?.previewBorder ? `${config.previewBorder.color} solid ${config.previewBorder.weight}px` : undefined,
106-
gridArea: "preview",
107-
cursor: "pointer",
108-
}}
109-
/>
98+
{config?.previewImageFormat?.format === "svg" && config.previewImageFormat.customFont
99+
? (
100+
<div
101+
onClick={editorWindow ? focusEditor : editDiagram}
102+
style={{
103+
border: config?.previewBorder ? `${config.previewBorder.color} solid ${config.previewBorder.weight}px` : undefined,
104+
gridArea: "preview",
105+
cursor: "pointer",
106+
}}
107+
>
108+
<object
109+
data={value.dataUrl}
110+
type="image/svg+xml"
111+
height={value.dimensions.height}
112+
width={value.dimensions.width}
113+
style={{ pointerEvents: "none" }} // we must handle click in the parent div as click events are not triggered from the object element
114+
>
115+
Preview of the current diagram
116+
</object>
117+
</div>
118+
)
119+
: (
120+
<img
121+
alt="Preview of the current diagram"
122+
height={value.dimensions.height}
123+
width={value.dimensions.width}
124+
src={value.dataUrl}
125+
onClick={editorWindow ? focusEditor : editDiagram}
126+
style={{
127+
border: config?.previewBorder ? `${config.previewBorder.color} solid ${config.previewBorder.weight}px` : undefined,
128+
gridArea: "preview",
129+
cursor: "pointer",
130+
}}
131+
/>
132+
)
133+
}
110134
</div>
111135
)
112136
: (

src/constants/readmeSnippets.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,15 @@ export const exampleConfiguration: Required<Config> = {
55
color: "#000000", // border color
66
weight: 1, // border width
77
},
8-
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.
8+
previewImageFormat: {
9+
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.
10+
// customFont: { // this can only be used with format: "svg"
11+
// customFontConfigType: "nameAndUrl", // alternatively this can also be "fontFaceDefinition"
12+
// fontUrl: "<url to our custom font>", // this must only be used with customFontConfigType: "nameAndUrl"
13+
// fontName: "<name of our custom font, this must be used inside the svg elements>", // this must only be used with customFontConfigType: "nameAndUrl"
14+
// // fontFaceDefinition: "<font face definition>", // this must only be used with customFontConfigType: "fontFaceDefinition"
15+
// }
16+
},
917
configuration: { // diagrams.net configuration, see https://www.diagrams.net/doc/faq/configure-diagram-editor for available keys
1018
colorNames: {
1119
"000000": "Our color",

src/handleDiagramsEvent.ts

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ export const handleDiagramsEvent = ({ config, editorWindowOrigin, editorWindow,
2525
const sendExportMessage = () => {
2626
postMessage({
2727
action: "export",
28-
format: config?.previewImageFormat,
28+
format: config?.previewImageFormat?.format,
2929
});
3030
};
3131

@@ -66,9 +66,10 @@ export const handleDiagramsEvent = ({ config, editorWindowOrigin, editorWindow,
6666
return;
6767
}
6868
case "export": {
69+
const svgStyleDef = config?.previewImageFormat?.format === "svg" ? createSvgStyleDef(config.previewImageFormat) : null;
6970
setValue({
7071
xml: data.xml,
71-
dataUrl: data.data,
72+
dataUrl: svgStyleDef ? replaceStyleDef(data.data, svgStyleDef) : data.data,
7273
dimensions: {
7374
width: Math.ceil(data.bounds.width),
7475
height: Math.ceil(data.bounds.height),
@@ -90,7 +91,35 @@ export const handleDiagramsEvent = ({ config, editorWindowOrigin, editorWindow,
9091
return;
9192
}
9293
}
94+
};
95+
96+
const createSvgStyleDef = (config: Config["previewImageFormat"] & { format: "svg" }) => {
97+
switch (config.customFont?.customFontConfigType) {
98+
case undefined:
99+
return null;
100+
case "nameAndUrl":
101+
return `@font-face { font-family: "${config.customFont.fontName}"; src: url("${config.customFont.fontUrl}"); }`;
102+
case "fontFaceDefinition":
103+
return config.customFont.fontFaceDefinition;
104+
default:
105+
throw new Error(`Unknown customFontConfigType "${(config.customFont as any).customFontConfigType}"`);
93106
}
107+
};
108+
109+
const replaceStyleDef = (dataUrl: string, newStyleDef: string): string => {
110+
const dataUrlPrefix = "data:image/svg+xml;base64,";
111+
const inputBase64 = dataUrl.replace(dataUrlPrefix, "");
112+
const inputBase64Bytes = Uint8Array.from(atob(inputBase64), m => m?.codePointAt(0) ?? 0);
113+
const decodedSvg = new TextDecoder().decode(inputBase64Bytes);
114+
115+
// replace the style tag
116+
const svgWithReplacedStyleDef = decodedSvg.replace(/<defs><style type="text\/css">.+<\/style><\/defs>/, `<defs><style type="text/css">${newStyleDef}</style></defs>`);
117+
118+
const resultBytes = new TextEncoder().encode(svgWithReplacedStyleDef);
119+
const resultBase64 = btoa(String.fromCodePoint(...resultBytes));
120+
121+
return dataUrlPrefix + resultBase64;
122+
};
94123

95124
type ExportMessage = Readonly<{
96125
action: "export";

src/useCustomElementContext.tsx

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,21 @@ export type Config = Readonly<{
1515
color: string;
1616
weight: number;
1717
}>;
18-
previewImageFormat?: "svg" | "png"; // svg is the default
18+
previewImageFormat?: PngImageFormatConfig | SvgImageFormatConfig; // svg is the default
1919
configuration?: Readonly<Record<string, unknown>>;
2020
}>;
2121

22+
type PngImageFormatConfig = Readonly<{ format: "png" }>;
23+
24+
type SvgImageFormatConfig = Readonly<{
25+
format: "svg";
26+
customFont?: SvgFontUrlConfig | SvgFontFaceDefinitionConfig;
27+
}>;
28+
29+
type SvgFontUrlConfig = Readonly<{ customFontConfigType: "nameAndUrl"; fontName: string; fontUrl: string }>;
30+
31+
type SvgFontFaceDefinitionConfig = Readonly<{ customFontConfigType: "fontFaceDefinition"; fontFaceDefinition: string }>;
32+
2233
type Params = Readonly<{
2334
heightPadding: number;
2435
emptyHeight: number;
@@ -71,12 +82,23 @@ export const useCustomElementContext = ({ heightPadding, emptyHeight }: Params)
7182
}
7283
};
7384

85+
const isPngFormatConfig: (v: unknown) => v is PngImageFormatConfig = tg.ObjectOf({ format: tg.ValueOf(["png"]) });
86+
87+
const isSvgFontUrlConfig: (v: unknown) => v is SvgFontUrlConfig = tg.ObjectOf({ customFontConfigType: tg.ValueOf(["nameAndUrl"]), fontName: tg.isString, fontUrl: tg.isString });
88+
89+
const isSvgFontFaceDefinitionConfig: (v: unknown) => v is SvgFontFaceDefinitionConfig = tg.ObjectOf({ customFontConfigType: tg.ValueOf(["fontFaceDefinition"]), fontFaceDefinition: tg.isString });
90+
91+
const isSvgFormatConfig: (v: unknown) => v is SvgImageFormatConfig = tg.ObjectOf({
92+
format: tg.ValueOf(["svg"]),
93+
customFont: tg.OptionalOf(tg.OneOf([isSvgFontUrlConfig, isSvgFontFaceDefinitionConfig])),
94+
});
95+
7496
const isStrictlyConfig: (v: unknown) => v is Config = tg.ObjectOf({
7597
previewBorder: tg.OptionalOf(tg.ObjectOf({
7698
color: tg.isString,
7799
weight: tg.isNumber,
78100
})),
79-
previewImageFormat: tg.ValueOf(["svg", "png"] as const),
101+
previewImageFormat: tg.OptionalOf(tg.OneOf([isPngFormatConfig, isSvgFormatConfig])),
80102
configuration: tg.OptionalOf(tg.isObject),
81103
});
82104

0 commit comments

Comments
 (0)