UNPKG

@remotion/gif

Version:

Embed GIFs in a Remotion video

1,285 lines (1,260 loc) • 41.6 kB
// 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 };