Effiziente JPEG-Optimierung in PHP mit jpegoptim
Führen Sie jpegoptim auf einer temporären Kopie aus, prüfen Sie den Exit-Status und veröffentlichen Sie das Ergebnis nur, wenn der gewählte Ausgabename noch verfügbar ist. Dieses PHP-CLI-Beispiel lässt Ihr Quell-JPEG unverändert und verweigert das Ersetzen einer vorhandenen Ausgabe, auch wenn die Eingabe beschädigt ist.
Wählen Sie, was geändert werden soll
Verlustfreie JPEG-Optimierung ordnet komprimierte Daten neu an, ohne sie erneut mit Qualitätsverlust zu codieren. Das bedeutet nicht, dass die resultierende Datei bytegleich ist. Das folgende Skript bietet drei separate Modi:
| Modus | jpegoptim-Optionen | Beabsichtigte Änderung |
|---|---|---|
lossless (Standard) | --strip-none | Kompression optimieren und Metadaten beibehalten. |
strip | --strip-all --force | Metadaten entfernen, ohne die JPEG-Qualität zu reduzieren. |
quality80 | --strip-none --max=80 | Qualitätsreduktion erlauben und Metadaten beibehalten. |
Das jpegoptim-Handbuch beschreibt
--max als Qualitätsobergrenze, nicht als prozentuale Reduktion der
Dateigröße. Ein JPEG, dessen Qualität bereits darunter liegt, kann verlustfrei verarbeitet werden.
Vermeiden Sie --max und --size, wenn die Bildqualität
erhalten bleiben muss.
Über Metadaten müssen Sie separat entscheiden: Beim Entfernen von EXIF kann die Ausrichtung
verloren gehen, und das Entfernen von ICC-Profilen kann die Farbwiedergabe verändern. Verwenden
Sie strip nur, wenn diese Änderungen akzeptabel sind. Die Option erzwingt
ein Neuschreiben, damit die Metadatenentfernung nicht wegen fehlender Größenersparnis übersprungen
wird. Selbst --strip-all kann JFIF- oder Adobe-Marker zurücklassen, die der
Encoder erzeugt hat; --strip-none erlaubt außerdem, diese Marker neu zu erzeugen.
Keiner der beiden Modi verspricht eine bytegetreue Archivierung der Metadaten.
Installieren Sie die CLI-Werkzeuge
Verwenden Sie ein lokales Linux-Verzeichnis, das Sie kontrollieren, mit Platz für eine temporäre
Kopie und einem Dateisystem, das Hardlinks unterstützt. Sie benötigen PHP CLI mit aktiviertem
proc_open() und die ausführbare Datei von jpegoptim.
Installieren Sie beides unter Ubuntu 24.04 mit APT:
sudo apt-get update &&
sudo apt-get install -y php-cli jpegoptim &&
php --version &&
jpegoptim --version
Ubuntu 24.04 enthält jpegoptim 1.4.7 als Paket, das die hier verwendeten Optionen unterstützt. Dieser Ablauf wurde mit PHP 8.3 und jpegoptim 1.4.7 sowie mit PHP 8.5.10 und jpegoptim 1.5.6 getestet. Er benötigt weder Composer noch GD noch einen Webserver.
Erstellen Sie ein neues Arbeitsverzeichnis:
mkdir jpeg-demo && cd jpeg-demo
Wenn dieser Befehl fehlschlägt, brechen Sie ab und wählen Sie einen noch nicht verwendeten
Verzeichnisnamen. Legen Sie ein eigenes JPEG als input.jpg in diesem
Verzeichnis ab. Speichern Sie das folgende Skript daneben als optimize-jpeg.php.
Schreiben Sie eine separate Ausgabe mit PHP
Das Ausgabeverzeichnis muss bereits existieren. Das Skript prüft vor der Verarbeitung auf
Namenskonflikte und gibt der fertiggestellten temporären Datei anschließend mit der
PHP-Funktion link() ihren endgültigen Namen.
Dieser Vorgang verweigert auch eine Ausgabe, die während der Ausführung von jpegoptim angelegt
wurde. Beim Aufräumen wird nur der temporäre Name entfernt.
<?php
declare(strict_types=1);
function main(array $args): void
{
if (count($args) < 3 || count($args) > 4) {
throw new InvalidArgumentException(
'Usage: php optimize-jpeg.php INPUT OUTPUT [lossless|strip|quality80]'
);
}
$options = match ($args[3] ?? 'lossless') {
'lossless' => ['--strip-none'],
'strip' => ['--strip-all', '--force'],
'quality80' => ['--strip-none', '--max=80'],
default => throw new InvalidArgumentException('Unknown optimization mode.'),
};
$binary = getenv('JPEGOPTIM_BINARY') ?: '/usr/bin/jpegoptim';
if (!str_starts_with($binary, '/') || !is_file($binary) || !is_executable($binary)) {
throw new RuntimeException('Set JPEGOPTIM_BINARY to an executable absolute path.');
}
$input = realpath($args[1]);
if ($input === false || !is_file($input) || !is_readable($input)) {
throw new RuntimeException('Input must be a readable local file.');
}
$directory = realpath(dirname($args[2]));
if ($directory === false || !is_dir($directory) || !is_writable($directory)) {
throw new RuntimeException('Output directory must exist and be writable.');
}
$output = $directory . '/' . basename($args[2]);
if (file_exists($output) || is_link($output)) {
throw new RuntimeException('Output already exists; choose a new filename.');
}
$before = filesize($input);
if ($before === false || $before === 0) {
throw new RuntimeException('Input is empty or its size cannot be read.');
}
$temporary = tempnam($directory, '.jpegoptim-');
if ($temporary === false) {
throw new RuntimeException('Cannot create a temporary file.');
}
try {
// tempnam can fall back to the system temp directory; keep publication on one filesystem.
if (dirname($temporary) !== $directory || !copy($input, $temporary)) {
throw new RuntimeException('Cannot create the working copy in the output directory.');
}
$process = proc_open(
[$binary, '--nofix', '--quiet', ...$options, '--', $temporary],
[0 => ['file', '/dev/null', 'r'], 1 => STDERR, 2 => STDERR],
$pipes
);
if (!is_resource($process)) {
throw new RuntimeException('Cannot start jpegoptim.');
}
$status = proc_close($process);
if ($status !== 0) {
throw new RuntimeException("jpegoptim failed (exit $status); no output published.");
}
clearstatcache(true, $temporary);
$after = filesize($temporary);
if ($after === false || $after === 0) {
throw new RuntimeException('jpegoptim did not leave a nonempty output.');
}
if (!link($temporary, $output)) {
throw new RuntimeException('Cannot publish output; check for a collision or filesystem error.');
}
} finally {
unlink($temporary);
}
printf("Created %s: %d -> %d bytes; saved %d bytes\n", $output, $before, $after, $before - $after);
}
try {
main($argv);
} catch (Throwable $error) {
fwrite(STDERR, 'Error: ' . $error->getMessage() . "\n");
exit(1);
}
proc_open() akzeptiert ein Argument-Array,
sodass Pfade nicht zu Shell-Befehlen werden. jpegoptim erhält nur die temporäre Datei, niemals
die Quelle oder das endgültige Ziel. Die Option --nofix weist sowohl
Dekompressionswarnungen als auch Fehler zurück, statt ein beschädigtes JPEG zu reparieren.
Eine MIME-Prüfung allein würde nicht belegen, dass das gesamte Bild decodiert werden kann.
Führen Sie den standardmäßigen verlustfreien Modus aus:
php optimize-jpeg.php input.jpg lossless.jpg
Das Skript verwendet /usr/bin/jpegoptim, den Speicherort des Ubuntu-Pakets. Wenn Sie es
an anderer Stelle installiert haben, setzen Sie JPEGOPTIM_BINARY für den Aufruf auf
den absoluten Pfad. Übernehmen Sie diese Einstellung oder beliebige Optimierungsoptionen nicht
aus einer HTTP-Anfrage.
Ein erfolgreicher Durchlauf endet mit Exit-Status null und gibt den Ausgabepfad, die Byteanzahl von
Eingabe und Ausgabe sowie die eingesparten Bytes aus. Eine Ersparnis von null ist gültig: Sie
erhalten trotzdem eine separate Ausgabe. Eine negative Ersparnis bedeutet, dass die Ausgabe
größer geworden ist, was der erzwungene Modus strip erlaubt. Ein erneuter
Aufruf mit demselben Ausgabenamen schlägt fehl, ohne diese Datei zu verändern. Auch die Verwendung
des Eingabenamens als Ausgabe schlägt fehl.
Eine beschädigte Eingabe, eine fehlende ausführbare Datei oder ein fehlgeschlagener jpegoptim-Prozess führt zu einem Exit-Status ungleich null. Bei diesen Fehlern wird keine endgültige Ausgabe veröffentlicht, und die temporäre Kopie wird bei der normalen Ausnahmebehandlung entfernt. Vorhandene Ausgaben und die Quelle bleiben erhalten.
Kompressionsergebnisse messen
Vergleichen Sie die anderen Modi mit neuen Ausgabenamen:
php optimize-jpeg.php input.jpg stripped.jpg strip &&
php optimize-jpeg.php input.jpg quality80.jpg quality80
Notieren Sie die Byteanzahl zusammen mit den installierten Versionen und dem Modus. Vergleichen Sie repräsentative Fotos, kleine Thumbnails und Bilder, die bereits mit einem anderen Werkzeug optimiert wurden; eine Ersparnis bei einem Bild lässt keine Rückschlüsse auf ein anderes zu. Das Skript ändert keine Bildabmessungen und garantiert keine Zieldateigröße.
Öffnen Sie die Ausgaben in einem Bildbetrachter. Prüfen Sie bei quality80 feine
Details, Farbverläufe und Text in voller Größe, bevor Sie die Qualitätsänderung akzeptieren.
Vergleichen Sie bei strip Ausrichtung und Farben mit dem Original. Wenn
Sie eine Prüfung auf Verlustfreiheit automatisieren, decodieren Sie Quelle und Ausgabe mit
demselben Decoder und vergleichen Sie Pixelpuffer und Abmessungen; ein Vergleich der Hashes der
JPEG-Dateien prüft dagegen die Bytegleichheit.
Rufen Sie das Skript aus einem Worker auf
Behalten Sie bei einem Batch-Job oder einer Laravel-Anwendung das Original und weisen Sie jedem Bild ein neues Ziel zu. Führen Sie diesen CLI-Vorgang in einem Worker aus und prüfen Sie den Exit-Status, bevor Sie eine Ausgabe als verfügbar vermerken. Bei einem fehlgeschlagenen Job sollte die Quelle für Diagnose oder einen erneuten Versuch erhalten bleiben. Auch ein Composer-Wrapper benötigt die native ausführbare Datei und eine explizite Regelung für Fehler und Zielkonflikte.
Sicherheitsaspekte
Dieses Beispiel verarbeitet lokale Dateien in Verzeichnissen, die unter der Kontrolle desselben
vertrauenswürdigen Kontos stehen. Es ist weder ein Upload-Endpunkt noch eine Sandbox für
bösartige Dateien. Die Ausgabeberechtigungen bleiben privat (die temporäre Datei beginnt mit
Modus 0600); richten Sie etwaige spätere
Berechtigungen zur Bereitstellung im Web bewusst ein.
Ein Upload-Dienst benötigt außerdem Byte- und Pixelgrenzen, isolierte Verarbeitung und eine
Zeitgrenze im Worker, die den externen Prozess beendet. Die PHP-Funktion
set_time_limit() begrenzt unter Linux nicht die Zeit,
die externe Vorgänge beanspruchen. Dieses kleine CLI-Skript hat kein Prozess-Timeout, und ein
erzwungenes Beenden kann temporäre Dateien zurücklassen. Halten Sie das Arbeitsverzeichnis privat
und lassen Sie den Worker, dem diese Dateien gehören, nach unterbrochenen Jobs aufräumen.
