Python Pillow: Open, Save, Resize, Crop, and Rotate Images
python pillow open save resize crop and rotate images: Learn how to open, save, resize, crop, and rotate images with Python Pillow. Covers core Image methods, EXIF ori...
python pillow open save resize crop and rotate images requires a clear understanding of the core syntax, runtime behavior, and practical implementation patterns demonstrated in the examples below.
When you need to open, save, resize, crop, and rotate images in Python, Pillow is the standard library for the job. This article covers the core operations: open, save, resize, crop, and rotate images using Pillow's Image module. We'll also cover EXIF orientation and performance considerations that matter when processing many images.
Opening an Image and Understanding Its Mode
The first step is to load an image with Image.open(). This returns an Image object that holds the pixel data and metadata. Pillow does not load the full pixel data until you access it, which is useful for reading only the header.
from PIL import Image img = Image.open("photo.jpg") print(img.format, img.size, img.mode)
The mode tells you how pixels are stored. Common modes are 'RGB' for color images, 'RGBA' for color with transparency, 'L' for grayscale, and 'P' for palette-based images. Knowing the mode matters because some operations behave differently depending on it. For example, converting an RGBA image to JPEG requires dropping the alpha channel.
If you need to work with the image data immediately, call img.load() to force the pixel data into memory. Otherwise, Pillow reads it lazily when needed.
Saving Images in Different Formats
The save() method writes the image to disk. The format is usually inferred from the file extension, but you can also pass an explicit format argument.
img.save("output.png") img.save("output.jpg", format="JPEG", quality=85, optimize=True)
For JPEG, the quality parameter controls compression. Lower values produce smaller files with more artifacts. The optimize option enables additional compression passes, which can reduce file size at the cost of a little extra processing time. For PNG, you can use compress_level to trade speed against file size.
When saving to a format that does not support the current mode, you may need to convert first. For example, saving an RGBA image as JPEG raises an error because JPEG has no alpha channel. Convert it to RGB first:
img.convert("RGB").save("output.jpg")
Resizing Images with resize and thumbnail
resize() returns a new image with the exact dimensions you specify. The resample parameter controls the interpolation algorithm. The default is Image.BICUBIC, which gives good quality for most uses. For downscaling, Image.LANCZOS often produces the sharpest result.
resized = img.resize((800, 600), resample=Image.LANCZOS)
Note that resize() does not preserve the aspect ratio unless you compute the new dimensions yourself. If you want a thumbnail that fits within a bounding box while keeping the original proportions, use thumbnail() instead. It modifies the image in place and only scales down, never up.
thumb = img.copy() thumb.thumbnail((200, 200))
thumbnail() sets the image size to the largest size that fits inside the box while preserving aspect ratio. It is more memory-efficient than resize() for creating small previews because it works on a copy and does not require a full-size intermediate.
Cropping Images with the crop Method
Cropping extracts a rectangular region from the image. The crop() method takes a box tuple in the form (left, upper, right, lower). Coordinates are measured from the top-left corner, with left and upper being the starting point and right and lower the exclusive end points.
box = (100, 50, 400, 300) # left, upper, right, lower cropped = img.crop(box)
The resulting image has a size of (right - left, lower - upper). If you need to crop relative to the center, you can compute the box dynamically:
width, height = img.size crop_width, crop_height = 200, 200 left = (width - crop_width) // 2 top = (height - crop_height) // 2 right = left + crop_width bottom = top + crop_height center_cropped = img.crop((left, top, right, bottom))
crop() returns a new image; it does not modify the original.
Rotating Images with rotate and transpose
The rotate() method rotates the image counter-clockwise by the given angle. By default, the image is rotated in place without expanding the canvas, so corners may be clipped. Set expand=True to enlarge the canvas to fit the rotated image.
rotated = img.rotate(45, expand=True, resample=Image.BICUBIC)
The center parameter lets you rotate around a specific point. Without it, rotation is around the image center.
For right-angle rotations, transpose() is more efficient because it does not resample. Use Image.ROTATE_90, Image.ROTATE_180, or Image.ROTATE_270 to rotate clockwise by multiples of 90 degrees. You can also mirror the image with Image.FLIP_LEFT_RIGHT and Image.FLIP_TOP_BOTTOM.
rotated_90 = img.transpose(Image.ROTATE_90) flipped = img.transpose(Image.FLIP_LEFT_RIGHT)
transpose() returns a new image and is faster than rotate() for these fixed angles because it only rearranges pixels.
Handling EXIF Orientation and ImageOps.exif_transpose
Digital cameras store orientation information in EXIF metadata. When you open a photo with Image.open(), the pixel data is not automatically rotated to match the orientation. If you rotate the image manually, you may end up with the wrong orientation.
Pillow provides ImageOps.exif_transpose() to apply the EXIF orientation automatically. It returns a new image with the correct orientation, or the original image if no orientation tag is present.
from PIL import ImageOps img = Image.open("photo.jpg") img = ImageOps.exif_transpose(img)
This is especially important when processing photos from phones or cameras. Always call exif_transpose() before resizing or cropping so that the dimensions and coordinates refer to the visually correct orientation.
Performance and Memory Considerations
Working with large images can consume a lot of memory. Each pixel is stored as a tuple of values, so a 4000x3000 RGB image uses roughly 36 MB of memory. Operations like resize() and crop() create new image objects, so the original and the result exist simultaneously.
To reduce memory usage, close the file after loading the image data if you no longer need the file handle:
with Image.open("large.jpg") as img: img.load() resized = img.resize((1000, 750))
The with block ensures the file is closed. For batch processing, consider processing one image at a time and explicitly deleting large images when done.
thumbnail() is memory-friendly because it works on a copy and scales down. If you only need a small preview, avoid loading the full-size image into memory by using Image.open() and then thumbnail() without calling load() first. Pillow will read the necessary data lazily.
Common Pitfalls: Mode, Coordinate Order, and In-Place Operations
Several mistakes are common when working with Pillow.
Mode mismatches: Some operations fail if the image mode is not compatible. For example, saving an RGBA image as JPEG fails, and rotating an image with a palette mode may produce unexpected results. Convert to a standard mode like RGB or RGBA before processing.
Coordinate order: crop() uses (left, upper, right, lower), while resize() uses (width, height). Mixing these up is easy. Always double-check the order.
In-place vs. new object: thumbnail() modifies the image in place, while resize(), crop(), rotate(), and transpose() return new images. If you forget to assign the result, the original remains unchanged.
# Wrong: resize returns a new image, but we ignore it img.resize((800, 600)) # Correct: assign the result img = img.resize((800, 600))
EXIF orientation: As mentioned, exif_transpose() should be called early in the pipeline. Otherwise, you might crop or resize based on the raw sensor orientation, leading to wrong output.
By keeping these details in mind, you can reliably open, save, resize, crop, and rotate images with Pillow in your Python projects.