JS實現獲取GIF總幀數的方法詳解

前言

有一個Gif圖片,我們想要獲取它的總幀數,超過一定幀數的圖片告知用戶不可上傳,在服務端有很多現成的庫可以使用,這種做法不是很友好,前端需要先將gif上傳至服務端,服務端解析完畢後將結果返回,大大降低瞭用戶體驗。

那麼如何通過js在上傳前就拿到它的總幀數來判斷呢?本文就跟大傢分享一種解決方案,並將其封裝成插件發佈至npm倉庫,歡迎各位感興趣的開發者閱讀本文。

寫在前面

此插件已經發佈至npm,采用原生JS編寫支持任意一個前端框架,如果你對其實現原理不感興趣,隻是想拿來解決你的實際問題,可以直接通過npm/yarn來安裝,命令如下:

# yarn安裝
yarn add gif-parser-web

# npm安裝
npm install gif-parser-web --save

文檔地址請移步:README.md

思路分析

我們都知道無論什麼文件在計算機中都是以流的形式進行存儲的,因此我們可以通過讀取文件流來拿到它的所有信息。Gif類型的文件也是如此,我們隻要能知道它的文件流結構就可以根據它的規則進行解析讀取瞭。

什麼是Gif

Gif的全稱是Graphics Interchange Format,是一種位圖,以8位色重現真彩色的圖像。采用LZW壓縮算法進行編碼,可以有效的減少圖像文件在網上的傳輸時間,我們在網站上看到的會動的表情包,基本上都是Gif格式的。

組成結構

正如上面所說,我們想解析gif就得先知道它的文件流結構,在What's In A GIF網站中我們知道瞭它是由多種不同類型的塊組成,如下所示:

  • 未標記塊:Header(文件頭)、Logical Screen Descriptor(邏輯屏幕描述符)、Global Color Table(全局顏色表)、局部顏色表(Local Color Table)
  • 控制塊:圖形控制擴展(Graphics Control Extension)
  • 圖形渲染塊:純文本擴展(Plain Text Extension)、圖像描述符(Image Descriptor)
  • 特殊用途塊:應用擴展( Application Extension)、註解擴展(Comment Extension)、數據流結束標記(Trailer)
  • 圖像數據塊:圖像數據(Image Data)

解析原理

瞭解完gif的組成結構後,接下來我們來看下如何獲取它的數據流,如下所示:

  • 讀取Gif圖片文件(從url讀取或者從本地上傳的File類型的數據)
  • 將讀取到的數據轉成arrayBuffer
  • arrayBuffer放到DataView
  • 使用DataView底層的相關API來讀取十六進制編碼
  • 對十六進制編碼進行解碼,獲取圖像的信息

它的解碼過程如下圖所示:

  • 從Header開始順著箭頭一直讀到PlainTextExtension完成第一幀的讀取,其中GlobalColorTable、ApplicationExtension、CommentExtension、LocalColorTable、PlainTextExtension不一定存在
  • 接下來重復GraphicControlExtension、ImageDescriptor、ImageData 讀取剩下的幀圖片數據
  • 直至讀取到Trailer標識,就完成瞭整個Gif的讀取

註意:在讀取過程中,每個塊都有自己特殊的編碼標記。

數據塊分析

我們瞭解完gif的構成後,接下來我們來看下每一個具體的數據塊的編碼信息。

Header Block

該數據塊用於標記數據流的開始,位於文件頭數據流的上下文內,裡面包含瞭gif的簽名與版本信息,它是必須存在的且隻有一個。

該塊在數據流中占6個字節,其中簽名與版本信息各占3個字節,即:

  • 數據流的0-2位置的元素一定表示gif的簽名信息
  • 數據流的3-5位置的元素一定表示gif的版本信息

我們以89a格式的gif為例,它的Header信息就如下所示:

  • Signature的16進制值為47、49、46,將其轉換為Unicode編碼字符後就為:"G"、"I"、"F"
  • Version的16進制值為38、39、61,將其轉換為Unicode編碼字符後就為:"8"、"9"、"a"

我們來看下如何用代碼來讀取。

// 假設我們已經得到瞭dataView
const signature = dataView.getUint16(0); // 使用getUint16方法從0號位置開始連續獲取2個字節的值,轉換成轉換為Unicode編碼為:G I
const version = dataView.getUint16(2); // 使用getUint16方法從2號位置開始連續獲取2個字節的值,轉換成轉換為Unicode編碼為:F 8

Logical Screen Descriptor

該數據塊中定義瞭圖像在設備中顯示所需的參數,位於Header數據塊的後面,它是必須存在的且隻有一個,其值的坐標是相對於虛擬屏幕左上角計算出來的。

