为什么有的GIF图片只会播放一遍,而有的会重复播放?关于gif你想知道的一切!

​ 我们现在说的GIF图片全称是Graphics Interchange Format,是一种256色的图片压缩(LZW协议)文档,主要用来存储动态图片。GIF图片有两种格式分布是1987年的GIF87a和1989年GIF89a版本。
​ 其中GIF89a扩展了图形控制区块、备注、说明、应用程序接口等四个区块,并提供了对透明色和多帧动画的支持。这也是我们现在最广为使用的协议。
​ GIF89a 设定部分属性如下图所示

​ 1995年Netscape 为了让GIF可以更好的支持动画和视频剪辑,GIF89a又增加了图像控制扩展功能(Graphics Control Extension (GCE)),每个frame的图片都可以添加GCE部分,用来描述frame之间的延迟时间和循环次数。
image
上图中 31D行,定义的就是重复播放次数。
​ 所以,如果你的GIF动画不循环播放,一种可能是GCE设置成了1就是只播放一次;第二种可能是你的GIF动画中没有GCE部分。要是GIF动画没有GCE,那就要看播放GIF动画的工具是怎么处理默认逻辑了,有可能只播放一次,也有可能循环播放。
​ 网上制作循环GIF图一般使用PS软件,我这里补充一段将不循环的GIF图转为循环的Python代码,对于程序员们更友好和便捷。
​ 这个问题是有实际场景的:知乎目前的文章只允许插入1MB大小以内的GIF图,而大多数表情包之外的动图都超出了这个大小,这时就需要对GIF的尺寸进行裁剪,或者进行抽帧。在Mac自带的预览功能里可以直接完成这两个操作,但代价就是原本循环播放的GIF变得只动一次了。使用以下代码:

from PIL import ImageSequence, Image
import imageio, os 

location = "/path/to/"  # 文件目录
gif_file = os.path.join(location, "origin.gif")  # 输入GIF文件名
out_file = os.path.join(location, "output.gif")  # 输出GIF文件名

# 这里如果想使用灰度图,可以使用`.convert("L")`代替,参考https://pillow-cn.readthedocs.io/zh_CN/latest/handbook/concepts.html#mode
frames = [frame.copy().convert("RGBA") for frame in ImageSequence.Iterator(Image.open(gif_file))]
imageio.mimsave(out_file, frames, 'GIF', duration=0.5)  # duration表示两帧之间播放的间隔时间,也可不设,使用默认

​ 再添加一些逻辑,可以将不循环的GIF图批量转为循环的。在GIF的保存时,不要使用网上流行的这种(可能会影响循环):

frames[0].save("out.gif", save_all=True, append_images=frames[1:])

​ 另外,也可以针对知乎的这个1MB场景,配合以上代码,写一个自动的GIF压缩程序~

导语

GIF(Graphics Interchange Format)原义是“图像互换格式”,是CompuServe公司在1987年开发出的图像文件格式,可以说是互联网界的老古董了。

GIF格式可以存储多幅彩色图像,如果将这些图像连续播放出来,就能够组成最简单的动画。所以常被用来存储“动态图片”,通常时间短,体积小,内容简单,成像相对清晰,适于在早起的慢速互联网上传播。

本来,随着网络带宽的拓展和视频技术的进步,这种图像已经渐渐失去了市场。可是,近年来流行的表情包文化,让老古董GIF图有了新的用武之地。

图片图片

表情包通常来源于手绘图像,或是视频截取,目前有很多方便制作表情包的小工具。

这类图片通常具有文件体积小,内容简单,兼容性好(无需解码工具即可在各类平台上查看),对画质要求不高的特点,刚好符合GIF图的特性。

所以,老古董GIF图有了新的应用场景。

本文的应用场景

新的应用场景带来新的需求,在本文所面临的场景中,需要批量为用户推送GIF表情包,希望在运营人员上传图包的时候,服务器可以自动完成缩略图的批量生成工作。

一批图像大约有200-500张,以缩略图列表的形式展示在客户端。

根据我们使用测试数据进行的统计GIF图表情包的尺寸大部分在200k-500k之间,批量推送的一个重要问题就是数据量太大,因此,我们希望能够在列表里展示体积较小的缩略图,用户点击后,再单独拉取原图。

传统的GIF缩略图是静态的,通常是提取第一帧,但在表情包的情形下,这种方式不足以表达出图片中信息。比如下面的例子

图片

图片

——第一帧完全看不出重点啊!

所以,我们希望缩略图也是动态的,并尽可能和原图相似。

对于传统图片来说,文件大小一般和图片分辨率(尺寸)正相关,所以,生成缩略图最直观的思路就是缩小尺寸,resize大法

但是在GIF图的场合,这个方式不再高效,因为GIF图的文件大小还受到一个重要的因素制约——帧数

以这张柴犬表情为例,原图宽度200,尺寸1.44M,等比缩放到150之后,尺寸还是1.37M,等比缩放到100,相当于尺寸变为原来的四分之一,体积还是749K

图片

图片

图片

可见,resize大法的压缩率并不理想,收效甚微。

而且,我们所得到的大部分表情图素材,分辨率已经很小了,为了保证客户端展示效果,不能够过度减少尺寸,不然图片会变得模糊。

所以,想要对GIF图进行压缩,只能从别的方向入手。

探寻GIF格式的存储

想要压缩一个文件,首先要了解它是如何存储的。毕竟,编程的事——万变不离其宗嘛。

图片

作为一种古老的格式,GIF的存储规则也相对简单,容易理解,一个GIF文件主要由以下几部分组成。

  • 文件头
  • 图像帧信息
  • 注释

下面我们来分别探究每个部分。

文件头

GIF格式文件头和一般文件头差别不大,也包含有

  • 格式声明
  • 逻辑屏幕描述块
  • 全局调色盘

格式声明

图片

Signature 为“GIF”3 个字符;Version 为“87a”或“89a”3 个字符。

逻辑屏幕描述块

图片

前两字节为像素单位的宽、高,用以标识图片的视觉尺寸。

