DEV Community

Cover image for 9 Browser-Based Image Tools, One Architecture: Canvas API, WebAssembly, and Zero Uploads
Shaishav Patel
Shaishav Patel

Posted on

9 Browser-Based Image Tools, One Architecture: Canvas API, WebAssembly, and Zero Uploads

9 Browser-Based Image Tools, One Architecture: Canvas API, WebAssembly, and Zero Uploads

I built 9 image tools for UltimateTools — compress, convert, resize, crop, rotate, blur, watermark, HTML-to-image, and YouTube thumbnail downloader. Every tool runs entirely in the browser. No server processing, no file uploads, no temporary storage.

Here's the architecture that makes this work, and the specific browser APIs behind each tool.


The shared foundation

Every image tool follows the same pipeline:

File input (drag-and-drop or file picker)
        ↓
Load into HTMLImageElement or Canvas
        ↓
Transform (tool-specific logic)
        ↓
Export as Blob (toBlob / toDataURL)
        ↓
Download (createObjectURL → anchor click)
Enter fullscreen mode Exit fullscreen mode

The loading and downloading code is shared. What changes per tool is the transform step.

Loading images

const loadImage = (file: File): Promise<HTMLImageElement> => {
  return new Promise((resolve, reject) => {
    const img = new Image();
    img.onload = () => resolve(img);
    img.onerror = reject;
    img.src = URL.createObjectURL(file);
  });
};
Enter fullscreen mode Exit fullscreen mode

URL.createObjectURL is faster than FileReader.readAsDataURL for large files — it creates a reference to the file in memory without base64-encoding it.

Downloading results

const downloadBlob = (blob: Blob, filename: string) => {
  const url = URL.createObjectURL(blob);
  const a = document.createElement('a');
  a.href = url;
  a.download = filename;
  a.click();
  URL.revokeObjectURL(url);
};
Enter fullscreen mode Exit fullscreen mode

Every tool uses this pattern. No server round-trip.


Tool 1: Image Compressor

API: canvas.toBlob(callback, mimeType, quality)

The quality parameter (0 to 1) controls JPEG compression. At 0.8, you typically get 60–80% size reduction with no visible quality loss.

const compress = async (file: File, quality: number): Promise<Blob> => {
  const img = await loadImage(file);
  const canvas = document.createElement('canvas');
  canvas.width = img.naturalWidth;
  canvas.height = img.naturalHeight;
  canvas.getContext('2d')!.drawImage(img, 0, 0);

  return new Promise(resolve => {
    canvas.toBlob(blob => resolve(blob!), 'image/jpeg', quality);
  });
};
Enter fullscreen mode Exit fullscreen mode

For PNG, the quality parameter is ignored — PNG is lossless. But re-encoding through Canvas still strips unnecessary metadata, giving 20–40% reduction on typical screenshots.

Batch processing (up to 20 images) runs sequentially to avoid memory spikes. ZIP download uses JSZip.


Tool 2: Image Converter

API: Same toBlob — just change the MIME type.

const convert = async (file: File, targetFormat: string): Promise<Blob> => {
  const img = await loadImage(file);
  const canvas = document.createElement('canvas');
  canvas.width = img.naturalWidth;
  canvas.height = img.naturalHeight;
  canvas.getContext('2d')!.drawImage(img, 0, 0);

  const mimeType = `image/${targetFormat}`; // jpeg, png, or webp
  return new Promise(resolve => {
    canvas.toBlob(blob => resolve(blob!), mimeType, 0.92);
  });
};
Enter fullscreen mode Exit fullscreen mode

Supported conversions: JPG ↔ PNG ↔ WEBP. The Canvas API handles input format detection automatically — you just change the output MIME type.

One gotcha: converting a transparent PNG to JPEG fills the alpha channel with black. The fix is to draw a white rectangle before drawing the image:

ctx.fillStyle = '#ffffff';
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.drawImage(img, 0, 0);
Enter fullscreen mode Exit fullscreen mode

Tool 3: Image Resizer

API: Canvas dimensions + drawImage scaling.

const resize = async (
  file: File,
  targetWidth: number,
  targetHeight: number
): Promise<Blob> => {
  const img = await loadImage(file);
  const canvas = document.createElement('canvas');
  canvas.width = targetWidth;
  canvas.height = targetHeight;
  canvas.getContext('2d')!.drawImage(img, 0, 0, targetWidth, targetHeight);

  return new Promise(resolve => {
    canvas.toBlob(blob => resolve(blob!), file.type, 0.92);
  });
};
Enter fullscreen mode Exit fullscreen mode

The user picks width/height or a percentage. Aspect ratio lock is a UI toggle — when enabled, changing width auto-calculates height:

const newHeight = Math.round(targetWidth / aspectRatio);
Enter fullscreen mode Exit fullscreen mode

Tool 4: Image Crop

API: drawImage with source rectangle parameters.

drawImage has an overload that takes source coordinates — this is the crop:

ctx.drawImage(
  img,
  sx, sy, sWidth, sHeight,  // source rectangle (crop area)
  0, 0, sWidth, sHeight     // destination (full canvas)
);
Enter fullscreen mode Exit fullscreen mode

The crop selection UI uses a draggable overlay with resize handles. The user drags to select an area, and the coordinates map directly to the drawImage source parameters.

Aspect ratio presets (1:1, 16:9, 4:3, 3:2) constrain the selection rectangle.


Tool 5: Rotate Image

API: Canvas 2D transforms — translate + rotate.

