package storage import ( "encoding/binary" "image" "image/color" "image/draw" ) // EXIF Orientation(TIFF tag 0x0112)的取值含义:描述「存储的像素」要如何变换 // 才是用户肉眼看到的方向。手机竖拍照片几乎都是 6 或 8。 const ( exifOrientTopLeft = 1 // 正常 exifOrientTopRight = 2 // 水平镜像 exifOrientBottomRight = 3 // 旋转 180 exifOrientBottomLeft = 4 // 垂直镜像 exifOrientLeftTop = 5 // 转置 exifOrientRightTop = 6 // 顺时针 90 exifOrientRightBottom = 7 // 反转置 exifOrientLeftBottom = 8 // 顺时针 270 ) // jpegOrientation 解析 JPEG 的 EXIF Orientation,返回 1~8。 // // Go 标准库 image.Decode 不读 EXIF(Pillow 默认也不读,需显式 exif_transpose), // 因此手机竖拍照片解出来的像素是横躺的。更严重的是转码成 WebP 时 EXIF 会被 // 整体丢弃,方向信息随之永久消失、无法事后补救,故必须在转码前归一。 // // 未找到 Orientation、格式异常或非 JPEG 时返回 1(identity),调用方无需区分 // 「没有方向信息」和「解析失败」。 func jpegOrientation(data []byte) int { if len(data) < 4 || data[0] != 0xFF || data[1] != 0xD8 || data[2] != 0xFF { return exifOrientTopLeft } for i := 2; i+4 <= len(data); { if data[i] != 0xFF { return exifOrientTopLeft // 段结构损坏,放弃解析 } marker := data[i+1] // 0xFF 填充字节、SOI、Standalone 标记、RSTn 均无长度字段。 if marker == 0xFF || marker == 0x01 || (marker >= 0xD0 && marker <= 0xD9) { i += 2 continue } segLen := int(binary.BigEndian.Uint16(data[i+2 : i+4])) if segLen < 2 || i+2+segLen > len(data) { return exifOrientTopLeft } // APP1 (0xE1),载荷形如 "Exif\x00\x00" + TIFF 块。 if marker == 0xE1 && segLen >= 10 { payload := data[i+4 : i+2+segLen] if len(payload) >= 6 && string(payload[:6]) == "Exif\x00\x00" { if o := tiffOrientation(payload[6:]); o != 0 { return o } } } // SOS (0xDA) 之后是熵编码的像素数据,不再有元数据段。 if marker == 0xDA { return exifOrientTopLeft } i += 2 + segLen } return exifOrientTopLeft } // tiffOrientation 从 TIFF 块的 IFD0 中取出 Orientation;取不到返回 0。 func tiffOrientation(tiff []byte) int { if len(tiff) < 8 { return 0 } 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 } if bo.Uint16(tiff[2:4]) != 42 { // 42 是 TIFF 魔数 return 0 } ifd := int(bo.Uint32(tiff[4:8])) if ifd < 8 || ifd+2 > len(tiff) { return 0 } entries := int(bo.Uint16(tiff[ifd : ifd+2])) pos := ifd + 2 for i := 0; i < entries; i++ { if pos+12 > len(tiff) { return 0 } // SHORT(3) 且 count==1 时,值内联在 4 字节 value 字段的前 2 字节。 if bo.Uint16(tiff[pos:pos+2]) == 0x0112 && bo.Uint16(tiff[pos+2:pos+4]) == 3 && bo.Uint32(tiff[pos+4:pos+8]) == 1 { v := int(bo.Uint16(tiff[pos+8 : pos+10])) if v >= exifOrientTopLeft && v <= exifOrientLeftBottom { return v } return 0 } pos += 12 } return 0 } // orientationSwapsDimensions 报告该 Orientation 是否会交换宽高。 // 2/3/4 只是镜像或 180 度旋转,宽高不变;5/6/7/8 会把横躺的像素转正,宽高互换。 func orientationSwapsDimensions(orientation int) bool { switch orientation { case exifOrientLeftTop, exifOrientRightTop, exifOrientRightBottom, exifOrientLeftBottom: return true } return false } // orientedImage 把源图按 EXIF Orientation 重映射为目标坐标,供 draw.Draw // 一次性拷贝,避免逐像素重复计算源矩形。 type orientedImage struct { src image.Image sx0, sy0 int // 源矩形左上角(源图 Bounds().Min 通常为 0,但不作假设) w, h int // 源宽高 dstW, dstH int // 目标宽高 ori int } func (o *orientedImage) ColorModel() color.Model { return o.src.ColorModel() } func (o *orientedImage) Bounds() image.Rectangle { return image.Rect(0, 0, o.dstW, o.dstH) } // At 按 Orientation 反查源像素坐标。目标 (x,y) 命中源 (sx,sy)。 func (o *orientedImage) At(x, y int) color.Color { sx, sy := 0, 0 switch o.ori { case exifOrientTopRight: // 水平镜像 sx, sy = o.w-1-x, y case exifOrientBottomRight: // 旋转 180 sx, sy = o.w-1-x, o.h-1-y case exifOrientBottomLeft: // 垂直镜像 sx, sy = x, o.h-1-y case exifOrientLeftTop: // 转置 sx, sy = y, x case exifOrientRightTop: // 顺时针 90 sx, sy = y, o.h-1-x case exifOrientRightBottom: // 反转置 sx, sy = o.w-1-y, o.h-1-x case exifOrientLeftBottom: // 顺时针 270 sx, sy = o.w-1-y, x default: // exifOrientTopLeft sx, sy = x, y } return o.src.At(o.sx0+sx, o.sy0+sy) } // applyJPEGOrientation 按 EXIF Orientation 重绘像素;orientation 为 1 时原样返回。 // // 归一后方向信息已烘进像素,尺寸在 5/6/7/8 时宽高互换——调用方必须在此之后再 // 做尺寸上限判断,否则限边会作用在横躺的像素上。 func applyJPEGOrientation(img image.Image, orientation int) image.Image { if orientation <= exifOrientTopLeft || orientation > exifOrientLeftBottom { return img } b := img.Bounds() sw, sh := b.Dx(), b.Dy() if sw == 0 || sh == 0 { return img } dstW, dstH := sw, sh if orientationSwapsDimensions(orientation) { dstW, dstH = sh, sw } dst := image.NewRGBA(image.Rect(0, 0, dstW, dstH)) draw.Draw(dst, dst.Bounds(), &orientedImage{ src: img, sx0: b.Min.X, sy0: b.Min.Y, w: sw, h: sh, dstW: dstW, dstH: dstH, ori: orientation, }, image.Point{}, draw.Src) return dst }