該塊在數據流中占7個字節,包含的信息如下所示:

  • Canvas Width 圖片的寬度(以像素為單位),占2個字節空間。
  • Canvas Height 圖片的高度(以像素為單位),占2個字節空間。
  • Packed Fields 壓縮字段,占1字節空間,裡面包含4個值
    • Global Color Table Flag 全局顏色標記,用於標識全局顏色表。如果值為0則表示不存在全局顏色塊;如果值為1則表示全局顏色塊緊跟於此塊之後。
    • Color Resolution 顏色分辨率,即顏色的位數,有1位、8位、16位、32位等。在gif格式的圖像定義中,它的顏色不能超過256種,深度不能超過8位。
    • Sort Flag 排序標記,0為未設置,1為按重要性遞減排序,最重要的顏色在前。
    • Size of Global Color Table 全局顏色表的大小,如果值為1,則該字段中的值用於計算全局顏色表中包含的字節數。
  • Background Color Index 背景顏色索引,它描述瞭全局顏色表的索引,背景顏色是用於屏幕上未被圖像覆蓋的像素的顏色。如果全局顏色標記設置為0,該字段將會被忽略。
  • Pixel Aspect Ratio 像素縱橫比,用於計算原始圖像中像素縱橫比的近似值的因子。如果該值不為0,則近似值的計算公式為:(N + 15) / 64 ,N為像素縱橫比,它的值為像素寬度與其高度的商。

我們用代碼來獲取下它的寬度與高度。

// 假設我們已經得到瞭dataView
const width = this.dataView.getUint16(6, true);
const height = this.dataView.getUint16(8, true);

Global Color Table

該數據塊包含瞭一個顏色表,由紅-綠-藍三元組的字節序列構成。正如前面所說,它並非必須存在,如果存在的話它將位於Logical Screen Descriptor塊的後面。

所占的字節數為3*2^(N+1),N為全局顏色表的大小 + 1,該數據塊在數據流中隻存在一個,如下圖所示。

我們來看下代碼的實現。

let pos = 0;
const PaletteColorsRGB = [];
const gifInfo = {}

// 解析全局調色板
const unpackedField = getBitArray(dataView.getUint8(10));
if (unpackedField[0]) {
  const globalPaletteSize = getPaletteSize(unpackedField);
  gifInfo.globalPalette = true;
  // 計算全局調色板的大小
  gifInfo.globalPaletteSize = globalPaletteSize / 3;
  // 調整指針位置
  pos += globalPaletteSize;
  // 遍歷獲取此塊區域的所有顏色並存起來
  for (let i = 0; i < gifInfo.globalPaletteSize; i++) {
    const palettePos = 13 + i * 3;
    const r = dataView.getUint8(palettePos);
    const g = dataView.getUint8(palettePos + 1);
    const b = dataView.getUint8(palettePos + 2);
    PaletteColorsRGB.push({ r, g, b });
  }
}
pos += 13;

// 獲取調色板大小函數
function getPaletteSize(palette: Array<number>): number {
  return 3 * Math.pow(2, 1 + bitToInt(palette.slice(5, 8)));
}

Graphics Control Extension

該數據塊包含瞭處理圖形渲染塊時需要使用的參數,它隻包含瞭一個數據子塊。該塊中記錄瞭7種數據的描述,如下所示:

  • Extension Introducer 擴展導入符,標識擴展塊的開始,包含固定值0x21
  • Graphic Control Label 圖形控制標簽,用於將當前塊標識為圖形控制擴展,包含固定值0xF9
  • Byte Size 塊中的字節數,在此字段之後,直到但不包括終止符。該字段包含固定值4,裡面包含瞭4種數據的描述。
    • Reserved for Future Use 保留模塊
    • Disposal Method 處理方法,表示圖形在顯示後的處理方式。
    • User Input Flag 用戶輸入標識,在繼續之前是否需要用戶輸入,如果是0則不需要用戶輸入,1代表需要用戶輸入。輸入的性質由程序決定(如回車、鼠標點擊等)
    • Transparency Color Flag 透明標識,用於描述是否在透明索引字段中給出瞭透明索引。0:未給出透明索引;1:給出瞭透明索引
  • Delay Time 當前幀圖像的延遲時間,如果不為0,則表示該字段在繼續處理數據流之前等待的百分之一秒(即gif每一幀的時長)
  • Transparency Index 透明度指數
  • Block Terminator 塊終止符,用於標識圖形控制擴展塊的結束

此處我們最關心的就是如何取出gif每一幀的時長,我們來看下代碼的實現。

// 假設我們已經得到瞭dataView且pos可能指向圖形控制快
const type = dataView.getUint8(pos);
// 圖形控制塊
if (type === 0xf9) {
  const length = dataView.getUint8(this.pos + 2);
  if (length === 4) {
    // 獲取每一幀的時長
    const delay = getFrameDuration(dataView.getUint16(pos + 4, true));
    pos += 8;
  }
}

Image Descriptor