Packet里是调色盘信息,分别来看——

Global Color Table Flag 为全局颜色表标志,即为1时表明全局颜色表有定义。

Color Resolution 代表颜色表中每种基色位长(需要+1),为111时,每个颜色用8bit表示,即我们熟悉的RGB表示法,一个颜色三字节。

Sort Flag 表示是否对颜色表里的颜色进行优先度排序,把常用的排在前面,这个主要是为了适应一些颜色解析度低的早期渲染器,现在已经很少使用了。

Global Color Table 表示颜色表的长度,计算规则是值+1作为2的幂,得到的数字就是颜色表的项数,取最大值111时,项数=256,也就是说GIF格式最多支持256色的位图,再乘以Color Resolution算出的字节数,就是调色盘的总长度。

这四个字段一起定义了调色盘的信息。

Background color Index 定义了图像透明区域的背景色在调色盘里的索引。

Pixel Aspect Ratio 定义了像素宽高比,一般为0。

什么是调色盘?我们先考虑最直观的图像存储方式,一张分辨率M×N的图像,本质是一张点阵,如果采用Web最常见的RGB三色方式存储,每个颜色用8bit表示,那么一个点就可以由三个字节(3BYTE = 24bit)表达,比如0xFFFFFF可以表示一个白色像素点,0x000000表示一个黑色像素点。

如果我们采用最原始的存储方式,把每个点的颜色值写进文件,那么我们的图像信息就要占据就是3×M×N字节,这是静态图的情况,如果一张GIF图里有K帧,点阵信息就是3×M×N×K。

下面这张兔子snowball的表情有18帧,分辨率是200×196,如果用上述方式计算,文件尺寸至少要689K。

图片

但实际文件尺寸只有192K,它一定经历过什么……

我们可以使用命令行图片处理工具gifsicle来看看它的信息。

gifsicle -I snowball.gif > snowball.txt

我们得到下面的文本

    5.gif 19 images
      logical screen 200x196
      global color table [128]
      background 93
      loop forever
      extensions 1
      + image #0 200x196 transparent 93
    disposal asis delay 0.04s
      + image #1 200x188 transparent 93
    disposal asis delay 0.04s
    ........

可以看到,global color table [128]就是它的调色盘,长度128。

为了确认,我们再用二进制查看器查看一下它的文件头

图片

可以看到Packet里的字段的确符合我们的描述。

在实际情况中,GIF图具有下面的特征

(1)一张图像最多只会包含256个RGB值。

(2)在一张连续动态GIF里,每一帧之间信息差异不大,颜色是被大量重复使用的。

在存储时,我们用一个公共的索引表,把图片中用到的颜色提取出来,组成一个调色盘,这样,在存储真正的图片点阵时,只需要存储每个点在调色盘里的索引值。

如果调色盘放在文件头,作为所有帧公用的信息,就是公共(全局)调色盘,如果放在每一帧的帧信息中,就是局部调色盘。GIF格式允许两种调色盘同时存在,在没有局部调色盘的情况下,使用公共调色盘来渲染。

图片

这样,我们可以用调色盘里的索引来代表实际的颜色值。

一个256色的调色盘,24bit的颜色只需要用9bit就可以表达了。

调色盘还可以进一步减少,128色,64色,etc,相应的压缩率就会越来越大……

还是以兔子为例,我们还可以尝试指定它的调色盘大小,对它进行重压缩

    gifsicle --colors=64 5.gif > 5-64.gif
    gifsicle --colors=32 5.gif > 5-32.gif
    gifsicle --colors=16 5.gif > 5-16.gif
    gifsicle --colors=2 5.gif > 5-2.gif
    ......

依然使用gifsicle工具,colors参数就是调色盘的长度,得到的结果

图片

图片

图片

图片

注意到了2的时候,图像已经变成了黑白二值图。

居然还能看出是个兔子……

所以我们得出结论——如果可以接受牺牲图像的部分视觉效果,就可以通过减色来对图像做进一步压缩。

文件头所包含的对我们有用的信息就是这些了,我们继续往后看。

帧信息描述

帧信息描述就是每一帧的图像信息和相关标志位,在逐项了解它之前,我们首先探究一下帧的存储方式。

我们已经知道调色盘相关的定义,除了全局调色盘,每一帧可以拥有自己的局部调色盘,渲染顺序更优先,它的定义方式和全局调色盘一致,只是作用范围不同

直观地说,帧信息应该由一系列的点阵数据组成,点阵中存储着一系列的颜色值。点阵数据本身的存储也是可以进行压缩的,GIF图所采用的是LZW压缩算法。
这样的压缩和图像本身性质无关,是字节层面的,文本信息也可以采用(比如常见的gzip,就是LZW和哈夫曼树的一个实现)

基于表查询的无损压缩是如何进行的?基本思路是,对于原始数据,将每个第一次出现的串放在一个串表中,用索引来表示串,后续遇到同样的串,简化为索引来存储(串表压缩法)

举一个简单的例子来说明LZW算法的核心思路。

有原始数据:ABCCAABCDDAACCDB

可以看出,原始数据里只包括4个字符A,B,C,D,四个字符可以用2bit的索引来表示,0-A,1-B,2-C,3-D。

原始字符串存在重复字符,比如AB,CC,都重复出现过。用4代表AB,5代表CC,上面的字符串可以替代表示为45A4CDDAA5DB

这样就完成了压缩,串长度从16缩减到12。对原始信息来说,LZW压缩是无损的。

除了采用LZW之外,帧信息存储过程中还采取了一些和图像相关的优化手段,以减小文件的体积,直观表述就是——公共区域排除、透明区域叠加

这是ImageMagick官方范例里的一张GIF图。

图片

根据直观感受,这张图片的每一帧应该是这样的。

图片

但实际上,进行过压缩优化的图片,每一帧是这样的。

图片

首先,对于各帧之间没有变化的区域进行了排除,避免存储重复的信息。
其次,对于需要存储的区域做了透明化处理,只存储有变化的像素,没变化的像素只存储一个透明值。

这样的优化在表情包中也是很常见的,举个栗子

