fix(file): 修掉 EXIF 朝向 bug(竖拍照片会躺倒)+ 上传改一步 + 默认不留原始件
一、客户决定:不用保持原件,只要压缩满足效果
fill.storage.keep-raw 开关,默认 false。交付物就是压缩后的规范件。
顺带把上传从两步变一步:
原来 upload(存原图)→ process(读原图→压缩→存规范件)
现在 upload(收上来→立刻压缩→只存规范件)
留原始件是两步存在的**唯一理由**;不要它之后这步只剩代价
(多一个"传了但没处理"的中间态、多一次往返、多一份存储)。
process 保留为"重新规范化",但没有原始件时**直接返回现状而不报错**。
二、🐛 抓到一个会命中绝大多数真实照片的 bug:EXIF 朝向
手机竖拍时**像素其实是横的** + 一个 Orientation=6(显示时转 90°)。
浏览器认这个标记所以看着是正的;而 ImageIO **不读 EXIF**、重编码又丢掉它 →
输出成了"像素横着、也没有标记"的图 → **机构看到的是一张转了 90° 的身份证**。
实测(修前):输入 3200×2400(横)+ O=6 → 输出 1600×1200(仍横)、EXIF=None ❌
修后:1200×1600(竖)✓ 且输出不再带 EXIF(否则会被转两次)
**为什么 E2E 一直没抓到**:测试图都是程序生成的、不带 EXIF;
而真实学生手机拍的竖拍照片几乎全带 —— 这个 bug 会命中绝大多数真实材料,
是靠"想到这一层"发现的,不是靠测试。
实现:module/file/service/ExifOrientation.java
· 自己解析 JPEG 的 APP1/EXIF(只用一个 tag,三十行,不引新依赖)
· **读不出来时一律"不转"** —— 宁可保持原样,也不凭猜测把图转错
· 八种朝向用**逐像素索引映射**,不用 AffineTransform:
索引一看就懂、容易对着规范核;AffineTransform 写错了很难发现(图还是能出,只是方向不对)
· **转正必须在任何缩放之前** —— 转置会互换宽高,尺寸计算得基于转正后的
三、为"前端压缩"铺路的关键改动
compress() 现在会判断"是不是已经达标了":
没触发缩放 && 没有 EXIF 需转正 && 体积本来就在上限内 → **原样返回,不重编码**
因为前端马上会按材料项目标先压一遍(会转正并去掉 EXIF),服务端再压就是
**JPEG 二次编码** —— 白掉一次画质。quality 用 NOT_REENCODED=0 标记"没重编码过",
排查"这张图为什么有点糊"时能一眼看出被压过几次。
这条对 P4 截图尤其合适:原样保持 PNG,表格线一点不糊。
四、顺手修的
· url/content 的 original=true:没有原始件时**优雅退回到交付件**并如实标注
(FileUrl 加 original 字段)—— 学生点"看原图"只是想看自己传的那份,
报"文件不存在"是纯粹的困惑
· sniffExt():按**字节幻数**判类型,不信文件名后缀(学生传的 .jpg 里装 PNG 是常事,
而这个结论会写进数据库、决定导出包的扩展名)
五、E2E
e2e-m3-file 37/37,新增第 7 节专守 EXIF:
输入 3200×2400(横)+ O=6 → 输出 1200x1600
✓ 输出是竖的 ✓ 长边仍受 P2 约束 ✓ 不再带 EXIF
原始件相关断言改成**按策略断言**(开/关两种都对),而不是写死"必须保留"——
那会让"改配置"看起来像"代码坏了"。
全量 467 条断言、0 失败。
⏳ 还没做:**前端压缩**("传 800KB 而不是 3MB")。本轮做的是为它铺路:
服务端不再二次编码 + EXIF 转正机制已就位(前端要用 JS 做同样的事)。
前端要做:照片类读 EXIF→canvas 旋正→长边 1600→q85;P1/P4 不压直传;
压完仍超就回退原图让服务端兜底。
⚠️ 流程坑(本轮白花三次编译):**不要把 Java 字符串字面量放进 PowerShell here-string
传给 Python** —— @"..."@ 会把 " 吃掉(return "bin"; 变成 return bin;)。
带引号的代码一律用 edit 工具改,不要过转义层。
This commit is contained in:
@@ -1399,3 +1399,96 @@ m1-materials 68 / m1-form 48 / m1-task 71 / m1-orgs 48
|
||||
3. 学生端提交前的**图片自检提示**(现在只有校验清单,预览已经能用,可以串起来)
|
||||
4. 待办(非阻塞):C1 **小程序资质**(已明确会阻塞学生真正开始填报)、
|
||||
C5 隐私声明法务文案、D1 OCR 引擎选型、D5 一寸照片阈值标定、D6 微信三通道体积上限真机实测
|
||||
|
||||
|
||||
---
|
||||
|
||||
## §一·二十 ⚠️ 抓到一个会命中"绝大多数真实照片"的 bug:EXIF 朝向
|
||||
|
||||
### 客户决定(本轮起点)
|
||||
> **"不用保持原件,只要压缩可以满足效果即可。"**
|
||||
|
||||
于是:**不再保留学生上传的原始件**,交付物就是压缩后的规范件。
|
||||
(顺带把上传从两步变一步,见下。)开关 `fill.storage.keep-raw` 保留,默认 false。
|
||||
|
||||
### 🐛 EXIF:一个真实学生照片几乎必然带、而测试图从不带的坑
|
||||
|
||||
**手机竖着拍时,文件里的像素其实是横的**,外加一个 `EXIF Orientation=6`
|
||||
(意思是"显示时顺时针转 90°")。浏览器认这个标记,所以学生自己看着是正的。
|
||||
|
||||
但 Java 的 `ImageIO.read()` **不读 EXIF**,重编码又会把 EXIF 丢掉 →
|
||||
输出成了"**像素横着、也没有标记**"的图 —— **机构看到的是一张转了 90° 的身份证**。
|
||||
|
||||
**实测确认**(修前):
|
||||
```
|
||||
输入:像素 3200×2400(横)+ Orientation=6 → 应为竖的
|
||||
输出:1600×1200(仍是横的),EXIF Orientation=None ❌ 躺倒
|
||||
```
|
||||
**修后**:`1200×1600`(竖)✓ 且输出里不再带 EXIF(否则会被转两次)
|
||||
|
||||
**为什么 E2E 一直没抓到**:测试图都是程序生成的、**不带 EXIF**;
|
||||
而真实学生用手机拍的,**竖拍几乎全都带这个标记** —— 也就是说这个 bug
|
||||
会命中绝大多数真实材料,而我们是靠"想到这一层"才发现的,不是靠测试。
|
||||
|
||||
**实现**:`module/file/service/ExifOrientation.java`
|
||||
- 自己解析 JPEG 的 APP1/EXIF(只用到一个 tag,三十行,**不引新依赖**)
|
||||
- **读不出来时一律"不转"**:宁可保持原样,也不要凭猜测把图转错
|
||||
- 八种朝向用**逐像素索引映射**,不用 `AffineTransform` —— 索引一看就懂、
|
||||
容易对着规范核;AffineTransform 的平移/旋转组合写错了很难发现(图还是能出,只是方向不对)
|
||||
- **转正必须在任何缩放之前**:转置会互换宽高,长边/固定尺寸的计算得基于转正后的尺寸
|
||||
|
||||
### 流程从两步变一步
|
||||
```
|
||||
原来:upload(存原图)→ process(读原图 → 压缩 → 存规范件)
|
||||
现在:upload(收上来 → 立刻压缩 → 只存规范件)
|
||||
```
|
||||
留原始件是两步存在的**唯一理由**;客户不要它之后,这一步就只剩代价了
|
||||
(多一个"传了但没处理"的中间态、多一次往返、多一份存储)。
|
||||
`process` 保留为"重新规范化",但**没有原始件时直接返回现状而不报错** ——
|
||||
否则"上传后顺手再调一次"的无害调用会炸掉。
|
||||
|
||||
### 为"前端压缩"铺路的一个关键改动
|
||||
`FileProcessService.compress()` 现在会判断**"是不是已经达标了"**:
|
||||
```
|
||||
没触发缩放 && 没有 EXIF 需要转正 && 体积本来就在上限内 → **原样返回,不重编码**
|
||||
```
|
||||
因为前端马上会按材料项的目标先压一遍(它会转正并去掉 EXIF),
|
||||
服务端再压一次就是 **JPEG 二次编码** —— 白掉一次画质,换不回任何东西。
|
||||
`quality` 用 `NOT_REENCODED = 0` 标记"没重编码过",排查"这张图为什么有点糊"时
|
||||
能一眼看出它被压过几次。
|
||||
|
||||
> 这条对 **P4 截图** 尤其合适:原样保持 PNG,表格线一点不糊。
|
||||
|
||||
### 顺手修的
|
||||
- `url(original=true)` / `content(original=true)`:**没有原始件时优雅退回到交付件**
|
||||
并如实标注(`FileUrl` 加了 `original` 字段)——
|
||||
学生点"看原图"只是想看自己传的那份,报"文件不存在"是纯粹的困惑
|
||||
- `sniffExt()`:按**字节幻数**判类型,不信文件名的后缀
|
||||
(学生上传的 `.jpg` 里装一张 PNG 是常事,而这个结论会写进数据库、决定导出包的扩展名)
|
||||
|
||||
### E2E
|
||||
- `e2e-m3-file.py` **37/37**,新增第 7 节专门守 EXIF:
|
||||
```
|
||||
输入:像素 3200×2400(横)+ Orientation=6
|
||||
输出:1200x1600
|
||||
✓ 输出是竖的 ✓ 长边仍受 P2 约束(≤1600) ✓ 不再带 EXIF(否则会被转两次)
|
||||
```
|
||||
- 原始件相关的断言改成**按策略断言**(`keep-raw` 开/关两种都是对的),
|
||||
而不是写死"原始件必须在" —— 那会让"改配置"看起来像"代码坏了"
|
||||
- **全量 467 条断言、0 失败**(m0 36 / m0-user 55 / m1 系列 235 / m3 系列 141)
|
||||
|
||||
### ⏳ 还没做(下一步)
|
||||
**前端压缩("传 800KB 而不是 3MB")还没做** —— 本轮做的是**为它铺路**:
|
||||
服务端不再二次编码 ✓ + EXIF 转正机制已就位(前端要用 JS 做同样的事)✓
|
||||
|
||||
前端要做的是:
|
||||
1. **照片类**(P2/P3):读 EXIF → canvas 旋正 → 长边降到 1600 → q85 → 上传
|
||||
2. **P1 证件照 / P4 截图**:**不压**,直传原图(canvas 只能出 JPEG,会把表格线糊掉 ✗)
|
||||
3. **压完仍超 / 压失败** → 回退成原图上传,让服务端兜底(不能让界面卡住)
|
||||
4. `uni.compressImage`(小程序端)与 canvas(H5)行为要对齐
|
||||
|
||||
### 环境/流程备注(本轮又踩的坑)
|
||||
**不要把 Java 字符串字面量放进 PowerShell here-string 里传给 Python** ——
|
||||
`@"..."@` 会把 `"` 吃掉,于是 `return "bin";` 变成 `return bin;`、
|
||||
`throw new BizException(..., "文案")` 变成语法错误。
|
||||
本轮为这个坑白花了三次编译。**带引号的代码一律用 edit 工具改,不要过转义层。**
|
||||
|
||||
Reference in New Issue
Block a user