DEV3 Technologies home

Crop and resize an image on a canvas

Pure helpers to centre-crop an image to any aspect ratio and export it at exact pixel dimensions using a canvas.

React / Next.jsUtilitiescanvasimagecropresize

Code

export type Rect = { x: number; y: number; w: number; h: number };

/**
 * Largest rectangle inside the source that has the target aspect ratio
 * (like CSS object-fit: cover). focus is 0..1 per axis, 0.5 = centred.
 */
export function coverRect(
  srcW: number,
  srcH: number,
  outW: number,
  outH: number,
  focus: { x?: number; y?: number } = {},
): Rect {
  const target = outW / outH;
  const source = srcW / srcH;
  let w = srcW;
  let h = srcH;
  if (source > target) w = srcH * target; // source is too wide: trim the sides
  else h = srcW / target; // source is too tall: trim top and bottom
  const x = (srcW - w) * (focus.x ?? 0.5);
  const y = (srcH - h) * (focus.y ?? 0.5);
  return { x, y, w, h };
}

export async function renderCrop(
  image: CanvasImageSource,
  rect: Rect,
  outW: number,
  outH: number,
  type: 'image/png' | 'image/jpeg' | 'image/webp' = 'image/png',
  quality = 0.92,
): Promise<Blob> {
  const canvas = document.createElement('canvas');
  canvas.width = Math.round(outW);
  canvas.height = Math.round(outH);
  const ctx = canvas.getContext('2d');
  if (!ctx) throw new Error('Canvas not supported');

  // Round and clamp so we never sample outside the source bitmap.
  const sw = (image as { width: number }).width;
  const sh = (image as { height: number }).height;
  const sx = Math.max(0, Math.round(rect.x));
  const sy = Math.max(0, Math.round(rect.y));
  const cw = Math.min(sw - sx, Math.round(rect.w));
  const ch = Math.min(sh - sy, Math.round(rect.h));

  ctx.imageSmoothingQuality = 'high';
  ctx.drawImage(image, sx, sy, cw, ch, 0, 0, canvas.width, canvas.height);

  return new Promise((resolve, reject) =>
    canvas.toBlob((b) => (b ? resolve(b) : reject(new Error('Export failed'))), type, quality),
  );
}

How it works

What it solves

Profile pictures, banners and social cards all need exact dimensions, and users upload whatever their camera produced. Squashing the picture to fit distorts it. This pair of helpers separates the maths (which part of the source to keep) from the drawing, so a crop UI, a preset button or a batch job can all share them.

How it works

coverRect implements "object-fit: cover" for a source of any shape: it compares the source and target aspect ratios and returns the largest rectangle with the target ratio, positioned by a focus point (0 to 1 on each axis, default centre). Because it returns plain numbers it is trivial to unit test.

renderCrop takes any CanvasImageSource, a source rectangle and the output size, then uses the nine-argument form of drawImage to crop and scale in a single pass. Rounding the rectangle and clamping it inside the source avoids one-pixel transparent edges. High quality smoothing is requested, and the result is returned as a blob with a chosen type.

Adapting it

  • Feed renderCrop a rectangle from a drag-to-crop overlay instead of coverRect.
  • Pass a focus point from face detection or a user click for smarter framing.
  • Multiply output dimensions by devicePixelRatio for retina previews.

Pitfalls

  • Downscaling by more than half in one step looks jagged; resize in two steps for large reductions.
  • Remote images taint the canvas unless served with CORS headers, which makes toBlob throw.
  • Output size is exact, so check it does not exceed browser canvas limits (about 16k pixels per side).

How to use it

  1. Save as lib/cropResize.ts (no dependencies).
  2. Decode the file with createImageBitmap(file).
  3. const rect = coverRect(bmp.width, bmp.height, 1200, 630) then await renderCrop(bmp, rect, 1200, 630).
  4. Pass { x: 0.5, y: 0.2 } as the focus to bias the crop toward the top.

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