Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for replacing custom fonts in svgs with links to the fonts #15

Merged
merged 1 commit into from
Feb 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading