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>
145 lines
3.9 KiB
Go
145 lines
3.9 KiB
Go
// Package imageutil enthält, was mehrere Bildpfade teilen: heute die
|
||
// EXIF-Drehung.
|
||
package imageutil
|
||
|
||
import (
|
||
"encoding/binary"
|
||
"image"
|
||
)
|
||
|
||
// HeadBytes ist die Menge, die zum Finden der Drehung gelesen werden muss.
|
||
// Der EXIF-Block steht unmittelbar hinter dem Dateianfang; 64 KB decken auch
|
||
// Kameras ab, die ein Vorschaubild mit hineinlegen.
|
||
const HeadBytes = 64 << 10
|
||
|
||
/*
|
||
Orientation liest die EXIF-Drehung (1–8) aus dem Anfang einer JPEG-Datei.
|
||
Alles Unklare ergibt 1, also „nicht drehen" – ein unlesbarer Block ist kein
|
||
Grund, ein Bild zu verweigern.
|
||
|
||
**Warum von Hand statt mit einer Bibliothek:** Gebraucht wird genau ein Feld.
|
||
Der Weg dorthin ist kurz und liegt fest: JPEG-Segmente durchgehen bis APP1,
|
||
dort "Exif\0\0", dahinter ein TIFF-Kopf, darin der Verzeichniseintrag mit der
|
||
Kennung 0x0112.
|
||
|
||
**Warum es überhaupt gebraucht wird:** Kameras und Telefone speichern die
|
||
Bildpunkte fast immer quer und legen die Drehung als Notiz daneben. Browser
|
||
befolgen sie von selbst – Go nicht. Ohne diese Auswertung liegt jedes hochkant
|
||
aufgenommene Foto im Vorschaubild auf der Seite, während dasselbe Foto in der
|
||
großen Ansicht aufrecht steht.
|
||
*/
|
||
func Orientation(head []byte) int {
|
||
if len(head) < 4 || head[0] != 0xFF || head[1] != 0xD8 { // SOI
|
||
return 1
|
||
}
|
||
i := 2
|
||
for i+4 <= len(head) {
|
||
if head[i] != 0xFF {
|
||
return 1 // kein Segmentanfang mehr – hier ist nichts zu holen
|
||
}
|
||
marker := head[i+1]
|
||
// Bildinhalt erreicht: ab hier kommt kein EXIF mehr.
|
||
if marker == 0xDA || marker == 0xD9 {
|
||
return 1
|
||
}
|
||
size := int(binary.BigEndian.Uint16(head[i+2 : i+4]))
|
||
if size < 2 || i+2+size > len(head) {
|
||
return 1
|
||
}
|
||
if marker == 0xE1 {
|
||
if o, ok := orientationInAPP1(head[i+4 : i+2+size]); ok {
|
||
return o
|
||
}
|
||
}
|
||
i += 2 + size
|
||
}
|
||
return 1
|
||
}
|
||
|
||
func orientationInAPP1(seg []byte) (int, bool) {
|
||
const hdr = "Exif\x00\x00"
|
||
if len(seg) < len(hdr)+8 || string(seg[:len(hdr)]) != hdr {
|
||
return 0, false
|
||
}
|
||
tiff := seg[len(hdr):]
|
||
|
||
var bo binary.ByteOrder
|
||
switch {
|
||
case tiff[0] == 'I' && tiff[1] == 'I':
|
||
bo = binary.LittleEndian
|
||
case tiff[0] == 'M' && tiff[1] == 'M':
|
||
bo = binary.BigEndian
|
||
default:
|
||
return 0, false
|
||
}
|
||
if bo.Uint16(tiff[2:4]) != 42 {
|
||
return 0, false
|
||
}
|
||
off := int(bo.Uint32(tiff[4:8]))
|
||
if off < 8 || off+2 > len(tiff) {
|
||
return 0, false
|
||
}
|
||
count := int(bo.Uint16(tiff[off : off+2]))
|
||
entry := off + 2
|
||
for n := 0; n < count; n++ {
|
||
if entry+12 > len(tiff) {
|
||
return 0, false
|
||
}
|
||
if bo.Uint16(tiff[entry:entry+2]) == 0x0112 {
|
||
// Der Wert ist ein SHORT und steht deshalb im Eintrag selbst,
|
||
// nicht anderswo in der Datei.
|
||
v := int(bo.Uint16(tiff[entry+8 : entry+10]))
|
||
if v >= 1 && v <= 8 {
|
||
return v, true
|
||
}
|
||
return 0, false
|
||
}
|
||
entry += 12
|
||
}
|
||
return 0, false
|
||
}
|
||
|
||
// Apply dreht und spiegelt ein Bild so, wie die EXIF-Notiz es vorsieht.
|
||
// Bei 1 (und allem Unbekannten) kommt das Bild unverändert zurück – dann
|
||
// entsteht auch keine Kopie.
|
||
func Apply(img image.Image, orientation int) image.Image {
|
||
if orientation <= 1 || orientation > 8 {
|
||
return img
|
||
}
|
||
b := img.Bounds()
|
||
w, h := b.Dx(), b.Dy()
|
||
|
||
// Die vier Fälle ab 5 vertauschen Breite und Höhe.
|
||
outW, outH := w, h
|
||
if orientation >= 5 {
|
||
outW, outH = h, w
|
||
}
|
||
dst := image.NewRGBA(image.Rect(0, 0, outW, outH))
|
||
|
||
// Für jeden Zielpunkt den Quellpunkt bestimmen. Andersherum (Quelle →
|
||
// Ziel) bliebe bei ungeraden Größen gelegentlich eine Lücke.
|
||
for y := 0; y < outH; y++ {
|
||
for x := 0; x < outW; x++ {
|
||
var sx, sy int
|
||
switch orientation {
|
||
case 2: // waagerecht gespiegelt
|
||
sx, sy = w-1-x, y
|
||
case 3: // 180 Grad
|
||
sx, sy = w-1-x, h-1-y
|
||
case 4: // senkrecht gespiegelt
|
||
sx, sy = x, h-1-y
|
||
case 5: // an der Hauptdiagonalen gespiegelt
|
||
sx, sy = y, x
|
||
case 6: // 90 Grad im Uhrzeigersinn
|
||
sx, sy = y, h-1-x
|
||
case 7: // an der Nebendiagonalen gespiegelt
|
||
sx, sy = w-1-y, h-1-x
|
||
case 8: // 90 Grad gegen den Uhrzeigersinn
|
||
sx, sy = w-1-y, x
|
||
}
|
||
dst.Set(x, y, img.At(b.Min.X+sx, b.Min.Y+sy))
|
||
}
|
||
}
|
||
return dst
|
||
}
|