图片

上面这个表情的文件大小是278KB,帧数是14

我们试着用工具将它逐帧拆开,这里使用另一个命令行图像处理工具ImageMagick

gm convert source.gif target_%d.gif

图片

可以看出,除了第一帧之外,后面的帧都做了不同程度的处理,文件体积也比第一帧小。

这样的压缩处理也是无损的,带来的压缩比和原始图像的具体情况有关,重复区域越多,压缩效果越好,但相应地,也需要存储一些额外的信息,来告诉引擎如何渲染,具体包括

  • 帧数据长宽分辨率,相对整图的偏移位置
  • 透明彩色索引——填充透明点所用的颜色
  • Disposal Method——定义该帧对于上一帧的叠加方式
  • Delay Time——定义该帧播放时的停留时间

其中值得额外说明的是Disposal Method,它定义的是帧之间的叠加关系,给定一个帧序列,我们用怎样的方式把它们渲染成起来。

详细参数定义,可以参考该网站的范例
http://www.theimage.com/animation/pages/disposal.html

Disposal Method和透明颜色一起,定义了帧之间的叠加关系。在实际使用中,我们通常把第一帧当做基帧(background),其余帧向前一帧对齐的方式来渲染,这里不再赘述。

理解了上面的内容,我们再来看帧信息的具体定义,主要包括

  • 帧分隔符
  • 帧数据说明
  • 点阵数据(它存储的不是颜色值,而是颜色索引)
  • 帧数据扩展(只有89a标准支持)

1和3比较直观,第二部分和第四部分则是一系列的标志位,定义了对于“帧”需要说明的内容。

帧数据说明

图片

除了上面说过的字段之外,还多了一个Interlace Flag,表示帧点阵的存储方式,有两种,顺序隔行交错,为 1 时表示图像数据是以隔行方式存放的。最初 GIF 标准设置此标志的目的是考虑到通信设备间传输速度不理想情况下,用这种方式存放和显示图像,就可以在图像显示完成之前看到这幅图像的概貌,慢慢的变清晰,而不觉得显示时间过长。

帧数据扩展是89a标准增加的,主要包括四个部分。

1、程序扩展结构(Application Extension)主要定义了生成该gif的程序相关信息

图片

2、注释扩展结构(Comment Extension)一般用来储存图片作者的签名信息

图片

3、图形控制扩展结构(Graphic Control Extension)这部分对图片的渲染比较重要

图片

除了前面说过的Dispose Method、Delay、Background Color之外,User Input用来定义是否接受用户输入后再播放下一帧,需要图像解码器对应api的配合,可以用来实现一些特殊的交互效果。

4、平滑文本扩展结构(Plain Text Control Extension)

图片

89a标准允许我们将图片上的文字信息额外储存在扩展区域里,但实际渲染时依赖解码器的字体环境,所以实际情况中很少使用。

以上扩展块都是可选的,只有Label置位的情况下,解码器才会去渲染

需求场景——给表情包减负

说完了基本原理,来分析一下我们的实际问题。

给大量表情包生成缩略图,在不损耗原画质的前提下,尽可能减少图片体积,节省用户流量。

之前说过,单纯依靠resize大法不能满足我们的要求,没办法,只能损耗画质了,主要有两个思路,减少颜色减少帧数

减少颜色——图片情况各异,标准难以控制,而且会造成缩略图和原图视觉差异比较明显

减少帧数——通过提取一些间隔帧,比如对于一张10帧的动画,提取其中的提取1,3,5,7,9帧。来减少图片的整体体积,似乎更可行。

先看一个成果,就拿文章开头的图做栗子吧

图片图片

看上去连贯性不如以前,但是差别不大,作为缩略图的视觉效果可以接受,由于帧数减小,体积也可以得到明显的优化。体积从428K缩到了140K

但是,在开发初期,我们尝试暴力间隔提取帧,把帧重新连接压成新的GIF图,这时,会得到这样的图片。

图片

主要有两个问题。

1、帧数过快

2、能看到明显的残留噪点

分析我们上面的原理,不难找到原因,正是因为大部分GIF存储时采用了公共区域排除和透明区域叠加的优化,如果我们直接间隔抽帧,再拼起来,就破坏了原来的叠加规则,不该露出来的帧露出来了,所以才会产生噪点。

所以,我们首先要把原始信息恢复出来。

两个命令行工具,gifsicle和ImageMagick都提供这样的命令。

gm convert -coalesce source.gif target_%d.gif
gifsicle --unoptimize source.gif > target.gif

图片

还原之后抽帧,重建新的GIF,就可以解决问题2了。

注意重建的时候,可以应用工具再进行对透明度和公共区域的优化压缩。

至于问题1,也是因为我们没有对帧延迟参数Delay Time做处理,直接取原帧的参数,帧数减少了,速度一定会加快。

所以,我们需要把抽去的连续帧的总延时加起来,作为新的延迟数据,这样可以保持缩略图和原图频率一致,看起来不会太过鬼畜,也不会太过迟缓。

提取出每一帧的delay信息,也可以通过工具提供的命令来提取。

gm identify -verbose source.gif
gifsicle -I source.gif

在实际应用中,抽帧的间隔gap是根据总帧数frame求出的

frame<8 gap=1
9<frame<20 gap=2
21<frame<30 gap=3
31<frame<40 gap=4
frame>40 gap=5

delay值的计算还做了归一化处理,如果新生成缩略图的帧间隔平均值大于200ms,则统一加速到均值200ms,同时保持原有节奏,这样可以避免极端情况下,缩略图过于迟缓。

具体实现

本文介绍的算法主要应用于手Q热图功能的后台管理系统,使用Nodejs编写。
ImageMagick是一个较为常用的图像处理工具,除了gif还可以处理各类图像文件,有node封装的版本可以使用。
gifsicle只有可执行版本,在服务器上重新编译源码后,采用spawn调起子进程的方式实现。

ImageMagick对于图片信息的解析较为方便,可以直接得到结构化信息。
gifsicle支持命令管道级联,处理图片速度较快。
实际生产过程中,同时采用了两个工具。

