How to Convert Between JPG and PNG in Python
Web developers frequently need to convert between JPG (small file size, no transparency) and PNG (lossless quality, transparency support). While the conversion seems simple, handling alpha channels correctly is essential to avoid errors and visual artifacts.
This guide covers proper conversion techniques using the Pillow library.
Basic Setup
pip install Pillow
from PIL import Image
JPG to PNG (Simple)
Converting JPG to PNG is straightforward since JPG has no transparency to preserve:
from PIL import Image
def jpg_to_png(source_path, dest_path):
"""Convert JPG to PNG format."""
img = Image.open(source_path)
img.save(dest_path, 'PNG')
# Usage
jpg_to_png("photo.jpg", "photo.png")
With Optimization
from PIL import Image
def jpg_to_png_optimized(source_path, dest_path, compress_level=6):
"""
Convert JPG to PNG with compression.
Args:
compress_level: 0-9, higher = smaller file but slower
"""
img = Image.open(source_path)
img.save(dest_path, 'PNG', optimize=True, compress_level=compress_level)
jpg_to_png_optimized("photo.jpg", "photo.png")
PNG to JPG (Handling Transparency)
PNG files use RGBA mode (with alpha channel for transparency). JPG only supports RGB, so direct conversion fails:
from PIL import Image
# ⛔️ This crashes on transparent PNGs!
img = Image.open("logo_transparent.png")
img.save("logo.jpg") # OSError: cannot write mode RGBA as JPEG
OSError: cannot write mode RGBA as JPEG occurs when saving a PNG with transparency as JPG. You must handle the alpha channel explicitly.
The Background Composite Method (Recommended)
from PIL import Image
def png_to_jpg(source_path, dest_path, background_color=(255, 255, 255), quality=95):
"""
Convert PNG to JPG with proper transparency handling.
Args:
background_color: RGB tuple for transparent areas (default: white)
quality: JPG quality 1-100
"""
img = Image.open(source_path)
if img.mode in ('RGBA', 'LA', 'P'):
# Create background canvas
background = Image.new('RGB', img.size, background_color)
# Handle palette mode with transparency
if img.mode == 'P':
img = img.convert('RGBA')
# Paste using alpha channel as mask
if img.mode == 'RGBA':
background.paste(img, mask=img.split()[3])
else:
background.paste(img, mask=img.split()[1]) # LA mode
background.save(dest_path, 'JPEG', quality=quality)
else:
# No transparency, simple conversion
img.convert('RGB').save(dest_path, 'JPEG', quality=quality)
# Usage
png_to_jpg("logo_transparent.png", "logo.jpg") # White background
png_to_jpg("icon.png", "icon.jpg", background_color=(0, 0, 0)) # Black background
Simple Conversion (Transparency Becomes Black)
from PIL import Image
def png_to_jpg_simple(source_path, dest_path, quality=95):
"""
Simple PNG to JPG conversion.
Warning: Transparent areas become BLACK.
"""
img = Image.open(source_path)
rgb_img = img.convert('RGB')
rgb_img.save(dest_path, 'JPEG', quality=quality)
# Only use when transparency isn't present or black background is acceptable
png_to_jpg_simple("photo.png", "photo.jpg")
Checking Image Mode Before Conversion
from PIL import Image
def analyze_image(path):
"""Check image properties before conversion."""
img = Image.open(path)
info = {
'path': path,
'format': img.format,
'mode': img.mode,
'size': img.size,
'has_transparency': img.mode in ('RGBA', 'LA', 'P'),
}
# Check if palette mode has transparency
if img.mode == 'P':
info['has_transparency'] = 'transparency' in img.info
return info
# Usage
print(analyze_image("unknown.png"))
# {'path': 'unknown.png', 'format': 'PNG', 'mode': 'RGBA', 'size': (800, 600), 'has_transparency': True}
Batch Conversion
Convert All PNGs to JPGs
from PIL import Image
from pathlib import Path
def batch_png_to_jpg(input_dir, output_dir=None, background=(255, 255, 255)):
"""Convert all PNGs in a directory to JPGs."""
input_path = Path(input_dir)
output_path = Path(output_dir) if output_dir else input_path
output_path.mkdir(parents=True, exist_ok=True)
converted = 0
for png_file in input_path.glob("*.png"):
try:
img = Image.open(png_file)
if img.mode in ('RGBA', 'LA', 'P'):
if img.mode == 'P':
img = img.convert('RGBA')
bg = Image.new('RGB', img.size, background)
if img.mode == 'RGBA':
bg.paste(img, mask=img.split()[3])
else:
bg.paste(img, mask=img.split()[1])
img = bg
else:
img = img.convert('RGB')
output_file = output_path / f"{png_file.stem}.jpg"
img.save(output_file, 'JPEG', quality=90)
converted += 1
except Exception as e:
print(f"Error converting {png_file.name}: {e}")
print(f"Converted {converted} files")
# Usage
batch_png_to_jpg("images/", "converted/")
Convert All JPGs to PNGs
from PIL import Image
from pathlib import Path
def batch_jpg_to_png(input_dir, output_dir=None):
"""Convert all JPGs in a directory to PNGs."""
input_path = Path(input_dir)
output_path = Path(output_dir) if output_dir else input_path
output_path.mkdir(parents=True, exist_ok=True)
for jpg_file in input_path.glob("*.jpg"):
img = Image.open(jpg_file)
output_file = output_path / f"{jpg_file.stem}.png"
img.save(output_file, 'PNG', optimize=True)
# Also handle .jpeg extension
for jpeg_file in input_path.glob("*.jpeg"):
img = Image.open(jpeg_file)
output_file = output_path / f"{jpeg_file.stem}.png"
img.save(output_file, 'PNG', optimize=True)
batch_jpg_to_png("photos/", "png_output/")
Preserving Metadata
from PIL import Image
from PIL.ExifTags import TAGS
def convert_preserving_metadata(source, dest, target_format):
"""Convert image while preserving EXIF metadata."""
img = Image.open(source)
# Extract EXIF data
exif = img.info.get('exif')
# Handle conversion
if target_format.upper() == 'JPEG' and img.mode in ('RGBA', 'P'):
if img.mode == 'P':
img = img.convert('RGBA')
background = Image.new('RGB', img.size, (255, 255, 255))
background.paste(img, mask=img.split()[3])
img = background
elif target_format.upper() == 'JPEG':
img = img.convert('RGB')
# Save with EXIF if available
if exif:
img.save(dest, target_format, exif=exif, quality=95)
else:
img.save(dest, target_format, quality=95)
# Usage
convert_preserving_metadata("photo.png", "photo.jpg", "JPEG")
Resizing During Conversion
from PIL import Image
def convert_and_resize(source, dest, max_size=(1920, 1080), background=(255, 255, 255)):
"""Convert format and resize in one operation."""
img = Image.open(source)
# Resize maintaining aspect ratio
img.thumbnail(max_size, Image.Resampling.LANCZOS)
# Determine output format from extension
is_jpg = dest.lower().endswith(('.jpg', '.jpeg'))
if is_jpg:
if img.mode in ('RGBA', 'LA', 'P'):
if img.mode == 'P':
img = img.convert('RGBA')
bg = Image.new('RGB', img.size, background)
if img.mode == 'RGBA':
bg.paste(img, mask=img.split()[3])
else:
bg.paste(img, mask=img.split()[1])
img = bg
else:
img = img.convert('RGB')
img.save(dest, 'JPEG', quality=90)
else:
img.save(dest, 'PNG', optimize=True)
# Usage
convert_and_resize("large_logo.png", "thumbnail.jpg", max_size=(200, 200))
Format Comparison
| Feature | JPG | PNG |
|---|---|---|
| Transparency | No | Yes (alpha channel) |
| Compression | Lossy | Lossless |
| File Size | Smaller | Larger |
| Best For | Photos, gradients | Logos, text, screenshots |
| Color Depth | 24-bit | Up to 48-bit |
Summary
| Conversion | Method | Key Consideration |
|---|---|---|
| JPG → PNG | img.save("out.png") | Simple, automatic |
| PNG → JPG | Composite onto background | Required for transparency |
| PNG → JPG (simple) | .convert("RGB") | Transparency becomes black |
When converting PNG to JPG, always use the background composite method unless you're certain the PNG has no transparency. A simple .convert("RGB") turns transparent pixels black, which is rarely the desired result for logos and graphics.