Last updated: May 12, 2025

<span aria-hidden="true" id="efficient-png-optimization-in-python-with-oxipng"></span>

# Efficient PNG optimization in Python with Oxipng

![Kevin van Zonneveld](/assets/images/teammates/avatar-kvz-4.jpg?dpl=dpl_5zDg5SaYi7KaFpi2w3C4HPYbgapj)

#### Kevin van Zonneveld

Co-founder · Amsterdam, The Netherlands · Show bio

[](https://x.com/kvz)[](https://github.com/kvz)

Optimizing images is crucial for enhancing web performance and reducing bandwidth usage. PNG files are lossless and high-quality, yet they often ship with excess metadata and sub-optimal compression. In this DevTip, we’ll show how to shrink PNGs from Python by calling the modern, Rust-powered Oxipng engine through its actively maintained Python binding, `pyoxipng`.

<span aria-hidden="true" id="why-optimize-png-images"></span>

## Why optimize PNG images?

Smaller images mean faster page loads, better Core Web Vitals, lower CDN bills, and happier users. On mobile connections, a significant reduction in file size can be the difference between a snappy interface and a bounce-inducing wait. Effective PNG optimization can often reduce file sizes by 15-40% without any loss of visual quality.

<span aria-hidden="true" id="optipng-vs-oxipng"></span>

## Optipng vs. Oxipng

[OptiPNG⁠](http://optipng.sourceforge.net/) is the classic, C-based command-line tool for lossless PNG compression. It's been a reliable choice for years. However,[Oxipng⁠](https://github.com/shssoichiro/oxipng) is a modern, Rust rewrite designed for speed and efficiency. It's fully compatible with OptiPNG's lossless optimizations but adds multithreading, SIMD acceleration, and improved heuristics for potentially better compression ratios and faster execution.

The `pyoxipng` library provides Pythonic access to the Oxipng Rust core, allowing you to leverage its performance directly within your Python applications without needing to manage external processes. While this post focuses on `pyoxipng` due to its performance and active maintenance, the optimization principles are similar to those of the original OptiPNG.

<span aria-hidden="true" id="prerequisites"></span>

## Prerequisites

Before you start, ensure you have the necessary tools installed:

```bash
# Check Python version (3.8+ recommended)
python --version

# Install pyoxipng and optional dependencies
pip install pyoxipng pillow numpy

```

* **Python**: Version 3.8 or newer is recommended.
* **pyoxipng**: The core library for PNG optimization (`pip install pyoxipng`). Version 9.1.0 or newer is advised.
* **Pillow**: Used for image loading and manipulation in advanced examples (`pip install pillow`).
* **NumPy**: Required for array handling when working with raw image data (`pip install numpy`).

<span aria-hidden="true" id="optimize-a-single-png"></span>

## Optimize a single PNG

Optimizing a single file is straightforward using the `optimize` function:

```python
import oxipng

try:
    # Optimize 'input.png' and save it as 'output.png'
    # Level ranges from 0 (fastest, least compression) to 6 (slowest, best compression). Default is 2.
    oxipng.optimize('input.png', 'output.png', level=4)
    print("Optimization successful!")
except oxipng.PngError as e:
    print(f"Error optimizing PNG: {e}")
except FileNotFoundError:
    print("Error: Input file not found.")

```

With default settings (`level=2`), you can typically expect a 15–25% reduction in file size, often completed in milliseconds. Increasing the level improves compression but takes longer.

<span aria-hidden="true" id="batch-optimization-with-threads"></span>

## Batch optimization with threads

For optimizing multiple images, using threads can significantly speed up the process, especially on multi-core systems. `pyoxipng` is thread-safe.

```python
import oxipng
import os
from concurrent.futures import ThreadPoolExecutor
from pathlib import Path

# Define source and destination directories
SRC_DIR = Path('images_source')
DST_DIR = Path('images_optimized')
DST_DIR.mkdir(exist_ok=True) # Create destination if it doesn't exist

# Find all PNG files in the source directory
png_files = list(SRC_DIR.glob('*.png'))

# Function to optimize a single PNG file
def optimize_png(source_path: Path) -> None:
    dest_path = DST_DIR / source_path.name
    try:
        oxipng.optimize(source_path, dest_path, level=4)
        print(f'✓ Optimized: {source_path.name}')
    except oxipng.PngError as e:
        print(f'✗ Failed: {source_path.name} - {e}')
    except Exception as e:
        print(f'✗ Error processing {source_path.name}: {e}')

# Use a threadpoolexecutor for parallel processing
# Adjust max_workers based on your CPU cores (os.cpu_count() is a good starting point)
num_workers = os.cpu_count() or 4 # Default to 4 if cpu_count() fails
with ThreadPoolExecutor(max_workers=num_workers) as executor:
    # Map the optimize_png function to all found PNG files
    # Use list() to ensure all futures complete before moving on
    list(executor.map(optimize_png, png_files))

print("\nBatch optimization complete.")

```

Set the `max_workers` in `ThreadPoolExecutor` (e.g., to `os.cpu_count()`) to maximize throughput. On an 8-core machine, processing hundreds of small PNGs can take just a few seconds.

<span aria-hidden="true" id="choosing-the-right-optimization-level"></span>

## Choosing the right optimization level

`pyoxipng` offers optimization levels from 0 to 6. Higher levels yield smaller files but require more processing time.

|Level|Typical Savings|Relative Speed|When to Use|
|-|-|-|-|
|0|5-10%|Very Fast|Quick checks, dev builds where speed is key|
|2 (default)|15-25%|Fast|General web assets, good balance|
|4|20-35%|Moderate|Production deployments, aiming for size|
|6|25-40%|Slowest|Final archives, one-off optimizations|

Choose the level that best balances compression needs with processing time constraints for your use case.

<span aria-hidden="true" id="preserving-or-stripping-metadata"></span>

## Preserving or stripping metadata

By default, `pyoxipng` might strip some non-essential metadata chunks. You can control this behavior using the `strip` parameter:

```python
import oxipng

try:
    # Strip all possible metadata for maximum size reduction
    oxipng.optimize(
        'input_with_meta.png',
        'output_stripped.png',
        level=4,
        strip=oxipng.Strip.all() # Strip all ancillary chunks (like text, exif, etc.)
    )
    print("Optimized with maximum metadata stripping.")

    # Preserve all metadata
    oxipng.optimize(
        'input_with_meta.png',
        'output_preserved.png',
        level=4,
        strip=oxipng.Strip.none() # Keep all metadata chunks
        # preserve_attrs=True # Optionally preserve file system attributes (mtime, etc.)
    )
    print("Optimized while preserving metadata.")

except oxipng.PngError as e:
    print(f"Error during optimization: {e}")
except FileNotFoundError:
    print("Error: Input file 'input_with_meta.png' not found.")

```

Use `strip=oxipng.Strip.all()` to remove maximum metadata for the smallest file size. Use`strip=oxipng.Strip.safe()` to keep essential metadata like color profiles but remove others. Use`strip=oxipng.Strip.none()` (or omit `strip`) to preserve all metadata chunks. `preserve_attrs=True`keeps file system attributes like modification time, which is usually not necessary for web delivery.

<span aria-hidden="true" id="advanced-in-memory-optimization-pipelines"></span>

## Advanced: in-memory optimization pipelines

If you're generating or manipulating images in memory (e.g., using Pillow), you can optimize the raw pixel data directly without writing intermediate files. This requires providing image dimensions and color type information.

```python
import oxipng
import numpy as np
from PIL import Image
from pathlib import Path

try:
    # Load an image using Pillow and convert to a suitable format (e.g., RGB or RGBA)
    img = Image.open('logo_source.png')
    # Ensure image mode is compatible (e.g., RGB, RGBA, L for grayscale)
    if img.mode not in ('RGB', 'RGBA', 'L'):
        img = img.convert('RGBA') # Convert to RGBA if needed

    # Convert image to a NumPy array
    img_array = np.array(img, dtype=np.uint8)
    height, width = img_array.shape[:2] # Get height and width

    # Get raw bytes from the NumPy array
    raw_bytes = img_array.tobytes()

    # Define the color type based on Pillow's image mode
    if img.mode == 'RGB':
        # For RGB, specify transparency key if needed, e.g., black (0,0,0)
        color_type = oxipng.ColorType.rgb(None) # None means no specific color is transparent
    elif img.mode == 'RGBA':
        color_type = oxipng.ColorType.rgba()
    elif img.mode == 'L': # Grayscale
        color_type = oxipng.ColorType.grayscale()
    else:
        raise ValueError(f"Unsupported image mode for Oxipng: {img.mode}")

    # Create a RawImage object
    raw_image = oxipng.RawImage(
        data=raw_bytes,
        width=width,
        height=height,
        color_type=color_type
    )

    # Optimize the raw image data in memory (level 6 for max compression)
    optimized_png_bytes = raw_image.create_optimized_png(level=6)

    # Save the optimized bytes to a file
    output_path = Path('logo_optimized_from_memory.png')
    output_path.write_bytes(optimized_png_bytes)
    print(f"Successfully optimized image from memory and saved to {output_path}")

except FileNotFoundError:
    print("Error: Source image file 'logo_source.png' not found.")
except ImportError:
    print("Error: Pillow or NumPy not installed. Run 'pip install pillow numpy'")
except oxipng.PngError as e:
    print(f"Error during in-memory optimization: {e}")
except Exception as e:
    print(f"An unexpected error occurred: {e}")

```

This approach is useful for integrating optimization directly into image processing workflows, avoiding disk I/O for intermediate steps.

<span aria-hidden="true" id="minimal-benchmark-script"></span>

## Minimal benchmark script

To get a feel for the performance on your own images, use a simple benchmark script:

```python
import time
import oxipng
import shutil
import pathlib
import os

# Define the source file for the benchmark
source_file = pathlib.Path('sample.png')

# Check if the source file exists
if not source_file.exists():
    print(f"Error: Benchmark file '{source_file}' not found. Please create a sample PNG file named 'sample.png'.")
    exit()

target_file = pathlib.Path('sample_optimized_benchmark.png')

# Copy the source file to keep the original intact
shutil.copy(source_file, target_file)

original_size = source_file.stat().st_size

print(f"Optimizing '{target_file.name}' (Level 4)...")
start_time = time.perf_counter()
try:
    # Optimize the copied file in place
    oxipng.optimize(target_file, target_file, level=4)
    end_time = time.perf_counter()
    optimized_size = target_file.stat().st_size
    size_diff = original_size - optimized_size
    time_taken = end_time - start_time

    print(f"Original size: {original_size / 1024:.1f} KiB")
    print(f"Optimized size: {optimized_size / 1024:.1f} KiB")
    if original_size > 0:
        print(f"Saved: {size_diff / 1024:.1f} KiB ({size_diff / original_size * 100:.1f}% reduction)")
    else:
        print("Saved: 0.0 KiB (Original size was 0)")
    print(f"Time taken: {time_taken:.3f} seconds")

except oxipng.PngError as e:
    print(f"Optimization failed: {e}")
except Exception as e:
    print(f"An unexpected error occurred: {e}")
finally:
    # Clean up the copied benchmark file
    if target_file.exists():
        try:
            os.remove(target_file)
            print(f"Cleaned up '{target_file.name}'.")
        except OSError as e:
            print(f"Error removing benchmark file '{target_file.name}': {e}")

```

Run this script with various PNG files and optimization levels (`level=...`) to gather real-world performance data relevant to your assets.

<span aria-hidden="true" id="troubleshooting"></span>

## Troubleshooting

Here are some common issues and potential solutions when using `pyoxipng`:

|Symptom|Likely Cause|Fix|
|-|-|-|
|ModuleNotFoundError: No module named 'oxipng'|pyoxipng not installed or venv inactive|Activate virtual environment (source venv/bin/activate or .\venv\Scripts\activate) then pip install pyoxipng|
|oxipng.PngError: Not a PNG file|Input file is corrupted or not PNG|Validate input files; ensure they are valid PNGs before processing. Use libraries like Pillow to check format if needed.|
|oxipng.PngError: Failed to parse header (IHDR)|Input file is corrupted or not PNG|Check file integrity. Skip files that fail validation.|
|High memory usage or crashes on large images|High level or too many threads|Lower optimization level, reduce max\_workers in ThreadPoolExecutor, or process large files sequentially. Consider system memory limits.|
|FileNotFoundError|Input file path is incorrect|Verify that the input file path exists and is accessible by the script. Check relative vs. absolute paths.|

<span aria-hidden="true" id="error-handling-template"></span>

## Error handling Template

Robust applications should include error handling to manage potential issues during optimization:

```python
import oxipng
import sys
from pathlib import Path

input_path = Path('image.png')
output_path = Path('image_optimized.png')

try:
    print(f"Attempting to optimize '{input_path}' to '{output_path}'...")
    oxipng.optimize(input_path, output_path, level=4)
    print(f"Successfully optimized.")

except oxipng.PngError as exc:
    # Handle specific Oxipng errors (e.g., invalid PNG format, processing errors)
    sys.exit(f'Optimization failed for {input_path}: {exc}')

except FileNotFoundError:
    # Handle case where the input file doesn't exist
    sys.exit(f'Error: Input file not found at {input_path}')

except PermissionError:
    # Handle case where script lacks permissions to read input or write output
    sys.exit(f'Error: Permission denied for file operations involving {input_path} or {output_path}')

except Exception as exc:
    # Catch any other unexpected errors during the process
    sys.exit(f'An unexpected error occurred during optimization: {exc}')

```

This structure helps prevent crashes and provides informative messages if optimization fails for various reasons.

<span aria-hidden="true" id="managed-alternative"></span>

## Managed alternative

If you prefer not to manage the optimization process yourself, cloud-based services like Transloadit can handle it automatically. Transloadit's [🤖 /image/optimize](/services/image-processing.md) Robot can optimize PNGs (and other formats) as part of an upload and processing workflow:

```json
{
  "steps": {
    "optimized": {
      "robot": "/image/optimize",
      "use": ":original",
      "priority": "compression-ratio",
      "progressive": false,
      "preserve_meta_data": true
    }
  }
}

```

This Assembly Step takes the original uploaded file (`:original`) and applies optimization using the `/image/optimize` Robot, prioritizing the compression ratio while preserving metadata.

<span aria-hidden="true" id="wrap-up"></span>

## Wrap-up

Using `pyoxipng`, you can easily integrate high-performance, lossless PNG optimization into your Python projects. Whether you're batch-processing assets during a build step, optimizing user uploads on the fly, or integrating with complex image processing pipelines, `pyoxipng` provides a fast and efficient solution based on the modern Oxipng engine. This helps you deliver leaner web pages and applications with improved loading times. For a fully managed solution, consider using a service like Transloadit to handle image optimization automatically as part of your file processing workflows.

\#python#image-processing#png-optimization#optipng#pyoxipng#image-processing-service

### 👩‍💻 Join 20k+ developers

Sign up for our [monthly newsletter](/newsletters.md) to receive direct links to 3 exclusive tech — and 2 product updates. No less, no more.

Your email:

Get access

## File uploading and encoding. Made simple.

Transloadit streamlines file handling for developers, trusted by brands like Coursera and The New York Times. We’re known for a reliable API, top-notch support, and a strong commitment to open source, with projects like [Uppy⁠](https://uppy.io) and [Tus⁠](https://tus.io) setting standards in file processing.

[Sign up](/c/)[Book a Demo](https://survey.typeform.com/to/kRg47Xi5)

No credit card needed · 5 GB included in the free plan

Cancel anytime