const {spawn} = require('child_process');const image = gm("src2/"+file)
  image.identify((err, val) => {    if(!val.Scene){
          console.log(file+" has err:"+err)          return
    }    let frames_count = val.Scene[0].replace(/\d* of /, '') * 1
    let gap = countGap(frames_count)    let delayList = [];    let totaldelay = 0
    if(val.Delay!=undefined){          let i          for (i = 0; i < val.Delay.length; i ++) {
            delayList[i] = val.Delay[i].replace(/x\d*/, '') * 1
            totaldelay+=delayList[i]
          }          for (; i < val.Scene.length; i ++) {
            delayList[i] = 8
            totaldelay+=delayList[i]
          }
    }else{          for (let i = 0; i < val.Scene.length; i ++) {
            delayList[i] = 8
            totaldelay+=delayList[i]
          }
    }    let totalFrame = parseInt(frames_count/gap)    //判断是否速度过慢,需要进行归一加速处理
    if(totaldelay/totalFrame>20){          let scale =(totalFrame*1.0*20)/totaldelay          for (let i = 0; i < delayList.length; i ++) {
            delayList[i] = parseInt(delayList[i] * scale)
          }
    }    let params=[]    params.push("--colors=255")    params.push("--unoptimize")    params.push("src2/"+file)    let tempdelay = delayList[0]    for (let i = 1; i < frames_count; i ++) {          if(i%gap==0){            params.push("-d"+tempdelay)            params.push("#"+(i-gap))
            tempdelay=0
          }
      tempdelay += delayList[i]
    }    params.push("--optimize=3")    params.push("-o")    params.push("src2/"+file+"gap-keepdelay.gif")
    spawn("gifsicle", params, { stdio: 'inherit' })
})

测试时,采用该算法随机选择50张gif图进行压缩,原尺寸15.5M被压缩到6.0M,压缩比38%,不过由于该算法的压缩比率和具体图片质量、帧数、图像特征有关,测试数据仅供参考。

本文到这里就结束了,原来看似简单的表情包,也有不少文章可做。

谢谢观看,希望文中介绍的知识和研究方法对你有所启发。

You Don't Know Gif - 分析 gif 文件和一些奇怪的 gif 功能

发表 - 2022-01-13 | 18分钟

他是波平

是的,我指的是您可以在大型网站(如 Google 拥有的Tenor或 Facebook 拥有的 giphy )上找到的主流常见 gif 。每个人都喜欢的用于共享动画短片的文件格式。

大多数人都知道的gif

大多数人都知道 gif,gif 是一种动画文件格式。您可能看过 gif 文件并认为这些文件非常大。也许你看着它们会想:哇,这些图片清晰度很低。但归根结底,当您想到 gif 时,您可能会将其视为短动画文件格式。

然而,这个用例与编写 gif 的人们期望它的用途截然不同。在这篇文章中,我们将深入剖析 gif 文件,并在此过程中讨论它的一些更时髦的特性。

请注意,这篇文章应该是对如何理解 gif 格式及其一些更深奥的功能的有趣探索。如果您想真正学习如何解析 gif,我会推荐以下资源:

在这段时间里,我实际上使用这些资源制作了一个几乎不兼容的 gif 解析器,这些资源称为awesome-gif,它将解析一些 gif。我不建议使用它。

反正上贴。

gif的历史

gif 文件格式由 Compuserve 于 1987 年创建。早在 1987 年,gif 是一种相当紧凑的格式!它使用压缩,而不仅仅是任何压缩,而是 LZW 压缩。许多较旧的文件格式(一些由 Compuserve 制作)使用 RLE(运行长度编码),在许多情况下效率不高。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. 

无论如何,让我们开始吧;)

gif的解剖结构

我将通过一个示例来完成此操作,因此,如果您想继续学习,请随意!右键单击并下载,一切顺利。

网络安全向日葵2

如果您在家里跟随,您所需要的只是一台安装了 hexdump 工具的机器。我将使用预装在大多数 unix(Linux、macOS)上的 xxd,或者可以与包 vim-common一起安装。

gif 标题

每个 gif 都以一个标题开头,其中的魔法位表示它是什么类型的 gif,以及一些额外的信息,提供有关图像的基本细节。

🜛 xxd Sunflower_as_gif_websafe_89a.gif | head -1 # and some arrows 00000000: -> 4749 4638 3961 <- dc00 0501 f700 0002 0102  GIF89a.......... 

xxd 使前几个字节的工作对我们来说很容易,如果有意义的话,它会尝试将字节解码为 ascii。看看那个,GIF89a!这是经过认证的有效 gif!

每个字母代表一个字节,所以我们在这里寻找的神奇字节是:0x47, 0x49, 0x46, 0x38, 0x39, 0x61.

可选的最后三个字节可能是:0x38, 0x37, 0x61如果只支持 gif87a 文件格式。我们不会像 gif89 那样进入旧版本的格式。

标头中没有什么其他有趣的东西,因为它只是静态字节,所以让我们继续前进。

绕道:嘿,谁接受 gif87a?

在研究 gif 时,我想看看是否有任何一个主要的 gif 托管服务提供商会接受并保留 gif87a。他们会工作,还是会出错?

这是我们之前看到的向日葵的 gif87a 版本。此图像的 87a 版本将仅用于本节。

旧标准

让我们将图像上传到 4 个主要的 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。周二我得和 giphy 团队谈谈……

无论如何回到探索文件格式。

逻辑屏幕描述符

嘿,您的图像如何以特定分辨率显示?假设我们在 macOS 的预览中使用“获取信息”功能,它怎么知道这张图片是 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 文件格式的所有字节都指定为 little endian 5

首先是宽度,即 0x00dc(从 dc00 重新排序)=> 220 十进制

然后是长度为 0x0105(从 0501 重新排序)=> 261 十进制

Detour:这是否意味着我们对 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 

如果设置了全局颜色表位,这将具有关于全局颜色表的信息,该信息将出现在逻辑屏幕描述符之后。

