├── commitlint.config.js ├── public ├── favicon.ico └── index.html ├── babel.config.js ├── .editorconfig ├── .prettierrc.json ├── src ├── lib │ ├── GetBitArray.ts │ ├── GetPaletteSize.ts │ ├── ReadSubBlock.ts │ └── GetGlobalPalette.ts ├── model │ └── FrameImage.ts ├── type │ └── GifInfoType.ts └── main.ts ├── .gitignore ├── webstorm.config.js ├── vue.config.js ├── .eslintrc.js ├── tsconfig.json ├── license ├── README.md └── package.json /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { extends: ["@commitlint/config-angular"] }; 2 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/likaia/gif-parser-web/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ["@vue/cli-plugin-babel/preset"], 3 | plugins: [] 4 | }; 5 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | # 对所有文件有效 4 | [*] 5 | charset = utf-8 6 | tab_width = 2 7 | indent_style = space 8 | indent_size = 2 -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 2, 3 | "useTabs": false, 4 | "endOfLine": "auto", 5 | "singleQuote": false, 6 | "semi": true, 7 | "trailingComma": "none", 8 | "bracketSpacing": true 9 | } 10 | -------------------------------------------------------------------------------- /src/lib/GetBitArray.ts: -------------------------------------------------------------------------------- 1 | // 获取二进制数组 2 | export function getBitArray(val: number): Array { 3 | const bits = []; 4 | for (let i = 7; i >= 0; i--) { 5 | bits.push(val & (1 << i) ? 1 : 0); 6 | } 7 | return bits; 8 | } 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | 6 | # local env files 7 | .env.local 8 | .env.*.local 9 | 10 | # Log files 11 | npm-debug.log* 12 | yarn-debug.log* 13 | yarn-error.log* 14 | pnpm-debug.log* 15 | 16 | # Editor directories and files 17 | .idea 18 | .vscode 19 | *.suo 20 | *.ntvs* 21 | *.njsproj 22 | *.sln 23 | *.sw? 24 | -------------------------------------------------------------------------------- /src/lib/GetPaletteSize.ts: -------------------------------------------------------------------------------- 1 | // 获取调色板大小 2 | export function getPaletteSize(palette: Array): number { 3 | return 3 * Math.pow(2, 1 + bitToInt(palette.slice(5, 8))); 4 | } 5 | 6 | // 获取每一帧gif的持续时间 7 | export function getFrameDuration(duration: number): number { 8 | return (duration / 100) * 1000; 9 | } 10 | 11 | // 二进制转十进制 12 | function bitToInt(bitArray: Array) { 13 | return bitArray.reduce(function (s, n) { 14 | return s * 2 + n; 15 | }, 0); 16 | } 17 | -------------------------------------------------------------------------------- /src/model/FrameImage.ts: -------------------------------------------------------------------------------- 1 | import { frameImageType } from "@/type/GifInfoType"; 2 | 3 | export class FrameImage { 4 | public imageInfo: frameImageType; 5 | 6 | constructor() { 7 | this.imageInfo = { 8 | identifier: "0", // 帧标识(当前为第几帧画面) 9 | localPalette: false, 10 | localPaletteSize: 0, 11 | interlace: false, 12 | comments: [], 13 | text: "", 14 | left: 0, 15 | top: 0, 16 | width: 0, 17 | height: 0, 18 | delay: 0, 19 | disposal: 0 20 | }; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /webstorm.config.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | // eslint-disable-next-line @typescript-eslint/no-var-requires 3 | const path = require("path"); 4 | 5 | function resolve(dir) { 6 | return path.join(__dirname, ".", dir); 7 | } 8 | 9 | module.exports = { 10 | context: path.resolve(__dirname, "./"), 11 | resolve: { 12 | extensions: [".js", ".vue", ".json"], 13 | alias: { 14 | "@": resolve("src"), 15 | "@views": resolve("src/views"), 16 | "@comp": resolve("src/components"), 17 | "@core": resolve("src/core"), 18 | "@utils": resolve("src/utils") 19 | } 20 | } 21 | }; 22 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | <%= htmlWebpackPlugin.options.title %> 9 | 10 | 11 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /vue.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | // 强制css内联,不然会导致样式失效问题 3 | css: { extract: false }, 4 | assetsDir: "static", 5 | productionSourceMap: false, 6 | chainWebpack: (config) => { 7 | if (process.env.NODE_ENV === "production") { 8 | config.module.rule("ts").uses.delete("cache-loader"); 9 | config.module 10 | .rule("ts") 11 | .use("ts-loader") 12 | .loader("ts-loader") 13 | .tap((opts) => { 14 | opts.transpileOnly = false; 15 | opts.happyPackMode = false; 16 | return opts; 17 | }); 18 | } 19 | }, 20 | // 自定义webpack配置 21 | configureWebpack: { 22 | output: { 23 | // 对外暴露default属性 24 | libraryExport: "default" 25 | } 26 | }, 27 | parallel: false 28 | }; 29 | -------------------------------------------------------------------------------- /src/type/GifInfoType.ts: -------------------------------------------------------------------------------- 1 | export type gifInfoType = { 2 | // GIF的有效性 3 | valid: boolean; 4 | globalPalette: boolean; 5 | globalPaletteSize: number; 6 | // 全局调色板RGB信息数组 7 | globalPaletteColorsRGB: Array<{ r: number; g: number; b: number }>; 8 | loopCount: number; 9 | height: number; 10 | width: number; 11 | animated: boolean; 12 | images: Array; 13 | // 帧标识(当前为第几帧画面) 14 | identifier: string; 15 | duration: number; 16 | }; 17 | 18 | export type frameImageType = { 19 | identifier: string; 20 | localPalette: boolean; 21 | localPaletteSize: number; 22 | interlace: boolean; 23 | comments: Array; 24 | text: string; 25 | left: number; 26 | top: number; 27 | width: number; 28 | height: number; 29 | delay: number; 30 | disposal: number; 31 | }; 32 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | node: true 5 | }, 6 | extends: [ 7 | "eslint:recommended", 8 | "@vue/typescript/recommended", 9 | "@vue/prettier", 10 | "@vue/prettier/@typescript-eslint" 11 | ], 12 | parserOptions: { 13 | ecmaVersion: 2020 14 | }, 15 | plugins: [ 16 | // 用到的插件 17 | "@typescript-eslint", 18 | "prettier" 19 | ], 20 | rules: { 21 | "no-console": process.env.NODE_ENV === "production" ? "warn" : "off", 22 | "no-debugger": process.env.NODE_ENV === "production" ? "warn" : "off", 23 | "prettier/prettier": "error", // prettier标记的地方抛出错误信息 24 | "spaced-comment": [2, "always"], // 注释后面必须写两个空格 25 | "@typescript-eslint/no-explicit-any": ["off"], // 关闭any校验 26 | "no-constant-condition": "off", // 允许常量作为语句的表达式 27 | "no-case-declarations": "off" // 允许在case块内声明变量 28 | } 29 | }; 30 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "esnext", 5 | "strict": true, 6 | "jsx": "preserve", 7 | "importHelpers": true, 8 | "moduleResolution": "node", 9 | "skipLibCheck": true, 10 | "esModuleInterop": true, 11 | "allowSyntheticDefaultImports": true, 12 | "sourceMap": false, 13 | "baseUrl": ".", 14 | "types": [ 15 | "webpack-env" 16 | ], 17 | // 配置别名 18 | "paths": { 19 | "@/*": [ 20 | "src/*" 21 | ] 22 | }, 23 | "declaration": true,// 是否生成声明文件 24 | "declarationDir": "dist/type",// 声明文件打包的位置 25 | "lib": [ 26 | "esnext", 27 | "dom", 28 | "dom.iterable", 29 | "scripthost" 30 | ] 31 | }, 32 | "include": [ 33 | "src/**/*.ts", 34 | "tests/**/*.ts", 35 | ], 36 | "exclude": [ 37 | "node_modules" 38 | ] 39 | } 40 | -------------------------------------------------------------------------------- /src/lib/ReadSubBlock.ts: -------------------------------------------------------------------------------- 1 | // 读取GIF的子数据块 2 | export function readSubBlock( 3 | view: DataView, 4 | pos: number, 5 | read: boolean 6 | ): { data: string; size: number } { 7 | const subBlock = { 8 | data: "", 9 | size: 0 10 | }; 11 | while (true) { 12 | const size = view.getUint8(pos + subBlock.size); 13 | if (size === 0) { 14 | subBlock.size++; 15 | break; 16 | } 17 | if (read) { 18 | // 将当前指针指向的数据块内容转为字符串数据 19 | subBlock.data += byteToString(view, size, pos + subBlock.size + 1); 20 | } 21 | subBlock.size += size + 1; 22 | } 23 | return subBlock; 24 | } 25 | 26 | export function byteToString( 27 | view: DataView, 28 | length: number, 29 | byteOffset: number 30 | ): string { 31 | let value = ""; 32 | for (let i = 0; i < length; ++i) { 33 | const char = view.getUint8(byteOffset + i); 34 | value += String.fromCharCode(char > 127 ? 65533 : char); 35 | } 36 | return value; 37 | } 38 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 gif-parser-web 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/lib/GetGlobalPalette.ts: -------------------------------------------------------------------------------- 1 | import { getBitArray } from "@/lib/GetBitArray"; 2 | import { getPaletteSize } from "@/lib/GetPaletteSize"; 3 | import { gifInfoType } from "@/type/GifInfoType"; 4 | 5 | /** 6 | * 获取全局调色板信息 7 | * @param dataView 8 | * @param gifInfo 9 | * @param pos 10 | */ 11 | export function getGlobalPaletteInfo( 12 | dataView: DataView, 13 | gifInfo: gifInfoType, 14 | pos: number 15 | ): { 16 | pos: number; 17 | PaletteColorsRGB: Array<{ r: number; g: number; b: number }>; 18 | } { 19 | const PaletteColorsRGB = []; 20 | // 解析全局调色板 21 | const unpackedField = getBitArray(dataView.getUint8(10)); 22 | if (unpackedField[0]) { 23 | const globalPaletteSize = getPaletteSize(unpackedField); 24 | gifInfo.globalPalette = true; 25 | // 计算全局调色板的大小 26 | gifInfo.globalPaletteSize = globalPaletteSize / 3; 27 | // 调整指针位置 28 | pos += globalPaletteSize; 29 | // 遍历获取此块区域的所有颜色并存起来 30 | for (let i = 0; i < gifInfo.globalPaletteSize; i++) { 31 | const palettePos = 13 + i * 3; 32 | const r = dataView.getUint8(palettePos); 33 | const g = dataView.getUint8(palettePos + 1); 34 | const b = dataView.getUint8(palettePos + 2); 35 | PaletteColorsRGB.push({ r, g, b }); 36 | } 37 | } 38 | pos += 13; 39 | return { 40 | pos, 41 | PaletteColorsRGB 42 | }; 43 | } 44 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # gif-parser-web · [![npm](https://img.shields.io/badge/npm-v1.0.5-2081C1)](https://www.npmjs.com/package/gif-parser-web) [![yarn](https://img.shields.io/badge/yarn-v1.0.5-F37E42)](https://yarnpkg.com/package/gif-parser-web) [![github](https://img.shields.io/badge/GitHub-depositary-9A9A9A)](https://github.com/likaia/gif-parser-web) [![](https://img.shields.io/github/issues/likaia/gif-parser-web)](https://github.com/likaia/gif-parser-web/issues) [![]( https://img.shields.io/github/forks/likaia/gif-parser-web)](``https://github.com/likaia/gif-parser-web/network/members) [![]( https://img.shields.io/github/stars/likaia/gif-parser-web)](https://github.com/likaia/gif-parser-web/stargazers) 2 | 3 | ## 写在前面 4 | 关于此插件的更多介绍以及实现原理请移步👉:[JS获取GIF总帧数](https://www.kaisir.cn/post/142) 5 | 6 | ## 插件安装 7 | ```bash 8 | # yarn安装 9 | yarn add gif-parser-web 10 | 11 | # npm安装 12 | npm install gif-parser-web --save 13 | ``` 14 | 15 | ## 插件使用 16 | 由于插件采用原生js编写且不依赖任何第三方库,因此它可以在任意一台支持js的设备上运行。 17 | 18 | ### import形式使用插件 19 | * 在需要获取Gif图像信息的业务代码中导入插件 20 | ```javascript 21 | import GifParse from "gif-parser-web"; 22 | ``` 23 | * 在业务代码中使用时实例化插件,调用对应的方法即可 24 | ```javascript 25 | const gifParse = new GifParse("插件支持传入一个图像url作为可选参数"); 26 | const gifInfo = gifParse.getInfo("此处支持File类型的数据作为可选参数,如果传入则使用此处的参数作为gif数据源"); 27 | gifInfo.then((res)=>{ 28 | console.log("解析完成", res); 29 | }) 30 | ``` 31 | 32 | ### cdn形式使用插件 33 | * 将插件的`dist`文件夹复制到你的项目中 34 | * 使用`script`标签引入dist目录下的`gifParserPlugin.umd.js`文件 35 | ```javascript 36 | 37 | ``` 38 | * 在业务代码中使用时实例化插件,调用对应的方法即可 39 | ```javascript 40 | const gifParse = new gifParserPlugin("插件支持传入一个图像url作为可选参数"); 41 | const gifInfo = gifParse.getInfo("此处支持File类型的数据作为可选参数,如果传入则使用此处的参数作为gif数据源"); 42 | gifInfo.then((res)=>{ 43 | console.log("解析完成", res); 44 | }) 45 | ``` 46 | > 注意⚠️:GitHub中是不会上传dist目录的,你想要自己将项目clone到本地,编译得到dist文件夹。 47 | > 48 | >当然,你也可以直接下载[gifParserPlugin.umd.js](https://unpkg.com/gif-parser-web@1.0.5/dist/gifParserPlugin.umd.js)文件来使用 49 | 50 | 51 | ## 写在最后 52 | 至此,插件的使用方法就介绍完了。 53 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gif-parser-web", 3 | "description": "在web前端项目内获取GIF图片时长、帧数等信息(可在任意一个基于JS开发的框架中运行)", 4 | "version": "1.0.5", 5 | "main": "dist/gifParserPlugin.common.js", 6 | "types": "dist/type/main.d.ts", 7 | "publisher": "magicalprogrammer@qq.com", 8 | "private": false, 9 | "scripts": { 10 | "build": "vue-cli-service build --target lib --name gifParserPlugin src/main.ts", 11 | "changelog": "conventional-changelog -p angular -i CHANGELOG.md -s", 12 | "commit": "git-cz" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "git+https://github.com/likaia/gif-parser-web.git" 17 | }, 18 | "keywords": [ 19 | "gif-parser", 20 | "web-gif-parser", 21 | "gif-parser-web", 22 | "js-gif-parser", 23 | "gif-parser-js", 24 | "获取gif时长", 25 | "解析gif", 26 | "gif解析器", 27 | "js解析gif" 28 | ], 29 | "dependencies": { 30 | }, 31 | "author": "likaia", 32 | "license": "MIT", 33 | "bugs": { 34 | "url": "https://github.com/likaia/gif-parser-web/issues" 35 | }, 36 | "homepage": "https://github.com/likaia/gif-parser-web#readme", 37 | "devDependencies": { 38 | "@commitlint/cli": "^11.0.0", 39 | "@commitlint/config-angular": "^11.0.0", 40 | "@vue/cli-plugin-babel": "~4.5.0", 41 | "@typescript-eslint/eslint-plugin": "^4.18.0", 42 | "@typescript-eslint/parser": "^4.18.0", 43 | "@vue/cli-plugin-eslint": "~4.5.12", 44 | "@vue/cli-plugin-typescript": "~4.5.12", 45 | "@vue/cli-service": "~4.5.12", 46 | "@vue/eslint-config-prettier": "^6.0.0", 47 | "@vue/eslint-config-typescript": "^7.0.0", 48 | "eslint": "^6.7.2", 49 | "eslint-plugin-prettier": "^3.3.1", 50 | "eslint-plugin-vue": "^6.2.2", 51 | "prettier": "^2.2.1", 52 | "typescript": "~4.1.5", 53 | "commitizen": "^4.2.2", 54 | "cz-conventional-changelog": "^3.3.0", 55 | "husky": "^4.3.0" 56 | }, 57 | "config": { 58 | "commitizen": { 59 | "path": "./node_modules/cz-conventional-changelog" 60 | } 61 | }, 62 | "husky": { 63 | "hooks": { 64 | "commit-msg": "commitlint -E HUSKY_GIT_PARAMS" 65 | } 66 | }, 67 | "volta": { 68 | "node": "14.16.0", 69 | "yarn": "1.22.17" 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import type { gifInfoType } from "@/type/GifInfoType"; 2 | import { getBitArray } from "@/lib/GetBitArray"; 3 | import { getFrameDuration, getPaletteSize } from "@/lib/GetPaletteSize"; 4 | import { FrameImage } from "@/model/FrameImage"; 5 | import { byteToString, readSubBlock } from "@/lib/ReadSubBlock"; 6 | import { getGlobalPaletteInfo } from "@/lib/GetGlobalPalette"; 7 | 8 | export default class GifParser { 9 | private urlLoadStatus: boolean | undefined = undefined; 10 | private dataView: DataView | undefined; 11 | // 当前指向DataView的指针位置 12 | private pos = 0; 13 | // 当前解析的帧索引 14 | private index = 0; 15 | private gifInfo: gifInfoType = { 16 | valid: false, 17 | globalPalette: false, 18 | globalPaletteSize: 0, 19 | globalPaletteColorsRGB: [], 20 | loopCount: 0, 21 | height: 0, 22 | width: 0, 23 | animated: false, 24 | images: [], 25 | duration: 0, 26 | identifier: "0" 27 | }; 28 | 29 | constructor(url?: string) { 30 | if (url) { 31 | this.urlLoadStatus = false; 32 | // 解析url,将其转化为DataView格式的数据 33 | fetch(url) 34 | .then((response) => response.arrayBuffer()) 35 | .then((arrayBuffer) => { 36 | return new DataView(arrayBuffer); 37 | }) 38 | .then((dataView) => { 39 | // GIF加载成功 40 | this.urlLoadStatus = true; 41 | this.dataView = dataView; 42 | }); 43 | } 44 | } 45 | /** 46 | * 获取图像信息 47 | * @param gifStream 48 | */ 49 | public async getInfo(gifStream?: File): Promise { 50 | // 参数有效性校验 51 | await this.validityCheck(gifStream); 52 | // url与gifStream都未传入则抛出异常 53 | if (this.dataView == null) { 54 | throw new Error("未找到GIF解析源, 请检查参数是否正确传入"); 55 | } 56 | 57 | // 只解析GIF8格式的图像:使用getUint16获取2个字节十六进制值,判断它是否满足Gif格式的Header块的签名与版本号 58 | // 47 49 为签名信息,转换为Unicode编码为:G I 59 | // 46 38 为版本信息,转换为Unicode编码为:F 8 60 | if ( 61 | this.dataView.getUint16(0) != 0x4749 || 62 | this.dataView.getUint16(2) != 0x4638 63 | ) { 64 | return this.gifInfo; 65 | } 66 | // 经过上述判断后,此时的GIF已经有效了 67 | this.gifInfo.valid = true; 68 | // 获取GIF图像的宽,高 69 | this.gifInfo.width = this.dataView.getUint16(6, true); 70 | this.gifInfo.height = this.dataView.getUint16(8, true); 71 | 72 | // 解析全局调色板 73 | const { pos, PaletteColorsRGB } = getGlobalPaletteInfo( 74 | this.dataView, 75 | this.gifInfo, 76 | this.pos 77 | ); 78 | // 修改指针位置 79 | this.pos = pos; 80 | // 设置全局调色板 81 | this.gifInfo.globalPaletteColorsRGB = PaletteColorsRGB; 82 | 83 | // gif每一帧的图片描述 84 | let frame = new FrameImage(); 85 | // 循环读取dataView, 读取gif每一帧的数据信息 86 | while (true) { 87 | try { 88 | const block = this.dataView.getUint8(this.pos); 89 | switch (block) { 90 | // 图形控制扩展 91 | case 0x21: 92 | this.parseGraphicsControlBlock(this.dataView, frame); 93 | break; 94 | // 图片描述块数据 95 | case 0x2c: 96 | frame.imageInfo.left = this.dataView.getUint16(this.pos + 1, true); 97 | frame.imageInfo.top = this.dataView.getUint16(this.pos + 3, true); 98 | frame.imageInfo.width = this.dataView.getUint16(this.pos + 5, true); 99 | frame.imageInfo.height = this.dataView.getUint16( 100 | this.pos + 7, 101 | true 102 | ); 103 | const unpackedField = getBitArray( 104 | this.dataView.getUint8(this.pos + 9) 105 | ); 106 | if (unpackedField[0]) { 107 | // 本地块数据 108 | const localPaletteSize = getPaletteSize(unpackedField); 109 | frame.imageInfo.localPalette = true; 110 | frame.imageInfo.localPaletteSize = localPaletteSize / 3; 111 | this.pos += localPaletteSize; 112 | } 113 | if (unpackedField[1]) { 114 | // 帧交错标识 115 | frame.imageInfo.interlace = true; 116 | } 117 | 118 | this.gifInfo.images.push(frame.imageInfo); 119 | this.index++; 120 | // 重置frame,继续下一轮解析 121 | frame = new FrameImage(); 122 | frame.imageInfo.identifier = this.index.toString(); 123 | 124 | // 判断当前GIF是否可以动 125 | if (this.gifInfo.images.length > 1 && !this.gifInfo.animated) { 126 | this.gifInfo.animated = true; 127 | } 128 | 129 | this.pos += 11; 130 | const subBlock = readSubBlock(this.dataView, this.pos, false); 131 | this.pos += subBlock.size; 132 | break; 133 | // 末尾数据块 134 | case 0x3b: 135 | return this.gifInfo; 136 | // 未知数据块 137 | default: 138 | this.pos++; 139 | break; 140 | } 141 | } catch (e) { 142 | this.gifInfo.valid = false; 143 | return this.gifInfo; 144 | } 145 | } 146 | } 147 | 148 | // 获取GIF加载状态 149 | private gifLoadStatus(): Promise { 150 | return new Promise((resolve) => { 151 | let time = 0; 152 | // 超时时间 153 | const defaultTimeOut = 5000; 154 | const timer = setInterval(() => { 155 | time += 100; 156 | // 请求超时 157 | if (time >= defaultTimeOut) { 158 | clearInterval(timer); 159 | resolve(false); 160 | } 161 | // 加载完毕 162 | if (this.urlLoadStatus === true) { 163 | clearInterval(timer); 164 | resolve(true); 165 | } 166 | }, 100); 167 | }); 168 | } 169 | 170 | // 图形控制块数据解析 171 | private parseGraphicsControlBlock(dataView: DataView, frame: FrameImage) { 172 | const type = dataView.getUint8(this.pos + 1); 173 | // 图形控制块 174 | if (type === 0xf9) { 175 | const length = dataView.getUint8(this.pos + 2); 176 | if (length === 4) { 177 | const delay = getFrameDuration(dataView.getUint16(this.pos + 4, true)); 178 | frame.imageInfo.delay = delay; 179 | // gif总时长自增 180 | this.gifInfo.duration += delay; 181 | 182 | const unpackedField = getBitArray(dataView.getUint8(this.pos + 3)); 183 | const disposal = unpackedField.slice(3, 6).join(""); 184 | frame.imageInfo.disposal = parseInt(disposal, 2); 185 | 186 | this.pos += 8; 187 | return; 188 | } 189 | this.pos++; 190 | return; 191 | } 192 | // 非图形控制块 193 | this.pos += 2; 194 | // 获取子模块,做进一步解析 195 | const subBlock = readSubBlock(dataView, this.pos, true); 196 | switch (type) { 197 | // 应用扩展块数据 198 | case 0xff: 199 | const identifier = byteToString(dataView, 8, this.pos + 1); 200 | // 可能会出现多个应用程序扩展块,我们需要确保仅在标识符为 NETSCAPE 时处理GIF循环次数 201 | if (identifier === "NETSCAPE") { 202 | this.gifInfo.loopCount = dataView.getUint8(this.pos + 14); 203 | } 204 | break; 205 | // 帧标识块数据 206 | case 0xce: 207 | frame.imageInfo.identifier = subBlock.data; 208 | break; 209 | // 解释块数据 210 | case 0xfe: 211 | frame.imageInfo.comments.push(subBlock.data); 212 | break; 213 | // 纯文本扩展块数据 214 | case 0x01: 215 | frame.imageInfo.text = subBlock.data; 216 | break; 217 | default: 218 | break; 219 | } 220 | this.pos += subBlock.size; 221 | } 222 | 223 | // gif文件流有效性校验 224 | private async validityCheck(gifStream: File | undefined) { 225 | if (typeof this.urlLoadStatus !== "undefined" && !this.urlLoadStatus) { 226 | // 等待url加载成功 227 | const loadResult = await this.gifLoadStatus(); 228 | if (!loadResult) { 229 | throw new Error("GIF加载超时"); 230 | } 231 | } 232 | // 如果文件流存在则使用gifStream参数作为解析源 233 | if (gifStream) { 234 | const gifBuffer = await gifStream.arrayBuffer(); 235 | // 字节长度必须大于10才能继续解析 236 | if (gifBuffer.byteLength < 10) return this.gifInfo; 237 | this.dataView = new DataView(gifBuffer); 238 | } 239 | } 240 | } 241 | --------------------------------------------------------------------------------