Saving Output

Saving Canvas Output to Files and Servers

canvas.toBlob(callback, type, quality) encodes asynchronously; prefer it to toDataURL(), which blocks and returns Base64 a third larger. PNG is the default, quality (0 to 1) applies to JPEG and WebP, and an unsupported type falls back to PNG, so check blob.type.

One cover encoded four ways, and a download linkHTML
<canvas id="c" style="width:64px; height:86px; float:left; margin-right:12px"></canvas>
<script src="hidpi.js"></script><pre id="out" style="margin:0"></pre>
<script>
  const ctx = hidpi(c), encode = (type, q) => new Promise(done => c.toBlob(done, type, q));
  ctx.fillStyle = '#2a9d8f';  ctx.fillRect(0, 0, 64, 86);
  ctx.fillStyle = '#fff3e0';  ctx.font = 'bold 12px Georgia';  ctx.fillText('Gardens', 6, 20);
  (async () => {
    for (const [type, q] of [['image/png'], ['image/jpeg', 0.9], ['image/jpeg', 0.5],
                             ['image/webp', 0.8]])
      out.textContent += `${type} ${q ?? ''} -> ${(await encode(type, q)).size} bytes\n`;
    const link = Object.assign(document.body.appendChild(document.createElement('a')), {
      download: 'gardens-cover.png', textContent: 'Download gardens-cover.png' });
    link.href = URL.createObjectURL(await encode('image/png'));  __done = true;
  })();
</script>
Browser output of Listing 2.19
Browser output of 19

To upload instead, put the blob in a FormData (form.append('cover', blob, 'cover.png')) and fetch() it with method: 'POST': the server receives an ordinary file upload, with no Base64 to strip.

Encoding a cover with toBlob as PNG, JPEG and WebP and comparing the file sizesHTMLLive
<!doctype html>
<style>
  body { margin: 0; padding: 8px; font: 13px system-ui, sans-serif; background: #fafaf7; max-width: 600px; }
  .row { display: flex; gap: 12px; align-items: flex-start; }
  #cover { width: 120px; height: 170px; }
  #chart { width: 460px; height: 170px; background: #fff; border: 1px solid #ddd; }
</style>
<div class="row"><canvas id="cover"></canvas><canvas id="chart"></canvas></div>
<p><a id="link">Preparing download…</a></p>
<script>
  function hidpi(c, r = devicePixelRatio) {
    c.width = c.clientWidth * r;  c.height = c.clientHeight * r;
    const ctx = c.getContext('2d');  ctx.scale(r, r);  return ctx;
  }
  const c = document.getElementById('cover');
  const ctx = hidpi(c);
  // A small cover to encode, with a gradient so the lossy formats have work to do
  const g = ctx.createLinearGradient(0, 0, 120, 170);
  g.addColorStop(0, '#2a9d8f');  g.addColorStop(1, '#264653');
  ctx.fillStyle = g;  ctx.fillRect(0, 0, 120, 170);
  ctx.fillStyle = '#fff3e0';  ctx.font = 'bold 18px Georgia';
  ctx.fillText('Gardens', 10, 34);  ctx.fillText('in Glass', 10, 56);
  for (let i = 0; i < 6; i++) {
    ctx.fillStyle = `hsl(${140 + i * 12} 50% ${40 + i * 6}%)`;
    ctx.beginPath();  ctx.ellipse(30 + i * 13, 140 - (i % 2) * 18, 9, 22, 0.3, 0, 7);  ctx.fill();
  }

  // toBlob is asynchronous: wrap it in a promise
  const encode = (type, q) => new Promise(done => c.toBlob(done, type, q));
  const chart = hidpi(document.getElementById('chart'));

  (async () => {
    const formats = [['image/png'], ['image/jpeg', 0.9], ['image/jpeg', 0.5], ['image/webp', 0.8]];
    const results = [];
    for (const [type, q] of formats) {
      const blob = await encode(type, q);
      // An unsupported type falls back to PNG, so report blob.type, not the request
      results.push({ label: `${blob.type} ${q ?? ''}`, size: blob.size });
    }
    const max = Math.max(...results.map(r => r.size));
    chart.font = '12px sans-serif';
    chart.fillStyle = '#263238';  chart.fillText('Encoded size (bytes)', 10, 18);
    results.forEach((r, i) => {
      const y = 34 + i * 33, w = r.size / max * 250;
      chart.fillStyle = '#263238';  chart.fillText(r.label, 10, y + 16);
      chart.fillStyle = ['#1f5f8b', '#e09a10', '#f2c46d', '#3f7d3a'][i];
      chart.fillRect(130, y, w, 22);
      chart.fillStyle = '#263238';  chart.fillText(r.size.toLocaleString(), 136 + w, y + 16);
    });
    // A download link for the PNG, from an object URL rather than a Base64 data URL
    const link = document.getElementById('link');
    link.href = URL.createObjectURL(await encode('image/png'));
    link.download = 'gardens-cover.png';
    link.textContent = 'Download gardens-cover.png';
  })();
</script>