Skip to content
Tools Without AI

Prefer toolswithoutai.com in Google Search so our tools and guides show up more often. Google has to confirm it — we cannot set this for you.

Browser engineering · · updated

HTML5 Canvas image processing without leaving the tab

On-device image compression is a decode, resample, encode loop. The File API yields a Blob, createImageBitmap produces a bitmap, Canvas 2D draws a scaled copy, and canvas.toBlob emits JPEG or WebP. No pixels leave the tab. Cloud compressors exist only because this pipeline is not obvious. You can run it in the image compressor or hit a byte budget with compress image to size.

How does decode-to-bitmap work in the browser?

The File object is a Blob with a MIME type. createImageBitmap(file) is the modern decode path: supporting engines can honor EXIF orientation and resize at decode time via resizeWidth and resizeHeight, which avoids an HTMLImageElement in the DOM. HTMLImageElement.decode() plus a blob URL is the fallback when ImageBitmap is missing or when you need element-based CSS. Color space matters: a Display P3 JPEG drawn onto an sRGB canvas is gamut-mapped by the engine, not by your quality slider, so “visually the same” is not bit-identical.

drawImage resampling is not Photoshop bicubic. Chrome, Safari, and Firefox use different filters. imageSmoothingEnabled and imageSmoothingQuality ('low' | 'medium' | 'high') change the kernel but not a documented cubic. If you need fewer aliases, downsample in steps (halve until near the target) rather than a single 8000→800 blit. The image resizer is the product form of that blit: you choose an edge, the canvas does the rest.

HEIC and some CMYK JPEGs fail decode in parts of the stack. Feature-detect and surface a local error. Do not silently upload “for conversion.” A format converter that stays on-canvas is still this pipeline with a different toBlob type; see image format converter.

What happens in the Canvas 2D raster?

The backing store is premultiplied alpha. PNG with translucent edges drawn onto an opaque JPEG export will darken fringes. Fill the canvas with white (or a chosen matte) before drawImage when the output format has no alpha. Maximum canvas dimensions are engine-specific; Safari on iOS is the usual cliff. When createImageBitmap fails, the file may be too large, truncated, or in a color mode the decoder rejects.

GPU acceleration is not guaranteed. A huge canvas can fall back to CPU and stall input handlers. OffscreenCanvas in a dedicated worker keeps the page clickable: transfer the File, decode, draw, convertToBlob, transfer the Blob back. Do not structured-clone a 40 MB ImageData if you can transfer an ArrayBuffer. That worker pattern is the same isolation idea as chunked hashing — keep the UI thread for clicks, not pixels.

How is JPEG, WebP, or PNG emitted locally?

canvas.toBlob(callback, type, quality) encodes with the browser’s encoder. Quality 0–1 is not comparable across Chrome and Safari. PNG ignores quality and can grow after a lossy source because it is still a filter+deflate of the raster you drew. WebP often beats JPEG at the same visual error for photographs. There is no progressive JPEG control and no MozJPEG trellis in the Canvas API.

Use the image compressor when you only need a quality slider. Use compress image to size when the constraint is kilobytes (email, Open Graph). Chain with resize image when the constraint is pixels. All three stay in RAM; none need a second website.

const bmp = await createImageBitmap(file, { resizeWidth: 1600, resizeQuality: "high" });
const canvas = new OffscreenCanvas(bmp.width, bmp.height);
canvas.getContext("2d").drawImage(bmp, 0, 0);
const blob = await canvas.convertToBlob({ type: "image/webp", quality: 0.82 });

How do you hit a target byte size?

Binary-search quality (for example 0.4–0.95), encode, read Blob.size, narrow the range. If the floor quality is still too large, reduce width/height and search again. Stop at a minimum edge so you do not emit a 16-pixel thumbnail by accident. This is still local: the loop never uses fetch except for the JS bundle. Prove it with the Network-tab procedure.

Target-size loops allocate many intermediate Blobs. Let them go out of scope; do not keep an array of every probe. On phones, cap attempts. The File you started from can be retained for a “reset”; the probes should not.

What should you strip besides pixels?

Re-encode drops GPS unless you copy APP1. That is usually what you want for social posts. Thumbnail IFDs, camera serials, and XMP in PNG tEXt are separate. The EXIF article walks TIFF-in-JPEG. If you must keep pixels lossless and only drop tags, prefer a stripper over a JPEG quality pass.

Large camera dumps are Blob problems before they are canvas problems. Blob.slice lets you hash or peek without decoding 50 MP twice. Decode once at a bounded size, then encode.

How does this relate to the rest of the stack?

Canvas is raster. PDF page trees are vector-ish object graphs — do not “compress a PDF” by screenshotting pages unless you intend a raster PDF. Image-in-PDF tools still use this decode path per page image. Privacy is the same as every other file tool: no upload, which is data minimization implemented as architecture.

Frequently asked questions

How does a browser compress an image without uploading it?

It decodes the file locally, draws a possibly smaller bitmap onto a canvas, and encodes a new Blob with toBlob or OffscreenCanvas.convertToBlob. Confirm with a Network-tab audit: the original File must not appear as a request body.

Does re-encoding on canvas strip EXIF GPS?

Usually yes. JPEG APP1 and HEIC metadata boxes are not copied by drawImage. Apply orientation before encode. Details are in the EXIF metadata guide. Use remove EXIF when you need a dedicated strip without a quality slider.

Why does a 12 MP photo freeze the page?

Decode and drawImage often run on the main thread unless you use a worker plus OffscreenCanvas. Engines also cap canvas dimensions (often around 16k pixels on a side). Blob slicing does not shrink pixel count; you still must downsample.

How do you hit an exact byte size?

Binary-search JPEG/WebP quality, then reduce edge length if quality floors out. After each toBlob, inspect Blob.size. That is what compress image to size does. Do not POST the image to a compress API.

Is browser JPEG the same as MozJPEG?

No. canvas.toBlob uses the engine encoder. Quality 0.8 in Chrome is not quality 80 in MozJPEG. Compare outputs locally; do not assume a CDN recompress is identical.