Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0fa868821b | ||
|
|
8b3962c5e5 | ||
|
|
9626f6b5df |
@@ -19,7 +19,7 @@
|
|||||||
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 FONT_AWESOME_URL = `https://cdnjs.cloudflare.com/ajax/libs/font-awesome/${FAVersion}`;
|
||||||
|
|
||||||
type Exporter = (context: CanvasRenderingContext2D, image: HTMLImageElement) => () => void;
|
type Exporter = (context: CanvasRenderingContext2D, image: HTMLImageElement) => () => void;
|
||||||
|
|
||||||
@@ -32,7 +32,23 @@
|
|||||||
return svgElement;
|
return svgElement;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getBase64SVG = (svg?: HTMLElement, width?: number, height?: number): string => {
|
/**
|
||||||
|
* Converts a Blob to base64 string
|
||||||
|
*/
|
||||||
|
export function blobToBase64(blob: Blob): Promise<string> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.addEventListener('load', () => resolve(reader.result as string));
|
||||||
|
reader.addEventListener('error', () => reject(new Error('Failed to convert blob to base64')));
|
||||||
|
reader.readAsDataURL(blob);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const getBase64SVG = async (
|
||||||
|
svg?: HTMLElement,
|
||||||
|
width?: number,
|
||||||
|
height?: number
|
||||||
|
): Promise<string> => {
|
||||||
if (svg) {
|
if (svg) {
|
||||||
// Prevents the SVG size of the interface from being changed
|
// Prevents the SVG size of the interface from being changed
|
||||||
svg = svg.cloneNode(true) as HTMLElement;
|
svg = svg.cloneNode(true) as HTMLElement;
|
||||||
@@ -44,13 +60,88 @@
|
|||||||
svg = getSvgElement();
|
svg = getSvgElement();
|
||||||
}
|
}
|
||||||
|
|
||||||
const svgString = svg.outerHTML
|
try {
|
||||||
.replaceAll('<br>', '<br/>')
|
// Fetch both regular and brands Font Awesome CSS and font files
|
||||||
.replaceAll(/<img([^>]*)>/g, (m, g: string) => `<img ${g} />`);
|
const [fontAwesomeCSS, solidFontBlob, brandsFontBlob, regularFontBlob] = 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())
|
||||||
|
.catch(() => new Blob()),
|
||||||
|
fetch(`${FONT_AWESOME_URL}/webfonts/fa-brands-400.woff2`)
|
||||||
|
.then((res) => res.blob())
|
||||||
|
.catch(() => new Blob()),
|
||||||
|
fetch(`${FONT_AWESOME_URL}/webfonts/fa-regular-400.woff2`)
|
||||||
|
.then((res) => res.blob())
|
||||||
|
.catch(() => new Blob())
|
||||||
|
]);
|
||||||
|
|
||||||
return toBase64(`<?xml version="1.0" encoding="UTF-8"?>
|
// Convert font blobs to base64
|
||||||
<?xml-stylesheet href="${FONT_AWESOME_URL}" type="text/css"?>
|
const [solidFontBase64, brandsFontBase64, regularFontBase64] = await Promise.all([
|
||||||
${svgString}`);
|
blobToBase64(solidFontBlob).then((data) => data.split(',')[1]),
|
||||||
|
blobToBase64(brandsFontBlob).then((data) => data.split(',')[1]),
|
||||||
|
blobToBase64(regularFontBlob).then((data) => data.split(',')[1])
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Create font-face definitions for all Font Awesome types
|
||||||
|
const fontFaceCSS = `
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Font Awesome 6 Free';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 900;
|
||||||
|
src: url(data:font/woff2;base64,${solidFontBase64}) format('woff2');
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Font Awesome 6 Free';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
src: url(data:font/woff2;base64,${regularFontBase64}) format('woff2');
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Font Awesome 6 Brands';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
src: url(data:font/woff2;base64,${brandsFontBase64}) format('woff2');
|
||||||
|
}
|
||||||
|
|
||||||
|
.fa, .fas {
|
||||||
|
font-family: 'Font Awesome 6 Free';
|
||||||
|
font-weight: 900;
|
||||||
|
}
|
||||||
|
|
||||||
|
.far {
|
||||||
|
font-family: 'Font Awesome 6 Free';
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fab {
|
||||||
|
font-family: 'Font Awesome 6 Brands';
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fa, .fas, .far, .fab {
|
||||||
|
font-size: 16px;
|
||||||
|
width: 16px;
|
||||||
|
fill: currentColor;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Fix self-closing tags for XML compatibility
|
||||||
|
const svgString = svg.outerHTML
|
||||||
|
.replaceAll('<br>', '<br/>')
|
||||||
|
.replaceAll(/<img([^>]*)>/g, (m, g: string) => `<img ${g} />`)
|
||||||
|
.replace('<style>', `<style>${fontFaceCSS} ${fontAwesomeCSS}`);
|
||||||
|
|
||||||
|
return toBase64(svgString);
|
||||||
|
} catch {
|
||||||
|
// Fallback to simple SVG if font loading fails
|
||||||
|
const svgString = svg.outerHTML
|
||||||
|
.replaceAll('<br>', '<br/>')
|
||||||
|
.replaceAll(/<img([^>]*)>/g, (m, g: string) => `<img ${g} />`);
|
||||||
|
|
||||||
|
return toBase64(svgString);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const simulateDownload = (download: string, href: string): void => {
|
const simulateDownload = (download: string, href: string): void => {
|
||||||
@@ -100,7 +191,7 @@ ${svgString}`);
|
|||||||
exporter(context, image)();
|
exporter(context, image)();
|
||||||
$inputStateStore.panZoom = true;
|
$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, canvas.width, canvas.height)}`;
|
||||||
// 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(() => {
|
||||||
@@ -160,8 +251,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