你真的了解 gif 嗎?分析 gif 文件和一些奇怪的 gif 特性
點擊上方?前端Q,關(guān)注公眾號
回復(fù)加群,加入前端Q技術(shù)交流群
是的,我指的是主流的,遍布全網(wǎng)的普通 gif,谷歌旗下的 Tenor 或 Facebook 旗下的 giphy 這樣的網(wǎng)站到處都是這種 gif。Gif 是所有人都喜歡的,用來分享簡短動畫片斷的文件格式。
正如大多數(shù)人所知道的那樣,gif 是一種動畫文件格式。你可能看過 gif 文件的信息,覺得這些文件可真夠大的。也許你看了它們后會想:哇,這些圖片的清晰度好低啊。但不管怎樣,提到 gif 時,你對它的印象應(yīng)該就是一種短小的動畫文件格式。
然而,這種用例和編寫 gif 的開發(fā)者所期望的用途大相徑庭。在這篇文章中,我們將深入了解 gif 文件的結(jié)構(gòu),并在這一過程中討論它的一些有趣特性。
請注意,這篇文章要探索的是如何理解 gif 格式這一主題,并考察它的一些更深奧的特性。如果你想深入學(xué)習如何解析 gif 文件,我推薦以下這些資源。
W3 規(guī)范
Matthew Flickinger:gif 里有什么?
我發(fā)現(xiàn) ntfs.com 的這份指南也可以幫助入門
寫文章的時候我實際上用這些資源做了一個勉強符合要求的 gif 解析器,名為 awful-gif,可以解析一些 gif。我不建議大家使用它。
下面進入正題。
gif 文件格式是由 Compuserve 在 1987 年創(chuàng)建的。在 1987 年的時候,gif 還是一個相當緊湊的格式!它使用了壓縮方法,而且不是一般的壓縮方法,而是 LZW 壓縮技術(shù)。許多舊的文件格式(其中有些是 Compuserve 制作的)使用的則是 RLE(Run Length Encoding),在許多情況下效率沒那么高。gif 的一個重要取勝因素就是其良好的壓縮率和色域(全 256 色,太棒了?。#ㄗ?1)
兩年后,gif 文件格式加入了補充內(nèi)容(gif89a),增加了許多我們今天眾所周知和喜愛的特性。
通過 gif89a 規(guī)范,我們可以快速總結(jié)出 gif89 與 gif87a 支持的所有特性的區(qū)別。
AppendixA. Quick Reference Table.Block Name Required Label Ext. Vers.Application Extension Opt. (*) 0xFF (255) yes 89aComment Extension Opt. (*) 0xFE (254) yes 89aGlobal Color Table Opt. (1) none no 87aGraphic Control Extension Opt. (*) 0xF9 (249) yes 89aHeader Req. (1) none no N/AImage Descriptor Opt. (*) 0x2C (044) no 87a (89a)Local Color Table Opt. (*) none no 87aLogical Screen Descriptor Req. (1) none no 87a (89a)Plain Text Extension Opt. (*) 0x01 (001) yes 89aTrailer Req. (1) 0x3B (059) no 87aUnlabeled BlocksHeader Req. (1) none no N/ALogical Screen Descriptor Req. (1) none no 87a (89a)Global Color Table Opt. (1) none no 87aLocal Color Table Opt. (*) none no 87aGraphic-Rendering BlocksPlain Text Extension Opt. (*) 0x01 (001) yes 89aImage Descriptor Opt. (*) 0x2C (044) no 87a (89a)Control BlocksGraphic Control Extension Opt. (*) 0xF9 (249) yes 89aSpecial Purpose BlocksTrailer Req. (1) 0x3B (059) no 87aComment Extension Opt. (*) 0xFE (254) yes 89aApplication Extension Opt. (*) 0xFF (255) yes 89alegend: (1) if present, at most one occurrence(*) zero or more occurrences(+) one or more occurrences
對于沒有讀過整份規(guī)范的人們來說,這里面的大部分內(nèi)容可謂不知所云,所以讓我們討論一下 gif 是如何組合在一起的,順便再談?wù)勊囊恍┢婀种帯?/p>
在我們開始之前,先從規(guī)范中找些樂趣。
AppendixD. Conventions.Animation - The Graphics Interchange Format is not intended as a platform foranimation, even though it can be done in a limited way.
附錄D.? 公約。
動畫——這個圖形交換格式不是要成為一個動畫平臺,盡管它在某種程度上可以做到這一點。
下面我會用一個例子來具體分析。如果你想跟著做,下載它即可。(注 2)

