APNG 格式

为了更简单的让非专业人士理解,先了解下字符编码,为了不那么复杂,本文仅介绍2个辅助理解字节码,一个是常规的UTF8,一个是跟本文有关的HEX

  • 如果你使用记事本打开一张图片,会出现一串的乱码,这个其实是以UTF8编码的方式呈现,可以很简单的在node.js里使用Stream复现这个效果

    const fs = require('fs')
    // 以1.png创建一个可读写的stream流
    let readerStream = fs.createReadStream('images/1.png')
    // 设置编码格式
    readerStream.setEncoding('utf8')
    
    let data = ''
    
    // 使用readStream对象的EventEmitter监听事件
    readerStream.on('data', chunk => {
      data += chunk
    })
    readerStream.on('end', () => {
      console.log(data)
    });
    readerStream.on('error', err => {
      console.log(err.stack)
    });
    
    // 所以软件可以修改apng,原理都是如此大同小异
  • 或者也可以使用ES6中的FileReader API,直接在浏览器中预览。

    <input id="input" type="file">
    <script>
        let input = document.querySelector('#input')
        input.addEventListener('change', e => {
            let fileReader = new FileReader
            fileReader.onload = f => {
                console.log(f) //控制台打印数据,点击f.target.result右边的小图标即可预览
            }
            fileReader.readAsArrayBuffer(e.target.files[0]) //以二进制读取
        })
    </script>

    效果

  • 如果你使用另外的工具,比如winhex,打开图片,则会以 hex的编码方式呈现,复现时只需要把上面的setEncoding设置为hex

PNG文件是一种二进制的位图,由特定的文件头+若干文件块(chunk)组成 一个PNG文件的基本结构是这样的

|-- PNG Signature --|-- IHDR --|-- IDAT --|-- IEND --|

PNG 签名表示这是一个PNG文件 IHDR 是图片的基本信息,如宽高,色彩等 IDAT 是具体图片图像数据块,一个PNG文件有可能包含多个IDAT数据块 IEND 表示一个PNG文件的结尾

PNG的文件块(chunk)是特定格式的二进制数据块,其基本格式如下

|--4:长度--|--4:标识符--|--N:内容,长度由前面参数决定--|--4:CRC32--|

一个基本的APNG文件是在PNG文件格式上增加acTL, fcTL等动画控制块形成的。 此处引用张现成的图片说明 一下

请输入图片描述

acTL是动画控制块,包括 帧数和播放次数

fcTL是帧控制块,包括帧的大小位置,序号,延时,清除方式,混合方式等信息 第一个fcTL块后面跟的是一个或多个 IDAT 块 第N个fcTL块后面跟的是一个或多个 fdAT 块 fdAT的内容构成上,比IDAT多了一个序号,这个序号是整个文件 fcTL和fdAT 两种块一起共享的 一个fcTL以及后面跟的所有内容块,组成了APNG的一个帧

acTL

acTL块的格式如下

|--4:长度0x08--|--4:acTL--|--4:帧数--|--4:循环数--|--4:CRC32--|

结合原图我们查看一下内容,

请输入图片描述

  • 00 00 00 08 表示本块内容的长度(8字节)对于 acTL块来说是固定的
  • 61 63 54 4C 是 "acTL" 四字母的ASCII码
  • 00 00 00 19 表示本图片一共有0x19=== 25帧
  • 00 00 00 00 表示本图片的播放次数为:无限循环播放

fcTL

fcTL块的格式如下

(0) |--------------4:长度---------------|--------------4:fcTL---------------|
(8) |--------------4:序列号-------------|--------------4:宽度----------------|
(16)|--------------4:高度---------------|--------------4:X偏移--------------|
(24)|--------------4:Y偏移-------------|----2:延时分子----|----2:延时分母----|
(32)|-1:清除方式-|-1:混合方式-|-----------4:CRC32----------|

既然acTL告诉我们一共有25帧,那么fcTL块就会有25个,我们先看一下第一帧的fcTL

请输入图片描述

  • 00 00 00 1a 表示本块内容的长度(0x1a,即26字节)对于 fcTL块来说是固定的
  • 66 63 54 4C 是 "fcTL" 四字母的ASCII码
  • 00 00 00 00 表示本帧的序号为0
  • 00 00 00 94 表示本帧的宽度为 0x94 === 148 像素,高度也类似
  • 后面的 8字节00表示当前帧的位置是无偏移的
  • 00 32 03 E8 表示当前帧的播放延时为 0x32 / 0x03E8 即 50 / 1000 === 50ms
  • 01 表示本帧的清除方式为 【清除为背景】
  • 00 表示本帧的混合方式为 【覆盖】

关于清除方式 ,混合方式,可以看一下这篇文章 https://developer.mozilla.org/zh-CN/docs/Mozilla/Tech/APNG 在本篇文章的例子中,我们比较关注的是 序号,和fcTL的整体意义。

后续的帧就不重复写了,各帧的fcTL chunk ,字段意义是一样的。在本例子火狐图片中,除了序号和crc,都是一样的。

校验APNG格式(js检测机制)

校验 APNG 格式就是判断文件是否存在类型为 acTL 的块。因此需要依序读取文件中的每一块,获取块类型等数据。块的读取是根据上文所述的 PNG 块的基本组成结构进行处理,流程实现如下图所示:

请输入图片描述

off 初始值为 8,即 PNG Signature 的字节大小,然后依序读取每一块。首先读取 4 个字节获取数据块长度 length,继续读取 4 个字节获取数据块类型,然后执行回调函数处理本块的数据,根据回调函数返回值 res、块类型和 off 值判断是否需要继续读取下一块(res 值表示是否要继续读取下一块数据,默认为 undefined 继续读取)。如果继续则 off 值累加 4 + 4 + length + 4,偏移到下一块的开始循环执行,否则直接结束。关键代码如下:

const parseChunks = (bytes, callback) => {
    let off = 8;
    let res, length, type;
    do {
        length = readDWord(bytes, off);
        type = readString(bytes, off + 4, 4);
        res = callback(type, bytes, off, length);
        off += 12 + length;
    } while (res !== false && type !== 'IEND' && off < bytes.length);
};

调用 parseChunks 从头开始查找,一旦存在 type === 'acTL' 的块就返回 false 停止读取,关键实现如下:

let isAnimated = false;
parseChunks(bufferBytes, (type) => {
    if (type === 'acTL') {
        isAnimated = true;
        return false;
    }
    return true;
});
if (!isAnimated) {
    reject('Not an animated PNG');
    return;
}
最后修改:2022 年 03 月 10 日
如果觉得我的文章对你有用,请随意赞赏