JS實(shí)現(xiàn)獲取GIF總幀數(shù)的方法詳解
前言
有一個(gè)Gif圖片,我們想要獲取它的總幀數(shù),超過一定幀數(shù)的圖片告知用戶不可上傳,在服務(wù)端有很多現(xiàn)成的庫(kù)可以使用,這種做法不是很友好,前端需要先將gif上傳至服務(wù)端,服務(wù)端解析完畢后將結(jié)果返回,大大降低了用戶體驗(yàn)。
那么如何通過js在上傳前就拿到它的總幀數(shù)來判斷呢?本文就跟大家分享一種解決方案,并將其封裝成插件發(fā)布至npm倉(cāng)庫(kù),歡迎各位感興趣的開發(fā)者閱讀本文。
寫在前面
此插件已經(jīng)發(fā)布至npm,采用原生JS編寫支持任意一個(gè)前端框架,如果你對(duì)其實(shí)現(xiàn)原理不感興趣,只是想拿來解決你的實(shí)際問題,可以直接通過npm/yarn來安裝,命令如下:
# yarn安裝 yarn add gif-parser-web # npm安裝 npm install gif-parser-web --save
文檔地址請(qǐng)移步:README.md
思路分析
我們都知道無論什么文件在計(jì)算機(jī)中都是以流的形式進(jìn)行存儲(chǔ)的,因此我們可以通過讀取文件流來拿到它的所有信息。Gif類型的文件也是如此,我們只要能知道它的文件流結(jié)構(gòu)就可以根據(jù)它的規(guī)則進(jìn)行解析讀取了。
什么是Gif
Gif的全稱是Graphics Interchange Format,是一種位圖,以8位色重現(xiàn)真彩色的圖像。采用LZW壓縮算法進(jìn)行編碼,可以有效的減少圖像文件在網(wǎng)上的傳輸時(shí)間,我們?cè)诰W(wǎng)站上看到的會(huì)動(dòng)的表情包,基本上都是Gif格式的。
組成結(jié)構(gòu)
正如上面所說,我們想解析gif就得先知道它的文件流結(jié)構(gòu),在What's In A GIF網(wǎng)站中我們知道了它是由多種不同類型的塊組成,如下所示:
- 未標(biāo)記塊:Header(文件頭)、Logical Screen Descriptor(邏輯屏幕描述符)、Global Color Table(全局顏色表)、局部顏色表(Local Color Table)
- 控制塊:圖形控制擴(kuò)展(Graphics Control Extension)
- 圖形渲染塊:純文本擴(kuò)展(Plain Text Extension)、圖像描述符(Image Descriptor)
- 特殊用途塊:應(yīng)用擴(kuò)展( Application Extension)、注解擴(kuò)展(Comment Extension)、數(shù)據(jù)流結(jié)束標(biāo)記(Trailer)
- 圖像數(shù)據(jù)塊:圖像數(shù)據(jù)(Image Data)
解析原理
了解完gif的組成結(jié)構(gòu)后,接下來我們來看下如何獲取它的數(shù)據(jù)流,如下所示:
- 讀取Gif圖片文件(從url讀取或者從本地上傳的File類型的數(shù)據(jù))
- 將讀取到的數(shù)據(jù)轉(zhuǎn)成
arrayBuffer - 將
arrayBuffer放到DataView中 - 使用
DataView底層的相關(guān)API來讀取十六進(jìn)制編碼 - 對(duì)十六進(jìn)制編碼進(jìn)行解碼,獲取圖像的信息
它的解碼過程如下圖所示:
- 從Header開始順著箭頭一直讀到PlainTextExtension完成第一幀的讀取,其中GlobalColorTable、ApplicationExtension、CommentExtension、LocalColorTable、PlainTextExtension不一定存在
- 接下來重復(fù)GraphicControlExtension、ImageDescriptor、ImageData 讀取剩下的幀圖片數(shù)據(jù)
- 直至讀取到Trailer標(biāo)識(shí),就完成了整個(gè)Gif的讀取

注意:在讀取過程中,每個(gè)塊都有自己特殊的編碼標(biāo)記。
數(shù)據(jù)塊分析
我們了解完gif的構(gòu)成后,接下來我們來看下每一個(gè)具體的數(shù)據(jù)塊的編碼信息。
Header Block
該數(shù)據(jù)塊用于標(biāo)記數(shù)據(jù)流的開始,位于文件頭數(shù)據(jù)流的上下文內(nèi),里面包含了gif的簽名與版本信息,它是必須存在的且只有一個(gè)。
該塊在數(shù)據(jù)流中占6個(gè)字節(jié),其中簽名與版本信息各占3個(gè)字節(jié),即:
- 數(shù)據(jù)流的0-2位置的元素一定表示gif的簽名信息
- 數(shù)據(jù)流的3-5位置的元素一定表示gif的版本信息
我們以89a格式的gif為例,它的Header信息就如下所示:
- Signature的16進(jìn)制值為47、49、46,將其轉(zhuǎn)換為Unicode編碼字符后就為:"G"、"I"、"F"
- Version的16進(jìn)制值為38、39、61,將其轉(zhuǎn)換為Unicode編碼字符后就為:"8"、"9"、"a"

