/** * Serialise an inline SVG chart to a PNG download (2× scale). CSS variables are resolved to literal colours * first because the serialised SVG is rendered in an isolated , where `var(--…)` has no value. */ export async function downloadSvgAsPng(svg: SVGSVGElement, filename: string, scale = 2): Promise { const clone = svg.cloneNode(true) as SVGSVGElement; const rootStyle = getComputedStyle(document.documentElement); const resolveVars = (s: string) => s.replace(/var\((--[\w-]+)\)/g, (_, name: string) => rootStyle.getPropertyValue(name).trim() || '#888'); const all = [clone, ...Array.from(clone.querySelectorAll('*'))]; const src = [svg, ...Array.from(svg.querySelectorAll('*'))]; all.forEach((el, i) => { const s = src[i]; if (!s) return; const cs = getComputedStyle(s); for (const prop of ['fill', 'stroke', 'stroke-width', 'stroke-dasharray', 'font-family', 'font-size', 'font-weight', 'opacity', 'fill-opacity', 'shape-rendering', 'stroke-linecap', 'stroke-linejoin']) { const v = cs.getPropertyValue(prop); if (v) el.style.setProperty(prop, v); } for (const attr of ['fill', 'stroke']) { const a = el.getAttribute(attr); if (a && a.includes('var(')) el.setAttribute(attr, resolveVars(a)); } const st = el.getAttribute('style'); if (st && st.includes('var(')) el.setAttribute('style', resolveVars(st)); }); const width = svg.clientWidth || Number(svg.getAttribute('width')) || 800; const height = svg.clientHeight || Number(svg.getAttribute('height')) || 400; clone.setAttribute('xmlns', 'http://www.w3.org/2000/svg'); clone.setAttribute('width', String(width)); clone.setAttribute('height', String(height)); const bg = rootStyle.getPropertyValue('--surface').trim() || '#ffffff'; const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect'); rect.setAttribute('width', '100%'); rect.setAttribute('height', '100%'); rect.setAttribute('fill', bg); clone.insertBefore(rect, clone.firstChild); const xml = new XMLSerializer().serializeToString(clone); const blob = new Blob([xml], { type: 'image/svg+xml;charset=utf-8' }); const url = URL.createObjectURL(blob); try { const img = new Image(); await new Promise((resolve, reject) => { img.onload = () => resolve(); img.onerror = () => reject(new Error('svg load failed')); img.src = url; }); const canvas = document.createElement('canvas'); canvas.width = Math.round(width * scale); canvas.height = Math.round(height * scale); const ctx = canvas.getContext('2d'); if (!ctx) throw new Error('no canvas'); ctx.scale(scale, scale); ctx.drawImage(img, 0, 0, width, height); const png = canvas.toDataURL('image/png'); const a = document.createElement('a'); a.href = png; a.download = filename.endsWith('.png') ? filename : `${filename}.png`; a.click(); } finally { URL.revokeObjectURL(url); } }