PNG 檔案格式
Portable Network Graphics,一種無損的點陣圖格式。
- MIME 類型
- image/png
- 副檔名
- .png
- 分類
- 圖片
Magic number(檔案簽章)
| 0x00000000 | 89504E470D0A1A0A | ·PNG···· | PNG magic + CRLF guard |
關於 PNG 格式
PNG 儲存影像時完全不損失畫質,並支援完整的 alpha 透明度,因此成為螢幕截圖、logo、圖示,以及任何邊緣銳利的圖形的預設選擇。每個 PNG 都以 8 位元組的簽章開頭,其中刻意包含一組換行字元 — 這樣一旦檔案經過文字模式傳輸而損壞,就會明顯壞掉而不是悄悄出錯。
檔頭結構,一個位元組一個位元組看
| 位移 | 長度 | 欄位 | 意義 |
|---|---|---|---|
| 0 | 8 | PNG signature | 固定是 89 50 4E 47 0D 0A 1A 0A。第一個位元組的最高位元被設起來,7 位元的傳輸一剝掉就看得出來;第 1 到 3 個位元組拼出 PNG;後面那組 CR LF、DOS 的檔案結束符 1A 和結尾的 LF 存在的唯一理由,就是讓被文字模式傳輸搞爛的檔案大聲壞掉,而不是默默出錯。 |
| 8 | 4 | IHDR chunk length | IHDR 資料的位元組數,大端序,固定是 00 00 00 0D — 13 個位元組。PNG 的每個 chunk 都是長度、類型、資料、CRC 這個順序,而 IHDR 一定是第一個。 |
| 12 | 4 | IHDR chunk type | 四個 ASCII 字母 IHDR。chunk 類型的大小寫本身有意義:首字母大寫代表這個 chunk 是關鍵的,解碼器看不懂就必須拒絕整個檔案,不能跳過去。 |
| 16 | 4 | Width | 影像寬度(像素),大端序 32 位元無號整數。0 不合法,規格上限是 2^31 - 1。工具報出誇張的尺寸,幾乎都是把這四個位元組當成小端序在讀。 |
| 20 | 4 | Height | 影像高度(像素),編碼方式和寬度一模一樣。這裡加上位移 16,是 PNG 唯一交代自己尺寸的地方 — 檔案裡沒有第二份可以拿來對照。 |
| 24 | 1 | Bit depth | 每個取樣的位元數:1、2、4、8 或 16。不是每種深度都配得上每種色彩類型 — 索引色最多到 8,兩種帶 alpha 的類型則從 8 起跳。 |
| 25 | 1 | Colour type | 0 灰階、2 全彩、3 索引色(必須有 PLTE chunk)、4 灰階加 alpha、6 全彩加 alpha。PNG「透明度不見了」的時候就是讀這個位元組:本來寫 6 的地方變成 2。 |
| 26 | 1 | Compression method | 固定是 0,代表 deflate,滑動視窗最大 32768 位元組。從來沒有第二種方法被登記過,所以只要不是 0 就是標頭壞了。 |
| 27 | 1 | Filter method | 固定是 0,也就是那組五種、逐掃描線自適應的濾波器。每一列實際用哪一種,是壓縮串流裡面的一個位元組,不在標頭這裡。 |
| 28 | 1 | Interlace method | 0 是不交錯,1 是 Adam7。Adam7 讓檢視器可以先畫出一張粗略的圖,代價是檔案大個幾個百分比;現在幾乎沒有東西會產生它。 |
| 29 | 4 | IHDR CRC-32 | 對 chunk 類型和 chunk 資料算出來的 CRC-32,不包含長度欄位。位移 12 到 28 之間動任何一個位元組,這裡就對不上 — 手改標頭會被 hex 編輯器抓到就是靠這個。 |
| 33 | variable | First chunk after IHDR | 下一組長度加類型。通常是 IDAT,不然就是 PLTE、tRNS、gAMA、sRGB、iCCP、pHYs、eXIf 或 acTL 之一 — 那些必須排在像素資料前面的色彩與中介資料 chunk。 |
| -12 | 12 | IEND trailer | 完好的 PNG 一律以同樣的十二個位元組結尾:00 00 00 00 49 45 4E 44 AE 42 60 82,一個長度為零的 IEND chunk 加上它那個固定的 CRC。結尾不是這串,檔案就是被截斷了。 |
怎麼跟長得像的格式分開
| 容易搞混的 | 怎麼分 |
|---|---|
| APNG | 動畫 PNG 的前 33 個位元組跟一般 PNG 完全一樣 — 同樣的簽章、同樣的 IHDR。差別在多了一個 acTL 動畫控制 chunk,規格要求它必須排在第一個 IDAT 之前,所以在前幾百個位元組裡找 61 63 54 4C 就對了。不認得 acTL 的解碼器只會畫出靜止的第一格,而這正是設計上預期的降級行為。 |
| ICO | 現在的 .ico 可以直接把整張 PNG 塞進去,所以 hex dump 裡確實會出現 PNG 簽章 — 只是不在位移 0。ICO 開頭是 00 00 01 00,接著一份目錄,每一筆記著各張影像的位移;PNG 簽章就坐在其中某個位移上。斷定它是 PNG 之前,先看位移 0。 |
| JPEG | 沒什麼玄機:FF D8 FF 對上 89 50 4E 47。但還是值得查,因為大多數編輯器的「另存為 PNG」是把 JPEG 已經有損的像素再編一次,不是真的轉換;而且截圖被改名成 .jpg 之後,裡面還是 PNG 的位元組,照樣到處都開得起來。 |
| WebP | 兩者都能處理帶 alpha 的無損資料,也都常出現在截圖輸出。WebP 是 RIFF 檔案:位移 0 是 52 49 46 46,接四個位元組的小端序長度,位移 8 是 57 45 42 50。它裡面任何地方都不會有 PNG 簽章。 |
常見的壞法
- 文字模式傳輸。老的 FTP 用戶端和某些 CI 的產出物步驟會轉換換行字元,位移 4-5 的 0D 0A 就塌成單一個 0A,整個檔案往前位移一個位元組,那之後每個 chunk 的 CRC 全部對不上。那組簽章當初就是為了抓這件事設計的。
- 下載被截斷。簽章和 IHDR 都完好,所以尺寸還讀得出來,檢視器會把影像上半部畫出來,然後在 IDAT 串流用完的地方掉進一片灰色或透明的列。判斷方式是看最後十二個位元組,IEND 不見了。
- chunk 裡面有一個位元組被翻掉。libpng 會因為 CRC 對不上而拒收,瀏覽器通常照樣畫出來,於是同一張 PNG 在 Chrome 裡「正常」,在 build script 裡就掛掉。把每個 chunk 的 CRC 重算一次,就知道是哪一個被動過。
- 前面被塞了垃圾。一個多出來的 UTF-8 BOM、一頁 HTML 錯誤訊息,或是在影像本體之前噴出來的 PHP warning,都會把簽章擠離位移 0。影像的每個位元組其實都還在;解法就是把 89 50 4E 47 之前的東西全部刪掉。
- 色彩類型被悄悄改寫。某個流程用色彩類型 2 而不是 6 重新編碼,alpha 通道就沒了。結果是一張完全合法、但沒有透明度的 PNG,而整個檔案裡只有第 25 個位元組會承認這件事。
這個格式現在還在用嗎
只要圖裡有硬邊或透明度,PNG 到現在都還是預設答案:螢幕截圖、UI 素材、logo、示意圖、圖示,還有任何要放進 README 的東西。照片這一塊它幾十年前就輸給 JPEG,近年又被 WebP 和 AVIF 拉開 — 那兩個在漸層多的影像上把 PNG 打得很難看,而且一樣有 alpha。PNG 真正無可取代的場合是「之後還會再編輯」的檔案,因為它是無損的:一張截圖存成 PNG,來回裁切、加註解十幾次都不會變樣,同一張存成 JPEG 則每存一次就肉眼可見地爛一點。它從 1997 年就被所有瀏覽器支援,所以不像那些新格式,永遠不需要準備 fallback。
在這個站上打開 PNG 檔
這些全部在你的瀏覽器裡跑,檔案不會離開你的電腦。
相關格式
容易和 PNG 搞混的格式,可以比對它們的開頭位元組。