我們來看下如何用代碼來讀取。
// 假設(shè)我們已經(jīng)得到了dataView const signature = dataView.getUint16(0); // 使用getUint16方法從0號(hào)位置開始連續(xù)獲取2個(gè)字節(jié)的值,轉(zhuǎn)換成轉(zhuǎn)換為Unicode編碼為:G I const version = dataView.getUint16(2); // 使用getUint16方法從2號(hào)位置開始連續(xù)獲取2個(gè)字節(jié)的值,轉(zhuǎn)換成轉(zhuǎn)換為Unicode編碼為:F 8
Logical Screen Descriptor
該數(shù)據(jù)塊中定義了圖像在設(shè)備中顯示所需的參數(shù),位于Header數(shù)據(jù)塊的后面,它是必須存在的且只有一個(gè),其值的坐標(biāo)是相對(duì)于虛擬屏幕左上角計(jì)算出來的。
該塊在數(shù)據(jù)流中占7個(gè)字節(jié),包含的信息如下所示:
- Canvas Width 圖片的寬度(以像素為單位),占2個(gè)字節(jié)空間。
- Canvas Height 圖片的高度(以像素為單位),占2個(gè)字節(jié)空間。
- Packed Fields 壓縮字段,占1字節(jié)空間,里面包含4個(gè)值
- Global Color Table Flag 全局顏色標(biāo)記,用于標(biāo)識(shí)全局顏色表。如果值為0則表示不存在全局顏色塊;如果值為1則表示全局顏色塊緊跟于此塊之后。
- Color Resolution 顏色分辨率,即顏色的位數(shù),有1位、8位、16位、32位等。在gif格式的圖像定義中,它的顏色不能超過256種,深度不能超過8位。
- Sort Flag 排序標(biāo)記,0為未設(shè)置,1為按重要性遞減排序,最重要的顏色在前。
- Size of Global Color Table 全局顏色表的大小,如果值為1,則該字段中的值用于計(jì)算全局顏色表中包含的字節(jié)數(shù)。
- Background Color Index 背景顏色索引,它描述了全局顏色表的索引,背景顏色是用于屏幕上未被圖像覆蓋的像素的顏色。如果全局顏色標(biāo)記設(shè)置為0,該字段將會(huì)被忽略。
- Pixel Aspect Ratio 像素縱橫比,用于計(jì)算原始圖像中像素縱橫比的近似值的因子。如果該值不為0,則近似值的計(jì)算公式為:
(N + 15) / 64,N為像素縱橫比,它的值為像素寬度與其高度的商。

我們用代碼來獲取下它的寬度與高度。
// 假設(shè)我們已經(jīng)得到了dataView const width = this.dataView.getUint16(6, true); const height = this.dataView.getUint16(8, true);
Global Color Table
該數(shù)據(jù)塊包含了一個(gè)顏色表,由紅-綠-藍(lán)三元組的字節(jié)序列構(gòu)成。正如前面所說,它并非必須存在,如果存在的話它將位于Logical Screen Descriptor塊的后面。
所占的字節(jié)數(shù)為3*2^(N+1),N為全局顏色表的大小 + 1,該數(shù)據(jù)塊在數(shù)據(jù)流中只存在一個(gè),如下圖所示。

