webp fix v1
This commit is contained in:
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user