For PNG output, call canvas.toBlob(callback, 'image/png'). The API returns undefined; the callback receives the encoded Blob or null if an image cannot be created. A canvas containing disallowed cross-origin pixels can instead throw a SecurityError. The MDN toBlob reference documents these separate failure paths.
Prepare a small export with known content
This original fixture draws a dispatch count at a fixed bitmap size. It uses no external image or font, so the first export can be diagnosed without unrelated asset permissions.
<canvas id="dispatch" width="800" height="400"
aria-label="Dispatch count: 24 crates"></canvas>
<button id="export" type="button">Prepare PNG</button>
<a id="download" hidden>Download dispatch PNG</a>
<p id="export-status" role="status"></p>
const canvas = document.querySelector('#dispatch');
const context = canvas.getContext('2d');
context.fillStyle = '#ffffff';
context.fillRect(0, 0, canvas.width, canvas.height);
context.fillStyle = '#154e46';
context.fillRect(60, 120, 480, 110);
context.font = '32px sans-serif';
context.fillText('Dispatch: 24 crates', 60, 80);
The white rectangle becomes part of the exported image. A white CSS background on the <canvas> element would affect its display box without painting white pixels into a PNG.
Keep the count in accessible page text as well when using this in an actual report. A PNG preserves the drawing, not a table of editable shipment records.
Offer the file after encoding completes
Run this code after the drawing code, in the same script:
const button = document.querySelector('#export');
const link = document.querySelector('#download');
const status = document.querySelector('#export-status');
let currentURL;
button.addEventListener('click', () => {
button.disabled = true;
link.hidden = true;
status.textContent = 'Encoding PNG...';
if (currentURL) {
URL.revokeObjectURL(currentURL);
currentURL = undefined;
link.removeAttribute('href');
}
try {
canvas.toBlob(blob => {
button.disabled = false;
if (!blob) {
status.textContent = 'No image was created. Try again.';
return;
}
if (blob.type !== 'image/png') {
status.textContent = `Unexpected image type: ${blob.type}`;
return;
}
currentURL = URL.createObjectURL(blob);
link.href = currentURL;
link.download = 'dispatch-count.png';
link.hidden = false;
status.textContent = `PNG ready: ${blob.size} bytes.`;
}, 'image/png');
} catch (error) {
button.disabled = false;
status.textContent = `Export failed: ${error.name}`;
}
});
The second click replaces the old export and releases its object URL. The download stays available until then, so the user can choose when to save. This avoids assuming a synthetic download triggered from an asynchronous callback will be accepted in every embedding environment.
Changing the canvas later does not update an already-created Blob. Press Prepare PNG again after changing the count. For a larger application, invalidate the old download whenever the underlying drawing changes, so a visible link cannot quietly offer an earlier chart.
Check the file's dimensions and format
For this fixture, the expected PNG dimensions are 800 by 400 pixels, even if CSS displays the canvas at a smaller width. The canvas bitmap dimensions determine the export; the browser's layout dimensions do not.
Save the file, open it in an image viewer, and inspect its properties and the words "Dispatch: 24 crates." The byte count varies with drawing and encoding, so it is not a fixed expected number. These are checks to run with the fixture, not claimed results from an executed download.
For JPEG or WebP, the optional quality argument applies only to formats supporting lossy compression. Requesting an unsupported type falls back to PNG, so inspect blob.type before choosing the extension. Renaming a PNG to .jpg does not convert its contents.
If the labels look blurry, fix the drawing's bitmap size before exporting. The canvas drawing guide explains the difference between bitmap and CSS size. Enlarging an already-painted bitmap does not restore missing detail.
Diagnose a cross-origin failure at the image source
A logo can display in an <img> and still make the canvas unreadable after drawImage(). An image from another origin needs the appropriate CORS permission before its pixels can participate in an exportable canvas. Set crossOrigin = 'anonymous' before setting the image's src, and configure the image server to permit the request. The client attribute alone cannot make the server authorize access.
If unauthorized pixels have already been drawn, catching the error only explains the failure. Rebuild the drawing from permitted assets. MDN's tainted canvas guidance covers that origin-clean requirement.
A blocked download after a valid Blob is a different issue: inspect the frame's download permissions and the link activation. The Blob URL guide covers address lifetime and download restrictions. For an editable vector chart, use the canvas-to-SVG choices instead of treating a PNG export as recovered geometry.
Finish by opening the saved image beside the current dispatch record. Confirm the count and label match before inserting the image into the shift report.