const rotateImage = async (file: File, degrees: number): Promise<Blob> => {
  const img = await loadImage(file);
  const radians = (degrees * Math.PI) / 180;

  // Swap dimensions for 90/270 degree rotations
  const swap = degrees === 90 || degrees === 270;
  const canvas = document.createElement('canvas');
  canvas.width = swap ? img.naturalHeight : img.naturalWidth;
  canvas.height = swap ? img.naturalWidth : img.naturalHeight;

  const ctx = canvas.getContext('2d')!;
  ctx.translate(canvas.width / 2, canvas.height / 2);
  ctx.rotate(radians);
  ctx.drawImage(img, -img.naturalWidth / 2, -img.naturalHeight / 2);

  return new Promise(resolve => {
    canvas.toBlob(blob => resolve(blob!), file.type, 0.92);
  });
};
Enter fullscreen mode Exit fullscreen mode

The dimension swap for 90° and 270° rotations is easy to forget — a 1920x1080 image rotated 90° becomes 1080x1920. Without swapping, the image gets clipped.


Tool 6: Blur Image

API: Canvas filter property.

const blurImage = async (file: File, radius: number): Promise<Blob> => {
  const img = await loadImage(file);
  const canvas = document.createElement('canvas');
  canvas.width = img.naturalWidth;
  canvas.height = img.naturalHeight;

  const ctx = canvas.getContext('2d')!;
  ctx.filter = `blur(${radius}px)`;
  ctx.drawImage(img, 0, 0);

  return new Promise(resolve => {
    canvas.toBlob(blob => resolve(blob!), file.type, 0.92);
  });
};
Enter fullscreen mode Exit fullscreen mode

The filter property on Canvas2D works like CSS filters. The blur radius is adjustable via a slider. One line of code for the actual blur — the rest is UI.


Tool 7: Watermark Image

API: Canvas text rendering — fillText with transforms.

const addWatermark = (ctx: CanvasRenderingContext2D, text: string, options: WatermarkOptions) => {
  ctx.save();
  ctx.globalAlpha = options.opacity;
  ctx.fillStyle = options.color;
  ctx.font = `${options.fontSize}px ${options.fontFamily}`;
  ctx.translate(options.x, options.y);
  ctx.rotate((options.rotation * Math.PI) / 180);
  ctx.fillText(text, 0, 0);
  ctx.restore();
};
Enter fullscreen mode Exit fullscreen mode

The tool supports:

  • Custom text, font size, color, and opacity
  • Position (center, corners, or drag to place)
  • Rotation angle
  • Tiled/repeated watermark mode (draws the text in a grid pattern)

Tiled mode uses nested loops to fill the canvas:

for (let y = -canvas.height; y < canvas.height * 2; y += spacing) {
  for (let x = -canvas.width; x < canvas.width * 2; x += spacing) {
    addWatermark(ctx, text, { ...options, x, y });
  }
}
Enter fullscreen mode Exit fullscreen mode

The negative start and 2x overshoot ensures coverage even when the text is rotated.


Tool 8: HTML to Image

API: html2canvas library.

This one can't use the native Canvas API alone — rendering arbitrary HTML requires a full layout engine. html2canvas re-implements CSS rendering on a Canvas element:

import html2canvas from 'html2canvas';

const captureHtml = async (element: HTMLElement): Promise<Blob> => {
  const canvas = await html2canvas(element, {
    scale: 2,          // 2x for retina quality
    useCORS: true,     // handle cross-origin images
    backgroundColor: null, // transparent background
  });

  return new Promise(resolve => {
    canvas.toBlob(blob => resolve(blob!), 'image/png');
  });
};
Enter fullscreen mode Exit fullscreen mode

The user types or pastes HTML/CSS in an editor, sees a live preview, and exports it as PNG or JPEG. Useful for social media cards, code snippets, or styled text.


Tool 9: YouTube Thumbnail Downloader

API: YouTube's predictable thumbnail URL pattern.

This is the only tool that doesn't use Canvas at all. YouTube stores thumbnails at known URLs:

const getThumbnailUrls = (videoId: string) => ({
  maxres: `https://img.youtube.com/vi/${videoId}/maxresdefault.jpg`,
  hq: `https://img.youtube.com/vi/${videoId}/hqdefault.jpg`,
  mq: `https://img.youtube.com/vi/${videoId}/mqdefault.jpg`,
  sd: `https://img.youtube.com/vi/${videoId}/sddefault.jpg`,
});
Enter fullscreen mode Exit fullscreen mode

The video ID is extracted from the URL using a regex that handles all YouTube URL formats — youtube.com/watch?v=, youtu.be/, youtube.com/embed/, etc.

The download uses a server-side proxy route because YouTube images don't set CORS headers — you can't fetch them directly from the browser and trigger a download. The proxy fetches the image and returns it with the correct Content-Disposition header.


Why client-side matters

Every image tool that processes files (8 out of 9) runs entirely in the browser:

  • Privacy: Files never leave the device. No server logs, no temporary storage.
  • Speed: No upload/download latency. A 10MB image compresses in under a second.
  • Cost: No server compute costs for image processing.
  • Offline: Once loaded, the tools work without internet (except YouTube thumbnails).

The trade-off is that heavy processing (like advanced PNG optimization with pngquant) requires server-side or WebAssembly. For most use cases — compress, convert, resize, crop, rotate, blur, watermark — the Canvas API is more than enough.


The full suite

All 9 tools are live at ultimatetools.io/tools/image-tools/. Everything is free, no account required.

The same architecture pattern (load → Canvas → transform → download) powers all of them. If you're building browser-based tools, the Canvas API is remarkably capable — most image operations are 5-10 lines of actual processing code wrapped in UI.

Questions about any of these implementations? Drop them in the comments.

Top comments (0)