便攜式網(wǎng)絡圖形(PNG)規(guī)范(第二版)

信息技術——計算機圖形和圖像處理——便攜式網(wǎng)絡圖形:功能規(guī)范(ISO/IEC 15948:2003-E)

2003年11月10日W3C正式推薦

當前版本:http://www.w3.org/TR/2003/REC-PNG-20031110

最新版本:http://www.w3.org/TR/PNG

先前版本:http://www.w3.org/TR/2003/PR-PNG-20030520

編輯:David Duce(牛津布魯克斯大學壮不,第二版)

作者:在網(wǎng)頁上自己找吧。

第二版內容相比第一版修正了一些規(guī)范设凹,相關細節(jié)請參閱勘誤表

另外,這個文檔還沒有被翻譯成中文蕊唐。

Copyright ? 2003 W3C? (MIT, ERCIM, Keio), All Rights Reserved. W3C liability, trademark, document use and software licensing rules apply.

摘要

便攜式網(wǎng)絡圖形(PNG)是一種可擴展的文件格式归敬,用于圖像的無損壓縮存儲。它可以保存圖片的多項內容硕勿,例如顏色索引哨毁、灰度通道、彩色通道源武、透明通道挑庶、1至16位的深度采樣等。是 GIF 格式的無專利代替品软能,也可以在一定范圍內代替 TIFF 格式使用迎捺。

該格式具有流式讀取,快速檢測文件完整性的特點查排,避免了文件傳輸中的由網(wǎng)絡波動導致的一些問題凳枝,在實際應用中表現(xiàn)出色。此外跋核,格式中可以添加伽馬曲線信息以及色度信息岖瑰,使其在不同平臺、不同的屏幕色差中表現(xiàn)一致砂代。

本文檔是專門用于闡釋 PNG 網(wǎng)絡流媒體格式的標準規(guī)范蹋订。

文檔狀態(tài)

本文檔是2003年10月14日公布的第二版PNG標準規(guī)范,同時也是國際標準 ISO/IEC 15948∶2003刻伊。除了封面和樣板差異露戒,文檔內容與國際標準內容一致。但不保證此文檔長期有效捶箱,請在W3C出版物列表上查找和更新最新版本智什。

文檔的內容基于1996年10月公布的第一版PNG標準規(guī)范,結合了所有已知的勘誤和澄清丁屎,已通過 ISO/IEC/JTC 1/SC 24荠锭、W3C、PNG 開發(fā)小組的審查晨川。截至公布日证九,已擁有超過180種圖像瀏覽器*、超過100種圖像編輯軟件*支持了 PNG 格式共虑;SVG* 瀏覽器也都完全支持了 PNG 格式愧怜;盡管 HTML 規(guī)范并沒有提及,但是仍有60款 HTML 瀏覽器*提供了選擇看蚜。相關軟件的支持可以訪問pngstatus獲取叫搁。

如需反饋,請通過郵箱聯(lián)系 PNG 組,問題和答復會在網(wǎng)頁上公布渴逻。

此文檔內容網(wǎng)上公開疾党,出版商 PNG 組不收取任何專利費用。

注意:

1惨奕、網(wǎng)頁上的這篇文檔雪位,為了保證圖片在 HTML 瀏覽器中正常顯示,同時提供了 SVG梨撞、PNG 兩種格式雹洗,在不同的瀏覽器中會顯示不同的版本。

2卧波、一些Bug时肿,現(xiàn)在已經(jīng)解決了。

文檔還不存在英語以外的版本(我是假的)港粱。

說明

本國際標準的設計目標是:

  1. 可移植性:格式的編碼螃成、解碼和傳輸應與軟件、硬件無關查坪。
  2. 完備性:盡可能兼容真彩寸宏、索引和灰度圖像,在每種情況下都可以添加透明度偿曙、顏色空間信息和輔助信息(如文本注釋)氮凝。
  3. 串行編碼解碼:可以串行生成和讀寫,使數(shù)據(jù)能夠在串行通信上實時生成和顯示望忆。
  4. 漸進式圖像:在傳輸數(shù)據(jù)流時罩阵,可以開始呈現(xiàn)圖像的近似值,并隨著數(shù)據(jù)流的接收而逐漸清晰炭臭。
  5. 傳輸錯誤:可以可靠地檢測數(shù)據(jù)傳輸錯誤永脓。
  6. 無損耗:過濾和壓縮應該保存所有的信息。
  7. 性能:任何過濾鞋仍、壓縮和漸進式圖像呈現(xiàn)都應以高效解碼和呈現(xiàn)為目標。讀取圖像(解碼)比寫入圖像(編碼)更重要搅吁,必要時犧牲編碼速度威创。
  8. 壓縮:壓縮要有效率。
  9. 簡單性:標準要簡單谎懦,容易被開發(fā)者理解肚豺。
  10. 互換性:PNG 解碼器應能讀取所有符合 PNG 規(guī)范的數(shù)據(jù)/流。
  11. 靈活性:可以添加私人擴展界拦,但不能損害互換性吸申。
  12. 不觸犯法律:不使用侵權的算法。

目錄

[TOC]

1 范圍

便攜式網(wǎng)絡圖形(PNG 讀作 “ping”)是一種可擴展的文件格式,用于圖像的無損壓縮存儲截碴。它可以保存圖片的多項內容梳侨,例如顏色索引、灰度通道日丹、彩色通道走哺、透明通道、1至16位的深度采樣等哲虾。是 GIF 格式的無專利代替品丙躏,也可以在一定范圍內代替 TIFF 格式使用。

該格式具有流式讀取束凑,漸進式讀取晒旅,快速檢測文件完整性的特點,避免了文件傳輸中的由網(wǎng)絡波動導致的一些問題汪诉,在實際應用中表現(xiàn)出色敢朱。此外,格式中可以添加伽馬曲線信息以及色度信息摩瞎,使其在不同平臺拴签、不同的屏幕色差中表現(xiàn)一致。

本文檔是專門用于闡釋 PNG 網(wǎng)絡流媒體格式的標準規(guī)范旗们。

2 參考文獻

參考了下列規(guī)范性文件蚓哩。這些規(guī)范有可能失效,但這里沿用舊版上渴;對于沒有標明日期的岸梨,使用最新版本。

文檔編號 文檔名稱 備注
ISO 639:1988 語言名稱表示代碼
ISO/IEC 646:1991 國際標準化組織信息技術——信息交換用 ISO 7位編碼字符集
ISO/IEC 3309:1993 信息技術——電信和系統(tǒng)間的信息交換——高級數(shù)據(jù)鏈路控制(HDLC)程序稠氮,幀結構
ISO/IEC 8859-1:1998 信息技術——8位單字節(jié)編碼圖形字符集——第1部分:拉丁字母表第1號 1
ISO/IEC 9899:1990(R1997) C語言
ISO/IEC 10646-1:1993 信息技術——通用多八位字節(jié)編碼字符集(UCS)——第1部分:體系結構和基本多語言體系
IEC 61966-2-1 多媒體系統(tǒng)和設備——顏色測量和管理——第2-1部分:默認RGB顏色空間 2
CIE-15.2 比色法曹阔,第二版(CIE Publication15.2-1986 / ISBN 3-900-734-00-3)
ICC.1: 1998-09 文件顏色格式(by International Color Consortium) 3
ICC.1A: 1999-04 ICC.1: 1998-09的添加版 3
RFC-1123 互聯(lián)網(wǎng)主機的需求——應用和支持 4
RFC-1950 ZLIB壓縮數(shù)據(jù)格式規(guī)范,第3.3版 5
RFC-1951 DEFLATE壓縮數(shù)據(jù)格式規(guī)范隔披,第1.3版 6
RFC-2045 MIME(多用途互聯(lián)網(wǎng)郵件擴展)——第一部分:互聯(lián)網(wǎng)消息體的格式 7
RFC-2048 MIME——第四部分:注冊程序 8
RFC-3066 語言鑒別標簽 9

備注:

  1. 為了方便使用赃份,這里是簡化的規(guī)范,只需要使用部分編碼奢米,參考:https://www.w3.org/TR/PNG/iso_8859-1.txt抓韩,當然,你可以使用它的字符超集鬓长。
  2. 參考:http://www.iec.ch/
  3. 參考:http://www.color.org/
  4. 參考:http://www.ietf.org/rfc/rfc1123.txt
  5. 參考:http://www.ietf.org/rfc/rfc1950.txt
  6. 參考:http://www.ietf.org/rfc/rfc1951.txt
  7. 參考:http://www.ietf.org/rfc/rfc2045.txt
  8. 參考:http://www.ietf.org/rfc/rfc2048.txt
  9. 參考:http://www.ietf.org/rfc/rfc2066.txt

3 術語谒拴、定義和縮寫術語

本文檔適用以下定義

