183 lines
5.6 KiB
Go
183 lines
5.6 KiB
Go
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
|
||
}
|