spb/countryatlas
Public
TypeScript 57%
Python 38.6%
JavaScript 3.6%
CSS 0.6%
1/**2 * Serialise an inline SVG chart to a PNG download (2× scale). CSS variables are resolved to literal colours3 * first because the serialised SVG is rendered in an isolated <img>, where `var(--…)` has no value.4 */5export async function downloadSvgAsPng(svg: SVGSVGElement, filename: string, scale = 2): Promise<void> {6 const clone = svg.cloneNode(true) as SVGSVGElement;7 const rootStyle = getComputedStyle(document.documentElement);8 const resolveVars = (s: string) => s.replace(/var\((--[\w-]+)\)/g, (_, name: string) => rootStyle.getPropertyValue(name).trim() || '#888');9 const all = [clone, ...Array.from(clone.querySelectorAll<SVGElement>('*'))];10 const src = [svg, ...Array.from(svg.querySelectorAll<SVGElement>('*'))];11 all.forEach((el, i) => {12 const s = src[i];13 if (!s) return;14 const cs = getComputedStyle(s);15 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']) {16 const v = cs.getPropertyValue(prop);17 if (v) el.style.setProperty(prop, v);18 }19 for (const attr of ['fill', 'stroke']) {20 const a = el.getAttribute(attr);21 if (a && a.includes('var(')) el.setAttribute(attr, resolveVars(a));22 }23 const st = el.getAttribute('style');24 if (st && st.includes('var(')) el.setAttribute('style', resolveVars(st));25 });26 const width = svg.clientWidth || Number(svg.getAttribute('width')) || 800;27 const height = svg.clientHeight || Number(svg.getAttribute('height')) || 400;28 clone.setAttribute('xmlns', 'http://www.w3.org/2000/svg');29 clone.setAttribute('width', String(width));30 clone.setAttribute('height', String(height));31 const bg = rootStyle.getPropertyValue('--surface').trim() || '#ffffff';32 const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');33 rect.setAttribute('width', '100%');34 rect.setAttribute('height', '100%');35 rect.setAttribute('fill', bg);36 clone.insertBefore(rect, clone.firstChild);37 const xml = new XMLSerializer().serializeToString(clone);38 const blob = new Blob([xml], { type: 'image/svg+xml;charset=utf-8' });39 const url = URL.createObjectURL(blob);40 try {41 const img = new Image();42 await new Promise<void>((resolve, reject) => {43 img.onload = () => resolve();44 img.onerror = () => reject(new Error('svg load failed'));45 img.src = url;46 });47 const canvas = document.createElement('canvas');48 canvas.width = Math.round(width * scale);49 canvas.height = Math.round(height * scale);50 const ctx = canvas.getContext('2d');51 if (!ctx) throw new Error('no canvas');52 ctx.scale(scale, scale);53 ctx.drawImage(img, 0, 0, width, height);54 const png = canvas.toDataURL('image/png');55 const a = document.createElement('a');56 a.href = png;57 a.download = filename.endsWith('.png') ? filename : `${filename}.png`;58 a.click();59 } finally {60 URL.revokeObjectURL(url);61 }62}63