当前位置:   article > 正文

判断GIF类型并使用ImageDecoder解析GIF图

判断GIF类型并使用ImageDecoder解析GIF图

一、判断是否为GIF图片类型

在JavaScript中,判断用户上传的文件是否为GIF文件类型时,通常可以通过检查文件的type属性或文件的拓展名来判断,但是由于文件拓展名可以轻易被用户修改,type属性是由浏览器根据文件拓展名猜测得出的,因此这种判断方式并不总是准确的。

为了更准确的判断文件类型,需要读取文件的头部字节,并检查这些字节是否符合GIF文件的规范。

要检查文件头部字节以确定文件是否为GIF格式,可以使用JavaScript的FileReader API来读取文件的前几个字节,并与GIF文件的魔数(Magic Number)进行比较。GIF文件的魔数是GIF87a或GIF89a,它们位于文件的前几个字节中。

  1. // 文件输入元素
  2. const fileInput = document.getElementById('file-input')
  3. // 监听文件变化事件
  4. fileInput.addEventListener('change', async fucntion(e) {
  5. // 获取用户选择的文件
  6. const file = e.target.files[0]
  7. if(file) {
  8. const isGif = await checkGifFileType(file)
  9. if(isGif) {
  10. // 是gif
  11. } else {
  12. // 不是gif
  13. }
  14. }
  15. })
  1. /**
  2. * 检查文件类型是否是gif
  3. *
  4. * @param {*} file
  5. * @returns
  6. */
  7. export function checkGifFileType(file) {
  8. // 创建一个FileReader对象
  9. const reader = new FileReader();
  10. // 读取文件的前几个字节
  11. reader.readAsArrayBuffer(file.slice(0, 6)); // GIF的魔数只需要6个字节
  12. return new Promise((resolve, reject) => {
  13. reader.onload = function (e) {
  14. // 将ArrayBuffer转换为Uint8Array以便读取字节
  15. const arrayBuffer = e.target.result;
  16. const uint8Array = new Uint8Array(arrayBuffer);
  17. // GIF文件的魔数可以是'GIF87a'或'GIF89a',转换为ASCII码分别为[71, 73, 70, 56, 55, 97]或[71, 73, 70, 56, 57, 97]
  18. // 检查文件的前6个字节是否匹配GIF的魔数
  19. const gif87a = [71, 73, 70, 56, 55, 97];
  20. const gif89a = [71, 73, 70, 56, 57, 97];
  21. if (
  22. (uint8Array[0] === gif87a[0] &&
  23. uint8Array[1] === gif87a[1] &&
  24. uint8Array[2] === gif87a[2] &&
  25. uint8Array[3] === gif87a[3] &&
  26. uint8Array[4] === gif87a[4] &&
  27. uint8Array[5] === gif87a[5]) ||
  28. (uint8Array[0] === gif89a[0] &&
  29. uint8Array[1] === gif89a[1] &&
  30. uint8Array[2] === gif89a[2] &&
  31. uint8Array[3] === gif89a[3] &&
  32. uint8Array[4] === gif89a[4] &&
  33. uint8Array[5] === gif89a[5])
  34. ) {
  35. resolve(true); // 是GIF文件
  36. } else {
  37. resolve(false); // 不是GIF文件
  38. }
  39. };
  40. reader.onerror = function (error) {
  41. reject(error);
  42. };
  43. });
  44. }

二、解析GIF,并在canvas上播放

GIF本质上和视频一样,都是一帧一帧的图片拼合而成,所以,在canvas上播放GIF功能实现的要点,就是获取具体某一帧的资源。

  1. 使用ImageDecoder API解析GIF每一帧的图像资源;
  2. 将该图像资源绘制在canvas画布上;

注意:目前ImageDecoder仅Chrome浏览器支持,如果考虑兼容性,可以使用libgif.js等第三方库去解析GIF。

1、资源获取

使用fetch方法获取GIF图片资源,注意跨域问题。

  1. fetch("xxx.gif").then((response) => {
  2. // response.body 就是图像资源数据
  3. });

2、使用ImageDecoder解析

imageDecoder对象就包含了一系列的属性和方法,用来对解析好的图像数据进行各种各样的处理。

  1. const imageDecoder = new ImageDecoder({
  2. data: response.body,
  3. type: "image/gif"
  4. });

获取GIF第一帧的图形数据,则可以:

  1. imageDecoder.decode({
  2. frameIndex: 0
  3. }).then((result) => {
  4. // result 对象就是解析后的结果
  5. });

result 对象包括下面这些属性,其中result.image 的返回值是一个 VideoFrame 对象,包含很多属性和方法,例如,帧图像的编码尺寸,显示尺寸,时间戳,时间间隔等,可以作为 ImageSource 绘制在 canvas 画布上。

  1. {
  2. // 解码的图像
  3. image: VideoFrame,
  4. // 如果为true,则表示该图像包含最终的完整细节输出。
  5. complete: boolean
  6. }

result.image.timestamp:当前帧出现的时间戳,单位为微分秒,即万分之一秒

