<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          面試官:如何限制 Gif 圖片上傳的幀數(shù)?

          共 5143字,需瀏覽 11分鐘

           ·

          2022-06-02 14:01

          點(diǎn)擊上方?前端Q,關(guān)注公眾號(hào)

          回復(fù)加群,加入前端Q技術(shù)交流群


          有一個(gè)Gif圖片,我們想要獲取它的總幀數(shù),超過一定幀數(shù)的圖片告知用戶不可上傳,在服務(wù)端有很多現(xiàn)成的庫可以使用,這種做法不是很友好,前端需要先將gif上傳至服務(wù)端,服務(wù)端解析完畢后將結(jié)果返回,大大降低了用戶體驗(yàn)。

          那么如何通過js在上傳前就拿到它的總幀數(shù)來判斷呢?本文就跟大家分享一種解決方案,并將其封裝成插件發(fā)布至npm倉庫,歡迎各位感興趣的開發(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的讀取
          GIF file stream diagram

          注意:在讀取過程中,每個(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"
          GIF header block layout

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

          //?假設(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為像素縱橫比,它的值為像素寬度與其高度的商。
          GIF logical screen descriptor block layout

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

          //?假設(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è),如下圖所示。

          GIF global color table block layout

          我們來看下代碼的實(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?????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í)長)
          • Transparency Index 透明度指數(shù)
          • Block Terminator 塊終止符,用于標(biāo)識(shí)圖形控制擴(kuò)展塊的結(jié)束
          GIF graphic control extension block layout

          此處我們最關(guān)心的就是如何取出gif每一幀的時(shí)長,我們來看下代碼的實(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í)長
          ????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 局部顏色表的大小
          GIF image descriptor block layout

          Image Data

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

          GIF image data block layout
          GIF image data block layout

          每解析完一輪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?{
          ????//?參數(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è)簡單的demo來測(cè)試下。

          <meta?charset="utf-8">
          <title>gifParserPlugin?demotitle>
          <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í)長為11400ms,總共有114幀
          image-20220526204406993

          插件地址

          該插件已發(fā)布至npm,地址為請(qǐng)移步??:

          • npm地址:gif-parser-web
          • GitHub地址:gif-parser-web-github


          往期推薦


          深入了解前端路由 hash 與 history 差異
          一位 Google 工程師的十年總結(jié)
          手把手 從 0 到 1 搞定官網(wǎng)開發(fā)

          最后


          • 歡迎加我微信,拉你進(jìn)技術(shù)群,長期交流學(xué)習(xí)...

          • 歡迎關(guān)注「前端Q」,認(rèn)真學(xué)前端,做個(gè)專業(yè)的技術(shù)人...

          點(diǎn)個(gè)在看支持我吧
          瀏覽 36
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  se999se | 中文黄色毛片 | 很很操很很干 | 日韩视频一区 | 国产一区二区三区皇色网站 |