Toolmingo
Guides8 min read

How to Resize an Image (JavaScript, Python, Go, PHP)

Learn how to resize images programmatically — proportional scaling, fixed dimensions, fit/fill/cover modes — with code examples in JavaScript, Python, Go, and PHP.

How to Resize an Image

Resizing is the most common image operation in web development — generating thumbnails, fitting images into fixed layout slots, producing responsive image variants, or reducing file size before upload. This guide explains the core resizing model, the three fundamental modes (fit, fill, cover), and working implementations in JavaScript, Python, Go, and PHP.


The Resizing Model

Resizing maps an image from its original W × H pixels to a new targetW × targetH. Two sub-problems arise immediately:

  1. What happens to the aspect ratio? Stretching to an arbitrary width and height distorts the image. Usually you want proportional scaling.
  2. What happens to leftover space (or excess content)? When the target ratio differs from the source ratio, you either pad with empty space or crop the overflow.

The three standard modes:

Mode Behaviour Result size
Fit (contain) Scale down until the whole image fits inside targetW × targetH. Letterbox padding fills the rest. Exactly targetW × targetH
Cover Scale up/down until the image fills targetW × targetH completely. Crop the overflow. Exactly targetW × targetH
Fill Stretch/squish to exactly targetW × targetH, ignoring the aspect ratio. Exactly targetW × targetH
Proportional (no crop/pad) Scale to fit within one dimension, keep the ratio, output size may differ from target. targetW and ≤ targetH

Proportional Scaling Formula

When you only specify one dimension (e.g. "make width 800 px, keep ratio"):

scale  = targetW / originalW
newH   = round(originalH × scale)

When you specify both and want to fit inside the box without cropping:

scaleW = targetW / originalW
scaleH = targetH / originalH
scale  = min(scaleW, scaleH)      # use the more constrained axis
newW   = round(originalW × scale)
newH   = round(originalH × scale)

For cover (fill and crop), use max instead of min.


JavaScript

Browser: Canvas API

/**
 * Resize an image using the Canvas API.
 * @param {HTMLImageElement} img - loaded <img> element
 * @param {number} targetW - target width in pixels
 * @param {number} targetH - target height in pixels (0 = auto from ratio)
 * @returns {Promise<Blob>} - resized image as JPEG Blob
 */
function resizeImage(img, targetW, targetH = 0) {
  const srcW = img.naturalWidth;
  const srcH = img.naturalHeight;

  // Proportional scaling when only width is given
  const scale = targetH === 0
    ? targetW / srcW
    : Math.min(targetW / srcW, targetH / srcH);

  const outW = Math.round(srcW * scale);
  const outH = Math.round(srcH * scale);

  const canvas = document.createElement("canvas");
  canvas.width  = outW;
  canvas.height = outH;

  const ctx = canvas.getContext("2d");
  ctx.drawImage(img, 0, 0, outW, outH);

  return new Promise((resolve) => canvas.toBlob(resolve, "image/jpeg", 0.9));
}

// Cover resize: fill 400×300, crop excess
function coverResize(img, targetW, targetH) {
  const scale = Math.max(targetW / img.naturalWidth, targetH / img.naturalHeight);
  const scaledW = Math.round(img.naturalWidth  * scale);
  const scaledH = Math.round(img.naturalHeight * scale);

  const canvas = document.createElement("canvas");
  canvas.width  = targetW;
  canvas.height = targetH;

  const ctx = canvas.getContext("2d");
  const offsetX = Math.round((scaledW - targetW) / 2);
  const offsetY = Math.round((scaledH - targetH) / 2);

  // Draw scaled image offset so the centre aligns with the canvas
  ctx.drawImage(img, -offsetX, -offsetY, scaledW, scaledH);

  return new Promise((resolve) => canvas.toBlob(resolve, "image/jpeg", 0.9));
}

Node.js: Sharp

Sharp is the standard image library for Node.js — it uses libvips and is very fast.

import sharp from "sharp";

// Proportional fit (no upscaling)
await sharp("input.jpg")
  .resize({ width: 800, withoutEnlargement: true })
  .toFile("resized.jpg");

// Fit inside 800×600 box (letterbox / contain)
await sharp("input.jpg")
  .resize(800, 600, { fit: "contain", background: { r: 255, g: 255, b: 255 } })
  .toFile("fit.jpg");

// Cover 400×300 (crop to fill)
await sharp("input.jpg")
  .resize(400, 300, { fit: "cover", position: "centre" })
  .toFile("cover.jpg");

// Exact dimensions (may distort)
await sharp("input.jpg")
  .resize(400, 300, { fit: "fill" })
  .toFile("fill.jpg");

Sharp's fit values map directly to the modes above: "contain", "cover", "fill", "inside", "outside".


Python

Pillow provides Image.resize() and the helper Image.thumbnail().

from PIL import Image, ImageOps

