如何用JS获取gif中的一帧帧图片

1,483 阅读2分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第1天,点击查看活动详情

概述

在 Scratch 二次开发或者其它对图片有比较大处理需求的场景中,可能会遇到这样一个需求——将 gif 图片中的每一帧切割出来,让用户自行选择里面需要用到的帧,再将其重新组合。

里面原始需求就是获取 gif 中的每一帧图片,把 gif 转换成一张张 png 图片。

效果如下:

将这张 gif 切割成一帧帧的 png 图片: 示例 gif

如图:

gif 切帧.gif

案例 Demo

实现方案

先安装 omggif 依赖库:

npm i omggif --save

以下函数可以直接使用

import { GifReader } from 'omggif';

export default (arrayBuffer, onFrame) => {
  const canvas = document.createElement('canvas');
  const ctx = canvas.getContext('2d');
  const gifReader = new GifReader(new Uint8Array(arrayBuffer));
  const numFrames = gifReader.numFrames();
  canvas.width = gifReader.width;
  canvas.height = gifReader.height;

  let imageData = ctx.createImageData(canvas.width, canvas.height);
  let previousData = ctx.createImageData(canvas.width, canvas.height);

  const loadFrame = (i) => {
    const framePixels = [];
    gifReader.decodeAndBlitFrameRGBA(i, framePixels);
    const { x, y, width, height, disposal } = gifReader.frameInfo(i);
    for (let row = 0; row < height; row++) {
      for (let column = 0; column < width; column++) {
        const indexOffset = 4 * (x + y * canvas.width);
        const j = indexOffset + 4 * (column + row * canvas.width);
        if (framePixels[j + 3]) {
          imageData.data[j + 0] = framePixels[j + 0];
          imageData.data[j + 1] = framePixels[j + 1];
          imageData.data[j + 2] = framePixels[j + 2];
          imageData.data[j + 3] = framePixels[j + 3];
        }
      }
    }

    ctx.clearRect(0, 0, canvas.width, canvas.height);
    ctx.putImageData(imageData, 0, 0);

    const dataUrl = canvas.toDataURL();

    switch (disposal) {
      case 2: // "Return to background", blank out the current frame
        ctx.clearRect(x, y, width, height);
        imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
        break;
      case 3: // "Restore to previous", copy previous data to current
        imageData = ctx.createImageData(canvas.width, canvas.height);
        imageData.data.set(previousData.data);
        break;
      default:
        // 0 and 1, as well as 4+ modes = do-not-dispose, so cache frame
        previousData = ctx.getImageData(0, 0, canvas.width, canvas.height);
        break;
    }
    onFrame(i, dataUrl, numFrames);

    if (i < numFrames - 1) {
      setTimeout(() => {
        loadFrame(i + 1);
      });
    }
  };

  loadFrame(0);
};

简单解释一下,函数中两个参数:

  1. arrayBuffer
  2. onFrame
    • 回调函数,返回每一帧的 base64 dataURL
    • 回调函数的参数:
      • frameNumber,当前是第几帧,从 0 开始
      • dataURL,base64 字符串
      • numFrames,一共有多少帧

后记

本来想结合 libgif.js,对比两个库切割帧的效果,但发现这个库侧重点好像是通过 js 控制 gif 的播放、暂停,不太符合前文提到的需求场景,另外,使用的时候还需要搭配 <img> 元素,感觉不太方便(没深入研究,如有错误,欢迎指出)

另外,除了切割 gif 之外,相对的就是将多种图片合成一张 gif,有两个备选库:

  1. gifshot
  2. gif.js

据说 gifshot 效果比 gif.js 好一点,支持的参数更多、速度也快一点,后续有时间验证一下,再对比对比。