JS实现获取GIF总帧数的方法详解

目录
  • 前言
  • 写在前面
  • 思路分析
    • 什么是Gif
    • 组成结构
    • 解析原理
  • 数据块分析
    • Header Block
    • Logical Screen Descriptor
    • Global Color Table
    • Graphics Control Extension
    • Image Descriptor
    • Image Data
  • 实现代码
  • 测试用例
  • 插件地址

前言

有一个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总帧数内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • 使用p5.js实现动态GIF图片临摹重现

    前言 根据互动媒体技术老师的实验要求,临摹了一张GIF动态图,使用p5.js进行重现. 博客里面会有实现逻辑以及实现代码,在最后还会有一张自己实现的扩展图. 原图 实现步骤 规律总结 1.观察图片可以看到,整个图是由两个部分组成的,其中一个是棍状体,一个是螺旋状体. 2.棍状体从外到内越来越窄,整个图形在做绕固定旋转圆心的匀速圆周运动. 3.螺旋状体也是在做绕固定旋转圆心的匀速圆周运动. 4.螺旋状体的旋转角速度比棍状体的旋转角速度大. 具体实现逻辑 为了方便分析,我截取了一个静态的瞬间: 那么

  • js实现GIF动图分解成多帧图片上传

    在项目中遇到需要支持上传gif图片,并把其分解的帧图片一次展示给用户.话不多说直接上代码 分解gif图片需要使用libgif-js这个库! 1. 引入Git库 import SuperGif from './libgif.js' 2. 分解Gif为png图片 const GifDecomposer = { structureGifObject (gifFiles, cb) { // gifFiles 获取的文件对象 e.target.files[0] const gifImg = documen

  • JS控制GIF图片的停止与显示

    在掷骰子游戏中,通过需要控制骰子的转动以及转动结果的显示,骰子的转动可以用GIF动图来实现,每次投掷骰子的结果可以通过点击按钮显示静态图片.代码如下: <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>CSS实现GIF动图的停止与开始(骰子)</title> <style type="text/css"> <

  • js实现GIF图片的分解和合成

    无意中看到一篇文章写得是关于纯前端处理GIF图片的问题,感觉挺有意思的所以自己也实现了一下: 主要用到的有两个第三方库:合成GIF图片的gif.js和分解的libgif.js; 分解GIF 1. 引入Git库 import SuperGif from './libgif.js' 2. 处理图片 var file = e.target.files[0]; console.log(file.type.indexOf('image/gif')); load_gif(file); function lo

  • js获取 gif 的帧数的代码实例

    使用 javascript 获取 GIF 图的帧数,如果帧数过大,则不让传到服务器 这里是使用一个插件: github地址为: https://github.com/buzzfeed/libgif-js <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title></title> <meta name="viewport" con

  • JS实现获取GIF总帧数的方法详解

    目录 前言 写在前面 思路分析 什么是Gif 组成结构 解析原理 数据块分析 Header Block Logical Screen Descriptor Global Color Table Graphics Control Extension Image Descriptor Image Data 实现代码 测试用例 插件地址 前言 有一个Gif图片,我们想要获取它的总帧数,超过一定帧数的图片告知用户不可上传,在服务端有很多现成的库可以使用,这种做法不是很友好,前端需要先将gif上传至服务端

  • JS中获取 DOM 元素的绝对位置实例详解

    在操作页面滚动和动画时经常会获取 DOM 元素的绝对位置,例如 本文 左侧的悬浮导航,当页面滚动到它以前会正常地渲染到文档流中,当页面滚动超过了它的位置,就会始终悬浮在左侧. 本文会详述各种获取 DOM 元素绝对位置 的方法以及对应的兼容性.关于如何获取 DOM 元素高度和滚动高度,请参考视口的宽高与滚动高度 一文. 概述 这些是本文涉及的 API 对应的文档和标准,供查阅: API 用途 文档 标准 offsetTop 相对定位容器的位置 MDN CSSOM View Module clien

  • Node.js 中的 fs 模块与Path模块方法详解

    概述: 文件系统模块是一个简单包装的标准 POSIX 文件 I/O 操作方法集.可以通过调用 require("fs") 来获取该模块.文件系统模块中的所有方法均有异步和同步版本. 文件系统模块中的异步方法需要一个完成时的回调函数作为最后一个传入形参. 回调函数的构成由调用的异步方法所决定,通常情况下回调函数的第一个形参为返回的错误信息. 如果异步操作执行正确并返回,该错误形参则为null或者undefined.如果使用的是同步版本的操作方法,一旦出现错误,会以通常的抛出错误的形式返回

  • 利用JS将图标字体渲染为图片的方法详解

    目录 前言 实现方式 html css js 效果 前言 在软件开发中肯定要用到图标,比如下图的 Groove 音乐中就用到了许多图标.一种获取这些图标的方法是把 Groove 音乐截个图,然后熟练地开启 Photoshop,开始抠图.这种方式很逊,效率也很低(虽然我刚开始就是这么干的). 如果打开 C:/Program File/WindowsApps(需要修改权限才能进入),可以发现几个名字里带 ZuneMusic 的文件夹,其中的某一个文件夹中会有字体文件 SegMVR2.ttf.这是一个

  • 获取Django项目的全部url方法详解

    在为一个项目添加权限时,遇到一个问题,就是为项目所有的url设置权限,但是一个一个手动输入太麻烦了,所以考虑用代码获取到一个项目所有的url 首先,考虑到项目最外层的urlpartterns,因为所有的url都要通过这里 urlpatterns = [ # url(r'^admin/', admin.site.urls), url(r'^arya/', site.urls), url(r'^index/', index), ] 先循环打印一下这个列表,看一下拿到的结果: <RegexURLRes

  • 在js里怎么实现Xcode里的callFuncN方法(详解)

    本人使用的WebStorm编辑器,里面没有callFuncN, 不记得Lua是否支持callFuncN,如果不支持相信应该能用同样的方法做到. 废话不多说,贴代码: loadDown : function () { var dis = this.left_move.getPositionY() - this.left.getPositionY(); // 得到一个距离 var act1 = new cc.moveBy(0.5,cc.p(0,-dis)); var act2 = cc.callFu

  • vue 父组件通过$refs获取子组件的值和方法详解

    前言 在vue项目中组件之间的通讯是很常见的问题,同时也是很重要的问题,我们大致可以将其分为三种情况: 父传子:在父组件中绑定值,在子组件中用props接收 子传父:在父组件中监听一个事件,在子组件中利用$emit触发这个事件并带上数据作为第二个参数,这时父组件中监听事件的回调函数就会被调用,回调函数的参数就是子组件带上来的数据,这样就可以在父组件中使用子组件的数据了, 兄弟之间的传递:我们可以使用事件总线(eventBus)来轻松的解决,其实就是发布订阅者模式 今天我们要看的是父组件如何直接调

  • JS合并两个数组的3种方法详解

    这篇文章主要介绍了JS合并两个数组的3种方法详解,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 需要将两个数组合并成为一个的情况.比如: var a = [1,2,3]; var b = [4,5,6]; 有两个数组a.b,需求是将两个数组合并成一个.方法如下: 1.concat js的Array对象提供了一个叫concat()方法,连接两个或更多的数组,并返回结果. var c = a.concat(b); //c=[1,2,3,4,5,6]

  • Python获取网络时间戳的两种方法详解

    目录 方法一 代码实现 调用方法 返回结果 方法二 代码实现 调用方法 返回结果 在我们进行注册码的有效期验证时,通常使用获取网络时间的方式来进行比对. 以下为获取网络时间的几种方式. 方法一 需要的时间会比较长,个别电脑上可能会出现不兼容现象 代码实现 def get_web_server_time(self, host_URL, year_str='-', time_str=':'): ''' 获取网络时间,需要的时间会比较长,个别电脑上可能会出现不兼容现象 :param host_URL:

  • JS实现将数据导出到Excel的方法详解

    修改之前项目代码的时候,发现前人导出excel是用纯javascript实现的.并没有调用后台接口. 之前从来没这么用过,记录一下.以备不时之需. 方法一: 将table标签,包括tr.td等对json数据进行拼接,将table输出到表格上实现,这种方法的弊端在于输出的是伪excel,虽说生成xls为后缀的文件,但文件形式上还是html,代码如下: <html> <head>     <p style="font-size: 20px;color: red;&quo

随机推荐