This commit is contained in:
Sidharth Vinod
2025-05-26 22:49:02 +05:30
parent 9626f6b5df
commit eca8781445
3 changed files with 88 additions and 45 deletions
+2
View File
@@ -9,6 +9,7 @@
"corg",
"cssnano",
"esserializer",
"fontawesome",
"fsegurai",
"gantt",
"gitgraph",
@@ -30,6 +31,7 @@
"Stackable",
"tailwindcss",
"uparrow",
"webfonts",
"zenuml"
],
"vitest.commandLine": "pnpm test:unit",
+1 -1
View File
@@ -115,7 +115,7 @@
]
},
"engines": {
"node": ">=20.19.1"
"node": ">=20.19.0"
},
"packageManager": "pnpm@10.10.0+sha512.d615db246fe70f25dcfea6d8d73dee782ce23e2245e3c4f6f888249fb568149318637dca73c2c5c8ef2a4ca0d5657fb9567188bfab47f566d1ee6ce987815c39",
"pnpm": {
+85 -44
View File
@@ -19,38 +19,56 @@
import ExternalLinkIcon from '~icons/material-symbols/open-in-new-rounded';
import WidthIcon from '~icons/material-symbols/width-rounded';
const FONT_AWESOME_URL = `https://cdnjs.cloudflare.com/ajax/libs/font-awesome/${FAVersion}`;
const fontAwesomeURLs = (() => {
const baseUrl = `https://cdnjs.cloudflare.com/ajax/libs/font-awesome/${FAVersion}`;
return {
css: `${baseUrl}/css/all.min.css`,
woff2: `${baseUrl}/webfonts/fa-solid-900.woff2`
};
})();
type Exporter = (context: CanvasRenderingContext2D, image: HTMLImageElement) => () => void;
const getFileName = (extension: string) =>
`mermaid-diagram-${dayjs().format('YYYY-MM-DD-HHmmss')}.${extension}`;
const getSvgElement = () => {
const svgElement = document.querySelector('#container svg')?.cloneNode(true) as HTMLElement;
svgElement.setAttribute('xmlns:xlink', 'http://www.w3.org/1999/xlink');
return svgElement;
const getSvgElement = async () => {
try {
$inputStateStore.panZoom = false;
await new Promise((resolve) => setTimeout(resolve, 1000));
await waitForRender();
const svgElement = document.querySelector('#container svg');
if (!svgElement) {
throw new Error('svg not found');
}
svgElement.setAttribute('xmlns:xlink', 'http://www.w3.org/1999/xlink');
return {
svg: svgElement.cloneNode(true) as HTMLElement,
box: svgElement.querySelector('g')!.getBoundingClientRect()
};
} finally {
setTimeout(() => {
$inputStateStore.panZoom = true;
}, 10000);
}
};
const getBase64SVG = async (
svg?: HTMLElement,
width?: number,
height?: number
): Promise<string> => {
if (svg) {
// Prevents the SVG size of the interface from being changed
svg = svg.cloneNode(true) as HTMLElement;
}
height && svg?.setAttribute('height', `${height}px`);
width && svg?.setAttribute('width', `${width}px`); // Workaround https://stackoverflow.com/questions/28690643/firefox-error-rendering-an-svg-image-to-html5-canvas-with-drawimage
if (!svg) {
svg = getSvgElement();
const injectFontAwesome = async ({
svgString,
fontAwesomeEmbedMode
}: {
svgString: string;
fontAwesomeEmbedMode: 'raw' | 'url';
}) => {
if (fontAwesomeEmbedMode === 'url') {
return `<?xml version="1.0" encoding="UTF-8"?>
<?xml-stylesheet href="${fontAwesomeURLs.css}" type="text/css"?>
${svgString}`;
}
const [fontAwesomeCSS, fontBlob] = await Promise.all([
fetch(`${FONT_AWESOME_URL}/css/all.min.css`).then((response) => response.text()),
fetch(`${FONT_AWESOME_URL}/webfonts/fa-solid-900.woff2`).then((res) => res.blob())
fetch(fontAwesomeURLs.css).then((response) => response.text()),
fetch(fontAwesomeURLs.woff2).then((res) => res.blob())
]);
const reader = new FileReader();
const fontBase64 = await new Promise<string>((resolve) => {
@@ -61,7 +79,7 @@
};
reader.readAsDataURL(fontBlob);
});
const fontFaceCSS = `
const styleString = `
@font-face {
font-family: 'Font Awesome 6 Free';
font-style: normal;
@@ -77,14 +95,47 @@
width: 16px;
fill: black;
}
`;
${fontAwesomeCSS}
`;
return svgString.replace('<style>', `<style>${styleString}`);
};
const getBase64SVG = async ({
svg,
box,
width,
height,
embedFontAwesome
}: {
svg?: HTMLElement;
box?: DOMRect;
width?: number;
height?: number;
embedFontAwesome?: boolean;
} = {}): Promise<string> => {
if (!svg || !box) {
({ svg, box } = await getSvgElement());
}
console.log(box);
// Prevents the SVG size of the interface from being changed
// svg = svg.cloneNode(true) as HTMLElement;
height && svg.setAttribute('height', `${height}px`);
width && svg.setAttribute('width', `${width}px`); // Workaround https://stackoverflow.com/questions/28690643/firefox-error-rendering-an-svg-image-to-html5-canvas-with-drawimage
svg.setAttribute('viewBox', `0 0 ${box.width} ${box.height}`);
const svgString = svg.outerHTML
.replaceAll('<br>', '<br/>')
.replaceAll(/<img([^>]*)>/g, (m, g: string) => `<img ${g} />`)
.replace('<style>', `<style>${fontFaceCSS} ${fontAwesomeCSS}`);
.replaceAll(/<img([^>]*)>/g, (m, g: string) => `<img ${g} />`);
return toBase64(svgString);
const svgStringWithFontAwesome = await injectFontAwesome({
svgString,
fontAwesomeEmbedMode: embedFontAwesome ? 'raw' : 'url'
});
console.log(svgStringWithFontAwesome);
return toBase64(svgStringWithFontAwesome);
};
const simulateDownload = (download: string, href: string): void => {
@@ -96,16 +147,10 @@
};
const exportImage = async (event: Event, exporter: Exporter) => {
$inputStateStore.panZoom = false;
await new Promise((resolve) => setTimeout(resolve, 1000));
await waitForRender();
event.stopPropagation();
event.preventDefault();
const canvas = document.createElement('canvas');
const svg = document.querySelector<HTMLElement>('#container svg');
if (!svg) {
throw new Error('svg not found');
}
const box = svg.getBoundingClientRect();
const { svg, box } = await getSvgElement();
if (imageSizeMode === 'width') {
const ratio = box.height / box.width;
@@ -132,18 +177,14 @@
const image = new Image();
image.addEventListener('load', () => {
exporter(context, image)();
$inputStateStore.panZoom = true;
});
image.src = `data:image/svg+xml;base64,${await getBase64SVG(svg, canvas.width, canvas.height)}`;
image.src = `data:image/svg+xml;base64,${await getBase64SVG({ svg, box, height: canvas.height, width: canvas.width, embedFontAwesome: true })}`;
// Fallback to set panZoom to true after 2 seconds
// This is a workaround for the case when the image is not loaded
setTimeout(() => {
if (!$inputStateStore.panZoom) {
$inputStateStore.panZoom = true;
}
}, 2000);
event.stopPropagation();
event.preventDefault();
// setTimeout(() => {
// if (!$inputStateStore.panZoom) {
// }
// }, 2000);
};
const downloadImage: Exporter = (context, image) => {