Mobiltelefone speichern Fotos aus Effizienzgründen oft verkehrt herum und verzeichnen den Trick in den EXIF-Metadaten. Damit kommen jedoch nicht alle Apps zurecht. Mike Schilli macht das Verfahren mit Go idiotensicher.
Irgendwie scheine ich mein Mobiltelefon beim Fotografieren falsch zu halten: Es kommt oft vor, dass ein Bild, das in der Fotosammlung des Handys richtig aussieht, plötzlich auf dem Kopf steht oder auf der Seite liegt, wenn ich es mit Whatsapp an Freunde senden will. Ein Blick auf die Metadaten des betreffenden Fotos offenbart, was schiefläuft (Listing 1).
Listing 1
Metadaten
$ exiftool pic.jpg | grep Rotate
Orientation: Rotate 180
Offensichtlich speichert das Telefon das Bild in falscher Ausrichtung ab, spart sich aber die Korrektur und zeigt im EXIF-Header des JPEG-Formats an, wie es eigentlich richtig gedreht gehört. Ich weiß nicht, wer sich das ausgedacht hat, aber anzunehmen, dass jede beliebige App erst im EXIF-Header nachsieht und das Bild dann korrekt ausrichtet, scheint mir ein grundlegender Denkfehler zu sein. Das sollte doch die originale Kamera-App bei der Aufnahme erledigen, statt die Arbeit flussabwärts wieder und wieder einer unüberschaubaren Vielzahl von verarbeiteten Foto-Apps aufzubürden.
Verkehrte Welt
Abbildung 1 illustriert, dass das Mobiltelefon in seiner Foto-App das Bild noch richtig herum anzeigt, obwohl es falsch abgelegt wurde. Die Desktop-Version des Whatsapp-Clients im Browser ignoriert aber offensichtlich den zugehörigen EXIF-Header und würde das Bild auf dem Kopf stehend verschicken. Beim Empfänger dürfte es wohl Kopfschütteln auslösen, wenn das Foto zu der Nachricht meines Surf-Ausflugs an den Ocean Beach in San Francisco auf dem Kopf ankäme.
Höchste Zeit also, ein Go-Programm zu schreiben, das ein vom Telefon geholtes Foto korrekt ausrichtet und den EXIF-Header für die erst falsche und dann richtige Orientierung löscht – ganz so, wie es Gimp (Abbildung 3) macht, wenn es ein verdrehtes Foto sieht. Nebenbei lohnt es sich, einige Algorithmen zur Bildrotation um 90 oder 180 Grad zu studieren: Schließlich könnte jemand beim nächsten Vorstellungsgespräch danach fragen.
Ein digitales Foto ist letztlich eine n-mal-m-Matrix aus Pixelwerten, und um es auf den Kopf zu stellen, also eine Drehung um 180 Grad auszuführen, vertauscht ein Algorithmus einfach Pixelwerte ober- und unterhalb der Mittellinie. Die X-Werte der Pixel laufen traditionsgemäß von links nach rechts im Bild, während sich die Y-Werte von oben nach unten erhöhen. So lässt sich der Ursprung des Fotos links oben mit (0,0) ansprechen, während die rechte untere Ecke an den Koordinaten »(w-1,h-1)« liegt. Dabei geben »w« die Bildbreite und »h« die Bildhöhe jeweils in Pixeln an.
Doppelt spiegeln
Aber Vorsicht: Wer einfach die Y-Werte spiegelt, findet am Ende ein Spiegelbild des ursprünglichen Fotos vor. Vielmehr kommen kleine X-Werte (Pixel links oben) bei der 180-Grad-Drehung unten rechts zu liegen. Der Rotieralgorithmus muss also nicht nur die Y-Werte spiegeln, sondern auch noch die X-Werte. Ein in der oberen Bildhälfte ansässiges Pixel mit den Koordinaten »(x0,y0)« kommt so in der unteren Bildhälfte an der Stelle »x1,y1« zu liegen. Der Abstand von »y0« zur Mittellinie ist dabei gleich dem Abstand von »y1« zur Mittellinie. Gleichzeitig liegt »x0« so weit vom linken Bildrand entfernt wie »x1« vom rechten.
Listing 2 zeigt den Algorithmus, der ein JPEG-Foto auf den Kopf stellt. Die zwei verschachtelten For-Schleifen ab Zeile 13 hangeln sich durch die Y-Werte von 0 bis zum unteren Bildrand und durch die X-Werte von 0 bis zur rechten Kante. Diese Koordinaten entsprechen den Positionen »x0« und »y0« des Ausgangspixels. Innerhalb der Doppelschleife holt Zeile 15 den Original-Pixelwert mit »jimg.At(x,y)« ab. Es speichert ihn an der gespiegelten Position im neu angelegten, modifizierbaren Foto »dimg« an der Position »x1,y1«. Die Lage errechnet sich aus dem Abstand von »x0« vom rechten beziehungsweise »y0« vom unteren Bildrand. So spiegelt der Algorithmus das Bild effizient sowohl an der horizontalen wie an der vertikalen Mittellinie und stellt es damit wie gewünscht auf den Kopf.
Listing 2
rotate-180.go
package main
import (
"image"
)
func rot180(jimg image.Image) *image.RGBA {
bounds := jimg.Bounds()
width, height := bounds.Max.X, bounds.Max.Y
dimg := image.NewRGBA(bounds)
for y := 0; y < height; y++ {
for x := 0; x < width; x++ {
dimg.Set(width-1-x, height-1-y, jimg.At(x, y))
}
}
return dimg
}
Beschreibbare Kopie
Wenn Go ein JPEG-Foto von der Platte liest, kommt es üblicherweise in einem Pixel-Array zu liegen, das sich nicht modifizieren lässt. Da der Algorithmus aber damit herumfuhrwerken möchte, legt Zeile 11 in Listing 2 mit »NewRGBA()« zunächst ein beschreibbares Foto mit denselben Maßen wie das Original an, lässt es aber leer. Anschließend kann die Funktion »rot180« mit »jimg.At()« die Pixel des Originals auslesen und sie mit »Set(x,y,Wert« einzeln gespiegelt ins Zielfoto übertragen. Zeile 24 gibt das fertig gedrehte Bild als Pointer an das aufrufende Hauptprogramm zurück.
Soweit der Algorithmus zum Rotieren eines Fotos im Speicher um 180 Grad. Doch wie kommt das Bild, das ja im JPEG-Format komprimiert auf der Platte liegt, anfangs überhaupt als Pixelmatrix in den Speicher? Die Funktion »imgMod()« in Listing 3 nimmt dazu den Namen der Fotodatei in »srcFile« entgegen, im zweiten Parameter den Namen der Zieldatei und im dritten eine Callback-Funktion, die die gewünschte Rotation des Bilds im Speicher vornimmt.
Funktionen sind in Go vollwertige Datentypen und lassen sich problemlos anderen Funktionen mitgeben, mit dem Auftrag: “hier ist der Algorithmus, den du auf die Daten anwendest”. Listing 3 öffnet in Zeile 11 die Originaldatei zum Lesen, dekodiert die JPEG-Daten mit »Decode()« aus dem Standardpaket image/jpeg und legt sie in der Variablen »jimg« ab, falls keine Fehler auftreten. Zeile 21 ruft dann die vorher in der Variablen »cb« als Parameter hereingereichte Funktion auf, übergibt ihr die Bilddaten in »jimg« und empfängt die modifizierten Daten des dann rotierten Bilds in »dimg«. Bleibt nur noch, eine neue Zieldatei »dstFile« anzulegen und in Zeile 30 die JPEG-codierten Daten für das modifizierte Foto hineinzuschreiben.
Listing 3
imgmod.go
package main
import (
"image"
"image/jpeg"
"log"
"os"
)
func imgMod(srcFile string, dstFile string, cb func(image.Image) *image.RGBA) {
f, err := os.Open(srcFile)
if err != nil {
log.Fatalf("os.Open failed: %v", err)
}
jimg, _, err := image.Decode(f)
if err != nil {
log.Fatalf("image.Decode failed: %v", err)
}
dimg := cb(jimg)
if err != nil {
log.Fatalf("Modifier failed")
}
f, err = os.Create(dstFile)
if err != nil {
log.Fatalf("os.Create failed: %v", err)
}
err = jpeg.Encode(f, dimg, nil)
if err != nil {
log.Fatalf("jpeg.Encode failed: %v", err)
}
}
Rotieren um 90 Grad
Doch nicht alle falsch gespeicherten Bilder stehen auf dem Kopf. Manchmal liegen sie auch auf der Seite und müssen um 90 Grad gedreht werden. Der Aufruf des Werkzeugs »exiftool« zeigt für die JPEG-Datei in diesen Fällen dann etwas wie Orientation: 90**CW an. Das signalisiert, dass man das Bild um 90 Grad im Uhrzeigersinn (clockwise) drehen muss, um es mit der richtigen Orientierung anzuzeigen. In Abbildung 4 stellt die Desktop-App Gimp fest, dass ein Foto des wenig bekannten Parks Billy Goat Hill in San Francisco um eine Vierteldrehung nach rechts rotiert gehört, und bietet den entsprechenden Service auch gleich an.
Wie funktioniert eine Vierteldrehung von Pixeln nun in einer 2D-Matrix? Abbildung 5 zeigt schematisch, wie die erste Pixelreihe mit den Werten (1,2,3,4) durch eine 90-Grad-Drehung der Matrix im Uhrzeigersinn als ganz rechte Spalte im Ergebnis landet.
Während der Algorithmus so Reihen in Spalten umwandelt, ändert sich auch die Dimension des Zielbilds: Handy-Fotos sind typischerweise nicht quadratisch, sondern rechteckig, und ein um 90 Grad gedrehtes Foto ändert nicht nur seine Pixelwerte, sondern vertauscht auch Länge und Breite des resultierenden Gesamtbilds. Listing 4 trägt dem Rechnung, indem Zeile 10 die in »bounds« liegenden Dimensionen in X- und Y-Richtung vertauscht, sodass das in Zeile 12 mit »NewRGBA()« generierte, modifizierbare Zielbild bereits die Dimensionen des rotierten Rechtecks aufweist und nicht mehr die des Originals.
Listing 4
rotate-90.go
package main
import (
"image"
)
func rot90(jimg image.Image) *image.RGBA {
bounds := jimg.Bounds()
width, height := bounds.Max.X, bounds.Max.Y
bounds.Max.X, bounds.Max.Y = bounds.Max.Y, bounds.Max.X
dimg := image.NewRGBA(bounds)
for y := 0; y < height; y++ {
for x := 0; x < width; x++ {
org := jimg.At(x, y)
dimg.Set(height-y, x, org)
}
}
return dimg
}
Aus X wird Y
Die doppelte For-Schleife ab Zeile 14 iteriert zeilenweise durch das Ausgangsbild, nimmt mit »jimg.At()« aktuelle Pixelwerte entgegen und speichert sie spaltenweise mit »dimg.Set()« von rechts nach links in die Zielmatrix – so einfach geht das.
Mit den beiden in Code gegossenen Algorithmen kann nun das Hauptprogramm in Listing 5 ein Foto von der Platte holen, die EXIF-Header auslesen, feststellen, ob sich darin ein »Orientation«-Tag findet, und die Rotation ins korrigierte Format einleiten. Wer bislang EXIF-Header von JPEG-Fotos mit Exiftool ausgelesen hat, dürfte sich wundern, dass der wahre Wert eines Orientation-Tags in den EXIF-Headern keineswegs ein String mit der Gradangabe ist, sondern ein Integer-Wert. Er nimmt die Werte 6 ( 90 Grad im Uhrzeigersinn), 3 (180 Grad) oder 8 (90 Grad entgegen dem Uhrzeigersinn) an. Weitere Werte des Standards, der auch gespiegelte Fotos unterstützt, zeigt Abbildung 6; sie kommen in der Praxis aber seltener vor.
Listing 5
autorot.go
package main
import (
"flag"
"fmt"
"github.com/rwcarlsen/goexif/exif"
"os"
"path"
)
func main() {
flag.Usage = func() {
fmt.Printf("Usage: %s jpg-file\n", path.Base(os.Args[0]))
os.Exit(1)
}
flag.Parse()
if len(flag.Args()) != 1 {
flag.Usage()
}
jpgFile := flag.Arg(0)
f, err := os.Open(jpgFile)
if err != nil {
panic(err)
}
data, err := exif.Decode(f)
if err != nil {
panic(err)
}
orient, err := data.Get(exif.Orientation)
if err != nil {
fmt.Printf("No orientation header found.\n")
os.Exit(0)
}
val, err := orient.Int(0)
if err != nil {
panic(err)
}
switch val {
case 3:
imgMod(jpgFile, jpgFile, rot180)
case 6:
imgMod(jpgFile, jpgFile, rot90)
default:
panic("Unknown orientation")
}
}
Die in einem JPEG-Foto verborgenen EXIF-Tags sind nicht einfach auszulesen, aber zum Glück bietet die Go-Community auf Github einige Bibliotheken an, die den Job auf den Aufruf einer Funktion »Decode()« reduzieren. Listing 5 zeigt das Hauptprogramm »autorot«, das die Library »goexif« des Github-Users rwcarlsen nutzt. Eine Snapshot-Ausgabe vor einiger Zeit verwendete ein ähnliches Produkt [1].
Mit »data.Get()« holt Listing 5 in Zeile 34 den Wert des Tags »Orientation« aus dem EXIF-Salat. Falls sich das Tag nicht findet, gibt es auch nichts zu tun, denn das Bild ist bereits richtig orientiert. Wird das Programm allerdings fündig, holt Zeile 40 den ersten Integer-Wert des Tags heran (Index 0), und das Switch-Konstrukt ab Zeile 45 ermittelt, welche Art von korrigierender Rotation (90 oder 180 Grad) das Bild benötigt, und ruft die Modifiziererfunktion »imgMod« mit der entsprechenden Algorithmusfunktion als Parameter auf. Ausgangs- und Zieldatei benennen die Aufrufe von »imgMod()« in Listing 5 jeweils identisch, also überschreibt »autorot« einfach die Originaldateien. Erscheint Ihnen das zu gefährlich, benennen Sie die Zieldatei in ».bak« um, dann passiert garantiert kein Malheur.
Wenn Gos »image«-Library das JPEG-Bild wieder auf die Platte zurückschreibt, schließt es die ursprünglichen EXIF-Daten komplett aus, also steht dort auch kein »Orientation-Header« mehr.
Programm übersetzen
Um das Binary »autorot« zu erzeugen, verwenden Sie die Kommandosequenz aus Listing 6. So holt der Compiler »go« die notwendigen Bibliotheken von Github ab, kompiliert sie und bindet sie ein. Es entsteht wie immer ein Executable, das bereits alle Abhängigkeiten enthält. Es lässt sich also problemlos auf einen anderen Rechner kopieren und läuft dort klaglos, ein ähnliches Betriebssystem und eine passende Prozessorarchitektur vorausgesetzt.
Listing 6
autorot kompilieren
$ go mod init autorot $ go mod tidy $ go build autorot.go rotate-90.go rotate-180.go imgmod.go
Fazit
Perfekt ist das Programm noch nicht: Ihm fehlen noch die Rotation gegen den Uhrzeigersinn sowie Algorithmen für exotischere EXIF-Werte. Mit den beschriebenen Grundlagen lässt sich das aber leicht hinzufügen. Wie immer setzt Go hier dem Hobbyhandwerker keine Grenzen. (uba/jlu)
Der Autor
Michael Schilli arbeitet als Software Engineer in der kalifornischen San Francisco Bay Area. In seiner seit 1997 laufenden Kolumne forscht er jeden Monat nach praktischen Anwendungen verschiedener Programmiersprachen. Unter mailto:mschilli@perlmeister.com beantwortet er gern Ihre Fragen.
Infos
- Snapshot: Mike Schilli, “Spuren verwischen”, LM 09/2020, S. 86, https://www.lm-online.de/44559
- Listings zu diesem Artikel: http://www.linux-magazin.de/static/listings/magazin/2022/02/snapshot/











