<!-- Markdown version of https://ocxly.com/build-browser-image-resizer.html · auto-generated, may lag the live page -->

# 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](image-resizer.html) 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:

```
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](#ref-1)

```
<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](#ref-2)

```
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](#ref-3)

```
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](#ref-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:

```
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](#ref-5)

```
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](#ref-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](#ref-7) One JPEG caveat: it has no alpha channel, so paint a solid background before drawing if your source might be transparent:

```
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](#ref-8)

```
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](#ref-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](#ref-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](#ref-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](tools.html) 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)](https://developer.mozilla.org/en-US/docs/Web/API/File_API/Using_files_from_web_applications)
2. [MDN Web Docs — URL.createObjectURL() (blob: object URLs)](https://developer.mozilla.org/en-US/docs/Web/API/URL/createObjectURL_static)
3. [MDN Web Docs — CanvasRenderingContext2D.drawImage() (the scaling nine/five-argument forms)](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/drawImage)
4. [MDN Web Docs — CanvasRenderingContext2D.imageSmoothingQuality](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/imageSmoothingQuality)
5. [MDN Web Docs — HTMLCanvasElement.toBlob() (type and quality arguments)](https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/toBlob)
6. [WHATWG HTML Standard — canvas element: toBlob and serialising bitmaps (quality applies only to lossy types)](https://html.spec.whatwg.org/multipage/canvas.html#dom-canvas-toblob)
7. [web.dev (Google) — Use WebP images (typical size savings vs JPEG/PNG)](https://web.dev/articles/serve-images-webp)
8. [MDN Web Docs — The a element: the download attribute](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/a#download)
9. [MDN Web Docs — URL.revokeObjectURL() (releasing object URLs)](https://developer.mozilla.org/en-US/docs/Web/API/URL/revokeObjectURL_static)
10. [MDN Web Docs — createImageBitmap() (imageOrientation: "from-image" for EXIF rotation)](https://developer.mozilla.org/en-US/docs/Web/API/Window/createImageBitmap)
11. [MDN Web Docs — Allowing cross-origin use of images and canvas (tainted canvases)](https://developer.mozilla.org/en-US/docs/Web/HTML/How_to/CORS_enabled_image)

---
*Source: [ocxly.com/build-browser-image-resizer.html](https://ocxly.com/build-browser-image-resizer.html) — OCXLY, free 100% client-side privacy-first tools. A step-by-step tutorial on building a client-side image resizer with the Canvas API: read a file with no upload, draw it smaller with drawImage, export with toBlob, and handle format, quality, aspect ratio and EXIF orientation. Cited to MDN and the HTML standard.*
