Gif的进化版 —— Apng的解析以及编辑
想必大家都用都过图,知道表情包一般都是
gif
图,本文将带你了解它的船新升级版本 ——Apng
。希望通过此文,大家能对Apng
以及如何通过js
操作二进制数据有所了解。
起因
某天 UI 提了一个需求想把项目中的动图的延迟做调整,项目中使用的 apng 格式图片,由于调整后间隔是固定的,我想的是直接修改 Apng 数据就好了,不想额外引入 apng-js,而且它不支持导出为图片,还需要把现有的图片改成组件。虽然现在也能找到一些支持修改 Apng 的工具,但用起来不是很顺手(不太符合这个需求),要么就是没有相应的程序包,所以打算自己整一个。
简介
说起png
,大家都有所耳闻,png
的全称是Portable Network Graphics(便携式网络图形),是一种支持无损数据压缩的光栅图形文件格式,被开发为一种改进的gif
(有说法称png
是png not gif的简写)。
我们对png
的第一印象一般都是支持透明度,对gif
的印象一般都是动图(各种表情包~),那为什么说png
是gif
的改进型呢?
这里有一段故事:gif
的全称是 Graphics Interchange Format( 图形交换格式 ),最初是为了填补跨平台图像格式的空白,可以理解为静态图像和视频之间的空白。gif
在当时来说已经满足了需求,问题在于gif
压缩使用了LZW
算法(串表压缩算法),而这个算法的商用是需要授权的,在这个阴影笼罩下,业界很快就推出了不含LZW
算法的图片,png
由此诞生。
在我们的认识中,gif
的主要特点是支持动态图像的,基于这一点,png
又怎么能够替代gif
呢?这个要基于当时的实际情况考虑,当时动态的gif
的应用是比较少的,并且基于其他各种原因,png
被确定应是一种单图像格式规范,多图像的支持由后续的扩展实现,称为MNG
全称是Multiple-image Network Graphics。MNG
于 2001 年推出,但可惜的是没有被各大浏览器支持,最后销声匿迹。
2004 年,Mozilla
内部诞生了一种叫做Apng
的动图格式,它本身由一帧帧png
组成,结合优异的压缩算法,能在与gif
图相当大小的情况下实现更好的现实效果,在不支持Apng
的浏览器里也能降级为静态的png
图片,下面是gif
与Apng
的效果对比。
Apng
在近几年已经得到各大浏览器的支持,在需要显示动态图且对图片质量有要求的场景,我们可以尝试使用Apng
,由于png
本身的特性,显示效果会比gif
更好。
Apng 格式
Apng
作为png
的一种扩展,主要的整个数据结果与png
一致,只是多定义了几种数据块类型,Apng
的结构如下图所示,它实际上是由多张png
图片组成的,通过acTL
模块控制动画效果,fcTL
模块控制帧间的过度效果。图像的数据块结构保持与png
一致,除了第一帧之外的数据帧命名为fdAT
,其他的数据块与png
一致。
每个数据块的组成如下图,除了文件头,其他块都可以按照这个规则去解析
接下我们简单了解一下几个主要数据块。
PNG signature
这一段文件头是固定的,称为魔数magic number,是png
专用的文件头。
IHDR
文件头数据块IHDR
(header chunk):它包含有png
文件中存储的图像数据的基本信息,并要作为第一个数据块出现在png
数据流中,而且一个png
数据流中只能有一个文件头数据块。它储存了以下一些数据
width
长度height
高度bit depth
图像深度color type
颜色类型compression method
压缩方法filter method
滤波器方法interlace method
非隔行扫描方法
其中需要关注的是width
和height
,png
的宽高在这里定义。
color type
代表图像的颜色类型,能区分是灰阶,索引色还是真彩色等等,当值为 3 时,代表是索引色,此时需要提供一个PLTE
数据块。扩展一下,png
又可以细分为png-8
,png-24
,png-32
,其中png-8
使用的就是索引色,具体可以参考下图:
acTL
动画控制数据块animation control chunk,用于控制动画的效果,提供了两个数据
- num_frames 总共有多少帧
- num_play 动画播放多少次,0 为循环播放
fcTL
帧控制数据块frame control chunk,用于控制如何帧间混合效果,是直接覆盖还是部分覆盖,偏移量和延时等信息,提供的数据有:
sequence_number 帧序号
width 宽度
height 高度
x_offset 此帧数据 x 轴偏移量
y_offset 此帧数据 y 轴偏移量
delay_num 间隔分子
delay_den 间隔分母
dispose_op 在显示该帧之前,需要对前面缓冲输出区域做何种处理。
blend_op 帧渲染类型
这里可以发现Apng
优化体积的一种手段,每一帧的数据提供的画面不一定是完整,需要结合上一帧来渲染,如果整个画面只有部分变化,实际上当前帧只提供了变化部分的图像数据,通过这样的方式可以有效地节省文件体积。
dispose_op
规定了在显示下一帧之前要做的处理,取值对应的处理方式:
- 0: 不做任何处理,保持原样
- 1: 把当前缓冲区的数据都处理为透明
- 2: 渲染下一帧之前把缓冲区恢复成之前的状况
blend_op
规定了当前帧的内容的处理方式,取值与对应的处理方式:
- 0: 将当前帧的内容替换到当前缓冲区上
- 1: 将当前帧的内容混合到当前缓冲区中
fdAT
帧数据块frame data chunk,基本结构与IDAT
一致,只是数据部分前面多出4个字节
表明帧序号。
JS 中的二进制处理
经过上面的介绍,我们了解了Apng
在png
之上新加的几个数据块的结构,在此基础之上,我们可以通过解析Apng
的内容并修改对应位置的数据来调整Apng
的表现,并预览修改后的效果。
我们用javascript
来解析apng
文件,首先就需要了解一下js
中处理二进制数据的几个方法:
ArrayBuffer
代表一个二进制数据,表示通用的、固定长度的原始二进制数据缓冲区,不可编辑。可以理解为这个对象存了一系列的01
数据。
DataView
数据视图,是一个可以从 二进制ArrayBuffer
对象中读写多种数值类型的底层接口,使用它时,不用考虑不同平台的字节序问题。通俗的理解就是ArrayBuffer
的编辑要委托给DataView
来处理。
Uint8Array
表示一个 8 位无符号整型数组,创建时内容被初始化为0
。创建完后,可以以对象的方式或使用数组下标索引的方式引用数组中的元素。
实现编辑
我们这次处理大概就用上以上几个对象来处理,首先我们根据magic number
判断一下这个文件是不是png
,然后就可以开始遍历这个数据对象,代码如下:
1 | const travelsChunk = ( |
这里做的处理主要就是根据块的结构,把整个二进制对象分割成不同的数据块做处理,遍历直到结束。
通过解析每个数据块的第 4 到 8 的字节,我们可以获得数据块的名称,然后根据每个数据块不同的名称,我们把需要支持编辑的数据块做处理,其它的做保留,例如一个数据块的基类被定义为:
1 | class Chunk<T = object> { |
后续我们修改内容就通过统一的暴露的方法就可以了,剩下的就是用户界面的工作以及预览。
实现预览
解析了Apng
文件之后,我们就可以根据fcTL
的配置去渲染每一帧的图片,根据acTL
的配置决定要播放多少次。其中主要的处理就是fcTL
中的dispose_op
以及blend_op
中定义的处理,我们使用canvas
去渲染的话,这一部分还是比较好处理的,我们只需要根据相应的数据保留或者清除对应区域的图像就行,以下为处理的示例:
1 | // 上一帧定义了要把数据区清空 |
然后我们只需要根据acTL
中定义的播放次数进行播放,就了预览。
至此我们就实现了编辑与预览的功能,例如下面这个例子,我们可以把一张只播放一次的apng
图片修改成循环播放:
也可以把每一帧的间隔调小一点然后,让小象跑得更快(注意的一点是大部分浏览器都把最小间隔设定在11ms
,如果小于这个值,间隔会自动提升):
以上的功能大家都可以在这里在线 demo中体验到。
打包应用
实现了这些功能之后,作为一个编辑器工具,它是完全可以离线使用,我们可以把它打包成一个App
。作为一个小工具,不希望有太大的体积,除了electron
之外,我们还有很多其他的选择:tauri,NeutralinoJS ,Chromely ,electrino ,go-astilectron ,wails等等,它们各有千秋。从star
数出发,我选择使用tauri
来打包应用。
tauri
前端直接使用系统的webview
,与系统交互部分使用rust
编写,虽然兼容性肯定是不如electron
,但用来做一个小工具绰绰有余。使用起来也比较方便,只需要安装一下rust
以及配置下环境就可以了,使用系统层面的API
也比较方便,直接看下API 文档,然后直接调用就可以了,最后就可以根据不同的环境打包成不同的安装包。
但tauri
直接使用系统webview
的方式也导致了他的兼容性堪忧,在mac
中就不可避免的要去面对safari
这位重量级大哥。比如我这个小工具中,tauri
表现出来的对文件的支持就比较糟糕,在mac
下input[type=file]
无法唤起文件选择框(issues),在linux
下input
是没有问题了,但文件的drop
事件没有触发(issues)。在mac
还会有在使用 canvas
时不时会崩溃等问题,感觉这条路实现的跨端的应用还是比较坎坷的……😂
总结
png
作为gif
的替代品,虽然现在的热度并不高,但它凭借优秀的表现,相信它会在日后成为主流的选择。通过这篇文章我们也可看到现在web
生态的蓬勃发展,现在你可以相对方便地去操作文件,调用系统API
,开发一款自己的应用,能做的事情越来越多,希望后续也能继续保持这个势头发展下 🤯!