OcxlyDev · Tutorial

Build a browser image resizer — no server, no upload

You can resize an image entirely in the browser: the file never leaves the user's device, there's no server to run, and it works offline. This tutorial walks through exactly how our Image Resizer tool is built, one primitive at a time, so you can build your own. The whole thing is a short pipeline of standard Web APIs — read a file, decode it, draw it smaller on a <canvas>, and hand the result back as a downloadable file.

The whole pipeline in one line

Every browser image tool — resize, compress, convert, crop — is the same four steps:

the pipeline
File  →  <img> / ImageBitmap  →  <canvas> (draw at new size)  →  Blob  →  download

Nothing here needs a library or a network request. Let's build each arrow.

1 · Read the file without uploading it

Give the user a file input (and, nicely, a drag‑and‑drop zone). The File object you get back is already the raw bytes in memory — reading it is a local operation, not an upload.1

html
<input type="file" id="file" accept="image/*" hidden>
<div id="drop">Drop an image, or click to choose</div>

To turn those bytes into something drawable, wrap the file in an object URL — a short blob: link to the in‑memory data — and load it into an Image. createObjectURL() is instant and never copies the file anywhere.2

javascript
const img = new Image();

function loadFile(file) {
  if (!file.type.startsWith("image/")) return;   // ignore non-images
  const url = URL.createObjectURL(file);          // blob: link to the bytes
  img.onload = () => {
    // img.naturalWidth / naturalHeight are now the real pixel size
    setupControls(img.naturalWidth, img.naturalHeight);
  };
  img.src = url;                                   // decode happens here
}

Wire it to both the input and the drop zone. A drop is just a drop event whose dataTransfer.files holds the same File objects the input would give you — so both paths call the one loadFile().

2 · Draw it at a new size on a canvas

The resize itself is a single call. Create a <canvas> at the target dimensions and let drawImage() paint the source image into it — the five‑argument form drawImage(image, dx, dy, dWidth, dHeight) scales as it draws.3

javascript
function resizeTo(width, height, type, quality) {
  const canvas = document.createElement("canvas");
  canvas.width = width;
  canvas.height = height;

  const ctx = canvas.getContext("2d");
  ctx.imageSmoothingEnabled = true;
  ctx.imageSmoothingQuality = "high";        // ask for the best resampling
  ctx.drawImage(img, 0, 0, width, height);   // scale into the canvas
  return canvas;
}

Two lines matter for quality. The browser interpolates when it scales, and imageSmoothingQuality = "high" requests its best resampling filter rather than a fast‑and‑blocky one.4 For large downscales, one clean step like this generally beats naïve pixel‑dropping; if you ever need extreme reductions, downscaling in halves is a known trick, but for typical photo work a single high‑quality draw is enough.

3 · Keep the aspect ratio

If the user edits the width, recompute the height from the original ratio (and vice‑versa) so nothing stretches. Store the ratio once when the image loads:

javascript
const ratio = img.naturalWidth / img.naturalHeight;

widthInput.addEventListener("input", () => {
  if (lockAspect.checked) {
    heightInput.value = Math.round(widthInput.value / ratio);
  }
});
// scale presets are just: w = round(naturalWidth * 0.5), etc.

That's the entire "lock aspect ratio" feature. Turning the lock off simply skips the recompute, letting the user squash or stretch on purpose.

4 · Export: format and quality

Read the canvas back out as a compressed file with toBlob(). It's asynchronous — you get the finished Blob in a callback — and it takes the MIME type and, for lossy formats, a quality between 0 and 1.5

javascript
canvas.toBlob(
  (blob) => { /* blob is the encoded image file */ },
  "image/webp",   // or "image/jpeg", "image/png"
  0.9             // quality 0–1 (ignored for PNG)
);

The quality argument only applies to lossy encoders — JPEG and WebP; it is meaningless for lossless PNG, which is why our tool hides the slider when PNG is selected.6 Format choice is real: WebP typically lands 25–35% smaller than JPEG at comparable quality, so it's the best default for photos on the web; keep PNG for logos, screenshots, and anything with transparency.7 One JPEG caveat — it has no alpha channel, so paint a solid background before drawing if your source might be transparent:

javascript
if (type === "image/jpeg") {
  ctx.fillStyle = "#fff";
  ctx.fillRect(0, 0, width, height);   // otherwise transparency turns black
}
ctx.drawImage(img, 0, 0, width, height);

5 · Hand back the download

A Blob becomes a saved file the same way the input became an image: wrap it in an object URL and click a hidden anchor carrying the download attribute, which names the file.8

javascript
canvas.toBlob((blob) => {
  const url = URL.createObjectURL(blob);
  const a = document.createElement("a");
  a.href = url;
  a.download = "resized-" + width + "x" + height + ".webp";
  a.click();
  setTimeout(() => URL.revokeObjectURL(url), 4000);   // free the memory
}, "image/webp", 0.9);

That revokeObjectURL() matters: object URLs pin their data in memory until you release them, so revoke every one you create once you're done with it.9 Do the same for the source URL when the user loads a new file.

The gotchas nobody mentions

EXIF orientation. Phone photos often carry an "orientation" flag instead of physically rotating the pixels. Plain drawImage ignores it, so a portrait shot can come out sideways. The fix is to decode with createImageBitmap(file, { imageOrientation: "from-image" }), which bakes the rotation in before you draw.10

Tainted canvases. If you ever draw an image loaded cross‑origin without permission, the canvas becomes "tainted" and toBlob throws a security error. Reading a user's own File (as here) is same‑origin data, so it's safe — but it's the first thing to check if export suddenly fails.11

Memory. Very large images are large canvases; free object URLs promptly and don't hold references to old canvases. Feature support: WebP encoding via toBlob is widely available now, but a defensive tool checks that blob isn't null and falls back to JPEG if a browser can't produce the requested type.

Why do it in the browser at all?

Because the alternative — upload, resize server‑side, download — costs you a server, bandwidth, and your users' trust. The client‑side version is private by construction (the bytes never leave the device), free to run, works offline, and is instant with no round trip. For resizing, compression, format conversion, cropping and watermarking, the canvas is all you need. Our whole tools collection is built on this principle: the useful work happens on your machine, and nothing is uploaded.

References

  1. MDN Web Docs — Using files from web applications (the File object, input and drag-and-drop)
  2. MDN Web Docs — URL.createObjectURL() (blob: object URLs)
  3. MDN Web Docs — CanvasRenderingContext2D.drawImage() (the scaling nine/five-argument forms)
  4. MDN Web Docs — CanvasRenderingContext2D.imageSmoothingQuality
  5. MDN Web Docs — HTMLCanvasElement.toBlob() (type and quality arguments)
  6. WHATWG HTML Standard — canvas element: toBlob and serialising bitmaps (quality applies only to lossy types)
  7. web.dev (Google) — Use WebP images (typical size savings vs JPEG/PNG)
  8. MDN Web Docs — The a element: the download attribute
  9. MDN Web Docs — URL.revokeObjectURL() (releasing object URLs)
  10. MDN Web Docs — createImageBitmap() (imageOrientation: "from-image" for EXIF rotation)
  11. MDN Web Docs — Allowing cross-origin use of images and canvas (tainted canvases)