便携式网络图形(PNG)规范(第二版)

信息技术——计算机图形和图像处理——便携式网络图形:功能规范(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(牛津布鲁克斯大学,第二版)

作者:在网页上自己找吧。

第二版内容相比第一版修正了一些规范,相关细节请参阅勘误表。

另外,这个文档还没有被翻译成中文。

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

摘要

便携式网络图形(PNG)是一种可扩展的文件格式,用于图像的无损压缩存储。它可以保存图片的多项内容,例如颜色索引、灰度通道、彩色通道、透明通道、1至16位的深度采样等。是 GIF 格式的无专利代替品,也可以在一定范围内代替 TIFF 格式使用。

该格式具有流式读取,快速检测文件完整性的特点,避免了文件传输中的由网络波动导致的一些问题,在实际应用中表现出色。此外,格式中可以添加伽马曲线信息以及色度信息,使其在不同平台、不同的屏幕色差中表现一致。

本文档是专门用于阐释 PNG 网络流媒体格式的标准规范。

文档状态

本文档是2003年10月14日公布的第二版PNG标准规范,同时也是国际标准 ISO/IEC 15948∶2003。除了封面和样板差异,文档内容与国际标准内容一致。但不保证此文档长期有效,请在W3C出版物列表上查找和更新最新版本。

文档的内容基于1996年10月公布的第一版PNG标准规范,结合了所有已知的勘误和澄清,已通过 ISO/IEC/JTC 1/SC 24、W3C、PNG 开发小组的审查。截至公布日,已拥有超过180种图像浏览器*、超过100种图像编辑软件*支持了 PNG 格式;SVG* 浏览器也都完全支持了 PNG 格式;尽管 HTML 规范并没有提及,但是仍有60款 HTML 浏览器*提供了选择。相关软件的支持可以访问pngstatus获取。

如需反馈,请通过邮箱联系 PNG 组,问题和答复会在网页上公布。

此文档内容网上公开,出版商 PNG 组不收取任何专利费用。

注意:

1、网页上的这篇文档,为了保证图片在 HTML 浏览器中正常显示,同时提供了 SVG、PNG 两种格式,在不同的浏览器中会显示不同的版本。

2、一些Bug,现在已经解决了。

文档还不存在英语以外的版本(我是假的)。

说明

本国际标准的设计目标是:

  1. 可移植性:格式的编码、解码和传输应与软件、硬件无关。
  2. 完备性:尽可能兼容真彩、索引和灰度图像,在每种情况下都可以添加透明度、颜色空间信息和辅助信息(如文本注释)。
  3. 串行编码解码:可以串行生成和读写,使数据能够在串行通信上实时生成和显示。
  4. 渐进式图像:在传输数据流时,可以开始呈现图像的近似值,并随着数据流的接收而逐渐清晰。
  5. 传输错误:可以可靠地检测数据传输错误。
  6. 无损耗:过滤和压缩应该保存所有的信息。
  7. 性能:任何过滤、压缩和渐进式图像呈现都应以高效解码和呈现为目标。读取图像(解码)比写入图像(编码)更重要,必要时牺牲编码速度。
  8. 压缩:压缩要有效率。
  9. 简单性:标准要简单,容易被开发者理解。
  10. 互换性:PNG 解码器应能读取所有符合 PNG 规范的数据/流。
  11. 灵活性:可以添加私人扩展,但不能损害互换性。
  12. 不触犯法律:不使用侵权的算法。

目录

[TOC]

1 范围

便携式网络图形(PNG 读作 “ping”)是一种可扩展的文件格式,用于图像的无损压缩存储。它可以保存图片的多项内容,例如颜色索引、灰度通道、彩色通道、透明通道、1至16位的深度采样等。是 GIF 格式的无专利代替品,也可以在一定范围内代替 TIFF 格式使用。

该格式具有流式读取,渐进式读取,快速检测文件完整性的特点,避免了文件传输中的由网络波动导致的一些问题,在实际应用中表现出色。此外,格式中可以添加伽马曲线信息以及色度信息,使其在不同平台、不同的屏幕色差中表现一致。

本文档是专门用于阐释 PNG 网络流媒体格式的标准规范。

2 参考文献

参考了下列规范性文件。这些规范有可能失效,但这里沿用旧版;对于没有标明日期的,使用最新版本。

文档编号 文档名称 备注
ISO 639:1988 语言名称表示代码
ISO/IEC 646:1991 国际标准化组织信息技术——信息交换用 ISO 7位编码字符集
ISO/IEC 3309:1993 信息技术——电信和系统间的信息交换——高级数据链路控制(HDLC)程序,帧结构
ISO/IEC 8859-1:1998 信息技术——8位单字节编码图形字符集——第1部分:拉丁字母表第1号 1
ISO/IEC 9899:1990(R1997) C语言
ISO/IEC 10646-1:1993 信息技术——通用多八位字节编码字符集(UCS)——第1部分:体系结构和基本多语言体系
IEC 61966-2-1 多媒体系统和设备——颜色测量和管理——第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 互联网主机的需求——应用和支持 4
RFC-1950 ZLIB压缩数据格式规范,第3.3版 5
RFC-1951 DEFLATE压缩数据格式规范,第1.3版 6
RFC-2045 MIME(多用途互联网邮件扩展)——第一部分:互联网消息体的格式 7
RFC-2048 MIME——第四部分:注册程序 8
RFC-3066 语言鉴别标签 9

备注:

  1. 为了方便使用,这里是简化的规范,只需要使用部分编码,参考: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 位深度)