颜色分辨率决定全局颜色表中每种颜色的字节数。

排序标志应该是一个标志,它通过以最有用到最不有用的方式对颜色进行排序,告诉解码器较早的颜色更重要。

全局颜色表的大小,嗯,颜色表有多大。

在我们的向日葵字节 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 颜色选择器。

Detour:嘿等一下,全局颜色表是可选的吗?

如果您有敏锐的眼光,您可能已经注意到字节 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。看起来怎么样?

这里没有gct!

惊人的!太棒了!精彩的!截至撰写本文时,它只是一个完美的黑色方块。我尝试过的每一个渲染器都是这种情况。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. 

Detour:嘿等一下,像素长宽比,那是什么?

像素并不总是方形的!字节也不总是 8 位,但这是我不会讨论的切线。

Gif,实际上还有一些其他最流行的现代图像格式支持非方形像素。

嘿!我想知道我们最流行的 gif 渲染器在渲染非方形像素时的兼容性如何。可能非常合规。让我们在 Firefox 和 Chrome 中尝试一个流行的测试,看看它们的外观: http: //frs.badcoffee.info/PAR_AcidTest/

最好的渲染器

呃…… 嗯,那是东西。这些是按顺序排列的:jpg、png 和 gif。Firefox、Chrome 和 Preview 都忽略了纵横比。

不幸的是,这广泛不受支持,目前 Firefox 中有一个 16 年前的错误:https 😕/bugzilla.mozilla.org/show_bug.cgi?id=333377

即使是Gifiddle,我发现的最兼容的 gif 查看器也不支持非方形像素: https 😕/github.com/ata4/gifiddle/issues/1

当我们尝试更多模糊的 gif 功能时,我们将更多地讨论 gifiddle。

如果你真的想显示非方形像素,你可以通过按摩 gimp 来做到这一点。grafx2 显然也可以处理非常具体的奇数分辨率的像素。虽然我自己没有测试过。

在全局颜色表上

全局颜色表 (GCT) 很容易成为 gif 中最无聊的部分。您以 3 的倍数从 0 跳转到全局颜色表的大小。这里真的没有什么值得讨论的。

说明这一点的最好方法是指向我的 可怕 gif项目,它将输出向日葵 GCT 中的所有颜色(可能还有其他图像)。

GCT 解析就 在这里 ,您可以看到它并没有什么特别之处。

运行:

cargo run --quiet -- --gif-file ./experiments/Sunflower_as_gif_websafe.gif 

可选的图形控制扩展

现在我们有了图形控制扩展(GCE),由扩展引入者引入:0x21(引入扩展),然后是 0xF9(!)

有许多扩展,但至少在现代使用中,图形控制扩展可以说是最重要的扩展之一。GCE 需要通过允许帧之间的延迟时间等来使 gifs “动画化”。

🜛 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。

Detour:等一下,我们可以摆脱块大小吗?

啊,如果块大小总是一个静态常量,那么它并不重要,是吗?从技术上讲,它是规范的一部分,但实际上并没有做任何事情。让我们玩我们最喜欢的在最受欢迎的图像查看器中打开它的游戏。

对于这些测试,我将使用一个更简单的 gif 来更容易地查看发生了什么:

简单的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 预览:

预览测试

预览符合标准!我们喜欢看!即使对于可能永远不会得到更新的文件格式在技术上并不重要。


接下来让我们试试火狐:

火狐测试

Firefox 知道它是一个静态值并忽略它的结果。不完全符合标准,但可能是最明智的做法。


铬测试

删除块大小时,Chrome 会变得有点麻烦。不完全颠倒它,它确实……我不确定。Chrome 肯定是最不符合这里标准的。

回到图形控制扩展

在我们读取块大小之后,我们得到一个打包字段,描述为:

      <Packed Fields>  =     Reserved                      3 Bits                             Disposal Method               3 Bits                             User Input Flag               1 Bit                             Transparent Color Flag        1 Bit 

在我们的图像中,所有这些字段都设置为 0,所以我将对其进行解释。

保留是为 gif22a 出来的时候设置的,我们需要这三个位来做一些好事。

用户输入用于获取用户输入以通过鼠标单击或键盘按下将 gif 推进到下一个图像。

透明索引用于设置我们是否应该允许透明。

绕道:等待第二个 GIFS 可以接受用户输入???

是的,你没看错。Gifs可以接受用户输入以前进到下一帧。这个可怜的家伙围绕使用 png 重新创建此功能构建了一个完整的站点。可惜他像我一样被困在里面 2 年后没有阅读 gif 规范。

虽然我们在 gif 的怪异功能部分,但我们不妨讨论一下 gif 支持的其他怪异功能,即纯文本扩展。

纯文本扩展允许 gif 制作者在任何他们喜欢的地方嵌入等宽文本,并直接在图像上使用一些基本样式。

像用户输入扩展这样的纯文本扩展,可能从来没有被任何 gif 查看器实现过,除了那些古怪的人为了好玩而制作的那些,比如制作gifiddle的人。

BOB_89A.gif,很可能是互联网上发布的第一个 gif,它是一个使用两者的 gif 示例。

这里 BOB_89A.gif 在现代浏览器中呈现(您自己的浏览器,如果不是现代浏览器,请更新它):

BOB_89A

然而,如果你把它放入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.,..... 

再次由 BANG (!)(引入扩展)和 0xfe 评论扩展表示。之后,注释应该被读取 255 个字节或直到读取 0x00。

其余图像数据

之后就没什么好说的了。该图像跳过了大多数其他 gif 功能,例如本地颜色表和动画,因此 gif 的其余大部分只是数据和终止符。

老实说,lzw 压缩并不难学,但解释它不是这篇博文的目的。如果您想了解它,Matthew Flickinger在他的网站上有一篇很棒的帖子

附加部分:真彩色 gif

你知道 GIF 可以是真彩色吗?它需要一点点疯狂,但如果你记得世界“本地颜色表”,那么它可能是有道理的。每个数据段都可以有自己的本地颜色表,因此如果你将 gif 分解成足够多的部分,你就会得到真实的颜色!

