当前位置: 首页 > 科技观察

再来说说JS获取GIF的总帧数

时间:2023-03-20 02:03:49 科技观察

前言中有一张GIF图片,我们想获取它的总帧数,如果图片超过一定的帧数,则不允许用户上传,服务端有很多现成的库可以使用,这个方法不是很友好。前端需要先将gif上传到服务器,服务器解析后返回结果,大大降低了用户体验。那么如何通过上传前的js总帧数来判断呢?本文将分享一个解决方案给大家,并打包成插件发布到npm仓库。欢迎有兴趣的开发者阅读本文。写在前面这个插件已经发布到npm。原生JS编写,支持任何前端框架。如果你对其实现原理不感兴趣,只想用它来解决你的实际问题,可以直接通过npm/yarn安装。命令如下:#yarninstallyarnaddgif-parser-web#npminstallnpminstallgif-parser-web--保存文件地址,请移步:README.md思想分析我们都知道,不管是什么文件在计算机中,它是以流的形式存储的,所以我们可以通过读取文件流来获取它的所有信息。Gif类型的文件也是如此,我们只要知道它的文件流结构,就可以按照它的规则进行解析和读取。什么是GifGif的全称是GraphicsInterchangeFormat,是一种以8位颜色再现真彩色图像的位图。使用LZW压缩算法进行编码,可以有效减少图像文件在互联网上的传输时间。我们在网站上看到的动人表情基本上都是Gif格式的。组成结构上面说了,我们要解析gif,首先要知道它的文件流结构。在What'sInAGIF网站中,我们知道它是由很多不同类型的块组成的,如下:,局部颜色表(LocalColorTable)。控制块:图形控制扩展。图形渲染块:纯文本扩展(PlainTextExtension)、图像描述符(ImageDescriptor)。特殊用途块:应用程序扩展(ApplicationExtension)、注释扩展(CommentExtension)、数据流结束标记(Trailer)。图像数据块:图像数据(ImageData)。分析原理了解了gif的组成结构之后,我们来看下如何获取它的数据流,如下:读取Gif图片文件(从url读取或上传本地文件类型数据);read将取出的数据转换成arrayBuffer;将arrayBuffer放入DataView;使用DataView底层的相关API读取十六进制码;对十六进制码进行解码,得到图像的信息。其解码过程如下图所示:沿着箭头从Header开始读取,直到PlainTextExtension完成第一帧的读取,其中GlobalColorTable、ApplicationExtension、CommentExtension、LocalColorTable、PlainTextExtension不一定存在。接下来,重复GraphicControlExtension、ImageDescriptor、ImageData读取剩余的帧图像数据。直到Trailer标志被读取,整个Gif的读取就完成了。GIF文件流图注意:在读取过程中,每个chunk都有自己特殊的编码标志。数据块分析了解了gif的构成之后,我们来看一下具体每个数据块的编码信息。HeaderBlock这个数据块用来标记数据流的开始。它位于文件头数据流的上下文中。它包含gif的签名和版本信息。它必须存在,而且只有一个。该块在数据流中占6个字节,其中签名和版本信息各占3个字节,即:数据流中0-2位置的元素必须代表签名信息数据中3-5位置的元素streamofgif必须指明gif的版本信息。我们以89a格式的gif为例。它的Header信息如下:Signature的十六进制值为47、49、46,将其转换成Unicode编码字符后,将是:“G”、“I”、“F”版本的十六进制值分别是38、39、61,转换成Unicode编码后的字符分别是:“8”、“9”、“a”;GIFheaderblocklayoutus让我们看看如何用代码读取它。//假设我们已经获取到dataViewconstsignature=dataView.getUint16(0);//使用getUint16方法从0位置开始连续获取2个字节的值,并转换成Unicode:GIconstversion=dataView.getUint16(2);//使用getUint16方法连续获取从第2个位置开始的2个字节的值,转换成Unicode编码:F8LogicalScreenDescriptor该数据块定义了设备需要的图像显示参数,位于Header数据块后面,它必须存在且只有一个,其值的坐标是相对于虚拟屏幕的左上角计算的。该块在数据流中占用7个字节,包含的信息如下:CanvasWidth图像的宽度(以像素为单位),占用2个字节的空间。画布高度图像的高度(以像素为单位),占用2个字节。PackedFields压缩字段,占用1个字节空间,其中包含4个值GlobalColorTableFlag全局颜色标志,用于标识全局颜色表。如果值为0,表示没有全局色块;如果值为1,则表示全局色块跟随此块。ColorResolution颜色分辨率,即颜色的位数,有1位、8位、16位、32位等。在gif格式的图像定义中,其颜色不能超过256位,其深度不能超过8位。SortFlag排序标志,0不设置,1按重要性降序排序,最重要的颜色在前。SizeofGlobalColorTable全局色表的大小,如果值为1,则该字段的值用于计算全局色表包含的字节数。BackgroundColorIndex背景颜色索引,描述全局颜色表的索引,背景颜色用于屏幕上未被图像覆盖的像素点的颜色。如果全局颜色标志设置为0,则该字段将被忽略。像素纵横比用于计算原始图像中像素纵横比近似值的系数。如果该值不为0,则近似值计算为:(N+15)/64,其中N为像素纵横比,其值为像素宽度与其高度的商。GIF逻辑屏幕描述符块布局我们使用代码来获取它的宽度和高度。//假设我们有dataViewconstwidth=this.dataView.getUint16(6,true);constheight=this.dataView.getUint16(8,true);全局颜色表这个数据块包含一个颜色表,由红-绿-蓝三元组组成的字节序列。如前所述,它不一定存在,如果存在,它将位于LogicalScreenDescriptor块的后面。占用的字节数为3*2^(N+1),N为全局色表的大小+1。数据流中只有一个数据块,如下图所示。GIF全局色表块布局我们来看看代码的实现。让pos=0;constPaletteColorsRGB=[];constgifInfo={}//解析全局调色板constunpackedField=getBitArray(dataView.getUint8(10));if(unpackedField[0]){constglobalPaletteSize=getPaletteSize(unpackedField);gifInfo.globalPalette=true;//计算全局调色板的大小gifInfo.globalPaletteSize=globalPaletteSize/3;//调整指针位置pos+=globalPaletteSize;//遍历得到该区域的所有颜色并保存for(leti=0;i):number{return3*Math.pow(2,1+bitToInt(palette.slice(5,8)));}GraphicsControlExtension该数据块包含处理图形渲染块所需的参数,它只包含一个数据子块。该块中记录了7种数据的描述,如下:ExtensionIntroducer扩展介绍器标识扩展块的开始,包含一个固定值0x21。GraphicControlLabel图形控件标签,用于标识当前块为图形控件扩展,包含固定值0xF9;字节大小块中的字节数,在此字段之后,直到但不包括终止符。该字段包含固定值4,包含4种数据的描述。保留供将来使用保留模块。DisposalMethod处理方式,指图形显示后的处理方式。UserInputFlag用户输入标志,继续前是否需要用户输入,为0表示不需要用户输入,为1表示需要用户输入。输入的性质由程序决定(如回车、鼠标点击等)。TransparencyColorFlag透明标志,用于描述透明索引字段中是否给出了透明索引。0:没有给出透明索引;1:给定透明索引。DelayTime当前帧图像的延迟时间,如果不为0,则表示该字段在继续处理数据流之前等待的百分之一秒(即gif每一帧的持续时间)。透明度指数透明度指数。BlockTerminator块终止符,用来标记图形控件扩展块的结束。GIF图控扩展块布局这里我们最关心的是如何获取gif每一帧的时长。我们来看看代码的实现。//假设我们已经得到了dataView,pos可能指向图形控制块consttype=dataView.getUint8(pos);//图形控制块if(type===0xf9){constlength=dataView.getUint8(this.位置+2);if(length===4){//获取每一帧的时长constdelay=getFrameDuration(dataView.getUint16(pos+4,true));位置+=8;}}ImageDescriptor一个gif文件可能包含多个图像,每个图像以一个图像描述符块开始。该块在数据流中占用10个字节。该块中记录了6种数据的描述,如下:ImageSeparator图像分隔符,用于标识本数据块的开始,其固定值为0x2C。ImageLeftPosition图像左侧位置,从图像左边缘到逻辑屏幕左边缘的行数(以像素为单位)。ImageTopPosition图像顶部位置,从图像顶部边缘到逻辑屏幕顶部边缘的行数(以像素为单位)。图像宽度图像宽度。图像高度图像高度。压缩字段压缩块。LocalColorTableFlagLocalcolortableflag,紧跟图像描述符的局部色表是否存在,0:不存在,则使用全局色表,1:存在,则使用紧跟其后的LocalColorTable数据块.InterlaceFlag是隔行标志位,标识图像是否隔行扫描(four-pass隔行扫描模式下图像是隔行扫描的)。SortFlag-指示本地颜色图是否已排序。0:不设置排序,1:按重要性降序排序,最重要的颜色在前。局部颜色表的大小局部颜色表的大小。GIF图像描述符块layoutImageData该块由一系列子块组成,每个子块最大255字节,包含图像中每个像素的活动颜色表的索引,像素索引从左到右和从上往下依次排列。每个索引必须在活动颜色表的大小范围内,从0开始。索引序列使用具有可变长度代码的LZW算法进行编码,如下所示。GIF图片数据块布局GIF图片数据块布局每轮ImageDescriptor解析后,都需要读取DataSub-blocks,直到读取完所有的子块。实现代码通过前面的了解,我们知道了Gif图片中各个数据块的组成原理,接下来就可以编写代码来解决我们遇到的问题了。我们来梳理一下数据块分析章节的思路。核心代码如下:插件初始化时,接受一个url作为可选参数。如果存在,则使用fetch解析url,将最终数据放入dataView中。暴露一个getInfo方法来获取Gif信息,接受一个File类型的可选参数,如果url与该参数同时传入,则优先使用该参数。完整代码:main.ts。导出默认类GifParser{privateurlLoadStatus:boolean|未定义=未定义;私有数据视图:DataView|不明确的;//当前指针位置指向DataViewprivatepos=0;//当前解析的帧索引privateindex=0;privategifInfo:gifInfoType={valid:false,globalPalette:false,globalPaletteSize:0,globalPaletteColorsRGB:[],loopCount:0,height:0,width:0,animated:false,images:[],duration:0,identifier:"0"};构造函数(url?:字符串){如果(url){this.urlLoadStatus=false;//解析url并将其转换为DataView格式的数据fetch(url).then((response)=>response.arrayBuffer()).then((arrayBuffer)=>{returnnewDataView(arrayBuffer);}).then((dataView)=>{//GIF加载成功this.urlLoadStatus=true;this.dataView=dataView;});}}/***获取图片信息*@paramgifStream*/publicasyncgetInfo(gifStream?:File):Promise{//参数有效性检查awaitthis.validityCheck(gifStream);//如果url和gifStream都没有传入,会抛出异常}//只解析GIF8格式的图片:使用getUint16得到2字节的16进制值,判断是否满足Gif格式的Header块的签名和版本号//4749为签名信息,转为Unicode编码:GI//4638为版本信息,转为Unicode编码:F8if(this.dataView.getUint16(0)!=0x4749||this.dataView.getUint16(2)!=0x4638){returnthis.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进行测试gifParserPlugin演示运行结果如下。gif的宽度为748px,高度为358px。动图总时长11400ms,共114帧。