雜記 › 技術筆記 › GIF 縮完尺寸只剩首格:一個檔名叫 .gif 的 PNG 檔案

GIF 縮完尺寸只剩首格:一個檔名叫 .gif 的 PNG 檔案

一張做了半天的動圖,為了塞進對話框把寬從 200 改成 100,下載回來發現它不動了。這不是「縮得太小所以看不出來」——把它當成二進位檔案打開,前 8 個位元組是 89 50 4E 47 0D 0A 1A 0A,PNG 的簽名;GIF 應該以 GIF8 開頭。而副檔名還是 .gif。我把這條路徑從頭查了一遍,結論比預想的更靠底層。

先把釘得住的事實攤開:

  • 原始檔 anim_2frame.gif 597 B,以位元組級走訪讀到 2 個圖形控制擴充和 2 個影像描述符,影格延時是 30 和 30;產物 1028 B 裡這兩類塊的計數都是 0,APNG 的 acTL 也是 0。
  • 5 個影格的 fiveframe_rg.gif 5223 B 和只有 1 個影格的 oneframe_rg.gif 1071 B,餵同一個輸出尺寸,產物位元組一模一樣,都是 1028 B。
  • 品質滑桿從 10 拉到 100,GIF 的三次產物全是 1028 B;同一根滑桿對 JPEG 給出 2038 B 與 54687 B。

影格數不能靠解碼器回報,也不能數裸位元組

樣板裡 frame、delay、loop、fps 出現 0 次,drawImage 全文只有 1 次(第 224 行):取影格這個動作根本不存在,只有一次把整張 <img> 重畫到畫布上的重繪。

但「代碼裡沒寫」不等於「產物裡沒有」。影格數得實測。ImageDecoder 在本機這份建置裡對這批樣張讀不出影格,不能當證據;退一步去數 GIF 影像描述符那個 0x2C 也會翻車:2912 B 的 PNG 產物上樸素掃描數出 9 個,3068 B 那份數出 7 個,zlib 壓完的資料裡這些位元組自然會出現。所以影格數一律走結構化走訪,再用產物的 chunk 鏈(IHDR IDAT IDAT IEND)與 acTL=0 確認它既不是 GIF 動畫也不是 APNG。

像素側更直接:twoframe_rg.gif 第一個影格是紅、第二個影格是純藍,畫到畫布之後 meanR=179.52 / meanB=0,藍通道恆為 0;t0、t700、t2200 三次取樣的數值一字不變——畫上去的就是首格。

產物位元組與來源影格數完全脫鉤

源位元組與產物位元組 原始檔 產物 597 1028 5223 1028 1071 1028 2 影格 5 影格 1 影格

圖 1|三張動圖的影格數與來源體積都不同,同一輸出尺寸下的產物卻全停在 1028 B;灰柱是來源位元組,綠柱是產物位元組,1 個影格那張是對照組。

產物體積只跟畫布上的像素有關,跟來源檔案裡疊了幾層無關。

請求、檔名、魔數,三樣各說各話

口徑實測
第 227 行交給編碼器的 mimeimage/gif
第 246 行拼出的下載檔名anim_2frame_resized.gif
產物前 8 個位元組89 50 4E 47 0D 0A 1A 0A
同畫布顯式請求 image/png384 B,雜湊 cf6a3a06,與上一行逐位元組相同

回落不在代碼裡:第 228 行那條格式改寫只管 image/bmp,GIF 在第 227 行是無條件透傳 originalFile.type。瀏覽器根本沒有 GIF 編碼器,於是 toBlob 安靜地改用 PNG。表格最後一列就是證據:請求換成 image/png,長度與雜湊一位不差。

品質滑桿對 GIF 是一根空桿

滑桿取 10、90、100 時,第 229 行 parseInt(value) / 100 確實把 0.1、0.9、1 傳進了 toBlob,但三次產物都是 1028 B、雜湊都是 5808201c,最低檔與最高檔逐位元組相同。同一根滑桿對 JPEG 樣張給出 2038 B 與 54687 B——滑桿沒壞,是 PNG 編碼器不讀這個參數。

編碼器在場也一樣掉影格