一個gif文件可能會包含多個圖像,每個圖像都以一個圖像描述符塊開始。這個塊在數據流中占10個字節。該塊中記錄瞭6種數據的描述,如下所示:

  • Image Separator 圖像分割符,用於標識此數據塊的開頭,它的固定值為0x2C
  • Image Left Position 圖像左位置,圖像左邊緣距離邏輯屏幕左邊緣的行數(以像素為單位)
  • Image Top Position 圖像頂部位置,圖像頂部邊緣相對於邏輯屏幕頂部邊緣的行數(以像素為單位)
  • Image Width 圖像寬度
  • Image Height 圖像高度
  • Packed Field 壓縮塊
    • Local Color Table Flag 局部顏色表標志,緊跟在該圖像描述符之後的局部顏色表的存在,0:不存在,則使用全局顏色表,1:存在,則使用緊跟其後的Local Color Table數據塊
    • Interlace Flag 隔行標志,標識圖像是否是隔行的(圖像以四遍交錯模式交錯)
    • Sort Flag 排序標志 – 指示本地顏色表是否已排序。0:未設置排序,1:按重要性遞減排序,最重要的顏色在前
    • Size of Local Color Table 局部顏色表的大小

Image Data

該塊由一系列子塊組成,每個子塊的大小最多為255字節,包含對圖像中每個像素的活動顏色表的索引, 像素索引按從左到右和從上到下的順序排列。 每個索引必須在活動顏色表的大小范圍內,從 0 開始。 索引序列使用具有可變長度代碼的 LZW 算法進行編碼,如下所示。

每解析完一輪Image Descriptor都需要讀取下Data Sub-blocks,直至所有子塊被讀取完畢。

實現代碼

通過前面的瞭解,我們知道瞭Gif圖像中每個數據塊的組成原理,接下來我們就可以編寫代碼來解決我們所遇到的問題瞭

我們將數據塊分析章節的思路整理下,核心代碼如下所示:

  • 插件初始化的時候,接受一個url作為可選參數,如果存在則使用fetch解析這個url,將最終的數據放入dataView中
  • 暴露一個getInfo方法用於獲取Gif的信息,接受一個File類型的可選參數,如果url與此參數同時傳入,則優先使用此參數
  • 完整代碼:main.ts
export default class GifParser {
  private urlLoadStatus: boolean | undefined = undefined;
  private dataView: DataView | undefined;
  // 當前指向DataView的指針位置
  private pos = 0;
  // 當前解析的幀索引
  private index = 0;
  private gifInfo: gifInfoType = {
    valid: false,
    globalPalette: false,
    globalPaletteSize: 0,
    globalPaletteColorsRGB: [],
    loopCount: 0,
    height: 0,
    width: 0,
    animated: false,
    images: [],
    duration: 0,
    identifier: "0"
  };
  constructor(url?: string) {
    if (url) {
      this.urlLoadStatus = false;
      // 解析url,將其轉化為DataView格式的數據
      fetch(url)
        .then((response) => response.arrayBuffer())
        .then((arrayBuffer) => {
          return new DataView(arrayBuffer);
        })
        .then((dataView) => {
          // GIF加載成功
          this.urlLoadStatus = true;
          this.dataView = dataView;
        });
    }
  }
  /**
   * 獲取圖像信息
   * @param gifStream
   */
  public async getInfo(gifStream?: File): Promise<gifInfoType> {
    // 參數有效性校驗
    await this.validityCheck(gifStream);
    // url與gifStream都未傳入則拋出異常
    if (this.dataView == null) {
      throw new Error("未找到GIF解析源, 請檢查參數是否正確傳入");
    }
    
    // 隻解析GIF8格式的圖像:使用getUint16獲取2個字節十六進制值,判斷它是否滿足Gif格式的Header塊的簽名與版本號
    // 47 49 為簽名信息,轉換為Unicode編碼為:G I
    // 46 38 為版本信息,轉換為Unicode編碼為:F 8
    if (
      this.dataView.getUint16(0) != 0x4749 ||
      this.dataView.getUint16(2) != 0x4638
    ) {
      return this.gifInfo;
    }
    
    // 經過上述判斷後,此時的GIF已經有效瞭
    this.gifInfo.valid = true;
    // 獲取GIF圖像的寬,高
    this.gifInfo.width = this.dataView.getUint16(6, true);
    this.gifInfo.height = this.dataView.getUint16(8, true);
    
    // 獲取全局調色板、讀取每一幀的圖像信息等代碼省略,請移步GitHub查看完整代碼
  }
}

測試用例

最後,我們將插件打包,寫一個簡單的demo來測試下。

<meta charset="utf-8">
<title>gifParserPlugin demo</title>
<script src="./gifParserPlugin.umd.js"></script>


<script>
async function getGifInfo(e) {
  const gifParser = new gifParserPlugin()
  const gifInfo = gifParser.getInfo(e.target.files[0])
  gifInfo.then((res) => {
    console.log("解析完成", res);
  })
}
window.onload = function() {
  const input = document.getElementById('input');
  input.addEventListener('change', getGifInfo);
}
</script>

<input type="file" id="input">

運行結果如下所示。

  • gif的寬度是748px,高度是358px
  • gif的總時長為11400ms,總共有114幀

插件地址

該插件已發佈至npm,地址為請移步:

npm地址:gif-parser-web

GitHub地址:gif-parser-web-github

到此這篇關於JS實現獲取GIF總幀數的方法詳解的文章就介紹到這瞭,更多相關JS獲取GIF總幀數內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!

推薦閱讀: