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
+55
View File
@@ -122,6 +122,7 @@
### 踩坑记录(都是我自己写错的)
1. **`gifFrameCount` 恒返回 0**:漏跳 7 字节 Logical Screen Descriptor,
`pos` 停在 LSD 首字节(宽度低位)→ 首轮命中 `default` 返回 0。
LSD 的 packed 在 `data[10]`(=pos+4),**必须无条件 pos += 7 再按 flag 加色表**。
2. **`buildJPEGWithOrientation` panic**:`entry[10:12]` 只有 2 字节却调 `PutUint32`。
@@ -132,3 +133,57 @@
5. **WebP 桩不产出字节** → 依赖真实编码器的端到端断言在本机无法成立,
已改为 `t.Skip`(桩环境跳过),像素级映射由单测覆盖。
---
## 部署后线上验证(用户已部署 box-dev-api)
新增 `dev/devcheck-avatar.py`(头像链路,可复跑)。**A 分档线上确认生效**:
上传 1600x1600 JPEG(124,941 B)→ 存储为 **512x512 WebP, 1,496 B**。
### 🔴 线上验证暴露的真实缺口(待修)
**EXIF 归一的位置太晚,比例校验看不到归一后的方向。**
- `s3.go:110` `asp.check()` 只用 `image.DecodeConfig` 拿宽高,**不读 EXIF**;
- EXIF 归一在 `ConvertStaticImageToWebP` 内部(第 240+ 行),**晚于比例校验**。
- 后果:手机竖拍照片(像素 4032x3024 + Orientation=6,实际显示 3024x4032)
直传头像会被判「比例不符 1:1(偏差 0.333)」而拒绝——**尽管用户看到的是方形**。
实测线上确实如此:传 1536x512+EXIF=6 被拒,头像 URL 未变。
- 前端走 cropper(canvas 会归一方向)不受影响,故只影响**绕过前端的直传**。
- 修法方向:`asp.check` 先读 `jpegOrientation`,对 5/6/7/8 交换宽高再比比例。
别为一次校验就完整解码(大图内存),只做 EXIF 段解析 + 宽高交换即可。
### 验证脚本踩坑
- 提问成功页**不渲染图片**(`templates/question/success.html` 无图片区),
图片只在 `question.go:50` 的**详情页**渲染;且提问图 key **不带 `picture/` 前缀**
(是 `<年>/<月>/<纳秒>.<ext>`),头像才带 `picture/`。→ 断言正则要分开写。
- `/user/profile/update` 是完整资料表单,`UpdateProfile` 有 12+ 个 `required` 字段
(`internal/form/user.go:7`)。只发 `_csrf` 会被挡在 Bind 阶段、头像根本不上传。
做法:从 profile 页**抓取所有 input/select/textarea 的 name=value 回填**。
- 头像上传成功是 **302**,头像 URL 在跳转后的页面里(不是响应体)。
## 补完 B1 + B3(2026-10-15 夜)
### B1 补完:EXIF 比例校验缺口
- `aspect.check` 签名改为 `(cfg image.Config, orientation int) error`——**不再自己DecodeConfig**,
由调用方传入已解析的宽高,顺带消除一次重复的文件头读取。
- 内部:`orientationSwapsDimensions(orientation)` 为真(5/6/7/8)则交换 w/h 再比比例。
- `imagecheck` 新增 `CheckAspectRatioSize(w, h, wantW, wantH, tol)`;
`CheckAspectRatio(r,...)` 改为薄封装(内部走 DecodeConfig + 调Size 版),行为不变。
- `exif.go` 抽出 `orientationSwapsDimensions`,`applyJPEGOrientation` 里的 switch 改为调它(去重)。
### B3:合并重复解码(峰值内存减半)
- 根因:`uploadPictureToS3` 里 `image.Decode` 为拿 format 解一遍 → `ConvertStaticImageToWebP` 内再解一遍。
- 改法:`image.DecodeConfig` 一次同时拿 **format + 宽高**(不解像素),
供「像素上限校验 / 比例校验 / WebP 透传判断」三处共用;完整解码只发生在真正要转码时。
- `ConvertStaticImageToWebP` 签名 `(io.Reader, spec)` → `([]byte, spec)`:
调用方手里已有原始字节,不必再包一层 `bytes.Reader`(也不必内部 `io.ReadAll` 复制)。
内部随之删掉重复的 `CheckPixelLimit`(调用方已在解码前校验过)。
- `route/question/page.go` 同步:删掉 `image.Decode`取 format 与 `DecodeConfig`取 cfg 的两次调用,
改为一次 `DecodeConfig`;新增本地 `fitsWithinSide(cfg, maxSide)`。
- `dev/s3stub/s3_stub.go` 同步签名(去掉 `io` import)。
- **实测堆峰值增量**(runtime.MemStats,含保留引用强制不回收):
| 输入 | 一次解码 | 两次解码 |
|---|---|---|
| JPEG 4000×3000 (2.3MB) | 18.1 MB | 36.1 MB |
| PNG 3000×3000 (33KB) | 36.1 MB | 72.1 MB |
→ 正好 2 倍。1C1G 上并发上传大图时这是决定性的。