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 Pillowfrom PIL import ImageJPG 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')
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
img = Image.open("logo_transparent.png")
img.save("logo.jpg") # OSError: cannot write mode RGBA as JPEGThe 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'):
background = Image.new('RGB', img.size, background_color)
if img.mode == 'P':
img = img.convert('RGBA')
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:
img.convert('RGB').save(dest_path, 'JPEG', quality=quality)
png_to_jpg("logo_transparent.png", "logo.jpg") # White background
png_to_jpg("icon.png", "icon.jpg", background_color=(0, 0, 0)) # Black backgroundSimple 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)
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'),
}
if img.mode == 'P':
info['has_transparency'] = 'transparency' in img.info
return info
print(analyze_image("unknown.png"))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")
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)
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)
exif = img.info.get('exif')
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')
if exif:
img.save(dest, target_format, exif=exif, quality=95)
else:
img.save(dest, target_format, quality=95)
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)
img.thumbnail(max_size, Image.Resampling.LANCZOS)
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)
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 |