114 lines
3.4 KiB
Go
114 lines
3.4 KiB
Go
|
|
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
|
||
|
|
}
|