js玩转APNG -- 逆转火狐

APNG是一种常见的网页动画,兼容性较好,交互性差,要想对其进行深入了解,则要了解其文件格式。本文以一个具体的问题为例,带你深入了解APNG的格式。

带着问题学习 -- 逆转火狐

先上问题:有一张火狐logo的图片,原图是顺时针旋转的,我们怎么来把它改为逆时针旋转呢?

js玩转APNG -- 逆转火狐

动画的基本原理

帧动画的基本原理是这样的,事先准备若干张静态图片(关键帧),每张图片之间有细微的差异,在快速顺序切换各个关键帧时,利用人眼视觉暂留的原理,给用户一个动画的错觉。

具体到火狐原图,其实他包含了25张关键帧,每一帧之间火狐旋转的角度有一点差别,然后每50ms播放一帧,这样就形成了动画

js玩转APNG -- 逆转火狐

鉴于以上原理,我们的整体思路其实还是比较简单的,把以上所有帧的播放顺序倒过来,就能把火狐逆转了。但在APNG里面实现,同时有新的问题

  1. 如何区别每一帧?
  2. 如何把播放顺序倒转?

    所以我们下一步是要学习APNG的文件格式

APNG 格式

PNG文件是一种二进制的位图,由特定的文件头+若干文件块(chunk)组成

一个PNG文件的基本结构是这样的

js javascript">|-- PNG Signature --|-- IHDR --|-- IDAT --|-- IEND --|

PNG 签名表示这是一个PNG文件

IHDR 是图片的基本信息,如宽高,色彩等

IDAT 是具体图片图像数据块,一个PNG文件有可能包含多个IDAT数据块

IEND 表示一个PNG文件的结尾

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

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

一个基本的APNG文件是在PNG文件格式上增加acTL, fcTL等动画控制块形成的。

此处引用张现成的图片说明 一下

js玩转APNG -- 逆转火狐

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

fcTL是帧控制块,包括帧的大小位置,序号,延时,清除方式,混合方式等信息

第一个fcTL块后面跟的是一个或多个 IDAT 块

第N个fcTL块后面跟的是一个或多个 fdAT 块

fdAT的内容构成上,比IDAT多了一个序号,这个序号是整个文件 fcTL和fdAT 两种块一起共享的

一个fcTL以及后面跟的所有内容块,组成了APNG的一个帧

acTL

acTL块的格式如下

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

结合原图我们用十六进制查看器看一下内容,

js玩转APNG -- 逆转火狐

  • 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

js玩转APNG -- 逆转火狐

  • 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的格式有比较深入的了解,回到前面两个问题

  1. 如何区别每一帧?

一个fcTL以及后面跟的所有内容块,组成了APNG的一个帧

  1. 如何把播放顺序倒转?

除了把帧数据倒过来以外,我们还要注意

第一帧的数据块为 IDAT ,不包含序号,

第N帧的数据块为 fdAT ,包含4字节的序号,其中序号是 fcTL和 fdAT 共享的

每一个块要改,都要同时计算其CRC数据

代码与实施

工欲善其事,必先利其器

我们下面要进行代码操作了,这些都是二进制操作,不太可能一蹴而就的,所以我们需要一些调试的手段辅助处理。我们应该可以预料到,对APNG文件进行此操作,文件的大小、帧的个数、序列号个数是不会变的,所以在开发的过程中,我们可以把这一部分信息输出出来,方便自己调试,并且对照修改前后的两个文件的信息

// eachChunk是对 PNG 每个chunk进行遍历的函数

