GIF 格式解析之表情包是如何動起來的

ES2049發表於2022-02-11

前言

“沒有表情包怎麼聊天?”

作為破除人類交流困境的神器,沒有什麼場景是一張表情包不能表達的。想像一下,當你正同時開啟 N 個 VSCode 瘋狂打碼的時候,DING~ 的一聲脆響,產品經理髮來一條訊息:昨天提的那幾個 bug 修復好了嗎?​

基於「能發圖就不打字」的原則,是時候祭出收藏夾中了大殺器了↓

DOING.gif

簡單的動圖創造了進退自如的交流空間,這些動圖就是我們通常使用的 GIF 圖。

業務背景

然而,在風控場景下,黑灰產利用 GIF 圖片多幀的特性,把非法圖片注入其中,再通過手動修改副檔名的方式,偽裝成普通圖片,無疑給風險防控增加了難度。一閃而過的非法主圖,讓運營小二防不勝防;圖片最終定格在看似沒什麼問題的尾幀上,讓運營小二難以捕捉到有效的關鍵資訊。

原圖.jpg

GIF 圖片雖然有“動”的性質,但在 Web 中被一視同仁地做為圖片處理,沒有提供任何特殊待遇 API,所以無法控制 GIF 圖片的播放、暫停、結束監聽等事件那麼有沒有辦法能讓一閃而過且定格在尾幀的 GIF 圖“動”起來呢?接下來我們深入探究,剖析一下 GIF 圖裡都有神馬。

GIF 格式介紹

影像互換格式(Graphics Interchange Format)簡稱 GIF,是一種點陣圖形檔案格式,以 8 位色(即 256 種顏色)重現真彩色的影像,GIF檔案內部分成許多儲存塊,用來儲存多幅圖象或者是決定圖象表現行為的控制塊,用以實現動畫和互動式應用。GIF 具有 GIF87a 和 GIF89a 兩個版本。

GIF 是一種點陣圖。點陣圖的大致原理是:圖片由許多的畫素組成,每一個畫素都被指定了一種顏色,這些畫素綜合起來就構成了圖片。8 位的「位」即顏色深度,顏色深度由一個影像的位深決定,簡單來說就是最多支援多少種顏色(舉個例子,位深為 1 的畫素有兩個值:黑和白。位深越大,影像可包含的顏色越多,顏色表現越準確,8 位 GIF 圖最多包含 256 種顏色)​

GIF87a 版本是 1987 年推出的,一個檔案儲存一個影像,嚴格不支援透明畫素;GIF87a 採用 LZW 壓縮演算法,它能夠在保持影像質量的前提下將影像尺寸壓縮百分之二十到二十五。​

GIF89a 版本是 1989 年推出的很有特色的版本,該版本允許一個檔案儲存多個影像,可實現動畫功能,允許某些畫素透明。這個版本中,為 GIF 文件擴充了圖形控制區塊、備註、說明、應用程式程式設計介面 4 個區塊,並提供了對透明色和多幀動畫的支援,如果將這些影像連續播放出來,就能夠組成最簡單的動畫。所以常被用來儲存“動態圖片”,通常時間短,體積小,內容簡單,成像相對清晰。在現在我們所說的 GIF 一版都是 89a 的格式。

GIF 檔案結構拆解

想要知道圖片是如何“動”起來的,首先了解它是如何儲存的。我們引用網路上的一張圖,來看看 GIF 格式的影像檔案結構:

image.png

圖片來源:What's In A GIF

GIF 格式的檔案按塊儲存,整體上分為三部分:

  • 檔案頭(Header)
  • GIF 資料流(GIF Data Stream)
  • 檔案結尾(Trailer)

其中,資料流中的文字擴充套件塊、應用擴充套件塊和註釋擴充套件塊我們跳過不看,讓圖片“動”起來的祕訣就存在於 圖形控制擴充套件(Graphic Control Extension) 中。下面讓我們用一個栗子來一探究竟吧。

樣例準備

樣例圖片

開啟可見圖片會有瞬間閃爍效果。

十六進位制轉換器

