webp fix v1

This commit is contained in:
tamakyi
2026-10-15 11:41:19 +08:00
parent 6ffc87e4a8
commit e4457e3da4
15 changed files with 1297 additions and 39 deletions
+173
View File
@@ -0,0 +1,173 @@
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
}
// 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
switch orientation {
case exifOrientLeftTop, exifOrientRightTop, exifOrientRightBottom, exifOrientLeftBottom:
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
}