有一个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的全称是Graphics Interchange Format,是一种位图,以8位色重现真彩色的图像。采用LZW压缩算法进行编码,可以有效的减少图像文件在网上的传输时间,我们在网站上看到的会动的表情包,基本上都是Gif格式的。
正如上面所说,我们想解析gif就得先知道它的文件流结构,在What's In A GIF网站中我们知道了它是由多种不同类型的块组成,如下所示:
了解完gif的组成结构后,接下来我们来看下如何获取它的数据流,如下所示:
arrayBuffer
arrayBuffer
放到DataView
中DataView
底层的相关API来读取十六进制编码它的解码过程如下图所示:
注意:在读取过程中,每个块都有自己特殊的编码标记。
我们了解完gif的构成后,接下来我们来看下每一个具体的数据块的编码信息。
该数据块用于标记数据流的开始,位于文件头数据流的上下文内,里面包含了gif的签名与版本信息,它是必须存在的且只有一个。
该块在数据流中占6个字节,其中签名与版本信息各占3个字节,即:
我们以89a格式的gif为例,它的Header信息就如下所示:
我们来看下如何用代码来读取。
// 假设我们已经得到了dataView const signature = dataView.getUint16(0); // 使用getUint16方法从0号位置开始连续获取2个字节的值,转换成转换为Unicode编码为:G I const version = dataView.getUint16(2); // 使用getUint16方法从2号位置开始连续获取2个字节的值,转换成转换为Unicode编码为:F 8
该数据块中定义了图像在设备中显示所需的参数,位于Header数据块的后面,它是必须存在的且只有一个,其值的坐标是相对于虚拟屏幕左上角计算出来的。
该块在数据流中占7个字节,包含的信息如下所示:
(N + 15) / 64
,N为像素纵横比,它的值为像素宽度与其高度的商。我们用代码来获取下它的宽度与高度。
// 假设我们已经得到了dataView const width = this.dataView.getUint16(6, true); const height = this.dataView.getUint16(8, true);
该数据块包含了一个颜色表,由红-绿-蓝三元组的字节序列构成。正如前面所说,它并非必须存在,如果存在的话它将位于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))); }
该数据块包含了处理图形渲染块时需要使用的参数,它只包含了一个数据子块。该块中记录了7种数据的描述,如下所示:
0x21
。0xF9
4
,里面包含了4种数据的描述。此处我们最关心的就是如何取出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; } }
一个gif文件可能会包含多个图像,每个图像都以一个图像描述符块开始。这个块在数据流中占10个字节。该块中记录了6种数据的描述,如下所示:
0x2C
。该块由一系列子块组成,每个子块的大小最多为255字节,包含对图像中每个像素的活动颜色表的索引, 像素索引按从左到右和从上到下的顺序排列。 每个索引必须在活动颜色表的大小范围内,从 0 开始。 索引序列使用具有可变长度代码的 LZW
算法进行编码,如下所示。
每解析完一轮Image Descriptor都需要读取下Data Sub-blocks,直至所有子块被读取完毕。
通过前面的了解,我们知道了Gif图像中每个数据块的组成原理,接下来我们就可以编写代码来解决我们所遇到的问题了
我们将数据块分析章节的思路整理下,核心代码如下所示:
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">
运行结果如下所示。
该插件已发布至npm,地址为请移步:
npm地址:gif-parser-web
GitHub地址:gif-parser-web-github