跳到主要内容

污染CORS与像素导出

1. origin-clean 怎么被弄脏

一张 canvas 默认干净。下列绘制会把它标脏(不完全清单,以 WHATWG 为准):

  • 跨源 HTMLImageElement / SVGImageElement 且请求不是 CORS 成功
  • 跨源 HTMLVideoElement 当前帧
  • 另一张 已经脏了的 canvas / ImageBitmap
  • 某些跨源 createImageBitmap 失败路径下的占位

同源 blob URL、data URL、已 CORS 成功的图,保持干净。

脏了之后:

  • 仍然可以继续 drawImage / fill 给用户看
  • 不能读像素、不能导出、不能把这张表面当干净源再画到另一张还指望另一张干净
export function assertClean(canvas: HTMLCanvasElement): void {
try {
canvas.getContext("2d")?.getImageData(0, 0, 1, 1);
} catch (error) {
if (error instanceof DOMException && error.name === "SecurityError") {
throw new Error("canvas is tainted; export and readback are blocked");
}
throw error;
}
}

不要在热路径用 1×1 getImageData 当「是否脏」探针——它会触发读回。架构上 跟踪你画过的源:所有位图必须经过自己的 loader,loader 只产出干净的 ImageBitmap

2. 加载路径:先 CORS,再 bitmap

export async function loadCleanBitmap(url: string): Promise<ImageBitmap> {
const response = await fetch(url, {
mode: "cors",
credentials: "omit",
});
if (!response.ok) {
throw new Error(`fetch ${url} -> ${response.status}`);
}
const blob = await response.blob();
return createImageBitmap(blob);
}

fetch + createImageBitmapnew Image(); img.crossOrigin = "anonymous" 更好控:

  • 失败是 HTTP 状态,不是「onload 了但 canvas 脏了」
  • 不依赖解码完成时机
  • credentials: "omit" 避免把 cookie 带到跨源图床上(否则 CORS 头必须是精确 Origin 而不能是 *

如果必须走 HTMLImageElement

export function loadImageElement(url: string): Promise<HTMLImageElement> {
return new Promise((resolve, reject) => {
const image = new Image();
image.crossOrigin = "anonymous";
image.decoding = "async";
image.onload = () => resolve(image);
image.onerror = () => reject(new Error(`image failed: ${url}`));
image.src = url;
});
}

crossOrigin 必须在设 src 之前 设。设晚了,这次请求按无 CORS 走,画上去就脏。CDN 没回 Access-Control-Allow-Origin 时,onload 仍可能触发——元素能显示在 <img> 里,但 不能 画进要导出的 canvas。产品表现是「预览正常,点下载才报 SecurityError」。

3. 导出:toBlob 优先,编码在主线程外

export function exportPng(canvas: HTMLCanvasElement): Promise<Blob> {
return new Promise((resolve, reject) => {
canvas.toBlob((blob) => {
if (!blob) {
reject(new Error("toBlob returned null"));
return;
}
resolve(blob);
}, "image/png");
});
}

export async function exportJpeg(
canvas: HTMLCanvasElement,
quality: number,
): Promise<Blob> {
const blob = await new Promise<Blob | null>((resolve) => {
canvas.toBlob(resolve, "image/jpeg", quality);
});
if (!blob) {
throw new Error("jpeg encode failed");
}
return blob;
}
API问题
toDataURL同步编码 + 巨大 base64 字符串,主线程卡死,内存翻倍
toBlob异步,给 Blob;下载用 URL.createObjectURL
OffscreenCanvas.convertToBlobWorker 里导出,主线程只收 Blob

JPEG 丢掉 alpha。透明区域变黑或变白取决于引擎填的底。需要透明就 PNG。需要高压缩且无透明,先在不透明表面上画,再 JPEG。

质量参数:JPEG 的 quality 是 0–1。WebP 同理,但编码器支持度用探测,不要假设所有 WebView 都能 image/webp

下载:

export function downloadBlob(blob: Blob, filename: string): void {
const url = URL.createObjectURL(blob);
const anchor = document.createElement("a");
anchor.href = url;
anchor.download = filename;
anchor.rel = "noopener";
document.body.append(anchor);
anchor.click();
anchor.remove();
URL.revokeObjectURL(url);
}

4. 隐私与指纹

干净 canvas 的像素也可以做指纹(字体、光栅差异、GPU)。若产品有隐私预算:

  • 不要在登录前的第三方脚本里 toDataURL
  • 用户导出是正当读回;静默采样整张表面不是
  • desynchronized / 不同 colorSpace 会改变像素,不要用像素 hash 当「场景是否相等」的测试金标——用场景图序列化

5. 失败形态

症状原因
预览正常,导出 SecurityError跨源图无 CORS,或 crossOrigin 设晚了
导出卡 2 秒toDataURL 同步编码 4K × DPR
PNG 很大、JPEG 黑底alpha + JPEG
另一张离屏也被脏drawImage 了已污染源
fetch CORS 失败图床 * + 带 cookie,或根本没有 ACAO

权威资料

核对日期:2026-08-26