如果你在家里跟著學(xué),只需要一臺安裝有 hexdump 工具的機器即可。我要用的是 xxd,它預(yù)裝在大多數(shù) unix 系統(tǒng)(Linux、macOS)上,或者可以通過 vim-common 包安裝。
gif 頭
每個 gif 都以一個頭開始,其中的 magic 位標志著它是什么類型的 gif,還有一點額外的信息,提供關(guān)于圖像的基本細節(jié)。
?? xxd Sunflower_as_gif_websafe_89a.gif | head -1 # and some arrows00000000: -> 4749 4638 3961 <- dc00 0501 f700 0002 0102 gif89a..........
用 xxd 可以輕松把 gif 頭信息解碼為 ascii(如果它有意義的話)。看看上面的內(nèi)容寫著,gif89a!這是一個經(jīng)過認證的有效 gif!
每個字母都是一個字節(jié),所以我們在這里要找的 magic 字節(jié)是:0x47、;0x49、0x46、0x38、0x39、0x61。
另外,最后三個字節(jié)可能是:0x38、0x37、0x61,如果只支持 gif87a 文件格式會是這樣。我們主要研究 gif89,老版本的格式就一帶而過了。
此外 gif 頭里面就沒有什么有趣的東西了,因為它只是靜態(tài)文本,所以我們繼續(xù)往前走。
先等一下問個問題:誰會接受 gif87a 呢?
在研究 gif 時,我想看看主要的 gif 托管供應(yīng)商是否會接受和保留 gif87a 規(guī)范的格式。它們能正常使用嗎,還是說只能報錯?
這是我們之前看到的向日葵的 gif87a 版本。這個版本只用在這里。

我們來把圖像上傳到 4 家頭部 gif 托管供應(yīng)商:
tenor
giphy
imgur
gfycat
我們開始的時候 gif 頭是這樣:
?? xxd Sunflower_as_gif_websafe_gif87a.gif | head -100000000:?4749?4638?3761?fa00?2901?f500?00ff?cc33??gif87a..)......3
以下是重新下載我剛上傳的圖片后的結(jié)果。
Tenor 重編碼為 gif89a:
?? Downloads xxd tenor.gif | head -100000000:?4749?4638?3961?a401?f201?f700?0006?0406??gif89a..........
giphy 重編碼為 gif89a:
?? Downloads xxd giphy.gif | head -100000000:?4749?4638?3961?fa00?2901?f525?0000?0000??gif89a..)..%....
其實這有點忽悠人,giphy 只接受動畫形式的 gif,所以我們必須點擊編輯按鈕(顯示幀編輯器),然后點擊完成才行。gif87a 規(guī)范中允許存儲多張圖片,但它們不能有延遲(因此沒有動畫,見注 3)。
imgur 保留了原始文件?。?!
?? Downloads xxd aUxm3NN.gif | head -100000000:?4749?4638?3761?fa00?2901?f500?00ff?cc33??gif87a..)......3
至于 gfycat,它一直卡在最后的“編碼“階段整整 20 分鐘。希望我沒有在周末讓他們的一位可憐的工程師看到什么警報。
以上簡短分析表明,由世界上最大的兩家科技公司所有的兩家最大的托管供應(yīng)商并不尊重我的舊 gif 文件,而是完全重寫了它。事實上,對于 giphy 這家公司來說,它似乎只尊重一種 gif......
總之回到探索文件格式的話題上。
邏輯屏幕描述符
那么你的圖像是如何顯示成某個分辨率的呢?假設(shè)我們在 macOS 的 Preview 中使用“get info“特性,它是怎么知道這張圖片是 220x261 的?

信不信由你,這是在文件格式中內(nèi)置的?。ㄗ?4)
字節(jié) 0x6-0xA 就是這部分信息,另外還加了點內(nèi)容。字節(jié) 0x6 和 0x8 指的是長度和寬度。
?? xxd Sunflower_as_gif_websafe_89a.gif | head -1 # and some arrows00000000:??4749?4638?3961?->?dc00?0501?<-?f700?0002?0102??gif89a..........
每個維度有兩個字節(jié)來指定大小。同樣一定要記住,gif 文件格式中的所有字節(jié)都被指定為小 endian(注 5)。
首先是寬度,是 0x00dc(從 dc00 重新排序)=> 220(十進制)。
然后是長度,是 0x0105(從 0501 重新排序)=> 261(十進制)。
慢著,這是否意味著我們的 gif 有一個分辨率限制?
這就對了!因為每個位置只有兩個字節(jié),所以寬度或長度都不能大于 65535。我們可以嘗試在 gimp 中制作一個 1x65536 的新 gif 來驗證這一點。

