DEV3 Technologies home

Zip generated files in the browser

Resize an image into several PNG sizes on a canvas and bundle them into one downloadable zip with JSZip.

React / Next.jsUtilitieszipjszipcanvasdownload

Code

import JSZip from 'jszip';

// Draws the image centred inside a square canvas without distortion.
async function renderPng(img: ImageBitmap, size: number): Promise<Blob> {
  const canvas = document.createElement('canvas');
  canvas.width = canvas.height = size;
  const ctx = canvas.getContext('2d');
  if (!ctx) throw new Error('Canvas not supported');
  const scale = Math.min(size / img.width, size / img.height);
  const w = img.width * scale;
  const h = img.height * scale;
  ctx.imageSmoothingQuality = 'high';
  ctx.drawImage(img, (size - w) / 2, (size - h) / 2, w, h);
  return new Promise((resolve, reject) =>
    canvas.toBlob((b) => (b ? resolve(b) : reject(new Error('PNG export failed'))), 'image/png'),
  );
}

export function downloadBlob(blob: Blob, filename: string) {
  const url = URL.createObjectURL(blob);
  const a = document.createElement('a');
  a.href = url;
  a.download = filename;
  document.body.appendChild(a);
  a.click();
  a.remove();
  setTimeout(() => URL.revokeObjectURL(url), 0); // revoke after the click has been handled
}

export async function zipIcons(img: ImageBitmap, sizes: number[], filename = 'icons.zip') {
  const zip = new JSZip();
  for (const size of sizes) {
    zip.file(`icon-${size}.png`, await renderPng(img, size)); // one at a time keeps memory low
  }
  zip.file('manifest.txt', sizes.map((s) => `icon-${s}.png ${s}x${s}`).join('\n'));
  const blob = await zip.generateAsync({ type: 'blob', compression: 'DEFLATE' });
  downloadBlob(blob, filename);
  return blob.size;
}

Needs: jszip@^3.10.1

How it works

What it solves

Tools that generate many assets, such as icon sets, thumbnails or exports, are annoying if the user must save each file by hand. Packaging them into a single zip in the browser avoids a server and keeps private images on the device.

How it works

renderPng draws the source image onto a square canvas of a given size, using contain scaling so non-square art is letterboxed instead of stretched, and returns a PNG blob. zipIcons loops over the requested sizes, renders each, and adds it to a JSZip instance with a predictable name such as icon-32.png. Rendering runs one size at a time to keep memory modest.

A text manifest is added as a plain string so the same helper shows both blob and text entries. generateAsync({ type: 'blob', compression: 'DEFLATE' }) produces the archive; PNGs are already compressed, so deflate saves little there but helps for text and SVG. downloadBlob creates an object URL, clicks a temporary anchor and revokes the URL on the next tick so the download is not cancelled.

Adapting it

  • Add apple-touch-icon.png at 180 px by including 180 in the sizes list.
  • Call JSZip's folder('icons') to nest files.
  • Pass an onProgress callback from generateAsync for large archives.

Pitfalls

  • Revoking the object URL immediately can abort the save in some browsers.
  • Sizes below 16 px lose detail; start from a vector or large source.
  • Zip file names are case-sensitive on some systems; keep them lowercase.

How to use it

  1. Run npm i jszip.
  2. Save as lib/zipIcons.ts; use it only in client code.
  3. const bmp = await createImageBitmap(file); await zipIcons(bmp, [16, 32, 48, 180]).
  4. The browser downloads icons.zip containing the PNGs and a small manifest.

Licence: DEV3 Snippet Licence. Free to use, change and ship in personal and commercial projects. Attribution is appreciated, not required. Please do not redistribute snippets as a collection.

Related snippets