How to Crop an Image
Cropping is the most fundamental image editing operation — selecting a rectangular region and discarding everything outside it. Whether you are building a profile-photo uploader, a thumbnail pipeline, or a document scanner, you will eventually need to crop images in code. This guide explains the cropping model, common patterns, and working implementations in JavaScript, Python, Go, and PHP.
The Cropping Model
Every crop operation needs four numbers:
| Parameter | Meaning |
|---|---|
x |
Left edge of the crop region (pixels from the left) |
y |
Top edge of the crop region (pixels from the top) |
width |
Width of the crop region |
height |
Height of the crop region |
The output image is width × height pixels. The origin (0, 0) is always the top-left corner of the source image.
Source image (800 × 600)
┌──────────────────────────┐
│ │
│ ┌──────────┐ │
│ │ crop │ │
│ │ x=100 │ │
│ │ y=50 │ │
│ │ w=300 │ │
│ │ h=200 │ │
│ └──────────┘ │
│ │
└──────────────────────────┘
Bounds check: x + width ≤ image_width and y + height ≤ image_height. Exceeding the bounds causes errors or silent clipping depending on the library.
Common Cropping Patterns
1. Fixed-coordinate crop
You know exactly which region you want. Pass x, y, width, height directly.
Use case: cropping a signature region from a scanned form, extracting a logo from a screenshot.
2. Center crop
Take the middle of an image. Given a target size tw × th:
x = (image_width - tw) / 2
y = (image_height - th) / 2
Use case: generating square thumbnails from landscape or portrait photos.
3. Aspect-ratio crop
Crop to a specific ratio (e.g. 16:9) using as much of the image as possible:
if image_width / image_height > target_ratio:
# image is wider — constrain by height
crop_height = image_height
crop_width = crop_height * target_ratio
else:
# image is taller — constrain by width
crop_width = image_width
crop_height = crop_width / target_ratio
x = (image_width - crop_width) / 2
y = (image_height - crop_height) / 2
Use case: video thumbnails, social media cards, responsive image variants.
4. Smart / face-aware crop
Detect the subject (face, object) first, then place the crop window around it. This is a separate topic (object detection), but the cropping step is still the same four-number rectangle.
JavaScript
Browser: Canvas API
/**
* Crop an image using the Canvas API (browser).
* @param {HTMLImageElement} img - loaded <img> element
* @param {number} x - left edge
* @param {number} y - top edge
* @param {number} width - crop width
* @param {number} height - crop height
* @returns {Promise<Blob>} - cropped image as PNG Blob
*/
function cropImage(img, x, y, width, height) {
const canvas = document.createElement("canvas");
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext("2d");
// drawImage(source, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight)
ctx.drawImage(img, x, y, width, height, 0, 0, width, height);
return new Promise((resolve) => canvas.toBlob(resolve, "image/png"));
}
// Center-crop example
function centerCrop(img, size) {
const x = Math.floor((img.naturalWidth - size) / 2);
const y = Math.floor((img.naturalHeight - size) / 2);
return cropImage(img, x, y, size, size);
}
Node.js: Sharp
Sharp is the standard server-side image library for Node.js.
import sharp from "sharp";
// Fixed-coordinate crop
await sharp("input.jpg")
.extract({ left: 100, top: 50, width: 300, height: 200 })
.toFile("cropped.jpg");
// Center crop to 400×400
async function centerCropFile(inputPath, outputPath, size) {
const meta = await sharp(inputPath).metadata();
const left = Math.floor((meta.width - size) / 2);
const top = Math.floor((meta.height - size) / 2);
await sharp(inputPath)
.extract({ left, top, width: size, height: size })
.toFile(outputPath);
}
// Aspect-ratio crop (16:9)
async function aspectRatioCrop(inputPath, outputPath, ratioW = 16, ratioH = 9) {
const meta = await sharp(inputPath).metadata();
const ratio = ratioW / ratioH;
let cropW, cropH;
if (meta.width / meta.height > ratio) {
cropH = meta.height;
cropW = Math.floor(cropH * ratio);
} else {
cropW = meta.width;
cropH = Math.floor(cropW / ratio);
}
const left = Math.floor((meta.width - cropW) / 2);
const top = Math.floor((meta.height - cropH) / 2);
await sharp(inputPath)
.extract({ left, top, width: cropW, height: cropH })
.toFile(outputPath);
}
Python
The Pillow library uses a (left, upper, right, lower) box tuple — note the difference from width/height.
from PIL import Image
def crop_image(input_path: str, output_path: str,
x: int, y: int, width: int, height: int) -> None:
"""Crop using top-left corner + dimensions."""
with Image.open(input_path) as img:
box = (x, y, x + width, y + height)
cropped = img.crop(box)
cropped.save(output_path)
def center_crop(input_path: str, output_path: str, size: int) -> None:
"""Crop a square from the centre of the image."""
with Image.open(input_path) as img:
w, h = img.size
x = (w - size) // 2
y = (h - size) // 2
cropped = img.crop((x, y, x + size, y + size))
cropped.save(output_path)
def aspect_ratio_crop(input_path: str, output_path: str,
ratio_w: int = 16, ratio_h: int = 9) -> None:
"""Centre-crop to the given aspect ratio."""
with Image.open(input_path) as img:
w, h = img.size
ratio = ratio_w / ratio_h
if w / h > ratio:
crop_h = h
crop_w = int(crop_h * ratio)
else:
crop_w = w
crop_h = int(crop_w / ratio)
x = (w - crop_w) // 2
y = (h - crop_h) // 2
cropped = img.crop((x, y, x + crop_w, y + crop_h))
cropped.save(output_path)
Pillow box format:
| Parameter | Meaning |
|---|---|
left |
x (pixels from left) |
upper |
y (pixels from top) |
right |
x + width |
lower |
y + height |
Go
The standard library image package includes a SubImage method on most image types.
package main
import (
"image"
"image/jpeg"
"image/png"
"os"
"path/filepath"
"strings"
)
// CropImage crops src to the given rectangle and saves as PNG or JPEG.
func CropImage(src image.Image, x, y, width, height int, outputPath string) error {
type subImager interface {
SubImage(r image.Rectangle) image.Image
}
si, ok := src.(subImager)
if !ok {
return fmt.Errorf("image does not support SubImage")
}
rect := image.Rect(x, y, x+width, y+height)
cropped := si.SubImage(rect)
out, err := os.Create(outputPath)
if err != nil {
return err
}
defer out.Close()
ext := strings.ToLower(filepath.Ext(outputPath))
if ext == ".jpg" || ext == ".jpeg" {
return jpeg.Encode(out, cropped, &jpeg.Options{Quality: 90})
}
return png.Encode(out, cropped)
}
// CenterCrop crops a square of `size` from the centre.
func CenterCrop(src image.Image, size int, outputPath string) error {
b := src.Bounds()
x := (b.Dx() - size) / 2
y := (b.Dy() - size) / 2
return CropImage(src, x, y, size, size, outputPath)
}
SubImagereturns a view into the same pixel data — it does not copy pixels. To get an independent copy, encode to a buffer and re-decode, or draw onto a newimage.RGBA.
PHP
GD (built into PHP) and Imagick (ImageMagick extension) both support cropping.
GD
<?php
/**
* Crop an image file and save the result.
*/
function cropImage(
string $inputPath,
string $outputPath,
int $x,
int $y,
int $width,
int $height
): void {
$src = imagecreatefromstring(file_get_contents($inputPath));
if ($src === false) {
throw new RuntimeException("Cannot open image: $inputPath");
}
$dst = imagecrop($src, ['x' => $x, 'y' => $y, 'width' => $width, 'height' => $height]);
if ($dst === false) {
imagedestroy($src);
throw new RuntimeException("Crop failed — check bounds.");
}
$ext = strtolower(pathinfo($outputPath, PATHINFO_EXTENSION));
match ($ext) {
'jpg', 'jpeg' => imagejpeg($dst, $outputPath, 90),
'png' => imagepng($dst, $outputPath),
'webp' => imagewebp($dst, $outputPath, 90),
default => throw new RuntimeException("Unsupported format: $ext"),
};
imagedestroy($src);
imagedestroy($dst);
}
// Center crop to square
function centerCrop(string $inputPath, string $outputPath, int $size): void {
$src = imagecreatefromstring(file_get_contents($inputPath));
$x = (imagesx($src) - $size) / 2;
$y = (imagesy($src) - $size) / 2;
cropImage($inputPath, $outputPath, (int)$x, (int)$y, $size, $size);
imagedestroy($src);
}
Imagick
<?php
$img = new Imagick('input.jpg');
$img->cropImage(300, 200, 100, 50); // width, height, x, y
$img->writeImage('cropped.jpg');
$img->destroy();
Note: Imagick parameter order is
cropImage(width, height, x, y)— the opposite ofx, y, width, height. Easy to mix up.
Quick Reference
| Pattern | Formula |
|---|---|
| Fixed crop | x, y, width, height — direct |
| Center square | x = (W−s)/2, y = (H−s)/2 |
| 16:9 from landscape | cropH = H, cropW = H × 16/9, centre |
| 16:9 from portrait | cropW = W, cropH = W × 9/16, centre |
| 4:3 thumbnail | cropH = H or cropW = W (whichever fits), centre |
| Safe bounds check | x+width ≤ W and y+height ≤ H |
Common Pitfalls
1. Off-by-one at the right/bottom edge
If the crop region ends exactly at the image border, some libraries are exclusive (right = x + width) and some are inclusive. Always check the docs.
2. Pillow box is (left, upper, right, lower), not (x, y, w, h)
The most common Python mistake. right = x + width, lower = y + height.
3. Imagick parameter order is reversed
cropImage(width, height, x, y) — not (x, y, width, height).
4. Cropping a JPEG twice degrades quality
Each JPEG encode/decode cycle loses information. Decode → crop → encode once. Never crop by re-saving a JPEG without purpose.
5. Go SubImage shares memory
The returned image.Image from SubImage is a view, not a copy. Modifying the original will change the cropped view. For a true copy, draw into a new image.RGBA.
FAQ
Can I crop without losing image quality?
Yes — if you save as PNG (lossless) or use JPEG lossless crop tools (e.g. jpegtran -crop). Standard JPEG encode after crop is lossy by definition.
How do I crop a circle instead of a rectangle? Crop a square first, then apply a circular mask (alpha channel). The "crop" is still rectangular; the circle is a transparency mask on top.
What's the difference between crop and resize? Crop removes pixels outside a region — the remaining pixels are unchanged. Resize keeps all content but scales the number of pixels up or down. You often crop first, then resize.
How do I crop to a percentage of the image?
Multiply percentages by pixel dimensions: x = pct_x * width / 100, then use those pixel values.
Does cropping change the file size? Yes. A smaller crop region = fewer pixels = smaller file (all else equal). But the final size also depends on image complexity and format.
What resolution should I target for profile photos? 400×400 px is a common minimum. Most platforms accept up to 2000×2000 px and resize internally. A 400–800 px square PNG or JPEG at 85% quality is the safe default.