// 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. // // 256 KB, nicht 64: Ein APP1-Segment darf selbst schon 65 535 Byte groß sein – // Telefone legen ihr eigenes Vorschaubild dort hinein –, und davor können // weitere Segmente stehen. Bei 64 KB reichte der Puffer im Grenzfall nicht // einmal für das eine Segment. const HeadBytes = 256 << 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 { return 1 } end := i + 2 + size truncated := end > len(head) if truncated { // Der Puffer endet mitten im Segment. Trotzdem nachsehen: Das // Verzeichnis mit der Drehung steht ganz am Anfang des Blocks, // das eingebettete Vorschaubild dahinter. Aufzugeben hieße, wegen // eines Bildes im Bild die Drehung zu verlieren. end = len(head) } if marker == 0xE1 && i+4 < end { if o, ok := orientationInAPP1(head[i+4 : end]); ok { return o } } if truncated { return 1 } i = end } 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 }