eachChunk(bytes, (type, bytes, off, length) => {

const dv = new DataView(bytes.buffer);

textDOM.value += (type + '\n');

const obj = {};

switch (type) {

case 'fdAT':

obj.sequence_number = dv.getUint32(off + 8);

obj.crc = dv.getUint32(off + 8 + length);

break;

case 'fcTL':

obj.sequence_number = dv.getUint32(off + 8);

obj.width = dv.getUint32(off + 8 + 4);

obj.height = dv.getUint32(off + 8 + 8);

obj.x_offset = dv.getUint32(off + 8 + 12);

obj.y_offset = dv.getUint32(off + 8 + 16);

obj.delay = (dv.getUint16(off + 8 + 20) / (dv.getUint16(off + 8 + 22) || 100))* 1000;

obj.dispose_op = dv.getUint8(off + 8 + 24);

obj.blend_op = dv.getUint8(off + 8 + 25);

obj.crc = dv.getUint32(off + 8 + 26);

break;

default:

break;

}

textDOM.value += (JSON.stringify(obj) + '\n');

效果如下:

js玩转APNG -- 逆转火狐

我们可以看到这张图片一共有109个序列号 (sequence number),如果逆转操作前后序列号及其他信息不对,可以快速定位到检验不通过的地方,快速进行修正。

第一次遍历

由于我们只能按顺序读取文件内容,所以我们可能要遍历两次,第一次的时候主要是记录每一帧的位置偏移,还有把一些非数据的帧(如IHDR)记录下来

即形成以下的数据结构

js玩转APNG -- 逆转火狐

第二次是针对该数据结构的遍历,

先在“帧内容”里面进行遍历,拿出最后一帧,

然后在帧内进行遍历

对非内容块的读写,有时候会误改了IHDR,acTL等模块,这一部分如果出错,则会导致浏览器无法识别这是一张图片,此时如果强行用img.src 进行设置,会展示为404图片,即:

js玩转APNG -- 逆转火狐

这时候我们要仔细检查相应模块的内容是否正确。

第二次遍历

如果chunk是 fcTL,则要重新开始序号,并且重新计算crc32,相关代码如下

dv.setUint32(off + 8, sn++); // sn是一个文件级别的计数器,dv是当前帧(1个fcTL+若干数据)组成的ArrayBuffer的dataView

const fcTLCrc32 = CRC32.byte(chunk.subarray(off + 4, off + 8 + 26)); // 自己计算的crc32

dv.setUint32(off + 8 + 26, fcTLCrc32); // CRC32

dataArr.push(subBuffer(chunk, off, 8+length+4)); // subBuffer的功能是按指定下标拷贝一份新的ArrayBuffer

如果是 fdAT,

并且是第一帧,则要改为 IDAT

  1. 把chunk标识改了
  2. 把序号去掉

/**

* 输入标识名和内容,生成一个新的ArrayBuferr块

* @param {string} type

* @param {Uint8Array} dataBytes

* @return {Uint8Array}

*/

var makeChunkBytes = function (type, dataBytes) {

const crcLen = type.length + dataBytes.length;

const bytes = new Uint8Array(crcLen + 8);

const dv = new DataView(bytes.buffer);

dv.setUint32(0, dataBytes.length);

bytes.set(makeStringArray(type), 4);

bytes.set(dataBytes, 8);

var crc = CRC32.byte(bytes, 4, crcLen);

dv.setUint32(crcLen + 4, crc);

returnbytes;

};

const newData = makeChunkBytes('IDAT', chunk.subarray(off + 4 + 8, off + 8 + length)); // 4是sn,8是长度+chunk 标识

dataArr.push(newData);

如果不是第一帧,要改sn和crc32

dv.setUint32(off + 8, sn++);

dataArr.push(subBuffer(chunk, off, 8+length+4));

如果chunk标识是 IDAT,则要改为fdAT,并增加sn

case'IDAT':

const newFdAT = new Uint8Array(length + 4);

newFdAT.set([0,0,0,sn++]);

newFdAT.set(subBuffer(chunk, off + 8, length), 4);

dataArr.push(makeChunkBytes('fdAT', newFdAT));

break;

可以看到fcTL是APNG的播放控制内容,如果我们修改了一张APNG后,图片的大小正常,但显示为一片空白,或者只有一张静态的图片,那可以断定是fcTL这一块出现了问题,我们要仔细排查相应模块。

最后,把以上所有的数据装进一个PNG的容器里面,即前面是PNG 签名,IHDR, acTL,后面是 IEND 块,就能输出一份PNG图片了

const dataArr = [PNGSignature];

// .....

case'IHDR':

case'acTL':

dataArr.push(subBuffer(bytes, off, 12 + length));

// ......

dataArr.push(IEND_CHUNK);

const blob = new Blob(dataArr,{ 'type': 'image/png' });

const url = URL.createObjectURL(blob);

imgDOM.src = url;

整体代码思路如下:

js玩转APNG -- 逆转火狐

最终效果如下:

js玩转APNG -- 逆转火狐

相关资料

  • https://developer.mozilla.org/zh-CN/docs/Mozilla/Tech/APNG
  • https://aotu.io/notes/2016/11/07/apng/

以上是 js玩转APNG -- 逆转火狐 的全部内容, 来源链接: utcz.com/a/115543.html

回到顶部