手機上明明轉正了,傳到電腦又躺倒
前年在手機上拍的一張照片,相簿裡點了一下旋轉,看著是正的;傳給電腦拖進一個老式看圖軟體,又躺回去了。當時懷疑沒存成功,來回轉三趟,瀏覽器裡那張被轉了兩遍,直接倒過來。
後來才看清:相簿那次旋轉沒動像素,只改了一個叫 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 個像素改變。