其他文件格式在這方面也差不多。如果你想下載理論上最寬的 png,可以點這里。這個文件很小,但打開它的時候你的圖像瀏覽器可能會崩潰。Firefox 瀏覽器很難打開它,并報告了一個錯誤,盡管它是符合規(guī)范的。

回到邏輯屏幕描述符上
不過邏輯屏幕描述符還沒說完,接下來是一組打包的字段。用規(guī)范中的圖表解釋比較容易。
Color Resolution 3 BitsSort Flag 1 Bit?????????????????????????????Size?of?Global?Color?Table????3?Bits
這里有關(guān)于全局顏色表(Global Color Table)的信息,如果設(shè)置了全局顏色表位,它將出現(xiàn)在邏輯屏幕描述符之后。
顏色分辨率(Color Resolution)決定了全局顏色表中每種顏色有多少個字節(jié)。
排序標志(Sort Flag)會告訴解碼器排在前面的顏色更重要,它會以有用的程度從高到低排序顏色。
而全局顏色表的大小是說,顏色表有多大。
在我們向日葵圖片的 0xA 字節(jié)中,我們有 0xF7 的結(jié)果
?? xxd Sunflower_as_gif_websafe_89a.gif | head -100000000:?4749?4638?3961?dc00?0501?->?f7?<-?00?0002?0102??gif89a..........
或者在二進制中就是:1111 0111
這意味著我們的 gif 基本滿載,除了 GCT 沒有排序。
┌──────────GCT not sorted▼ by importance1111 0111▲─── ───GCT set───────────┘ ▲ ▲│ │3 bytes per │ └─────GCT is 768 bytescolor ─────────────┘ (max size)(max?resolution)
全局顏色表保存了每個字節(jié)部分所使用的顏色。它們是 0-255 的標準 RGB 值,你可以在任何現(xiàn)代 RGB 取色器里使用這些數(shù)值。
等一下,那個全局顏色表是可選的嗎?
你可能已經(jīng)注意到 0xA 字節(jié)的第一位說 GCT 可以是可選的。這的確很有趣。我們?nèi)绾卧跊]有指定它需要什么顏色的情況下渲染圖像呢?
根據(jù)下面的規(guī)范:
顏色表——全局顏色表和局部顏色表都是可選的;如果存在全局顏色表,它將用于數(shù)據(jù)流中沒有給出局部顏色表的所有圖像;如果存在局部顏色表,它將覆蓋全局顏色表。然而,如果兩個顏色表都不存在,應(yīng)用程序可以自由地使用一個任意的顏色表。
如果我們拿走一張圖像的全局顏色表,現(xiàn)代渲染器會對我們的圖像做什么呢?我敢肯定會有一些驚人的事情發(fā)生。
我們的圖像指定的顏色表大小為 768 字節(jié)。它從 0xA 字節(jié)開始...... 假設(shè)我們像這樣把 0xA 字節(jié)的最有意義的比特清零。
然后刪除到第 789 字節(jié)(獨占)。
?? xxd Sunflower_as_gif_89a-no-gct.gif | head -100000000:?4749?4638?3961?dc00?0501?007f?8121?f904??gif89a.......!..
現(xiàn)在第一行是上面這樣結(jié)束的,這仍然是一個完全有效的 gif。
簡直了!在寫這篇文章的時候,它就只顯示一個完美的黑色方塊。在我試過的每一個渲染器中都是這樣的情況。Gimp、Chrome、Firefox、Preview、gifiddle,隨便哪個都一樣。
總之回到邏輯屏幕描述符上。
繼續(xù)談邏輯屏幕描述符
在描述全局顏色表的字節(jié)之后,有兩個描述屏幕描述符的末端字節(jié)。
字節(jié) B 是背景顏色,指的是全局顏色表的索引;字節(jié) C 是像素長寬比,描述了像素的方正度。
xxd Sunflower_as_gif_websafe_89a.gif | head -100000000: 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.
等一下,像素長寬比是什么?
像素并不總是正方形的!字節(jié)也不總是 8 位,但這一點就不多說了。
gif 和其他一些最流行的現(xiàn)代圖像格式都支持非正方形像素。
我想知道最流行的 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
如果你真的想顯示非方形像素,可以用調(diào)整過的 gimp 來做。此外,grafx2 顯然可以處理非常特定的奇怪像素分辨率。不過我還沒有親自測試過。
回到全局顏色表
全局顏色表(GCT)顯然是 gif 最無聊的部分。這里真的沒有什么值得談?wù)摰臇|西。
我的 awful-gif 項目可以輸出向日葵的 GCT 中的所有顏色(也許其他圖像也行)。
GCT 的解析就在這里,你可以看到它真的沒有什么特別的地方。
用下面的命令運行:
cargo?run?--quiet?--?--gif-file?./experiments/Sunflower_as_gif_websafe.gif可選的圖形控制擴展
下面我們講圖形控制擴展(GCE),由擴展引入器 0x21 引入(extension introduced),然后是 0xF9(!)
可用的擴展有許多,但圖形控制擴展可以說是最重要的擴展之一,至少在現(xiàn)代用例中是這樣。GCE 允許各幀之間存在顯示延遲,這樣 gif 才能成為“動畫“。GCE 還允許其他一些事項。
?? xxd Sunflower_as_gif_websafe_89a.gif | head -50 | tail -200000300: 88ae b091 a5b1 a4b9 be94 887f 81 -> 21 f904 .............!..00000310:?0000?0000?<-?0021?fe51?4669?6c65?2073?6f75??.....!.QFile?sou
這個 gif 并不是動畫,所以這里并沒有發(fā)生很多事情。正如你所看到的,上面有很多零,但我們還是一個字節(jié)一個字節(jié)來講。
第一個字節(jié)是塊大小,在這個例子中是 0x04,但實際上根據(jù)規(guī)范它總是 0x04。
等一下,我們能不能把塊大小去掉?
如果塊大小總是一個靜態(tài)的常數(shù),那么它就不太重要了是嗎?從技術(shù)上講,它是規(guī)范的一部分,但實際上并沒有什么作用。我們再在流行的圖像瀏覽器中打開它看看。
在這些測試中我將使用一個更簡單的 gif,這樣更容易看到發(fā)生了什么情況:
在下面的測試中我對它做了修改,刪除了 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..&g00000060: a70d 3564 bd1a b52e 25b7 f905 8729 de31 ..5d....%....).100000070: 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 的文本文件中,然后執(zhí)行:xxd -r invalid.hex > invalid.gif
(更新的字節(jié)在:0x16,從 0x4->0x03)
第一個是 macOS Preview:

