前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >js玩转APNG -- 逆转火狐

js玩转APNG -- 逆转火狐

作者头像
IMWeb前端团队
发布2019-12-13 09:13:29
2.3K0
发布2019-12-13 09:13:29
举报
文章被收录于专栏:IMWeb前端团队IMWeb前端团队

本文作者:IMWeb p2227 原文出处:IMWeb社区 未经同意,禁止转载

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

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

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

原图
原图

动画的基本原理

帧动画的基本原理是这样的,事先准备若干张静态图片(关键帧),每张图片之间有细微的差异,在快速顺序切换各个关键帧时,利用人眼视觉暂留的原理,给用户一个动画的错觉。 具体到火狐原图,其实他包含了25张关键帧,每一帧之间火狐旋转的角度有一点差别,然后每50ms播放一帧,这样就形成了动画

原图前8帧
原图前8帧

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

  1. 如何区别每一帧?
  2. 如何把播放顺序倒转? 所以我们下一步是要学习APNG的文件格式

APNG 格式

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

代码语言:javascript
复制
|-- PNG Signature --|-- IHDR --|-- IDAT --|-- IEND --|

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

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

代码语言:javascript
复制
|--4:长度--|--4:标识符--|--N:内容,长度由前面参数决定--|--4:CRC32--|

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

三个独立的 PNG 文件组成 APNG 的示意图
三个独立的 PNG 文件组成 APNG 的示意图

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

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

acTL

acTL块的格式如下

代码语言:javascript
复制
|--4:长度0x08--|--4:acTL--|--4:帧数--|--4:循环数--|--4:CRC32--|

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

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

fcTL

fcTL块的格式如下

代码语言:javascript
复制
(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

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

  1. 如何区别每一帧?

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

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

除了把帧数据倒过来以外,我们还要注意 第一帧的数据块为 IDAT ,不包含序号, 第N帧的数据块为 fdAT ,包含4字节的序号,其中序号是 fcTL和 fdAT 共享的 每一个块要改,都要同时计算其CRC数据

代码与实施

工欲善其事,必先利其器

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

代码语言:javascript
复制
// 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图片,即:

img不显示
img不显示

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

第二次遍历

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

代码语言:javascript
复制
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. 把序号去掉
代码语言:javascript
复制
/**
 * 输入标识名和内容,生成一个新的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);
    return bytes;
};

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

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

代码语言:javascript
复制
dv.setUint32(off + 8, sn++);
dataArr.push(subBuffer(chunk, off, 8+length+4));

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

代码语言:javascript
复制
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图片了

代码语言:javascript
复制
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;

整体代码思路如下:

整体代码思路
整体代码思路

最终效果如下:

最终效果
最终效果

相关资料

本文参与?腾讯云自媒体分享计划,分享自作者个人站点/博客。
如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客?前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与?腾讯云自媒体分享计划? ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 带着问题学习 -- 逆转火狐
  • 动画的基本原理
  • APNG 格式
    • acTL
      • fcTL
      • 转换思路
      • 代码与实施
        • 工欲善其事,必先利其器
          • 第一次遍历
            • 第二次遍历
            • 相关资料
            相关产品与服务
            容器服务
            腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
            领券
            问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
            http://www.vxiaotou.com