手机上明明转正了,发到电脑又躺倒
前年在手机上拍的一张照片,相册里点了一下旋转,看着是正的;传给电脑拖进一个老版看图软件,又躺回去了。当时怀疑没存成功,来回转三趟,浏览器里那张被转了两遍,直接倒过来。
后来才看清:相册那次旋转没动像素,只改了一个叫 Orientation 的标记;而在 图片旋转翻转 里点一次"向右转 90°",像素是真搬了一遍,两件事付的账完全不同。
只改标签,像素一行没动
核验时手写了一组 JPEG,把 APP1 段(FFE1 加 Exif\0\0 加小端 TIFF)里标签 0x0112 依次写成 1 到 8。声明 6 的那张 1268 B,剥掉元数据再解码是 80×120——文件里躺着的像素真躺了 90°,"正"只活在标签里;带着标签解码,naturalWidth 乘 naturalHeight 就是 120×80。
读标签的看到正的,不读的只能按存储像素排版——那次"旋转"改的是看法,不是文件。
浏览器替代码把方向还了回来
模板一共 206 行,grep 一遍 exif、orientation、createImageBitmap,命中 0 处:它一行都不解析方向标记。解码只有 new Image() 配 URL.createObjectURL,onload 接住转正后的两个值(第 130 行)。
Orientation 1 到 8 全被还原成同一个显示方向:存储 80×120 的 5、6、7、8 进工具后都是 120×80,导出画布内容两两相同,1–4 同 hash 962d4215、5–8 同 hash da1c444c。写法不规范也会翻车:同一份 IFD、同一个 6,PNG 的 eXIf 写成规范裸 TIFF 那份 727 B 被转正成 80×120,照搬 JPEG 式前缀的 733 B 那份被忽略、仍是 120×80,信息行与文件名一起变。
图 1|存储宽高、浏览器还回来的宽高与两组画布 hash。
真旋转要付的两笔账
90° 的置换本身精确:det_q90 的 90° 画布与手工整倍重排的参考同为 160x240:eceb4a99:153600:24483724,无插值。但导出必过 canvas.toBlob(..., 0.92)(第 186–187 行),质量写死、界面调不了。det_q90.jpg 的 28210 B 就算停在 0° 直接下载也变 28003 B、38373 个像素改变、平均 13.814;q100 那张 104764 B 掉到 30510 B、38400 个像素全不同、平均 73.74。逐字节不变的只有 PNG:mark.png 581 B 导出还是 581 B。真要省的是体积(图片压缩)。
第二笔是元数据。23 条导出记录一律 outAPP1=0、outOri=null,PNG 输出只剩 IHDR/IDAT/IEND、不带 eXIf;那张写了 5 条 IFD 的手机照片,SAMSUNG、SM-G9910、2026:03:07 21:14:52 几个字符串字段导出后全部不存在。厂商、机型、拍摄时间、GPS 一起没了,换来转 90° 后输出 JPEG 的 SOF 直接写成 80×120,下游读不读标记都只剩这一个方向。
下载四趟比连点四趟伤得多
同一次会话里连点 4 次"向右转 90°",角度折回 0(0→90→180→-90→0),画布 hash 回到起点,两次导出都是 28003 B、逐字节相同。
实际动作是转一趟就下载再看:det_q90 起始 28210 B,4 轮产物依次 28373、26986、26324、26167 B,最终画布与原图 38396 个像素不同、平均偏差 24.549。4 个 90° 不等于没转。只有 PNG 真回了原点:mark.png 581 B 走 591、580、594、581 B,最终 hash 与原始相同、0 个像素差异,且每轮 orientationTags 都是 null。
两个会留痕的开关
关掉"扩大画布容纳整张图"不是少留白,是真裁:det_q90 关掉的画布仍是 240×160,只保住 25600 个不透明像素,90° 丢 12800(33.33%)、45° 丢 8823(22.60%)、30° 丢 7211(18.54%),只有 180° 一个像素不丢。
图 2|det_q90 关掉扩大画布后被裁掉的像素。
"保持原格式"只对 image/jpeg 成立(第 176 行):mark.webp 692 B 得到 1327 B 的 PNG,mark.bmp 28854 B 变 581 B,shape.svg 165 B 变 2687 B(SVG 一载入就按 180×120 光栅化),anim.gif 597 B 只留第一帧,逐帧得先 GIF 拆图。名实不符时更险:fake.webp 实为 1200×900 的 JPEG,39587 B,导出 504277 B 的 PNG。
填透明却选"JPG(不透明)"也不吭声:第 185 行把格式改成 PNG,alpha.png 1873 B 拿到的是 alpha_旋转200x120.png 1873 B,与"保持原格式"逐字节相同,下拉框还停在 JPG;填充改白色才编出 5342 B 的 JPG。
两种旋转的分工
| 事项 | 只改 Orientation | 真搬像素再导出 |
|---|---|---|
| 文件里的像素 | 80×120 不动 | 搬成 120×80,SOF 同步 |
| 字节 | 几乎不变 | 28210 B 变 28003 B |
| 元数据 | 只剩标记在变 | outAPP1=0、outOri=null |
| 不读标记的下游 | 仍然躺倒 | 只剩一个方向 |
下游都读标记时,改标签就够;要发进不读标记的老系统,就得真搬一次像素并接受重编码。另外一次只有 1 张:只读 files[0](第 88 行),载入新图时第 134 行 resetAll() 把 angle=135 与水平翻转清零,上一张的参数不带过去。
Q:在电脑上转正存一次,以后就不会再躺了吗?
真搬像素再导出才不会:那种文件标记归零、像素已正,读不读 EXIF 都只剩一个方向。只在相册里点旋转,下游一不读标记就还是躺的。
Q:转完为什么机型和拍摄时间都没了?
导出是画布重画再 toBlob,元数据进不了这条链路。23 条导出记录里那张 5 条 IFD 的 SAMSUNG 照片字符串字段全空,PNG 也不带 eXIf。
Q:JPEG 转 90° 到底会不会糊?
置换不糊(画布 hash 160x240:eceb4a99 与手工置换参考一致),糊在重编码:0.92 写死在第 186 行,det_q90 28210 B 停在 0° 也变 28003 B、38373 个像素改变。