package web import ( "bytes" "encoding/binary" "github.com/disintegration/imaging" ) // normalizeOrientation returns image bytes that are physically upright. // // Phone cameras store a photo in the sensor's native orientation plus an EXIF // "Orientation" tag telling viewers to rotate it. We bake that rotation into the // pixels (and drop the tag) so the stored receipt is upright in every consumer — // the browser, downloads, the AI classifier, a future PDF export — not just ones // that happen to honor EXIF. // // It only acts when the orientation is actually KNOWN: a JPEG carrying an EXIF // Orientation tag of 2..8. With no tag (the common case — there's no way to know // which way is up) or an already-upright tag of 1, the original bytes are returned // verbatim, so we neither guess nor needlessly recompress. PDFs and non-JPEG // images pass through untouched, as does anything that fails to decode. func normalizeOrientation(data []byte, mime string) []byte { if mime != "image/jpeg" { return data } switch exifOrientation(data) { case 0, 1: return data // no metadata to act on, or already upright } // imaging.AutoOrientation reads the EXIF tag and rotates/flips to upright. img, err := imaging.Decode(bytes.NewReader(data), imaging.AutoOrientation(true)) if err != nil { return data // undecodable — keep the original rather than lose it } var buf bytes.Buffer if err := imaging.Encode(&buf, img, imaging.JPEG, imaging.JPEGQuality(90)); err != nil { return data } return buf.Bytes() } // exifOrientation returns the EXIF Orientation tag (1..8) of a JPEG, or 0 when the // image has no such tag. It is a minimal, allocation-free scan: it walks the JPEG // segment markers to the APP1 (Exif) block and reads tag 0x0112 from IFD0. func exifOrientation(data []byte) int { if len(data) < 4 || data[0] != 0xFF || data[1] != 0xD8 { // SOI return 0 } i := 2 for i+4 <= len(data) { if data[i] != 0xFF { return 0 // not at a marker boundary — give up } marker := data[i+1] if marker == 0xDA || marker == 0xD9 { // SOS (image data begins) or EOI return 0 } segLen := int(data[i+2])<<8 | int(data[i+3]) if segLen < 2 { return 0 } segStart, segEnd := i+4, i+2+segLen if segEnd > len(data) { return 0 } if marker == 0xE1 { // APP1 if o, ok := orientationFromAPP1(data[segStart:segEnd]); ok { return o } } i = segEnd } return 0 } // orientationFromAPP1 reads the Orientation tag out of an APP1 segment's payload // (the "Exif\0\0" header followed by a TIFF block). Returns (0, false) for any // malformed or non-Exif segment. func orientationFromAPP1(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 } ifdOff := int(bo.Uint32(tiff[4:8])) if ifdOff < 8 || ifdOff+2 > len(tiff) { return 0, false } count := int(bo.Uint16(tiff[ifdOff : ifdOff+2])) for k, entry := 0, ifdOff+2; k < count; k, entry = k+1, entry+12 { if entry+12 > len(tiff) { return 0, false } if bo.Uint16(tiff[entry:entry+2]) == 0x0112 { // Orientation val := int(bo.Uint16(tiff[entry+8 : entry+10])) if val >= 1 && val <= 8 { return val, true } return 0, false } } return 0, false }