術語 定義
alpha 表示像素的透明度,0 表示像素完全透明英上,值越大越不透明炭序。
alpha 壓縮 不使用透明通道,通過標記一些顏色或者灰度苍日,使這些顏色或灰度的被當作透明惭聂。
alpha 分離 當所有顏色都是不透明的時候,就不需要 alpha 通道了易遣。
alpha 表 一個用于表示透明的索引表彼妻。
輔助塊 PNG 文件由許多塊組成,輔助塊是一些記載額外信息的塊豆茫,允許缺失/跳過侨歉。
位深度 [3.1](# 3.1 位深度)
字節(jié) 字節(jié)占用8位內存;也稱八位位組揩魂。一個字節(jié)可以表示 0~255幽邓。
字節(jié)順序 [3.2](#3.2 字節(jié)順序)
通道 一個像素用于表示紅、綠火脉、藍牵舵、灰度和透明度的部分。
色度 顏色是由亮度和色度共同表示的倦挂,可以通過色圖來查找顏色畸颅。是色圖啦!
一個標準的 PNG 文件由許多塊組成方援,分為關鍵塊和輔助塊没炒。
顏色類型 PNG 圖片有五種:灰度、真彩犯戏、索引送火、灰度帶透明、真彩帶透明先匪,記做顏色類型种吸。
復合 通過透明度合并前景圖像和背景圖像來形成圖像。
關鍵塊 必須讀取的 PNG 塊。
數(shù)據(jù)流 是文件的一部分或者全部字節(jié)順序。數(shù)據(jù)流是即時生成的內存數(shù)據(jù)。
deflate 一種 LZ77 解壓算法。PNG 使用了這個算法的 0 模式袒啼。參考 RFC-1951
交付圖像 通過數(shù)據(jù)流解碼得到的圖像,表示已經(jīng)可以交付用戶/圖像緩沖區(qū)使用了拓萌。
濾波器 為了提高圖片壓縮效率碍沐,用來轉換圖片掃描數(shù)據(jù)的過濾。
圖像緩沖區(qū) 內存中的一個區(qū)域激涤,其中的數(shù)據(jù)專門用于屏幕顯示拟糕。
伽馬曲線值 人眼對自然亮度感知是非線性的(韋伯-費希勒定理)判呕。文件中記錄的顏色 = 屏幕顏色^{gamma}
灰度 使用單一通道,來表示亮度送滞,也就是單純的黑白圖片侠草。
圖片數(shù)據(jù) 掃描圖片后得到的一維數(shù)組,這個數(shù)組可以映射在內存塊里供用戶使用犁嗅。
索引顏色 [3.3](#3.3 索引顏色)
構建索引 指索引顏色的排序边涕,通過序號可以從調色板中獲取顏色。此外還有 alpha 表索引褂微。
交錯模式 [3.4](#3.4 交錯模式)
無損壓縮 數(shù)據(jù)壓縮的方法功蜓,可以精確地重建原始數(shù)據(jù)。
有損壓縮 數(shù)據(jù)壓縮的方法宠蚂,近似地重建原始數(shù)據(jù)式撼。(我要的是全損壓縮的算法啦!)
亮度 人眼感知的亮度求厕。亮度和色度組合成為了人眼中的色彩著隆。它的能量級別是對數(shù)增長的。
LZ77 Ziv 和 Lempel 先生在1977年發(fā)表了一篇論文呀癣,論文描述了這種數(shù)據(jù)壓縮算法美浦。
網(wǎng)絡字節(jié)順序 與主機字節(jié)順序相反,網(wǎng)絡字節(jié)順序是指從低位往高位排序的字節(jié)项栏。
調色板 調色板是一個顏色數(shù)組浦辨,每個顏色占用3個8位字節(jié),最多儲存256種顏色忘嫉。
Pass 提取 一種提取圖片縮略圖的方法荤牍,用于交錯模式。
像素 在計算機屏幕上庆冕,每個顏色的點被稱為像素康吵,它是一種字節(jié)數(shù)據(jù)。
PNG 數(shù)據(jù)流 符合 PNG 規(guī)范的數(shù)據(jù)流访递。
PNG 解碼器 可以處理 PNG 數(shù)據(jù)流晦嵌,生成交付圖像的東西,一般是軟件解碼拷姿。
PNG 編輯器 可以修改惭载、生成符合規(guī)范的 PNG 數(shù)據(jù)流的軟件。
PNG 編碼器 將二維像素數(shù)組轉化為 PNG 數(shù)據(jù)流的東西响巢。
PNG 文件 硬盤中保存的 PNG 數(shù)據(jù)流描滔。
PNG 4字節(jié)帶符號整數(shù) 4個字節(jié)的帶符號整數(shù)可以表示 -2^{31}+12^{31}-1 之間的整數(shù)。之所以這么限制是因為有些編程語言不支持 -2^{31}踪古。
PNG 4字節(jié)無符號整數(shù) 4個字節(jié)的帶符號整數(shù)可以表示 0 到 2^{32}-1 之間的整數(shù)含长。之所以這么限制是因為有些編程語言不支持無符號整數(shù)(然而我沒見過)券腔。
PNG 圖像 PNG 編碼器所產(chǎn)出的圖像文件。
PNG 簽名 PNG 數(shù)據(jù)流的開頭8個字符拘泞,用以區(qū)別與其它數(shù)據(jù)流纷纫。
縮小圖 也就是交錯模式下,Pass 提取所解析出來的縮小圖陪腌,最多七張辱魁。下面也會稱為縮略圖。
標準圖像 BMP诗鸭、GIF染簇、JPG 等其他標準圖像文件。
RGB 合并 將不同的顏色通道混合在一起只泼。因為在 PNG 中剖笙,不同的通道是排在一起的。
樣本 集合中隨機抽取的元素请唱,用它來代表全部的元素特性弥咪。屬于泛指。
樣本深度 樣本的二進制長度十绑。
樣本深度放縮 標準圖像的樣本深度不是統(tǒng)一的聚至,但是 PNG 要求一致,所以需要放縮這些深度本橙。
掃描線 圖像一行一行的數(shù)據(jù)扳躬。因為以前的顯示器是一行一行顯示的。
原圖 給 PNG 編碼器處理前的圖像甚亭。
真彩 包含三個以上(紅贷币、綠、藍)通道的圖像模式亏狰。
白點 [3.5](#3.5 白點)
zlib 一個用于壓縮解壓縮的函數(shù)庫役纹。
CRC 循環(huán)冗余校驗碼,用于檢測傳輸誤差的校驗碼暇唾。通過加密算法計算得到促脉。
CRT 陰極射線管,曾經(jīng)的顯示器〔咧荩現(xiàn)在用 LCD 液晶顯示器瘸味。
LSB 在網(wǎng)絡字節(jié)順序中,最高位的改動對值的影響最小够挂,被稱為最低有效比特(LSB)旁仿。
LUT Look up table 的縮寫,是計算機上一種常用的數(shù)據(jù)轉跳方式孽糖。
MSB 在網(wǎng)絡字節(jié)順序中丁逝,最低位的改動對字節(jié)值的影響最大汁胆,被稱為最高有效比特(MSB)梭姓。

3.1 位深度

索引模式下霜幼,表示索引顏色的字節(jié)長;顏色模式下誉尖,表示顏色樣本的字節(jié)長罪既。

一般來說很少見到 1,2铡恕,4 的深度琢感,因為不好讀取,現(xiàn)代計算機采用內存對齊的讀取方式探熔,如果是長度小于8比特驹针,就需要額外使用位域(bit field)讀取了,效率就會降低诀艰。

3.2 字節(jié)順序

在計算機發(fā)明的時候柬甥,設計了緩存、內存和硬盤的訪問方式——地址查詢其垄。

地址從低往高排列苛蒲,依次讀取出來的數(shù)據(jù)被稱為大端模式(Big-endian),反過來绿满,從地址高位往低位讀就是小端模式(Little-endian)臂外。這兩種都是字節(jié)順序。

大端模式和人的閱讀方式相同喇颁,但是不知道為什么被淘汰了漏健,它現(xiàn)在主要在 IBM PowerPC 和網(wǎng)絡上運作,所以也被稱為網(wǎng)絡字節(jié)順序(network byte order)橘霎。

而現(xiàn)在的主機一般使用的 X86蔫浆、ARM 處理器都是小端模式,被稱為主機字節(jié)順序(host byte order)茎毁。

PNG 使用網(wǎng)絡字節(jié)順序克懊。

3.3 索引顏色

普通的真彩圖片,至少由紅七蜘、綠谭溉、藍三個通道組成,也就是每個像素占用了三個字節(jié)以上的空間橡卤。這樣圖片的壓縮效率就很低扮念。

因此,我們構建一個調色板碧库,在調色板里預設好我們需要使用到的顏色柜与,后續(xù)使用時巧勤,只需要提供調色板的索引位置就可以了。

而且為了防止調色板占用過大空間弄匕,我們把調色板的容量設定在256以內颅悉。索引位置也就不會超過256,只需要一個字節(jié)就可以表示迁匠,一個字節(jié)只占用一個通道剩瓶,因此索引模式下,只有一個通道城丧。

3.4 交錯模式

一個圖片是由許多像素點組成的延曙,可以視作一個二維數(shù)組。我們可以提取其中幾個特殊的位置亡哄,比如橫行每隔一個提取一個像素枝缔,豎列每隔一個提取一個像素,這樣組成一個新的二維數(shù)組蚊惯,可以簡略失真的表示原圖愿卸。這種提取方法叫做 Pass 提取(pass extraction)拣挪,提取之后形成的數(shù)據(jù)是交錯的擦酌,每段都可以包含整個圖片的縮略部分。只需要重新組合就能產(chǎn)生完整的圖像菠劝,即使沒有完整的數(shù)據(jù)赊舶,只要其中一部分就可以得到縮略圖。是網(wǎng)絡傳輸中加快圖像傳輸?shù)姆绞礁险铮贿^現(xiàn)在網(wǎng)速很快笼平,幾乎用不到了。

3.5 白點

色圖的中間區(qū)域是白色的舔痪,被我們稱作白點寓调,是色圖的一個參數(shù)。另一個參數(shù)是基準锄码,用作色圖的平移夺英。

我們可以設置白點的坐標,來改變中間白色區(qū)域滋捶,使其向紅色痛悯、綠色、藍色偏移重窟,用以調整色圖的變化程度载萌。

4 概念

4.1 圖像

PNG 規(guī)范不指定應用程序接口,但是涉及四種圖像:原圖、標準圖像扭仁、PNG 圖像垮衷、交付圖像。關系如下:

graph LR;
    subgraph ;
        id0[原圖]--理論上-->id1[標準圖像];
        id0-.實際上.->id2[PNG 圖像];
    end;
          id2--編碼-->id3[PNG 數(shù)據(jù)流];
          id3---id5[PNG 文件];
          id3--解碼-->id4[PNG 圖像];
    subgraph ;
          id6[標準圖像]--理論上-->id7;
          id4-.實際上.->id7[交付圖像];
    end;
像素通道 紅色通道 綠色通道 藍色通道 透明通道
二進制編碼舉例 1000 11011 0010 1000

二進制編碼占用的比特數(shù)乖坠,就是樣本深度搀突。

4.2 顏色空間

PNG 有三種管理色彩空間的方法:使用 ICC 配置、使用 sRGB 配置瓤帚、使用色度基準和白點位置配置描姚。

ICC 配置比較靈活,易于適配戈次;sRGB 配置需要設置一個特定的顏色空間,可能會占用較多的容量筒扒;最后一種比較精確怯邪。前兩種也推薦使用伽馬值。

4.3 標準圖像與 PNG 圖像的變換

4.3.1介紹

我們需要通過一些手段花墩,將標準圖像轉換為 PNG 圖像悬秉。流程如下:

graph LR;
    id[標準圖像]-->id0[透明通道分離];
    id0-->id4[RGB 合并];
    id4-->id5[alpha 壓縮];
    id5-->id7[樣本深度放縮];
    id0-->id6[構建索引];
    id6-->id7;
    id7-->id8[PNG 圖片];

4.3.2 透明通道分離

分離透明通道,實際上冰蘑,很多標準圖像沒有透明通道和泌,這樣可以默認為無透明度,節(jié)省一個通道祠肥。

4.3.3 構建索引

如果不同像素值的個數(shù)少于 256武氓,樣本深度小于等于8,可以開始構建索引仇箱。

4.3.4 RGB 合并

如果县恕,顏色樣本深度一致,而且每個通道都一樣的值剂桥,可以用一個通道來表示所有忠烛,即灰度圖。

4.3.5 alpha 壓縮

不用 alpha 通道表示透明度的一種方法权逗,需要設置背景色美尸。

4.3.6 樣本深度放縮

不是所有的深度都被 PNG 支持,只有 1斟薇、2师坎、4、8奔垦、16屹耐,如果不是這些數(shù)的話,深度就要通過軟件調節(jié)。

比如原始深度是 5惶岭,現(xiàn)在要把它變成 8寿弱,也就是擴大了。

如果不同通道有不同的深度按灶,我們就會選取最大深度來調整症革。

這種深度變換是可逆的。

4.4 PNG 圖像

一共有五種:

  1. 真彩透明(RGBA):每個像素由四個樣本組成:紅色鸯旁、綠色噪矛、藍色和透明。
  2. 灰度透明(LA):每個像素由兩個樣本組成:灰色和透明铺罢。
  3. 真彩(RGB):每個像素由三個樣本組成:紅色艇挨、綠色、藍色和藍色韭赘∷醣酰可以設置透明色來表示透明。
  4. 灰度(L):每個像素由單個樣本組成:灰色泉瞻÷雎可以設置透明色來表示透明。
  5. 索引:每個像素由調色板中的索引組成(可能還有 alpha 表)袖牙。

4.5 PNG 的編碼

4.5.1 介紹

  1. Pass 提炔嗑蕖:交錯模式下,PNG 圖像的像素可以被重新排列鞭达,以形成多個較小的圖像司忱,縮小圖,或者直接稱呼為 Pass碉怔。
  2. 掃描線序列化:將圖片(原圖或縮小的圖片)的數(shù)據(jù)從左到右烘贴、一行一行的讀取,形成一個個數(shù)組撮胧。
  3. 過濾:使用濾波器將每個掃描線數(shù)組轉換桨踪。
  4. 壓縮:將過濾后的數(shù)據(jù)壓縮。
  5. 分塊:將壓縮過的數(shù)據(jù)分為多個塊芹啥,并添加校驗碼锻离。
  6. 數(shù)據(jù)流構建:塊被插入到數(shù)據(jù)流中。
graph LR;
    id[PNG 圖片]-->id0[Pass 提取];
    id0-->id1[掃描線序列化];
    id1-->id2[過濾];
    id2-->id3[壓縮];
    id3-->id4[分塊];
    id4-->id5[數(shù)據(jù)流構建];

4.5.2 Pass 提取

這里采用兩種方法進行 Pass 提取墓怀。

第一種是空方法汽纠,也就是什么都不做。(所以為什么要這么死板的把這個也計作一種方法)

第二種通過多次掃描得到七個縮小圖傀履。也就是 Adam7 算法(不是深度學習的那個 Adam 算法)虱朵。

不過這個算法在國內網(wǎng)站這個幾乎找不到,維基百科https://en.wikipedia.org/wiki/Adam7_algorithm)介紹的也不是很清楚。所以我這里就簡單說一下:

  1. 這個算法的目標是把一張圖片分成很多縮略圖碴犬,這個變換必須是可逆的絮宁,多個縮略圖也能轉換成一張完整的圖片。這樣就能防止文件損壞后服协,無法再讀取圖片了绍昂,因為縮略圖還在,通過一定的嘗試偿荷,還是可以恢復原始的圖片窘游。
  2. 最簡單的分法,就是隔列/行提取跳纳,比如:每隔一個像素提取一個(或者每隔一行提取一行)忍饰,就可以得到兩個數(shù)組,每個數(shù)組都是原始圖像的縮略數(shù)據(jù)棒旗。Adam 算法也是在隔列/行提取的基礎上展開的喘批。
  3. 把原始的二維像素數(shù)組視作一個棋盤,每格代表一個像素铣揉。原始的數(shù)組就是從左到右烙常、從上到下一格一格的提取『撸現(xiàn)在我們把這個讀取面擴大,像”田“字一樣滴须,一次性讀取四個像素台猴,按照下表中 Adam3 的 2x2 矩陣來分(分成四個縮略圖也是可以的朽合,但是這里分成三個),第一個像素放在第一張縮略圖上饱狂,第二個像素放在第二張縮略圖上曹步,第三和第四個像素放在第三張縮略圖上。如果讀取到邊緣休讳,無法讀出“田”字數(shù)據(jù)讲婚,就假裝讀取,把該有的像素放到縮略圖上俊柔,不存在的就跳過筹麸。這就有 3 個縮略圖。
  4. 再次擴展雏婶,按照 Adam5 的 4x4 矩陣讀取物赶,就可以讀取 5 個縮略圖。再擴展一次就是 8x8 矩陣了留晚,可以一次性提取 7 個縮略圖酵紫。PNG 規(guī)范采用的時候,普遍圖片不超過三四百的尺寸,用 8x8 矩陣足夠了奖地。所以選定 Adam7 作為交錯模式的算法橄唬。
Adam1 矩陣:  // 也就是普通的掃描方法
1

Adam3 矩陣:  // 它是由 Adam1 插值得到的,注意它的奇數(shù)行和奇數(shù)項與 Adam1 相同
1, 2  // 偶數(shù)列添加 2
3, 3  // 偶數(shù)行添加 3

Adam5 矩陣:  // 它是由 Adam3 插值得到的鹉动,注意它的奇數(shù)行和奇數(shù)項與 Adam3 相同
1, 4, 2, 4  // 偶數(shù)列添加 4
5, 5, 5, 5  // 偶數(shù)行添加 5
3, 4, 3, 4
5, 5, 5, 5

Adam7 矩陣:  // 它是由 Adam5 插值得到的轧坎,注意它的奇數(shù)行和奇數(shù)項與 Adam5 相同
1, 6, 4, 6, 2, 6, 4, 6  // 偶數(shù)列添加 6
7, 7, 7, 7, 7, 7, 7, 7  // 偶數(shù)行添加 7
5, 6, 5, 6, 5, 6, 5, 6
7, 7, 7, 7, 7, 7, 7, 7
3, 6, 4, 6, 3, 6, 4, 6
7, 7, 7, 7, 7, 7, 7, 7
5, 6, 5, 6, 5, 6, 5, 6
7, 7, 7, 7, 7, 7, 7, 7

4.5.3 掃描線序列化

把上面的到的縮小圖(當然空方法讀出來的是原圖),逐行再讀取一遍泽示。(這里的操作就可以有很多了缸血,比如把上面的提取變成一個 yield)

4.5.4 過濾

有幾種過濾類型,會把過濾類型寫到過濾數(shù)組之前械筛。

4.5.5 壓縮

就是編碼加密捎泻。

4.5.6 分塊

把編碼后的數(shù)據(jù)分成一塊或者多塊。

4.6 附加信息

信息類型 描述
背景色 顯示圖像時要使用固體背景顏色埋哟。
伽馬和色度 圖像輸出強度的伽馬值笆豁,以及圖像中使用的色度范圍。
國際刑事法院簡介 顏色空間的描述赤赊。
圖像直方圖 預計圖像使用調色板頻率闯狱。
物理像素尺寸 用于顯示 PNG 圖像的預期像素大小和寬高比,你要知道像素和真實長度是有區(qū)別的抛计。
有效位 在樣本中有效的位數(shù)/深度哄孤。
sRGB色彩空間 渲染意向(由 ICC 定義)和圖像采樣符合的顏色空間。
建議調色板 簡化的調色板吹截,當計算機不能顯示圖像中的全部顏色時瘦陈,可以使用該調色板。
文本數(shù)據(jù) 與圖像相關的文本信息波俄。
時間 文件修改時間晨逝。
透明信息(色) 沒有透明通道時,用這個代替透明懦铺。

4.7 PNG 數(shù)據(jù)流

4.7.1 塊

一個標準的 PNG 文件由許多塊組成捉貌,每個塊有四個部分:長度、名稱阀趴、數(shù)據(jù)主體昏翰、校驗碼。

4.7.2 塊類型

標準的 PNG 定義有 18 種塊類型刘急,此外你可以添加自定義的各種塊棚菊。

這 18 種塊類型有:

關鍵塊:

IHDR(image header 文件頭)、PLTE(palette 調色板)叔汁、IDAT(image data 圖片內容)统求、IEND(image end 文件結尾)

輔助塊:

透明相關:tRNS(transparency information 透明信息)

顏色相關:cHRM(chromaticities and white point 色度和白點)检碗、gAMA(gamma 伽馬值)、iCCP(embedded ICC profile 嵌入式 ICC 概述)码邻、sBIT(significant bits 有效位)折剃、sRGB(standard RGB colour space 標準RGB顏色空間)

文本相關:iTXt(international textual data 國際化文本)、tEXt(textual data 文本)像屋、zTXt(zip textual data 壓縮的文本)

時間相關:tIME(last-modification time 最新修改時間)

其他:bKGD(background colour 背景色)怕犁、hIST(histogram 直方圖)、pHYs(physical pixel dimensions 物理像素尺寸)己莺、sPLT(Suggested palette 建議調色板)奏甫、

4.8 錯誤處理

傳輸錯誤或文件損壞,這會破壞數(shù)據(jù)流的大部分或全部凌受;語法錯誤阵子,出現(xiàn)無效塊或者丟失塊。

兩種錯誤處理方式要區(qū)別胜蛉。

4.9 拓展和注冊

你可以向 ISO/IEC 或者 PNG Group 提交相關擴展挠进,注冊新的塊類型和文本關鍵字,拓展新的過濾算法誊册、交錯模式的算法领突、壓縮算法。

5 數(shù)據(jù)流結構

5.1 介紹

就是數(shù)據(jù)流的二進制的結構啦案怯。

5.2 PNG 簽名

所有 PNG 數(shù)據(jù)流的前八個字符攘须,都是 137 80 78 78 71 13 10 26 10

用 bytes 表示就是 b'\x89PNG\r\n\x1a\n'

這個簽名表示接下來的數(shù)據(jù)都是 PNG 數(shù)據(jù)流,如果遇到空字符不要打斷殴泰,需要出現(xiàn) IEND 才算結束。

5.3 塊布局

每個塊由這四個部分組成:

  1. 長度浮驳,占有4個字節(jié)悍汛,表示數(shù)據(jù)主體的長度。
  2. 塊名稱至会,占有4個字節(jié)离咐,不同的塊有不同的類型標記。
  3. 數(shù)據(jù)主體奉件。(當長度為 0 時宵蛀,數(shù)據(jù)主體可以為空)
  4. CRC 校驗碼,占有4個字節(jié)县貌,是類型標記數(shù)據(jù)主體的 crc32 加密碼术陶。

5.4 塊名稱約定

通過名稱約定,使得 PNG 解碼器在不能識別當前塊的用途時煤痕,也能通過名稱來獲取相關信息梧宫。

塊的名稱有四位:

第一位表示輔助接谨,小寫表示這個塊是輔助塊,大寫表示這個塊是關鍵塊塘匣。

第二位表示私有脓豪,小寫表示這個塊是私有的,而非國際標準定義的忌卤,大小表示前述 18 種塊類型扫夜。

第三位是保留位,小寫表示這個塊是被拋棄的驰徊,大寫表示可以使用笤闯。(用于約定將來的擴展)

第四位表示復制安全性,也就是 PNG 編輯器在編輯圖片的時候辣垒,如果遇到不安全的數(shù)據(jù)塊望侈,就不會完全的復制,而是有選擇的勋桶,大寫表示 PNG 編輯器可以完全的復制脱衙,而不需要擔心任何問題。

5.5 循環(huán)冗余碼算法

x^{32} + x^{26} + x^{23} + x^{22} + x^{16} + x^{12} + x^{11} + x^{10} + x^8 + x^7 + x^5 + x^4 + x^2 + x + 1

具體參考 crc32 算法

5.6 塊排序

因為 PNG 圖像是可以流式讀取的例驹,也就是說不需要讀到文件尾捐韩,就可以在 PNG 瀏覽器里預覽了。

所以有些東西需要在讀取圖像內容之前準備好鹃锈,比如索引調色板荤胁。

塊名稱 允許多個 順序限制
IHDR 必須第一位
PLTE Shall be first
IDAT 如果有多個,必須連續(xù)出現(xiàn)
IEND 必須最后
cHRM PLTE 之前
gAMA PLTE 之前
iCCP PLTE 之前屎债,與 sRGB 互斥
sBIT PLTE 之前
sRGB PLTE 之前仅政,與 iCCP 互斥
bKGD PLTE 和 IDAT 之間
hIST PLTE 和 IDAT 之間
tRNS PLTE 和 IDAT 之間
pHYs IDAT 之前
sPLT IDAT 之前
tIME
iTXt
tEXt
zTXt

6 標準圖像與 PNG 圖像的變換

好像在 4.3 章節(jié)寫過了?

6.1 顏色類型和值

我們在 4.4 章寫過盆驹,有五種顏色類型:

顏色類型 通道數(shù)
灰度 0 1
真彩 2 3
索引 3 1
灰度帶透明 4 2
真彩帶透明 6 4

顏色類型記錄在 IHDR 里圆丹。

灰度模式下,亮度取決于 gAMA躯喇、sRGB辫封、iCCP,如果沒有這些廉丽,則取決于機器倦微。

顏色樣本不一定和光強成正比,可以通過設置 gAMA 來調節(jié)正压。

值的計算方法如下:初始為 0欣福,使用了調色板加上1,使用了真彩加上2蔑匣,使用了透明通道加上4劣欢∽厮校灰度下是無法使用索引。

6.2 透明的表示方法

有四種方式表示透明:使用透明通道凿将、使用 tRNS 塊設置透明顏色信息校套、索引中在 tRNS 設置 alpha 表、不使用透明通道也不使用 tRNS 表示完全不透明牧抵。

透明通道的樣本深度是 8 和 16笛匙,透明通道保存在像素之中,alpha=0 表示完全透明犀变,alpha=2^{樣本深度}-1表示完全不透明妹孙。透明度用于圖像前景色和背景色的復合。

一些普通的圖片不包含透明度获枝,甚至已經(jīng)把像素值乘以透明度蠢正,提前做好了以黑色為背景的復合步驟;但是 PNG 不這么做省店。

7 PNG 圖像編碼

7.1 整形和字節(jié)順序

整形(int)是多位字節(jié)嚣崭,short 是兩個,int 是四個懦傍,long 是八個雹舀。

PNG 使用的是網(wǎng)絡字節(jié)順序,MSB 在高位粗俱,LSB 在低位说榆。

MSB B2 B1 LSB
short 15 14 13 12 11 10 9 - - 8 7 6 5 4 3 2 1 0
int 31 30 29 28 27 26 25 24 23 22 21 20 19 18 17 16 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0

7.2 掃描線

也就是每個 PNG 圖像的行,緊湊排列各個像素寸认。

深度少于 8 時签财,掃描線結尾可能是湊不齊字節(jié),這些不使用的字節(jié)不進行處理偏塞。

7.3 濾波器

濾波器可以提高壓縮數(shù)據(jù)的壓縮性荠卷,而且是可逆的。PNG 允許過濾掃描線數(shù)據(jù)烛愧,換句話說就是可以不進行濾波。

過濾后的字節(jié)序列與過濾前的相同掂碱,但是根據(jù)不同的濾波類型怜姿,會在最前面添加一個字節(jié)的標記。如果沒有增加長度就表明沒有過濾過疼燥。具體過濾方法沧卢,在后面解釋。

8 交錯模式和 Pass 提取

交錯模式可以提高 CRT 顯示器上網(wǎng)絡圖片的加載速度(換句話醉者,沒有網(wǎng)絡但狭,沒有 CRT 顯示器披诗,交錯模式就沒有用了)。

參考[4.5.2 Pass 提取](#4.5.2 Pass 提取)

介于 Adam7 的特點立磁,寬或者高小于5的圖像會缺少縮略圖(2在第五列呈队,3在第五行)。

9 過濾

9.1 過濾方法和濾波器類型

過濾的目的在于提高壓縮率唱歧。過濾的方法不是唯一的宪摧,交錯模式下,所有縮小圖都應該使用同一種方法的濾波器颅崩;非交錯模式下几于,只有一張圖,當然也是只有一種方法沿后。

這個標準里定義了一種 0 號方法沿彭,其他的編號為將來保留。0 號方法包含五種類型的濾波器尖滚,每個掃描線可以使用不同的濾波器類型喉刘。

PNG 規(guī)范不強制濾波器的類型,具體的選擇方法后面在說熔掺。

9.2 0 號方法

濾波器是針對字節(jié)的饱搏,和像素、通道置逻、深度無關推沸。只要給字節(jié)就能過濾。

這里是幾個參數(shù)的定義:

  1. x:將要被過濾的字節(jié)券坞;
  2. a:前一個像素中對應 x 的字節(jié)鬓催;比如 x 是一個像素的第 2 個字節(jié),a 就是前一個像素中第 2 個字節(jié)恨锚;如果一個像素只有一個字節(jié)宇驾,x 就是這個像素的唯一字節(jié),a 就是前一個像素的唯一字節(jié)猴伶,兩者緊挨著课舍;
  3. b:前一行像素中對應 x 的字節(jié);
  4. c:b 所在像素的前一個像素對應 x 的字節(jié)(也是對應 b 的字節(jié))他挎;
... c b ...
... a x ...

Org() 表示字節(jié)原始值筝尾;

Flt() 表示過濾后的值;

Rc() 表示重構的值办桨;

Paeth() 參見[9.4 濾波器類型4](#9.4 濾波器類型4)筹淫。

濾波器類型 名稱 過濾函數(shù) 重建函數(shù)
0 None Flt(x) = Org(x) Rc(x) = Flt(x)
1 Sub Flt(x) = Org(x) - Org(a) Rc(x) = Flt(x) + Rc(a)
2 Up Flt(x) = Org(x) - Org(b) Rc(x) = Flt(x) + Rc(b)
3 Average Flt(x) = Org(x) - floor((Org(a) + Org(b)) / 2) Rc(x) = Flt(x) + floor((Rc(a) + Rc(b)) / 2)
4 Paeth Flt(x) = Org(x) - Paeth(Org(a), Org(b), Org(c)) Rc(x) = Flt(x) + Paeth(Rc(a), Rc(b), Rc(c))

如果沒有前一個像素,就用 0 代替呢撞。每個縮小圖的第一行沒有前一行损姜,也用 0 代替饰剥。

因為使用了過濾,重建時也要按照這個順序進行計算摧阅。

濾波器的輸入輸出值都是無符號字節(jié)汰蓉。

9.3 濾波器類型3

0、1逸尖、2 三種類型的濾波器都很簡單古沥,隔列/隔行相減就是。

但是第三種類型娇跟,Flt(x) = Org(x) - floor((Org(a) + Org(b)) / 2) 中岩齿,Org(a) + Org(b) 存在溢出的情況,不能使用 byte 運算苞俘,應該是 short 或者更多位盹沈,當然也有右移的算法。

9.4 濾波器類型4

Paeth 算法先計算三個相鄰像素(左吃谣、上乞封、左上)的線性值,選擇與計算值最接近的相鄰像素再次計算岗憋。注意緩存不要溢出肃晚。它的函數(shù)如下:

def Paeth(a, b, c):
    p = a + b - c
    pa = abs(p - a)
    pb = abs(p - b)
    pc = abs(p - c)
    if pa <= pb and pa <= pc:
        Pr = a
    elif pb <= pc:
        Pr = b
    else:
        Pr = c
    return Pr
def Paeth(a, b, c):
    p = a + b - c
    p = {abs(p - a): a, abs(p - b): b, abs(p - c): c}
    return p[sorted(p.keys())[0]]

10 壓縮

與上面的過濾一樣,默認的就是 0 號方法仔戈。這兩個在 IHDR 里面有標記关串。

當然,這里用的是 zlib 的壓縮监徘,使用默認的等級為 8晋修,壓縮字節(jié)不超過 32768。

import zlib

zlib.compress(data=..., level=8)
壓縮塊的內容 字節(jié)數(shù)
zlib 壓縮標志 1
附加校驗標志 1
壓縮數(shù)據(jù)塊 n
校驗值 4

這個校驗值和 PNG 塊的校驗值不一樣凰盔,兩者不能混同墓卦。

多個過濾后的行,會被打包壓縮成一個 zlib 數(shù)據(jù)流户敬,并放到多個 PNG 塊里落剪,多個 PNG 塊解開得到的是一個 zlib 數(shù)據(jù)流。

當然尿庐,這個還涉及到異步讀取著榴。zlib 數(shù)據(jù)流本身就是可以中斷的,即使中斷屁倔,排列較前的數(shù)據(jù)還是可以讀取出來的,這樣才有交錯模式的解讀暮胧,所以針對上面的 python 方法锐借,有如下改進:

import zlib

decompress = zlib.decompressobj()

for chunk_type, chunk_data, chunk_crc in IDATs:
    unzip_data = decompress.decompress(chunk_data)
    handle(unzip_data)

unzip_data = decompress.flush()
handle(unzip_data)

構建一個持續(xù)的讀取问麸,邊讀取邊解析。

11 塊的介紹

下面是 18 個 PNG 規(guī)范塊的介紹:

11.1 b'IHDR'

IHDR 是 PNG 數(shù)據(jù)流中的第一個塊钞翔。組成如下:

用途 字節(jié)長度 類型
表示寬度 4 unsigned int
表示高度 4 unsigned int
表示深度 1 1严卖,2,4布轿,8哮笆,16
表示顏色類型 1 0,2汰扭,3稠肘,4,6
壓縮方法萝毛,這里只有 0 號方法 1 0
濾波方法项阴,這里只有 0 號方法 1 0
是否交錯 1 bool

所以 IHDR 塊長度是 13 不會改變。

11.2 b'PLTE'

用途 字節(jié)長度 類型
紅色通道 1 unsigned byte
綠色通道 1 unsigned byte
藍色通道 1 unsigned byte
... ... ...

調色板是一個二維數(shù)組笆包,可以看作 Array[n][3]环揽,用索引 n 來表示顏色。

因此庵佣,n 不會超過 256歉胶,PLTE 塊的長度也是 3 的倍數(shù)。

調色板無論如何都是 8 位深度巴粪,即使圖像是 1耙箍、2、4 的深度蒸健,調色板也還是 8勘高。

11.3 b'IDAT'

所有的 IDAT 塊合起來是一個 zlib 數(shù)據(jù)流,參考[10 壓縮](#10 壓縮)晶通。

11.4 b'IEND'

這段數(shù)據(jù)是空的璃氢,表示 PNG 數(shù)據(jù)流結束了。當然狮辽,這個塊損壞也不會有事一也。

11.5 b'tRNS'

這是表示透明度信息的塊,有三種構成:

灰度模式(凡是等于這個灰度的顏色被認做透明)

用途 字節(jié)長度 類型
灰度樣本值 2 unsigned short

真彩模式(由這三個值表示一個透明色)

用途 字節(jié)長度 類型
紅色樣本值 2 unsigned short
綠色樣本值 2 unsigned short
藍色樣本值 2 unsigned short

索引模式(索引模式下喉脖,tRNS 相當于一個 alpha 表椰苟,這個表和索引一樣大,對應表示索引的透明度)

用途 字節(jié)長度 類型
表示調色板[0]的透明度 1 unsigned short
表示調色板[1]的透明度 1 unsigned short
... ... ...

為什么灰度模式和真彩模式用 2 字節(jié)表示呢树叽,因為需要適配 16 位深度舆蝴,而索引模式深度永遠小于等于 8。

11.6 b'cHRM'

這個塊用于設置一個 CIE 色度空間。組成如下:

用途 字節(jié)長度 類型
白點橫坐標 4 unsigned int
白點縱坐標 4 unsigned int
紅點橫坐標 4 unsigned int
紅點縱坐標 4 unsigned int
綠點橫坐標 4 unsigned int
綠點縱坐標 4 unsigned int
藍點橫坐標 4 unsigned int
藍點縱坐標 4 unsigned int

儲存值是實際值的 100000 倍洁仗。

CIE 色度空間是一個二維圖像层皱,它由紅綠藍白四個點構建一個近視三角形的包圍圈,來設置顏色的偏移程度赠潦。

11.7 b'gAMA'

這個塊只儲存了一個 unsigned int叫胖,同樣的它的值需要除以 100000,得到實際的 gamma 值她奥。

11.8 b'iCCP'

這個塊用于設置一個 ICC 描述瓮增。

用途 字節(jié)長度 類型
ICC 描述的名稱 1-79 char[]
空字符 1 0
壓縮方法 1 unsigned byte
壓縮的描述信息 n compressed bytes

11.9 b'sBIT'

PNG 只支持固定的深度,如果原圖深度不匹配哩俭,就會強制的放縮绷跑,但是原始信息會保留在這里,用于恢復原始圖像携茂。(所以一般不會用到你踩,為什么要把標準圖像轉化成非標準圖像呢)

針對不同通道數(shù),有不同的 sBIT 長度讳苦。

顏色類型 長度(字節(jié))
灰度(0) 1(灰度)
真彩(2)索引(3) 3(紅+綠+藍)
灰度帶透明(4) 2(灰度+透明)
真彩帶透明(6) 4(紅+綠+藍+透明)

11.10 b'sRGB'

使用 sRGB 色彩空間带膜,這時候不能用 ICC 描述了。sRGB 只包含有一個 unsigned byte鸳谜,表示渲染意圖膝藕。

值的意義如下:

渲染意圖的值 含義 說明
0 感知 犧牲色度準確度,對輸出設備的色域進行適配咐扭,用于照片芭挽。
1 相對色度 顏色匹配外觀的圖像,如標識蝗肪。
2 飽和 保留飽和度袜爪,適用于圖表和圖形。
3 絕對色度 保證圖像的絕對色度薛闪,用于不同設備的圖像的輸出辛馆。

使用 sRGB 時,推薦使用 gAMA豁延、cHRM昙篙,因為有些設備不支持 sRGB,這樣就可以兼容性使用诱咏。

11.11 b'tExt'

關鍵字 后續(xù)文字的含義
Title 標題
Author 作者
Description 對于圖片的描述
Copyright 圖像版權
Creation Time 文件創(chuàng)建時間
Software 文件創(chuàng)建使用的軟件
Disclaimer 法律免責申明
Warning 關于內容的警告
Source 拍攝圖像的設備
Comment 其他建議

上面是一個文本信息會使用到的關鍵字苔可,關鍵字其實不是很關鍵,就是一個定義而已袋狞,可以自己改動焚辅。但是符合上述的是可以被圖像軟件標準讀取映屋。

tEXt 含有如下成分:

用途 字節(jié)長度 類型
關鍵字 1-79 char[]
空字符 1 0
文本串 n char[]

11.12 b'zTXt'

用途 字節(jié)長度 類型
關鍵字 1-79 char[]
空字符 1 0
壓縮方法 1 unsigned byte
壓縮文本 n compressed data

當然,這里的壓縮方法也是 0同蜻,用 zlib 解壓后面的數(shù)據(jù)

11.13 b'iTXt'

國際文本數(shù)據(jù)秧荆,有點高大上的感覺。

用途 字節(jié)長度 類型
關鍵詞 1-79 char[]
空字符 1 0
壓縮標志 1 bool
壓縮方法(當然這里也是 0) 1 0
用于標記語言種類 0字節(jié)或更多字節(jié)(字符串) char[]
空字符 1 0
翻譯的關鍵詞 n char[]
空字符 1 0
文本 n char[]

語言種類參見 RCF-3066埃仪、ISO 646、ISO 639陕赃。

11.14 b'bKGD'

背景色哦卵蛉。

顏色類型 長度(字節(jié))
灰度(0)灰度帶透明(4) 2
真彩(2)真彩帶透明(6) 6(紅+綠+藍,每個通道么库。2 字節(jié))
索引(3) 1(指向調色板順序)

11.15 b'hIST'

直方圖給出了調色板中每種顏色的近似使用頻率傻丝。

如果 PNG 瀏覽器無法提供調色板里所有的顏色,那么直方圖可以輔助創(chuàng)建相似的調色板供使用诉儒。

當然葡缰,現(xiàn)在不存在不能提供完整的調色板的軟件了。

用途 字節(jié)長度 類型
頻率 2 unsigned short
... ... ...

11.16 b'pHYs'

這個塊用于表示像素在屏幕上的實際尺寸忱反。結構如下:

用途 字節(jié)長度 類型
橫向每個單位的像素 4 unsigned int
縱向每個單位的像素 4 unsigned int
單位說明 1 unsigned byte

單位說明有兩個泛释,F(xiàn)alse 的時候表示這個塊只表示長寬的比,而不是真實值温算;True 的時候以米為單位怜校,即一個單位為一米,一米包含多少個像素注竿。

11.17 b'sPLT'

用途 字符長度 類型
調色板名稱 1-79 char[]
空字符 1 0
樣本深度 1 1茄茁、2、4巩割、8裙顽、16
紅色通道 1-2 unsigned byte/short
植物通道 1-2 unsigned byte/short
藍色通道 1-2 unsigned byte/short
alpha 1-2 unsigned byte/short
頻率 2 unsigned short
... ... ...

具體的通道長度,由樣本深度決定宣谈,深度為 16 就是兩個愈犹,小于等于 8 就是一個。

調色板名稱是區(qū)分大小寫的蒲祈,并受到與關鍵字參數(shù)相同的限制甘萧。

在灰度PNG圖像中,每個目通常相等的紅色梆掸、綠色扬卷、藍色和藍色值,但這不是必需的酸钦。

每一個頻率值與圖像的像素的比例成正比怪得,不是實際頻率。

11.18 b'tIME'

用途 字節(jié)長度 類型
表示年的數(shù)字 (用完整數(shù)字;如:1995 而不是 95) 2 unsigned short
表示月的數(shù)字 (1-12) 1 unsigned byte
表示日的數(shù)字 (1-31) 1 unsigned byte
表示時的數(shù)字(0-23) 1字節(jié) unsigned byte
表示分的數(shù)字(0-59) 1字節(jié) unsigned byte
表示秒的數(shù)字(允許跳躍秒徒恋,0-60) 1字節(jié) unsigned byte

這里用宇宙時間(Universal Time蚕断,UTC)

12 PNG 編碼和解碼

后面的公式太復雜了,算了入挣,直接上簡單的代碼吧

import struct
import zlib
import io
import itertools

HEADER = b'\x49\x48\x44\x52'
PALETTE = b'\x50\x4c\x54\x45'
DATA = b'\x49\x44\x41\x54'
END = b'\x49\x45\x4e\x44'


COLOR_TYPE_GRAY = 0
COLOR_TYPE_GRAY_ALPHA = 4
COLOR_TYPE_PALETTE = 3
COLOR_TYPE_RGB = 2
COLOR_TYPE_RGB_ALPHA = 6


def read_iterator(iter_data, size):
    for _ in range(size):
        try:
            yield next(iter_data)
        except StopIteration:
            yield 0


# 這兩個函數(shù)是通過輸入長和寬亿乳,來分割當前圖像像素的
def adam7iter(width, height):
    reduced_order = 0
    reduced_seven = (
        ((height + 7) // 8, (width + 7) // 8),
        ((height + 7) // 8, (width + 3) // 8),
        ((height + 3) // 8, (width + 3) // 4),
        ((height + 3) // 4, (width + 1) // 4),
        ((height + 1) // 4, (width + 1) // 2),
        ((height + 1) // 2, (width + 0) // 2),
        ((height + 0) // 2, (width + 0) // 1)
    )

    for reduced_height, reduced_width in reduced_seven:
        if reduced_height and reduced_width:
            yield reduced_order

            for _ in range(reduced_height):
                yield reduced_width
        reduced_order -= 1


def adam1iter(width, height):
    yield 0

    for _ in range(height):
        yield width


adam7sub1 = (
    0, 5, 3, 5, 1, 5, 3, 5,
    6, 6, 6, 6, 6, 6, 6, 6,
    4, 5, 4, 5, 4, 5, 4, 5,
    6, 6, 6, 6, 6, 6, 6, 6,
    2, 5, 3, 5, 2, 5, 3, 5,
    6, 6, 6, 6, 6, 6, 6, 6,
    4, 5, 4, 5, 4, 5, 4, 5,
    6, 6, 6, 6, 6, 6, 6, 6,
)


def do_nothing(*args):
    pass


def sub_reconstructor(previous_line, current_line, start_bytes):
    for i, j in enumerate(range(start_bytes, len(current_line))):
        a = current_line[i]
        x = current_line[j]
        current_line[j] = (x + a) & 0xff


def up_reconstructor(previous_line, current_line, start_bytes):
    for i in range(len(current_line)):
        x = current_line[i]
        b = previous_line[i]
        current_line[i] = (x + b) & 0xff


def average_reconstructor(previous_line, current_line, start_bytes):
    for i, j in enumerate(range(len(current_line)), start=-start_bytes):
        x = current_line[j]
        if i < 0:
            a = 0
        else:
            a = current_line[i]
        b = previous_line[j]
        current_line[j] = (x + ((a + b) >> 1)) & 0xff


def predictor_reconstructor(previous_line, current_line, start_bytes):
    for i, j in enumerate(range(len(current_line)), start=-start_bytes):
        x = current_line[j]
        if i < 0:
            pr = previous_line[j]
        else:
            a = current_line[i]
            c = previous_line[i]
            b = previous_line[j]
            pa = abs(b - c)
            pb = abs(a - c)
            pc = abs(a + b - c - c)
            if pa <= pb and pa <= pc:
                pr = a
            elif pb <= pc:
                pr = b
            else:
                pr = c
        current_line[j] = (x + pr) & 0xff


reconstruct_function = (do_nothing, sub_reconstructor, up_reconstructor,
                        average_reconstructor, predictor_reconstructor)


def un_filter_image_lines(image_lines, pixel_bytes):
    """ 返回每行的過濾 """
    previous_line = None

    for line in image_lines:
        filter_mode = line[0]
        filter_line = line[1:]

        reconstruct_function[filter_mode](
            previous_line, filter_line, pixel_bytes
        )
        previous_line = filter_line
        yield filter_line


def un_interlace(un_filter_images, width, height, pixel_bytes):
    # 下面方法,把每個圖片的所有行當成一個一緯數(shù)組來看待径筏,也就有 7 個數(shù)組
    reduced = tuple(itertools.chain(*m) for m in un_filter_images)

    for h in range(height):
        h_1 = (h % 8) << 3  # 偏移量
        for w in range(width):
            image_order = adam7sub1[h_1 + w % 8]  # 當前圖像的編號
            current = reduced[image_order]  # 切換數(shù)組

            for _ in range(pixel_bytes):  # 從數(shù)組里提取多個字節(jié)
                yield next(current)


def read_png(stream):
    if not stream.read(8) == b'\x89PNG\r\n\x1a\n':
        stream.seek(0, 0)  # 重置數(shù)據(jù)流
        return

    # 讀取需要的數(shù)據(jù)
    width = height = depth = color_type = interlace = pixel_bytes = \
        palette = None
    data = []

    while True:
        try:
            length, mask = struct.unpack('!I4s', stream.read(8))
            chunk = stream.read(length)
            crc32 = struct.unpack('!I', stream.read(4))[0]

            if zlib.crc32(mask + chunk) != crc32:
                break

            if mask == HEADER:
                (width, height, depth, color_type, compress_method,
                 filter_method, interlace) = struct.unpack('!2I5B', chunk)

                planes = (1, -1, 3, 1, 2, -1, 4)[color_type]
                pixel_bytes = (depth * planes + 7) // 8

            elif mask == PALETTE:
                palette = tuple(
                    chunk[i: i + 3] for i in range(0, length, 3))

            elif mask == DATA:
                data.append(chunk)

            elif mask == END:
                break

        except struct.error:
            break

    # LZ77 解壓
    decompress_obj = zlib.decompressobj()
    unzip_data = itertools.chain(
        *(decompress_obj.decompress(chunk) for chunk in data),
        decompress_obj.flush()
    )

    # 按行分割葛假,adam 表示 pass 提取算法得到的縮小圖的每行像素個數(shù)
    adam = (adam7iter if interlace else adam1iter)(width, height)
    reduced_images = tuple([] for _ in range(7 if interlace else 1))  # 容器
    current_image = 0  # 不要這個變量也能正常運行

    while True:
        try:
            line_bytes = next(adam)
            if line_bytes > 0:  # 大于 0 表示讀取長度
                reduced_images[current_image].append(bytearray(
                    # 這是一個從迭代器中依次讀取數(shù)量個數(shù)的方法
                    # 下面的意思是從 unzip_data 里讀取一定數(shù)量的字節(jié)
                    # 這個字節(jié)長度是縮小圖每行的字節(jié)長度 +1,多出來的是濾波標記
                    read_iterator(unzip_data, line_bytes * pixel_bytes + 1))
                )
            else:  # 小于等于 0 表示切換圖片滋恬,注意縮小圖是 7 張
                current_image = abs(line_bytes)

        except StopIteration:
            break

    # 濾波重構
    un_filter_images = (  # 把多個圖片的打包在一起
        # 把一個圖片的行打包在一起
        tuple(un_filter_image_lines(image_lines, pixel_bytes))
        for image_lines in reduced_images
    )

    # 數(shù)據(jù)回填
    if interlace:
        result = un_interlace(un_filter_images, width, height, pixel_bytes)
    else:
        result = itertools.chain(*next(un_filter_images))  # 這時只有一個圖片

    if depth == 16:  # 要記得放縮深度哦
        result = (j for i, j in enumerate(result) if (i % 2))

    elif palette:
        result = itertools.chain(*(palette[i] for i in result))

    return width, height, color_type, bytes(result)


def t_gray_to_rgb_alpha(data, alpha):
    for i in data:
        yield i
        yield i
        yield i
        yield alpha


def t_gray_alpha_to_rgb_alpha(data, background):
    while True:
        try:
            gray, alpha = next(data), next(data)
            yield background + (gray - background) * alpha // 255
        except StopIteration:
            break


def t_palette_to_rgb(data, palette, alpha, background):
    br, bg, bb = palette[background]
    for m in data:
        rr, rg, rb = palette[m]
        ra = alpha[m]
        yield br + (rr - br) * ra // 255
        yield bg + (rg - bg) * ra // 255
        yield bb + (rb - bb) * ra // 255


def t_palette_to_rgb_alpha(data, palette, alpha):
    for m in data:
        rr, rg, rb = palette[m]
        yield rr
        yield rg
        yield rb
        yield alpha[m]


def t_palette_to_rgb2(data, palette):
    for i in data:
        rr, rg, rb = palette[i]
        yield rr
        yield rg
        yield rb


def t_rgb_to_rgb(data, alpha, background):
    br, bg, bb = background
    while True:
        try:
            rr, rg, rb = next(data), next(data), next(data)
            yield br + (rr - br) * alpha // 255
            yield bg + (rg - bg) * alpha // 255
            yield bb + (rb - bb) * alpha // 255
        except StopIteration:
            break


def t_rgb_to_rgb_alpha(data, alpha):
    while True:
        try:
            yield next(data)
            yield next(data)
            yield next(data)
            yield alpha
        except StopIteration:
            break


def t_rgb_alpha_to_rgb(data, background):
    br, bg, bb = background
    while True:
        try:
            rr, rg, rb, ra = next(data), next(data), next(data), next(data)
            yield br + (rr - br) * ra // 255
            yield bg + (rg - bg) * ra // 255
            yield bb + (rb - bb) * ra // 255
        except StopIteration:
            break


def read_png2(stream):
    if not stream.read(8) == b'\x89PNG\r\n\x1a\n':
        stream.seek(0, 0)  # 重置數(shù)據(jù)流
        return

    # 讀取需要的數(shù)據(jù)
    width = height = depth = color_type = interlace = pixel_bytes = \
        palette = background = extra_alpha = None
    data = []

    while True:
        try:
            length, mask = struct.unpack('!I4s', stream.read(8))
            chunk = stream.read(length)
            crc32 = struct.unpack('!I', stream.read(4))[0]

            if zlib.crc32(mask + chunk) != crc32:
                break

            if mask == HEADER:
                (width, height, depth, color_type, compress_method,
                 filter_method, interlace) = struct.unpack('!2I5B', chunk)

                planes = (1, -1, 3, 1, 2, -1, 4)[color_type]
                pixel_bytes = (depth * planes + 7) // 8

            elif mask == PALETTE:
                palette = tuple(
                    chunk[i: i + 3] for i in range(0, length, 3))

            elif mask == DATA:
                data.append(chunk)

            elif mask == END:
                break

            elif mask == b'tRNS':  # 透明信息
                if color_type == COLOR_TYPE_GRAY:
                    extra_alpha = chunk[1]  # 只取一半
                elif color_type == COLOR_TYPE_RGB:  # 只取一半
                    extra_alpha = chunk[1], chunk[3], chunk[5]
                if color_type == COLOR_TYPE_PALETTE:
                    # 這時候是一個 alpha table
                    length = len(chunk)
                    extra_alpha = tuple(
                        chunk[i] if i < length else 255 for i in range(256)
                    )

            elif mask == b'bKGD':
                if color_type in (COLOR_TYPE_GRAY, COLOR_TYPE_GRAY_ALPHA):
                    background = chunk[1]
                elif color_type in (COLOR_TYPE_RGB, COLOR_TYPE_RGB_ALPHA):
                    background = chunk[1], chunk[3], chunk[5]
                elif color_type == COLOR_TYPE_PALETTE:
                    background = ord(chunk)  # 指向 palette 不過這里寫做代替

        except struct.error:
            break

    # LZ77 解壓
    decompress_obj = zlib.decompressobj()
    unzip_data = itertools.chain(
        *(decompress_obj.decompress(chunk) for chunk in data),
        decompress_obj.flush()
    )

    # 按行分割聊训,adam 表示 pass 提取算法得到的縮小圖的每行像素個數(shù)
    adam = (adam7iter if interlace else adam1iter)(width, height)
    reduced_images = tuple([] for _ in range(7 if interlace else 1))  # 容器
    current_image = 0  # 不要這個變量也能正常運行

    while True:
        try:
            line_bytes = next(adam)
            if line_bytes > 0:  # 大于 0 表示讀取長度
                reduced_images[current_image].append(bytearray(
                    # 這是一個從迭代器中依次讀取數(shù)量個數(shù)的方法
                    # 下面的意思是從 unzip_data 里讀取一定數(shù)量的字節(jié)
                    # 這個字節(jié)長度是縮小圖每行的字節(jié)長度 +1,多出來的是濾波標記
                    read_iterator(unzip_data, line_bytes * pixel_bytes + 1))
                )
            else:  # 小于等于 0 表示切換圖片恢氯,注意縮小圖是 7 張
                current_image = abs(line_bytes)

        except StopIteration:
            break

    # 濾波重構
    un_filter_images = (  # 把多個圖片的打包在一起
        # 把一個圖片的行打包在一起
        tuple(un_filter_image_lines(image_lines, pixel_bytes))
        for image_lines in reduced_images
    )

    # 數(shù)據(jù)回填
    if interlace:
        result = un_interlace(un_filter_images, width, height, pixel_bytes)
    else:
        result = itertools.chain(*next(un_filter_images))  # 這時只有一個圖片

    if depth == 16:  # 要記得放縮深度哦
        result = (j for i, j in enumerate(result) if (i % 2))

    if color_type == COLOR_TYPE_GRAY:
        if extra_alpha:  # 如果有額外的 alpha 通道
            if background:  # 把額外通道合并到灰度通道上带斑,這比較簡單
                mode = 'gray'
                result = (background + (i - background) * extra_alpha // 255
                          for i in result)

            else:
                mode = 'alpha'  # 為了把透明度全部體現(xiàn)
                result = t_gray_to_rgb_alpha(result, extra_alpha)

        else:  # 不做任何處理
            mode = 'gray'

    elif color_type == COLOR_TYPE_GRAY_ALPHA:
        # 因為已經(jīng)有了 alpha 通道,所以不關心 extra_alpha
        mode = 'alpha'
        result = t_gray_alpha_to_rgb_alpha(result, background or 0)

    elif color_type == COLOR_TYPE_PALETTE:
        if extra_alpha:
            if background:
                mode = 'rgb'
                result = t_palette_to_rgb(
                    result, palette, extra_alpha, background
                )
            else:
                mode = 'alpha'
                result = t_palette_to_rgb_alpha(result, palette, extra_alpha)

        else:
            mode = 'rgb'
            result = t_palette_to_rgb2(result, palette)

    elif color_type == COLOR_TYPE_RGB:
        if extra_alpha:
            if background:
                mode = 'rgb'
                result = t_rgb_to_rgb(result, extra_alpha, background)

            else:
                mode = 'alpha'
                result = t_rgb_to_rgb_alpha(result, extra_alpha)

        else:  # 不做任何處理
            mode = 'rgb'

    elif color_type == COLOR_TYPE_RGB_ALPHA:
        if background:
            mode = 'rgb'
            result = t_rgb_alpha_to_rgb(result, background)
        else:
            mode = 'alpha'

    else:
        raise

    return width, height, mode, bytes(result)


# 初版不支持額外的透明信息和背景色
read_png = read_png2


def write_png(width, height, mode, data):
    if mode == 'light':
        color_type = COLOR_TYPE_GRAY
        row_bytes = width
    elif mode == 'rgb':
        color_type = COLOR_TYPE_RGB
        row_bytes = width * 3
    elif mode == 'alpha':
        color_type = COLOR_TYPE_RGB_ALPHA
        row_bytes = width * 4
    else:
        raise

    header_data = struct.pack('!2I5B', width, height, 8, color_type, 0, 0, 0)
    header_crc32 = struct.pack('!I', zlib.crc32(HEADER + header_data))

    def meow():
        iter_data = iter(data)
        for _ in range(height):
            yield 0
            for _ in range(row_bytes):
                yield next(iter_data)

    filtered_image = bytes(meow())
    zipped_image = zlib.compress(filtered_image)
    zipped_length = struct.pack('!I', len(zipped_image))
    zipped_crc32 = struct.pack('!I', zlib.crc32(DATA + zipped_image))

    result = io.BytesIO()
    result.write(b'\x89PNG\r\n\x1a\n')
    result.write(b'\x00\x00\x00\r\x49\x48\x44\x52')
    result.write(header_data)
    result.write(header_crc32)
    result.write(zipped_length)
    result.write(DATA)
    result.write(zipped_image)
    result.write(zipped_crc32)
    result.write(b'\x00\x00\x00\x00\x49\x45\x4e\x44\xaeB`\x82')

    return result.getvalue()


def write_png2(width, height, mode, data, data_split=40503):
    if mode == 'light':
        color_type = COLOR_TYPE_GRAY
        row_bytes = width
    elif mode == 'rgb':
        color_type = COLOR_TYPE_RGB
        row_bytes = width * 3
    elif mode == 'alpha':
        color_type = COLOR_TYPE_RGB_ALPHA
        row_bytes = width * 4
    else:
        raise

    header_data = struct.pack('!2I5B', width, height, 8, color_type, 0, 0, 0)
    header_crc32 = struct.pack('!I', zlib.crc32(HEADER + header_data))

    filtered_image = io.BytesIO()

    for i in range(0, len(data), row_bytes):
        filtered_image.write(b'\x00')
        filtered_image.write(data[i: i + row_bytes])

    zipped_data = zlib.compress(filtered_image.getvalue())
    split_data = (zipped_data[i: i + data_split]
                  for i in range(0, len(zipped_data), data_split))

    result = io.BytesIO()
    result.write(b'\x89PNG\r\n\x1a\n')
    result.write(b'\x00\x00\x00\r\x49\x48\x44\x52')
    result.write(header_data)
    result.write(header_crc32)

    for s_data in split_data:
        s_length = struct.pack('!I', len(s_data))
        s_crc32 = struct.pack('!I', zlib.crc32(DATA + s_data))

        result.write(s_length)
        result.write(DATA)
        result.write(s_data)
        result.write(s_crc32)

    result.write(b'\x00\x00\x00\x00\x49\x45\x4e\x44\xaeB`\x82')

    return result.getvalue()


# 初版不支持 256 * 256 以上的信息
write_png = write_png2

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末勋拟,一起剝皮案震驚了整個濱河市勋磕,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌指黎,老刑警劉巖朋凉,帶你破解...
    沈念sama閱讀 222,807評論 6 518
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異醋安,居然都是意外死亡杂彭,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,284評論 3 399
  • 文/潘曉璐 我一進店門吓揪,熙熙樓的掌柜王于貴愁眉苦臉地迎上來亲怠,“玉大人,你說我怎么就攤上這事柠辞⊥呕啵” “怎么了?”我有些...
    開封第一講書人閱讀 169,589評論 0 363
  • 文/不壞的土叔 我叫張陵叭首,是天一觀的道長习勤。 經(jīng)常有香客問我,道長焙格,這世上最難降的妖魔是什么图毕? 我笑而不...
    開封第一講書人閱讀 60,188評論 1 300
  • 正文 為了忘掉前任,我火速辦了婚禮眷唉,結果婚禮上予颤,老公的妹妹穿的比我還像新娘囤官。我一直安慰自己,他們只是感情好蛤虐,可當我...
    茶點故事閱讀 69,185評論 6 398
  • 文/花漫 我一把揭開白布党饮。 她就那樣靜靜地躺著,像睡著了一般驳庭。 火紅的嫁衣襯著肌膚如雪刑顺。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,785評論 1 314
  • 那天饲常,我揣著相機與錄音捏检,去河邊找鬼。 笑死不皆,一個胖子當著我的面吹牛,可吹牛的內容都是我干的熊楼。 我是一名探鬼主播霹娄,決...
    沈念sama閱讀 41,220評論 3 423
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼鲫骗!你這毒婦竟也來了犬耻?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 40,167評論 0 277
  • 序言:老撾萬榮一對情侶失蹤执泰,失蹤者是張志新(化名)和其女友劉穎枕磁,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體术吝,經(jīng)...
    沈念sama閱讀 46,698評論 1 320
  • 正文 獨居荒郊野嶺守林人離奇死亡计济,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 38,767評論 3 343
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了排苍。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片沦寂。...
    茶點故事閱讀 40,912評論 1 353
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖淘衙,靈堂內的尸體忽然破棺而出传藏,到底是詐尸還是另有隱情,我是刑警寧澤彤守,帶...
    沈念sama閱讀 36,572評論 5 351
  • 正文 年R本政府宣布毯侦,位于F島的核電站,受9級特大地震影響具垫,放射性物質發(fā)生泄漏侈离。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 42,254評論 3 336
  • 文/蒙蒙 一做修、第九天 我趴在偏房一處隱蔽的房頂上張望霍狰。 院中可真熱鬧抡草,春花似錦、人聲如沸蔗坯。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,746評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽宾濒。三九已至腿短,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間绘梦,已是汗流浹背橘忱。 一陣腳步聲響...
    開封第一講書人閱讀 33,859評論 1 274
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留卸奉,地道東北人钝诚。 一個月前我還...
    沈念sama閱讀 49,359評論 3 379
  • 正文 我出身青樓,卻偏偏與公主長得像榄棵,于是被迫代替她去往敵國和親凝颇。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 45,922評論 2 361

推薦閱讀更多精彩內容