真彩色gif

大多数 gif 不这样做有几个原因。

首先,生成的图像将是巨大的。每个 256 色的新调色板将额外消耗 768 个字节。

其次,现在的渲染器不会“正确”渲染图像。默认情况下,如果未指定,浏览器通常会在帧之间设置 0.1 的延迟。

然而,真正兼容的 gif 渲染器将正确显示真彩色 gif。因此,如果您有空间、内存和备用 CPU,为什么不拥有一个真彩色 gif 呢?我们都可以有一点本色作为一种享受。

如果您想了解有关真彩色 gif 的更多信息,维基百科有一个完整的部分,称为真彩色

原文链接:https://blog.darrien.dev/posts/you-dont-know-gif/

Darrien's technical blog

Documenting the technical stuff I do in my spare time

Home All Posts About

You Don't Know Gif - An analysis of a gif file and some weird gif features

Published - 2022-01-13 | 18min

He’s boppin

Yes I am referring to the mainstay common gif you’ll find all over the web at large sites like Google’s owned Tenor or Facebook’s owned giphy. Everyone’s favorite file format for sharing short animated snippets.

The gif as most people know it

As most people know gif, gif is an animated file format. You might have looked at gif files and thought wow these files are pretty large. Perhaps you looked at them and thought: wow, these pictures are low definition. But at the end of the day, when you think of gif, you probably think of it as the short animated file format.

However this use case is drastically different from what the folks who wrote gif to expected it to be used for. In this post we’ll dive into the anatomy of a gif file and discuss some of its funkier features along the way.

Note that this post is supposed to be a fun exploration of how to understand the gif format and some of its more esoteric features. If you want to actually learn how to parse gifs, I would recommend these resources:

During this time I actually made a barely compliant gif parser using these resources called awful-gif which will parse some gifs. I don’t recommend using it.

Anyway onto the post.

The history of gif

The gif file format was created in 1987 by Compuserve. Back in 1987 gif was a rather compact format! It used compression, and not just any compression, but LZW compression. Many older file formats (some made by Compuserve) used RLE (Run Length Encoding) which in many cases isn’t nearly as efficient. One of the big win factors for gif was its solid compression ratio and good color gamut (a full 256 colors, wow!).1

Two years later an addendum to the gif file format was created (gif89a) which added many of the features we know and love today.

Via the gif89a spec we can get a quick summary of all of the features supported in gif89 vs 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 

Much of this is going to be gibberish to folks who haven’t read the whole spec before, so let’s discuss how a gif is put together a bit, and we’ll talk about some of its oddities along the way.

Some fun before we begin from the spec:

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. 

Anyway let’s begin 😉

The anatomy of a gif

I’m going to walk through this with an example, so if you’d like to follow along, feel free! Right click and download and you’re good to go.

Websafe sunflower2

If you’re following along at home, all you need is a machine with a hexdump tool installed. I’ll be using xxd which is preinstalled on most unixes (Linux, macOS), or can be installed with the package vim-common.

The gif header

Every gif starts with a header where the magic bits signifying what what type of gif it is and a little extra information giving basic details about the image.

🜛 xxd Sunflower_as_gif_websafe_89a.gif | head -1 # and some arrows 00000000: -> 4749 4638 3961 <- dc00 0501 f700 0002 0102  GIF89a.......... 

xxd makes the work easy for us for the first few bytes and tries to decode bytes to ascii if it makes sense. Take a look at that, GIF89a! It’s a certified valid gif!

Each letter stands out as one byte, so the magic bytes we’re looking for here are: 0x47, 0x49, 0x46, 0x38, 0x39, 0x61.

Optionally last three bytes may be: 0x38, 0x37, 0x61 if only supporting the gif87a file format. We won’t go into the older version of the format as much as gif89.

There isn’t much else interesting in the header since it’s just static bytes, so let’s keep moving on.

Detour: Hey who accepts gif87a?

While looking into gifs I wanted to see if either of the major gif hosting providers would accept and retain gif87a. Would they work, or just err out?

Here’s a gif87a version of the sunflower we were looking at earlier. The 87a version of this image will only be used for this section.

The old standard

Let’s upload the image to 4 of the major gif hosting providers:

This is what we start with:

🜛 xxd Sunflower_as_gif_websafe_gif87a.gif | head -1 00000000: 4749 4638 3761 fa00 2901 f500 00ff cc33  GIF87a..)......3 

Here are the results after redownloading the image I just uploaded.

Tenor re-encodes to gif89a:

🜛 Downloads xxd tenor.gif | head -1 00000000: 4749 4638 3961 a401 f201 f700 0006 0406  GIF89a.......... 

giphy re-encodes to a gif89a:

🜛 Downloads xxd giphy.gif | head -1 00000000: 4749 4638 3961 fa00 2901 f525 0000 0000  GIF89a..)..%.... 

Actually that’s a little disingenuous, giphy ONLY accepts animated gifs, so we have to click edit (which shows the frame editor) and click done. Multiple images are allowed to be stored in the gif87a spec, but they cannot have a delay (hence no animation3). I guess I’m not sure what I expected here.


imgur preserves the original file!!!

🜛 Downloads xxd aUxm3NN.gif | head -1 00000000: 4749 4638 3761 fa00 2901 f500 00ff cc33  GIF87a..)......3 

As for gfycat, well it’s been stuck in the last phase of “encoding” the for last 20 minutes. Hopefully I didn’t make an alert for a poor engineer on the weekend.


This short analysis shows the two biggest hosting providers made or owned by two of the largest tech companies in the world don’t respect my old gif and completely rewrite it. In fact for a company called giphy it only seems to respect one kind of gif. I’ll have to have a talk with the giphy team on Tuesday…

Anyway back to exploring the file format.

The logical screen descriptor

Hey how does your image show up in a certain resolution? Say we use the “get info” function in preview on macOS, how does it know this image is 220x261?

get info from preview

Well believe it or not, that’s built into the file format4

Bytes 0x6-0xA have this and a little more info in it. Bytes 0x6 and 0x8 refer to the length and width.

