fix(gifMaker): add gifuct js to handle all gif types decoding (#1182)
* fix(gifMaker): add gifuct js to handle all gif types decoding * fix(gifMaker): address code review * fix(gifMaker): greptile haunts me in my sleep * Update src/equicordplugins/gifMaker/utils/encoder.ts Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> --------- Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
This commit is contained in:
parent
a7eb93c875
commit
06f6caa1d8
6 changed files with 175 additions and 87 deletions
|
|
@ -55,6 +55,7 @@
|
||||||
"@vap/shiki": "0.10.5",
|
"@vap/shiki": "0.10.5",
|
||||||
"fflate": "^0.8.2",
|
"fflate": "^0.8.2",
|
||||||
"gifenc": "github:mattdesl/gifenc#64842fca317b112a8590f8fef2bf3825da8f6fe3",
|
"gifenc": "github:mattdesl/gifenc#64842fca317b112a8590f8fef2bf3825da8f6fe3",
|
||||||
|
"gifuct-js": "^2.1.2",
|
||||||
"idb": "8.0.3",
|
"idb": "8.0.3",
|
||||||
"jsqr": "1.4.0",
|
"jsqr": "1.4.0",
|
||||||
"monaco-editor": "^0.55.1",
|
"monaco-editor": "^0.55.1",
|
||||||
|
|
|
||||||
21
pnpm-lock.yaml
generated
21
pnpm-lock.yaml
generated
|
|
@ -8,9 +8,7 @@ overrides:
|
||||||
'@eslint/plugin-kit@<0.3.4': '>=0.3.4'
|
'@eslint/plugin-kit@<0.3.4': '>=0.3.4'
|
||||||
|
|
||||||
patchedDependencies:
|
patchedDependencies:
|
||||||
'@types/less@3.0.8':
|
'@types/less@3.0.8': 641e6c93bb737bac7fc283416857bd095cd85bcbcba63becb7a8bbcc78f73076
|
||||||
hash: 641e6c93bb737bac7fc283416857bd095cd85bcbcba63becb7a8bbcc78f73076
|
|
||||||
path: patches/@types__less@3.0.8.patch
|
|
||||||
|
|
||||||
importers:
|
importers:
|
||||||
|
|
||||||
|
|
@ -52,6 +50,9 @@ importers:
|
||||||
gifenc:
|
gifenc:
|
||||||
specifier: github:mattdesl/gifenc#64842fca317b112a8590f8fef2bf3825da8f6fe3
|
specifier: github:mattdesl/gifenc#64842fca317b112a8590f8fef2bf3825da8f6fe3
|
||||||
version: https://codeload.github.com/mattdesl/gifenc/tar.gz/64842fca317b112a8590f8fef2bf3825da8f6fe3
|
version: https://codeload.github.com/mattdesl/gifenc/tar.gz/64842fca317b112a8590f8fef2bf3825da8f6fe3
|
||||||
|
gifuct-js:
|
||||||
|
specifier: ^2.1.2
|
||||||
|
version: 2.1.2
|
||||||
idb:
|
idb:
|
||||||
specifier: 8.0.3
|
specifier: 8.0.3
|
||||||
version: 8.0.3
|
version: 8.0.3
|
||||||
|
|
@ -1770,9 +1771,12 @@ packages:
|
||||||
engines: {node: '>= 14'}
|
engines: {node: '>= 14'}
|
||||||
|
|
||||||
gifenc@https://codeload.github.com/mattdesl/gifenc/tar.gz/64842fca317b112a8590f8fef2bf3825da8f6fe3:
|
gifenc@https://codeload.github.com/mattdesl/gifenc/tar.gz/64842fca317b112a8590f8fef2bf3825da8f6fe3:
|
||||||
resolution: {tarball: https://codeload.github.com/mattdesl/gifenc/tar.gz/64842fca317b112a8590f8fef2bf3825da8f6fe3}
|
resolution: {gitHosted: true, tarball: https://codeload.github.com/mattdesl/gifenc/tar.gz/64842fca317b112a8590f8fef2bf3825da8f6fe3}
|
||||||
version: 1.0.3
|
version: 1.0.3
|
||||||
|
|
||||||
|
gifuct-js@2.1.2:
|
||||||
|
resolution: {integrity: sha512-rI2asw77u0mGgwhV3qA+OEgYqaDn5UNqgs+Bx0FGwSpuqfYn+Ir6RQY5ENNQ8SbIiG/m5gVa7CD5RriO4f4Lsg==}
|
||||||
|
|
||||||
glob-parent@5.1.2:
|
glob-parent@5.1.2:
|
||||||
resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
|
resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
|
||||||
engines: {node: '>= 6'}
|
engines: {node: '>= 6'}
|
||||||
|
|
@ -2046,6 +2050,9 @@ packages:
|
||||||
resolution: {integrity: sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==}
|
resolution: {integrity: sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
|
js-binary-schema-parser@2.0.3:
|
||||||
|
resolution: {integrity: sha512-xezGJmOb4lk/M1ZZLTR/jaBHQ4gG/lqQnJqdIv4721DMggsa1bDVlHXNeHYogaIEHD9vCRv0fcL4hMA+Coarkg==}
|
||||||
|
|
||||||
js-tokens@4.0.0:
|
js-tokens@4.0.0:
|
||||||
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
|
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
|
||||||
|
|
||||||
|
|
@ -4536,6 +4543,10 @@ snapshots:
|
||||||
|
|
||||||
gifenc@https://codeload.github.com/mattdesl/gifenc/tar.gz/64842fca317b112a8590f8fef2bf3825da8f6fe3: {}
|
gifenc@https://codeload.github.com/mattdesl/gifenc/tar.gz/64842fca317b112a8590f8fef2bf3825da8f6fe3: {}
|
||||||
|
|
||||||
|
gifuct-js@2.1.2:
|
||||||
|
dependencies:
|
||||||
|
js-binary-schema-parser: 2.0.3
|
||||||
|
|
||||||
glob-parent@5.1.2:
|
glob-parent@5.1.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
is-glob: 4.0.3
|
is-glob: 4.0.3
|
||||||
|
|
@ -4808,6 +4819,8 @@ snapshots:
|
||||||
has-symbols: 1.1.0
|
has-symbols: 1.1.0
|
||||||
set-function-name: 2.0.2
|
set-function-name: 2.0.2
|
||||||
|
|
||||||
|
js-binary-schema-parser@2.0.3: {}
|
||||||
|
|
||||||
js-tokens@4.0.0: {}
|
js-tokens@4.0.0: {}
|
||||||
|
|
||||||
js-yaml@4.1.0:
|
js-yaml@4.1.0:
|
||||||
|
|
|
||||||
|
|
@ -453,17 +453,16 @@ function GifMakerModal({ url, isVideo, sourceWidth, sourceHeight, ...props }: Re
|
||||||
}
|
}
|
||||||
|
|
||||||
function openGifMakerFromItem(item) {
|
function openGifMakerFromItem(item) {
|
||||||
const isGif = item.format === 1;
|
|
||||||
const directUrl = typeof item.src === "string" ? item.src : null;
|
const directUrl = typeof item.src === "string" ? item.src : null;
|
||||||
|
|
||||||
const candidates = collectCandidateUrls(item);
|
const candidates = collectCandidateUrls(item);
|
||||||
const adjustedCandidates = new Set<string>();
|
const adjustedCandidates = new Set<string>();
|
||||||
if (directUrl) adjustedCandidates.add(applyTenorMp4Fix(normalizeUrl(directUrl), isGif));
|
if (directUrl) adjustedCandidates.add(applyTenorMp4Fix(normalizeUrl(directUrl)));
|
||||||
for (const candidate of candidates) {
|
for (const candidate of candidates) {
|
||||||
adjustedCandidates.add(applyTenorMp4Fix(candidate, isGif));
|
adjustedCandidates.add(applyTenorMp4Fix(candidate));
|
||||||
}
|
}
|
||||||
|
|
||||||
const preferredUrl = directUrl ? applyTenorMp4Fix(normalizeUrl(directUrl), isGif) : null;
|
const preferredUrl = directUrl ? applyTenorMp4Fix(normalizeUrl(directUrl)) : null;
|
||||||
const orderedUrls = orderCandidateUrls(preferredUrl, adjustedCandidates);
|
const orderedUrls = orderCandidateUrls(preferredUrl, adjustedCandidates);
|
||||||
if (!orderedUrls.length) return;
|
if (!orderedUrls.length) return;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -177,13 +177,3 @@
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.vc-gifmaker-capture-wrapper {
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 1px;
|
|
||||||
height: 1px;
|
|
||||||
opacity: 0.01;
|
|
||||||
pointer-events: none;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
@ -4,9 +4,9 @@
|
||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { sleep } from "@utils/misc";
|
|
||||||
import type { PluginNative } from "@utils/types";
|
import type { PluginNative } from "@utils/types";
|
||||||
import { applyPalette, GIFEncoder, quantize } from "gifenc";
|
import { applyPalette, GIFEncoder, quantize } from "gifenc";
|
||||||
|
import { decompressFrames, parseGIF } from "gifuct-js";
|
||||||
|
|
||||||
import { CAPTIONS } from "../captions";
|
import { CAPTIONS } from "../captions";
|
||||||
import { measureTextLines } from "../captions/caption";
|
import { measureTextLines } from "../captions/caption";
|
||||||
|
|
@ -16,6 +16,7 @@ const MAX_FRAMES = 50;
|
||||||
const INTERNAL_FPS = 10;
|
const INTERNAL_FPS = 10;
|
||||||
const INTERNAL_MAX_DURATION = 3;
|
const INTERNAL_MAX_DURATION = 3;
|
||||||
const PALETTE_COLORS = 255;
|
const PALETTE_COLORS = 255;
|
||||||
|
const MAX_GIF_SCAN_BYTES = 524288; // 512KB
|
||||||
|
|
||||||
const ALLOWED_MEDIA_HOSTS = new Set([
|
const ALLOWED_MEDIA_HOSTS = new Set([
|
||||||
"cdn.discordapp.com",
|
"cdn.discordapp.com",
|
||||||
|
|
@ -44,6 +45,17 @@ function isDiscordCdnUrl(url: string): boolean {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function fetchFullGifBytes(url: string): Promise<Uint8Array> {
|
||||||
|
const resolved = resolveMediaUrl(url);
|
||||||
|
if (MediaNative) {
|
||||||
|
const { data } = await MediaNative.fetchMedia(resolved);
|
||||||
|
return new Uint8Array(data);
|
||||||
|
}
|
||||||
|
const res = await fetch(resolved);
|
||||||
|
if (!res.ok) throw new Error(`fetch failed: ${res.status}`);
|
||||||
|
return new Uint8Array(await res.arrayBuffer());
|
||||||
|
}
|
||||||
|
|
||||||
async function getMediaBlobUrl(url: string): Promise<string> {
|
async function getMediaBlobUrl(url: string): Promise<string> {
|
||||||
if (MediaNative) {
|
if (MediaNative) {
|
||||||
const { data, type } = await MediaNative.fetchMedia(url);
|
const { data, type } = await MediaNative.fetchMedia(url);
|
||||||
|
|
@ -155,6 +167,7 @@ async function encodeFrames(
|
||||||
options: GifMakerOptions,
|
options: GifMakerOptions,
|
||||||
frameCount: number,
|
frameCount: number,
|
||||||
drawFrame: (ctx: CanvasRenderingContext2D, i: number) => void | Promise<void>,
|
drawFrame: (ctx: CanvasRenderingContext2D, i: number) => void | Promise<void>,
|
||||||
|
delays?: number[],
|
||||||
): Promise<Blob> {
|
): Promise<Blob> {
|
||||||
const canvas = document.createElement("canvas");
|
const canvas = document.createElement("canvas");
|
||||||
const ctx = canvas.getContext("2d", { willReadFrequently: true });
|
const ctx = canvas.getContext("2d", { willReadFrequently: true });
|
||||||
|
|
@ -164,7 +177,7 @@ async function encodeFrames(
|
||||||
canvas.width = width;
|
canvas.width = width;
|
||||||
canvas.height = gifHeight;
|
canvas.height = gifHeight;
|
||||||
|
|
||||||
const delay = Math.round(1000 / INTERNAL_FPS);
|
const defaultDelay = Math.round(1000 / INTERNAL_FPS);
|
||||||
|
|
||||||
const frameData: Uint8ClampedArray[] = [];
|
const frameData: Uint8ClampedArray[] = [];
|
||||||
for (let i = 0; i < frameCount; i++) {
|
for (let i = 0; i < frameCount; i++) {
|
||||||
|
|
@ -199,7 +212,7 @@ async function encodeFrames(
|
||||||
for (let i = 0; i < frameCount; i++) {
|
for (let i = 0; i < frameCount; i++) {
|
||||||
const index = applyPalette(frameData[i], palette);
|
const index = applyPalette(frameData[i], palette);
|
||||||
gif.writeFrame(index, width, gifHeight, {
|
gif.writeFrame(index, width, gifHeight, {
|
||||||
delay,
|
delay: delays ? delays[i] : defaultDelay,
|
||||||
palette: i === 0 ? palette : undefined,
|
palette: i === 0 ? palette : undefined,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -271,50 +284,99 @@ export async function getSourceFrameInfo(url: string, isVideo: boolean): Promise
|
||||||
|
|
||||||
export async function createGif(url: string, isVideo: boolean, options: GifMakerOptions): Promise<Blob> {
|
export async function createGif(url: string, isVideo: boolean, options: GifMakerOptions): Promise<Blob> {
|
||||||
if (isVideo) return createGifFromVideo(url, options);
|
if (isVideo) return createGifFromVideo(url, options);
|
||||||
const info = await getSourceFrameInfo(url, false);
|
if (hasExt(url, ".gif")) {
|
||||||
if (info?.frameCount && info.frameCount > 1) {
|
try {
|
||||||
return createGifFromAnimatedImage(url, options);
|
return await createGifFromAnimatedImage(url, options);
|
||||||
|
} catch (err) {
|
||||||
|
if (!(err instanceof Error) || err.message !== "No animated frames found") {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return createGifFromImage(url, options);
|
return createGifFromImage(url, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getGifInfo(url: string): Promise<SourceFrameInfo | null> {
|
export function parseGifBytes(bytes: Uint8Array): SourceFrameInfo | null {
|
||||||
if (!MediaNative) return null;
|
if (bytes[0] !== 0x47 || bytes[1] !== 0x49 || bytes[2] !== 0x46) return null;
|
||||||
try {
|
|
||||||
const resolved = resolveMediaUrl(url);
|
|
||||||
const { data } = await MediaNative.fetchMedia(resolved);
|
|
||||||
const bytes = new Uint8Array(data);
|
|
||||||
|
|
||||||
if (bytes[0] !== 0x47 || bytes[1] !== 0x49 || bytes[2] !== 0x46) return null;
|
const frameWidth = bytes[6] | (bytes[7] << 8);
|
||||||
|
const frameHeight = bytes[8] | (bytes[9] << 8);
|
||||||
|
|
||||||
const frameWidth = bytes[6] | (bytes[7] << 8);
|
let frameCount = 0;
|
||||||
const frameHeight = bytes[8] | (bytes[9] << 8);
|
let totalDelay = 0;
|
||||||
|
let delayCount = 0;
|
||||||
|
|
||||||
let frameCount = 0;
|
const scanLimit = Math.min(bytes.length, MAX_GIF_SCAN_BYTES);
|
||||||
let totalDelay = 0;
|
for (let i = 0; i < scanLimit - 8; i++) {
|
||||||
let delayCount = 0;
|
if (bytes[i] === 0x2C) {
|
||||||
|
frameCount++;
|
||||||
for (let i = 0; i < bytes.length - 8; i++) {
|
if (frameCount > MAX_FRAMES) break;
|
||||||
if (bytes[i] === 0x2C) frameCount++;
|
}
|
||||||
if (bytes[i] === 0x21 && bytes[i + 1] === 0xF9 && bytes[i + 2] === 0x04) {
|
if (bytes[i] === 0x21 && bytes[i + 1] === 0xF9 && bytes[i + 2] === 0x04) {
|
||||||
const delay = bytes[i + 4] | (bytes[i + 5] << 8);
|
const delay = bytes[i + 4] | (bytes[i + 5] << 8);
|
||||||
if (delay > 0) {
|
if (delay > 0) {
|
||||||
totalDelay += delay;
|
totalDelay += delay;
|
||||||
delayCount++;
|
delayCount++;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (frameCount > 1 && delayCount > 0) {
|
if (frameCount > 1 && delayCount > 0) {
|
||||||
const avgFps = Math.round(100 / (totalDelay / delayCount));
|
const avgFps = Math.round(100 / (totalDelay / delayCount));
|
||||||
return { fps: Math.max(1, Math.min(60, avgFps)), frameCount, frameWidth, frameHeight };
|
return { fps: Math.max(1, Math.min(60, avgFps)), frameCount, frameWidth, frameHeight };
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getGifInfo(url: string): Promise<SourceFrameInfo | null> {
|
||||||
|
try {
|
||||||
|
const resolved = resolveMediaUrl(url);
|
||||||
|
|
||||||
|
let bytes: Uint8Array;
|
||||||
|
|
||||||
|
if (MediaNative) {
|
||||||
|
const { data } = await MediaNative.fetchMedia(resolved);
|
||||||
|
bytes = new Uint8Array(data);
|
||||||
|
} else {
|
||||||
|
bytes = await fetchGifBytes(resolved);
|
||||||
}
|
}
|
||||||
return null;
|
|
||||||
|
return parseGifBytes(bytes);
|
||||||
} catch {
|
} catch {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function fetchGifBytes(url: string): Promise<Uint8Array> {
|
||||||
|
const res = await fetch(url, {
|
||||||
|
headers: { Range: `bytes=0-${MAX_GIF_SCAN_BYTES}` }
|
||||||
|
});
|
||||||
|
if (res.ok) {
|
||||||
|
return new Uint8Array(await res.arrayBuffer());
|
||||||
|
}
|
||||||
|
|
||||||
|
const full = await fetch(url);
|
||||||
|
if (!full.ok) throw new Error(`fetch failed: ${full.status}`);
|
||||||
|
const reader = full.body?.getReader();
|
||||||
|
if (!reader) return new Uint8Array(await full.arrayBuffer()).slice(0, MAX_GIF_SCAN_BYTES);
|
||||||
|
const chunks: Uint8Array[] = [];
|
||||||
|
let total = 0;
|
||||||
|
while (total < MAX_GIF_SCAN_BYTES) {
|
||||||
|
const { done, value } = await reader.read();
|
||||||
|
if (done) break;
|
||||||
|
chunks.push(value);
|
||||||
|
total += value.length;
|
||||||
|
}
|
||||||
|
reader.cancel();
|
||||||
|
const combined = new Uint8Array(total);
|
||||||
|
let offset = 0;
|
||||||
|
for (const chunk of chunks) {
|
||||||
|
combined.set(chunk, offset);
|
||||||
|
offset += chunk.length;
|
||||||
|
}
|
||||||
|
return combined;
|
||||||
|
}
|
||||||
|
|
||||||
async function getWebpInfo(url: string): Promise<SourceFrameInfo | null> {
|
async function getWebpInfo(url: string): Promise<SourceFrameInfo | null> {
|
||||||
if (!MediaNative) return null;
|
if (!MediaNative) return null;
|
||||||
try {
|
try {
|
||||||
|
|
@ -407,45 +469,70 @@ async function getVideoSourceInfo(url: string): Promise<SourceFrameInfo | null>
|
||||||
}
|
}
|
||||||
|
|
||||||
async function createGifFromAnimatedImage(url: string, options: GifMakerOptions): Promise<Blob> {
|
async function createGifFromAnimatedImage(url: string, options: GifMakerOptions): Promise<Blob> {
|
||||||
const resolved = resolveMediaUrl(url);
|
const bytes = await fetchFullGifBytes(url);
|
||||||
const img = new Image();
|
const parsedGif = parseGIF(bytes.buffer as ArrayBuffer);
|
||||||
img.crossOrigin = "anonymous";
|
const frames = decompressFrames(parsedGif, true);
|
||||||
|
|
||||||
const wrapper = document.createElement("div");
|
if (frames.length <= 1) throw new Error("No animated frames found");
|
||||||
wrapper.className = "vc-gifmaker-capture-wrapper";
|
|
||||||
wrapper.appendChild(img);
|
|
||||||
document.body.appendChild(wrapper);
|
|
||||||
|
|
||||||
await new Promise<void>((resolve, reject) => {
|
const gifW = parsedGif.lsd.width;
|
||||||
img.onload = () => {
|
const gifH = parsedGif.lsd.height;
|
||||||
setTimeout(() => resolve(), 200);
|
|
||||||
};
|
|
||||||
img.onerror = () => {
|
|
||||||
wrapper.remove();
|
|
||||||
reject(new Error(`Failed to load: ${url}`));
|
|
||||||
};
|
|
||||||
if (isDiscordCdnUrl(resolved)) {
|
|
||||||
getMediaBlobUrl(resolved).then(blobUrl => {
|
|
||||||
blobUrlMap.set(img, blobUrl);
|
|
||||||
img.src = blobUrl;
|
|
||||||
}).catch(reject);
|
|
||||||
} else {
|
|
||||||
img.src = resolved;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
const composite = document.createElement("canvas");
|
||||||
const frameCount = Math.min(INTERNAL_FPS * INTERNAL_MAX_DURATION, MAX_FRAMES);
|
composite.width = gifW;
|
||||||
const interval = 1000 / INTERNAL_FPS;
|
composite.height = gifH;
|
||||||
|
const ctx = composite.getContext("2d", { willReadFrequently: true });
|
||||||
|
if (!ctx) throw new Error("Failed to get canvas context for GIF compositing.");
|
||||||
|
|
||||||
return await encodeFrames(options.width, options.height, options, frameCount, async (ctx, i) => {
|
const patchCanvas = document.createElement("canvas");
|
||||||
if (i > 0) {
|
|
||||||
await sleep(interval);
|
const frameCount = Math.min(frames.length, MAX_FRAMES);
|
||||||
|
const delays: number[] = [];
|
||||||
|
const rendered: HTMLCanvasElement[] = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < frameCount; i++) {
|
||||||
|
const frame = frames[i];
|
||||||
|
delays.push(frame.delay * 10);
|
||||||
|
|
||||||
|
if (i > 0) {
|
||||||
|
const prev = frames[i - 1];
|
||||||
|
if (prev.disposalType === 2) {
|
||||||
|
ctx.clearRect(prev.dims.left, prev.dims.top, prev.dims.width, prev.dims.height);
|
||||||
|
} else if (prev.disposalType === 3 && i > 1) {
|
||||||
|
const prevCtx = rendered[i - 2].getContext("2d");
|
||||||
|
if (prevCtx) {
|
||||||
|
const prevState = prevCtx.getImageData(0, 0, gifW, gifH);
|
||||||
|
ctx.putImageData(prevState, 0, 0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
ctx.drawImage(img, 0, 0, options.width, options.height);
|
}
|
||||||
});
|
|
||||||
} finally {
|
const patchData = new ImageData(
|
||||||
wrapper.remove();
|
new Uint8ClampedArray(frame.patch),
|
||||||
cleanupBlobUrl(img);
|
frame.dims.width,
|
||||||
|
frame.dims.height
|
||||||
|
);
|
||||||
|
patchCanvas.width = frame.dims.width;
|
||||||
|
patchCanvas.height = frame.dims.height;
|
||||||
|
const patchCtx = patchCanvas.getContext("2d");
|
||||||
|
if (!patchCtx) throw new Error("Failed to get canvas context for patch rendering.");
|
||||||
|
patchCtx.putImageData(patchData, 0, 0);
|
||||||
|
ctx.drawImage(patchCanvas, frame.dims.left, frame.dims.top);
|
||||||
|
|
||||||
|
const snap = document.createElement("canvas");
|
||||||
|
snap.width = gifW;
|
||||||
|
snap.height = gifH;
|
||||||
|
const snapCtx = snap.getContext("2d");
|
||||||
|
if (!snapCtx) throw new Error("Failed to get canvas context for frame snapshot.");
|
||||||
|
snapCtx.drawImage(composite, 0, 0);
|
||||||
|
rendered.push(snap);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return await encodeFrames(
|
||||||
|
options.width, options.height, options, frameCount,
|
||||||
|
(encodeCtx, i) => {
|
||||||
|
encodeCtx.drawImage(rendered[i], 0, 0, options.width, options.height);
|
||||||
|
},
|
||||||
|
delays
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,9 +15,7 @@ export function looksLikeUrl(value: string) {
|
||||||
return value.startsWith("http://") || value.startsWith("https://") || value.startsWith("//");
|
return value.startsWith("http://") || value.startsWith("https://") || value.startsWith("//");
|
||||||
}
|
}
|
||||||
|
|
||||||
export function applyTenorMp4Fix(url: string, isGif: boolean) {
|
export function applyTenorMp4Fix(url: string) {
|
||||||
if (isGif) return url;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { host } = new URL(url);
|
const { host } = new URL(url);
|
||||||
if (!host.endsWith("tenor.com")) return url;
|
if (!host.endsWith("tenor.com")) return url;
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue