Gedrehte Bild-Uploads in PHP mit jpegtran korrigieren
Um einen seitlich gedrehten JPEG-Upload zu korrigieren, transformieren Sie das gespeicherte Bild entsprechend seiner EXIF-Ausrichtung und setzen diese anschließend zurück. Der folgende PHP-Befehl speichert ein separates, korrekt ausgerichtetes JPEG ohne erneute verlustbehaftete Komprimierung. Er lehnt beschädigte Eingaben und Drehungen ab, die einen unkorrigierten Streifen am Bildrand hinterlassen würden.
EXIF-Ausrichtungsdaten verstehen
Ein EXIF-Ausrichtungs-Tag gibt einem Anzeigeprogramm vor, wie es das gespeicherte Bild darstellen
soll. Wenn Sie nur dieses Tag ändern, werden die Bilddaten nicht gedreht. Drehen Sie die Daten, ohne
das Tag zurückzusetzen, kann das Anzeigeprogramm das Bild erneut drehen. Lesen Sie die Ausrichtung
IFD0 des Hauptbildes aus, nicht die eines eingebetteten Thumbnails.
Alle acht Werte sind relevant, auch die gespiegelten Varianten. Wenden Sie die folgenden Operationen auf das gespeicherte Bild an. Die Drehwinkel werden im Uhrzeigersinn gemessen:
| EXIF-Wert | Korrektur | jpegtran-Argumente |
|---|---|---|
| 1 | Aufrecht belassen | Keine Transformation |
| 2 | Links nach rechts spiegeln | -flip horizontal |
| 3 | Auf den Kopf drehen | -rotate 180 |
| 4 | Oben nach unten spiegeln | -flip vertical |
| 5 | An der Diagonalen von oben links nach unten rechts spiegeln | -transpose |
| 6 | Um 90° drehen | -rotate 90 |
| 7 | An der Diagonalen von oben rechts nach unten links spiegeln | -transverse |
| 8 | Um 270° drehen | -rotate 270 |
Die Zuordnung folgt den Ausrichtungsdefinitionen von ExifTool. Hat das Hauptbild kein Ausrichtungs-Tag, lässt dieses Beispiel seine Geometrie unverändert. Es kann nicht erkennen, wie ein Foto ohne dieses Tag ausgerichtet sein sollte. Ein Wert außerhalb von 1–8 ist ein Fehler.
jpegtran für verlustfreie Drehungen verwenden
jpegtran ordnet quantisierte DCT-Koeffizienten neu an und vermeidet damit das
Decodieren und erneute Komprimieren von JPEGs bei einer Drehung mit GD oder ImageMagick.
„Verlustfrei“ beschreibt hier diese Koeffiziententransformation; die Bytes und Metadaten der
Ausgabedatei ändern sich.
Perfekte Transformationen hängen von den JPEG-Blockgrenzen ab.
-perfect lehnt eine nicht unterstützte Transformation ab;
-trim würde Randpixel verwerfen. Verwenden Sie zusätzlich
-strict, um Decoder-Warnungen, etwa zu abgeschnittenen Bilddaten, als Fehler
zu behandeln. Diese Optionen sind in der
Anleitung zur Verwendung von libjpeg-turbo dokumentiert.
Dieses Beispiel erstellt eine Kopie zur Anzeige. -copy icc behält das Farbprofil
bei, verwirft aber andere Metadaten der Quelle, darunter GPS, Kameradetails, Copyright-Felder, XMP,
Kommentare und eingebettete Thumbnails. ExifTool schreibt anschließend einen neuen Wert 1 für EXIF
Orientation. Bewahren Sie das Original auf, wenn Sie dessen Metadaten benötigen. Die Verwendung von
-copy all würde dagegen eine separate Regelung für veraltete Ausrichtungsfelder
und Vorschaubilder erfordern.
Die Linux-Befehlszeile vorbereiten
Verwenden Sie PHP CLI mit seiner EXIF-Erweiterung sowie jpegtran von
libjpeg-turbo und ExifTool. Die Werkzeuge müssen installiert und über
PATH verfügbar sein. Diese Anleitung wurde unter Linux mit PHP 8.5.10,
libjpeg-turbo 3.2.0 und ExifTool 13.55 getestet. Verwenden Sie für die Ausgabe ein privates lokales
Verzeichnis auf einem Dateisystem, das Hardlinks unterstützt.
Prüfen Sie Ihre vorhandene Installation, bevor Sie das Skript speichern:
php -d extension=exif -r 'if (!extension_loaded("exif") || !function_exists("proc_open")) { fwrite(STDERR, "PHP needs EXIF and proc_open.\n"); exit(1); } echo "PHP ", PHP_VERSION, ": EXIF and proc_open available\n";' &&
jpegtran -version &&
exiftool -ver
Die Option -d extension=exif aktiviert für diesen Aufruf eine installierte, dynamisch
ladbare EXIF-Erweiterung. Ist EXIF bereits aktiviert, lassen Sie diese Option durchgängig weg;
doppeltes Laden erzeugt eine Warnung. Kann PHP die Erweiterung nicht laden, installieren Sie die
passende Erweiterung für Ihren PHP-Build, bevor Sie fortfahren. Weitere Informationen finden Sie in
der EXIF-Installationsanleitung von PHP und den
CLI-Konfigurationsoptionen.
Die Versionsabfragen sollten libjpeg-turbo und ExifTool identifizieren, ohne PHP-Startwarnungen.
Den vollständigen PHP-Befehl speichern
Speichern Sie dies als orient-jpeg.php. Der Befehl akzeptiert einen Eingabepfad und
einen neuen Ausgabepfad. Er verwendet
exif_read_data() mit getrennten Abschnitten
und bricht bei Warnungen beim Lesen der Metadaten ab. Selbst JPEGs mit Ausrichtung 1 oder ohne
Ausrichtungs-Tag durchlaufen jpegtran; ein fehlendes Ausrichtungs-Tag ist
niemals ein Grund, ungeprüfte Bytes zu kopieren.
proc_open() mit einem Argument-Array startet
die Werkzeuge ohne Shell. Eingabepfade werden in absolute Pfade aufgelöst, damit ein Dateiname, der
mit einem Bindestrich beginnt, nicht als Werkzeugoption interpretiert wird. Wählen Sie die Pfade in
Ihrem Anwendungscode, wenn Sie diesen Befehl für einen Worker anpassen; das Beispiel autorisiert
keine Pfade, die ein HTTP-Client übermittelt.
<?php
declare(strict_types=1);
function runTool(array $arguments): void
{
$process = proc_open($arguments, [
0 => ['file', '/dev/null', 'r'],
1 => ['file', '/dev/null', 'w'],
2 => STDERR,
], $pipes);
if ($process === false || proc_close($process) !== 0) {
throw new RuntimeException($arguments[0] . ' failed; see the diagnostic above.');
}
}
function orientJpeg(string $input, string $output): string
{
$source = realpath($input);
if ($source === false || !is_file($source) || !is_readable($source)) {
throw new RuntimeException('Input must be an existing, readable local file.');
}
$directory = realpath(dirname($output));
if ($directory === false || !is_dir($directory) || !is_writable($directory)) {
throw new RuntimeException('Output directory must exist and be writable.');
}
$destination = $directory . DIRECTORY_SEPARATOR . basename($output);
if (file_exists($destination) || is_link($destination)) {
throw new RuntimeException('Output already exists; choose a new filename.');
}
$metadata = exif_read_data($source, null, true);
if ($metadata === false) {
throw new RuntimeException('Cannot read JPEG headers.');
}
$orientation = $metadata['IFD0']['Orientation'] ?? 1;
$transforms = [
1 => [],
2 => ['-flip', 'horizontal'],
3 => ['-rotate', '180'],
4 => ['-flip', 'vertical'],
5 => ['-transpose'],
6 => ['-rotate', '90'],
7 => ['-transverse'],
8 => ['-rotate', '270'],
];
if (!is_int($orientation) || !isset($transforms[$orientation])) {
throw new RuntimeException('EXIF Orientation must be an integer from 1 to 8.');
}
$temporary = tempnam($directory, '.orient-');
if ($temporary === false) {
throw new RuntimeException('Cannot create a temporary output.');
}
try {
runTool([
'jpegtran', '-strict', '-perfect', '-copy', 'icc',
...$transforms[$orientation], '-outfile', $temporary, $source,
]);
runTool(['exiftool', '-overwrite_original', '-IFD0:Orientation#=1', $temporary]);
$written = exif_read_data($temporary, null, true);
if (($written['IFD0']['Orientation'] ?? null) !== 1) {
throw new RuntimeException('Could not verify the output orientation tag.');
}
// A hard link publishes the finished file without replacing an existing name.
if (!link($temporary, $destination)) {
throw new RuntimeException('Could not create the output; choose a new filename.');
}
} finally {
unlink($temporary);
}
return $destination;
}
// Reject PHP warnings, including unreadable or malformed metadata, instead of guessing.
set_error_handler(static function (int $severity, string $message): never {
throw new ErrorException($message, 0, $severity);
});
try {
if ($argc !== 3) {
throw new RuntimeException('Usage: php orient-jpeg.php INPUT.jpg OUTPUT.jpg');
}
if (!extension_loaded('exif') || !function_exists('proc_open')) {
throw new RuntimeException('PHP needs EXIF and proc_open enabled.');
}
$output = orientJpeg($argv[1], $argv[2]);
fwrite(STDOUT, "Saved: $output\n");
} catch (Throwable $error) {
fwrite(STDERR, 'Failed: ' . $error->getMessage() . "\n");
exit(1);
}
Nur die temporäre Datei wird an die ExifTool-Option
-overwrite_original
übergeben. Die Quelle bleibt unverändert. Die PHP-Funktion
link() erstellt das Ziel nach erfolgreicher
Verarbeitung; eine vorhandene Datei oder ein Symlink wird abgelehnt. Ein Block mit
finally entfernt den temporären Namen sowohl bei Erfolg als auch bei
gewöhnlichen Fehlern.
Den Befehl auf ein hochgeladenes JPEG anwenden
Legen Sie ein echtes JPEG namens photo.jpg neben dem Skript ab und führen Sie
anschließend Folgendes aus:
php -d extension=exif orient-jpeg.php photo.jpg photo-fixed.jpg
Bei Erfolg gibt der Befehl Saved: gefolgt vom absoluten Ausgabepfad aus und
endet mit Status 0. Bei Fehlern schreibt er eine Diagnose auf die Standardfehlerausgabe und endet
mit Status 1. Ein erneuter Aufruf desselben Befehls verweigert das Überschreiben von
photo-fixed.jpg. Setzen Sie Pfade mit Leerzeichen in Anführungszeichen.
Prüfen Sie die resultierenden Metadaten:
exiftool -n -IFD0:Orientation -ImageWidth -ImageHeight ./photo-fixed.jpg
Orientation sollte 1 sein. Die Ausrichtungen 5–8 vertauschen Breite und Höhe. Öffnen Sie die Ausgabe und prüfen Sie ein asymmetrisches Merkmal, etwa einen Schriftzug, um sowohl die Drehung als auch die Seitenrichtigkeit zu bestätigen. Eine erneute Verarbeitung dieser Ausgabe unter einem anderen Dateinamen sollte die Bildgeometrie unverändert lassen.
Abgelehnte Dateien und Batch-Jobs behandeln
Die Diagnose „transformation is not perfect“ bedeutet, dass die angeforderte Operation nicht jeden
Randblock transformieren kann. Ein Beispiel ist eine Drehung um 90° bei einem JPEG mit einem
unvollständigen unteren Block. Bewahren Sie das Original auf und entscheiden Sie, ob Ihre Anwendung
einen Zuschnitt oder eine decodierte und neu codierte Ableitung zulässt. Wenn Sie
-perfect entfernen, ändern Sie diese Entscheidung stillschweigend. Dadurch kann
ein Randstreifen falsch ausgerichtet bleiben.
Leere Dateien, Dateien in anderen Formaten als JPEG, abgeschnittene Bilddaten und ungültige Ausrichtungswerte führen zu einem Fehler statt zu einer Erfolgsmeldung. Progressive JPEGs werden als Eingabe akzeptiert; dieser Befehl fordert keine progressive Ausgabe an. Er verspricht auch keine Konvertierung in ein bestimmtes JPEG-Kompatibilitätsprofil.
Für eine Queue oder einen Batch rufen Sie den Befehl einmal pro Eingabe mit jeweils einem eigenen Ziel auf und protokollieren jeden Exit-Status. Versuchen Sie die Verarbeitung abgelehnter Dateien erst erneut, nachdem Sie entschieden haben, wie Sie mit dem jeweiligen Fehler umgehen. Führen Sie diese Jobs außerhalb der HTTP-Anfrage aus: Erzwingen Sie Upload-Größenlimits und Ressourcenlimits, halten Sie Werkzeugdiagnosen in den Worker-Logs fest und geben Sie dem Client eine einfache Fehlermeldung zurück. Der lokale Befehl normalisiert die Ausrichtung, übernimmt aber weder die Upload-Authentifizierung noch Worker-Timeouts.
