GIF 这种动态图片已经流行了数十年,虽然后起之秀 webp 渐渐取代了 GIF,也有很多网站直接用视频作为代替,但 GIF 图片存量很大,很多网站依然在使用 GIF。浏览器、图片查看/编辑软件基本都支持 GIF,在这种广泛的技术支持下,GIF 应该还会存在很长时间。
我对于 GIF 编解码的需求来自于……丰富简历,然后就是希望 GIF 的播放可以暂停、添加滤镜,以及合成 GIF。可以看出,“然后”后面的需求其实都是有替代品(替代方案)的。
npm 包地址:www.npmjs.com/package/ama…
github 地址:github.com/tonyzhou189…
这个库提供了 GIF 编解码的功能。
具体的使用方法和 API 详见库的文档,这篇文字简要说一下实现。
关于 GIF
GIF 是由多帧图片组成的,每帧图片最多 256 色(包含透明像素),这一点与我们常说的 GIF 只有 256 色不太一样。下面我解释一下这个区别到底在哪。
GIF 图片数据是基于颜色表的,颜色表的大小是用一个字节表示的,而一个字节所能表示的最大整数是 256,所以颜色表最多 256 色,图片数据部分存储的是原始颜色在颜色表中的索引,这样一来就将原始 RGB(三个字节,24 位)压缩为了一个字节。图片数据部分变成索引之后还会用变长 LZW 算法进一步压缩。
GIF 的颜色表分为全局颜色表和局部颜色表。如果一个图片没有局部颜色表,那么这个图片所有帧都只能使用全局颜色表,这就是我们常说的 GIF 只有 256 色。那如果使用了局部颜色表呢?那么所有帧的总颜色就可以多于 256 色。
我上面有写 256 色包含透明像素。GIF 本身是不支持 RGBA 的,其透明色的做法是标明一个索引作为透明色,数据部分的用此索引的像素就是透明色。所以我们可以让每一帧都启用局部颜色表,并且只表示整个图片的一部分,其余部分用透明色,每一帧叠加,就形成了一个超过 256 色的图片。
另外,GIF 中的每一帧的尺寸不需要与整个图片相同,其可以只是一部分。这样我们也可以将一张图片切成 n 部分,每一部分作为一帧,叠加起来就是原图。也就是说,GIF 是可以表示真彩色的。
第五次索尔维会议
这个图片是通过以下网站生成:
这个图片本身不是很多颜色的,只是用来示例说明 GIF 可以通过分割图片的方式实现真彩色。但正如这幅图显示的,其每一部分需要一帧一帧的显示,那么如何才能一次性都显示出来呢?
事实上,帧延迟是可以设置为 0 的。
vii) Delay Time - If not 0, this field specifies the number of
hundredths (1/100) of a second to wait before continuing with the
processing of the Data Stream. The clock starts ticking immediately
after the graphic is rendered. This field may be used in
conjunction with the User Input Flag field.
GIF 标准说,在非零的情况下,以百分之一秒为单位设置延迟。并没有说明 0 该如何处理。现在的浏览器、图片查看软件应该都是有一个非 0 默认值。所以就算我设置了延迟为 0,浏览器播放依然会是一帧一帧的。我写的 amazing-gif 则考虑了 0 延迟,上面的图用 amazing-gif 播放的话就是一张静态图。为什么我要这么做?因为我想让 GIF 显示静态真彩色图片,虽然这没意义。
GIF 压缩用到了 LZW 算法,这个算法网上都有介绍,不同的是,GIF 的 LZW 是变长编码。我也写了关于此的文字。
将多张图片合成为 GIF 的过程中,需要对原图片量化处理,因为原图是真彩色,而 GIF 每帧最多只能 256 色,所以需要对真彩色压缩,并且尽可能让压缩后的颜色能够代表原图。这就用到了八叉树算法以及颜色抖动。
有了以上的基础,结合 GIF 标准文档基本就可以写出一个 GIF 编解码器了。
amazing-gif 除了编解码、播放 GIF 之外,还提供了一些滤镜。比如“灰度”、“黑白”、“反向”、“去色”等。
欢迎大家尝试 amazing-gif(以及点个 star)。