result.image.duration:当前帧持续的时长,单位为微分秒,即万分之一秒

  1. const canvas = document.querySelector("canvas");
  2. const context = canvas.getContext("2d");
  3. context.drawImage(result.image, 0, 0);

3、简单封装

  1. export function giftDecoder(url) {
  2. return new Promise((resolve, reject) => {
  3. fetch(url).then(async (response) => {
  4. // response.body 就是图像资源数据
  5. const imageDecoder = new ImageDecoder({
  6. data: response.body,
  7. type: "image/gif"
  8. });
  9. let _d = await imageDecoder.decode({ frameIndex: 0 })
  10. console.log(imageDecoder, _d)
  11. const track = imageDecoder.tracks.selectedTrack;
  12. console.log(track)
  13. let arr = []
  14. let totalTime = 0
  15. for(let i = 0; i < track.frameCount; i++) {
  16. let result = await imageDecoder.decode({
  17. frameIndex: i
  18. })
  19. if(result) {
  20. // result 对象就是解析后的结果
  21. // 1000000
  22. // timestamp:当前帧出现的时间戳,单位为微分秒
  23. // duration:当前帧持续的时长,单位为微分秒
  24. arr.push(result)
  25. if(i == track.frameCount - 1) {
  26. totalTime = result.image.timestamp + result.image.duration
  27. }
  28. } else {
  29. reject()
  30. break
  31. }
  32. }
  33. console.error('gif数组', arr)
  34. resolve({
  35. totalTime,
  36. list: arr
  37. })
  38. });
  39. })
  40. }
  1. // gif图特殊处理
  2. const gifData = await giftDecoder(gifUrl)
  3. console.error('gif图特殊处理', gifData)

三、使用 gifuct-js 库解析gif

gifuct-js是由Matt Way开发的一个轻量级JavaScript库,专门用于处理和创建GIF动图。它提供了强大的API,让开发者能够轻松地解码、编码、操作及优化GIF文件,适用于网页应用或任何需要在前端处理GIF的场景。

项目地址:https://gitcode.com/matt-way/gifuct-js

技术分析

  • 解码与编码能力
  • Gifuct-js的核心是其高效的GIF解码器和编码器。通过原生JavaScript实现,它能够快速解析GIF文件的逻辑屏幕描述、图像描述、图形控制扩展等信息,同时也能生成符合标准的新GIF文件。
  • 图像操作接口
  • 该项目提供了丰富的API,支持对帧进行添加、删除、重排、调整透明度等操作。例如,你可以通过addFrame方法将新的帧添加到GIF中,或者通过removeFrame方法移除特定帧,以实现动态效果的定制。
  • 性能优化
  • 考虑到前端性能,Gifuct-js设计时就注重了内存管理和运行效率。它采用流式处理,减少了不必要的数据复制,从而降低内存占用并提升处理速度。
  • 兼容性
  • Gifuct-js基于WebAssembly和JavaScript,使其在现代浏览器中具有良好的兼容性。对于不支持WebAssembly的老版本浏览器,项目还提供了一个纯JavaScript的回退方案。

 

1、安装

npm install gifuct-js

2、解码

此解码器使用**js-binary-schema-parser**解析 GIF 文件。这意味着要解码 GIF 文件,首先需要将其转换为 Uint8Array 缓冲区。

fetch

  1. import { parseGIF, decompressFrames } from 'gifuct-js';
  2. var promisedGif = fetch(gifURL)
  3. .then(resp => resp.arrayBuffer())
  4. .then(buff => {
  5. var gif = parseGIF(buff);
  6. var frames = decompressFrames(gif, true);
  7. return gif;
  8. });

XMLHttpRequest

  1. import { parseGIF, decompressFrames } from 'gifuct-js';
  2. var oReq = new XMLHttpRequest();
  3. oReq.open("GET", gifURL, true);
  4. oReq.responseType = "arraybuffer";
  5. oReq.onload = function (oEvent) {
  6. var arrayBuffer = oReq.response; // 注意:不是 oReq.responseText
  7. if (arrayBuffer) {
  8. var gif = parseGIF(arrayBuffer);
  9. var frames = decompressFrames(gif, true);
  10. // 处理帧数据
  11. }
  12. };
  13. oReq.send(null);

结果

decompressFrames(gif, buildPatch) 函数返回一个包含所有 GIF 图像帧及其元数据的数组。

  1. {
  2. // 每个像素的调色板索引
  3. pixels: [...],
  4. // GIF 帧的尺寸(见处置方法)
  5. dims: {
  6. top: 0,
  7. left: 10,
  8. width: 100,
  9. height: 50
  10. },
  11. // 此帧应显示的毫秒数
  12. delay: 50,
  13. // 处置方法(见下文)
  14. disposalType: 1,
  15. // 指向像素数据的颜色数组
  16. colorTable: [...],
  17. // 可选的代表透明度的色彩索引(见下文)
  18. transparentIndex: 33,
  19. // 绘制准备好的 Uint8ClampedArray 颜色补丁信息
  20. patch: [...]
  21. }
