Effiziente PNG-Optimierung in Python mit Oxipng
Komprimieren Sie mit pyoxipng eine PNG-Datei im Arbeitsspeicher und speichern
Sie sie nur, wenn das Ziel noch nicht existiert. Diese Anleitung liefert ein Skript für einzelne
Dateien und kleine Stapel, mit gemessenen Größen, Exit-Codes ungleich null bei Fehlern und einer
separaten Prüfung, ob die decodierten Pixel unverändert bleiben.
OptiPNG im Vergleich zu Oxipng
OptiPNG und
Oxipng sind separate PNG-Optimierer. Das hier verwendete Python-Paket,
pyoxipng, bindet die Rust-Bibliothek von Oxipng ein und wird als
oxipng importiert. Sie benötigen weder eine ausführbare OptiPNG-Datei noch
einen Unterprozess, um es aufzurufen.
Entscheiden, was erhalten bleiben soll
Verlustfreie Optimierung kann Komprimierung, Palette oder Farbtyp einer PNG-Datei ändern und dabei ihre decodierten Pixel erhalten. Sie verspricht keine bestimmte prozentuale Reduzierung. Eine bereits kompakte Datei wird möglicherweise gar nicht kleiner.
Das Beispiel lässt Quelldateien unverändert, lehnt vorhandene Ziele ab und fordert keine Entfernung
von Metadaten an. Mit optimize_alpha=False bleiben auch die Farbwerte transparenter Pixel
unverändert. Verwenden Sie es für lokale statische PNG-Dateien in Verzeichnissen, die Sie
kontrollieren. Die Pixelprüfung weiter unten deckt 8-Bit-RGB- und RGBA-Eingaben ab; sie validiert
weder Animationen noch 16-Bit-Präzision oder das Erscheinungsbild mit Farbmanagement.
Voraussetzungen
Die folgenden Befehle verwenden Bash und Python 3.12 mit venv und
pip. Sie wurden unter Linux mit Python 3.12.13 und
pyoxipng 9.1.1 getestet. Beginnen Sie in einem Verzeichnis, in dem
png-demo noch nicht existiert:
mkdir png-demo &&
cd png-demo &&
python3.12 -m venv .venv &&
.venv/bin/python -m pip install --only-binary=:all: pyoxipng==9.1.1 Pillow==12.3.0
Die Verkettung mit && stoppt die Einrichtung, wenn ein Schritt
fehlschlägt, auch wenn das Verzeichnis png-demo bereits existiert. Die Option
--only-binary erfordert ein kompatibles Wheel, statt einen Build aus dem
Rust-Quellcode zu starten.
PyPI listet die verfügbaren Wheels auf. Pillow wird nur für die
Pixelprüfung verwendet, nicht für die Optimierung. Führen Sie die übrigen Befehle aus
png-demo aus.
Eine einzelne PNG-Datei optimieren
Speichern Sie Folgendes als optimize_png.py. Das Skript übergibt codierte PNG-Bytes an
optimize_from_memory,
baut das Bild also nicht aus einem Pixel-Array von Pillow oder NumPy neu auf.
import argparse
from pathlib import Path
import sys
from time import perf_counter
import oxipng
def optimize_bytes(data: bytes, level: int) -> bytes:
candidate = oxipng.optimize_from_memory(
data,
level=level,
strip=oxipng.StripChunks.none(),
optimize_alpha=False,
)
return candidate if len(candidate) < len(data) else data
def optimize_file(source: Path, destination: Path, level: int) -> None:
start = perf_counter()
original = source.read_bytes()
optimized = optimize_bytes(original, level)
# Exclusive creation fails even for a dangling destination symlink.
output = destination.open("xb")
try:
with output:
output.write(optimized)
except OSError:
# Only remove the file this invocation successfully created.
destination.unlink()
raise
saved = len(original) - len(optimized)
print(
f"{source.name}: {len(original)} -> {len(optimized)} bytes; "
f"saved {saved} bytes; {perf_counter() - start:.3f} s"
)
def main() -> int:
parser = argparse.ArgumentParser(description="Optimize PNGs without overwriting outputs")
parser.add_argument("source", type=Path, help="PNG file or directory")
parser.add_argument("destination", type=Path, help="New PNG file or output directory")
parser.add_argument("--level", type=int, choices=range(7), default=2)
args = parser.parse_args()
try:
source = args.source.resolve(strict=True)
if source.is_dir():
if source == args.destination.resolve():
raise ValueError("Source and output directories must differ")
files = sorted(
path for path in source.iterdir()
if path.suffix.lower() == ".png" and not path.is_symlink() and path.is_file()
)
if not files:
raise ValueError("No PNG files found")
args.destination.mkdir(exist_ok=True)
pairs = [(path, args.destination / path.name) for path in files]
else:
pairs = [(source, args.destination)]
except (OSError, ValueError) as error:
parser.exit(1, f"Error: {error}\n")
failed = False
for input_path, output_path in pairs:
try:
optimize_file(input_path, output_path, args.level)
except (OSError, oxipng.PngError) as error:
print(f"Failed {input_path.name}: {type(error).__name__}: {error}", file=sys.stderr)
failed = True
return int(failed)
if __name__ == "__main__":
sys.exit(main())
Legen Sie eine PNG-Datei namens input.png neben das Skript und führen Sie dann
Folgendes aus:
.venv/bin/python optimize_png.py input.png output.png
Bei Erfolg gibt es die Byte-Anzahl von Eingabe und Ausgabe, die eingesparten Bytes und die
verstrichenen Sekunden einschließlich Datei-E/A aus. Eine Einsparung von null Bytes ist gültig:
Das Skript schreibt eine Kopie des Originals, wenn der Kandidat nicht kleiner ist. Ein erneuter
Aufruf desselben Befehls meldet FileExistsError, endet mit Status 1 und lässt
output.png unverändert. Auch die Eingabe selbst als Ziel zu verwenden schlägt
fehl, ohne sie zu verändern.
Pythons Modus "xb" entscheidet beim
Erstellen der Datei, dass nichts überschrieben wird. Eine beschädigte oder fehlende Eingabe führt
bereits davor zum Fehler. Wenn beim Schreiben oder Schließen der neu erstellten Datei
OSError ausgelöst wird, versucht das Skript, diese unvollständige Ausgabe zu
entfernen. Dies ist ein lokales Skript, kein atomarer Veröffentlichungsmechanismus: Ein erzwungener
Abbruch kann eine unvollständige Datei hinterlassen. Ein anderer Prozess sollte das Ziel erst nach
erfolgreichem Abschluss verwenden.
Ein Verzeichnis optimieren
Legen Sie Ihre PNG-Dateien in images_source ab und verwenden Sie dasselbe Skript:
.venv/bin/python optimize_png.py images_source images_optimized --level 2
Es erstellt bei Bedarf images_optimized und verarbeitet nur reguläre Dateien direkt
im Quellverzeichnis, die auf .png enden, unabhängig von Groß- und
Kleinschreibung. Unterverzeichnisse und symbolische Links werden übersprungen. Leerzeichen und
literale Zeichen % in Dateinamen werden unterstützt. Für einen
Befehlszeilenpfad, der mit - beginnt, verwenden Sie das Präfix
./ oder setzen Sie -- vor die Positionsargumente.
Jede vorhandene Ausgabe gilt als Fehler für die betreffende Datei. Andere Dateien werden dennoch verarbeitet, erfolgreiche Ausgaben bleiben erhalten, und der Stapel endet mit Status 1, wenn eine Datei fehlgeschlagen ist. Ein fehlendes Quellverzeichnis, ein leerer PNG-Bestand oder identische Quell- und Ausgabeverzeichnisse führen vor der Verarbeitung zum Fehler. Um den gesamten Stapel erneut auszuführen, wählen Sie ein neues Ausgabeverzeichnis.
Diese Schleife verarbeitet jeweils ein Bild, um die Anzahl gleichzeitig im Arbeitsspeicher vorliegender decodierter Bilder zu begrenzen. Oxipng selbst unterstützt Multithreading; für diese Arbeitslast ist keine Beschleunigung durch einen zusätzlichen Python-Thread-Pool nachgewiesen. Eingabebytes, Ausgabebytes und der Arbeitsbereich des Optimierers müssen für jedes Bild weiterhin in den Arbeitsspeicher passen.
Die passende Optimierungsstufe wählen
Die Anbindung akzeptiert Stufen von 0 bis 6, mit 2 als Standardwert. Das sind Voreinstellungen für die Suche, keine Qualitätseinstellungen oder garantierten Einsparungen. Beginnen Sie mit 2 und vergleichen Sie die Ergebnisse anhand Ihrer eigenen Dateien.
| Stufe | Ausprobieren für |
|---|---|
| 0 | Einen schnellen Ausgangswert |
| 2 | Routinemäßige Builds von Assets |
| 4 | Einen Vergleich mit höherem Suchaufwand |
| 6 | Einen Vergleich, bei dem die Laufzeit weniger zählt |
Messen, ohne Benchmark-Dateien zu erstellen
Speichern Sie dies als benchmark_png.py neben optimize_png.py und führen
Sie dann .venv/bin/python benchmark_png.py input.png aus. Es liest eine Datei, testet vier Stufen und schreibt
nur ins Terminal. Es gibt keinen temporären Ausgabedateinamen, der mit Ihren Dateien kollidieren
könnte.
import argparse
from pathlib import Path
import sys
from time import perf_counter
import oxipng
from optimize_png import optimize_bytes
parser = argparse.ArgumentParser()
parser.add_argument("source", type=Path)
args = parser.parse_args()
try:
data = args.source.read_bytes()
for level in (0, 2, 4, 6):
start = perf_counter()
result = optimize_bytes(data, level)
print(f"level={level}: {len(data)} -> {len(result)} bytes; {perf_counter() - start:.3f} s")
except (OSError, oxipng.PngError) as error:
sys.exit(f"Benchmark failed: {error}")
Jede Stufe beginnt mit den ursprünglichen Bytes. Die Zeitmessung umfasst die Optimierung, nicht aber das anfängliche Einlesen der Datei. Wiederholen Sie die Messung mit repräsentativen Dateien; eine einzelne Messung enthält Effekte durch Aufwärmphasen und Systemlast. Eine höhere Voreinstellung muss nicht bei jedem Bild ein kleineres Ergebnis liefern.
Metadaten erhalten oder entfernen
StripChunks.none() fordert keine Entfernung von Metadaten
an. Das ist nicht dasselbe, wie jeden Chunk Byte für Byte zu erhalten: Oxipng kann
Chunks entfernen, die durch eine Änderung des Farbtyps oder der Bittiefe ungültig werden.
Das Skript erstellt außerdem eine neue Datei im Dateisystem und kopiert daher weder Zeitstempel
noch Berechtigungen der Quelldatei.
StripChunks.safe() entfernt Metadaten, die für die Darstellung als unnötig gelten,
einschließlich Textkommentaren.
StripChunks.all() kann auch Farbprofile und Animations-Chunks entfernen. Verwenden Sie
keine dieser beiden Alternativen, wenn Ihre Aufgabe den Erhalt dieser Informationen erfordert.
Weder eine kleinere Datei noch ein identischer Pixelpuffer beweist, dass Metadaten erhalten
geblieben sind. Prüfen Sie die konkreten Felder, die Ihre Anwendung benötigt.
Decodierte Pixel vergleichen
Speichern Sie dies als check_pixels.py und führen Sie .venv/bin/python check_pixels.py input.png output.png aus.
Bei statischen 8-Bit-RGB/RGBA-Quell-PNGs vergleicht es Abmessungen und decodierte RGBA-Bytes,
einschließlich der Farbwerte vollständig transparenter Pixel. Eine optimierte Datei kann eine
Palette verwenden, daher werden beide Bilder vor dem Vergleich in einen gemeinsamen Modus
decodiert.
import argparse
import sys
from PIL import Image
parser = argparse.ArgumentParser()
parser.add_argument("source")
parser.add_argument("output")
args = parser.parse_args()
try:
with Image.open(args.source) as before, Image.open(args.output) as after:
if before.format != "PNG" or after.format != "PNG":
raise ValueError("Both files must be PNGs")
if before.n_frames != 1 or after.n_frames != 1:
raise ValueError("This check covers static PNGs only")
if before.mode not in ("RGB", "RGBA"):
raise ValueError("Use an 8-bit RGB or RGBA source for this check")
if before.size != after.size or before.convert("RGBA").tobytes() != after.convert("RGBA").tobytes():
raise ValueError("Decoded pixels differ")
print("Decoded RGBA pixels match")
except (OSError, ValueError) as error:
sys.exit(f"Pixel check failed: {error}")
tobytes() von Pillow
liefert decodierte Pixeldaten. Dieser Vergleich wendet keine ICC-Profile an und vergleicht weder
Text noch EXIF, Auflösung oder andere Metadaten. Verwenden Sie wie angegeben 8-Bit-Quelldateien:
Pillows RGB-Modus allein belegt nicht die Bittiefe der ursprünglichen PNG-Datei.
Fehlerbehebung
| Fehler | Maßnahme |
|---|---|
ModuleNotFoundError | Verwenden Sie .venv/bin/python des Projekts und wiederholen Sie dort den Installationsschritt. |
FileExistsError | Wählen Sie ein neues Ziel; prüfen Sie eine alte Ausgabe, bevor Sie sie selbst entfernen. |
FileNotFoundError | Prüfen Sie die Quelle und das übergeordnete Verzeichnis der Ausgabe. |
PngError | Prüfen Sie, ob die Eingabe eine vollständige, gültige PNG-Datei ist. |
PermissionError | Wählen Sie ein lesbares Quellverzeichnis und ein beschreibbares Ausgabeverzeichnis. |
Ein fehlgeschlagener Stapel kann neben Fehlern auch erfolgreiche Ausgaben hinterlassen. Lesen Sie stderr, um die betroffenen Dateinamen zu ermitteln; der Exit-Status beschreibt den Stapel, keine Rücknahme der Änderungen. Wenn der Speicherverbrauch zu hoch ist, behalten Sie die sequenzielle Schleife bei und testen Sie kleinere Bilder, bevor Sie die Optimierungsstufe erhöhen.
Verwaltete Alternative
Für einen von Transloadit verwalteten Upload-Workflow lesen Sie die Dokumentation zu /image/optimize und wählen Sie dessen Metadatenrichtlinie unabhängig von den Einstellungen des lokalen Skripts.
