gif-frame-count
Version:
在前端项目内获取GIF图片时长、帧数等信息(可在任意一个基于JS开发的框架中运行),包括小程序和app
251 lines (236 loc) • 8.09 kB
text/typescript
import type { gifInfoType } from "@/type/GifInfoType";
import { getBitArray } from "@/lib/GetBitArray";
import { getFrameDuration, getPaletteSize } from "@/lib/GetPaletteSize";
import { FrameImage } from "@/model/FrameImage";
import { byteToString, readSubBlock } from "@/lib/ReadSubBlock";
import { getGlobalPaletteInfo } from "@/lib/GetGlobalPalette";
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?: unknown): 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);
// 解析全局调色板
const { pos, PaletteColorsRGB } = getGlobalPaletteInfo(
this.dataView,
this.gifInfo,
this.pos
);
// 修改指针位置
this.pos = pos;
// 设置全局调色板
this.gifInfo.globalPaletteColorsRGB = PaletteColorsRGB;
// gif每一帧的图片描述
let frame = new FrameImage();
// 循环读取dataView, 读取gif每一帧的数据信息
while (true) {
try {
const block = this.dataView.getUint8(this.pos);
switch (block) {
// 图形控制扩展
case 0x21:
this.parseGraphicsControlBlock(this.dataView, frame);
break;
// 图片描述块数据
case 0x2c:
frame.imageInfo.left = this.dataView.getUint16(this.pos + 1, true);
frame.imageInfo.top = this.dataView.getUint16(this.pos + 3, true);
frame.imageInfo.width = this.dataView.getUint16(this.pos + 5, true);
frame.imageInfo.height = this.dataView.getUint16(
this.pos + 7,
true
);
const unpackedField = getBitArray(
this.dataView.getUint8(this.pos + 9)
);
if (unpackedField[0]) {
// 本地块数据
const localPaletteSize = getPaletteSize(unpackedField);
frame.imageInfo.localPalette = true;
frame.imageInfo.localPaletteSize = localPaletteSize / 3;
this.pos += localPaletteSize;
}
if (unpackedField[1]) {
// 帧交错标识
frame.imageInfo.interlace = true;
}
this.gifInfo.images.push(frame.imageInfo);
this.index++;
// 重置frame,继续下一轮解析
frame = new FrameImage();
frame.imageInfo.identifier = this.index.toString();
// 判断当前GIF是否可以动
if (this.gifInfo.images.length > 1 && !this.gifInfo.animated) {
this.gifInfo.animated = true;
}
this.pos += 11;
const subBlock = readSubBlock(this.dataView, this.pos, false);
this.pos += subBlock.size;
break;
// 末尾数据块
case 0x3b:
return this.gifInfo;
// 未知数据块
default:
this.pos++;
break;
}
} catch (e) {
this.gifInfo.valid = false;
return this.gifInfo;
}
}
}
// 获取GIF加载状态
private gifLoadStatus(): Promise<boolean> {
return new Promise<boolean>((resolve) => {
let time = 0;
// 超时时间
const defaultTimeOut = 5000;
const timer = setInterval(() => {
time += 100;
// 请求超时
if (time >= defaultTimeOut) {
clearInterval(timer);
resolve(false);
}
// 加载完毕
if (this.urlLoadStatus === true) {
clearInterval(timer);
resolve(true);
}
}, 100);
});
}
// 图形控制块数据解析
private parseGraphicsControlBlock(dataView: DataView, frame: FrameImage) {
const type = dataView.getUint8(this.pos + 1);
// 图形控制块
if (type === 0xf9) {
const length = dataView.getUint8(this.pos + 2);
if (length === 4) {
const delay = getFrameDuration(dataView.getUint16(this.pos + 4, true));
frame.imageInfo.delay = delay;
// gif总时长自增
this.gifInfo.duration += delay;
const unpackedField = getBitArray(dataView.getUint8(this.pos + 3));
const disposal = unpackedField.slice(3, 6).join("");
frame.imageInfo.disposal = parseInt(disposal, 2);
this.pos += 8;
return;
}
this.pos++;
return;
}
// 非图形控制块
this.pos += 2;
// 获取子模块,做进一步解析
const subBlock = readSubBlock(dataView, this.pos, true);
switch (type) {
// 应用扩展块数据
case 0xff:
const identifier = byteToString(dataView, 8, this.pos + 1);
// 可能会出现多个应用程序扩展块,我们需要确保仅在标识符为 NETSCAPE 时处理GIF循环次数
if (identifier === "NETSCAPE") {
this.gifInfo.loopCount = dataView.getUint8(this.pos + 14);
}
break;
// 帧标识块数据
case 0xce:
frame.imageInfo.identifier = subBlock.data;
break;
// 解释块数据
case 0xfe:
frame.imageInfo.comments.push(subBlock.data);
break;
// 纯文本扩展块数据
case 0x01:
frame.imageInfo.text = subBlock.data;
break;
default:
break;
}
this.pos += subBlock.size;
}
// gif文件流有效性校验
private async validityCheck(gifStream: any) {
if (typeof this.urlLoadStatus !== "undefined" && !this.urlLoadStatus) {
// 等待url加载成功
const loadResult = await this.gifLoadStatus();
if (!loadResult) {
throw new Error("GIF加载超时");
}
}
// 如果文件流存在则使用gifStream参数作为解析源
if (gifStream) {
// const gifBuffer = await gifStream.arrayBuffer();
const gifBuffer = gifStream;
// 字节长度必须大于10才能继续解析
if (gifBuffer instanceof ArrayBuffer && gifBuffer.byteLength < 10) {
return this.gifInfo;
}
// 判断gifBuffer类型并进行相应处理
if (gifBuffer instanceof ArrayBuffer) {
this.dataView = new DataView(gifBuffer as ArrayBuffer);
} else {
const buffer = await gifBuffer.arrayBuffer();
this.dataView = new DataView(buffer);
}
}
}
}