From 71cf868d33f32633380fae35f1260f802a0c5c1b Mon Sep 17 00:00:00 2001 From: Jiri Lojda Date: Tue, 20 Feb 2024 15:59:35 +0100 Subject: [PATCH] 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 --- README.md | 22 +++++++++++++-- src/IntegrationApp.tsx | 48 ++++++++++++++++++++++++--------- src/constants/readmeSnippets.ts | 10 ++++++- src/handleDiagramsEvent.ts | 33 +++++++++++++++++++++-- src/useCustomElementContext.tsx | 26 ++++++++++++++++-- 5 files changed, 120 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index 4e06e3e..5d46fd9 100644 --- a/README.md +++ b/README.md @@ -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 `` tag. You will need to use the `` tag to display such an SVG. +```jsonc +{ + "previewImageFormat": { + "format": "svg" + "customFontConfigType": "nameAndUrl", + "fontName": "", + "fontUrl": "" + }, + // 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 diff --git a/src/IntegrationApp.tsx b/src/IntegrationApp.tsx index 73be2cf..bd3d165 100644 --- a/src/IntegrationApp.tsx +++ b/src/IntegrationApp.tsx @@ -95,18 +95,42 @@ export const IntegrationApp: FC = () => { Delete diagram - Preview of the current diagram + {config?.previewImageFormat?.format === "svg" && config.previewImageFormat.customFont + ? ( +
+ + Preview of the current diagram + +
+ ) + : ( + Preview of the current diagram + ) + } ) : ( diff --git a/src/constants/readmeSnippets.ts b/src/constants/readmeSnippets.ts index a6e186b..396d291 100644 --- a/src/constants/readmeSnippets.ts +++ b/src/constants/readmeSnippets.ts @@ -5,7 +5,15 @@ export const exampleConfiguration: Required = { 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: "", // this must only be used with customFontConfigType: "nameAndUrl" + // fontName: "", // this must only be used with customFontConfigType: "nameAndUrl" + // // fontFaceDefinition: "", // 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", diff --git a/src/handleDiagramsEvent.ts b/src/handleDiagramsEvent.ts index 09830ca..43419b6 100644 --- a/src/handleDiagramsEvent.ts +++ b/src/handleDiagramsEvent.ts @@ -25,7 +25,7 @@ export const handleDiagramsEvent = ({ config, editorWindowOrigin, editorWindow, const sendExportMessage = () => { postMessage({ action: "export", - format: config?.previewImageFormat, + format: config?.previewImageFormat?.format, }); }; @@ -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), @@ -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(/`); + + const resultBytes = new TextEncoder().encode(svgWithReplacedStyleDef); + const resultBase64 = btoa(String.fromCodePoint(...resultBytes)); + + return dataUrlPrefix + resultBase64; +}; type ExportMessage = Readonly<{ action: "export"; diff --git a/src/useCustomElementContext.tsx b/src/useCustomElementContext.tsx index a556357..09bd253 100644 --- a/src/useCustomElementContext.tsx +++ b/src/useCustomElementContext.tsx @@ -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>; }>; +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; @@ -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), });