SPB Git forge

spb/countryatlas

Public
20commits 1branches 0releases
268.3 MBsize
maindefault branch
12 days agolast push
TypeScript 57% Python 38.6% JavaScript 3.6% CSS 0.6%
2.9 KB · 63 lines typescript
Raw Blame History
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