一、判断是否为GIF图片类型
在JavaScript中,判断用户上传的文件是否为GIF文件类型时,通常可以通过检查文件的type属性或文件的拓展名来判断,但是由于文件拓展名可以轻易被用户修改,type属性是由浏览器根据文件拓展名猜测得出的,因此这种判断方式并不总是准确的。
为了更准确的判断文件类型,需要读取文件的头部字节,并检查这些字节是否符合GIF文件的规范。
要检查文件头部字节以确定文件是否为GIF格式,可以使用JavaScript的FileReader API来读取文件的前几个字节,并与GIF文件的魔数(Magic Number)进行比较。GIF文件的魔数是GIF87a或GIF89a,它们位于文件的前几个字节中。
javascript
// 文件输入元素
const fileInput = document.getElementById('file-input')
// 监听文件变化事件
fileInput.addEventListener('change', async fucntion(e) {
// 获取用户选择的文件
const file = e.target.files[0]
if(file) {
const isGif = await checkGifFileType(file)
if(isGif) {
// 是gif
} else {
// 不是gif
}
}
})
javascript
/**
* 检查文件类型是否是gif
*
* @param {*} file
* @returns
*/
export function checkGifFileType(file) {
// 创建一个FileReader对象
const reader = new FileReader();
// 读取文件的前几个字节
reader.readAsArrayBuffer(file.slice(0, 6)); // GIF的魔数只需要6个字节
return new Promise((resolve, reject) => {
reader.onload = function (e) {
// 将ArrayBuffer转换为Uint8Array以便读取字节
const arrayBuffer = e.target.result;
const uint8Array = new Uint8Array(arrayBuffer);
// GIF文件的魔数可以是'GIF87a'或'GIF89a',转换为ASCII码分别为[71, 73, 70, 56, 55, 97]或[71, 73, 70, 56, 57, 97]
// 检查文件的前6个字节是否匹配GIF的魔数
const gif87a = [71, 73, 70, 56, 55, 97];
const gif89a = [71, 73, 70, 56, 57, 97];
if (
(uint8Array[0] === gif87a[0] &&
uint8Array[1] === gif87a[1] &&
uint8Array[2] === gif87a[2] &&
uint8Array[3] === gif87a[3] &&
uint8Array[4] === gif87a[4] &&
uint8Array[5] === gif87a[5]) ||
(uint8Array[0] === gif89a[0] &&
uint8Array[1] === gif89a[1] &&
uint8Array[2] === gif89a[2] &&
uint8Array[3] === gif89a[3] &&
uint8Array[4] === gif89a[4] &&
uint8Array[5] === gif89a[5])
) {
resolve(true); // 是GIF文件
} else {
resolve(false); // 不是GIF文件
}
};
reader.onerror = function (error) {
reject(error);
};
});
}
二、解析GIF,并在canvas上播放
GIF本质上和视频一样,都是一帧一帧的图片拼合而成,所以,在canvas上播放GIF功能实现的要点,就是获取具体某一帧的资源。
- 使用ImageDecoder API解析GIF每一帧的图像资源;
- 将该图像资源绘制在canvas画布上;
注意:目前ImageDecoder仅Chrome浏览器支持,如果考虑兼容性,可以使用libgif.js等第三方库去解析GIF。
1、资源获取
使用fetch
方法获取GIF图片资源,注意跨域问题。
javascript
fetch("xxx.gif").then((response) => {
// response.body 就是图像资源数据
});
2、使用ImageDecoder解析
imageDecoder
对象就包含了一系列的属性和方法,用来对解析好的图像数据进行各种各样的处理。
javascript
const imageDecoder = new ImageDecoder({
data: response.body,
type: "image/gif"
});
获取GIF第一帧的图形数据,则可以:
javascript
imageDecoder.decode({
frameIndex: 0
}).then((result) => {
// result 对象就是解析后的结果
});
result 对象包括下面这些属性,其中result.image
的返回值是一个 VideoFrame
对象,包含很多属性和方法,例如,帧图像的编码尺寸,显示尺寸,时间戳,时间间隔等,可以作为 ImageSource 绘制在 canvas 画布上。
javascript
{
// 解码的图像
image: VideoFrame,
// 如果为true,则表示该图像包含最终的完整细节输出。
complete: boolean
}
result.image.timestamp:当前帧出现的时间戳,单位为微分秒,即万分之一秒
result.image.duration:当前帧持续的时长,单位为微分秒,即万分之一秒
javascript
const canvas = document.querySelector("canvas");
const context = canvas.getContext("2d");
context.drawImage(result.image, 0, 0);
3、简单封装
javascript
export function giftDecoder(url) {
return new Promise((resolve, reject) => {
fetch(url).then(async (response) => {
// response.body 就是图像资源数据
const imageDecoder = new ImageDecoder({
data: response.body,
type: "image/gif"
});
let _d = await imageDecoder.decode({ frameIndex: 0 })
console.log(imageDecoder, _d)
const track = imageDecoder.tracks.selectedTrack;
console.log(track)
let arr = []
let totalTime = 0
for(let i = 0; i < track.frameCount; i++) {
let result = await imageDecoder.decode({
frameIndex: i
})
if(result) {
// result 对象就是解析后的结果
// 1000000
// timestamp:当前帧出现的时间戳,单位为微分秒
// duration:当前帧持续的时长,单位为微分秒
arr.push(result)
if(i == track.frameCount - 1) {
totalTime = result.image.timestamp + result.image.duration
}
} else {
reject()
break
}
}
console.error('gif数组', arr)
resolve({
totalTime,
list: arr
})
});
})
}
javascript
// gif图特殊处理
const gifData = await giftDecoder(gifUrl)
console.error('gif图特殊处理', gifData)
三、使用 gifuct-js 库解析gif
gifuct-js是由Matt Way开发的一个轻量级JavaScript库,专门用于处理和创建GIF动图。它提供了强大的API,让开发者能够轻松地解码、编码、操作及优化GIF文件,适用于网页应用或任何需要在前端处理GIF的场景。
技术分析
- 解码与编码能力
- Gifuct-js的核心是其高效的GIF解码器和编码器。通过原生JavaScript实现,它能够快速解析GIF文件的逻辑屏幕描述、图像描述、图形控制扩展等信息,同时也能生成符合标准的新GIF文件。
- 图像操作接口
- 该项目提供了丰富的API,支持对帧进行添加、删除、重排、调整透明度等操作。例如,你可以通过addFrame方法将新的帧添加到GIF中,或者通过removeFrame方法移除特定帧,以实现动态效果的定制。
- 性能优化
- 考虑到前端性能,Gifuct-js设计时就注重了内存管理和运行效率。它采用流式处理,减少了不必要的数据复制,从而降低内存占用并提升处理速度。
- 兼容性
- Gifuct-js基于WebAssembly和JavaScript,使其在现代浏览器中具有良好的兼容性。对于不支持WebAssembly的老版本浏览器,项目还提供了一个纯JavaScript的回退方案。
1、安装
bash
npm install gifuct-js
2、解码
此解码器使用**js-binary-schema-parser**解析 GIF 文件。这意味着要解码 GIF 文件,首先需要将其转换为 Uint8Array
缓冲区。
fetch
javascript
import { parseGIF, decompressFrames } from 'gifuct-js';
var promisedGif = fetch(gifURL)
.then(resp => resp.arrayBuffer())
.then(buff => {
var gif = parseGIF(buff);
var frames = decompressFrames(gif, true);
return gif;
});
XMLHttpRequest
javascript
import { parseGIF, decompressFrames } from 'gifuct-js';
var oReq = new XMLHttpRequest();
oReq.open("GET", gifURL, true);
oReq.responseType = "arraybuffer";
oReq.onload = function (oEvent) {
var arrayBuffer = oReq.response; // 注意:不是 oReq.responseText
if (arrayBuffer) {
var gif = parseGIF(arrayBuffer);
var frames = decompressFrames(gif, true);
// 处理帧数据
}
};
oReq.send(null);
结果
decompressFrames(gif, buildPatch)
函数返回一个包含所有 GIF 图像帧及其元数据的数组。
javascript
{
// 每个像素的调色板索引
pixels: [...],
// GIF 帧的尺寸(见处置方法)
dims: {
top: 0,
left: 10,
width: 100,
height: 50
},
// 此帧应显示的毫秒数
delay: 50,
// 处置方法(见下文)
disposalType: 1,
// 指向像素数据的颜色数组
colorTable: [...],
// 可选的代表透明度的色彩索引(见下文)
transparentIndex: 33,
// 绘制准备好的 Uint8ClampedArray 颜色补丁信息
patch: [...]
}
自动补丁生成:
如果 dcompressFrames()
函数的 buildPatch
参数为 true
,解析器不仅会返回解析和解压缩的 GIF 帧,还会为每个 GIF 帧图像创建可直接用于画布的 Uint8ClampedArray
数组,以便使用 ctx.putImageData()
等方式轻松绘制。这是常见需求,但由于它涉及到透明度假设,所以设为可选项。
处置方法:
像素数据是以每个像素的索引列表存储的。这些索引指向 colorTable
数组中的值,表示每个像素应绘制的颜色。GIF 的每一帧可能不是全尺寸,而是一个需要在特定位置绘制的补丁。disposalType
定义了如何在 GIF 画布上绘制该补丁。大多数情况下,该值为 1
,表示 GIF 帧应简单地覆盖现有的 GIF 画布,而不改变补丁尺寸之外的任何像素。
透明性:
如果一帧定义了 transparentIndex
,则意味着像素数据中与该索引匹配的任何像素不应被绘制。在使用画布绘图时,这表示将此像素的 alpha 值设置为 0
。
3、封装
javascript
import { parseGIF, decompressFrames } from 'gifuct-js'
export function giftDecoder(url) {
return new Promise((resolve, reject) => {
let arr = []
let totalTime = 0
if('ImageDecoder' in window) {
fetch(url).then(async (response) => {
// response.body 就是图像资源数据
let arr = []
let totalTime = 0
const imageDecoder = new ImageDecoder({
data: response.body,
type: "image/gif"
});
let _d = await imageDecoder.decode({ frameIndex: 0 })
console.log(imageDecoder, _d)
const track = imageDecoder.tracks.selectedTrack;
console.log(track)
for(let i = 0; i < track.frameCount; i++) {
let result = await imageDecoder.decode({
frameIndex: i
})
if(result) {
// result 对象就是解析后的结果
// 1000000
// timestamp:当前帧出现的时间戳,单位为微分秒
// duration:当前帧持续的时长,单位为微分秒
arr.push(result)
if(i == track.frameCount - 1) {
totalTime = result.image.timestamp + result.image.duration
}
} else {
reject()
break
}
}
console.error('gif数组', arr, response)
resolve({
totalTime:totalTime/1000000 ,
list: arr
})
});
} else {
let oReq = new XMLHttpRequest();
oReq.open("GET", url, true);
oReq.responseType = "arraybuffer";
oReq.onload = async function (oEvent) {
let arrayBuffer = oReq.response; // 注意:不是 oReq.responseText
if (arrayBuffer) {
let gif = parseGIF(arrayBuffer);
let frames = decompressFrames(gif, true);
// 处理帧数据
let accumulatedTime = 0
const _canvas = document.createElement('canvas')
const _ctx = _canvas.getContext('2d')
_canvas.width = gif.lsd.width
_canvas.height = gif.lsd.height
for(let i = 0; i < frames.length; i++) {
let frame = frames[i]
// _ctx.clearRect(0, 0, _canvas.width, _canvas.height)
let imageData = _ctx.createImageData(frame.dims.width, frame.dims.height)
imageData.data.set(frame.patch)
_ctx.putImageData(imageData, frame.dims.left, frame.dims.top)
let _blob = await canvasToBlob(_canvas, 'image/png')
let _url = URL.createObjectURL(_blob)
const img = new Image()
img.src = _url
await imgOnload(img)
totalTime += frame.delay
arr.push({
timestamp: accumulatedTime,
duration: frame.delay ,
img: img
})
accumulatedTime += frame.delay
}
resolve({
totalTime: totalTime/1000,
list: arr
})
// console.error('xxx', totalTime, arr)
} else {
reject()
}
};
oReq.send(null);
}
})
}
function canvasToBlob(canvas, type, quality) {
return new Promise(resolve => {
canvas.toBlob((blob) => {
resolve(blob)
}, type, quality)
})
}
function imgOnload(img) {
return new Promise(resolve => {
img.onload = function() {
resolve()
}
})
}
参考文档
https://developer.mozilla.org/en-US/docs/Web/API/VideoFrame
https://developer.mozilla.org/en-US/docs/Web/API/ImageDecoder