Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Use HTMLCanvasElement.toBlob() to encode a canvas as a PNG, create a temporary object URL for the resulting Blob, and trigger a download link. This asynchronous approach avoids building a large in-memory data URL and is the practical default for downloadable images.

The reliable canvas-to-PNG recipe

The complete operation has four steps: serialize the canvas, create an object URL, attach it to an anchor, and revoke the URL after the browser has started the download.

function downloadCanvasAsPng(canvas, filename = "canvas.png") {
  canvas.toBlob((blob) => {
    if (!blob) {
      throw new Error("Canvas could not be encoded.");
    }

    const url = URL.createObjectURL(blob);
    const link = document.createElement("a");
    link.href = url;
    link.download = filename;
    link.click();

    // Let the browser begin the download before releasing the URL.
    setTimeout(() => URL.revokeObjectURL(url), 0);
  }, "image/png");
}

Call it with the canvas element you want to save:

const canvas = document.querySelector("#drawing");
downloadCanvasAsPng(canvas, "my-drawing.png");

toBlob() runs asynchronously and passes the encoded Blob to its callback. The callback receives null when an image cannot be created. Serialization can also throw a SecurityError when the canvas is not origin-clean; the cross-origin section below explains that case.

A complete runnable example

This page creates a canvas, draws a simple scene, and adds a button that downloads the bitmap as PNG. The element’s bitmap dimensions are set in HTML, so the saved file is 800 by 450 pixels regardless of how CSS displays it.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
<!doctype html>
<html lang="en">
<meta charset="utf-8">
<title>Canvas PNG export</title>
<style>
  canvas { width: 400px; border: 1px solid #999; }
</style>
<canvas id="drawing" width="800" height="450"></canvas>
<button id="save" type="button">Save PNG</button>
<script>
  const canvas = document.querySelector("#drawing");
  const context = canvas.getContext("2d");

  context.fillStyle = "#152238";
  context.fillRect(0, 0, canvas.width, canvas.height);
  context.fillStyle = "#52d1dc";
  context.beginPath();
  context.arc(400, 225, 120, 0, Math.PI * 2);
  context.fill();
  context.fillStyle = "white";
  context.font = "bold 42px sans-serif";
  context.textAlign = "center";
  context.fillText("Canvas PNG", 400, 235);

  function downloadCanvasAsPng(canvas, filename = "canvas.png") {
    canvas.toBlob((blob) => {
      if (!blob) throw new Error("Canvas could not be encoded.");
      const url = URL.createObjectURL(blob);
      const link = document.createElement("a");
      link.href = url;
      link.download = filename;
      link.click();
      setTimeout(() => URL.revokeObjectURL(url), 0);
    }, "image/png");
  }

  document.querySelector("#save").addEventListener("click", () => {
    downloadCanvasAsPng(canvas, "canvas.png");
  });
</script>

Keeping the export behind a button click also makes the user action explicit. If you generate a canvas in response to another event, call the same function only after the drawing operations that should appear in the file have completed.

Canvas dimensions, CSS size, and what gets saved

PNG serialization uses the canvas bitmap, not its displayed CSS box. In the example, width="800" and height="450" define the output pixels, while canvas { width: 400px; } only changes on-screen presentation. Set the element's width and height attributes (or the corresponding JavaScript properties) to the intended output dimensions before drawing.

Changing CSS dimensions after drawing does not add detail to the exported file. Conversely, assigning a new bitmap width or height clears the drawing, so set those values before rendering. A zero-sized canvas cannot produce a useful image, and maximum canvas dimensions differ between browsers and devices; avoid assuming one universal limit for very large exports.

toBlob() versus toDataURL()

Both APIs serialize the same canvas bitmap and both enforce the same origin-clean security rule. They differ in how the result is represented and what you do next.

What’s actually slowing this PC down?

Pick the symptom - the matching free tool is one click away.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
Aspect toBlob() toDataURL()
Result A Blob delivered asynchronously to a callback A data-URL string returned immediately
Large-image memory Usually preferable for downloads; avoids holding the complete encoded image in a JavaScript string Can consume substantial memory because the complete image is represented as an in-memory string
Typical next step Create an object URL with URL.createObjectURL() and use an anchor or <img> Assign the returned string directly to an anchor's href or an image's src
Best fit Downloads, previews of larger images, and code that can handle an asynchronous callback Small images or APIs that specifically require an inline data URL

The concise data-URL alternative is:

const pngDataUrl = canvas.toDataURL("image/png");
const link = document.createElement("a");
link.href = pngDataUrl;
link.download = "canvas.png";
link.click();

For a large canvas, prefer the toBlob() pattern. Data URLs can also run into URL-length limits in places that impose them. When you keep an object URL for a visible preview or a link that remains on screen, revoke it when the user is finished; revoking it immediately can invalidate the resource before the browser uses it.

Why “Tainted canvases may not be exported” appears

A canvas can display a remote image and still be forbidden from being read. If an image from another origin is drawn without the required CORS permission, the canvas becomes tainted. Calls such as getImageData(), toBlob(), and toDataURL() then fail with a SecurityError.

Load a remote image with CORS enabled

Set the image's CORS mode before assigning its URL:

const image = new Image();
image.crossOrigin = "anonymous"; // set before src
image.onload = () => {
  const canvas = document.querySelector("#drawing");
  canvas.getContext("2d").drawImage(image, 0, 0);
  downloadCanvasAsPng(canvas);
};
image.src = "https://images.example.test/photo.png";

The image server must answer with an appropriate Access-Control-Allow-Origin response. The crossOrigin property (or the HTML crossorigin attribute) only requests a CORS-enabled fetch; it cannot override the remote server's policy.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

When client-side changes cannot fix it

If the remote server does not grant your origin access, no JavaScript-only change can make that bitmap exportable. Use an image hosted with the correct CORS headers, serve the asset from your own origin, or remove that cross-origin image from the canvas. Do not try to work around the browser's security exception by changing the download code; the restriction applies to serialization itself.

PNG format and output checks

  • PNG is the default when the format argument is omitted, and browsers fall back to PNG when a requested format is unsupported. Passing "image/png" makes the intent explicit.
  • If you request another format, inspect blob.type (or the data-URL prefix) to detect a fallback to PNG rather than assuming the requested type was honored.
  • The encoded file contains the canvas bitmap's pixels. CSS scaling does not change those dimensions.
  • For encoded formats that support resolution metadata, browser APIs use 96 dpi metadata. That value is metadata and does not change the bitmap's pixel width or height.
  • Very large or zero-sized canvases may fail or produce unusable output. Browser and device limits vary, so handle a null blob and test the largest dimensions your application permits.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Troubleshooting common failures

Symptom Likely cause Fix
The download throws SecurityError. A foreign-origin image or video tainted the canvas. Load the asset with CORS before setting src, and have the asset server return Access-Control-Allow-Origin. Otherwise use a same-origin or CORS-enabled asset.
The callback receives null. The browser could not encode the bitmap, often because it is invalid, empty, or beyond implementation limits. Check bitmap dimensions, confirm that drawing completed, reduce the export size, and handle the null result instead of creating an object URL.
The PNG is blank. Nothing was drawn yet, the canvas was cleared by changing its dimensions, or the export ran before asynchronous drawing finished. Set dimensions first, draw all content, wait for image onload, then call the export function.
The file is smaller or less detailed than expected. CSS dimensions were mistaken for bitmap dimensions. Set the canvas element's width and height attributes to the required pixel size before rendering.
The saved file has an unexpected type. The requested MIME type is unsupported and the browser used PNG fallback. Inspect blob.type or the data-URL prefix and use the matching file extension.
A preview disappears immediately. An object URL was revoked while it was still in use. Keep the URL alive for the lifetime of the preview and revoke it when the preview or link is removed.

Performance and reliability practices

  • Prefer toBlob() for downloads and larger images so the encoded result is handled as a binary Blob rather than a long JavaScript string.
  • Keep the callback's null check. Encoding is not guaranteed to succeed for every bitmap size or browser implementation.
  • Release object URLs after use. Long-lived previews should revoke them during cleanup, not immediately after assigning them.
  • Choose bitmap dimensions deliberately before drawing. Increasing only CSS size cannot improve exported resolution, while oversized bitmaps can hit device-specific limits.
  • Validate the output type when format matters, especially when requesting a format other than PNG.
  • Treat CORS as a server-side dependency. A browser property can request permission, but only the image server can grant it.

Or skip the browser setup: ScreenshotNeo

If your actual requirement is a screenshot of a rendered webpage that contains a canvas—not extraction of the canvas bitmap itself—ScreenshotNeo is the first alternative to try: it removes cookie banners, popups, and chat widgets before capture, bills only clean shots, and has a $5 paid plan for 3,000 shots.

One GET request captures a URL as a PNG, JPEG, WebP, or PDF. The API is useful when the page must be rendered in a controlled browser environment rather than exported by code running inside that page. See the complete parameter list in the ScreenshotNeo documentation.

cURL

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://example.com -o shot.webp

Python

import requests

r = requests.get(
    "https://api.screenshotneo.com/v1/shot",
    params={"access_key": "YOUR_API_KEY", "url": "https://example.com"},
    timeout=90,
)
r.raise_for_status()
open("shot.webp", "wb").write(r.content)

Node.js

const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://example.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);

ScreenshotNeo accepts consent banners as a visitor and removes more than 60 known consent platforms, newsletter popups, and chat widgets; each cleanup step can be disabled. Bot checks, CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not billed, and response headers identify the page verdict and billing status. Its MCP server exposes take_screenshot, get_page_info, and capture_pdf to Claude, Cursor, and other MCP clients.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Every plan includes the full feature set. The Free plan includes 1,000 screenshots per month with no card; paid plans are Starter $5 for 3,000, Growth $15 for 15,000, Pro $39 for 60,000, Scale $99 for 250,000, and Business $249 for 1,000,000. Yearly billing gives two months free. Sign up free for 1,000 screenshots a month with no card.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.