🜛 xxd Sunflower_as_gif_websafe_89a.gif | head -1 # and some arrows 00000000:  4749 4638 3961 -> dc00 0501 <- f700 0002 0102  GIF89a.......... 

Each dimension gets two bytes to specify size. Also it’s important to remember that all bytes in gif file format are specified as little endian5.

First comes width, which is 0x00dc (reordered from dc00) => 220 in decimal

Then comes length which is 0x0105 (reordered from 0501) => 261 in decimal

Detour: Does that mean we have a resolution limit on our gifs?

That’s right! Since we only get two bytes each, no one resolution, width or length can be larger than 65535. We can confirm that by trying to make a new gif of 1x65536 in gimp:

waaaay too big

Other file formats don’t fall short to this regard. If you’d like to download the widest png in existence you may do so here. It’s a small download, but may crash your image viewer if you open it. Firefox struggles to open it and says there is an error even though it’s spec compliant.

LIAR

Back to the logical screen descriptor

The logical screen descriptor isn’t done yet though, next come a set of packed fields. It’s easier to explain with the diagram from the spec:

     <Packed Fields>  =      Global Color Table Flag       1 Bit                             Color Resolution              3 Bits                             Sort Flag                     1 Bit                             Size of Global Color Table    3 Bits 

This has information on the global color table which will come after the logical screen descriptor if the global color table bit is set.

Color resolution decides how many bytes there are per color in the global color table.

The sort flag is supposed to be a flag that tells the decoder earlier colors are more important by sorting colors in a most to least useful fashion.

And the size of the global color table is, well, how big the color table is.

In our sunflower at byte 0xA, we have the result 0xF7

🜛 xxd Sunflower_as_gif_websafe_89a.gif | head -1 00000000: 4749 4638 3961 dc00 0501 -> f7 <- 00 0002 0102  GIF89a.......... 

Or in binary that is: 1111 0111

That basically means we have a fully loaded gif except that the GCT is not sorted.

                             ┌──────────GCT not sorted                             ▼          by importance                        1111 0111                        ▲───  ───      GCT set───────────┘ ▲    ▲                          │    │    3 bytes per           │    └─────GCT is 768 bytes    color    ─────────────┘          (max size) (max resolution) 

I suppose we’ve got this far and haven’t even discussed what the global color table is. The global color table holds the colors used in each section of bytes. They’re standard RGB values from 0-255 you could plug into any modern RGB color picker.

Detour: Hey hold on, is that global color table optional?

If you had a sharp eye, you might have noticed that the first bit in byte 0xA says the GCT can be optional. Well that’s interesting. How do we render an image without it specifying what colors it needs?

According to the spec:

Color Tables - Both color tables, the Global and the Local, are optional; if present, the Global Color Table is to be used with every image in the Data Stream for which a Local Color Table is not given; if present, a Local Color Table overrides the Global Color Table. However, if neither color table is present, the application program is free to use an arbitrary color table.

Well that’s neat. Hey what if we remove the global color table to an image, what might modern renders do with our image? Something amazing I’m sure.

Our image specifies color table size is 768 bytes. It starts on byte 0xA… say we just zero out the most significant bit of byte 0xA like so.

And then delete until byte 789 (exclusive):

🜛 xxd Sunflower_as_gif_89a-no-gct.gif | head -1 00000000: 4749 4638 3961 dc00 0501 007f 8121 f904  GIF89a.......!.. 

Now the first line ends like this which is still a perfectly valid gif. How might that look?

no gct here!

Amazing! Stupendous! Wonderful! As of the time of this writing, it’s just a perfectly black square. And this is the case in every single renderer I’ve tried. Gimp, Chrome, Firefox, Preview, gifiddle, you name it.

That’s a little boring, but I’m not sure what I expected.

Anyway back to the logical screen descriptor

The logical screen descriptor continued

After the bytes that describe the global color table, there are two final bytes describing the screen descriptor.

Byte B is the background color which refers to an index into the global color table, and byte C is the pixel aspect ratio, describing the squareness of the pixel.

🜛 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. 

Detour: Hey wait a second, pixel aspect ratio, what’s that?

Pixels weren’t always square! Also bytes weren’t always 8 bits, but that’s a tangent I won’t go into.

Gif, and actually some of the other most popular modern image formats support non-square pixels.

Hey! I wonder how compliant our most popular gif renderers are when rendering non-square pixels. Probably very compliant. Let’s try a popular test in Firefox and Chrome and see how they look: http://frs.badcoffee.info/PAR_AcidTest/

The finest renderers

Uhh…. well that’s something. Those are in order: jpg, png, and gif. And Firefox, Chrome, and Preview all ignore the aspect ratio.

Unfortunately this is widely unsupported and there is currently a 16 year old bug in Firefox for it: https://bugzilla.mozilla.org/show_bug.cgi?id=333377

Even Gifiddle, the most compliant gif viewer I’ve found doesn’t support non-square pixels: https://github.com/ata4/gifiddle/issues/1

We’ll discuss gifiddle more as we try out more obscure gif features.

If you really want to display non-square pixels, you can sort of massage gimp to do it. Also grafx2 can apparently handle very specific odd resolutions of pixels. I have not tested it myself though.

Onto the global color table

The global color table (GCT) is easily the most boring part of the gif. You jump through the image in multiples of 3 from 0 to the size of the global color table. There really isn’t anything worth talking about here.

The best way for me to illustrate this is to point to my awful-gif project and it’ll output all of the colors in the GCT of the sunflower (and maybe other images too).

The GCT parsing is right here and you can see there really isn’t anything special about it.

Run with:

cargo run --quiet -- --gif-file ./experiments/Sunflower_as_gif_websafe.gif 

The optional graphic control extension

Now we have the graphic control extension (GCE), introduced by the extension introducer: 0x21 (extension introduced) and then 0xF9 (!)

There are a number of extensions, but the graphic control extension is arguably one of the most important at least in modern day usage. The GCE is required to make gifs “animated” by allowing a delay time between frames, among other things.

