是的,我指的是主流的,遍布全网的普通 gif,谷歌旗下的 Tenor 或 Facebook 旗下的 giphy 这样的网站到处都是这种 gif。Gif 是所有人都喜欢的,用来分享简短动画片断的文件格式。
大多数人眼中的 gif
正如大多数人所知道的那样,gif 是一种动画文件格式。你可能看过 gif 文件的信息,觉得这些文件可真够大的。也许你看了它们后会想:哇,这些图片的清晰度好低啊。但不管怎样,提到 gif 时,你对它的印象应该就是一种短小的动画文件格式。
然而,这种用例和编写 gif 的开发者所期望的用途大相径庭。在这篇文章中,我们将深入了解 gif 文件的结构,并在这一过程中讨论它的一些有趣特性。
请注意,这篇文章要探索的是如何理解 gif 格式这一主题,并考察它的一些更深奥的特性。如果你想深入学习如何解析 gif 文件,我推荐以下这些资源。
- W3规范
- Matthew Flickinger:gif里有什么?
- 我发现 ntfs.com 的这份指南也可以帮助入门
写文章的时候我实际上用这些资源做了一个勉强符合要求的 gif 解析器,名为awful-gif,可以解析一些 gif。我不建议大家使用它。
下面进入正题。
gif 的历史
gif 文件格式是由 Compuserve 在 1987 年创建的。在 1987 年的时候,gif 还是一个相当紧凑的格式!它使用了压缩方法,而且不是一般的压缩方法,而是 LZW 压缩技术。许多旧的文件格式(其中有些是 Compuserve 制作的)使用的则是 RLE(Run Length Encoding),在许多情况下效率没那么高。gif 的一个重要取胜因素就是其良好的压缩率和色域(全 256 色,太棒了!)。(注 1)
两年后,gif 文件格式加入了补充内容(gif89a),增加了许多我们今天众所周知和喜爱的特性。
通过 gif89a规范,我们可以快速总结出 gif89 与 gif87a 支持的所有特性的区别。
Appendix A. Quick Reference Table. Block Name Required Label Ext. Vers. Application Extension Opt. (*) 0xFF (255) yes 89a Comment Extension Opt. (*) 0xFE (254) yes 89a Global Color Table Opt. (1) none no 87a Graphic Control Extension Opt. (*) 0xF9 (249) yes 89a Header Req. (1) none no N/A Image Descriptor Opt. (*) 0x2C (044) no 87a (89a) Local Color Table Opt. (*) none no 87a Logical Screen Descriptor Req. (1) none no 87a (89a) Plain Text Extension Opt. (*) 0x01 (001) yes 89a Trailer Req. (1) 0x3B (059) no 87a Unlabeled Blocks Header Req. (1) none no N/A Logical Screen Descriptor Req. (1) none no 87a (89a) Global Color Table Opt. (1) none no 87a Local Color Table Opt. (*) none no 87a Graphic-Rendering Blocks Plain Text Extension Opt. (*) 0x01 (001) yes 89a Image Descriptor Opt. (*) 0x2C (044) no 87a (89a) Control Blocks Graphic Control Extension Opt. (*) 0xF9 (249) yes 89a Special Purpose Blocks Trailer Req. (1) 0x3B (059) no 87a Comment Extension Opt. (*) 0xFE (254) yes 89a Application Extension Opt. (*) 0xFF (255) yes 89a legend: (1) if present, at most one occurrence (*) zero or more occurrences (+) one or more occurrences
对于没有读过整份规范的人们来说,这里面的大部分内容可谓不知所云,所以让我们讨论一下 gif 是如何组合在一起的,顺便再谈谈它的一些奇怪之处。
在我们开始之前,先从规范中找些乐趣。
Appendix D. Conventions. Animation - The Graphics Interchange Format is not intended as a platform for animation, even though it can be done in a limited way.
附录
D.公约。
动画——这个图形交换格式不是要成为一个动画平台,尽管它在某种程度上可以做到这一点。
gif 的结构
下面我会用一个例子来具体分析。如果你想跟着做,下载它即可。(注 2)
如果你在家里跟着学,只需要一台安装有 hexdump 工具的机器即可。我要用的是 xxd,它预装在大多数 unix 系统(Linux、macOS)上,或者可以通过vim-common包安装。
gif 头
每个 gif 都以一个头开始,其中的 magic 位标志着它是什么类型的 gif,还有一点额外的信息,提供关于图像的基本细节。
xxd Sunflower_as_gif_websafe_89a.gif | head -1 # and some arrows 00000000: -> 4749 4638 3961 <- dc00 0501 f700 0002 0102 gif89a..........
用 xxd 可以轻松把 gif 头信息解码为 ascii(如果它有意义的话)。看看上面的内容写着,gif89a!这是一个经过认证的有效 gif!
每个字母都是一个字节,所以我们在这里要找的 magic 字节是:0x47、;0x49、0x46、0x38、0x39、0x61。
另外,最后三个字节可能是:0x38、0x37、0x61,如果只支持 gif87a 文件格式会是这样。我们主要研究 gif89,老版本的格式就一带而过了。
此外 gif 头里面就没有什么有趣的东西了,因为它只是静态文本,所以我们继续往前走。
先等一下问个问题:谁会接受 gif87a 呢?
在研究 gif 时,我想看看主要的 gif 托管供应商是否会接受和保留 gif87a 规范的格式。它们能正常使用吗,还是说只能报错?
这是我们之前看到的向日葵的 gif87a 版本。这个版本只用在这里。
我们来把图像上传到 4 家头部 gif 托管供应商:
- tenor
- giphy
- imgur
- gfycat
我们开始的时候 gif 头是这样:
xxd Sunflower_as_gif_websafe_gif87a.gif | head -1 00000000: 4749 4638 3761 fa00 2901 f500 00ff cc33 gif87a..)......3
以下是重新下载我刚上传的图片后的结果。
Tenor 重编码为 gif89a:
Downloads xxd tenor.gif | head -1 00000000: 4749 4638 3961 a401 f201 f700 0006 0406 gif89a..........
giphy 重编码为 gif89a:
Downloads xxd giphy.gif | head -1 00000000: 4749 4638 3961 fa00 2901 f525 0000 0000 gif89a..)..%....
其实这有点忽悠人,giphy 只接受动画形式的 gif,所以我们必须点击编辑按钮(显示帧编辑器),然后点击完成才行。gif87a 规范中允许存储多张图片,但它们不能有延迟(因此没有动画,见注 3)。
imgur 保留了原始文件!!!
Downloads xxd aUxm3NN.gif | head -1 00000000: 4749 4638 3761 fa00 2901 f500 00ff cc33 gif87a..)......3
至于 gfycat,它一直卡在最后的“编码“阶段整整 20 分钟。希望我没有在周末让他们的一位可怜的工程师看到什么警报。
以上简短分析表明,由世界上最大的两家科技公司所有的两家最大的托管供应商并不尊重我的旧 gif 文件,而是完全重写了它。事实上,对于 giphy 这家公司来说,它似乎只尊重一种 gif......
总之回到探索文件格式的话题上。
逻辑屏幕描述符
那么你的图像是如何显示成某个分辨率的呢?假设我们在 macOS 的 Preview 中使用“get info“特性,它是怎么知道这张图片是 220x261 的?
信不信由你,这是在文件格式中内置的!(注 4)
字节 0x6-0xA 就是这部分信息,另外还加了点内容。字节 0x6 和 0x8 指的是长度和宽度。
xxd Sunflower_as_gif_websafe_89a.gif | head -1 # and some arrows 00000000: 4749 4638 3961 -> dc00 0501 <- f700 0002 0102 gif89a..........
每个维度有两个字节来指定大小。同样一定要记住,gif 文件格式中的所有字节都被指定为小 endian(注 5)。
首先是宽度,是 0x00dc(从 dc00 重新排序)=> 220(十进制)。
然后是长度,是 0x0105(从 0501 重新排序)=> 261(十进制)。
慢着,这是否意味着我们的 gif 有一个分辨率限制?
这就对了!因为每个位置只有两个字节,所以宽度或长度都不能大于 65535。我们可以尝试在 gimp 中制作一个 1x65536 的新 gif 来验证这
其他文件格式在这方面也差不多。如果你想下载理论上最宽的 png,可以点这里。这个文件很小,但打开它的时候你的图像浏览器可能会崩溃。Firefox 浏览器很难打开它,并报告了一个错误,尽管它是符合规范的。
回到逻辑屏幕描述符上
不过逻辑屏幕描述符还没说完,接下来是一组打包的字段。用规范中的图表解释比较容易。
<Packed Fields> = Global Color Table Flag 1 Bit Color Resolution 3 Bits Sort Flag 1 Bit Size of Global Color Table 3 Bits
这里有关于全局颜色表(Global Color Table)的信息,如果设置了全局颜色表位,它将出现在逻辑屏幕描述符之后。
颜色分辨率(Color Resolution)决定了全局颜色表中每种颜色有多少个字节。
排序标志(Sort Flag)会告诉解码器排在前面的颜色更重要,它会以有用的程度从高到低排序颜色。
而全局颜色表的大小是说,颜色表有多大。
在我们向日葵图片的 0xA 字节中,我们有 0xF7 的结果
xxd Sunflower_as_gif_websafe_89a.gif | head -1 00000000: 4749 4638 3961 dc00 0501 -> f7 <- 00 0002 0102 gif89a..........
或者在二进制中就是:1111 0111
这意味着我们的 gif 基本满载,除了 GCT 没有排序。
┌──────────GCT not sorted ▼ by importance 1111 0111 ▲─── ─── GCT set───────────┘ ▲ ▲ │ │ 3 bytes per │ └─────GCT is 768 bytes color ─────────────┘ (max size) (max resolution)
全局颜色表保存了每个字节部分所使用的颜色。它们是 0-255 的标准 RGB 值,你可以在任何现代 RGB 取色器里使用这些数值。
等一下,那个全局颜色表是可选的吗?
你可能已经注意到 0xA 字节的第一位说 GCT 可以是可选的。这的确很有趣。我们如何在没有指定它需要什么颜色的情况下渲染图像呢?
根据下面的规范:
颜色表——全局颜色表和局部颜色表都是可选的;如果存在全局颜色表,它将用于数据流中没有给出局部颜色表的所有图像;如果存在局部颜色表,它将覆盖全局颜色表。然而,如果两个颜色表都不存在,应用程序可以自由地使用一个任意的颜色表。
如果我们拿走一张图像的全局颜色表,现代渲染器会对我们的图像做什么呢?我敢肯定会有一些惊人的事情发生。
我们的图像指定的颜色表大小为 768 字节。它从 0xA 字节开始......假设我们像这样把 0xA 字节的最有意义的比特清零。
然后删除到第 789 字节(独占)。
xxd Sunflower_as_gif_89a-no-gct.gif | head -1 00000000: 4749 4638 3961 dc00 0501 007f 8121 f904 gif89a.......!..
现在第一行是上面这样结束的,这仍然是一个完全有效的 gif,长成这样子:
简直了!在写这篇文章的时候,它就只显示一个完美的黑色方块。在我试过的每一个渲染器中都是这样的情况。Gimp、Chrome、Firefox、Preview、gifiddle,随便哪个都一样。
总之回到逻辑屏幕描述符上。
继续谈逻辑屏幕描述符
在描述全局颜色表的字节之后,有两个描述屏幕描述符的末端字节。
字节 B 是背景颜色,指的是全局颜色表的索引;字节 C 是像素长宽比,描述了像素的方正度。
xxd Sunflower_as_gif_websafe_89a.gif | head -1 00000000: 4749 4638 3961 dc00 0501 f700 0002 0102 gif89a.......... ^ ^ | | Background color is color in index 0 of | GCT | Pixel aspect ratio is 0:0 or host pixel aspect ratio.
等一下,像素长宽比是什么?
像素并不总是正方形的!字节也不总是 8 位,但这一点就不多说了。
gif 和其他一些最流行的现代图像格式都支持非正方形像素。
我想知道最流行的 gif 渲染器在渲染非方形像素时兼容性如何。我们在 Firefox 和 Chrome 中做一个流行的测试,看看它们看起来如何:
http://frs.badcoffee.info/PAR_AcidTest/
上面依次是:jpg、png 和 gif。而 Firefox、Chrome 和 Preview 都忽略了长宽比。
不幸的是,这一特性普遍不被支持,而且目前在 Firefox 中有一个 16 年的老 bug:
https://bugzilla.mozilla.org/show_bug.cgi?id=333377
甚至 gifiddle 这个我能找到的兼容性最好的 gif 浏览器也不支持非方形像素:
https://github.com/ata4/gifiddle/issues/1
如果你真的想显示非方形像素,可以用调整过的gimp来做。此外,grafx2 显然可以处理非常特定的奇怪像素分辨率。不过我还没有亲自测试过。
回到全局颜色表
全局颜色表(GCT)显然是 gif 最无聊的部分。这里真的没有什么值得谈论的东西。
我的 awful-gif 项目可以输出向日葵的 GCT 中的所有颜色(也许其他图像也行)。
GCT 的解析就在这里,你可以看到它真的没有什么特别的地方。
用下面的命令运行:
cargo run --quiet -- --gif-file ./experiments/Sunflower_as_gif_websafe.gif
可选的图形控制扩展
下面我们讲图形控制扩展(GCE),由扩展引入器 0x21 引入(extension introduced),然后是 0xF9(!)
可用的扩展有许多,但图形控制扩展可以说是最重要的扩展之一,至少在现代用例中是这样。GCE 允许各帧之间存在显示延迟,这样 gif 才能成为“动画“。GCE 还允许其他一些事项。
xxd Sunflower_as_gif_websafe_89a.gif | head -50 | tail -2 00000300: 88ae b091 a5b1 a4b9 be94 887f 81 -> 21 f904 .............!.. 00000310: 0000 0000 <- 0021 fe51 4669 6c65 2073 6f75 .....!.QFile sou
这个 gif 并不是动画,所以这里并没有发生很多事情。正如你所看到的,上面有很多零,但我们还是一个字节一个字节来讲。
第一个字节是块大小,在这个例子中是 0x04,但实际上根据规范它总是 0x04。
等一下,我们能不能把块大小去掉?
如果块大小总是一个静态的常数,那么它就不太重要了是吗?从技术上讲,它是规范的一部分,但实际上并没有什么作用。我们再在流行的图像浏览器中打开它看看。
在这些测试中我将使用一个更简单的 gif,这样更容易看到发生了什么情况:
在下面的测试中我对它做了修改,删除了 GCE。修改后的版本以 xxd 格式保存在下面。
00000000: 4749 4638 3961 2000 3400 f0ff 00ff ffff gif89a .4....... 00000010: 0000 0021 f903 0500 0002 002c 0000 0000 ...!.......,.... 00000020: 2000 3400 0002 788c 8fa9 cb0b 0fa3 94ed .4...x......... 00000030: cc7b abc1 1cea d075 5fc8 8d64 a69d 68a5 .{.....u_..d..h. 00000040: 4e66 eba5 702c 3675 cddc a5bd e34e bfcb Nf..p,6u.....N.. 00000050: 0131 ace1 ea47 0405 9128 9f42 9714 2667 .1...G...(.B..&g 00000060: a70d 3564 bd1a b52e 25b7 f905 8729 de31 ..5d....%....).1 00000070: cd1c c9a2 016a 74db fc1e c7c3 f36f 9d7b .....jt......o.{ 00000080: d7e6 af7b 6a7f f607 13d8 32a8 5258 55e6 ...{j.....2.RXU. 00000090: 9608 b728 d748 f768 1789 f751 b950 0000 ...(.H.h...Q.P.. 000000a0: 3b ;
将其保存到一个名为 invalid.hex 的文本文件中,然后执行:xxd -r invalid.hex > invalid.gif
(更新的字节在:0x16,从 0x4->0x03)
第一个是 macOS Preview:
Preview 是符合标准的!
接下来我们试试 Firefox:
Firefox 知道这是一个静态值,并忽略了它的结果。这并不完全符合标准,但可能是最聪明的做法。
当块大小被移除后,Chrome 会有点抓狂。在这里,Chrome 肯定是最不符合标准的。
回到图形控制扩展
在我们读完块大小之后,是一个包装好的字段,描述如下。
<Packed Fields> = Reserved 3 Bits Disposal Method 3 Bits User Input Flag 1 Bit Transparent Color Flag 1 Bit
在我们的图像中所有这些字段都被设置为 0,所以我只解释它们。
Reserved 是为 gif22a 出现时设置的,我们需要这三个位来做一些好事。
User Input 是为了接受用户输入,通过点击鼠标或按下键盘将 gif 图片推进到下一幅。
透明索引是用来设置我们是否应该允许透明。
等一下,gif 可以接受用户输入???
是的,你没看错。gif 可以接受用户的输入来推进到下一帧。这个可怜的家伙为了用 png 重现这一特性建立了一个网站。真可惜,他像我一样被困在这里了,就因为他没看过 gif 规范。
我们不妨讨论 gif 支持的另一个奇怪特性,即纯文本扩展。
纯文本扩展允许 gif 制作者在他们喜欢的任何地方嵌入单色文本,并直接在图像上进行一些基本的样式设计。
纯文本扩展和用户输入扩展一样,除了像 gifiddle 的这样为了好玩而制作的 gif 查看器外,可能从未被任何 gif 查看器实现。
BOB_89A.gif 可能是有史以来在互联网上发布的第一个 gif,是一个同时使用这两种方式的 gif 例子。
下面是 BOB_89A.gif 在现代浏览器中的渲染。
然而,如果你把它放到 gifiddle 中,会得到一个非常不同的结果,最后的信息是一个非常重要的事实。
不过我不会剧透这个惊喜。你可以下载这个 gif 放到 gifiddle 里,看看会发生什么。
gifiddle 链接:
http://ata4.github.io/gifiddle/
任何现代浏览器或 gif 浏览器都不支持这两项特性。
如果你想阅读更多关于纯文本扩展的信息,可以看这里。
可选的注释扩展
接下来是注释扩展,实际上它可以出现在一个块可能开始的任何地方。然而它最常出现在 gif 的这一部分。
注释部分只允许包含 7 位的 ascii,并且是供人类阅读的。
由于注释部分只是 ascii,你可以直接发射字符串并在输出中找到注释。
strings Sunflower_as_gif_websafe_89a.gif | head -7 | tail -1 QFile source: https://commons.wikimedia.org/wiki/File:Sunflower_as_gif_websafe.gif
在这张图片中,它开始于图片的 0x310 字节。
xxd Sunflower_as_gif_websafe_89a.gif | head -55 | tail -6 00000310: 0000 0000 0021 fe51 4669 6c65 2073 6f75 .....!.QFile sou 00000320: 7263 653a 2068 7474 7073 3a2f 2f63 6f6d rce: https://com 00000330: 6d6f 6e73 2e77 696b 696d 6564 6961 2e6f mons.wikimedia.o 00000340: 7267 2f77 696b 692f 4669 6c65 3a53 756e rg/wiki/File:Sun 00000350: 666c 6f77 6572 5f61 735f 6769 665f 7765 flower_as_gif_we 00000360: 6273 6166 652e 6769 6600 2c00 0000 00dc bsafe.gif.,.....
图像数据的剩余部分
之后就没有什么可谈的了。这张图像跳过了大多数其他的 gif 特性,如本地颜色表和动画,所以这张 gif 剩下的大部分只是数据和终止符。
老实说 lzw 压缩并不难学,但本文并不是要讲这个话题。如果你想学习它,Matthew Flickinger 在他的网站上有一篇好文章。
附加内容:真彩 gif
你知道 gif 可以是真彩色的吗?这和“局部颜色表“有关系。每个数据段都允许有自己的局部颜色表,因此如果你把一个 gif 分成足够多的片断,你就可以得到真彩色了!
大多数 gif 不会这样做,有几个原因。
首先,这样生成的图像是非常大的。每一个新的 256 色调色板将消耗额外的 768 字节。
第二,现在的渲染器不会“正确“渲染这样的图像。浏览器在默认情况下,如果没有指定,通常会在帧之间设置 0.1 的延迟。
然而,一个真正符合规范要求的 gif 渲染器会正确地显示真彩色 gif。因此,如果你有足够的空间、内存和多余的 CPU,为什么不做一个真彩 gif 呢?
如果你想了解更多关于真彩 gif 的信息,维基百科上有一整个章节。
总结
感谢大家有耐心看到这里。gif 规范中还有更多部分我没有讲到,如果你有兴趣了解更多关于 gif 的信息,我建议你查看规范和我在文章顶部添加的那些链接。
注释
- https://en.wikipedia.org/wiki/gif#history︎
- 向日葵图片转自维基百科关于 gifs 的文章(见脚注 1)︎
- gif87a 在技术上是以比较有限的格式支持动画的。要了解更多信息,你可以试试 gifiddle 仓库上的 gif87a 动画例子:https://github.com/ata4/gifiddle︎
- 更多信息请参见 gif 规范的第 18 节(逻辑屏幕描述符)。
- 更多信息,请参见第 4 节。文档来自 gif 规范:https://www.w3.org/Graphics/gif/spec-gif89a.txt︎