def resize_proportional(input_path: str, output_path: str, max_width: int) -> None:
    """Scale to max_width, preserve aspect ratio."""
    with Image.open(input_path) as img:
        w, h = img.size
        scale = max_width / w
        new_size = (max_width, round(h * scale))
        resized = img.resize(new_size, Image.LANCZOS)
        resized.save(output_path)

def resize_fit(input_path: str, output_path: str,
               target_w: int, target_h: int) -> None:
    """Fit inside target box, preserve ratio (no padding)."""
    with Image.open(input_path) as img:
        # thumbnail() shrinks in-place and never upscales
        img.thumbnail((target_w, target_h), Image.LANCZOS)
        img.save(output_path)

def resize_cover(input_path: str, output_path: str,
                 target_w: int, target_h: int) -> None:
    """Cover: scale + centre-crop to exact target size."""
    with Image.open(input_path) as img:
        resized = ImageOps.fit(img, (target_w, target_h), Image.LANCZOS)
        resized.save(output_path)

def resize_exact(input_path: str, output_path: str,
                 target_w: int, target_h: int) -> None:
    """Exact dimensions — may distort."""
    with Image.open(input_path) as img:
        resized = img.resize((target_w, target_h), Image.LANCZOS)
        resized.save(output_path)

Resampling filter: Image.LANCZOS (Lanczos3) gives the best quality for downscaling. For upscaling, Image.BICUBIC is a good balance of speed and quality.


Go

The standard library image and golang.org/x/image/draw packages handle resizing.

package main

import (
    "image"
    "image/jpeg"
    "image/png"
    "math"
    "os"
    "path/filepath"
    "strings"

    "golang.org/x/image/draw"
)

// ResizeProportional scales src so that width equals targetW, preserving ratio.
func ResizeProportional(src image.Image, targetW int) image.Image {
    b := src.Bounds()
    scale := float64(targetW) / float64(b.Dx())
    targetH := int(math.Round(float64(b.Dy()) * scale))

    dst := image.NewRGBA(image.Rect(0, 0, targetW, targetH))
    draw.BiLinear.Scale(dst, dst.Bounds(), src, b, draw.Over, nil)
    return dst
}

// ResizeFit scales src to fit inside targetW×targetH, preserving ratio.
func ResizeFit(src image.Image, targetW, targetH int) image.Image {
    b := src.Bounds()
    scaleW := float64(targetW) / float64(b.Dx())
    scaleH := float64(targetH) / float64(b.Dy())
    scale := math.Min(scaleW, scaleH)

    outW := int(math.Round(float64(b.Dx()) * scale))
    outH := int(math.Round(float64(b.Dy()) * scale))

    dst := image.NewRGBA(image.Rect(0, 0, outW, outH))
    draw.BiLinear.Scale(dst, dst.Bounds(), src, b, draw.Over, nil)
    return dst
}

// SaveImage writes an image.Image to a JPEG or PNG file.
func SaveImage(img image.Image, path string) error {
    f, err := os.Create(path)
    if err != nil {
        return err
    }
    defer f.Close()

    ext := strings.ToLower(filepath.Ext(path))
    if ext == ".jpg" || ext == ".jpeg" {
        return jpeg.Encode(f, img, &jpeg.Options{Quality: 90})
    }
    return png.Encode(f, img)
}

Interpolation: draw.BiLinear is fast and good enough for most cases. For higher quality use draw.CatmullRom (similar to Lanczos). The golang.org/x/image/draw package also provides draw.NearestNeighbor for pixel art.


PHP

PHP ships with GD for basic resizing; the Imagick extension wraps ImageMagick for production-quality results.

GD

<?php
/**
 * Resize an image proportionally to a maximum width.
 */
function resizeProportional(
    string $inputPath,
    string $outputPath,
    int $maxWidth
): void {
    $src = imagecreatefromstring(file_get_contents($inputPath));
    if ($src === false) {
        throw new RuntimeException("Cannot open: $inputPath");
    }

    $srcW = imagesx($src);
    $srcH = imagesy($src);
    $scale = $maxWidth / $srcW;
    $dstW  = $maxWidth;
    $dstH  = (int) round($srcH * $scale);

    $dst = imagecreatetruecolor($dstW, $dstH);
    imagecopyresampled($dst, $src, 0, 0, 0, 0, $dstW, $dstH, $srcW, $srcH);

    saveGdImage($dst, $outputPath);
    imagedestroy($src);
    imagedestroy($dst);
}

/**
 * Fit inside targetW × targetH, preserve aspect ratio.
 */
function resizeFit(
    string $inputPath,
    string $outputPath,
    int $targetW,
    int $targetH
): void {
    $src    = imagecreatefromstring(file_get_contents($inputPath));
    $srcW   = imagesx($src);
    $srcH   = imagesy($src);
    $scale  = min($targetW / $srcW, $targetH / $srcH);
    $dstW   = (int) round($srcW * $scale);
    $dstH   = (int) round($srcH * $scale);

    $dst = imagecreatetruecolor($dstW, $dstH);
    imagecopyresampled($dst, $src, 0, 0, 0, 0, $dstW, $dstH, $srcW, $srcH);

    saveGdImage($dst, $outputPath);
    imagedestroy($src);
    imagedestroy($dst);
}

