返回 presentation-ai
echartSvgExport.ts
根目录 / src / components / presentation / export / echartSvgExport.ts
1 function ensureSvgDimensions(svg: SVGSVGElement, container: Element) {
2 const rect = container.getBoundingClientRect();
3 const width = Math.max(1, Math.round(rect.width));
4 const height = Math.max(1, Math.round(rect.height));
5
6 if (!svg.getAttribute("width")) {
7 svg.setAttribute("width", String(width));
8 }
9 if (!svg.getAttribute("height")) {
10 svg.setAttribute("height", String(height));
11 }
12 if (!svg.getAttribute("viewBox")) {
13 svg.setAttribute("viewBox", `0 0 ${width} ${height}`);
14 }
15 svg.setAttribute("xmlns", "http://www.w3.org/2000/svg");
16 }
17
18 const SVG_PRESENTATION_ATTRIBUTES = [
19 "color",
20 "fill",
21 "flood-color",
22 "font-family",
23 "stroke",
24 "stop-color",
25 ] as const;
26
27 function shouldResolveSvgValue(value: string): boolean {
28 const normalized = value.trim();
29
30 return (
31 normalized === "currentColor" ||
32 normalized === "inherit" ||
33 normalized.includes("var(")
34 );
35 }
36
37 function getResolvedStyleValue(
38 computedStyle: CSSStyleDeclaration,
39 property: string,
40 ): string {
41 return property === "color"
42 ? computedStyle.color
43 : computedStyle.getPropertyValue(property);
44 }
45
46 function inlineResolvedAttribute(
47 sourceElement: Element,
48 clonedElement: Element,
49 computedStyle: CSSStyleDeclaration,
50 attribute: (typeof SVG_PRESENTATION_ATTRIBUTES)[number],
51 ): void {
52 const value = clonedElement.getAttribute(attribute);
53 if (!value || !shouldResolveSvgValue(value)) return;
54
55 const resolvedValue = getResolvedStyleValue(computedStyle, attribute).trim();
56 if (!resolvedValue) return;
57
58 clonedElement.setAttribute(attribute, resolvedValue);
59
60 if (attribute === "color") {
61 return;
62 }
63
64 const sourceColor = getResolvedStyleValue(
65 getComputedStyle(sourceElement),
66 "color",
67 ).trim();
68 if (value.trim() === "currentColor" && sourceColor) {
69 clonedElement.setAttribute(attribute, sourceColor);
70 }
71 }
72
73 function inlineResolvedStyleProperties(
74 clonedElement: Element,
75 computedStyle: CSSStyleDeclaration,
76 ): void {
77 if (!(clonedElement instanceof SVGElement)) return;
78
79 for (const attribute of SVG_PRESENTATION_ATTRIBUTES) {
80 const value = clonedElement.style.getPropertyValue(attribute);
81 if (!value || !shouldResolveSvgValue(value)) continue;
82
83 const resolvedValue = getResolvedStyleValue(
84 computedStyle,
85 attribute,
86 ).trim();
87 if (!resolvedValue) continue;
88
89 clonedElement.style.setProperty(attribute, resolvedValue);
90 }
91 }
92
93 function inlineComputedSvgPresentationStyles(
94 sourceSvg: SVGSVGElement,
95 clonedSvg: SVGSVGElement,
96 ): void {
97 const sourceElements = [
98 sourceSvg,
99 ...Array.from(sourceSvg.querySelectorAll("*")),
100 ];
101 const clonedElements = [
102 clonedSvg,
103 ...Array.from(clonedSvg.querySelectorAll("*")),
104 ];
105
106 sourceElements.forEach((sourceElement, index) => {
107 const clonedElement = clonedElements[index];
108 if (!clonedElement) return;
109
110 const computedStyle = getComputedStyle(sourceElement);
111
112 for (const attribute of SVG_PRESENTATION_ATTRIBUTES) {
113 inlineResolvedAttribute(
114 sourceElement,
115 clonedElement,
116 computedStyle,
117 attribute,
118 );
119 }
120
121 inlineResolvedStyleProperties(clonedElement, computedStyle);
122 });
123 }
124
125 function svgSourceToBase64DataUrl(source: string): string {
126 const bytes = new TextEncoder().encode(source);
127 let binary = "";
128 for (const byte of bytes) {
129 binary += String.fromCharCode(byte);
130 }
131
132 return `data:image/svg+xml;base64,${btoa(binary)}`;
133 }
134
135 export function getEChartSvgDataUrl(element: Element): string | null {
136 const chartRoot = element.matches("[data-echart='true']")
137 ? element
138 : element.querySelector("[data-echart='true']");
139 if (!chartRoot) return null;
140
141 const svg = chartRoot.querySelector("svg");
142 if (!(svg instanceof SVGSVGElement)) return null;
143
144 const clonedSvg = svg.cloneNode(true);
145 if (!(clonedSvg instanceof SVGSVGElement)) return null;
146
147 ensureSvgDimensions(clonedSvg, chartRoot);
148 inlineComputedSvgPresentationStyles(svg, clonedSvg);
149
150 const source = new XMLSerializer().serializeToString(clonedSvg);
151 return svgSourceToBase64DataUrl(source);
152 }
153
153 lines TYPESCRIPT