webp fix v2

This commit is contained in:
tamakyi
2026-10-16 09:39:30 +08:00
parent e4457e3da4
commit da7e76a7db
10 changed files with 903 additions and 60 deletions
+35 -34
View File
@@ -43,11 +43,19 @@ type aspect struct {
W, H float64
}
func (a *aspect) check(data []byte) error {
// check 校验给定尺寸是否满足本用途的宽高比约束。
// orientation 是 EXIF Orientation:手机竖拍照片解出的像素是横躺的(5/6/7/8 会
// 交换宽高),若直接用原始像素宽高比对,合规的竖图会被误判为比例不符而拒。
// 修正只交换宽高、不解码像素,大图内存不受影响。
func (a *aspect) check(cfg image.Config, orientation int) error {
if a == nil {
return nil
}
return imagecheck.CheckAspectRatio(bytes.NewReader(data), a.W, a.H, imagecheck.DefaultAspectTolerance)
w, h := cfg.Width, cfg.Height
if orientationSwapsDimensions(orientation) {
w, h = h, w
}
return imagecheck.CheckAspectRatioSize(w, h, a.W, a.H, imagecheck.DefaultAspectTolerance)
}
// EncodeSpec 描述一次上传的 WebP 编码档位。
@@ -107,31 +115,31 @@ func uploadPictureToS3(file multipart.File, fileHeader *multipart.FileHeader, as
return "", errors.New("unsupported file type: not a valid image")
}
// 2.5 用途宽高比校验(头像 1:1 / 背景 16:9),绕过前端裁剪的直传在此被拦截
if err := asp.check(fileData); err != nil {
return "", err
}
fileReader := bytes.NewReader(fileData)
// 3. 解码前先校验像素总量(防解压炸弹),再解码图片获取格式;解码失败则直接返回错误。
if err := imagecheck.CheckPixelLimit(bytes.NewReader(fileData)); err != nil {
return "", err
}
_, format, err := image.Decode(fileReader)
// 3. 只解析文件头:一次拿到格式与宽高,供像素上限校验、比例校验、
// WebP 透传判断共用。image.DecodeConfig 不解像素,因此这里不产生解码开销。
// (此前为拿 format 先 image.Decode 一遍、转换时再解一遍,同一张图解码两次,
// 4096×4096 上限下峰值内存翻倍,1C1G 机器并发上传有 OOM 风险。)
cfg, format, err := image.DecodeConfig(bytes.NewReader(fileData))
if err != nil {
return "", errors.Wrap(err, "image decode failed, file may be corrupted")
}
// 3.5 像素总量校验(防解压炸弹),必须在任何完整解码之前。
if cfg.Width > 0 && cfg.Height > 0 &&
int64(cfg.Width)*int64(cfg.Height) > imagecheck.MaxImagePixels {
return "", imagecheck.ErrImageTooLarge
}
// 3.6 用途宽高比校验(头像 1:1 / 背景 16:9),绕过前端裁剪的直传在此被拦截。
// 比例校验必须与转码时的 EXIF 归一保持同一套方向,否则手机竖拍照片
// (像素横躺 + Orientation=6)会被误判为比例不符。
if err := asp.check(cfg, jpegOrientation(fileData)); err != nil {
return "", err
}
var uploadData []byte
var contentType string
// 重置 reader,以便后续可能的 resize 操作
if _, err := fileReader.Seek(0, io.SeekStart); err != nil {
return "", errors.Wrap(err, "reset reader")
}
// 4. 按格式转换或直接使用原图
switch format {
case "gif":
@@ -146,13 +154,12 @@ func uploadPictureToS3(file multipart.File, fileHeader *multipart.FileHeader, as
case "webp":
// 已是 WebP:仅当超尺寸上限时才重编(重编会再丢一代画质),
// 尺寸合规则原样上传。
cfg, _, err := image.DecodeConfig(bytes.NewReader(fileData))
if err == nil && fitsWithin(cfg.Width, cfg.Height, spec.MaxSide) {
if fitsWithin(cfg.Width, cfg.Height, spec.MaxSide) {
uploadData = fileData
contentType = "image/webp"
break
}
webpData, err := ConvertStaticImageToWebP(fileReader, spec)
webpData, err := ConvertStaticImageToWebP(fileData, spec)
if err != nil || len(webpData) >= len(fileData) {
uploadData = fileData
contentType = mimeTypeFromFormat(format)
@@ -162,7 +169,7 @@ func uploadPictureToS3(file multipart.File, fileHeader *multipart.FileHeader, as
}
default:
// 静态图片(JPEG / PNG 等)转静态 WebP
webpData, err := ConvertStaticImageToWebP(fileReader, spec)
webpData, err := ConvertStaticImageToWebP(fileData, spec)
if err != nil || len(webpData) >= len(fileData) {
// 转换失败、或转换后并不比原图小(小图标/线框图/已优化 JPEG
// 上有损 WebP 可能反而更大)→ 一律沿用原图。
@@ -211,16 +218,10 @@ func uploadPictureToS3(file multipart.File, fileHeader *multipart.FileHeader, as
// ConvertStaticImageToWebP 将静态图片(JPEG/PNG 等)按给定档位转换为 WebP 格式。
// spec.MaxSide 同时约束宽与高(此前只限宽,竖图长边不受约束)。
// 调用方负责比较转换前后的体积,决定是否采用转换结果。
func ConvertStaticImageToWebP(r io.Reader, spec EncodeSpec) ([]byte, error) {
// 先整体读入并校验像素总量(防解压炸弹),再完整解码。
data, err := io.ReadAll(r)
if err != nil {
return nil, errors.Wrap(err, "read image")
}
if err := imagecheck.CheckPixelLimit(bytes.NewReader(data)); err != nil {
return nil, errors.Wrap(err, "check image pixel limit")
}
//
// 接收已读入的字节而非 io.Reader:调用方手里已有原始字节,避免再包一层
// bytes.Reader 重复持有整份数据。
func ConvertStaticImageToWebP(data []byte, spec EncodeSpec) ([]byte, error) {
img, _, err := image.Decode(bytes.NewReader(data))
if err != nil {
return nil, errors.Wrap(err, "decode image")