@remotion/gif
Version:
Embed GIFs in a Remotion video
116 lines (115 loc) • 4.31 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.generate = exports.parse = void 0;
const gifuct_1 = require("./gifuct");
const decompress_frames_1 = require("./parser/decompress-frames");
const validateAndFix = (gif) => {
let currentGce = null;
for (const frame of gif.frames) {
currentGce = frame.gce ? frame.gce : currentGce;
// fix loosing graphic control extension for same frames
if ('image' in frame && !('gce' in frame) && currentGce !== null) {
frame.gce = currentGce;
}
}
};
const resetPixels = ({ typedArray, dx, dy, width, height, gifWidth, }) => {
const offset = dy * gifWidth + dx;
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
const taPos = offset + y * gifWidth + x;
typedArray[taPos * 4] = 0;
typedArray[taPos * 4 + 1] = 0;
typedArray[taPos * 4 + 2] = 0;
typedArray[taPos * 4 + 3] = 0;
}
}
};
const putPixels = (typedArray, frame, gifSize) => {
const { width, height, top: dy, left: dx } = frame.dims;
const offset = dy * gifSize.width + dx;
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
const pPos = y * width + x;
const colorIndex = frame.pixels[pPos];
if (colorIndex !== frame.transparentIndex) {
const taPos = offset + y * gifSize.width + x;
const color = frame.colorTable[colorIndex];
typedArray[taPos * 4] = color[0];
typedArray[taPos * 4 + 1] = color[1];
typedArray[taPos * 4 + 2] = color[2];
typedArray[taPos * 4 + 3] =
colorIndex === frame.transparentIndex ? 0 : 255;
}
}
}
};
const parse = (src, { signal, }) => fetch(src, { signal })
.then((resp) => {
var _a;
if (!((_a = resp.headers.get('Content-Type')) === null || _a === void 0 ? void 0 : _a.includes('image/gif')))
throw Error(`Wrong content type: "${resp.headers.get('Content-Type')}"`);
return resp.arrayBuffer();
})
.then((buffer) => (0, gifuct_1.parseGIF)(buffer))
.then((gif) => {
validateAndFix(gif);
return gif;
})
.then((gif) => Promise.all([
(0, decompress_frames_1.decompressFrames)(gif),
{ width: gif.lsd.width, height: gif.lsd.height },
]))
.then(([frames, options]) => {
const readyFrames = [];
const size = options.width * options.height * 4;
let canvas = new Uint8ClampedArray(size);
for (let i = 0; i < frames.length; ++i) {
const frame = frames[i];
// Read about different disposal types
// https://giflib.sourceforge.net/whatsinagif/animation_and_transparency.html
const prevCanvas = frames[i].disposalType === 3 ? canvas.slice() : null;
putPixels(canvas, frame, options);
readyFrames.push(canvas.slice());
// Disposal type 2: The canvas should be restored to the background color
if (frames[i].disposalType === 2) {
resetPixels({
typedArray: canvas,
dx: frame.dims.left,
dy: frame.dims.top,
width: frame.dims.width,
height: frame.dims.height,
gifWidth: options.width,
});
}
// Disposal type 3: The decoder should restore the canvas to its previous state before the current image was drawn
else if (frames[i].disposalType === 3) {
if (!prevCanvas) {
throw Error('Disposal type 3 without previous frame');
}
canvas = prevCanvas;
}
// Disposal type 1: Draw the next image on top of it
else {
canvas = readyFrames[i].slice();
}
}
return {
...options,
loaded: true,
delays: frames.map((frame) => frame.delay),
frames: readyFrames,
};
});
exports.parse = parse;
const generate = (info) => {
return {
...info,
frames: info.frames.map((buffer) => {
const image = new ImageData(info.width, info.height);
image.data.set(new Uint8ClampedArray(buffer));
return image;
}),
};
};
exports.generate = generate;