perf: 文章图片 WebP 转换改为二分查找质量,目标 ≤ 原文件大小

- 不再仅试 quality 80,改为二分查找 (50~80) 找最高质量 ≤ 原文件大小
- 新增 encodeWebPBinary 辅助函数:先试 80,太大则二分降到 50
- 降到 50 仍超限时回退原格式,避免产生马赛克级质量
- 与头像的激进二分(最低 quality 1)区分,文章图片保底 quality 50
This commit is contained in:
2026-05-31 03:08:33 +08:00
parent 6ea4e5365e
commit d8e58b5f8c

View File

@ -416,22 +416,19 @@ func (ctrl *PostController) UploadImage(c *gin.Context) {
isGIF := ext == ".gif"
// 强制解码验证文件是否为有效图片(防伪扩展名、图片投毒),验证失败直接拒绝
// JPEG/PNG解码后顺便做 WebP 压缩
// JPEG/PNG解码后做 WebP 压缩(二分查找质量,目标 ≤ 原文件大小)
if isJPEGPNG {
img, _, err := image.Decode(bytes.NewReader(data))
if err != nil {
common.Error(c, http.StatusBadRequest, "图片格式无效,请上传有效的图片文件")
return
}
// WebP 压缩存储保留原尺寸quality 80
var buf bytes.Buffer
if err := webp.Encode(&buf, img, &webp.Options{Quality: 80}); err == nil {
if buf.Len() > 0 && buf.Len() < len(data) {
filename := fmt.Sprintf("%d_%d.webp", uid, ts)
savePath = filepath.Join(storageDir, filename)
if err := os.WriteFile(savePath, buf.Bytes(), 0644); err == nil {
url = "/uploads/posts/" + filename
}
webpData := encodeWebPBinary(img, len(data))
if webpData != nil && len(webpData) < len(data) {
filename := fmt.Sprintf("%d_%d.webp", uid, ts)
savePath = filepath.Join(storageDir, filename)
if err := os.WriteFile(savePath, webpData, 0644); err == nil {
url = "/uploads/posts/" + filename
}
}
}
@ -452,7 +449,7 @@ func (ctrl *PostController) UploadImage(c *gin.Context) {
}
}
// 原格式存储(WebP / GIF / 转换后反而更大
// 原格式存储(GIF / 已有 WebP / 二分压缩仍超原文件大小
if savePath == "" {
filename := fmt.Sprintf("%d_%d%s", uid, ts, ext)
savePath = filepath.Join(storageDir, filename)
@ -474,3 +471,33 @@ func (ctrl *PostController) applyShortcode(post *model.Post) {
}
}
// encodeWebPBinary 二分查找 WebP 质量,找 ≤ targetBytes 的最高质量quality 50~80
// 返回 nil 表示最低质量仍超限,应由调用方回退原格式
func encodeWebPBinary(img image.Image, targetBytes int) []byte {
var buf bytes.Buffer
if err := webp.Encode(&buf, img, &webp.Options{Quality: 80}); err != nil {
return nil
}
if buf.Len() <= targetBytes {
return buf.Bytes()
}
low, high := 50, 80
var best []byte
for low <= high {
mid := (low + high) / 2
buf.Reset()
if err := webp.Encode(&buf, img, &webp.Options{Quality: float32(mid)}); err != nil {
return nil
}
if buf.Len() <= targetBytes {
best = make([]byte, buf.Len())
copy(best, buf.Bytes())
low = mid + 1
} else {
high = mid - 1
}
}
return best
}