傳送門

檔案頭

識別一張圖是不是 GIF 並不只看圖片擴充套件格式或者圖片是否會動,GIF 檔案的前 6 個位元組內容是 GIF 的署名和版本號,通過控制檯列印我們可以得到:

image.png對照 ASCII 編碼我們可以得到 47 49 46 38 39 61 對應 GIF 89a ​

簡單!繼續往下看↓

GIF 資料流

圖形控制擴充套件

我們通過觀察不難發現,圖片會有瞬間閃爍的效果,對比文章開頭表情包圖,為什麼有些 GIF 圖可以一直迴圈播放,有些卻是瞬間閃爍然後定格在第二幀呢?​

在 89a 版本,GIF 新增了圖形控制擴充套件塊,放在影像識別符號(Image Descriptor)的前面,用來控制緊跟在它後面的第一個圖象的顯示,圖形控制擴充套件塊的結構如下圖所示:

image.png

由上圖可見,整個擴充套件塊結構如下:

描述長度
擴充套件塊識別符號1 位元組、固定值 0x21
擴充套件塊標識1 位元組、固定值 0xF9
擴充套件塊子塊長度1 位元組
保留位3 位
處置方法3 位
使用者輸入標誌1 位
透明顏色標誌1 位
延遲時間2 位元組
透明顏色索引1 位元組
擴充套件塊尾1 位元組、固定值 0x00

找到它了!罪魁禍首就是延遲時間!延遲時間標記了需要暫停這個延遲時間後再繼續往下處理資料流,這裡可以理解為動圖中每一幀的停留時間,其單位為 1/100 秒。​

分析到這裡,有種茅塞頓開的感覺,回到程式碼中,我們通過控制檯可以看到原圖解析出來的資料是這樣的:

image.png

延遲時間:00 00,十六進位制轉換十進位制為:0​

我們通過手動設定延遲時間,就可以讓一閃而過的圖片“動”起來:

image.png

手動修改後的延遲時間:32 00,十六進位制轉換十進位制為:800

核心程式碼如下:

let p = 0; // 當前 Buffer 處理對應的下標

while (notEndOfFile && p < contentBuffer.length) {
  ...
  
    switch (contentBuffer[p++]) {
    case 0xf9:  // Graphics Control Extension
      if (contentBuffer[p++] !== 0x4 || contentBuffer[p+4] !== 0)
        throw new Error("Invalid graphics extension block.");
      p++; // graphicPackedFiled
      if (delay) {
        const delayArr = numberToByteArr(delay);
        contentBuffer[p] = delayArr[delayArr.length - 1];
        contentBuffer[p+1] = delayArr[delayArr.length - 2] || 0;
      }
      p = p + 4; // 略過 delay 2 位元組, transparentIndex 1 位元組,結束符號 1位元組
      break;
  }
}

檔案結尾

image.png

當所有子影像資料解析完畢,就會遇到檔案尾,這一部分只有一個值為 0 的位元組,標識一個 GIF 檔案結束。檔案尾固定為 0x3B

寫在最後

在上一篇解決圖片跨域的文章中筆者有介紹,藉助團隊 Serverless 能力搭建圖片跨域轉發伺服器,本次的 GIF 檔案解析方案是在原有的 BFF 層基礎能力之上搭建的。​

Octopus 圖片轉發服務詳細資訊

請求地址:https://xxx.fc.alibaba-inc.com/gifTransformer
請求方法:GET
引數:
url: 必傳,需要解析的圖片地址
loop: 非必傳,GIF 圖迴圈次數
delay: 非必傳,GIF 圖每一幀播放時間(ms)

返回結果:解析後的 GIF 圖

GIF 圖解析最終落地風險排查業務,解決了業務一直頭痛的黑灰產非法主圖判定難的問題,有興趣的同學不妨上手嘗試一下。

參考連結

作者:ES2049 | 黑眼豆豆

文章可隨意轉載,但請保留原文連結。
非常歡迎有激情的你加入 ES2049 Studio,簡歷請傳送至 caijun.hcj@alibaba-inc.com

相關文章