@remotion/gif
Version:
Embed GIFs in a Remotion video
1,285 lines (1,260 loc) • 41.6 kB
JavaScript
// src/get-gif-duration-in-seconds.ts
import { getRemotionEnvironment } from "remotion";
// src/lru/index.ts
class QuickLRU {
maxAge;
maxSize;
onEviction;
_size;
cache;
oldCache;
constructor(options) {
if (!(options.maxSize && options.maxSize > 0)) {
throw new TypeError("`maxSize` must be a number greater than 0");
}
if (typeof options.maxAge === "number" && options.maxAge === 0) {
throw new TypeError("`maxAge` must be a number greater than 0");
}
this.maxSize = options.maxSize;
this.maxAge = options.maxAge || Number.POSITIVE_INFINITY;
this.onEviction = options.onEviction;
this.cache = new Map;
this.oldCache = new Map;
this._size = 0;
}
_emitEvictions(cache) {
if (typeof this.onEviction !== "function") {
return;
}
for (const [key, item] of cache) {
this.onEviction(key, item.value);
}
}
_deleteIfExpired(key, item) {
if (item === undefined)
return true;
if (typeof item.expiry === "number" && item.expiry <= Date.now()) {
if (typeof this.onEviction === "function") {
this.onEviction(key, item.value);
}
return this.delete(key);
}
return false;
}
_getOrDeleteIfExpired(key, item) {
const deleted = this._deleteIfExpired(key, item);
if (deleted === false) {
return item.value;
}
}
_getItemValue(key, item) {
if (item === undefined)
return;
return item.expiry ? this._getOrDeleteIfExpired(key, item) : item.value;
}
_peek(key, cache) {
const item = cache.get(key);
return this._getItemValue(key, item);
}
_set(key, value) {
this.cache.set(key, value);
this._size++;
if (this._size >= this.maxSize) {
this._size = 0;
this._emitEvictions(this.oldCache);
this.oldCache = this.cache;
this.cache = new Map;
}
}
_moveToRecent(key, item) {
this.oldCache.delete(key);
this._set(key, item);
}
*_entriesAscending() {
for (const item of this.oldCache) {
const [key, value] = item;
if (!this.cache.has(key)) {
const deleted = this._deleteIfExpired(key, value);
if (deleted === false) {
yield item;
}
}
}
for (const item of this.cache) {
const [key, value] = item;
const deleted = this._deleteIfExpired(key, value);
if (deleted === false) {
yield item;
}
}
}
get(key) {
if (this.cache.has(key)) {
const item = this.cache.get(key);
return this._getItemValue(key, item);
}
if (this.oldCache.has(key)) {
const item = this.oldCache.get(key);
if (this._deleteIfExpired(key, item) === false) {
this._moveToRecent(key, item);
return item.value;
}
}
}
set(key, value, { maxAge = this.maxAge } = {}) {
const expiry = typeof maxAge === "number" && maxAge !== Number.POSITIVE_INFINITY ? Date.now() + maxAge : undefined;
if (this.cache.has(key)) {
this.cache.set(key, {
value,
expiry
});
} else {
this._set(key, { value, expiry });
}
}
has(key) {
if (this.cache.has(key)) {
return !this._deleteIfExpired(key, this.cache.get(key));
}
if (this.oldCache.has(key)) {
return !this._deleteIfExpired(key, this.oldCache.get(key));
}
return false;
}
peek(key) {
if (this.cache.has(key)) {
return this._peek(key, this.cache);
}
if (this.oldCache.has(key)) {
return this._peek(key, this.oldCache);
}
}
delete(key) {
const deleted = this.cache.delete(key);
if (deleted) {
this._size--;
}
return this.oldCache.delete(key) || deleted;
}
clear() {
this.cache.clear();
this.oldCache.clear();
this._size = 0;
}
resize(maxSize) {
if (!(maxSize && maxSize > 0)) {
throw new TypeError("`maxSize` must be a number greater than 0");
}
const items = [...this._entriesAscending()];
const removeCount = items.length - maxSize;
if (removeCount < 0) {
this.cache = new Map(items);
this.oldCache = new Map;
this._size = items.length;
} else {
if (removeCount > 0) {
this._emitEvictions(items.slice(0, removeCount));
}
this.oldCache = new Map(items.slice(removeCount));
this.cache = new Map;
this._size = 0;
}
this.maxSize = maxSize;
}
*keys() {
for (const [key] of this) {
yield key;
}
}
*values() {
for (const [, value] of this) {
yield value;
}
}
*[Symbol.iterator]() {
for (const item of this.cache) {
const [key, value] = item;
const deleted = this._deleteIfExpired(key, value);
if (deleted === false) {
yield [key, value.value];
}
}
for (const item of this.oldCache) {
const [key, value] = item;
if (!this.cache.has(key)) {
const deleted = this._deleteIfExpired(key, value);
if (deleted === false) {
yield [key, value.value];
}
}
}
}
*entriesDescending() {
let items = [...this.cache];
for (let i = items.length - 1;i >= 0; --i) {
const item = items[i];
const [key, value] = item;
const deleted = this._deleteIfExpired(key, value);
if (deleted === false) {
yield [key, value.value];
}
}
items = [...this.oldCache];
for (let i = items.length - 1;i >= 0; --i) {
const item = items[i];
const [key, value] = item;
if (!this.cache.has(key)) {
const deleted = this._deleteIfExpired(key, value);
if (deleted === false) {
yield [key, value.value];
}
}
}
}
*entriesAscending() {
for (const [key, value] of this._entriesAscending()) {
yield [key, value.value];
}
}
get size() {
if (!this._size) {
return this.oldCache.size;
}
let oldCacheSize = 0;
for (const key of this.oldCache.keys()) {
if (!this.cache.has(key)) {
oldCacheSize++;
}
}
return Math.min(this._size + oldCacheSize, this.maxSize);
}
}
// src/gif-cache.ts
var volatileGifCache = new QuickLRU({ maxSize: 30 });
var manuallyManagedGifCache = new Map;
// src/js-binary-schema-parser/parser.ts
var parse = (stream, schema, result = {}, parent = result) => {
if (Array.isArray(schema)) {
schema.forEach((partSchema) => {
return parse(stream, partSchema, result, parent);
});
} else if (typeof schema === "function") {
schema(stream, result, parent, parse);
} else {
const key = Object.keys(schema)[0];
if (Array.isArray(schema[key])) {
parent[key] = {};
parse(stream, schema[key], result, parent[key]);
} else {
parent[key] = schema[key](stream, result, parent, parse);
}
}
return result;
};
var loop = (schema, continueFunc) => {
return function(stream, result, parent, _parse) {
const arr = [];
let lastStreamPos = stream.pos;
while (continueFunc(stream, result, parent)) {
const newParent = {};
_parse(stream, schema, result, newParent);
if (stream.pos === lastStreamPos) {
break;
}
lastStreamPos = stream.pos;
arr.push(newParent);
}
return arr;
};
};
var conditional = (schema, conditionFunc) => (stream, result, parent, parseFn) => {
if (conditionFunc(stream, result, parent)) {
parseFn(stream, schema, result, parent);
}
};
// src/js-binary-schema-parser/uint8-parser.ts
var buildStream = (uint8Data) => ({
data: uint8Data,
pos: 0
});
var readByte = () => (stream) => {
return stream.data[stream.pos++];
};
var peekByte = (offset = 0) => (stream) => {
return stream.data[stream.pos + offset];
};
var readBytes = (length) => (stream) => {
return stream.data.subarray(stream.pos, stream.pos += length);
};
var peekBytes = (length) => (stream) => {
return stream.data.subarray(stream.pos, stream.pos + length);
};
var readString = (length) => (stream) => {
return Array.from(readBytes(length)(stream)).map((value) => String.fromCharCode(value)).join("");
};
var readUnsigned = (littleEndian) => (stream) => {
const bytes = readBytes(2)(stream);
return littleEndian ? (bytes[1] << 8) + bytes[0] : (bytes[0] << 8) + bytes[1];
};
var readArray = (byteSize, totalOrFunc) => (stream, result, parent) => {
const total = typeof totalOrFunc === "function" ? totalOrFunc(stream, result, parent) : totalOrFunc;
const parser = readBytes(byteSize);
const arr = new Array(total);
for (let i = 0;i < total; i++) {
arr[i] = parser(stream);
}
return arr;
};
var subBitsTotal = (bits, startIndex, length) => {
let result = 0;
for (let i = 0;i < length; i++) {
result += Number(bits[startIndex + i] && 2 ** (length - i - 1));
}
return result;
};
var readBits = (schema) => (stream) => {
const byte = readByte()(stream);
const bits = new Array(8);
for (let i = 0;i < 8; i++) {
bits[7 - i] = Boolean(byte & 1 << i);
}
return Object.keys(schema).reduce((res, key) => {
const def = schema[key];
if (def.length) {
res[key] = subBitsTotal(bits, def.index, def.length);
} else {
res[key] = bits[def.index];
}
return res;
}, {});
};
// src/js-binary-schema-parser/gif.ts
var subBlocksSchema = {
blocks: (stream) => {
const terminator = 0;
const chunks = [];
const streamSize = stream.data.length;
let total = 0;
for (let size = readByte()(stream);size !== terminator; size = readByte()(stream)) {
if (!size)
break;
if (stream.pos + size >= streamSize) {
const availableSize = streamSize - stream.pos;
chunks.push(readBytes(availableSize)(stream));
total += availableSize;
break;
}
chunks.push(readBytes(size)(stream));
total += size;
}
const result = new Uint8Array(total);
let offset = 0;
for (let i = 0;i < chunks.length; i++) {
result.set(chunks[i], offset);
offset += chunks[i].length;
}
return result;
}
};
var gceSchema = conditional({
gce: [
{ codes: readBytes(2) },
{ byteSize: readByte() },
{
extras: readBits({
future: { index: 0, length: 3 },
disposal: { index: 3, length: 3 },
userInput: { index: 6 },
transparentColorGiven: { index: 7 }
})
},
{ delay: readUnsigned(true) },
{ transparentColorIndex: readByte() },
{ terminator: readByte() }
]
}, (stream) => {
const codes = peekBytes(2)(stream);
return codes[0] === 33 && codes[1] === 249;
});
var imageSchema = conditional({
image: [
{ code: readByte() },
{
descriptor: [
{ left: readUnsigned(true) },
{ top: readUnsigned(true) },
{ width: readUnsigned(true) },
{ height: readUnsigned(true) },
{
lct: readBits({
exists: { index: 0 },
interlaced: { index: 1 },
sort: { index: 2 },
future: { index: 3, length: 2 },
size: { index: 5, length: 3 }
})
}
]
},
conditional({
lct: readArray(3, (_stream, _result, parent) => {
return 2 ** (parent.descriptor.lct.size + 1);
})
}, (_stream, _result, parent) => {
return parent.descriptor.lct.exists;
}),
{ data: [{ minCodeSize: readByte() }, subBlocksSchema] }
]
}, (stream) => {
return peekByte()(stream) === 44;
});
var textSchema = conditional({
text: [
{ codes: readBytes(2) },
{ blockSize: readByte() },
{
preData: (stream, _result, parent) => readBytes(parent.text.blockSize)(stream)
},
subBlocksSchema
]
}, (stream) => {
const codes = peekBytes(2)(stream);
return codes[0] === 33 && codes[1] === 1;
});
var applicationSchema = conditional({
application: [
{ codes: readBytes(2) },
{ blockSize: readByte() },
{
id: (stream, _result, parent) => readString(parent.blockSize)(stream)
},
subBlocksSchema
]
}, (stream) => {
const codes = peekBytes(2)(stream);
return codes[0] === 33 && codes[1] === 255;
});
var commentSchema = conditional({
comment: [{ codes: readBytes(2) }, subBlocksSchema]
}, (stream) => {
const codes = peekBytes(2)(stream);
return codes[0] === 33 && codes[1] === 254;
});
var GIF = [
{ header: [{ signature: readString(3) }, { version: readString(3) }] },
{
lsd: [
{ width: readUnsigned(true) },
{ height: readUnsigned(true) },
{
gct: readBits({
exists: { index: 0 },
resolution: { index: 1, length: 3 },
sort: { index: 4 },
size: { index: 5, length: 3 }
})
},
{ backgroundColorIndex: readByte() },
{ pixelAspectRatio: readByte() }
]
},
conditional({
gct: readArray(3, (_stream, result) => 2 ** (result.lsd.gct.size + 1))
}, (_stream, result) => result.lsd.gct.exists),
{
frames: loop([gceSchema, applicationSchema, commentSchema, imageSchema, textSchema], (stream) => {
const nextCode = peekByte()(stream);
return nextCode === 33 || nextCode === 44;
})
}
];
// src/gifuct/deinterlace.ts
var deinterlace = (pixels, width) => {
const newPixels = new Array(pixels.length);
const rows = pixels.length / width;
const cpRow = function(toRow, _fromRow) {
const fromPixels = pixels.slice(_fromRow * width, (_fromRow + 1) * width);
newPixels.splice(...[toRow * width, width].concat(fromPixels));
};
const offsets = [0, 4, 2, 1];
const steps = [8, 8, 4, 2];
let fromRow = 0;
for (let pass = 0;pass < 4; pass++) {
for (let toRow = offsets[pass];toRow < rows; toRow += steps[pass]) {
cpRow(toRow, fromRow);
fromRow++;
}
}
return newPixels;
};
// src/gifuct/lzw.ts
var lzw = (minCodeSize, data, pixelCount) => {
const MAX_STACK_SIZE = 4096;
const nullCode = -1;
const npix = pixelCount;
let available;
let code_mask;
let code_size;
let in_code;
let old_code;
var bits;
let code;
let i;
var datum;
var first;
var top;
var bi;
var pi;
const dstPixels = new Array(pixelCount);
const prefix = new Array(MAX_STACK_SIZE);
const suffix = new Array(MAX_STACK_SIZE);
const pixelStack = new Array(MAX_STACK_SIZE + 1);
const data_size = minCodeSize;
const clear = 1 << data_size;
const end_of_information = clear + 1;
available = clear + 2;
old_code = nullCode;
code_size = data_size + 1;
code_mask = (1 << code_size) - 1;
for (code = 0;code < clear; code++) {
prefix[code] = 0;
suffix[code] = code;
}
var datum;
var bits;
var first;
var top;
var pi;
var bi;
datum = bits = first = top = pi = bi = 0;
for (i = 0;i < npix; ) {
if (top === 0) {
if (bits < code_size) {
datum += data[bi] << bits;
bits += 8;
bi++;
continue;
}
code = datum & code_mask;
datum >>= code_size;
bits -= code_size;
if (code > available || code === end_of_information) {
break;
}
if (code === clear) {
code_size = data_size + 1;
code_mask = (1 << code_size) - 1;
available = clear + 2;
old_code = nullCode;
continue;
}
if (old_code === nullCode) {
pixelStack[top++] = suffix[code];
old_code = code;
first = code;
continue;
}
in_code = code;
if (code === available) {
pixelStack[top++] = first;
code = old_code;
}
while (code > clear) {
pixelStack[top++] = suffix[code];
code = prefix[code];
}
first = suffix[code] & 255;
pixelStack[top++] = first;
if (available < MAX_STACK_SIZE) {
prefix[available] = old_code;
suffix[available] = first;
available++;
if ((available & code_mask) === 0 && available < MAX_STACK_SIZE) {
code_size++;
code_mask += available;
}
}
old_code = in_code;
}
top--;
dstPixels[pi++] = pixelStack[top];
i++;
}
for (i = pi;i < npix; i++) {
dstPixels[i] = 0;
}
return dstPixels;
};
// src/gifuct/index.ts
var parseGIF = (arrayBuffer) => {
const byteData = new Uint8Array(arrayBuffer);
return parse(buildStream(byteData), GIF);
};
var decompressFrame = (frame, gct) => {
if (!frame.image) {
console.warn("gif frame does not have associated image.");
return null;
}
const { image } = frame;
const totalPixels = image.descriptor.width * image.descriptor.height;
let pixels = lzw(image.data.minCodeSize, image.data.blocks, totalPixels);
if (image.descriptor.lct?.interlaced) {
pixels = deinterlace(pixels, image.descriptor.width);
}
const resultImage = {
pixels,
dims: {
top: frame.image.descriptor.top,
left: frame.image.descriptor.left,
width: frame.image.descriptor.width,
height: frame.image.descriptor.height
},
colorTable: image.descriptor.lct?.exists ? image.lct : gct,
delay: (frame.gce?.delay || 10) * 10,
disposalType: frame.gce ? frame.gce.extras.disposal : 1,
transparentIndex: frame.gce ? frame.gce.extras.transparentColorGiven ? frame.gce.transparentColorIndex : -1 : -1
};
return resultImage;
};
// src/parser/decompress-frames.ts
var decompressFrames = (parsedGif) => {
return parsedGif.frames.filter((f) => {
return "image" in f;
}).map((f) => {
return decompressFrame(f, parsedGif.gct);
}).filter(Boolean).map((f) => f);
};
// src/parse-generate.ts
var validateAndFix = (gif) => {
let currentGce = null;
for (const frame of gif.frames) {
currentGce = frame.gce ? frame.gce : currentGce;
if ("image" in frame && !("gce" in frame) && currentGce !== null) {
frame.gce = currentGce;
}
}
};
var 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;
}
}
};
var 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;
}
}
}
};
var parse2 = (src, {
signal
}) => fetch(src, { signal }).then((resp) => {
if (!resp.headers.get("Content-Type")?.includes("image/gif"))
throw Error(`Wrong content type: "${resp.headers.get("Content-Type")}"`);
return resp.arrayBuffer();
}).then((buffer) => parseGIF(buffer)).then((gif) => {
validateAndFix(gif);
return gif;
}).then((gif) => Promise.all([
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];
const prevCanvas = frames[i].disposalType === 3 ? canvas.slice() : null;
putPixels(canvas, frame, options);
readyFrames.push(canvas.slice());
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
});
} else if (frames[i].disposalType === 3) {
if (!prevCanvas) {
throw Error("Disposal type 3 without previous frame");
}
canvas = prevCanvas;
} else {
canvas = readyFrames[i].slice();
}
}
return {
...options,
loaded: true,
delays: frames.map((frame) => frame.delay),
frames: readyFrames
};
});
var 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;
})
};
};
// src/worker/source.ts
var src = '"use strict";(()=>{var P=(t,r,e={},n=e)=>{if(Array.isArray(r))r.forEach(o=>P(t,o,e,n));else if(typeof r=="function")r(t,e,n,P);else{let o=Object.keys(r)[0];Array.isArray(r[o])?(n[o]={},P(t,r[o],e,n[o])):n[o]=r[o](t,e,n,P)}return e},M=(t,r)=>function(e,n,o,c){let i=[],a=e.pos;for(;r(e,n,o);){let s={};if(c(e,t,n,s),e.pos===a)break;a=e.pos,i.push(s)}return i},g=(t,r)=>(e,n,o,c)=>{r(e,n,o)&&c(e,t,n,o)};var W=t=>({data:t,pos:0}),m=()=>t=>t.data[t.pos++],U=(t=0)=>r=>r.data[r.pos+t],f=t=>r=>r.data.subarray(r.pos,r.pos+=t),k=t=>r=>r.data.subarray(r.pos,r.pos+t),v=t=>r=>Array.from(f(t)(r)).map(e=>String.fromCharCode(e)).join(""),b=t=>r=>{let e=f(2)(r);return t?(e[1]<<8)+e[0]:(e[0]<<8)+e[1]},E=(t,r)=>(e,n,o)=>{let c=typeof r=="function"?r(e,n,o):r,i=f(t),a=new Array(c);for(let s=0;s<c;s++)a[s]=i(e);return a},$=(t,r,e)=>{let n=0;for(let o=0;o<e;o++)n+=Number(t[r+o]&&2**(e-o-1));return n},I=t=>r=>{let e=m()(r),n=new Array(8);for(let o=0;o<8;o++)n[7-o]=!!(e&1<<o);return Object.keys(t).reduce((o,c)=>{let i=t[c];return i.length?o[c]=$(n,i.index,i.length):o[c]=n[i.index],o},{})};var z={blocks:t=>{let e=[],n=t.data.length,o=0;for(let a=m()(t);a!==0&&a;a=m()(t)){if(t.pos+a>=n){let s=n-t.pos;e.push(f(s)(t)),o+=s;break}e.push(f(a)(t)),o+=a}let c=new Uint8Array(o),i=0;for(let a=0;a<e.length;a++)c.set(e[a],i),i+=e[a].length;return c}},q=g({gce:[{codes:f(2)},{byteSize:m()},{extras:I({future:{index:0,length:3},disposal:{index:3,length:3},userInput:{index:6},transparentColorGiven:{index:7}})},{delay:b(!0)},{transparentColorIndex:m()},{terminator:m()}]},t=>{let r=k(2)(t);return r[0]===33&&r[1]===249}),H=g({image:[{code:m()},{descriptor:[{left:b(!0)},{top:b(!0)},{width:b(!0)},{height:b(!0)},{lct:I({exists:{index:0},interlaced:{index:1},sort:{index:2},future:{index:3,length:2},size:{index:5,length:3}})}]},g({lct:E(3,(t,r,e)=>2**(e.descriptor.lct.size+1))},(t,r,e)=>e.descriptor.lct.exists),{data:[{minCodeSize:m()},z]}]},t=>U()(t)===44),J=g({text:[{codes:f(2)},{blockSize:m()},{preData:(t,r,e)=>f(e.text.blockSize)(t)},z]},t=>{let r=k(2)(t);return r[0]===33&&r[1]===1}),Q=g({application:[{codes:f(2)},{blockSize:m()},{id:(t,r,e)=>v(e.blockSize)(t)},z]},t=>{let r=k(2)(t);return r[0]===33&&r[1]===255}),V=g({comment:[{codes:f(2)},z]},t=>{let r=k(2)(t);return r[0]===33&&r[1]===254}),K=[{header:[{signature:v(3)},{version:v(3)}]},{lsd:[{width:b(!0)},{height:b(!0)},{gct:I({exists:{index:0},resolution:{index:1,length:3},sort:{index:4},size:{index:5,length:3}})},{backgroundColorIndex:m()},{pixelAspectRatio:m()}]},g({gct:E(3,(t,r)=>2**(r.lsd.gct.size+1))},(t,r)=>r.lsd.gct.exists),{frames:M([q,Q,V,H,J],t=>{let r=U()(t);return r===33||r===44})}];var X=(t,r)=>{let e=new Array(t.length),n=t.length/r,o=function(s,d){let u=t.slice(d*r,(d+1)*r);e.splice(...[s*r,r].concat(u))},c=[0,4,2,1],i=[8,8,4,2],a=0;for(let s=0;s<4;s++)for(let d=c[s];d<n;d+=i[s])o(d,a),a++;return e};var Z=(t,r,e)=>{let c=e,i,a,s,d,u;var w;let l,p;var C,y,h,_,G;let x=new Array(e),B=new Array(4096),T=new Array(4096),F=new Array(4097),R=t,S=1<<R,O=S+1;for(i=S+2,u=-1,s=R+1,a=(1<<s)-1,l=0;l<S;l++)B[l]=0,T[l]=l;var C,w,y,h,G,_;for(C=w=y=h=G=_=0,p=0;p<c;){if(h===0){if(w<s){C+=r[_]<<w,w+=8,_++;continue}if(l=C&a,C>>=s,w-=s,l>i||l===O)break;if(l===S){s=R+1,a=(1<<s)-1,i=S+2,u=-1;continue}if(u===-1){F[h++]=T[l],u=l,y=l;continue}for(d=l,l===i&&(F[h++]=y,l=u);l>S;)F[h++]=T[l],l=B[l];y=T[l]&255,F[h++]=y,i<4096&&(B[i]=u,T[i]=y,i++,(i&a)===0&&i<4096&&(s++,a+=i)),u=d}h--,x[G++]=F[h],p++}for(p=G;p<c;p++)x[p]=0;return x};var j=t=>{let r=new Uint8Array(t);return P(W(r),K)},D=(t,r)=>{var i,a,s;if(!t.image)return console.warn("gif frame does not have associated image."),null;let{image:e}=t,n=e.descriptor.width*e.descriptor.height,o=Z(e.data.minCodeSize,e.data.blocks,n);return(i=e.descriptor.lct)!=null&&i.interlaced&&(o=X(o,e.descriptor.width)),{pixels:o,dims:{top:t.image.descriptor.top,left:t.image.descriptor.left,width:t.image.descriptor.width,height:t.image.descriptor.height},colorTable:(a=e.descriptor.lct)!=null&&a.exists?e.lct:r,delay:(((s=t.gce)==null?void 0:s.delay)||10)*10,disposalType:t.gce?t.gce.extras.disposal:1,transparentIndex:t.gce&&t.gce.extras.transparentColorGiven?t.gce.transparentColorIndex:-1}};var L=t=>t.frames.filter(r=>"image"in r).map(r=>D(r,t.gct)).filter(Boolean).map(r=>r);var Y=t=>{let r=null;for(let e of t.frames)r=e.gce?e.gce:r,"image"in e&&!("gce"in e)&&r!==null&&(e.gce=r)},ee=({typedArray:t,dx:r,dy:e,width:n,height:o,gifWidth:c})=>{let i=e*c+r;for(let a=0;a<o;a++)for(let s=0;s<n;s++){let d=i+a*c+s;t[d*4]=0,t[d*4+1]=0,t[d*4+2]=0,t[d*4+3]=0}},te=(t,r,e)=>{let{width:n,height:o,top:c,left:i}=r.dims,a=c*e.width+i;for(let s=0;s<o;s++)for(let d=0;d<n;d++){let u=s*n+d,l=r.pixels[u];if(l!==r.transparentIndex){let p=a+s*e.width+d,x=r.colorTable[l];t[p*4]=x[0],t[p*4+1]=x[1],t[p*4+2]=x[2],t[p*4+3]=l===r.transparentIndex?0:255}}},N=(t,{signal:r})=>fetch(t,{signal:r}).then(e=>{var n;if(!((n=e.headers.get("Content-Type"))!=null&&n.includes("image/gif")))throw Error(`Wrong content type: "${e.headers.get("Content-Type")}"`);return e.arrayBuffer()}).then(e=>j(e)).then(e=>(Y(e),e)).then(e=>Promise.all([L(e),{width:e.lsd.width,height:e.lsd.height}])).then(([e,n])=>{let o=[],c=n.width*n.height*4,i=new Uint8ClampedArray(c);for(let a=0;a<e.length;++a){let s=e[a],d=e[a].disposalType===3?i.slice():null;if(te(i,s,n),o.push(i.slice()),e[a].disposalType===2)ee({typedArray:i,dx:s.dims.left,dy:s.dims.top,width:s.dims.width,height:s.dims.height,gifWidth:n.width});else if(e[a].disposalType===3){if(!d)throw Error("Disposal type 3 without previous frame");i=d}else i=o[a].slice()}return{...n,loaded:!0,delays:e.map(a=>a.delay),frames:o}});var A=new Map;self.addEventListener("message",t=>{let{type:r,src:e}=t.data||t;switch(r){case"parse":{if(!A.has(e)){let n=new AbortController,o={signal:n.signal};A.set(e,n),N(e,o).then(c=>{self.postMessage(Object.assign(c,{src:e}),c.frames.map(i=>i.buffer))}).catch(c=>{self.postMessage({src:e,error:c,loaded:!0})}).finally(()=>{A.delete(e)})}break}case"cancel":{A.has(e)&&(A.get(e).abort(),A.delete(e));break}default:break}});})();\n';
// src/worker/index.ts
var makeWorker = () => {
const blob = new Blob([src], { type: "application/javascript" });
const url = URL.createObjectURL(blob);
const worker = new Worker(url);
URL.revokeObjectURL(url);
return worker;
};
// src/react-tools.ts
var parseGif = async ({
src: src2,
controller
}) => {
const raw = await parse2(src2, { signal: controller.signal });
return generate(raw);
};
var parseWithWorker = (src2) => {
const worker = makeWorker();
let handler = null;
const prom = new Promise((resolve, reject) => {
handler = (e) => {
const message = e.data || e;
if (message.src === src2) {
if (message.error) {
reject(new Error(message.error));
} else {
const data = message.error ? message : generate(message);
resolve(data);
worker.terminate();
}
}
};
worker.addEventListener("message", handler);
worker.postMessage({ src: src2, type: "parse" });
});
return {
prom,
cancel: () => {
worker.postMessage({ src: src2, type: "cancel" });
worker.removeEventListener("message", handler);
worker.terminate();
}
};
};
// src/resolve-gif-source.ts
var resolveGifSource = (src2) => {
if (typeof window === "undefined") {
return src2;
}
return new URL(src2, window.origin).href;
};
// src/get-gif-duration-in-seconds.ts
var calcDuration = (parsed) => {
return parsed.delays.reduce((sum, delay) => sum + delay, 0) / 1000;
};
var getGifDurationInSeconds = async (src2) => {
const resolvedSrc = resolveGifSource(src2);
const inCache = volatileGifCache.get(resolvedSrc) ?? manuallyManagedGifCache.get(resolvedSrc);
if (inCache) {
return calcDuration(inCache);
}
if (getRemotionEnvironment().isRendering) {
const renderingParsed = parseWithWorker(resolvedSrc);
const resolved = await renderingParsed.prom;
volatileGifCache.set(resolvedSrc, resolved);
return calcDuration(resolved);
}
const parsed = await parseGif({
src: resolvedSrc,
controller: new AbortController
});
volatileGifCache.set(resolvedSrc, parsed);
return calcDuration(parsed);
};
// src/Gif.tsx
import { forwardRef as forwardRef4 } from "react";
import { getRemotionEnvironment as getRemotionEnvironment2 } from "remotion";
// src/GifForDevelopment.tsx
import { forwardRef as forwardRef2, useEffect as useEffect3, useRef as useRef2, useState as useState3 } from "react";
import { continueRender, delayRender } from "remotion";
// src/canvas.tsx
import {
forwardRef,
useEffect as useEffect2,
useImperativeHandle,
useRef,
useState as useState2
} from "react";
// src/use-element-size.ts
import { useCallback, useEffect, useMemo, useState } from "react";
var elementSizeHooks = [];
var useElementSize = (ref) => {
const [size, setSize] = useState(null);
const observer = useMemo(() => {
if (typeof ResizeObserver === "undefined") {
return null;
}
return new ResizeObserver((entries) => {
const { contentRect } = entries[0];
const newSize = entries[0].target.getClientRects();
if (!newSize?.[0]) {
setSize(null);
return;
}
const probableCssParentScale = contentRect.width === 0 ? 1 : newSize[0].width / contentRect.width;
const width = probableCssParentScale > 0 ? newSize[0].width * (1 / probableCssParentScale) : newSize[0].width;
const height = probableCssParentScale > 0 ? newSize[0].height * (1 / probableCssParentScale) : newSize[0].height;
setSize({
width,
height
});
});
}, []);
const updateSize = useCallback(() => {
if (!ref.current) {
return;
}
const rect = ref.current.getClientRects();
if (!rect[0]) {
setSize(null);
return;
}
setSize({
width: rect[0].width,
height: rect[0].height
});
}, [ref]);
useEffect(() => {
if (!observer) {
return;
}
updateSize();
const { current } = ref;
if (ref.current) {
observer.observe(ref.current);
}
return () => {
if (current) {
observer.unobserve(current);
}
};
}, [observer, ref, updateSize]);
useEffect(() => {
elementSizeHooks.push(updateSize);
return () => {
elementSizeHooks = elementSizeHooks.filter((e) => e !== updateSize);
};
}, [updateSize]);
return size;
};
// src/canvas.tsx
import { jsx } from "react/jsx-runtime";
var calcArgs = (fit, frameSize, canvasSize) => {
switch (fit) {
case "fill": {
return [
0,
0,
frameSize.width,
frameSize.height,
0,
0,
canvasSize.width,
canvasSize.height
];
}
case "contain": {
const ratio = Math.min(canvasSize.width / frameSize.width, canvasSize.height / frameSize.height);
const centerX = (canvasSize.width - frameSize.width * ratio) / 2;
const centerY = (canvasSize.height - frameSize.height * ratio) / 2;
return [
0,
0,
frameSize.width,
frameSize.height,
centerX,
centerY,
frameSize.width * ratio,
frameSize.height * ratio
];
}
case "cover": {
const ratio = Math.max(canvasSize.width / frameSize.width, canvasSize.height / frameSize.height);
const centerX = (canvasSize.width - frameSize.width * ratio) / 2;
const centerY = (canvasSize.height - frameSize.height * ratio) / 2;
return [
0,
0,
frameSize.width,
frameSize.height,
centerX,
centerY,
frameSize.width * ratio,
frameSize.height * ratio
];
}
default:
throw new Error("Unknown fit: " + fit);
}
};
var makeCanvas = () => {
if (typeof document === "undefined") {
return null;
}
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d");
canvas.width = 0;
canvas.height = 0;
return ctx;
};
var Canvas = forwardRef(({ index, frames, width, height, fit, className, style }, ref) => {
const canvasRef = useRef(null);
const [tempCtx] = useState2(() => {
return makeCanvas();
});
const size = useElementSize(canvasRef);
useImperativeHandle(ref, () => {
return canvasRef.current;
}, []);
useEffect2(() => {
if (!size) {
return;
}
const imageData = frames[index];
const ctx = canvasRef.current?.getContext("2d");
if (imageData && tempCtx && ctx) {
if (tempCtx.canvas.width < imageData.width || tempCtx.canvas.height < imageData.height) {
tempCtx.canvas.width = imageData.width;
tempCtx.canvas.height = imageData.height;
}
if (size.width > 0 && size.height > 0) {
ctx.clearRect(0, 0, size.width, size.height);
tempCtx.clearRect(0, 0, tempCtx.canvas.width, tempCtx.canvas.height);
}
tempCtx.putImageData(imageData, 0, 0);
ctx.drawImage(tempCtx.canvas, ...calcArgs(fit, imageData, { width: size.width, height: size.height }));
}
}, [index, frames, fit, tempCtx, size]);
return /* @__PURE__ */ jsx("canvas", {
ref: canvasRef,
className,
style,
width: width ?? size?.width,
height: height ?? size?.height
});
});
// src/is-cors-error.ts
var isCorsError = (error) => {
return error.message.includes("Failed to fetch") || error.message.includes("Load failed") || error.message.includes("NetworkError when attempting to fetch resource");
};
// src/useCurrentGifIndex.tsx
import { useMemo as useMemo2 } from "react";
import { useCurrentFrame, useVideoConfig } from "remotion";
function useCurrentGifIndex({
delays,
loopBehavior,
playbackRate
}) {
const currentFrame = useCurrentFrame();
const videoConfig = useVideoConfig();
const duration = useMemo2(() => {
if (delays.length !== 0) {
return delays.reduce((sum, delay) => sum + (delay ?? 0), 0);
}
return 1;
}, [delays]);
if (delays.length === 0) {
return 0;
}
const updatedFrame = currentFrame / (1 / playbackRate);
const time = updatedFrame / videoConfig.fps * 1000;
if (loopBehavior === "pause-after-finish" && time >= duration) {
return delays.length - 1;
}
if (loopBehavior === "unmount-after-finish" && time >= duration) {
return -1;
}
let currentTime = time % duration;
for (let i = 0;i < delays.length; i++) {
const delay = delays[i];
if (currentTime < delay) {
return i;
}
currentTime -= delay;
}
return 0;
}
// src/GifForDevelopment.tsx
import { jsx as jsx2 } from "react/jsx-runtime";
var GifForDevelopment = forwardRef2(({
src: src2,
width,
height,
onError,
loopBehavior = "loop",
playbackRate = 1,
onLoad,
fit = "fill",
...props
}, ref) => {
const resolvedSrc = resolveGifSource(src2);
const [state, update] = useState3(() => {
const parsedGif = volatileGifCache.get(resolvedSrc) ?? manuallyManagedGifCache.get(resolvedSrc);
if (parsedGif === undefined) {
return {
delays: [],
frames: [],
width: 0,
height: 0
};
}
return parsedGif;
});
const [error, setError] = useState3(null);
const [id] = useState3(() => delayRender(`Rendering <Gif/> with src="${resolvedSrc}"`));
const currentOnLoad = useRef2(onLoad);
const currentOnError = useRef2(onError);
currentOnLoad.current = onLoad;
currentOnError.current = onError;
useEffect3(() => {
let done = false;
let aborted = false;
const { prom, cancel } = parseWithWorker(resolvedSrc);
const newHandle = delayRender("Loading <Gif /> with src=" + resolvedSrc);
prom.then((parsed) => {
currentOnLoad.current?.(parsed);
update(parsed);
volatileGifCache.set(resolvedSrc, parsed);
done = true;
continueRender(newHandle);
continueRender(id);
}).catch((err) => {
if (aborted) {
continueRender(newHandle);
return;
}
if (currentOnError.current) {
currentOnError.current(err);
} else {
setError(err);
}
});
return () => {
if (!done) {
aborted = true;
cancel();
}
continueRender(newHandle);
};
}, [id, resolvedSrc]);
if (error) {
console.error(error.stack);
if (isCorsError(error)) {
throw new Error(`Failed to render GIF with source ${src2}: "${error.message}". You must enable CORS for this URL. Open the Developer Tools to see exactly why this fetch failed.`);
}
throw new Error(`Failed to render GIF with source ${src2}: "${error.message}".`);
}
const index = useCurrentGifIndex({
delays: state.delays,
loopBehavior,
playbackRate
});
if (index === -1) {
return null;
}
return /* @__PURE__ */ jsx2(Canvas, {
fit,
index,
frames: state.frames,
width,
height,
...props,
ref
});
});
// src/GifForRendering.tsx
import { forwardRef as forwardRef3, useEffect as useEffect4, useRef as useRef3, useState as useState4 } from "react";
import { continueRender as continueRender2, delayRender as delayRender2, Internals } from "remotion";
import { jsx as jsx3 } from "react/jsx-runtime";
var GifForRendering = forwardRef3(({
src: src2,
width,
height,
onLoad,
onError,
loopBehavior = "loop",
playbackRate = 1,
fit = "fill",
...props
}, ref) => {
const resolvedSrc = resolveGifSource(src2);
const [state, update] = useState4(() => {
const parsedGif = volatileGifCache.get(resolvedSrc);
if (parsedGif === undefined) {
return {
delays: [],
frames: [],
width: 0,
height: 0
};
}
return parsedGif;
});
const [error, setError] = useState4(null);
const [renderHandle] = useState4(() => delayRender2(`Rendering <Gif/> with src="${resolvedSrc}"`));
const logLevel = Internals.useLogLevel();
useEffect4(() => {
return () => {
continueRender2(renderHandle);
};
}, [renderHandle]);
const index = useCurrentGifIndex({
delays: state.delays,
loopBehavior,
playbackRate
});
const currentOnLoad = useRef3(onLoad);
const currentOnError = useRef3(onError);
currentOnLoad.current = onLoad;
currentOnError.current = onError;
useEffect4(() => {
const controller = new AbortController;
let done = false;
let aborted = false;
const newHandle = delayRender2("Loading <Gif /> with src=" + resolvedSrc);
Internals.Log.verbose(logLevel, "Loading GIF with source", resolvedSrc);
const time = Date.now();
parseGif({ controller, src: resolvedSrc }).then((parsed) => {
Internals.Log.verbose(logLevel, "Parsed GIF in", Date.now() - time, "ms");
currentOnLoad.current?.(parsed);
update(parsed);
volatileGifCache.set(resolvedSrc, parsed);
done = true;
continueRender2(newHandle);
continueRender2(renderHandle);
}).catch((err) => {
if (aborted) {
continueRender2(newHandle);
return;
}
Internals.Log.error("Failed to load GIF", err);
if (currentOnError.current) {
currentOnError.current(err);
} else {
setError(err);
}
});
return () => {
if (!done) {
aborted = true;
controller.abort();
}
continueRender2(newHandle);
continueRender2(renderHandle);
};
}, [renderHandle, logLevel, resolvedSrc]);
if (error) {
Internals.Log.error(error.stack);
if (isCorsError(error)) {
throw new Error(`Failed to render GIF with source ${src2}: "${error.message}". You must enable CORS for this URL.`);
}
throw new Error(`Failed to render GIF with source ${src2}: "${error.message}". Render with --log=verbose to see the full stack.`);
}
if (index === -1) {
return null;
}
return /* @__PURE__ */ jsx3(Canvas, {
fit,
index,
frames: state.frames,
width,
height,
...props,
ref
});
});
// src/Gif.tsx
import { jsx as jsx4 } from "react/jsx-runtime";
var Gif = forwardRef4((props, ref) => {
const env = getRemotionEnvironment2();
if (env.isRendering) {
return /* @__PURE__ */ jsx4(GifForRendering, {
...props,
ref
});
}
return /* @__PURE__ */ jsx4(GifForDevelopment, {
...props,
ref
});
});
// src/preload-gif.ts
var preloadGif = (src2) => {
const resolvedSrc = resolveGifSource(src2);
if (volatileGifCache.has(resolvedSrc)) {
return {
waitUntilDone: () => Promise.resolve(),
free: () => volatileGifCache.delete(resolvedSrc)
};
}
if (manuallyManagedGifCache.has(resolvedSrc)) {
return {
waitUntilDone: () => Promise.resolve(),
free: () => manuallyManagedGifCache.delete(resolvedSrc)
};
}
const { prom, cancel } = parseWithWorker(resolvedSrc);
let deleted = false;
prom.then((p) => {
if (!deleted) {
manuallyManagedGifCache.set(resolvedSrc, p);
}
});
return {
waitUntilDone: () => prom.then(() => {
return;
}),
free: () => {
cancel();
deleted = true;
manuallyManagedGifCache.delete(resolvedSrc);
}
};
};
export {
preloadGif,
getGifDurationInSeconds,
Gif
};