
手撕主流图像(PNG/JPEG):从理解文件数据结构到图像尺寸解析的实战

本文将从底层开始剖析当前主流图像格式的文件格式,但并不对其中压缩算法展开深入的研究。故而在下更愿意称本文探讨的重心为——图像的数据结构,即不同类型的数据在存储设备中如何排布。同时我们作为一个用户,如何能从图像中准确的取出我们所需要的某一部分数据 —— 一张图像文件除了图像本体,还留存有许多元信息。
本文将对几种常见图片格式建立基本的认知模型,同时以 从图像数据流中提取出图像的分辨率,即宽高属性 作为实战案例。
PNG
参考2025年6月25日发布的第三版PNG规范[1]
- 关于PNG存储格式,主要参考官方说明的第五节Datastream structure
PNG图像的格式其实相对来说非常的清晰易懂。它由**文件署名(PNG signature)和数据块(chunks)**两种类型的结构组成,而且从宏观上看,它就是 —— 一个署名后面跟着很多个数据块。不同类型的数据块存储不同的数据。
文件署名域(PNG signature)
署名用于确认一个文件是否是PNG文件,是永远出现在PNG数据流最头部的八字节常量。
用16进制打开任意PNG文件, 会发现前八个字节总是这几个十六进制数 89 50 4E 47 0D 0A 1A 0A
这八个字节就是PNG的文件署名。出现这个签名则表明剩下的内容包含单个PNG图像,该图像由一系列以’IHDR’开头、以’IEND’的数据块组成。
为什么是“单张PNG图像”呢?实际上能播放动画的APNG也是这个署名开头:
因为从技术上讲,APNG也属于’a single PNG image’。在下认为这并不只是一个无趣的文字游戏,因为APNG诞生在PNG之后,为了实现向下兼容,它只是在原基础上增添了几种新的数据块类型,如果忽略新增的数据块类型,那么剩下来的部分,就是像存储正常的PNG一样存储APNG的第一帧,所以即便是不支持APNG的老旧浏览器,只要对无法解析的数据块类型进行抛弃,就能够回退到“显示第一帧”的状态。[4]
在理解了静态PNG格式之后再来看下面这张图,就能很容易理解APNG到底是怎么对原有的PNG进行拓展的了。这里从Wikipedia上借来一张APNG示例图,感兴趣可以用十六进制工具看看。在下的观察结果是完美符合上图的:
这里使用VScode的Hex Editer插件,对一张普通的静态的PNG照片的十六进制数据进行观察:
如图所示,文件署名域对应一张PNG图像的前八个字节,可以发现里面还偷藏了一个”PNG”。理论符合预期,当我们要处理图像时,若解析到前八个字节为PNG的署名,那么就可以认为,我们将要解析一张PNG图像了。
数据块
在署名之后,剩下的就都是一连串的数据块了。单个数据块的结构如下:
其实非常简单,只需要建立 四个Field 的认知模型即可,每个Field分别代表(按顺序排布)
length: 四字节无符号整数,用于指定 实际数据CHUNK DATA 的长度 —— 所以长度为0的时候CHUNK DATA域等效于不存在,就回退到了图中所示的三个Field的情况了。
chunk type: 四字节,每个字节可以理解为一个ASCII码
Each byte of a chunk type is restricted to the hexadecimal values 41 to 5A and 61 to 7A
熟悉的话其实会发现,这其实就是约束了每个字节对应的ASCII字符就是’A’-‘Z’ 或 ‘a’-‘z’, 也就是说是直接人类可读的英文字母编码
chunk data:实际的数据内容。长度由length域约束,可为0
CRC: 四字节, 仅根据CHUNK TYPE和CHUNK DATA计算出来的值,属于校验码,这里不用太在意其中细节。
至此,数据块基本结构的理解就已经完成了。整个PNG存储的模型也就出来了。再之后就只需要在用得到的时候补全对于不同类型数据块结构的认知即可。
就是说,接下来去翻官网的#11. Chunk specifications 就可以啦~
注:APNG增加的acTL、fcTL、fdAT类型在第三版的PNG规范中已经收录,也就是说都能在这份官方文档中查到。
关键数据块
有些类型的数据块基本上必须存在于一张PNG图像中,称为Critical Chunk。
以下为四种Critical Chunk,其中只有PLTE是可选的,其它都必须出现,而且必须以以下顺序出现
开头与结尾 IHDR-IEND
类型一个是IHDR,一个是IEND, 没必要记16进制码,直接记字母就行。
IHDR是非常特殊的块,它和IEND一起框定了一串数据的范围。可以理解为,一段有效的数据,比如一帧,它会是很多数据块,解析的时候遇到IHDR就知道这段数据开始了,遇到IEND意味着这段数据结束了。 前者必须是第一个数据块,而后者必须是最后一个数据块。
甚至还有人发现可以在IEND后面藏数据而不影响图像显示[2]。
其中IEND就是一个结束标志,其长度域为0,之后不再提,但IHDR类型对应的数据块还存储了一些有用的信息,图像的宽高属性就存储在这里,下一节详细解析。
IHDR Image header
IHDR总长度是固定的13bytes,内部数据排布如下:
Width | 4 bytes |
Height | 4 bytes |
Bit depth | 1 byte |
Color type | 1 byte |
Compression method | 1 byte |
Filter method | 1 byte |
Interlace method | 1 byte |
一般用户只需要关注前4个属性即可,分别代表图像的宽、图像的高、图像位深、颜色类型
宽高以像素为单位;位深指的是每个颜色通道的位深,而不是整个像素的位深,即记录的是每个颜色通道使用多少比特进行编码。
颜色类型(注意颜色类型和位深只有关系的):
官方文档通过有序列表暗示了这些IHDR属性是顺序排列的。不过更进一步,我们可以基于“协议本身是稳定的,市面上流传的PNG图像的这几个属性的排列顺序是稳定的”的假设,可以找一张PNG图像拆解开来验证这一点——结论:至少前四个属性是按顺序存储的。
这里宽是0x500,对应十进制1280,长为0x8E3,对应十进制2275,这正好就是在下这张测试图像分辨率中的宽高,通过测试。
0x08对应颜色位深8,0x02对应真彩色图像,RGB三色通道各使用8位编码,所以每个像素使用
- 注: 数据大端对齐,即高字节对应低地址
PLTE
Palette,调色盘。
含1-256个entries,每个entry三个字节,每个字节分别对应红绿蓝。
似乎主要是用于压缩等用途的元信息,也不是必须的数据块,非PNG图像的最小组成部分之一,这里跳过。
IDAT
Image Data, 图像真实的数据
注:不是将每个像素的RGB存储的原始数据,而是压缩算法的输出结果
必须的数据块类型就上述四种,其它感兴趣可以去文档中了解。
如何获得图像尺寸
基于上述观察,实际上只需要越过PNG的文件署名,直接进入IHDR头中的固定偏移位置取出对应数据即可。
JPEG
特别注意,JPEG和PNG不同,并非一个单一独立的规范,而存在很多不同的标准——不同的文件格式。这里参考T.81标准[3], 又称ISO/IEC 10918-1 标准, 算是JPEG压缩技术最权威最官方的一个技术标准了,能够解释现在流行的绝大部分JPEG文件的格式,如JFIF和Exif。文献很长,这里只关注JPEG的数据结构,基本可以从31页的Compressed data formats章节开始看。
由于JPEG格式与PNG不同,相对而言要复杂许多,所以这里不做非常详细地解读,搞清楚宏观的结构并理解如何获取图像尺寸就结束了。 (因为真实的数据反正也要过压缩算法,费劲搞清楚了感觉一般也用不到)。
宏观设计
从最高层次看,JPEG文件有点像之前的PNG,但不完全一样。PNG由一个署名打头,后面跟着很多数据块(chunk),而JPEG则直接由一系列的段(Segment)组成。而段又分标记段(Marker Segment)和熵编码数据段(Entropy-coded data segments),前者作为标记和引导,存储着大量元信息,而后者则是图像本体在压缩后的结果载体。
虽然文档中提到了“参数”,但在下认为这里提到的参数不过是“段”的组成部分,不需要单独拎出来理解。
Structurally, the compressed data formats consist of an ordered collection of parameters, markers, and entropy-coded data segments. Parameters and markers in turn are often organized into marker segments. Because all of these constituent parts are represented with byte-aligned codes, each compressed data format consists of an ordered sequence of 8-bit bytes. For each byte, a most significant bit (MSB) and a least significant bit (LSB) are defined
可以想象我们是一个流式的数据解析器,我们顺序地读入JPEG数据,会读到一个个段,在熵编码数据段之前会读到标记段(Marker Segement),其由一个标记Marker和若干跟随的参数(长度可为0)组成。我们可以利用这些像“路标”一样的标记Marker,识别后续内容,快速跳过不需要的信息,也可以从跟随其的参数中提取出一些需要的元信息。
整体上来看,JPEG文件的数据流就是很多的标记段和穿插的‘几个’熵编码数据段
- 对于Baseline JPEG,图像数据是一个完整的连续的熵编码数据段 (相当于一次扫描)
- 但是对于Progressive JPEG,每次扫描不断填充细节和色彩精度,熵编码数据段就不是连续的
- 不过从下图看,无论是基线还是渐进JPEG,都可以插入重启标记RST(同样会让数据段被分割开来), 以增加容错性。
我们不关注具体被压缩后的数据长什么样,所以对我们来说现在最重要的就是如何辨识标记(Marker)
标记Marker
在JPEG数据流中,找到字节0xFF,并且后面不是0x00, 就找到了Marker,0xFF后面跟的字节就决定了类型。
Marker的格式
一个Marker长两个字节
第一个字节:一定是 0xFF。 这个特别的值应该就是专门保留出来标识Marker的
第二个字节:
特殊情况
- 不能是0x00: 为了防止在数据段出现0xFF被误认为Marker,所有 ‘数据中的0xFF‘ 后面都会跟上0x00, 表明这是数据而非标记。
- 如果是0xFF: 被称为填充字节(fill bytes), 并不代表Marker的类型,仅做填充。可能是出于数据对齐的考量而设计,总之解析到0xFF的话,就继续往下看一字节,先前读到的第一个0xFF直接丢弃
正常情况,第二个字节标识了Marker的类型,标识了接下来的数据是什么类型的数据,具体匹配关系可以参见下表
特别说明:在Marker之后,即在整个标记段的剩余部分,可能还有其它数据。若有(跟着的不是0xff…),则紧跟着的为两字节长度参数,特指该Marker段除开Marker后参数的长度(不包含开头Marker的‘两’个字节,包含记录长度的两个字节)。这个参数可以用于快速跳过该Marker段达到下一个Marker段。
具体数据构造
让我们结合例子逐步剖析。
这里使用onlinehexeditor作为十六进制查看器,因为它还能提供对jpeg结构的部分信息的解析与提取,有助于辅助验证。
注:建议使用钉图工具如PixPin,将上面提到的Marker符号表钉在屏幕的某个位置,方便确认Marker类型。
上传一张jpeg图像A作为示例:
最上层
本节开篇展示了JPEG的多层级结构。在最上层,把Frame视为主体,则两侧分别有SOI和EOI,如图所示:
而图像A的实际数据头尾如图所示:
查表不难发现,开头的数据和结尾的数据恰恰就是SOI和EOI的Marker。所以中间的部分就是架构图中的Frame。
注: 这两个Maker没有长度参数,因为仅作标识作用,无内部信息需要存储。
Frame内
进入Frame,重点要找的是帧头Frame Header。Frame内部结构如图所示:
Frame header前
先看看Frame header前面有什么数据:
APP
DQT代表定义量化表Define Quantization Table,这跟JPEG的压缩算法直接相关。大致理解的话,应该是在压缩过程中——具体可参考文档中的DCT算法——对于处理后得到的8 × 8 DCT coefficient array,去除以量化表中的64个值。这里注意到DQT段的长度并非0x0041(多出来的第一个字节是量化精度), 而是0x0084, 是因为这里存了两个量化表,0x0082的长度,再算上长度数据本身的两个字节,就得到了0x0084的总长.
这里只是一个例子,表明Frame header前存在某些数据,至于有哪些,有多少,则根据具体情况会有所不同。
Frame header
这里就到了对于解析出图像尺寸而言最重要的Frame header了!
Frame header也是一个标记段Marker Segment,SOF类。
如图所示,遍历到SOF的Marker就说明遍历到了Frame header (不知道为什么文档35页配图位置附近只特别提了所有非差分类型的SOF,但Frame Header的结构应该都是一样的,即图中所示SOF
其参数代表的含义为:
- Lf:这个Segment的长度,和其他Segment保持一致。
- P: 样本精度,即每个采样点(sample)需要多少位来存储,即每个color的位深。
- Y: 行数,即图像的高
- X:一行的采样点数,即图像的宽
Y和X参数就是我们要寻找的图像尺寸信息,感兴趣可以看看它们的官方解释:
Y: Number of lines – Specifies the maximum number of lines in the source image. This shall be equal to the number of lines in the component with the maximum number of vertical samples (see A.1.1). Value 0 indicates that the number of lines shall be defined by the DNL marker and parameters at the end of the first scan (see B.2.5)
X: Number of samples per line – Specifies the maximum number of samples per line in the source image. This shall be equal to the number of samples per line in the component with the maximum number of horizontal samples (see A.1.1)
这几个参数的位宽如下图所示:
让我们结合先前图像A的例子来观察一下真实的数据,0xFFC0表示SOF
可以看到P为0x08,单色为8bits,三个颜色通道各8bits,每个像素就是24bits,符合位深度24的文件属性。
而高宽均为十六进制0x0438, 十进制数1080。这恰好符合文件属性。
- 注:如果看官方文档对P的说明的话,这里可能会有一点让人感到困惑的地方,因为官方的说法是P代表the precision in bits for the samples of the components in the frame。实际上sample是指一个color sample,而不是指一个像素。想象多个矩阵阵列即可理解,不同颜色的矩阵可以独立存储(即不是[R1, G1, B1], [R2, G2, B2], [R3, G3, B3] … 这样存,而是R单独一个矩阵,G单独一个矩阵这样存,对任一矩阵的任一元素,位深为P —— 虽然事实上不一定真的这样存,但逻辑上大致是这样)。
补充:刚才那张图高宽相同,不便验证高宽顺序,这里再换个例子看一遍,这是另一张位深24,分辨率4096x2160的JPEG图像:
可以看到,高0x0870, 即十进制2160,宽0x1000,即十进制4096, 完美符合预期,顺序也可以得到映证。
为了直观地感受到这张图是扁的,而非长的,这里附上分辨率4096x2160的样例图:

眼尖的话其实会发现刚才两个案例在SOF, 0xFFC0的Marker后面都出现了FFC4, 它对应的是Marker DHT, 即Define Huffman table(s), 哈夫曼表。这里涉及到压缩算法,不展开讲——因为它似乎有很多版本, 至少除了Huffman coding还有arithmetic coding。
如何获得图像尺寸
基于上述观察,我们只需要首先通过检验SOI marker,确认这是一张JPEG图像,然后一路跳过无用的标记段,直到遇到SOF
- 注:RST、EOI、SOI没有长度字段。以RST
(0xFFD7)为例,可以看到后面跟的既非新的marker也非长度,对于我们的场景(不关心“重启间隔”标记),就必须要通过遍历的方式才能安全地找到下一个标记。
基于ITU-T T.81标准的主流JPEG规范,如JFIF[5]和EXIF[6]等规范理论上都能够被支持。
Get Image Size
基于以上观察,这里给出获取PNG图像和大部分JPEG图像(包括JFIF和EXIF)宽高的js代码。仅供参考:
1 | /** |
- W3C. Portable Network Graphics (PNG) Specification (Third Edition). W3C, 2025-06-25 ↩
- maxiongying. PNG文件格式详解 . 博客园, 2018-08-25 ↩
- CCITT (The International Telegraph and Telephone Consultative Committee) / ITU (International Telecommunication Union). Recommendation T.81: Information Technology - Digital Compression and Coding of Continuous-tone Still Images - Requirements and Guidelines. ITU (International Telecommunication Union), 1992-9-18 ↩
- . APNG. wikipedia, 2025-9-10 ↩
- Recommendation ITU-T T.871. Digital compression and coding of continuous-tone still images: JPEG File Interchange Format (JFIF) . ITU, 2011-05 ↩
- Technical Standardization Committee on AV & IT Storage Systems and Equipment. EXIF文档,指出基于ISO/IEC 10918-1标准. Japan Electronics and Information Technology Industries Association, 2002-4 ↩
- 标题: 手撕主流图像(PNG/JPEG):从理解文件数据结构到图像尺寸解析的实战
- 作者: Prometheus0017
- 创建于 : 2025-09-17 16:40:04
- 更新于 : 2025-09-23 10:47:55
- 链接: https://blog-seeles-secret-garden.vercel.app/2025/09/17/图片格式解析/
- 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。