js玩转APNG -- 逆转火狐
APNG是一种常见的网页动画,兼容性较好,交互性差,要想对其进行深入了解,则要了解其文件格式。本文以一个具体的问题为例,带你深入了解APNG的格式。
带着问题学习 -- 逆转火狐
先上问题:有一张火狐logo的图片,原图是顺时针旋转的,我们怎么来把它改为逆时针旋转呢?
动画的基本原理
帧动画的基本原理是这样的,事先准备若干张静态图片(关键帧),每张图片之间有细微的差异,在快速顺序切换各个关键帧时,利用人眼视觉暂留的原理,给用户一个动画的错觉。
具体到火狐原图,其实他包含了25张关键帧,每一帧之间火狐旋转的角度有一点差别,然后每50ms播放一帧,这样就形成了动画
鉴于以上原理,我们的整体思路其实还是比较简单的,把以上所有帧的播放顺序倒过来,就能把火狐逆转了。但在APNG里面实现,同时有新的问题
- 如何区别每一帧?
- 如何把播放顺序倒转?
所以我们下一步是要学习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等动画控制块形成的。
此处引用张现成的图片说明 一下
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
表示本帧的序号为000 00 00 94
表示本帧的宽度为 0x94 === 148 像素,高度也类似- 后面的 8字节00表示当前帧的位置是无偏移的
00 32 03 E8
表示当前帧的播放延时为 0x32 / 0x03E8 即 50 / 1000 === 50ms01
表示本帧的清除方式为 【清除为背景】00
表示本帧的混合方式为 【覆盖】
关于清除方式 ,混合方式,可以看一下这篇文章 https://developer.mozilla.org/zh-CN/docs/Mozilla/Tech/APNG
在本篇文章的例子中,我们比较关注的是 序号,和fcTL的整体意义。
后续的帧就不重复写了,各帧的fcTL chunk ,字段意义是一样的。在本例子火狐图片中,除了序号和crc,都是一样的。
转换思路
前面我们已经对APNG的格式有比较深入的了解,回到前面两个问题
- 如何区别每一帧?
一个fcTL以及后面跟的所有内容块,组成了APNG的一个帧
- 如何把播放顺序倒转?
除了把帧数据倒过来以外,我们还要注意
第一帧的数据块为 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');
效果如下:
我们可以看到这张图片一共有109个序列号 (sequence number),如果逆转操作前后序列号及其他信息不对,可以快速定位到检验不通过的地方,快速进行修正。
第一次遍历
由于我们只能按顺序读取文件内容,所以我们可能要遍历两次,第一次的时候主要是记录每一帧的位置偏移,还有把一些非数据的帧(如IHDR)记录下来
即形成以下的数据结构
第二次是针对该数据结构的遍历,
先在“帧内容”里面进行遍历,拿出最后一帧,
然后在帧内进行遍历
对非内容块的读写,有时候会误改了IHDR,acTL等模块,这一部分如果出错,则会导致浏览器无法识别这是一张图片,此时如果强行用img.src 进行设置,会展示为404图片,即:
这时候我们要仔细检查相应模块的内容是否正确。
第二次遍历
如果chunk是 fcTL,则要重新开始序号,并且重新计算crc32,相关代码如下
dv.setUint32(off + 8, sn++); // sn是一个文件级别的计数器,dv是当前帧(1个fcTL+若干数据)组成的ArrayBuffer的dataViewconst 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
- 把chunk标识改了
- 把序号去掉
/*** 输入标识名和内容,生成一个新的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;
整体代码思路如下:
最终效果如下:
相关资料
- 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