「瀏覽器沒有 GIF 編碼器」解釋得了格式,解釋不了影格的去向。拿動畫 WebP 做對照:338 B 那張有 2 個 ANMF 塊,產物 620 B 仍是真的 WebP(魔數 52 49 46 46),ANMF 卻是 0;582 B 那張有 3 個 ANMF,產物 622 B、ANMF 也是 0。編碼器明明在運作,動畫控制塊照樣全數消失——掉格的地點在畫布這一側。這兩張只是對照樣本,不是可選的輸出格式。

透明比動畫活得久

twoframe_rg.gif 在 200x120 下有 7104 個 alpha 小於 255 的像素(共 24000),縮到 100x60 是 1776 個(共 6000),放大到 400x240 是 29500 個(共 96000),其中 2176 個是重取樣在透明邊界上新造的半透明像素。產物 PNG 頭 depth=8 ctype=6,alpha 通道完整保留——掉的是時間維度,顏色反而留得更完整。

上限只判一邊,縮小尺寸也不等於縮小體積

第 216 行是 w > 10000 || h > 10000,只看單邊,代碼裡沒有總像素上限。實測 2000x2000(4000000 像素)產物 84599 B、4000x4000(16000000 像素)產物 316985 B,請求的都是 image/gif,魔數都是 PNG。體積也一樣:597 B 那張依原尺寸 200x120 輸出反而得到 1028 B,比來源檔案大 431 B;只有降到 100x60 才第一次小於 597 B,是 384 B。

副檔名不算數,失敗也不吭聲

能不能進門只看第 129 行對 file.type 的那處正則,副檔名和魔數都不參與:5928 B 的 PNG 改名 .gif 被嗅探成 image/gif,依 640x640 解碼成功後照樣走完 GIF 那條匯出分支。壞檔案給的回饋是零——樣板只在第 141 行掛了 onload,全文 onerror 出現 0 次,72 B 的 corrupt.png 與 0 B 的 GIF 實測 toast 數都是 0。體積閘門是嚴格大於 20971520 B,要到 20971521 B 才觸發第 134 行那句提示。

文案說對了前半句

工具頁那段介紹寫著「本工具仅调整GIF第一帧的尺寸,不改变动画播放」,由 layout/tool.html 第 66 行渲染,讀者看得見。前半句與實測吻合,後半句被產物證偽:動畫控制塊一個都沒有,不是「沒被改變」,而是不存在。也別以為 GIF 有專屬優遇,全文 gif 只出現 4 次,全在第 11、12、129、130 行的文案與白名單裡。要逐影格處理,站內把 GIF 拆成一格一格那篇方向相反,這裡不重複;只想替首格換個大小,調整 GIF 尺寸這一步沒問題,只是拿到的是 PNG——這份樣板被五個工具共用,橫向對比可走通用的圖片尺寸調整。

Q:手動改一次副檔名,能把動畫救回來嗎?

不能。改副檔名只改名字,檔案裡本來就沒有第二個影格,也沒有影格延時和迴圈計數,走訪產物得到的是空陣列。

Q:那這張產物到底該怎麼用?

把它當成靜態 PNG 用:alpha 通道在,首格的透明邊界也在,比原始那張多影格 GIF 更好控制。

Q:寬高與品質之外,還有別的參數能保住動畫嗎?

沒有。介面上能動的只有寬高和品質,而後者對 GIF 連體積都不影響。

📎 相關文章

171! 才溢出,23! 就已經算錯:階乘頁那句「0~1000 不會溢出」的實測邊界 2026-10-27 一個漢字只算 5 bits:密碼強度評估器的破解時間什麼時候變成宇宙壽命級別 2026-10-27 文字↔ASCII 裡的「ASCII」其實是 UTF-8:1 個漢字吐出 3 個數,無分隔檔 100% 回不去 2026-10-27 它把餘數為 0 的那一行也打給你:GCD 過程展示頁的步數、摺疊順序與 29 步斐波那契 2026-10-27 BMP 進、PNG 出:12342 位元組的位圖變成 281 位元組,檔名卻還寫著 .bmp 2026-10-27