Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eca8781445 | ||
|
|
9626f6b5df |
Vendored
+2
@@ -9,6 +9,7 @@
|
|||||||
"corg",
|
"corg",
|
||||||
"cssnano",
|
"cssnano",
|
||||||
"esserializer",
|
"esserializer",
|
||||||
|
"fontawesome",
|
||||||
"fsegurai",
|
"fsegurai",
|
||||||
"gantt",
|
"gantt",
|
||||||
"gitgraph",
|
"gitgraph",
|
||||||
@@ -30,6 +31,7 @@
|
|||||||
"Stackable",
|
"Stackable",
|
||||||
"tailwindcss",
|
"tailwindcss",
|
||||||
"uparrow",
|
"uparrow",
|
||||||
|
"webfonts",
|
||||||
"zenuml"
|
"zenuml"
|
||||||
],
|
],
|
||||||
"vitest.commandLine": "pnpm test:unit",
|
"vitest.commandLine": "pnpm test:unit",
|
||||||
|
|||||||
+1
-1
@@ -115,7 +115,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=20.19.1"
|
"node": ">=20.19.0"
|
||||||
},
|
},
|
||||||
"packageManager": "pnpm@10.10.0+sha512.d615db246fe70f25dcfea6d8d73dee782ce23e2245e3c4f6f888249fb568149318637dca73c2c5c8ef2a4ca0d5657fb9567188bfab47f566d1ee6ce987815c39",
|
"packageManager": "pnpm@10.10.0+sha512.d615db246fe70f25dcfea6d8d73dee782ce23e2245e3c4f6f888249fb568149318637dca73c2c5c8ef2a4ca0d5657fb9567188bfab47f566d1ee6ce987815c39",
|
||||||
"pnpm": {
|
"pnpm": {
|
||||||
|
|||||||
@@ -19,38 +19,123 @@
|
|||||||
import ExternalLinkIcon from '~icons/material-symbols/open-in-new-rounded';
|
import ExternalLinkIcon from '~icons/material-symbols/open-in-new-rounded';
|
||||||
import WidthIcon from '~icons/material-symbols/width-rounded';
|
import WidthIcon from '~icons/material-symbols/width-rounded';
|
||||||
|
|
||||||
const FONT_AWESOME_URL = `https://cdnjs.cloudflare.com/ajax/libs/font-awesome/${FAVersion}/css/all.min.css`;
|
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;
|
type Exporter = (context: CanvasRenderingContext2D, image: HTMLImageElement) => () => void;
|
||||||
|
|
||||||
const getFileName = (extension: string) =>
|
const getFileName = (extension: string) =>
|
||||||
`mermaid-diagram-${dayjs().format('YYYY-MM-DD-HHmmss')}.${extension}`;
|
`mermaid-diagram-${dayjs().format('YYYY-MM-DD-HHmmss')}.${extension}`;
|
||||||
|
|
||||||
const getSvgElement = () => {
|
const getSvgElement = async () => {
|
||||||
const svgElement = document.querySelector('#container svg')?.cloneNode(true) as HTMLElement;
|
try {
|
||||||
svgElement.setAttribute('xmlns:xlink', 'http://www.w3.org/1999/xlink');
|
$inputStateStore.panZoom = false;
|
||||||
return svgElement;
|
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 = (svg?: HTMLElement, width?: number, height?: number): string => {
|
const injectFontAwesome = async ({
|
||||||
if (svg) {
|
svgString,
|
||||||
// Prevents the SVG size of the interface from being changed
|
fontAwesomeEmbedMode
|
||||||
svg = svg.cloneNode(true) as HTMLElement;
|
}: {
|
||||||
}
|
svgString: string;
|
||||||
height && svg?.setAttribute('height', `${height}px`);
|
fontAwesomeEmbedMode: 'raw' | 'url';
|
||||||
width && svg?.setAttribute('width', `${width}px`); // Workaround https://stackoverflow.com/questions/28690643/firefox-error-rendering-an-svg-image-to-html5-canvas-with-drawimage
|
}) => {
|
||||||
|
if (fontAwesomeEmbedMode === 'url') {
|
||||||
if (!svg) {
|
return `<?xml version="1.0" encoding="UTF-8"?>
|
||||||
svg = getSvgElement();
|
<?xml-stylesheet href="${fontAwesomeURLs.css}" type="text/css"?>
|
||||||
|
${svgString}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const [fontAwesomeCSS, fontBlob] = await Promise.all([
|
||||||
|
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) => {
|
||||||
|
reader.onloadend = () => {
|
||||||
|
const base64 = reader.result as string;
|
||||||
|
// Remove the data URL prefix (data:application/octet-stream;base64,)
|
||||||
|
resolve(base64.split(',')[1]);
|
||||||
|
};
|
||||||
|
reader.readAsDataURL(fontBlob);
|
||||||
|
});
|
||||||
|
const styleString = `
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Font Awesome 6 Free';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 900;
|
||||||
|
src: url(data:font/woff2;base64,${fontBase64}) format('woff2');
|
||||||
|
}
|
||||||
|
|
||||||
|
.fa {
|
||||||
|
font-family: 'Font Awesome 6 Free';
|
||||||
|
font-weight: 900;
|
||||||
|
// TODO: Make this dynamic from config
|
||||||
|
font-size: 16px;
|
||||||
|
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
|
const svgString = svg.outerHTML
|
||||||
.replaceAll('<br>', '<br/>')
|
.replaceAll('<br>', '<br/>')
|
||||||
.replaceAll(/<img([^>]*)>/g, (m, g: string) => `<img ${g} />`);
|
.replaceAll(/<img([^>]*)>/g, (m, g: string) => `<img ${g} />`);
|
||||||
|
|
||||||
return toBase64(`<?xml version="1.0" encoding="UTF-8"?>
|
const svgStringWithFontAwesome = await injectFontAwesome({
|
||||||
<?xml-stylesheet href="${FONT_AWESOME_URL}" type="text/css"?>
|
svgString,
|
||||||
${svgString}`);
|
fontAwesomeEmbedMode: embedFontAwesome ? 'raw' : 'url'
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(svgStringWithFontAwesome);
|
||||||
|
return toBase64(svgStringWithFontAwesome);
|
||||||
};
|
};
|
||||||
|
|
||||||
const simulateDownload = (download: string, href: string): void => {
|
const simulateDownload = (download: string, href: string): void => {
|
||||||
@@ -62,16 +147,10 @@ ${svgString}`);
|
|||||||
};
|
};
|
||||||
|
|
||||||
const exportImage = async (event: Event, exporter: Exporter) => {
|
const exportImage = async (event: Event, exporter: Exporter) => {
|
||||||
$inputStateStore.panZoom = false;
|
event.stopPropagation();
|
||||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
event.preventDefault();
|
||||||
await waitForRender();
|
|
||||||
const canvas = document.createElement('canvas');
|
const canvas = document.createElement('canvas');
|
||||||
const svg = document.querySelector<HTMLElement>('#container svg');
|
const { svg, box } = await getSvgElement();
|
||||||
if (!svg) {
|
|
||||||
throw new Error('svg not found');
|
|
||||||
}
|
|
||||||
|
|
||||||
const box = svg.getBoundingClientRect();
|
|
||||||
|
|
||||||
if (imageSizeMode === 'width') {
|
if (imageSizeMode === 'width') {
|
||||||
const ratio = box.height / box.width;
|
const ratio = box.height / box.width;
|
||||||
@@ -98,18 +177,14 @@ ${svgString}`);
|
|||||||
const image = new Image();
|
const image = new Image();
|
||||||
image.addEventListener('load', () => {
|
image.addEventListener('load', () => {
|
||||||
exporter(context, image)();
|
exporter(context, image)();
|
||||||
$inputStateStore.panZoom = true;
|
|
||||||
});
|
});
|
||||||
image.src = `data:image/svg+xml;base64,${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
|
// Fallback to set panZoom to true after 2 seconds
|
||||||
// This is a workaround for the case when the image is not loaded
|
// This is a workaround for the case when the image is not loaded
|
||||||
setTimeout(() => {
|
// setTimeout(() => {
|
||||||
if (!$inputStateStore.panZoom) {
|
// if (!$inputStateStore.panZoom) {
|
||||||
$inputStateStore.panZoom = true;
|
// }
|
||||||
}
|
// }, 2000);
|
||||||
}, 2000);
|
|
||||||
event.stopPropagation();
|
|
||||||
event.preventDefault();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const downloadImage: Exporter = (context, image) => {
|
const downloadImage: Exporter = (context, image) => {
|
||||||
@@ -160,8 +235,8 @@ ${svgString}`);
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const onDownloadSVG = () => {
|
const onDownloadSVG = async () => {
|
||||||
simulateDownload(getFileName('svg'), `data:image/svg+xml;base64,${getBase64SVG()}`);
|
simulateDownload(getFileName('svg'), `data:image/svg+xml;base64,${await getBase64SVG()}`);
|
||||||
logEvent('download', {
|
logEvent('download', {
|
||||||
type: 'svg'
|
type: 'svg'
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user