v156 · graphics · canvas

SVG foreignObject does not taint the canvas for blob URLs

Drawing an SVG that contains a <foreignObject> onto a canvas taints it when the SVG came from an HTTP URL, and does not when it came from a data: URI — every engine agrees on both. The blob: case was the disagreement: Chromium and WebKit tainted, Gecko did not. Chrome 156 matches Gecko, which matters because it removes the reason to reach for a data URI at all.

concepts

  1. Taint matrix

    The same SVG drawn onto three canvases from three URL schemes, then getImageData() called on each. The verdicts are read from real SecurityErrors, not from a table someone typed — so on a pre-156 browser the blob column reports tainted, and that is the change in one screen.

  2. HTML into a canvas

    The use case this unlocks: compose styled HTML, wrap it in a foreignObject, draw it to a canvas from a blob URL, and export a PNG. Edit the content and the styling live, then try the export and see whether the canvas lets you.

  3. What the blob URL saves

    Percent-encoding inflates the markup by about 60%, and the inflated string is held in memory alongside the source. Whether the data URI is also slower turns out to be a question worth measuring rather than assuming — this one measures it, on your machine, at sizes from a label to a full page.

why it shipped

foreignObject is the only way to get HTML content into a canvas — laid out by the real engine, with real fonts and real CSS — which is why every "screenshot this element" library is built on it. The tainting rules exist because an SVG can reference cross-origin content and canvas readback would leak it; a data: URI cannot reference anything the page did not already have, so it is safe, and every engine agrees.

A blob: URL is the same situation: its contents came from the page itself. Gecko treated it that way; Chromium and WebKit did not, so authors had to percent-encode their markup into a data URI to get a readable canvas — roughly 60% more bytes, held in memory as a string, for content that can run to hundreds of kilobytes. The shipping intent cites processing speed as the motivation; the cost demo here measures both paths so you can see what actually moves on your machine. Either way, Chrome 156 removes the need to choose.

the API

const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="400" height="200">
  <foreignObject width="400" height="200">
    <div xmlns="http://www.w3.org/1999/xhtml">${html}</div>
  </foreignObject>
</svg>`;

// Chrome 156: this canvas stays readable.
const url = URL.createObjectURL(new Blob([svg], { type: "image/svg+xml" }));
const image = new Image();
image.onload = () => {
  context.drawImage(image, 0, 0);
  URL.revokeObjectURL(url);
  canvas.toBlob(save);       // before 156: SecurityError
};
image.src = url;

references