Preview 是符合標準的!
接下來我們試試 Firefox:

Firefox 知道這是一個靜態(tài)值,并忽略了它的結(jié)果。這并不完全符合標準,但可能是最聰明的做法。

當塊大小被移除后,Chrome 會有點抓狂。在這里,Chrome 肯定是最不符合標準的。
回到圖形控制擴展
在我們讀完塊大小之后,是一個包裝好的字段,描述如下。
Disposal Method 3 BitsUser Input Flag 1 Bit?????????????????????????????Transparent?Color?Flag????????1?Bit
在我們的圖像中所有這些字段都被設(shè)置為 0,所以我只解釋它們。
Reserved 是為 gif22a 出現(xiàn)時設(shè)置的,我們需要這三個位來做一些好事。
User Input 是為了接受用戶輸入,通過點擊鼠標或按下鍵盤將 gif 圖片推進到下一幅。
透明索引是用來設(shè)置我們是否應(yīng)該允許透明。
等一下,gif 可以接受用戶輸入???
是的,你沒看錯。gif 可以接受用戶的輸入來推進到下一幀。這個可憐的家伙為了用 png 重現(xiàn)這一特性建立了一個網(wǎng)站。真可惜,他像我一樣被困在這里了,就因為他沒看過 gif 規(guī)范。
我們不妨討論 gif 支持的另一個奇怪特性,即純文本擴展。
純文本擴展允許 gif 制作者在他們喜歡的任何地方嵌入單色文本,并直接在圖像上進行一些基本的樣式設(shè)計。
純文本擴展和用戶輸入擴展一樣,除了像 gifiddle 的這樣為了好玩而制作的 gif 查看器外,可能從未被任何 gif 查看器實現(xiàn)。
BOB_89A.gif 可能是有史以來在互聯(lián)網(wǎng)上發(fā)布的第一個 gif,是一個同時使用這兩種方式的 gif 例子。
下面是 BOB_89A.gif 在現(xiàn)代瀏覽器中的渲染。

然而,如果你把它放到 gifiddle 中,會得到一個非常不同的結(jié)果,最后的信息是一個非常重要的事實。
不過我不會劇透這個驚喜。你可以下載這個 gif 放到 gifiddle 里,看看會發(fā)生什么。
gifiddle 鏈接:http://ata4.github.io/gifiddle/
任何現(xiàn)代瀏覽器或 gif 瀏覽器都不支持這兩項特性。
如果你想閱讀更多關(guān)于純文本擴展的信息,可以看這里。
可選的注釋擴展
接下來是注釋擴展,實際上它可以出現(xiàn)在一個塊可能開始的任何地方。然而它最常出現(xiàn)在 gif 的這一部分。
注釋部分只允許包含 7 位的 ascii,并且是供人類閱讀的。
由于注釋部分只是 ascii,你可以直接發(fā)射字符串并在輸出中找到注釋。
?? strings Sunflower_as_gif_websafe_89a.gif | head -7 | tail -1QFile?source:?https://commons.wikimedia.org/wiki/File:Sunflower_as_gif_websafe.gif
在這張圖片中,它開始于圖片的 0x310 字節(jié)。
?? xxd Sunflower_as_gif_websafe_89a.gif | head -55 | tail -600000310: 0000 0000 0021 fe51 4669 6c65 2073 6f75 .....!.QFile sou00000320: 7263 653a 2068 7474 7073 3a2f 2f63 6f6d rce: https://com00000330: 6d6f 6e73 2e77 696b 696d 6564 6961 2e6f mons.wikimedia.o00000340: 7267 2f77 696b 692f 4669 6c65 3a53 756e rg/wiki/File:Sun00000350: 666c 6f77 6572 5f61 735f 6769 665f 7765 flower_as_gif_we00000360:?6273?6166?652e?6769?6600?2c00?0000?00dc??bsafe.gif.,.....
圖像數(shù)據(jù)的剩余部分
之后就沒有什么可談的了。這張圖像跳過了大多數(shù)其他的 gif 特性,如本地顏色表和動畫,所以這張 gif 剩下的大部分只是數(shù)據(jù)和終止符。
老實說 lzw 壓縮并不難學(xué),但本文并不是要講這個話題。如果你想學(xué)習它,Matthew Flickinger 在他的網(wǎng)站上有一篇好文章。
附加內(nèi)容:真彩 gif
你知道 gif 可以是真彩色的嗎?這和“局部顏色表“有關(guān)系。每個數(shù)據(jù)段都允許有自己的局部顏色表,因此如果你把一個 gif 分成足夠多的片斷,你就可以得到真彩色了!

大多數(shù) gif 不會這樣做,有幾個原因。
首先,這樣生成的圖像是非常大的。每一個新的 256 色調(diào)色板將消耗額外的 768 字節(jié)。
第二,現(xiàn)在的渲染器不會“正確“渲染這樣的圖像。瀏覽器在默認情況下,如果沒有指定,通常會在幀之間設(shè)置 0.1 的延遲。
然而,一個真正符合規(guī)范要求的 gif 渲染器會正確地顯示真彩色 gif。因此,如果你有足夠的空間、內(nèi)存和多余的 CPU,為什么不做一個真彩 gif 呢?
如果你想了解更多關(guān)于真彩 gif 的信息,維基百科上有一整個章節(jié)。
感謝大家有耐心看到這里。gif 規(guī)范中還有更多部分我沒有講到,如果你有興趣了解更多關(guān)于 gif 的信息,我建議你查看規(guī)范和我在文章頂部添加的那些鏈接。
1.? https://en.wikipedia.org/wiki/gif#history?
2.? 向日葵圖片轉(zhuǎn)自維基百科關(guān)于 gifs 的文章(見腳注 1)?
3. ?gif87a 在技術(shù)上是以比較有限的格式支持動畫的。要了解更多信息,你可以試試 gifiddle 倉庫上的 gif87a 動畫例子:https://github.com/ata4/gifiddle?
4. ?更多信息請參見 gif 規(guī)范的第 18 節(jié)(邏輯屏幕描述符)。
5.更多信息,請參見第 4 節(jié)。文檔來自 gif 規(guī)范:https://www.w3.org/Graphics/gif/spec-gif89a.txt?
原文鏈接:
https://blog.darrien.dev/posts/you-dont-know-gif/

往期推薦



最后
歡迎加我微信,拉你進技術(shù)群,長期交流學(xué)習...
歡迎關(guān)注「前端Q」,認真學(xué)前端,做個專業(yè)的技術(shù)人...


