fix(thumbnails): EXIF-Drehung auswerten, hochkant blieb quer
Kameras speichern die Bildpunkte fast immer quer und legen die Drehung als EXIF-Notiz daneben. Browser befolgen sie von selbst - Go nicht. Deshalb stand dasselbe Foto in der grossen Ansicht aufrecht und lag im Vorschaubild auf der Seite. Neues Paket internal/imageutil liest genau ein Feld (0x0112) aus dem Anfang der Datei und dreht danach. Von Hand statt mit einer Bibliothek: Der Weg dorthin ist kurz und liegt fest, und gebraucht wird ein Feld. Gelesen werden nur die ersten 64 KB, damit ein 40-MB-Foto nicht zweimal im Speicher liegt. Gedreht wird vor dem Verkleinern - andersherum bekaeme ein hochkantes Foto den Zuschnitt eines Querformats. Bestehende Vorschaubilder aendern sich davon nicht. Dafuer neu: POST /admin/thumbnails/rebuild samt Knopf im Admin-Bereich. Auch Profilbilder aus OIDC laufen jetzt durch dieselbe Auswertung. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
edefce3572
commit
4bd9a6ca56
10 files changed
+514
-2
No files matched your search
@@ -0,0 +1,148 @@
|
||||
package imageutil
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"image"
|
||||
"image/color"
|
||||
"image/jpeg"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// jpegMitDrehung baut eine echte JPEG-Datei mit EXIF-Block. Von Hand, damit
|
||||
// der Test keine Abhängigkeit braucht und genau das prüft, was Kameras
|
||||
// schreiben: APP1 direkt hinter dem Dateianfang.
|
||||
func jpegMitDrehung(t *testing.T, o uint16, big bool) []byte {
|
||||
t.Helper()
|
||||
var body bytes.Buffer
|
||||
if err := jpeg.Encode(&body, image.NewGray(image.Rect(0, 0, 8, 8)), nil); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
raw := body.Bytes() // beginnt mit FFD8
|
||||
|
||||
var bo binary.AppendByteOrder = binary.LittleEndian
|
||||
order := []byte{'I', 'I'}
|
||||
if big {
|
||||
bo = binary.BigEndian
|
||||
order = []byte{'M', 'M'}
|
||||
}
|
||||
tif := make([]byte, 0, 26)
|
||||
tif = append(tif, order...)
|
||||
tif = bo.AppendUint16(tif, 42)
|
||||
tif = bo.AppendUint32(tif, 8) // Verzeichnis beginnt direkt dahinter
|
||||
tif = bo.AppendUint16(tif, 1) // ein Eintrag
|
||||
tif = bo.AppendUint16(tif, 0x0112)
|
||||
tif = bo.AppendUint16(tif, 3) // SHORT
|
||||
tif = bo.AppendUint32(tif, 1) // ein Wert
|
||||
tif = bo.AppendUint16(tif, o)
|
||||
tif = append(tif, 0, 0) // Auffüllung auf vier Byte
|
||||
tif = bo.AppendUint32(tif, 0) // kein weiteres Verzeichnis
|
||||
|
||||
payload := append([]byte("Exif\x00\x00"), tif...)
|
||||
seg := []byte{0xFF, 0xE1}
|
||||
seg = binary.BigEndian.AppendUint16(seg, uint16(len(payload)+2))
|
||||
seg = append(seg, payload...)
|
||||
|
||||
out := append([]byte{}, raw[:2]...) // SOI
|
||||
out = append(out, seg...)
|
||||
return append(out, raw[2:]...)
|
||||
}
|
||||
|
||||
func TestOrientationLiestAlleFaelle(t *testing.T) {
|
||||
for o := uint16(1); o <= 8; o++ {
|
||||
for _, big := range []bool{false, true} {
|
||||
got := Orientation(jpegMitDrehung(t, o, big))
|
||||
if got != int(o) {
|
||||
name := "II"
|
||||
if big {
|
||||
name = "MM"
|
||||
}
|
||||
t.Errorf("%s Drehung %d gelesen als %d", name, o, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Was keine Drehung trägt oder kaputt ist, muss 1 ergeben – niemals einen
|
||||
// Fehler und niemals einen Zufallswert.
|
||||
func TestOrientationBleibtBeiEinsWennUnklar(t *testing.T) {
|
||||
var ohne bytes.Buffer
|
||||
if err := jpeg.Encode(&ohne, image.NewGray(image.Rect(0, 0, 4, 4)), nil); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
mitDrehung := jpegMitDrehung(t, 6, false)
|
||||
|
||||
faelle := map[string][]byte{
|
||||
"leer": {},
|
||||
"kein JPEG": []byte("das hier ist ein Text"),
|
||||
"nur SOI": {0xFF, 0xD8},
|
||||
"ohne EXIF": ohne.Bytes(),
|
||||
"abgeschnitten": mitDrehung[:12],
|
||||
"Laenge zu gross": append([]byte{0xFF, 0xD8, 0xFF, 0xE1, 0xFF, 0xFF}, []byte("Exif\x00\x00")...),
|
||||
"Drehung 0": jpegMitDrehung(t, 0, false),
|
||||
"Drehung 99": jpegMitDrehung(t, 99, false),
|
||||
}
|
||||
for name, data := range faelle {
|
||||
if got := Orientation(data); got != 1 {
|
||||
t.Errorf("%s ergab %d statt 1", name, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Ein Bild mit einer eindeutigen Ecke: Nur so lässt sich prüfen, dass nicht
|
||||
// bloß Breite und Höhe getauscht wurden, sondern richtig herum gedreht.
|
||||
func markiert(w, h int) image.Image {
|
||||
img := image.NewRGBA(image.Rect(0, 0, w, h))
|
||||
for y := 0; y < h; y++ {
|
||||
for x := 0; x < w; x++ {
|
||||
img.Set(x, y, color.RGBA{0, 0, 0, 255})
|
||||
}
|
||||
}
|
||||
img.Set(0, 0, color.RGBA{255, 0, 0, 255}) // oben links rot
|
||||
return img
|
||||
}
|
||||
|
||||
func TestApplyDrehtRichtigHerum(t *testing.T) {
|
||||
const w, h = 4, 2
|
||||
src := markiert(w, h)
|
||||
|
||||
// Wo landet der rote Punkt von oben links?
|
||||
erwartet := map[int][2]int{
|
||||
1: {0, 0},
|
||||
2: {w - 1, 0}, // waagerecht gespiegelt
|
||||
3: {w - 1, h - 1}, // 180 Grad
|
||||
4: {0, h - 1}, // senkrecht gespiegelt
|
||||
5: {0, 0}, // Hauptdiagonale
|
||||
6: {h - 1, 0}, // 90 Grad im Uhrzeigersinn: links oben → rechts oben
|
||||
7: {h - 1, w - 1},
|
||||
8: {0, w - 1}, // 90 Grad dagegen: links oben → links unten
|
||||
}
|
||||
for o, want := range erwartet {
|
||||
out := Apply(src, o)
|
||||
b := out.Bounds()
|
||||
wantW, wantH := w, h
|
||||
if o >= 5 {
|
||||
wantW, wantH = h, w
|
||||
}
|
||||
if b.Dx() != wantW || b.Dy() != wantH {
|
||||
t.Errorf("Drehung %d: %dx%d, erwartet %dx%d", o, b.Dx(), b.Dy(), wantW, wantH)
|
||||
continue
|
||||
}
|
||||
r, _, _, _ := out.At(want[0], want[1]).RGBA()
|
||||
if r < 0x8000 {
|
||||
t.Errorf("Drehung %d: an %v steht kein roter Punkt", o, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Bei 1 darf kein neues Bild entstehen – das spart bei jedem Vorschaubild
|
||||
// eine vollständige Kopie.
|
||||
func TestApplyLaesstEinsUnberuehrt(t *testing.T) {
|
||||
src := markiert(3, 3)
|
||||
if Apply(src, 1) != src {
|
||||
t.Error("Drehung 1 hat kopiert")
|
||||
}
|
||||
if Apply(src, 0) != src || Apply(src, 9) != src {
|
||||
t.Error("unbekannte Drehung hat kopiert")
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user