自动补丁生成:

如果 dcompressFrames() 函数的 buildPatch 参数为 true,解析器不仅会返回解析和解压缩的 GIF 帧,还会为每个 GIF 帧图像创建可直接用于画布的 Uint8ClampedArray 数组,以便使用 ctx.putImageData() 等方式轻松绘制。这是常见需求,但由于它涉及到透明度假设,所以设为可选项。

处置方法:

像素数据是以每个像素的索引列表存储的。这些索引指向 colorTable 数组中的值,表示每个像素应绘制的颜色。GIF 的每一帧可能不是全尺寸,而是一个需要在特定位置绘制的补丁。disposalType 定义了如何在 GIF 画布上绘制该补丁。大多数情况下,该值为 1,表示 GIF 帧应简单地覆盖现有的 GIF 画布,而不改变补丁尺寸之外的任何像素。

透明性:

如果一帧定义了 transparentIndex,则意味着像素数据中与该索引匹配的任何像素不应被绘制。在使用画布绘图时,这表示将此像素的 alpha 值设置为 0

3、封装

  1. import { parseGIF, decompressFrames } from 'gifuct-js'
  2. export function giftDecoder(url) {
  3. return new Promise((resolve, reject) => {
  4. let arr = []
  5. let totalTime = 0
  6. if('ImageDecoder' in window) {
  7. fetch(url).then(async (response) => {
  8. // response.body 就是图像资源数据
  9. let arr = []
  10. let totalTime = 0
  11. const imageDecoder = new ImageDecoder({
  12. data: response.body,
  13. type: "image/gif"
  14. });
  15. let _d = await imageDecoder.decode({ frameIndex: 0 })
  16. console.log(imageDecoder, _d)
  17. const track = imageDecoder.tracks.selectedTrack;
  18. console.log(track)
  19. for(let i = 0; i < track.frameCount; i++) {
  20. let result = await imageDecoder.decode({
  21. frameIndex: i
  22. })
  23. if(result) {
  24. // result 对象就是解析后的结果
  25. // 1000000
  26. // timestamp:当前帧出现的时间戳,单位为微分秒
  27. // duration:当前帧持续的时长,单位为微分秒
  28. arr.push(result)
  29. if(i == track.frameCount - 1) {
  30. totalTime = result.image.timestamp + result.image.duration
  31. }
  32. } else {
  33. reject()
  34. break
  35. }
  36. }
  37. console.error('gif数组', arr, response)
  38. resolve({
  39. totalTime:totalTime/1000000 ,
  40. list: arr
  41. })
  42. });
  43. } else {
  44. let oReq = new XMLHttpRequest();
  45. oReq.open("GET", url, true);
  46. oReq.responseType = "arraybuffer";
  47. oReq.onload = async function (oEvent) {
  48. let arrayBuffer = oReq.response; // 注意:不是 oReq.responseText
  49. if (arrayBuffer) {
  50. let gif = parseGIF(arrayBuffer);
  51. let frames = decompressFrames(gif, true);
  52. // 处理帧数据
  53. let accumulatedTime = 0
  54. const _canvas = document.createElement('canvas')
  55. const _ctx = _canvas.getContext('2d')
  56. _canvas.width = gif.lsd.width
  57. _canvas.height = gif.lsd.height
  58. for(let i = 0; i < frames.length; i++) {
  59. let frame = frames[i]
  60. // _ctx.clearRect(0, 0, _canvas.width, _canvas.height)
  61. let imageData = _ctx.createImageData(frame.dims.width, frame.dims.height)
  62. imageData.data.set(frame.patch)
  63. _ctx.putImageData(imageData, frame.dims.left, frame.dims.top)
  64. let _blob = await canvasToBlob(_canvas, 'image/png')
  65. let _url = URL.createObjectURL(_blob)
  66. const img = new Image()
  67. img.src = _url
  68. await imgOnload(img)
  69. totalTime += frame.delay
  70. arr.push({
  71. timestamp: accumulatedTime,
  72. duration: frame.delay ,
  73. img: img
  74. })
  75. accumulatedTime += frame.delay
  76. }
  77. resolve({
  78. totalTime: totalTime/1000,
  79. list: arr
  80. })
  81. // console.error('xxx', totalTime, arr)
  82. } else {
  83. reject()
  84. }
  85. };
  86. oReq.send(null);
  87. }
  88. })
  89. }
  90. function canvasToBlob(canvas, type, quality) {
  91. return new Promise(resolve => {
  92. canvas.toBlob((blob) => {
  93. resolve(blob)
  94. }, type, quality)
  95. })
  96. }
  97. function imgOnload(img) {
  98. return new Promise(resolve => {
  99. img.onload = function() {
  100. resolve()
  101. }
  102. })
  103. }

参考文档

https://developer.mozilla.org/en-US/docs/Web/API/VideoFrame

https://developer.mozilla.org/en-US/docs/Web/API/ImageDecoder

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/小丑西瓜9/article/detail/659494
推荐阅读
相关标签
  

闽ICP备14008679号