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 20, 2024
1 parent e949943 commit 06cef1c
Show file tree
Hide file tree
Showing 4 changed files with 100 additions and 17 deletions.
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 06cef1c

Please sign in to comment.