Image Compression and Resizing with Python and Pillow
Optimizing images is critical for web performance and efficient storage. Python with Pillow gives you full control over quality, dimensions, and format.
Installation
pip install Pillow
Resize While Preserving Aspect Ratio
from PIL import Image
def resize_image(input_path, max_width, output_path):
with Image.open(input_path) as img:
ratio = max_width / img.width
new_height = int(img.height * ratio)
img.resize((max_width, new_height), Image.LANCZOS).save(output_path)
print(f"{img.width}x{img.height} -> {max_width}x{new_height}")
resize_image("photo.jpg", 800, "photo_800px.jpg")
Compress JPG with Quality Control
from PIL import Image
import os
def compress_jpg(input_path, output_path, quality=80):
with Image.open(input_path) as img:
if img.mode in ('RGBA', 'P', 'LA'):
img = img.convert('RGB')
img.save(output_path, format='JPEG', quality=quality, optimize=True, progressive=True)
kb_in = os.path.getsize(input_path) // 1024
kb_out = os.path.getsize(output_path) // 1024
print(f"{kb_in} KB -> {kb_out} KB ({100 - kb_out*100//kb_in:.0f}% smaller)")
compress_jpg("original.jpg", "compressed.jpg", quality=80)
Convert to WebP
from PIL import Image
import os
def to_webp(input_path, output_path=None, quality=80, lossless=False):
if output_path is None:
output_path = os.path.splitext(input_path)[0] + ".webp"
with Image.open(input_path) as img:
img.save(output_path, format='WEBP', quality=quality, lossless=lossless, method=6)
kb_in = os.path.getsize(input_path) // 1024
kb_out = os.path.getsize(output_path) // 1024
print(f"{input_path}: {kb_in} KB -> {kb_out} KB")
to_webp("photo.jpg")
# PNG with transparency -> WebP
with Image.open("logo.png") as img:
img.save("logo.webp", format="WEBP", quality=80)
JPG Quality Comparison
from PIL import Image
import os
img = Image.open("original.jpg")
for quality in [95, 85, 75, 60, 40]:
path = f"quality_{quality}.jpg"
img.save(path, format='JPEG', quality=quality, optimize=True)
kb = os.path.getsize(path) // 1024
print(f"Quality {quality:3d}: {kb:4d} KB")
img.close()
Typical for a 2 MB original:
Quality 95: 1850 KB
Quality 85: 980 KB <- recommended balance
Quality 75: 640 KB
Quality 60: 380 KB
Quality 40: 210 KB (visible degradation)
Batch Processing
from PIL import Image
import os
def batch_compress(input_folder, output_folder, max_width=1920, quality=85, fmt="WEBP"):
extensions = {'.jpg', '.jpeg', '.png', '.bmp', '.tiff', '.webp'}
os.makedirs(output_folder, exist_ok=True)
files = [f for f in os.listdir(input_folder)
if os.path.splitext(f)[1].lower() in extensions]
print(f"Processing {len(files)} images...")
for filename in files:
path_in = os.path.join(input_folder, filename)
out_name = os.path.splitext(filename)[0] + '.' + fmt.lower()
path_out = os.path.join(output_folder, out_name)
try:
with Image.open(path_in) as img:
if img.width > max_width:
ratio = max_width / img.width
img = img.resize((max_width, int(img.height * ratio)), Image.LANCZOS)
if fmt in ('JPEG', 'JPG') and img.mode in ('RGBA', 'P'):
img = img.convert('RGB')
img.save(path_out, format=fmt, quality=quality, optimize=True)
kb_in = os.path.getsize(path_in) // 1024
kb_out = os.path.getsize(path_out) // 1024
print(f" OK {filename} ({kb_in} KB) -> {out_name} ({kb_out} KB)")
except Exception as e:
print(f" ERROR {filename}: {e}")
batch_compress("original_photos/", "web_photos/", fmt="WEBP")
Square Thumbnail
from PIL import Image
def square_thumbnail(input_path, output_path, size=400):
with Image.open(input_path) as img:
img.thumbnail((size * 2, size * 2), Image.LANCZOS)
canvas = Image.new('RGB', (size, size), (255, 255, 255))
x = (size - img.width) // 2
y = (size - img.height) // 2
canvas.paste(img, (x, y))
canvas.save(output_path, quality=85)
square_thumbnail("product.jpg", "thumbnail.jpg", 400)
Add Watermark
from PIL import Image, ImageDraw, ImageFont
def add_watermark(input_path, output_path, text="(c) My Brand"):
with Image.open(input_path).convert('RGBA') as img:
overlay = Image.new('RGBA', img.size, (0, 0, 0, 0))
draw = ImageDraw.Draw(overlay)
try:
font = ImageFont.truetype("arial.ttf", 36)
except:
font = ImageFont.load_default()
bbox = draw.textbbox((0, 0), text, font=font)
x = img.width - (bbox[2] - bbox[0]) - 20
y = img.height - (bbox[3] - bbox[1]) - 20
draw.text((x, y), text, fill=(255, 255, 255, 180), font=font)
Image.alpha_composite(img, overlay).convert('RGB').save(output_path, quality=90)
add_watermark("photo.jpg", "photo_watermarked.jpg")
Format Comparison
| Format | Compression | Transparency | Animation | Best for |
|---|---|---|---|---|
| JPEG | Lossy | No | No | Photographs |
| PNG | Lossless | Yes | No | Logos, screenshots |
| WebP | Lossy/Lossless | Yes | Yes | Modern web |
| AVIF | Lossy/Lossless | Yes | Yes | Better compression |
| GIF | Lossless 8-bit | Yes (1-bit) | Yes | Simple animations |
Online Optimization
To compress images without installing Python, KaijuConverter converts between JPG, PNG, WebP and more directly in your browser.
Related conversions
Most teams that read this guide convert images in one of these directions: