GIF文件格式


參考文章:What's In A GIF

本文全面介紹了GIF文件格式的編碼格式。

GIF文件內容

GIF現在是一個W3C標準。它由一系列數據塊組成,前兩個塊是固定長度且固定格式的。之后的數據塊可以是變長的,但需要自我描述;它們的組成為:一個標志塊類型的字節,跟一個長度類型,再跟有效的數據負載。


GIF文件格式

上圖描述了這些不同類型的塊在GIF文件中的位置,中間的那個大塊可以被重復任意次。

接下來,我們來詳細說明每一個塊。

Header Block

所有GIF都必須以一個header block(頭部塊)開始。header的長度固定為6個字節,以ASCII編碼,開頭三個字節稱作signature(標志),它們必須為“GIF”這三個字母。接下來的三個字節指定version(版本),通常版本字符串為“89a“或者”87a“,現代GIF處理引擎均同時支持二者。考慮到最大兼容,除非文件包含GIF89的特性,GIF一般都會選擇87a

Header Block

Logical Screen Descriptor

Logical Screen Descriptor(邏輯屏幕描述符)緊跟在header后面。這個塊告訴decoder(解碼器)圖片需要占用的空間。它的大小固定為7個字節,以canvas width(畫布寬度)canvas height(畫布高度)開始。它們中的每個占用2個字節,均為無符號整形(0-65,535)。

在GIF文件格式中,所有的多字節值均以小端格式(little-endian format,從最后一個字節開始往前保存)存儲。比如在我們看來 0A 00,這個值實際寫作 000A,也就是十進制的10。

現代的處理引擎一般會忽略掉canvas width和canvas height,因為GIF最初被設計為一種類似picture wall(照片墻)的東西,思路是講圖片在一個virtual canvas(虛擬畫布)中顯示;現在GIF通常是作為用來保存動畫中的幀的圖片庫,相當于是一個圖片集合,由GIF處理引擎來在自己的canvas(畫布)中處理圖片,因此現在GIF格式中保存的信息顯得沒太大作用了。所以,現在canvas width和canvas height基本上就是個擺設了。

接下來的3個字節是4個域(field)打包值,也就是“logical screen descriptor”。例如,這個值為91 00 00,也就是二進制的 10010001,我們用一張圖來描述這些位代表的意義:


Logical Screen Descriptor

最高位字節稱為global color table flag(全局顏色表標志)。如果它為0,就表示沒有global color table。如果為1,表示后面會跟上global color table。因此,上面舉的例子表示開啟了global color table(通常都是開啟的)。

接下來的三個字節是color resolution(顏色分辨率)。只有當global color table flag為1時它們才有意義,它用來計算表的大小。比如如果這個值為N,就意味著global color table中包含2 ^ (N + 1)個條目。因此,例子中的001代表2 bits/pixel,如果是111則表示8 bits/pixel。

接下來的一個字節為sort flag(排序標志)。如果為1,global color table中的顏色以“重要性降低”(decreasing importance)來排序,通常也就是“頻率降低”方式。這個標志可能會對decoder有所幫助,不過并不是必要的,因此這里的例子將它簡單地設置為0。這個標志是歷史遺留產物。

接下來的字節會給我們提供background color index(背景色索引)。僅當global color table flag為1才有意義,此時這個標志應該設為0。在之前的“picture wall”模式中,我們說到GIF會有一個virtual canvas畫布,因此理所當然會存在一個背景色,而我們這個標志就是指出這個背景顏色。同樣,在現代引擎中它也隨著“picture wall模式”一起被棄用了。

最后一個字節是pixel aspect ratio(像素高寬比)。GIF標準沒有給出它存在的理由,不過目前對它的處理方式只是讀取其值并保存下來。

Global Color Table

GIF格式可以擁有global color table,或用于針對每個子圖片集,提供local color table。每個color table由一個RGB(就像通常我們見到的(255,0,0)紅色 那種)列表組成。

正如之前說到的,global color table的長度為2 ^ (N + 1),因此這個表占用 3 * 2 ^ (N + 1)個字節。


