分析PNG图片结构和规范

阅读 1.9k
思考这样一个问题,给你一张图片,如何判断图片的类型呢?

可能会有人觉得这很简单:读取图片,得到其后缀名称,就能知道其类型了,但这种方式真的准确吗?

答案是否定的,现在有一张png格式的图片,名称为test.png,但是有人不小心(或恶意)改成了test.jpg,那么,这张图片虽然看上去是jpg格式,但其本身还是png格式的。

因此,要准确判断图片的类型,就需要了解图片的结构和规范。由于图片有各种格式,常见的有PNG、JPG和GIF等,不同格式的图片,它们的规范是不一样的,在本篇中,我们只聊PNG格式的图片。

PNG图片的组成

根据W3C的规范,PNG由PNG签名(PNG signature)chunks组成,如下图所示:


首先是8字节的PNG signature,其后跟着各种chunks。

chunks分为两大类:Critical chunksAncillary chunks,其中,Critical chunks是必须要有的,而Ancillary chunks则是辅助块(非必需)。

我们以实际的图片为例:

const fs = require('fs/promises')
const path = require('path')

async function example() {
  try {
    const imgPath = path.join(__dirname, '../../../image/logo.png')
    const buff = await fs.readFile(imgPath, { encoding: '' }) // Buffer

    console.log(buff)
  } catch (err) {
    console.log(err)
  }
}

example()

使用vs code中的debug调试上面代码,在控制台打印结果如下:

buff: Buffer(27074) [137, 80, 78, 71, 13, 10, 26, 10, 0, 0, 0, 13, ...

上面程序会将test.png图片读成字节数组(Buffer)的形式,其图片的大小为27074bytes,即该数组的长度。

我们先来看看PNG signature。

PNG signature

在W3C规范中,它占8bytes,十六进制值为:89 50 4E 47 0D 0A 1A 0A

上面例子中,我们读取的Buffer值前8位如下:


正好和规范中定义的十六进制值相等。所以,判断一张图片类型是不是PNG,就判断Buffer中的前八位值。

Critical chunks

这部分是必须的,它包含四种类型,除PLTE是非必需的外,IHDR、IDAT和IEND都是必须要有的,IHDR紧根PNG signature,IDAT可能有多个,IEND必须位于最后。其W3C上的规范定义如下:

对于每个chunk而言,它的组成部分的规则在W3C中定义如下:


其中,LENGTH占4个字节,表示该Chunk的CHUNK DATA的长度,CHUNK TYPE表示该Chunk的类型,CHUNK DATA可能会没有,最后是4个字节的CRC值。

拿上面的例子来说,对于test.png图片,它的IHDR部分如下:


第8到32个字节是test.png图片的IHDR,其中,第8到11个字节是该chunk data的长度,值为13,怎么计算呢?从上图可以看出,Chunk Data是第16到第28个字节,长度刚好为13。Chunk type占四个字节,在W3C规范中,IHDR的值为十六进制:49 48 44 52,转换为十进制,刚好为:73 72 68 62,而IHDR四个字符的Unicode 值刚好和它一样。第16到19位表示图片宽度,该test.png的宽度为0 * 256 ** 3 + 0 * 256 ** 2 + 0 * 256 ** 1 + 218 * 256 ** 0,结果为474,高度同理。

IDAT,可能会有多个。

IEND必须位于末尾,对于test.png而言,其值如下所示:


和IHDR不同,它没有Chunk Data部分,所以它的Length为0。

Ancillary chunks

这部分作为PNG图片的辅助数据,并不是必须的,具体可以参考W3C上的规范文档:https://www.w3.org/TR/png-3/#11Ancillary-chunks,这里就不详细说明了。

参考

最后编辑于: 2023-09-27

评论(0条)

(必填)
复制成功