Effiziente PNG-Optimierung in Python mit Oxipng
Die Optimierung von Bildern ist entscheidend, um die Web-Performance zu verbessern und den
Bandbreitenverbrauch zu senken. PNG-Dateien sind verlustfrei und von hoher Qualität, bringen aber
oft überflüssige Metadaten und eine suboptimale Komprimierung mit. In diesem DevTip zeigen wir, wie
Sie PNGs aus Python heraus verkleinern, indem Sie die moderne, in Rust entwickelte Oxipng-Engine
über deren aktiv gepflegtes Python-Binding pyoxipng ansprechen.
Warum PNG-Bilder optimieren?
Kleinere Bilder bedeuten schnellere Seitenladezeiten, bessere Core Web Vitals, niedrigere CDN-Rechnungen und zufriedenere Nutzer. In Mobilfunknetzen kann eine deutliche Reduzierung der Dateigröße den Unterschied zwischen einer flüssigen Oberfläche und einer Wartezeit ausmachen, die zum Absprung führt. Eine effektive PNG-Optimierung kann Dateigrößen oft um 15-40 % verringern, ohne dass die visuelle Qualität leidet.
OptiPNG vs. Oxipng
OptiPNG ist das klassische, in C geschriebene Kommandozeilen-Werkzeug für die verlustfreie PNG-Komprimierung. Seit Jahren ist es eine zuverlässige Wahl. Oxipng ist dagegen eine moderne Neuimplementierung in Rust, die auf Geschwindigkeit und Effizienz ausgelegt ist. Es ist vollständig kompatibel mit den verlustfreien Optimierungen von OptiPNG, ergänzt sie aber um Multithreading, SIMD-Beschleunigung und verbesserte Heuristiken für potenziell bessere Komprimierungsraten und schnellere Ausführung.
Die Bibliothek pyoxipng bietet pythonischen Zugriff auf den in Rust
geschriebenen Oxipng-Kern, sodass Sie dessen Performance direkt in Ihren Python-Anwendungen nutzen
können, ohne externe Prozesse verwalten zu müssen. Dieser Beitrag konzentriert sich wegen ihrer
Performance und aktiven Pflege auf pyoxipng; die Optimierungsprinzipien ähneln
jedoch denen des ursprünglichen OptiPNG.
Voraussetzungen
Stellen Sie vor dem Start sicher, dass die erforderlichen Werkzeuge installiert sind:
# Check Python version (3.8+ recommended)
python --version
# Install pyoxipng and optional dependencies
pip install pyoxipng pillow numpy
- Python: Verwenden Sie eine unterstützte Python-3-Version.
- pyoxipng: Die zentrale Bibliothek für die PNG-Optimierung (
pip install pyoxipng). Version 9.1.0 oder neuer wird empfohlen. - Pillow: Wird in den fortgeschrittenen Beispielen zum Laden und Bearbeiten von Bildern
verwendet (
pip install pillow). - NumPy: Erforderlich für die Array-Verarbeitung beim Umgang mit rohen Bilddaten
(
pip install numpy).
Eine einzelne PNG-Datei optimieren
Das Optimieren einer einzelnen Datei gelingt mit der Funktion optimize ganz
einfach:
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.")
Mit den Standardeinstellungen (level=2) können Sie in der Regel eine
Reduzierung der Dateigröße um 15–25 % erwarten, oft innerhalb von Millisekunden. Ein höheres Level
verbessert die Komprimierung, dauert aber länger.
Stapeloptimierung mit Threads
Beim Optimieren mehrerer Bilder kann der Einsatz von Threads den Vorgang erheblich beschleunigen,
insbesondere auf Mehrkernsystemen. pyoxipng ist thread-sicher.
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.")
Setzen Sie die Option max_workers in ThreadPoolExecutor (z. B. auf
os.cpu_count()), um den Durchsatz zu maximieren. Auf einem Rechner mit 8 Kernen kann
die Verarbeitung von Hunderten kleiner PNG-Dateien nur wenige Sekunden dauern.
Das richtige Optimierungslevel wählen
pyoxipng bietet Optimierungslevel von 0 bis 6. Höhere Level liefern kleinere
Dateien, benötigen aber mehr Rechenzeit. Die Werte in der Spalte „Ersparnis“ sind typische
Richtwerte, keine garantierten Ergebnisse.
| Level | Ersparnis | Tempo | Einsatz |
|---|---|---|---|
| 0 | 5-10 % | Sehr schnell | Schnelle Checks, Dev-Builds mit Tempo |
| 2 (Standard) | 15-25 % | Schnell | Übliche Web-Assets, gute Balance |
| 4 | 20-35 % | Mittel | Produktion, Fokus Größe |
| 6 | 25-40 % | Sehr langsam | Finale Archive, einmalige Läufe |
Wählen Sie das Level, das Komprimierungsbedarf und verfügbare Rechenzeit für Ihren Anwendungsfall am besten ausbalanciert.
Metadaten erhalten oder entfernen
Standardmäßig fordert pyoxipng kein Entfernen von Metadaten an. Steuern Sie
das explizite Entfernen mit strip:
import oxipng
try:
# Strip all possible metadata for maximum size reduction
oxipng.optimize(
'input_with_meta.png',
'output_stripped.png',
level=4,
strip=oxipng.StripChunks.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.StripChunks.none() # Do not request metadata stripping
)
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.")
Verwenden Sie strip=oxipng.StripChunks.all(), um zusätzliche Chunks zu entfernen, einschließlich
Farbprofilen und Animationsdaten. Das kann das Erscheinungsbild eines Bildes verändern oder eine
Animation entfernen. Verwenden Sie strip=oxipng.StripChunks.safe(), um renderingrelevante Chunks zu
behalten, oder strip=oxipng.StripChunks.none() (den Standard), um kein Entfernen von Metadaten
anzufordern. Chunks, die nach einer Reduzierung des Farbtyps oder der Bittiefe ungültig werden,
können dennoch entfernt werden.
Fortgeschritten: Optimierungs-Pipelines im Arbeitsspeicher
Wenn Sie Bilder im Arbeitsspeicher erzeugen oder bearbeiten (z. B. mit Pillow), können Sie die rohen Pixeldaten direkt optimieren, ohne Zwischendateien zu schreiben. Dafür müssen Sie die Bildabmessungen und Informationen zum Farbtyp angeben.
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}")
Dieser Ansatz eignet sich, um die Optimierung direkt in Workflows der Bildverarbeitung einzubinden und Festplatten-I/O für Zwischenschritte zu vermeiden.
Minimales Benchmark-Skript
Um ein Gefühl für die Performance mit Ihren eigenen Bildern zu bekommen, verwenden Sie ein einfaches Benchmark-Skript:
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}")
Führen Sie dieses Skript mit verschiedenen PNG-Dateien und Optimierungsleveln
(level=...) aus, um Performance-Daten aus der Praxis für Ihre eigenen Assets zu
sammeln.
Fehlerbehebung
Hier sind einige häufige Probleme und mögliche Lösungen bei der Verwendung von
pyoxipng:
| Symptom | Mögliche Ursache | Lösung |
|---|---|---|
ModuleNotFoundError: No module named 'oxipng' | pyoxipng nicht installiert oder venv nicht aktiv | Virtuelle Umgebung aktivieren (source venv/bin/activate oder .\venv\Scripts\activate), dann pip install pyoxipng |
oxipng.PngError: Not a PNG file | Datei beschädigt oder kein PNG | Eingaben prüfen; vor der Verarbeitung nur gültige PNGs zulassen. Format bei Bedarf mit Pillow prüfen. |
oxipng.PngError: Failed to parse header (IHDR) | Datei beschädigt oder kein PNG | Dateiintegrität prüfen. Ungültige Dateien überspringen. |
| Hoher Speicherbedarf oder Abstürze bei großen Bildern | Level zu hoch oder zu viele Threads | Optimierungslevel level senken, max_workers in ThreadPoolExecutor reduzieren oder große Dateien nacheinander verarbeiten. Speichergrenzen des Systems beachten. |
FileNotFoundError | Pfad zur Eingabedatei falsch | Prüfen, ob der Pfad existiert und für das Skript zugänglich ist. Relative gegenüber absoluten Pfaden prüfen. |
Vorlage für die Fehlerbehandlung
Robuste Anwendungen sollten eine Fehlerbehandlung enthalten, um mögliche Probleme während der Optimierung abzufangen:
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}')
Diese Struktur hilft, Abstürze zu vermeiden, und liefert aussagekräftige Meldungen, wenn die Optimierung aus verschiedenen Gründen fehlschlägt.
Verwaltete Alternative
Wenn Sie den Optimierungsprozess nicht selbst verwalten möchten, können Cloud-Dienste wie Transloadit ihn automatisch übernehmen. Der Robot 🤖 /image/optimize von Transloadit kann PNGs (und andere Formate) als Teil eines Upload- und Verarbeitungs-Workflows optimieren:
{
"steps": {
"optimized": {
"robot": "/image/optimize",
"use": ":original",
"priority": "compression-ratio",
"progressive": false,
"preserve_meta_data": true
}
}
}
Dieser Assembly Step nimmt die ursprünglich hochgeladene Datei (:original) und
wendet die Optimierung mit dem Robot /image/optimize an, wobei die Komprimierungsrate Vorrang
erhält und die Metadaten erhalten bleiben.
Fazit
Mit pyoxipng integrieren Sie verlustfreie PNG-Optimierung mit hoher Performance
ganz einfach in Ihre Python-Projekte. Ob Sie Assets während eines Build-Schritts im Stapel
verarbeiten, Uploads von Nutzern im laufenden Betrieb optimieren oder die Optimierung in komplexe
Pipelines der Bildverarbeitung einbinden: pyoxipng bietet eine schnelle und
effiziente Lösung auf Basis der modernen Oxipng-Engine. So liefern Sie schlankere Webseiten und
Anwendungen mit kürzeren Ladezeiten aus. Für eine vollständig verwaltete Lösung können Sie einen
Dienst wie Transloadit nutzen, der die Bildoptimierung automatisch als Teil Ihrer Workflows zur
Dateiverarbeitung übernimmt.
