深入理解GIF檔案格式
大家好,本文我將給大家介紹一個完整的GIF(GraphicsInterChangeFormat)解碼器,使用了LZW(Lempel-Ziv-Welch Encoding,串標壓縮演算法)解壓縮器。
GIF本身已有25年的歷史,是最古老的影象壓縮格式,至今仍在普遍使用。雖然它與流行的PNG和JPG格式有競爭,但它仍然是一種相當常見的影象壓縮方法。正如你將看到的,GIF格式背後的許多設計考慮都是基於當時的硬體,而硬體本身現在已經過時了,但是格式總體上已經經受住了時間的考驗。
在GIF檔案的上下文中,影象是一個由彩色點組成的矩形陣列,稱為畫素。由於CRT(Cathode-Ray Tube,陰極射線管)顯示方式和它們的現代繼承者——液晶顯示器(LCD)的建造,這些畫素實際上是三種不同顏色分量的組合:紅色、綠色和藍色。通過改變每個畫素的顏色分量的強度,可以表示任何顏色。例如,深紫色是全強度紅色和全強度藍色的混合體。一般來說,顏色分量可以從0(不存在)到2 8 =256(全亮度)不等。
早期具有彩色功能的圖形硬體一次只能顯示16種不同的顏色,但現代顯示器可以顯示紅色、綠色和藍色強度的任何組合。這可以計算出2 24 =16777216種不同的顏色,遠遠超過人眼的實際識別能力。GIF可以追溯到20世紀80年代,當時16色顯示器很常見,2 24 “真彩色”顯示器很少見,GIF基於調色盤(palettes)的概念,設定其規範並稱之為顏色表(color tables)。雖然16色顯示器一次只能顯示16種不同的顏色,但最前面的應用程式可以選擇這16種顏色的224種可能的顏色分量強度組合,然後,GIF檔案格式先描述這些顏色強度值的調色盤,再將影象本身描述為該調色盤中的一系列索引。
顏色表:
示例1:索引四色笑臉影象
示例1演示了一個粗略繪製的笑臉影象。因為它只有四種顏色,所以只需要四個調色盤條目。這些調色盤條目是從一個大的顏色空間中繪製的,但是這裡的每個畫素都可以用2位表示。
這種“調色盤”的影象畫素壓縮得相當多。原則上每個畫素需要3個位元組來描述真彩色格式的影象,但是通過這種方法對影象進行調色盤化,16色影象的大小可以立即縮小6倍,因為每個畫素只需要4位。更清晰的顏色意味著更少的壓縮,但是256色是相當多的,甚至一個256色調色盤也代表了3:1的壓縮比。
GIF允許更復雜的壓縮,因為它允許編碼器將影象分割成單獨的塊,每個塊都可能有自己的調色盤,例如在左上象限中最常出現的具有256種顏色的影象,可以將此象限指定為自己的塊,具有和其他象限不同的單獨的調色盤。事實證明,沒有人利用這種超壓縮能力,而是改造它,以允許GIF動畫。我稍後再談這個。
GIF通過將LZW壓縮演算法應用於這些調色盤索引,進一步壓縮索引資料本身。它以一種相當粗糙的方式實現——索引本身被視為一個長的線性位元組序列,LZW被應用到位元組本身。這種方法沒有考慮到這樣一個事實,即,任何給定行的第一個畫素與前一行的第一個畫素,比上一行的最後一個畫素更有可能與前一行的第一個畫素相同(LZW演算法就是這樣看待畫素的)。
示例2:100畫素寬的GIF
在示例2中,畫素1和101比畫素100和101更有可能是相同的,但是LZW演算法將看到畫素101在100之後。儘管如此,LZW演算法最終提供了相當好的壓縮效果,它識別出單個行中的相似點,並重復從一行到下一行的模式。
與任何檔案格式一樣,GIF以標準化的標頭檔案開頭:
當然,ID標籤是一個標準的3位元組標籤,是三個ASCII位元組(G、I和F),它將檔案標識為GIF檔案。下面的3個位元組是版本——定義了兩個版本:87a和89a。你可能永遠不會遇到87aGIF(是的,'87和'89指的是規範釋出的年份)。寬度和高度緊隨其後:它們以兩個位元組的形式給出,以小Endian格式,因此最大可能的GIF是2 16 =65536 x65536畫素,這仍然遠遠大於任何可用的計算機顯示器的解析度(但我一直保留著希望……)。
下一個位元組是“欄位”位元組。欄位位元組被分解為(從最高有效位到最低有效位)如下:
Line"/>
W:標記全域性顏色表的存在。回想一下,GIF允許編碼器將影象細分為更大的壓縮塊,每個塊都有自己的顏色表,或者,可以給出一個所有塊都使用的單一“全域性”顏色表,如果設定了這個位,則標題後面立即是一個全域性顏色表。你會發現,幾乎所有的GIF都包含一個全域性顏色表。
X:顏色解析度位數減1。這是顯示單元被認為能夠支援的顏色數的基數為2的對數——這與實際GIF中的顏色數不一樣。如果顯示器只能顯示16種顏色,而GIF表示它期望顯示器支援256種,那麼軟體很可能就會中止該顯示器。現在,這個值是資訊性的(充其量)。
Y:“排序”標誌。為了理解排序標誌的效用,假設一個只能夠顯示16種顏色的顯示器顯示了一個64色的GIF。GIF編碼器可以使低解析度顯示器通過對調色盤按顏色的頻率排序來近似顯示(可能完全跳過其他顏色或試圖找到它們最接近的匹配)。如果編碼器這樣做,則設定此標誌。如今,這面標誌的設定並不是特別重要,因為16色顯示器已經相當少了。
z:全域性顏色表中的位數減1。因此,全域性顏色表包含2z+1項。
背景顏色索引:如果沒有指定由該GIF描述的矩形區域中的任何畫素,則將其設定為顏色。你可能會問,如何將其不設定畫素值?畢竟,GIF本身被描述為一個矩形。然而,回想一下,GIF允許影象被細分成更小的塊,它們本身不一定佔據整個影象顯示區域。如果由於某種原因,左上和右下象限被定義,但是塊的其餘部分不是,則一些畫素將被取消設定。
畫素長寬比:同樣,這也是資訊性的(充其量),這告訴解碼器在原始影象中畫素寬度與高度的比值是多少。沒有描述解碼器如何利用這些資訊,我不知道有哪個GIF解碼器會對此給予任何關注。
然後,可以像以下清單1所示那樣解析GIF標頭:
typedef struct { unsigned short width; unsigned short height; unsigned char fields; unsigned char background_color_index; unsigned char pixel_aspect_ratio; } screen_descriptor_t; /** * @param gif_file the file descriptor of a file containing a *GIF-encoded file.This should point to the first byte in *the file when invoked. */ static void process_gif_stream( int gif_file ) { unsigned char header[ 7 ]; screen_descriptor_t screen_descriptor; int color_resolution_bits; // A GIF file starts with a Header (section 17) if ( read( gif_file, header, 6 ) != 6 ) { perror( "Invalid GIF file (too short)" ); return; } header[ 6 ] = 0x0; // XXX there's another format, GIF87a, that you may still find // floating around. if ( strcmp( "GIF89a", header ) ) { fprintf( stderr, "Invalid GIF file (header is '%s', should be 'GIF89a')\n", header ); return; } // Followed by a logical screen descriptor // Note that this works because GIFs specify little-endian order; on a // big-endian machine, the height & width would need to be reversed. // Can't use sizeof here since GCC does byte alignment; // sizeof( screen_descriptor_t ) = 8! if ( read( gif_file, &screen_descriptor, 7 ) < 7 ) { perror( "Invalid GIF file (too short)" ); return; } color_resolution_bits = ( ( screen_descriptor.fields & 0x70 ) >> 4 ) + 1;
如果欄位指示GIF包含一個全域性顏色表,則它緊跟在標頭之後。大小是由標誌給出的,它指示全域性顏色表包含了多少個3位元組的條目,每個標記分別指示每個索引的紅色、綠色和藍色的強度。以下清單2演示了全域性顏色表的解析:
typedef struct { unsigned char r; unsigned char g; unsigned char b; } rgb; ... static void process_gif_stream( int gif_file ) { unsigned char header[ 7 ]; screen_descriptor_t screen_descriptor; int color_resolution_bits; int global_color_table_size;// number of entries in global_color_table rgb *global_color_table; ... if ( screen_descriptor.fields & 0x80 ) { int i; // If bit 7 is set, the next block is a global color table; read it global_color_table_size = 1 << ( ( ( screen_descriptor.fields & 0x07 ) + 1 ) ); global_color_table = ( rgb * ) malloc( 3 * global_color_table_size ); // XXX this could conceivably return a short count... if ( read( gif_file, global_color_table, 3 * global_color_table_size ) < 3 * global_color_table_size ) { perror( "Unable to read global color table" ); return; } }
這就結束了GIF檔案的標題部分。標題後面是一系列塊,每個塊用一個位元組的塊型別識別符號標出。其中最重要的是影象描述符塊0x2C,這是儲存實際影象索引的地方。其次是掛載塊0x3B,它指示一個GIF的結束——如果在檔案結束之前沒有遇到這個掛載塊,則該檔案以某種方式損壞。
因此,一旦解析了主標題和全域性顏色表(如果有的話),接下來要做的就是開始查詢塊,並根據它們的型別解析它們,如下列清單3所示:
#define IMAGE_DESCRIPTOR0x2C #define TRAILER0x3B ... static void process_gif_stream( int gif_file ) { unsigned char header[ 7 ]; screen_descriptor_t screen_descriptor; int color_resolution_bits; int global_color_table_size;// number of entries in global_color_table rgb *global_color_table; unsigned char block_type = 0x0; ... while ( block_type != TRAILER ) { if ( read( gif_file, █_type, 1 ) < 1 ) { perror( "Invalid GIF file (too short)" ); return; } switch ( block_type ) { case IMAGE_DESCRIPTOR: if ( !process_image_descriptor( gif_file, global_color_table, global_color_table_size, color_resolution_bits ) ) { return; } break; case TRAILER: break; ... default: fprintf( stderr, "Bailing on unrecognized block type %.02x\n", block_type ); return; } } }
影象描述符塊本身以一個9位元組的頭結構開始:
左端,頂部,寬度、高度表明在整個影象的上下文中這個塊的起點和終點。記住,GIF允許幾個塊組成一個完整的影象。
這些欄位又被分成以下幾個部分:
W:本地顏色表標誌。
X:交錯標誌。
Y:排序標誌。
Z:區域性顏色表的大小。
第3項和第4項為“保留”且沒有定義,它們應該始終設定為0。
本地顏色表、排序標誌和本地顏色表的大小的解析與全域性顏色表相同,本地顏色表的解析與全域性顏色表的解析完全相同,這裡不再重複。不管怎樣,你會發現很少有.gif檔案具有帶有本地顏色表的影象描述符。
在全域性標頭定義的標誌位元組中沒有類似項的一個位是隔行(interlace)標誌。要記住,GIF是針對慢速顯示硬體進行優化的,隔行掃描背後的理念是,顯示器可以在解析過程的早期以粗糙的格式顯示影象,並逐步顯示越來越多的細節,直到影象完全處理完畢。因此,如果設定了隔行標誌,則影象中的行將不按順序排列:首先,每隔8行;接下來,從每8行的第4行開始;然後,從每4行的第2行開始;最後每隔一行,完成影象。
示例3:26行GIF的交錯
示例3說明了如何儲存交錯檔案。解釋來說就是,綠色標記的行將被讀取和顯示,首先是黃色標記的行,然後是藍色標記的行,最後是所有剩餘的行。儘管如此,即使在今天,你還是會發現網際網路上漂浮著一些交錯的GIF檔案。
忽略LCT的解析,解析影象描述符頭,如下列清單4所示:
typedef struct { unsigned short image_left_position; unsigned short image_top_position; unsigned short image_width; unsigned short image_height; unsigned char fields; } image_descriptor_t; ... static int process_image_descriptor( int gif_file, rgb *gct, int gct_size, int resolution_bits ) { image_descriptor_t image_descriptor; int disposition; // TODO there could actually be lots of these if ( read( gif_file, ℑ_descriptor, 9 ) < 9 ) { perror( "Invalid GIF file (too short)" ); disposition = 0; goto done; } // TODO if LCT = true, read the LCT ... disposition = 1; done: return disposition; }
在影象描述符頭之後,在邏輯上仍然包含在影象描述符塊本身中,這是一系列的資料子塊。這些資料子塊是活動顏色表中的LZW壓縮索引,如果影象描述符塊包含一個區域性顏色表,則這些是本地顏色表中的索引,否則,它們是全域性顏色表中的索引。資料子塊本身被進一步細分為255個位元組的塊,每個塊宣告其長度為它的第一個單位元組。顯然,由於單個GIF可以是655356×65536=4294967296畫素(理論上來說),所以幾乎所有的影象描述符都將由多個數據子塊組成,即使它們是壓縮的。然而,解碼器目前沒有足夠的資訊來判斷有多少位元組的壓縮資料會跟隨而來。因此,解碼器必須轉而繼續讀取資料子塊,直到遇到一個零長度子塊,表示資料的壓縮結束。
這些壓縮資料的子塊可以讀入記憶體,如以下清單5所示。在這裡,我選擇通過反覆呼叫realloc來將整個混亂的東西塞進記憶體,任何值得認真對待的GIF實現都會在讀取資料時對其進行解壓縮,但這種實現更容易理解。
static int read_sub_blocks( int gif_file, unsigned char **data ) { int data_length; int index; unsigned char block_size; // Everything following are data sub-blocks, until a 0-sized block is // encountered. data_length = 0; *data = NULL; index = 0; while ( 1 ) { if ( read( gif_file, █_size, 1 ) < 1 ) { perror( "Invalid GIF file (too short): " ); return -1; } if ( block_size == 0 )// end of sub-blocks { break; } data_length += block_size; *data = realloc( *data, data_length ); // TODO this could be split across block size boundaries if ( read( gif_file, *data + index, block_size ) < block_size ) { perror( "Invalid GIF file (too short): " ); return -1; } index += block_size; } return data_length; } static int process_image_descriptor( int gif_file, rgb *gct, int gct_size, int resolution_bits ) { int compressed_data_length; unsigned char *compressed_data = NULL; ... if ( read( gif_file, ℑ_descriptor, 9 ) < 9 ) { perror( "Invalid GIF file (too short)" ); disposition = 0; goto done; } compressed_data_length = read_sub_blocks( gif_file, &compressed_data ); ... done: if ( compressed_data ) free( compressed_data ); return disposition; }
此時,壓縮資料完全包含在由COMPUT_DATA指定的緩衝區中,下一件要做的事就是解壓縮它。但是,要使用壓縮的GIF資料,需要做一些修改:
1.使用strcmp和strcat來實現LZW字典對GIF索引不起作用,因為它們包含嵌入零點,C的字串操作將其解釋為“字串結尾”。
2.該實現假設壓縮資料從8位程式碼開始,並在字典填充時擴充套件到9位。實際上,GIF允許可變的起始大小,一直到4位程式碼.。
要解決第一個問題,必須對字典本身進行改造。字典不是字串陣列,而是作為指標結構實現的。當遇到一個新的字典索引時,它是通過在最近的字典條目之上連線(使用strcat)最近讀取的程式碼來建立的。更靈活的方法是定義一個結構,允許每個條目指向其前身,如下述清單6所示:
typedef struct { unsigned char byte; int prev; int len; } dictionary_entry_t;
現在,由5個0x0位元組組成的字串將在記憶體中表示為:
這些結構中的每一個都將由一個字典條目來指示,字典的索引(現在是指向DECUDY_ENTITY_t的指標陣列)是從檔案中讀取的擴充套件程式碼。所以,如果前五個位元組都是0(這很常見),並且全域性顏色表包含64個條目:
在這裡,長度宣告只是一種便利,它不是絕對必要的(大多數GIF實現都省略了它,以加快解壓縮速度)。如果在資料流中遇到程式碼69,解壓縮器將查詢字典[69],發現它的位元組是0,發出0,然後跟蹤反向指標&Dictory[68],發出另一個0,依此類推。但是,這些條目是以“向後”的方式構建的,因此有必要以相反的順序發出它們。大多數GIF實現將位元組推送到堆疊上,然後將其彈出,以執行實際的解壓縮,與之相反的是,我會跟蹤長度,這樣我就知道在發出第一段程式碼時,輸出緩衝區中要向前跳轉多少位元組。
一旦這個被平方,解決第二個問題——可變大小的LZW程式碼的問題——就很簡單了:只需將起始程式碼大小傳遞到解壓縮例程中即可。
清單7是完成的GIF友好解壓縮例程。請注意,有一個12位程式碼的硬編碼限制,一旦字典擴充套件到212=4096項,就不允許再增長了,由編碼器來決定活動字典是否仍然在壓縮方面做得很好,或者它是否應該傳送一個清晰的程式碼,然後從頭開始。
int uncompress( int code_length, const unsigned char *input, int input_length, unsigned char *out ) { int maxbits; int i, bit; int code, prev = -1; dictionary_entry_t *dictionary; int dictionary_ind; unsigned int mask = 0x01; int reset_code_length; int clear_code; // This varies depending on code_length int stop_code;// one more than clear code int match_len; clear_code = 1 << ( code_length ); stop_code = clear_code + 1; // To handle clear codes reset_code_length = code_length; // Create a dictionary large enough to hold "code_length" entries. // Once the dictionary overflows, code_length increases dictionary = ( dictionary_entry_t * ) malloc( sizeof( dictionary_entry_t ) * ( 1 << ( code_length + 1 ) ) ); // Initialize the first 2^code_len entries of the dictionary with their // indices.The rest of the entries will be built up dynamically. // Technically, it shouldn't be necessary to initialize the // dictionary.The spec says that the encoder "should output a // clear code as the first code in the image data stream".It doesn't // say must, though... for ( dictionary_ind = 0; dictionary_ind < ( 1 << code_length ); dictionary_ind++ ) { dictionary[ dictionary_ind ].byte = dictionary_ind; // XXX this only works because prev is a 32-bit int (> 12 bits) dictionary[ dictionary_ind ].prev = -1; dictionary[ dictionary_ind ].len = 1; } // 2^code_len + 1 is the special "end" code; don't give it an entry here dictionary_ind++; dictionary_ind++; // TODO verify that the very last byte is clear_code + 1 while ( input_length ) { code = 0x0; // Always read one more bit than the code length for ( i = 0; i < ( code_length + 1 ); i++ ) { // This is different than in the file read example; that // was a call to "next_bit" bit = ( *input & mask ) ? 1 : 0; mask <<= 1; if ( mask == 0x100 ) { mask = 0x01; input++; input_length--; } code = code | ( bit << i ); } if ( code == clear_code ) { code_length = reset_code_length; dictionary = ( dictionary_entry_t * ) realloc( dictionary, sizeof( dictionary_entry_t ) * ( 1 << ( code_length + 1 ) ) ); for ( dictionary_ind = 0; dictionary_ind < ( 1 << code_length ); dictionary_ind++ ) { dictionary[ dictionary_ind ].byte = dictionary_ind; // XXX this only works because prev is a 32-bit int (> 12 bits) dictionary[ dictionary_ind ].prev = -1; dictionary[ dictionary_ind ].len = 1; } dictionary_ind++; dictionary_ind++; prev = -1; continue; } else if ( code == stop_code ) { if ( input_length > 1 ) { fprintf( stderr, "Malformed GIF (early stop code)\n" ); exit( 0 ); } break; } // Update the dictionary with this character plus the _entry_ // (character or string) that came before it if ( ( prev > -1 ) && ( code_length < 12 ) ) { if ( code > dictionary_ind ) { fprintf( stderr, "code = %.02x, but dictionary_ind = %.02x\n", code, dictionary_ind ); exit( 0 ); } // Special handling for KwKwK if ( code == dictionary_ind ) { int ptr = prev; while ( dictionary[ ptr ].prev != -1 ) { ptr = dictionary[ ptr ].prev; } dictionary[ dictionary_ind ].byte = dictionary[ ptr ].byte; } else { int ptr = code; while ( dictionary[ ptr ].prev != -1 ) { ptr = dictionary[ ptr ].prev; } dictionary[ dictionary_ind ].byte = dictionary[ ptr ].byte; } dictionary[ dictionary_ind ].prev = prev; dictionary[ dictionary_ind ].len = dictionary[ prev ].len + 1; dictionary_ind++; // GIF89a mandates that this stops at 12 bits if ( ( dictionary_ind == ( 1 << ( code_length + 1 ) ) ) && ( code_length < 11 ) ) { code_length++; dictionary = ( dictionary_entry_t * ) realloc( dictionary, sizeof( dictionary_entry_t ) * ( 1 << ( code_length + 1 ) ) ); } } prev = code; // Now copy the dictionary entry backwards into "out" match_len = dictionary[ code ].len; while ( code != -1 ) { out[ dictionary[ code ].len - 1 ] = dictionary[ code ].byte; if ( dictionary[ code ].prev == code ) { fprintf( stderr, "Internal error; self-reference." ); exit( 0 ); } code = dictionary[ code ].prev; } out += match_len; } }
我不想詳細討論這是如何工作的,但是等等,解壓縮的呼叫方如何知道解壓縮程式應該讀取多少位?GIF實際上要求影象描述符的資料區域的第一個位元組(在資料子塊本身之前)是一個位元組,指示第一個程式碼的長度。
unsigned char lzw_code_size; ... if ( read( gif_file, &lzw_code_size, 1 ) < 1 ) { perror( "Invalid GIF file (too short): " ); disposition = 0; goto done; } compressed_data_length = read_sub_blocks( gif_file, &compressed_data );
通過定義解壓縮,並且已知起始程式碼長度,影象描述符處理器現在可以對索引資料進行解壓縮。
int uncompressed_data_length = 0; unsigned char *uncompressed_data = NULL; ... compressed_data_length = read_sub_blocks( gif_file, &compressed_data ); uncompressed_data_length = image_descriptor.image_width * image_descriptor.image_height; uncompressed_data = malloc( uncompressed_data_length ); uncompress( lzw_code_size, compressed_data, compressed_data_length, uncompressed_data ); disposition = 1; ... done: if ( compressed_data ) free( compressed_data ); if ( uncompressed_data ) free( uncompressed_data ); return disposition; }
就是這樣,顯示影象現在是一個索引的顏色表和反交錯的資料行(如有必要的話)。所以,你可能會理所當然的想知道,為什麼有一個以上的塊型別定義時,影象描述符塊型別包含整個影象。GIF'87標準認識到該規範可能需要再擴充套件,因此包含了一個特殊的塊型別0x21以允許擴充套件。擴充套件塊首先包括一個指示其描述的擴充套件型別的位元組,下一個位元組指示擴充套件的長度——通過將擴充套件的長度作為擴充套件的標準部分,不能識別擴充套件的解碼器可以跳過擴充套件,但仍然會正確的處理檔案。當然,擴充套件的剩餘位元組是特定於擴充套件的型別的。所有擴充套件後面都有資料子塊,其格式與影象資料本身中的資料塊完全相同——如果擴充套件沒有任何關聯資料,則後跟單個0位元組的資料子塊。
GIF'87沒有定義任何擴充套件,但GIF'89編寫了四個擴充套件:純文字、註釋、特定於應用程式和圖形控制元件擴充套件。
·註釋擴充套件:這種擴充套件很少使用,它允許將可變長度的文字作為只讀註釋包含在GIF流中,這個註釋不會顯示在螢幕上,也沒有明確說明使用者如何能檢視到它。
· 文字擴充套件:與註釋擴充套件一樣,該擴充套件允許在GIF中包含文字資料。但是,與註釋擴充套件不同的是,此文字資料應該被覆蓋在此影象的頂部,並作為其顯示的一部分。編碼器無法指定字型資料(事實上,這個擴充套件只允許固定寬度的字型,不允許可變寬度的字型),這也是非常罕見的。
· 特定於應用程式:此擴充套件允許將任意資料填充到GIF中,它由標識應用程式的8個位元組和表示版本的3個位元組組成。顯然,子塊中後面的資料依賴於應用程式ID。
· 最後也是最複雜的一個擴充套件型別是圖形控制擴充套件(graphic control extension)。請記住,我提到沒有人使用GIF的多塊功能來對影象檔案進行超壓縮——相反,這用於建立動畫影象檔案。雖然GIF87a規範宣告,當遇到多個影象塊時,“影象之間沒有暫停,每個影象都被處理器立即處理”,大多數GIF解碼器用延遲覆蓋影象以創造動畫的幻覺。由於當時的軟體開始時相當慢,我懷疑這種動畫能力更像是意外,但它變得非常普遍,以至於現在有相當多的人相信GIF就是動畫GIF。
問題是,沒有標準的方法來表明這些動畫的幀之間應該經過多長時間 。GIF89接受了這一點並將其與圖形控制元件擴充套件編碼。這種擴充套件很常見,不僅僅是動畫GIF,而且你會一直遇到這些。圖形控制元件擴充套件以位元組位元組開頭:
r r x x x y z
r:保留,設定為0。
x:處理方法。這表示解碼器在單獨幀的顯示之間應該做什麼、是否應該空出顯示區域。
y:使用者輸入標誌。如果未設定,GIF動畫應自動顯示;否則,預期使用者將在動畫幀之間單擊。
z:透明度,如果是真,那麼擴充套件的最後一個位元組是透明索引。
標誌位元組之後是幀之間的2位元組延遲時間(以百分之一秒為單位)和單位元組透明度索引,記住不能覆蓋此索引,因為我們要使得底層框架可以“顯示”。
要處理這些擴充套件型別,請新增新的塊型別,如下述清單10所示:
#define EXTENSION_INTRODUCER0x21 #define IMAGE_DESCRIPTOR0x2C ... static void process_gif_stream( int gif_file ) { ... switch ( block_type ) { case EXTENSION_INTRODUCER: if ( !process_extension( gif_file ) ) { return; } break; case IMAGE_DESCRIPTOR:
然後處理擴充套件自身,如下述清單11所示:
#define EXTENSION_INTRODUCER0x21 #define IMAGE_DESCRIPTOR0x2C #define TRAILER0x3B #define GRAPHIC_CONTROL0xF9 #define APPLICATION_EXTENSION 0xFF #define COMMENT_EXTENSION0xFE #define PLAINTEXT_EXTENSION0x01 typedef struct { unsigned char extension_code; unsigned char block_size; } extension_t; typedef struct { unsigned char fields; unsigned short delay_time; unsigned char transparent_color_index; } graphic_control_extension_t; typedef struct { unsigned char application_id[ 8 ]; unsigned char version[ 3 ]; } application_extension_t; typedef struct { unsigned short left; unsigned short top; unsigned short width; unsigned short height; unsigned char cell_width; unsigned char cell_height; unsigned char foreground_color; unsigned char background_color; } plaintext_extension_t; static int process_extension( int gif_file ) { extension_t extension; graphic_control_extension_t gce; application_extension_t application; plaintext_extension_t plaintext; unsigned char *extension_data = NULL; int extension_data_length; if ( read( gif_file, &extension, 2 ) < 2 ) { perror( "Invalid GIF file (too short): " ); return 0; } switch ( extension.extension_code ) { case GRAPHIC_CONTROL: if ( read( gif_file, &gce, 4 ) < 4 ) { perror( "Invalid GIF file (too short): " ); return 0; } break; case APPLICATION_EXTENSION: if ( read( gif_file, &application, 11 ) < 11 ) { perror( "Invalid GIF file (too short): " ); return 0; } break; case 0xFE: // comment extension; do nothing - all the data is in the // sub-blocks that follow. break; case 0x01: if ( read( gif_file, &plaintext, 12 ) < 12 ) { perror( "Invalid GIF file (too short): " ); return 0; } break; default: fprintf( stderr, "Unrecognized extension code.\n" ); exit( 0 ); } // All extensions are followed by data sub-blocks; even if it's // just a single data sub-block of length 0 extension_data_length = read_sub_blocks( gif_file, &extension_data ); if ( extension_data != NULL ) free( extension_data ); return 1; }
當然,在這種情況下,我所做的只是解析資料,我不是在解釋它或用它做任何事情。請注意,在這種情況下,我正在硬編碼每個擴充套件結構的長度——嚴格來說,這是不必要的,因為長度由擴充套件標頭本身中的一個位元組指示。我也終止了一個無法識別的擴充套件,違反了規範,其實我應該跳過它們的。下一步是將索引擴充套件為實際影象並顯示它,同時遵守透明度和動畫規範,我會把它留給真正的GIF解碼器。到現在為止,你應該已經非常瞭解GIF影象格式的來龍去脈了。
參考文獻:
1.特里韋爾奇“高效能資料壓縮技術”,IEEE計算機,1984年6月。
2.GIF'89規範。
本文的原始碼:
ofollow,noindex">http://commandlinefanatic.com/gif.c