注意global color table被標記為可選的,因此它并不是每個GIF都有的,然而如果global color table flag被設置為1,就應該在logical screen Descriptor后跟上顏色表。


Global Color Table

Graphics Control Extension

Graphics Control Extension(圖像控制擴展)塊用于指定透明度以及控制動畫。它是GIF89中的可選擴展。在后面(Transparency and Animation)我們會詳細地介紹它的語義。

首個字節是extension introducer(擴展介紹符)。所有的擴展塊都以21開始。接下來是graphics control label,F9,表示這是一個graphic control extension。第三個字節是block size(塊大?。?。

接下來的字節是一個打包域,其1-3字節保留尚未使用,4-6表示disposal method(處理方法)。倒數第二個位是user input flag(用戶輸入標志),最后一位transparent color flag(透明色標志)。隨后的兩個字節為delay time值,以unsigned格式保存。之后是transparent color index(透明色索引)。最后,以00作為Block Terminator(塊結束符)。


Graphic Control Extension

Image Descriptor

一個GIF文件一般包含多個圖片。之前的圖片渲染模式一般是將多個圖片繪制到一個大的(virtual canvas)虛擬畫布上,而現在一般將這些圖片集用于實現動畫。

每個image都以一個image descriptor block(圖像描述塊)作為開頭,這個塊固定為10字節。

第一個字節是image separator(圖像分隔符)。每個image descriptor以2C作為開頭,后8個字節代表圖片的位置以及隨后的圖片數據的大小。

GIF中一個image不需要占用整個在logical screen descriptor中定義的canvas畫布大小。因此,image descriptor指定image left position和image top position來表示image在canvas中的起始位置。同樣,在現代渲染模式中它們是無用的。

接下來的塊指定image width和image height。每個值都是2字節的,無符號小端格式。

最后的字節是另一個packed field。首位是local color table flag(局部顏色表標志)。設置位1表示允許你指定圖片數據使用一個不同的顏色表(跟在后面)以取代全局顏色表。

第二個位是interlace flag(隔行掃描標志)。隔行掃描方式會改變圖像渲染到屏幕上的方式,從而減少煩人的視覺閃爍(類似垂直同步)。隔行掃描在顯示器上的效果是,第一輪掃描先立即模糊地顯示圖像,隨后再一輪將其填充銳化。這種方式會讓人們感覺更舒服,因為它可以讓人們模糊地意識到即將顯示的東西是什么,而不是等待像素點被一行一行地填充繪制。要支持這種顯示方式,圖片的掃描行需要以一種特定的順序來存儲,需要分為4個部分,每個部分都是一個完整的模糊顯示,通過4次顯示加成使得圖片越來越清晰,最終完全呈現。


Image Descriptor

Local Color Table

local Color Table(本地顏色表)的組織方式與global color table類似。當且僅當local color table flag被設置為1時,它才啟用,這個塊跟在Image Descriptor后。它僅僅對跟在它后面的那個Image Data(圖像數據)有用。如果沒有指定local color table,那么圖像就會使用global color table。

local color table的大小可以通過image descriptor中的相關值計算。計算方式與global color table相同。

Image Data

終于到了圖片數據實際存儲的地方。Image Data是由一系列的輸出編碼(output codes)構成,它們告訴decoder(解碼器)需要繪制在畫布上的每個顏色信息。這些編碼以字節碼的形式組織在這個塊中。

關于解碼這些輸出編碼到一張圖片的過程,之后還會詳細討論。這里我們僅僅來看一下這個塊的大小如何確定。

這個塊的第一個字節是LZW minimum code size(LZW是一種圖片壓縮方式)。這個值用來解碼這個經過壓縮的輸出編碼(同樣,之后會討論LZW壓縮方式的工作過程)。剩下的字節則代表data sub-blocks(數據子塊),它們以一種類似鏈表的方式組織。數據子塊由一些從1-256的字節碼組成,子塊的第一個字節告訴你后面實際跟了多少字節的數據。這個值可以是0到255之間的值。當你讀出的所有這些字節后,下一個讀出的字節會告訴你后面還跟了多少數據。我們會一個接一個地讀取這些數據子塊,直到到達一個子塊,它告訴你后面有0個字符,讓你滾。


Image Data

Plain Text Extension

GIF89標準允許你指定一段文本覆蓋在其跟隨的圖片上作為標題。這個東西基本上沒有什么卵用,瀏覽器、圖像處理應用(如PhotoShop)會直接忽略它,GIFLIB也不打算解釋它。

這個塊以extension introducer(21)開始。緊跟著的一個字節是plain text label(純文本標志)。這個標志設置為01,用以將plain text extension與其他extension區分開。下一個字節是block size(塊大小),這個東西告訴你離實際數據還距離多少個字節,或者換句話說,你可以從此處跳過多少個字節。這個值可能為0C,表示你可以向后跳過12個字節。最后跟的數據子塊的組織方式與Image Data中的類似。

Application Extension

GIF89標準允許在GIF中嵌入“應用指定“的信息。這個東西也不怎么用到。

同樣,這個塊以21開始。下一個字節同樣是extension label, 在application extension中為FF。接下來是block size,表明距離實際的application data開始還有多少個字節。這個值應該設置為0B,表示有11個字節。這11個字節用來保存兩個信息。第一個信息是application identifier,占用8個字節。后面3個字節是application authentication code。

Comment Extension

最后一個GIF89擴展類型是Comment Extension。這個塊允許你在GIF文件中嵌入ASCII文本,有時它被用來作為圖片的描述、圖像署名,以及類似GPS坐標這種可讀取的元數據。

第一個字節為21,extension label為FE標志這是一個comment label。同樣,它的數據子塊與前面的塊采用相同的組織形式。


Comment Extension

Trailer

尾部標志用來指明你已經到達文件尾端,固定為3B。


LZW圖片數據壓縮算法

GIF文件中的圖像是一種光柵格式,GIF存儲像素點的顏色在顏色表(Global Color Table或Local Color Table)中的索引。


樣本圖片

顏色表來自global color table block。這些顏色以它們在文件中出現的先后順序排列,第一個顏色的索引位置為0。我們以自左上到右下的順序對每個像素進行編碼,因此,樣本圖片可以這樣來編碼:

1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 1, 1, 1, 0, 0, 0, 0, 2, 2, 2, 1, 1, 1, 0, 0, 0, 0, 2, 2, 2, ...

這個序列是前五行的像素信息的編碼,我們可以以這種方式一直編碼下去,直到表示完整個圖片。不過,這樣耿直的編碼方式勢必導致圖片文件增大。還好,GIF格式允許我們利用數據重復輸出的方式來壓縮數據大小。

LZW壓縮

GIF使用的壓縮算法叫做LZW(Lempel-Ziv-Welch)Compression,我們簡單地看一下它的核心思想。使用這種方式壓縮,我們需要準備一張code table(編碼表)。這個編碼表允許我們指定一個簡單的編碼來代表一個較復雜的顏色序列。我們要做的第一步是初始化這個編碼表。例如,我們可以建立一張如下的表(用#將編碼與顏色值區分開):


編碼表示例

而LZW壓縮算法的核心思想,就是通過盡可能地提取出那些高頻重復的序列,將這些重復的顏色序列在編碼表中登記,然后用這些編碼來替換原始圖片數據,以達到壓縮的目的。

具體細節,請閱讀LZW Image Data。

動畫和透明

眾所周知,除了作為一種圖片存儲格式外,GIF圖像(特別是GIF89a)還可以實現很多特殊功能,比如透明和動畫。這些東西可以通過Graphics Control Extension塊的幫助來實現。下面是一個該塊的例子:


Graphic Control Extension

接下來,我會展示如何操作此塊以實現某些特殊的效果。

動畫

我們知道,動畫是通過快速播放一系列略微不同的圖片,從而產生的一種效果。GIF動畫也是如此,它存儲一些列圖片,先顯示一張圖片,然后告訴機器播放下一張圖片之前需要等待的時間,到時則繼續顯示下一張圖。

我們來看一個紅綠燈GIF,下面是這個文件的二進制內容。


根據我們之前講的GIF文件格式,我們可以輕易地讀懂這些二進制碼所代表的內容。首先是header,logical screen descriptor和全局顏色表,顏色表中定義了幾個我們需要用到的顏色(0=紅,1=綠,2=黃,3=淺灰,4=黑,5=白,6=黑(未使用),7=黑(未使用))。

然后跟著一個application extension block塊,這點可以從21 和 FF標志看出,這個擴展塊導致了這個GIF圖片無限重復播放,而不僅僅是播放一兩次就停了。從塊的第三個0B可以直到后面跟著11個字節的固定長度數據,內容為“NETSCAPE2.0”。接下來則是實際的“application data”,它被放在子塊中。子塊中一共保存了兩個值,第一個值是固定的01。然后,另一個值是一個無符號的值,指明動畫需要被重復多少次。樣本中,可以看到這個值為0,這說明這張圖片的動畫是一個無限循環的播放,這三個字節前的03告訴解碼器數據長度為3個字節,并且以00作為結束符。


樣本gif

這就是樣本的顯示效果,它由3個場景構成,紅綠燈中的顏色會依次閃爍呈現。

第一個組塊緊跟在application extension塊后,它是我們碰到的第一個graphic control extension。和所有擴展塊一樣,它以21開頭,隨后的F9標志說明了它是什么類型的擴展塊。然后我們會看到byte size,它總是為04。好了,這四個數據塊中,首先是一個打包域。

這個打包域保存了3個值。最前面3個字節為保留字節。其次三個字節指明disposal method,這個東西指定解碼器在解碼下個圖片數據塊時采用的操作。3個字節的大小意味著我們可以使用0-7之間的值。樣本圖片采用1來執行動畫,解碼器在這種方式下會留下先前的圖片信息在畫布上,然后直接將下一張圖片覆蓋繪制在上面。如果是值2,解碼器則會在繪制下一張圖片之前,先將畫布填充為背景色(在logical sreen descriptor中指定),之后再進行繪制。方式3不常用,而方式4-7尚未被定義。如果不想讓gif執行動畫,則應該指定為0。

第7個位是user input flag。當它為1時,意味著解碼器將在移動到下一個場景之前會等待某些來自人類觀看者的輸入。實際上很難見到這個標志位不為0的情況。最終的位是transparency flag(透明標志)。后面還會詳細介紹透明的信息,然而樣本圖片沒有透明色,因此為0。

接下來的兩個位是延遲時間。它表示在移動到下一個場景之前所等待的百分之一秒數。樣本圖片在第一個graphics control block中指定的值為100(64 00),意味著在播放下一張圖片之前的延遲為1秒。

graphics control extension block以塊結束符00結束。你可能注意到了這個快在后面還出現了兩次,并且只有延遲時間上有點差別(黃燈到紅燈只有半秒的延遲)。

下面的一個組塊為image descriptor。這個塊申明我們將會自左上到右下地在整個畫布(11px x 29px)上繪制圖片,這個塊后跟著image data,它包含了繪制第一個場景的所有編碼。

如果我們對比第一和第二個場景,會發現它們之間存在很多重復的像素色點。與其重新繪制整個畫布,我們可以指定只繪制兩個場景中不同的部分(也就是,一個覆蓋兩者改變區域的最小矩形)。你可以發現第二張圖片的image descriptor指定了第二張圖片的起始位置為(2,11),并且只繪制一個7x寬16px高的矩形區域。這個區域剛好覆蓋了兩個場景之間的不同部分。這一切得以實現,歸功于之前在graphics control extension中選擇了dispose method 1。

透明

GIF圖片一般是一個矩形區域,它覆蓋了圖像下面的背景,而透明允許“顯示圖像下面的內容”。這實際上是一個非常簡單的技巧:我們可以在顏色表中指定一個特殊的顏色,當繪制時遇到這個顏色時,我們就顯示背景。很簡單,在graphics control extension塊中有兩個標志位與設置透明相關,它們的意義很明確,這里就不再贅述了。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,546評論 6 533
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,570評論 3 418
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,505評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,017評論 1 313
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,786評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,219評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,287評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,438評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,971評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,796評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,995評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,540評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,230評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,662評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,918評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,697評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,991評論 2 374

推薦閱讀更多精彩內容