Canvas Image Extraction
A canvas element draws pixels with JavaScript, so there is no file URL to copy. To save it you call canvas.toDataURL(), which returns a PNG data URI. A canvas drawing cross-origin content becomes tainted and throws SecurityError.
Why it matters
Charts, photo editors, games, and data visualisations render straight to canvas, so the picture on screen has no file behind it. Exporting the canvas is the only way to keep it. And browser security means it works only when the canvas has stayed same-origin. Cross-origin canvases refuse export by design to prevent pixel leaking.
Understanding the tainted-canvas rule saves frustration when an export fails. It's not a bug; it's a security boundary.
How it works
A <canvas> element draws pixels using JavaScript APIs. When you want to save what's on screen, you call canvas.toDataURL(), which returns a PNG-encoded data URI with the entire image baked in as base64 text.
const canvas = document.querySelector('canvas');
const pngDataUri = canvas.toDataURL('image/png');
// Returns something like: data:image/png;base64,iVBORw0KGgo...
The catch: if the canvas has drawn any cross-origin image or resource (an image from a different domain, or a font from a CDN), the canvas becomes "tainted". Trying to export it throws a SecurityError. The browser won't let you extract pixels that came from another origin, because that could leak data you're not supposed to see.
To avoid tainting, every image on the canvas must either be same-origin or have CORS headers that allow it. A canvas drawing only same-origin images, text, and shapes exports cleanly.
What does not matter
The tainted-canvas check happens at export time, not when the image is drawn. You can draw cross-origin content to the canvas without error; the error only fires when you try to call toDataURL(). This can surprise you if you're working with third-party images.
Also, the size of the exported data URI doesn't matter. A 2000-pixel canvas exports to a string that can be several megabytes, and that's fine. Scalpel Images does skip very small canvases (1px x 1px, often trackers or 1x1 spacers) to avoid clutter.
Code example
// Same-origin canvas (works fine)
const canvas1 = document.querySelector('canvas');
const img1 = new Image();
img1.src = '/images/photo.jpg'; // Same origin
img1.onload = () => {
const ctx = canvas1.getContext('2d');
ctx.drawImage(img1, 0, 0);
const data = canvas1.toDataURL('image/png');
console.log(data); // Works: base64 data URI
};
// Cross-origin canvas (tainted)
const canvas2 = document.querySelector('canvas');
const img2 = new Image();
img2.src = 'https://cdn.example.com/image.jpg'; // Cross-origin
img2.onload = () => {
const ctx = canvas2.getContext('2d');
ctx.drawImage(img2, 0, 0);
try {
const data = canvas2.toDataURL('image/png');
} catch (e) {
console.error(e.message);
// SecurityError: tainted canvas
}
};
// Cross-origin canvas with CORS (works)
const canvas3 = document.querySelector('canvas');
const img3 = new Image();
img3.crossOrigin = 'anonymous';
img3.src = 'https://cdn.example.com/image.jpg';
img3.onload = () => {
const ctx = canvas3.getContext('2d');
ctx.drawImage(img3, 0, 0);
const data = canvas3.toDataURL('image/png'); // Works if server sends CORS headers
};
The fix for cross-origin images: set crossOrigin="anonymous" on the <img> tag and ensure the server sends Access-Control-Allow-Origin: * (or your domain).
How Scalpel Images shows it
Scalpel Images finds every <canvas> on the page and tries to export it. If the canvas is clean (same-origin), you get a tile with a CNV badge and a preview of the image. If it's tainted, you see the canvas grayed out with a note saying "tainted (cross-origin content)". You can select and download the clean canvases; tainted ones are skipped and never guessed at.