序列化下载与转位图
文档的真相是节点。下载 .svg 是把这棵树写成 XML;下载 .png 是一次 投影。投影不是可逆编辑格式。不要从 PNG 反推 path。
1. 序列化:XMLSerializer,不要 innerHTML
HTML 解析器吃过的 SVG,innerHTML 读回来可能丢命名空间、改大小写、塞进未知 HTML。写回未消毒字符串等于 注入 的入口。
const SVG_NS = "http://www.w3.org/2000/svg";
export function serializeSvg(root: SVGSVGElement): string {
if (!root.hasAttribute("xmlns")) {
root.setAttribute("xmlns", SVG_NS);
}
const vb = root.viewBox.baseVal;
if (!root.hasAttribute("viewBox") && vb.width > 0) {
root.setAttribute("viewBox", `${vb.x} ${vb.y} ${vb.width} ${vb.height}`);
}
return new XMLSerializer().serializeToString(root);
}
export function svgFileBlob(xml: string): Blob {
const withXmlDecl = xml.startsWith("<?xml")
? xml
: `<?xml version="1.0" encoding="UTF-8"?>\n${xml}`;
return new Blob([withXmlDecl], { type: "image/svg+xml;charset=utf-8" });
}
export function downloadBlob(blob: Blob, filename: string): void {
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = filename;
a.rel = "noopener";
a.click();
URL.revokeObjectURL(url);
}
自己的编辑器更好的路径:文档模型 → 生成 XML,根本不经过 DOM 的 innerHTML。DOM 只是视图。撤销也是文档命令,见 撤销重做。
xmlns:xlink 只在你还输出 xlink:href 时需要。新文档用 href。混用两套却只写一个 xmlns,下载后部分引擎丢 use。
2. 转 PNG:按 viewBox 离屏画,慎用 drawImage(svg)
把 SVG 当 HTMLImageElement 再 drawImage,生产上会撞三类问题:
| 坑 | 机制 |
|---|---|
| taint | 跨源 SVG 图、或 SVG 内跨源像素,画进 canvas 后读不回。见 Canvas 污染 CORS |
| 尺寸 | 当图解码时,用的是 SVG 的 width/height(或缺省 300×150),不是你以为的 viewBox 像素 |
| 外链 | <image href="https://...">、外部字体、外部 CSS,在「SVG 当图」模式下经常不加载 |
适用 drawImage:同源、无外链、宽高属性已经是你要的 CSS 像素、且你 不 需要读回像素(只合成预览)。
生产导出:克隆节点,剥外链或打成 data URL,按 viewBox 和 DPR 定 backing store,再 URL.createObjectURL 喂给 Image,画到 自己的 离屏 canvas。
export async function rasterizeSvg(input: {
svg: SVGSVGElement;
cssWidth: number;
cssHeight: number;
dpr: number;
}): Promise<Blob> {
const clone = input.svg.cloneNode(true);
if (!(clone instanceof SVGSVGElement)) {
throw new Error("clone failed");
}
clone.setAttribute("width", String(input.cssWidth));
clone.setAttribute("height", String(input.cssHeight));
const xml = serializeSvg(clone);
const url = URL.createObjectURL(svgFileBlob(xml));
try {
const image = await loadImage(url);
const canvas = document.createElement("canvas");
canvas.width = Math.max(1, Math.round(input.cssWidth * input.dpr));
canvas.height = Math.max(1, Math.round(input.cssHeight * input.dpr));
const ctx = canvas.getContext("2d");
if (!ctx) throw new Error("2d unavailable");
ctx.setTransform(input.dpr, 0, 0, input.dpr, 0, 0);
ctx.drawImage(image, 0, 0, input.cssWidth, input.cssHeight);
const blob = await new Promise<Blob>((resolve, reject) => {
canvas.toBlob((b) => (b ? resolve(b) : reject(new Error("toBlob returned null"))), "image/png");
});
return blob;
} finally {
URL.revokeObjectURL(url);
}
}
function loadImage(url: string): Promise<HTMLImageElement> {
return new Promise((resolve, reject) => {
const image = new Image();
image.onload = () => resolve(image);
image.onerror = () => reject(new Error("svg raster decode failed"));
image.src = url;
});
}
cssWidth / cssHeight 来自当前 viewport 盒子,或来自「导出 2× 预览」的目标 CSS 尺寸。dpr 是导出倍率,不是设备像素比必须跟屏幕走——打印 300 dpi 按物理尺寸另算,见 打印。
不要 toDataURL:同步编码 + 巨大字符串。blob URL 下载即可。克隆前剥掉 [data-chrome],避免选框进 PNG。
3. 外链 <image> 会丢
inline 编辑时外链图能显示,序列化后别人打开、或当 <img> 解码时经常空白。导出策略三选一,写进产品而不是碰运气:
- 导出前 fetch + CORS 打成 data URL(会 taint / 失败就要提示)。
- 导出包成 zip:SVG + 资源,href 改相对路径。
- 明确「仅几何,位图不打包」。
滤镜、蒙版在转 PNG 时按 canvas 像素分辨率 光栅,低 DPR 会发糊。这不是 viewBox 错了,是投影分辨率不够。
4. 失败形态
| 症状 | 原因 |
|---|---|
| 下下来的 SVG 浏览器打不开 | 缺 xmlns,或 innerHTML 吐的是 HTML 碎片 |
| PNG 是 300×150 的小图 | 当图解码走了缺省 replaced 尺寸,没按 viewBox 设宽高 |
| 预览有图,PNG 没图 | 外链 <image> 在安全模式下没加载 |
导出 SecurityError | 跨源 SVG / 跨源像素画进 canvas 再读回 |
| 编辑后再打开节点全没了 | 用未消毒 innerHTML 写回,解析器丢掉了 SVG 元素 |
权威资料
核对日期:2026-08-26