/**
* 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);
}
}