webp fix v2
This commit is contained in:
+35
-34
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user