function saveGdImage(\GdImage $img, string $path): void {
    $ext = strtolower(pathinfo($path, PATHINFO_EXTENSION));
    match ($ext) {
        'jpg', 'jpeg' => imagejpeg($img, $path, 90),
        'png'         => imagepng($img, $path),
        'webp'        => imagewebp($img, $path, 90),
        default       => throw new RuntimeException("Unsupported format: $ext"),
    };
}

Imagick

<?php
// Fit inside 800×600, preserve ratio
$img = new Imagick('input.jpg');
$img->thumbnailImage(800, 600, true);   // true = fit (bestfit)
$img->writeImage('resized.jpg');
$img->destroy();

// Cover: resize then crop to exact size
$img = new Imagick('input.jpg');
$img->cropThumbnailImage(400, 300);
$img->writeImage('cover.jpg');
$img->destroy();

thumbnailImage($w, $h, true) shrinks to fit; cropThumbnailImage($w, $h) scales and centre-crops — the Imagick equivalents of Sharp's contain and cover.


Quick Reference

Goal Sharp (Node) Pillow (Python) GD (PHP)
Width 800, keep ratio resize({ width: 800 }) thumbnail((800, 9999)) imagecopyresampled with scale = 800/W
Fit inside 800×600 resize(800, 600, { fit:'contain' }) thumbnail((800, 600)) scale = min(800/W, 600/H)
Cover 400×300 resize(400, 300, { fit:'cover' }) ImageOps.fit(img, (400,300)) imagecopyresampled + imagecrop
Exact 400×300 resize(400, 300, { fit:'fill' }) img.resize((400, 300)) imagecopyresampled

Common Pitfalls

1. Not preserving the aspect ratio

Passing both width and height without a fit mode to most libraries stretches the image. Use fit: "contain" (Sharp), thumbnail() (Pillow), or compute scale = min(targetW/W, targetH/H) manually.

2. Upscaling raster images

Enlarging a photo beyond its original resolution adds no detail — it just looks blurry. Add withoutEnlargement: true (Sharp) or check scale > 1 before applying. For SVG and vector art, upscaling is fine.

3. Using imagecopyresized instead of imagecopyresampled in PHP

imagecopyresized is fast but uses nearest-neighbour interpolation — the output is blocky. Always use imagecopyresampled for smooth results.

4. Losing transparency when resizing PNG to JPEG

JPEG has no alpha channel. A transparent PNG → JPEG conversion fills transparent areas with black by default. Either keep the output as PNG, or manually fill with white before encoding:

// Sharp: flatten transparency before JPEG
await sharp("transparent.png")
  .flatten({ background: "#ffffff" })
  .resize(400)
  .jpeg({ quality: 90 })
  .toFile("output.jpg");

5. Re-encoding JPEG multiple times

Each JPEG encode/decode cycle loses quality (generation loss). Always work from the original source file, not a previously resized JPEG.

6. Ignoring EXIF orientation

Many phone photos have Orientation metadata instead of actually rotating the pixels. Libraries may or may not auto-rotate. Sharp applies EXIF rotation automatically; Pillow requires ImageOps.exif_transpose().


FAQ

What is the best image size for a website? For full-width hero images aim for 1920 px wide. For thumbnails 400–600 px. For product cards 600–800 px. Always serve WebP where supported — it is 25–35 % smaller than JPEG at equivalent quality.

Should I resize on the client or server? Server-side: better control, consistent quality, can cache the result, no extra client CPU work. Client-side (Canvas): useful when the image is user-provided and you want to reduce upload size before sending.

What is the difference between resize and crop? Resize scales all pixels — the whole image, proportionally or with distortion. Crop removes pixels outside a selected rectangle — the remaining pixels are not scaled.

What resampling filter should I use? Lanczos (LANCZOS / Sinc) gives the best quality for downscaling. Bicubic is slightly faster with marginally less sharpness. Nearest-neighbour is for pixel art only. For upscaling, use bicubic or a super-resolution model.

How do I generate multiple sizes at once? With Sharp you can call .toFile() multiple times from a single pipeline by using .clone():

const pipeline = sharp("input.jpg");
await Promise.all([
  pipeline.clone().resize(1920).toFile("large.jpg"),
  pipeline.clone().resize(800).toFile("medium.jpg"),
  pipeline.clone().resize(400).toFile("thumb.jpg"),
]);

Does resizing reduce file size? Yes — fewer pixels = less data to encode. A 1920 px image resized to 400 px can be 80–95 % smaller on disk. Combine with compression for maximum savings.

Related tools

Keep reading

All Toolmingotools are free & run in your browser

No sign-up, no upload, no watermark. Your files never leave your device.

Browse all tools