🜛 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 

This gif isn’t exactly animated, so there isn’t a lot going on here. Lots of zeroes as you can see, but we’ll still go into each byte.

The first byte is the block size, which in this case is 0x04, but actually according to the spec is always 0x04.

Detour: Hold on just a second, can we get rid of the block size then?

Ah if the block size is always a static constant, it isn’t really important then, is it? Technically it’s part of the spec, but it doesn’t actually do anything. Let’s play our favorite game of opening it in the most popular image viewers.

For these tests I’ll be using a much simpler gif to make it easier to see what happens:

simple gif

For the following tests I’ve modified it to remove the GCE. The modified version is kept in xxd format below. To can reassemble it:

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                                       ; 

save it to a text file called invalid.hex and do: xxd -r invalid.hex > invalid.gif

(If you can’t find the updated byte, it’s at byte: 0x16 and changed from 0x4 -> 0x03)


First macOS Preview:

preview tests

Preview being standards compliant! We love to see it! Even if it technically doesn’t matter for a file format that will probably never get an update.


Next let’s try Firefox:

firefox tests

Firefox knows it’s a static value and ignores the result of it. Not exactly standards compliant, but probably the smartest thing to do.


chrome tests

Chrome goes a little bananas when the block size is removed. Not exactly inverting it, it does… something which I’m not certain. Chrome is most certainly the least compliant with the standards here.

Back to the graphics control extension

After we read past the block size, we then get a packed field described as:

      <Packed Fields>  =     Reserved                      3 Bits                             Disposal Method               3 Bits                             User Input Flag               1 Bit                             Transparent Color Flag        1 Bit 

All of these fields are set to 0 in our image, so I will just explain them.

Reserved is set for when gif22a comes out and we need those three bits for something good.

User input is for taking user input to advance a gif to the next image with a mouse click or keyboard press.

The transparent index is for setting whether or not we should allow transparency.

Detour: WAIT A SECOND GIFS CAN TAKE USER INPUT ???

Yeah you read that right. Gifs can take user input to advance to the next frame. This poor guy built a whole site around recreating this feature with pngs. A shame he didn’t read the gif spec after being stuck inside for 2 years like me.

While we’re in the weird features section of gif, we might as well discuss the other weird feature gif supports, the plain text extension.

The plain text extension allows the gif maker to embed monospace text wherever they like with some basic styling directly on the image.

The plain text extension like the user input extension, was likely never implemented by any gif viewer besides ones made by eccentric folk for fun like the guy who made gifiddle.

BOB_89A.gif, likely the first gif ever posted on the internet is an example of a gif that uses both.

Here BOB_89A.gif rendered in a modern browser (your own browser, if it’s not modern please update it):

BOB_89A

However if you put it into gifiddle, you get a very different result, with the last message being a very important truth.

I won’t spoil the surprise though, give it a right click download and then put that gif into gifiddle to see what happens.

Gifiddle link: http://ata4.github.io/gifiddle/

Neither of these features are supported by any modern browser or gif viewer.

If you’d like to read more about the plain text extension, you can do so here.

The optional comment extension

The comment extension to come next, and actually may appear anywhere a block may begin. However it most often appears in this part of a gif.

The comment section is only allowed to contain 7 bit ascii and is intended for humans to read.

Since the comment section is just ascii, you can just fire off strings and find the comment in the output:

🜛 strings Sunflower_as_gif_websafe_89a.gif | head -7 | tail -1 QFile source: https://commons.wikimedia.org/wiki/File:Sunflower_as_gif_websafe.gif 

In this image it starts around byte 0x310 in our image:

🜛 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.,..... 

Signified again by a BANG (!) (extension introduced) and 0xfe comment extension. Afterwards the comment should be read for 255 bytes or until a 0x00 is read.

The rest of the image data

There isn’t much more to talk about after that. This image skips out on most other gif features like the local color table and animations, so the majority of the rest of the gif is just data and terminators.

I’ll be perfectly honest, lzw compression isn’t terribly difficult to learn, but explaining it is not the purpose of this blogpost. If you’d like to learn it, Matthew Flickinger has a great post on his site about it.

Bonus section: True color gifs

Did you know that gifs can be true color? It requires a little insanity, but if you remember the world “local color table” then it might make sense. Each data segment is allowed to have its own local color table, and thus if you break a gif up into enough pieces, you get true color!

true color gif

Most gifs don’t do this for a couple of reasons.

First, the resulting image will be gigantic. Each new palette of 256 colors will consume an additional 768 bytes.

Second, renderers nowadays will not “properly” render the image. Browsers by default will often put a delay of 0.1 between frames if not specified.

However a truly compliant gif renderer will properly display a truecolor gif. So if you have the space, and the memory, and the spare CPU for it, why not have a true color gif? We can all have a little true color as a treat.

If you’d like more information on truecolor gifs, Wikipedia has a whole section on it called True color.

Wrapping things up

Anyway if you made it this far, I appreciate it. This was much longer than my average post here. But there was a lot to say!

In fact there’s really a lot more to say too. There are more parts of the gif spec I didn’t go over, and nuances to the format that I could, I dunno, write a whole spec about.

If you’re interested in learning more about gif, I recommend checking out the spec and all of the other links I added at the top of the post.

Otherwise, thanks for reading 😃


  1. https://en.wikipedia.org/wiki/GIF#history ↩︎
  2. Sunflower rehosted from Wikipedia article on gifs (see footnote #1) ↩︎
  3. Ok gif87a technically supports animation in a more limited format. But I’ve already written almost 3500 words at the time of this footnote and I don’t want to get into every detail. For more information you can try out the gif87a animation examples on the gifiddle repo: https://github.com/ata4/gifiddle ↩︎
  4. For more information, see section 18 (Logical screen descriptor) of the gif spec. ↩︎
  5. For more info, see section 4, About the Document. from the gif spec:https://www.w3.org/Graphics/GIF/spec-gif89a.txt ↩︎
posted @ 2022-04-28 15:08  轻狂书生han  阅读(4091)  评论(0编辑  收藏  举报