字节 字节占用8位内存;也称八位位组。一个字节可以表示 0~255。
字节顺序 [3.2](#3.2 字节顺序)
通道 一个像素用于表示红、绿、蓝、灰度和透明度的部分。
色度 颜色是由亮度和色度共同表示的,可以通过色图来查找颜色。是色图啦!
一个标准的 PNG 文件由许多块组成,分为关键块和辅助块。
颜色类型 PNG 图片有五种:灰度、真彩、索引、灰度带透明、真彩带透明,记做颜色类型。
复合 通过透明度合并前景图像和背景图像来形成图像。
关键块 必须读取的 PNG 块。
数据流 是文件的一部分或者全部字节顺序。数据流是即时生成的内存数据。
deflate 一种 LZ77 解压算法。PNG 使用了这个算法的 0 模式。参考 RFC-1951
交付图像 通过数据流解码得到的图像,表示已经可以交付用户/图像缓冲区使用了。
滤波器 为了提高图片压缩效率,用来转换图片扫描数据的过滤。
图像缓冲区 内存中的一个区域,其中的数据专门用于屏幕显示。
伽马曲线值 人眼对自然亮度感知是非线性的(韦伯-费希勒定理)。
灰度 使用单一通道,来表示亮度,也就是单纯的黑白图片。
图片数据 扫描图片后得到的一维数组,这个数组可以映射在内存块里供用户使用。
索引颜色 [3.3](#3.3 索引颜色)
构建索引 指索引颜色的排序,通过序号可以从调色板中获取颜色。此外还有 alpha 表索引。
交错模式 [3.4](#3.4 交错模式)
无损压缩 数据压缩的方法,可以精确地重建原始数据。
有损压缩 数据压缩的方法,近似地重建原始数据。(我要的是全损压缩的算法啦!)
亮度 人眼感知的亮度。亮度和色度组合成为了人眼中的色彩。它的能量级别是对数增长的。
LZ77 Ziv 和 Lempel 先生在1977年发表了一篇论文,论文描述了这种数据压缩算法。
网络字节顺序 与主机字节顺序相反,网络字节顺序是指从低位往高位排序的字节。
调色板 调色板是一个颜色数组,每个颜色占用3个8位字节,最多储存256种颜色。
Pass 提取 一种提取图片缩略图的方法,用于交错模式。
像素 在计算机屏幕上,每个颜色的点被称为像素,它是一种字节数据。
PNG 数据流 符合 PNG 规范的数据流。
PNG 解码器 可以处理 PNG 数据流,生成交付图像的东西,一般是软件解码。
PNG 编辑器 可以修改、生成符合规范的 PNG 数据流的软件。
PNG 编码器 将二维像素数组转化为 PNG 数据流的东西。
PNG 文件 硬盘中保存的 PNG 数据流。
PNG 4字节带符号整数 4个字节的带符号整数可以表示 到 之间的整数。之所以这么限制是因为有些编程语言不支持 。
PNG 4字节无符号整数 4个字节的带符号整数可以表示 0 到 之间的整数。之所以这么限制是因为有些编程语言不支持无符号整数(然而我没见过)。
PNG 图像 PNG 编码器所产出的图像文件。
PNG 签名 PNG 数据流的开头8个字符,用以区别与其它数据流。
缩小图 也就是交错模式下,Pass 提取所解析出来的缩小图,最多七张。下面也会称为缩略图。
标准图像 BMP、GIF、JPG 等其他标准图像文件。
RGB 合并 将不同的颜色通道混合在一起。因为在 PNG 中,不同的通道是排在一起的。
样本 集合中随机抽取的元素,用它来代表全部的元素特性。属于泛指。
样本深度 样本的二进制长度。
样本深度放缩 标准图像的样本深度不是统一的,但是 PNG 要求一致,所以需要放缩这些深度。
扫描线 图像一行一行的数据。因为以前的显示器是一行一行显示的。
原图 给 PNG 编码器处理前的图像。
真彩 包含三个以上(红、绿、蓝)通道的图像模式。
白点 [3.5](#3.5 白点)
zlib 一个用于压缩解压缩的函数库。
CRC 循环冗余校验码,用于检测传输误差的校验码。通过加密算法计算得到。
CRT 阴极射线管,曾经的显示器。现在用 LCD 液晶显示器。
LSB 在网络字节顺序中,最高位的改动对值的影响最小,被称为最低有效比特(LSB)。
LUT Look up table 的缩写,是计算机上一种常用的数据转跳方式。
MSB 在网络字节顺序中,最低位的改动对字节值的影响最大,被称为最高有效比特(MSB)。

3.1 位深度

索引模式下,表示索引颜色的字节长;颜色模式下,表示颜色样本的字节长。

一般来说很少见到 1,2,4 的深度,因为不好读取,现代计算机采用内存对齐的读取方式,如果是长度小于8比特,就需要额外使用位域(bit field)读取了,效率就会降低。

3.2 字节顺序

在计算机发明的时候,设计了缓存、内存和硬盘的访问方式——地址查询。

地址从低往高排列,依次读取出来的数据被称为大端模式(Big-endian),反过来,从地址高位往低位读就是小端模式(Little-endian)。这两种都是字节顺序。

大端模式和人的阅读方式相同,但是不知道为什么被淘汰了,它现在主要在 IBM PowerPC 和网络上运作,所以也被称为网络字节顺序(network byte order)。

而现在的主机一般使用的 X86、ARM 处理器都是小端模式,被称为主机字节顺序(host byte order)。

PNG 使用网络字节顺序。

3.3 索引颜色

普通的真彩图片,至少由红、绿、蓝三个通道组成,也就是每个像素占用了三个字节以上的空间。这样图片的压缩效率就很低。

因此,我们构建一个调色板,在调色板里预设好我们需要使用到的颜色,后续使用时,只需要提供调色板的索引位置就可以了。

而且为了防止调色板占用过大空间,我们把调色板的容量设定在256以内。索引位置也就不会超过256,只需要一个字节就可以表示,一个字节只占用一个通道,因此索引模式下,只有一个通道。

3.4 交错模式

一个图片是由许多像素点组成的,可以视作一个二维数组。我们可以提取其中几个特殊的位置,比如横行每隔一个提取一个像素,竖列每隔一个提取一个像素,这样组成一个新的二维数组,可以简略失真的表示原图。这种提取方法叫做 Pass 提取(pass extraction),提取之后形成的数据是交错的,每段都可以包含整个图片的缩略部分。只需要重新组合就能产生完整的图像,即使没有完整的数据,只要其中一部分就可以得到缩略图。是网络传输中加快图像传输的方式,不过现在网速很快,几乎用不到了。

3.5 白点

色图的中间区域是白色的,被我们称作白点,是色图的一个参数。另一个参数是基准,用作色图的平移。

我们可以设置白点的坐标,来改变中间白色区域,使其向红色、绿色、蓝色偏移,用以调整色图的变化程度。

4 概念

4.1 图像

PNG 规范不指定应用程序接口,但是涉及四种图像:原图、标准图像、PNG 图像、交付图像。关系如下:

graph LR;
    subgraph ;
        id0[原图]--理论上-->id1[标准图像];
        id0-.实际上.->id2[PNG 图像];
    end;
          id2--编码-->id3[PNG 数据流];
          id3---id5[PNG 文件];
          id3--解码-->id4[PNG 图像];
    subgraph ;
          id6[标准图像]--理论上-->id7;
          id4-.实际上.->id7[交付图像];
    end;
像素通道 红色通道 绿色通道 蓝色通道 透明通道
二进制编码举例 1000 11011 0010 1000

二进制编码占用的比特数,就是样本深度。

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 透明通道分离

分离透明通道,实际上,很多标准图像没有透明通道,这样可以默认为无透明度,节省一个通道。

4.3.3 构建索引

如果不同像素值的个数少于 256,样本深度小于等于8,可以开始构建索引。

4.3.4 RGB 合并

如果,颜色样本深度一致,而且每个通道都一样的值,可以用一个通道来表示所有,即灰度图。

4.3.5 alpha 压缩

不用 alpha 通道表示透明度的一种方法,需要设置背景色。

4.3.6 样本深度放缩

不是所有的深度都被 PNG 支持,只有 1、2、4、8、16,如果不是这些数的话,深度就要通过软件调节。

比如原始深度是 5,现在要把它变成 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. 扫描线序列化:将图片(原图或缩小的图片)的数据从左到右、一行一行的读取,形成一个个数组。
  3. 过滤:使用滤波器将每个扫描线数组转换。
  4. 压缩:将过滤后的数据压缩。
  5. 分块:将压缩过的数据分为多个块,并添加校验码。
  6. 数据流构建:块被插入到数据流中。
graph LR;
    id[PNG 图片]-->id0[Pass 提取];
    id0-->id1[扫描线序列化];
    id1-->id2[过滤];
    id2-->id3[压缩];
    id3-->id4[分块];
    id4-->id5[数据流构建];

4.5.2 Pass 提取

这里采用两种方法进行 Pass 提取。

第一种是空方法,也就是什么都不做。(所以为什么要这么死板的把这个也计作一种方法)

第二种通过多次扫描得到七个缩小图。也就是 Adam7 算法(不是深度学习的那个 Adam 算法)。

不过这个算法在国内网站这个几乎找不到,维基百科https://en.wikipedia.org/wiki/Adam7_algorithm)介绍的也不是很清楚。所以我这里就简单说一下:

  1. 这个算法的目标是把一张图片分成很多缩略图,这个变换必须是可逆的,多个缩略图也能转换成一张完整的图片。这样就能防止文件损坏后,无法再读取图片了,因为缩略图还在,通过一定的尝试,还是可以恢复原始的图片。
  2. 最简单的分法,就是隔列/行提取,比如:每隔一个像素提取一个(或者每隔一行提取一行),就可以得到两个数组,每个数组都是原始图像的缩略数据。Adam 算法也是在隔列/行提取的基础上展开的。
  3. 把原始的二维像素数组视作一个棋盘,每格代表一个像素。原始的数组就是从左到右、从上到下一格一格的提取。现在我们把这个读取面扩大,像”田“字一样,一次性读取四个像素,按照下表中 Adam3 的 2x2 矩阵来分(分成四个缩略图也是可以的,但是这里分成三个),第一个像素放在第一张缩略图上,第二个像素放在第二张缩略图上,第三和第四个像素放在第三张缩略图上。如果读取到边缘,无法读出“田”字数据,就假装读取,把该有的像素放到缩略图上,不存在的就跳过。这就有 3 个缩略图。
  4. 再次扩展,按照 Adam5 的 4x4 矩阵读取,就可以读取 5 个缩略图。再扩展一次就是 8x8 矩阵了,可以一次性提取 7 个缩略图。PNG 规范采用的时候,普遍图片不超过三四百的尺寸,用 8x8 矩阵足够了。所以选定 Adam7 作为交错模式的算法。
Adam1 矩阵:  // 也就是普通的扫描方法
1

Adam3 矩阵:  // 它是由 Adam1 插值得到的,注意它的奇数行和奇数项与 Adam1 相同
1, 2  // 偶数列添加 2
3, 3  // 偶数行添加 3

Adam5 矩阵:  // 它是由 Adam3 插值得到的,注意它的奇数行和奇数项与 Adam3 相同
1, 4, 2, 4  // 偶数列添加 4
5, 5, 5, 5  // 偶数行添加 5
3, 4, 3, 4
5, 5, 5, 5

Adam7 矩阵:  // 它是由 Adam5 插值得到的,注意它的奇数行和奇数项与 Adam5 相同
1, 6, 4, 6, 2, 6, 4, 6  // 偶数列添加 6
7, 7, 7, 7, 7, 7, 7, 7  // 偶数行添加 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 过滤

有几种过滤类型,会把过滤类型写到过滤数组之前。

4.5.5 压缩

就是编码加密。

4.5.6 分块

把编码后的数据分成一块或者多块。

4.6 附加信息

信息类型 描述
背景色 显示图像时要使用固体背景颜色。
伽马和色度 图像输出强度的伽马值,以及图像中使用的色度范围。
国际刑事法院简介 颜色空间的描述。
图像直方图 预计图像使用调色板频率。
物理像素尺寸 用于显示 PNG 图像的预期像素大小和宽高比,你要知道像素和真实长度是有区别的。
有效位 在样本中有效的位数/深度。
sRGB色彩空间 渲染意向(由 ICC 定义)和图像采样符合的颜色空间。
建议调色板 简化的调色板,当计算机不能显示图像中的全部颜色时,可以使用该调色板。
文本数据 与图像相关的文本信息。
时间 文件修改时间。
透明信息(色) 没有透明通道时,用这个代替透明。

4.7 PNG 数据流

4.7.1 块

一个标准的 PNG 文件由许多块组成,每个块有四个部分:长度、名称、数据主体、校验码。

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 错误处理

传输错误或文件损坏,这会破坏数据流的大部分或全部;语法错误,出现无效块或者丢失块。

两种错误处理方式要区别。

4.9 拓展和注册

你可以向 ISO/IEC 或者 PNG Group 提交相关扩展,注册新的块类型和文本关键字,拓展新的过滤算法、交错模式的算法、压缩算法。

5 数据流结构

5.1 介绍

就是数据流的二进制的结构啦。

5.2 PNG 签名

所有 PNG 数据流的前八个字符,都是 137 80 78 78 71 13 10 26 10

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

这个签名表示接下来的数据都是 PNG 数据流,如果遇到空字符不要打断,需要出现 IEND 才算结束。

5.3 块布局

每个块由这四个部分组成:

  1. 长度,占有4个字节,表示数据主体的长度。
  2. 块名称,占有4个字节,不同的块有不同的类型标记。
  3. 数据主体。(当长度为 0 时,数据主体可以为空)
  4. CRC 校验码,占有4个字节,是类型标记数据主体的 crc32 加密码。

5.4 块名称约定

通过名称约定,使得 PNG 解码器在不能识别当前块的用途时,也能通过名称来获取相关信息。

块的名称有四位:

第一位表示辅助,小写表示这个块是辅助块,大写表示这个块是关键块。

第二位表示私有,小写表示这个块是私有的,而非国际标准定义的,大小表示前述 18 种块类型。

第三位是保留位,小写表示这个块是被抛弃的,大写表示可以使用。(用于约定将来的扩展)

第四位表示复制安全性,也就是 PNG 编辑器在编辑图片的时候,如果遇到不安全的数据块,就不会完全的复制,而是有选择的,大写表示 PNG 编辑器可以完全的复制,而不需要担心任何问题。

5.5 循环冗余码算法

具体参考 crc32 算法

5.6 块排序

因为 PNG 图像是可以流式读取的,也就是说不需要读到文件尾,就可以在 PNG 浏览器里预览了。

所以有些东西需要在读取图像内容之前准备好,比如索引调色板。

块名称 允许多个 顺序限制
IHDR 必须第一位
PLTE Shall be first
IDAT 如果有多个,必须连续出现
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 章节写过了?

6.1 颜色类型和值

我们在 4.4 章写过,有五种颜色类型:

颜色类型 通道数
灰度 0 1
真彩 2 3
索引 3 1
灰度带透明 4 2
真彩带透明 6 4

颜色类型记录在 IHDR 里。

灰度模式下,亮度取决于 gAMA、sRGB、iCCP,如果没有这些,则取决于机器。

颜色样本不一定和光强成正比,可以通过设置 gAMA 来调节。

值的计算方法如下:初始为 0,使用了调色板加上1,使用了真彩加上2,使用了透明通道加上4。灰度下是无法使用索引。

6.2 透明的表示方法

有四种方式表示透明:使用透明通道、使用 tRNS 块设置透明颜色信息、索引中在 tRNS 设置 alpha 表、不使用透明通道也不使用 tRNS 表示完全不透明。

透明通道的样本深度是 8 和 16,透明通道保存在像素之中, 表示完全透明,表示完全不透明。透明度用于图像前景色和背景色的复合。

一些普通的图片不包含透明度,甚至已经把像素值乘以透明度,提前做好了以黑色为背景的复合步骤;但是 PNG 不这么做。

7 PNG 图像编码

7.1 整形和字节顺序

整形(int)是多位字节,short 是两个,int 是四个,long 是八个。

PNG 使用的是网络字节顺序,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 时,扫描线结尾可能是凑不齐字节,这些不使用的字节不进行处理。

7.3 滤波器

滤波器可以提高压缩数据的压缩性,而且是可逆的。PNG 允许过滤扫描线数据,换句话说就是可以不进行滤波。

过滤后的字节序列与过滤前的相同,但是根据不同的滤波类型,会在最前面添加一个字节的标记。如果没有增加长度就表明没有过滤过。具体过滤方法,在后面解释。

8 交错模式和 Pass 提取

交错模式可以提高 CRT 显示器上网络图片的加载速度(换句话,没有网络,没有 CRT 显示器,交错模式就没有用了)。

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

介于 Adam7 的特点,宽或者高小于5的图像会缺少缩略图(2在第五列,3在第五行)。

9 过滤

9.1 过滤方法和滤波器类型

过滤的目的在于提高压缩率。过滤的方法不是唯一的,交错模式下,所有缩小图都应该使用同一种方法的滤波器;非交错模式下,只有一张图,当然也是只有一种方法。

这个标准里定义了一种 0 号方法,其他的编号为将来保留。0 号方法包含五种类型的滤波器,每个扫描线可以使用不同的滤波器类型。

PNG 规范不强制滤波器的类型,具体的选择方法后面在说。

9.2 0 号方法

滤波器是针对字节的,和像素、通道、深度无关。只要给字节就能过滤。

这里是几个参数的定义:

  1. x:将要被过滤的字节;
  2. a:前一个像素中对应 x 的字节;比如 x 是一个像素的第 2 个字节,a 就是前一个像素中第 2 个字节;如果一个像素只有一个字节,x 就是这个像素的唯一字节,a 就是前一个像素的唯一字节,两者紧挨着;
  3. b:前一行像素中对应 x 的字节;
  4. c:b 所在像素的前一个像素对应 x 的字节(也是对应 b 的字节);
... c b ...
... a x ...

Org() 表示字节原始值;

Flt() 表示过滤后的值;

Rc() 表示重构的值;

Paeth() 参见[9.4 滤波器类型4](#9.4 滤波器类型4)。

滤波器类型 名称 过滤函数 重建函数
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 代替。

因为使用了过滤,重建时也要按照这个顺序进行计算。

滤波器的输入输出值都是无符号字节。

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 算法先计算三个相邻像素(左、上、左上)的线性值,选择与计算值最接近的相邻像素再次计算。注意缓存不要溢出。它的函数如下:

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,压缩字节不超过 32768。

import zlib

zlib.compress(data=..., level=8)
压缩块的内容 字节数
zlib 压缩标志 1
附加校验标志 1
压缩数据块 n
校验值 4

这个校验值和 PNG 块的校验值不一样,两者不能混同。

多个过滤后的行,会被打包压缩成一个 zlib 数据流,并放到多个 PNG 块里,多个 PNG 块解开得到的是一个 zlib 数据流。

当然,这个还涉及到异步读取。zlib 数据流本身就是可以中断的,即使中断,排列较前的数据还是可以读取出来的,这样才有交错模式的解读,所以针对上面的 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)

构建一个持续的读取,边读取边解析。

11 块的介绍

下面是 18 个 PNG 规范块的介绍:

11.1 b'IHDR'

IHDR 是 PNG 数据流中的第一个块。组成如下:

用途 字节长度 类型
表示宽度 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'

用途 字节长度 类型
红色通道 1 unsigned byte
绿色通道 1 unsigned byte
蓝色通道 1 unsigned byte
... ... ...

调色板是一个二维数组,可以看作 Array[n][3],用索引 n 来表示颜色。

因此,n 不会超过 256,PLTE 块的长度也是 3 的倍数。

调色板无论如何都是 8 位深度,即使图像是 1、2、4 的深度,调色板也还是 8。

11.3 b'IDAT'

所有的 IDAT 块合起来是一个 zlib 数据流,参考[10 压缩](#10 压缩)。

11.4 b'IEND'

这段数据是空的,表示 PNG 数据流结束了。当然,这个块损坏也不会有事。

11.5 b'tRNS'

这是表示透明度信息的块,有三种构成:

灰度模式(凡是等于这个灰度的颜色被认做透明)

用途 字节长度 类型
灰度样本值 2 unsigned short

真彩模式(由这三个值表示一个透明色)

用途 字节长度 类型
红色样本值 2 unsigned short
绿色样本值 2 unsigned short
蓝色样本值 2 unsigned short

索引模式(索引模式下,tRNS 相当于一个 alpha 表,这个表和索引一样大,对应表示索引的透明度)

用途 字节长度 类型
表示调色板[0]的透明度 1 unsigned short
表示调色板[1]的透明度 1 unsigned short
... ... ...

为什么灰度模式和真彩模式用 2 字节表示呢,因为需要适配 16 位深度,而索引模式深度永远小于等于 8。

11.6 b'cHRM'

这个块用于设置一个 CIE 色度空间。组成如下:

用途 字节长度 类型
白点横坐标 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 描述。

用途 字节长度 类型
ICC 描述的名称 1-79 char[]
空字符 1 0
压缩方法 1 unsigned byte
压缩的描述信息 n compressed bytes

11.9 b'sBIT'

PNG 只支持固定的深度,如果原图深度不匹配,就会强制的放缩,但是原始信息会保留在这里,用于恢复原始图像。(所以一般不会用到,为什么要把标准图像转化成非标准图像呢)

针对不同通道数,有不同的 sBIT 长度。

颜色类型 长度(字节)
灰度(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'

关键字 后续文字的含义
Title 标题
Author 作者
Description 对于图片的描述
Copyright 图像版权
Creation Time 文件创建时间
Software 文件创建使用的软件
Disclaimer 法律免责申明
Warning 关于内容的警告
Source 拍摄图像的设备
Comment 其他建议

上面是一个文本信息会使用到的关键字,关键字其实不是很关键,就是一个定义而已,可以自己改动。但是符合上述的是可以被图像软件标准读取。

tEXt 含有如下成分:

用途 字节长度 类型
关键字 1-79 char[]
空字符 1 0
文本串 n char[]

11.12 b'zTXt'

用途 字节长度 类型
关键字 1-79 char[]
空字符 1 0
压缩方法 1 unsigned byte
压缩文本 n compressed data

当然,这里的压缩方法也是 0,用 zlib 解压后面的数据

11.13 b'iTXt'

国际文本数据,有点高大上的感觉。

用途 字节长度 类型
关键词 1-79 char[]
空字符 1 0
压缩标志 1 bool
压缩方法(当然这里也是 0) 1 0
用于标记语言种类 0字节或更多字节(字符串) char[]
空字符 1 0
翻译的关键词 n char[]
空字符 1 0
文本 n char[]

语言种类参见 RCF-3066、ISO 646、ISO 639。

11.14 b'bKGD'

背景色哦。

颜色类型 长度(字节)
灰度(0)灰度带透明(4) 2
真彩(2)真彩带透明(6) 6(红+绿+蓝,每个通道。2 字节)
索引(3) 1(指向调色板顺序)

11.15 b'hIST'

直方图给出了调色板中每种颜色的近似使用频率。

如果 PNG 浏览器无法提供调色板里所有的颜色,那么直方图可以辅助创建相似的调色板供使用。

当然,现在不存在不能提供完整的调色板的软件了。

用途 字节长度 类型
频率 2 unsigned short
... ... ...

11.16 b'pHYs'

这个块用于表示像素在屏幕上的实际尺寸。结构如下:

用途 字节长度 类型
横向每个单位的像素 4 unsigned int
纵向每个单位的像素 4 unsigned int
单位说明 1 unsigned byte

单位说明有两个,False 的时候表示这个块只表示长宽的比,而不是真实值;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 就是一个。

调色板名称是区分大小写的,并受到与关键字参数相同的限制。

在灰度PNG图像中,每个目通常相等的红色、绿色、蓝色和蓝色值,但这不是必需的。

每一个频率值与图像的像素的比例成正比,不是实际频率。

11.18 b'tIME'

用途 字节长度 类型
表示年的数字 (用完整数字;如:1995 而不是 95) 2 unsigned short
表示月的数字 (1-12) 1 unsigned byte
表示日的数字 (1-31) 1 unsigned byte
表示时的数字(0-23) 1字节 unsigned byte
表示分的数字(0-59) 1字节 unsigned byte
表示秒的数字(允许跳跃秒,0-60) 1字节 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


# 这两个函数是通过输入长和宽,来分割当前图像像素的
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):
    # 下面方法,把每个图片的所有行当成一个一纬数组来看待,也就有 7 个数组
    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]  # 切换数组

            for _ in range(pixel_bytes):  # 从数组里提取多个字节
                yield next(current)


def read_png(stream):
    if not stream.read(8) == b'\x89PNG\r\n\x1a\n':
        stream.seek(0, 0)  # 重置数据流
        return

    # 读取需要的数据
    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 提取算法得到的缩小图的每行像素个数
    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(
                    # 这是一个从迭代器中依次读取数量个数的方法
                    # 下面的意思是从 unzip_data 里读取一定数量的字节
                    # 这个字节长度是缩小图每行的字节长度 +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
    )

    # 数据回填
    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)  # 重置数据流
        return

    # 读取需要的数据
    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 提取算法得到的缩小图的每行像素个数
    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(
                    # 这是一个从迭代器中依次读取数量个数的方法
                    # 下面的意思是从 unzip_data 里读取一定数量的字节
                    # 这个字节长度是缩小图每行的字节长度 +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
    )

    # 数据回填
    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'  # 为了把透明度全部体现
                result = t_gray_to_rgb_alpha(result, extra_alpha)

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

    elif color_type == COLOR_TYPE_GRAY_ALPHA:
        # 因为已经有了 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

你可能感兴趣的:(便携式网络图形(PNG)规范(第二版))