我們來看下代碼的實(shí)現(xiàn)。
let pos = 0;
const PaletteColorsRGB = [];
const gifInfo = {}
// 解析全局調(diào)色板
const unpackedField = getBitArray(dataView.getUint8(10));
if (unpackedField[0]) {
const globalPaletteSize = getPaletteSize(unpackedField);
gifInfo.globalPalette = true;
// 計(jì)算全局調(diào)色板的大小
gifInfo.globalPaletteSize = globalPaletteSize / 3;
// 調(diào)整指針位置
pos += globalPaletteSize;
// 遍歷獲取此塊區(qū)域的所有顏色并存起來
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;
// 獲取調(diào)色板大小函數(shù)
function getPaletteSize(palette: Array<number>): number {
return 3 * Math.pow(2, 1 + bitToInt(palette.slice(5, 8)));
}
Graphics Control Extension
該數(shù)據(jù)塊包含了處理圖形渲染塊時(shí)需要使用的參數(shù),它只包含了一個(gè)數(shù)據(jù)子塊。該塊中記錄了7種數(shù)據(jù)的描述,如下所示:
- Extension Introducer 擴(kuò)展導(dǎo)入符,標(biāo)識(shí)擴(kuò)展塊的開始,包含固定值
0x21。 - Graphic Control Label 圖形控制標(biāo)簽,用于將當(dāng)前塊標(biāo)識(shí)為圖形控制擴(kuò)展,包含固定值
0xF9 - Byte Size 塊中的字節(jié)數(shù),在此字段之后,直到但不包括終止符。該字段包含固定值
4,里面包含了4種數(shù)據(jù)的描述。- Reserved for Future Use 保留模塊
- Disposal Method 處理方法,表示圖形在顯示后的處理方式。
- User Input Flag 用戶輸入標(biāo)識(shí),在繼續(xù)之前是否需要用戶輸入,如果是0則不需要用戶輸入,1代表需要用戶輸入。輸入的性質(zhì)由程序決定(如回車、鼠標(biāo)點(diǎn)擊等)
- Transparency Color Flag 透明標(biāo)識(shí),用于描述是否在透明索引字段中給出了透明索引。0:未給出透明索引;1:給出了透明索引
- Delay Time 當(dāng)前幀圖像的延遲時(shí)間,如果不為0,則表示該字段在繼續(xù)處理數(shù)據(jù)流之前等待的百分之一秒(即gif每一幀的時(shí)長(zhǎng))
- Transparency Index 透明度指數(shù)
- Block Terminator 塊終止符,用于標(biāo)識(shí)圖形控制擴(kuò)展塊的結(jié)束

此處我們最關(guān)心的就是如何取出gif每一幀的時(shí)長(zhǎng),我們來看下代碼的實(shí)現(xiàn)。
// 假設(shè)我們已經(jīng)得到了dataView且pos可能指向圖形控制快
const type = dataView.getUint8(pos);
// 圖形控制塊
if (type === 0xf9) {
const length = dataView.getUint8(this.pos + 2);
if (length === 4) {
// 獲取每一幀的時(shí)長(zhǎng)
const delay = getFrameDuration(dataView.getUint16(pos + 4, true));
pos += 8;
}
}
Image Descriptor
一個(gè)gif文件可能會(huì)包含多個(gè)圖像,每個(gè)圖像都以一個(gè)圖像描述符塊開始。這個(gè)塊在數(shù)據(jù)流中占10個(gè)字節(jié)。該塊中記錄了6種數(shù)據(jù)的描述,如下所示:
- Image Separator 圖像分割符,用于標(biāo)識(shí)此數(shù)據(jù)塊的開頭,它的固定值為
0x2C。 - Image Left Position 圖像左位置,圖像左邊緣距離邏輯屏幕左邊緣的行數(shù)(以像素為單位)
- Image Top Position 圖像頂部位置,圖像頂部邊緣相對(duì)于邏輯屏幕頂部邊緣的行數(shù)(以像素為單位)
- Image Width 圖像寬度
- Image Height 圖像高度
- Packed Field 壓縮塊
- Local Color Table Flag 局部顏色表標(biāo)志,緊跟在該圖像描述符之后的局部顏色表的存在,0:不存在,則使用全局顏色表,1:存在,則使用緊跟其后的Local Color Table數(shù)據(jù)塊
- Interlace Flag 隔行標(biāo)志,標(biāo)識(shí)圖像是否是隔行的(圖像以四遍交錯(cuò)模式交錯(cuò))
- Sort Flag 排序標(biāo)志 - 指示本地顏色表是否已排序。0:未設(shè)置排序,1:按重要性遞減排序,最重要的顏色在前
- Size of Local Color Table 局部顏色表的大小

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


每解析完一輪Image Descriptor都需要讀取下Data Sub-blocks,直至所有子塊被讀取完畢。
實(shí)現(xiàn)代碼
通過前面的了解,我們知道了Gif圖像中每個(gè)數(shù)據(jù)塊的組成原理,接下來我們就可以編寫代碼來解決我們所遇到的問題了
我們將數(shù)據(jù)塊分析章節(jié)的思路整理下,核心代碼如下所示:
- 插件初始化的時(shí)候,接受一個(gè)url作為可選參數(shù),如果存在則使用fetch解析這個(gè)url,將最終的數(shù)據(jù)放入dataView中
- 暴露一個(gè)getInfo方法用于獲取Gif的信息,接受一個(gè)File類型的可選參數(shù),如果url與此參數(shù)同時(shí)傳入,則優(yōu)先使用此參數(shù)
- 完整代碼:main.ts
export default class GifParser {
private urlLoadStatus: boolean | undefined = undefined;
private dataView: DataView | undefined;
// 當(dāng)前指向DataView的指針位置
private pos = 0;
// 當(dāng)前解析的幀索引
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,將其轉(zhuǎn)化為DataView格式的數(shù)據(jù)
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> {
// 參數(shù)有效性校驗(yàn)
await this.validityCheck(gifStream);
// url與gifStream都未傳入則拋出異常
if (this.dataView == null) {
throw new Error("未找到GIF解析源, 請(qǐng)檢查參數(shù)是否正確傳入");
}
// 只解析GIF8格式的圖像:使用getUint16獲取2個(gè)字節(jié)十六進(jìn)制值,判斷它是否滿足Gif格式的Header塊的簽名與版本號(hào)
// 47 49 為簽名信息,轉(zhuǎn)換為Unicode編碼為:G I
// 46 38 為版本信息,轉(zhuǎn)換為Unicode編碼為:F 8
if (
this.dataView.getUint16(0) != 0x4749 ||
this.dataView.getUint16(2) != 0x4638
) {
return this.gifInfo;
}
// 經(jīng)過上述判斷后,此時(shí)的GIF已經(jīng)有效了
this.gifInfo.valid = true;
// 獲取GIF圖像的寬,高
this.gifInfo.width = this.dataView.getUint16(6, true);
this.gifInfo.height = this.dataView.getUint16(8, true);
// 獲取全局調(diào)色板、讀取每一幀的圖像信息等代碼省略,請(qǐng)移步GitHub查看完整代碼
}
}
測(cè)試用例
最后,我們將插件打包,寫一個(gè)簡(jiǎn)單的demo來測(cè)試下。
<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">
運(yùn)行結(jié)果如下所示。
- gif的寬度是748px,高度是358px
- gif的總時(shí)長(zhǎng)為11400ms,總共有114幀

插件地址
該插件已發(fā)布至npm,地址為請(qǐng)移步:
npm地址:gif-parser-web
GitHub地址:gif-parser-web-github
到此這篇關(guān)于JS實(shí)現(xiàn)獲取GIF總幀數(shù)的方法詳解的文章就介紹到這了,更多相關(guān)JS獲取GIF總幀數(shù)內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
JS圖片預(yù)加載三種實(shí)現(xiàn)方法解析
這篇文章主要介紹了JS圖片預(yù)加載三種實(shí)現(xiàn)方法解析,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-05-05
javascript實(shí)現(xiàn)簡(jiǎn)單頁(yè)面倒計(jì)時(shí)
這篇文章主要為大家詳細(xì)介紹了javascript實(shí)現(xiàn)簡(jiǎn)單頁(yè)面倒計(jì)時(shí),文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-03-03
利用10行js代碼實(shí)現(xiàn)上下滾動(dòng)公告效果
這篇文章主要給大家介紹了關(guān)于利用10行js代碼實(shí)現(xiàn)滾動(dòng)公告效果的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起看看吧。2017-12-12
利用了jquery的ajax實(shí)現(xiàn)二級(jí)聯(lián)互動(dòng)菜單
二級(jí)聯(lián)互動(dòng)菜單,利用了jquery的ajax實(shí)現(xiàn),具體實(shí)現(xiàn)如下,喜歡的朋友可以參考下2013-12-12
Javascript前端事件循環(huán)機(jī)制詳細(xì)講解
單線程的同步等待極大影響效率,任務(wù)不得不一個(gè)一個(gè)等待執(zhí)行,對(duì)于網(wǎng)頁(yè)應(yīng)用是無法接受的。所以Javascript使用事件循環(huán)機(jī)制來解決異步任務(wù)的問題。本文就來講講Javascript的事件循環(huán)機(jī)制,希望對(duì)你有所幫助2022-12-12
Javascript 獲取鼠標(biāo)當(dāng)前的位置實(shí)現(xiàn)方法
這篇文章主要介紹了Javascript 獲取鼠標(biāo)當(dāng)前的位置實(shí)現(xiàn)方法的相關(guān)資料,需要的朋友可以參考下2016-10-10
JS判斷空對(duì)象的幾個(gè)方法大盤點(diǎn)
在做數(shù)據(jù)交互的時(shí)候,我們經(jīng)常需要判斷數(shù)據(jù)或者對(duì)象是不是為空,避免當(dāng)接口異常時(shí)候前端頁(yè)面崩潰,下面這篇文章主要給大家介紹了關(guān)于JS判斷空對(duì)象的幾個(gè)方法,需要的朋友可以參考下2022-02-02

