From 2f30c454dcf3923406a41fc8d33f9a1d499078b8 Mon Sep 17 00:00:00 2001 From: Maya Date: Sun, 10 May 2026 16:37:16 +0300 Subject: [PATCH] fix: transparent images issues --- messages/en.json | 3 +- .../functional/FormatDropdown.svelte | 72 +++++++++++++------ src/lib/converters/ffmpeg.animated.ts | 57 ++++++++++----- src/lib/converters/ffmpeg.codecs.ts | 14 ++-- src/lib/converters/ffmpeg.svelte.ts | 6 +- src/lib/converters/magick.svelte.ts | 2 +- src/lib/converters/mediabunny.svelte.ts | 2 +- src/lib/converters/vertd.svelte.ts | 44 ++++-------- 8 files changed, 119 insertions(+), 81 deletions(-) diff --git a/messages/en.json b/messages/en.json index fffca5a..67fbad7 100644 --- a/messages/en.json +++ b/messages/en.json @@ -61,7 +61,8 @@ }, "image_sequence": { "image_sequence": "Image Sequence", - "fps": "FPS" + "fps": "FPS", + "transparency": "Transparency" }, "large_file_warning": "Due to browser / device limitations, video to audio conversion is disabled for this file as it is larger than {limit}GB. We recommend using Firefox or Safari for files of this size since they have less limitations.", "external_warning": { diff --git a/src/lib/components/functional/FormatDropdown.svelte b/src/lib/components/functional/FormatDropdown.svelte index 7f37791..09f9072 100644 --- a/src/lib/components/functional/FormatDropdown.svelte +++ b/src/lib/components/functional/FormatDropdown.svelte @@ -49,11 +49,16 @@ let imageSequenceFPS = $state( file?.conversionSettings?.imageSequenceFPS ?? 15, ); + // svelte-ignore state_referenced_locally + let imageSequenceTransparency = $state( + file?.conversionSettings?.imageSequenceTransparency ?? false, + ); $effect(() => { if (!file) return; file.conversionSettings.imageSequence = imageSequence; file.conversionSettings.imageSequenceFPS = imageSequenceFPS; + file.conversionSettings.imageSequenceTransparency = imageSequenceTransparency; }); const normalize = (str: string) => str.replace(/^\./, "").toLowerCase(); @@ -570,29 +575,52 @@ > {m["convert.archive_file.extract"]()} -
-
- - + +
+ +
+
+ + +
+
+ +
-
- + + +
+
+ + +
+
diff --git a/src/lib/converters/ffmpeg.animated.ts b/src/lib/converters/ffmpeg.animated.ts index 5a6f5fc..6b86551 100644 --- a/src/lib/converters/ffmpeg.animated.ts +++ b/src/lib/converters/ffmpeg.animated.ts @@ -1,5 +1,6 @@ import { toArgs, animatedImageFormats } from "$lib/converters/ffmpeg.codecs"; import type { ConversionSettings } from "$lib/types/conversion-settings"; +import { videoFormats } from "./vertd.svelte"; export function buildImageSequenceCommand( outputFormat: string, @@ -19,32 +20,52 @@ export function buildImageSequenceCommand( ]; const scaleFilter = "scale=trunc(iw/2)*2:trunc(ih/2)*2"; const isAnimatedImage = animatedImageFormats.includes(outputFormat); + const enableTransparency = settings.imageSequenceTransparency ?? false; - if ( - outputFormat === "mp4" || - outputFormat === "mkv" || - outputFormat === "mov" - ) { - baseArgs.push("-vf", scaleFilter, "-pix_fmt", "yuv420p"); - } else if (outputFormat === "webm") { - baseArgs.push( - "-vf", - scaleFilter, - "-pix_fmt", - "yuva420p", - "-auto-alt-ref", - "0", - ); + if (videoFormats.includes(outputFormat)) { + const fpsFilter = `fps=${settings.imageSequenceFPS || 15}`; + const blackCompositeFilter = + `color=c=black,format=rgb24[bg];` + + `[0:v]${fpsFilter},${scaleFilter},setsar=1[fg];` + + `[bg][fg]scale2ref[bg2][fg2];` + + `[bg2][fg2]overlay=format=auto:shortest=1,setsar=1`; + + if (outputFormat === "webm" && enableTransparency) { + baseArgs.push( + "-filter_complex", + `[0:v]${fpsFilter},${scaleFilter},setsar=1`, + "-pix_fmt", + "yuva420p", + "-auto-alt-ref", + "0", + ); + } else { + baseArgs.push( + "-filter_complex", + blackCompositeFilter, + "-pix_fmt", + "yuv420p", + ); + } } else if (outputFormat === "gif") { + const paletteuse = enableTransparency + ? "[p]paletteuse=alpha_threshold=128" + : "[p]paletteuse"; baseArgs.push( "-filter_complex", - `fps=${settings.imageSequenceFPS || 15},${scaleFilter},split[s0][s1];[s0]palettegen[p];[s1][p]paletteuse`, + `fps=${settings.imageSequenceFPS || 15},${scaleFilter},split[s0][s1];[s0]palettegen[p];[s1]${paletteuse}`, ); } else if (isAnimatedImage) { baseArgs.push("-vf", scaleFilter); - if (outputFormat === "apng") baseArgs.push("-plays", "0"); + if (outputFormat === "apng") { + baseArgs.push("-plays", "0"); + if (enableTransparency) baseArgs.push("-pix_fmt", "rgba"); + } else if (outputFormat === "webp") { + if (enableTransparency) baseArgs.push("-pix_fmt", "rgba"); + } } else { - baseArgs.push("-vf", scaleFilter, "-pix_fmt", "yuv420p"); + const pixFmt = enableTransparency ? "yuva420p" : "yuv420p"; + baseArgs.push("-vf", scaleFilter, "-pix_fmt", pixFmt); } baseArgs.push("output" + to); diff --git a/src/lib/converters/ffmpeg.codecs.ts b/src/lib/converters/ffmpeg.codecs.ts index 30a83eb..b3c1447 100644 --- a/src/lib/converters/ffmpeg.codecs.ts +++ b/src/lib/converters/ffmpeg.codecs.ts @@ -4,9 +4,6 @@ export const CONVERSION_BITRATES = ["auto", "custom", 16, 32, 64, 96, 128, 160, // prettier-ignore export const SAMPLE_RATES = ["auto", "custom", 8000, 11025, 12000, 16000, 22050, 24000, 32000, 44100, 48000, 96000,] as const; -// prettier-ignore -export const videoFormats = ["mkv", "mp4", "avi", "mov", "webm", "ts", "mts", "m2ts", "wmv", "mpg", "mpeg", "flv", "f4v", "vob", "m4v", "3gp", "3g2", "mxf", "ogv", "rm", "rmvb", "divx"]; - // prettier-ignore export const animatedImageFormats = ["gif", "webp", "apng"]; @@ -30,6 +27,7 @@ export const getCodecs = ( case ".m4v": case ".3gp": case ".3g2": + case ".nut": return { video: "libx264", audio: "aac" }; case ".wmv": return { video: "wmv2", audio: "wmav2" }; @@ -48,6 +46,14 @@ export const getCodecs = ( return { video: "mpeg2video", audio: "mp2" }; case ".mxf": return { video: "mpeg2video", audio: "pcm_s16le" }; + case ".h264": + return { video: "libx264", audio: "none" }; + case ".swf": + return { video: "flv1", audio: "mp3" }; + case ".amv": + return { video: "amv", audio: "adpcm_ima_amv" }; + case ".asf": + return { video: "wmv2", audio: "wmav2" }; // audio case ".mp3": @@ -76,7 +82,7 @@ export const getCodecs = ( // animated images case ".gif": case ".webp": - //case ".apng": + case ".apng": return { video: ext.slice(1), audio: "none" }; default: diff --git a/src/lib/converters/ffmpeg.svelte.ts b/src/lib/converters/ffmpeg.svelte.ts index 9c9c94f..d4a170c 100644 --- a/src/lib/converters/ffmpeg.svelte.ts +++ b/src/lib/converters/ffmpeg.svelte.ts @@ -7,7 +7,6 @@ import { m } from "$lib/paraglide/messages"; import { Settings } from "$lib/sections/settings/index.svelte"; import { ToastManager } from "$lib/util/toast.svelte"; import { - videoFormats, getCodecs, toArgs, lossless, @@ -19,6 +18,7 @@ import type { SettingDefinition, ConversionSettings, } from "$lib/types/conversion-settings"; +import { videoFormats } from "./vertd.svelte"; // TODO: differentiate in UI? (not native formats) export class FFmpegConverter extends Converter { @@ -58,7 +58,7 @@ export class FFmpegConverter extends Converter { new FormatInfo("m4b", true, true), new FormatInfo("voc", true, true), new FormatInfo("weba", true, true), - ...videoFormats.map((f) => new FormatInfo(f, true, true, false, 0)), + ...videoFormats.map((f: string) => new FormatInfo(f, true, true, false, 0)), ]; public readonly reportsProgress = true; @@ -194,7 +194,7 @@ export class FFmpegConverter extends Converter { if (!to.startsWith(".")) to = `.${to}`; const conversionSettings = - Object.keys(settings).length > 0 + Object.keys(settings).length > 5 // TODO: find better way to do this lmfao, rn we are just assuming all settings are present if there's at least 5 keys but ts bad ? settings : await this.getDefaultSettings(); // use defaults if not provided diff --git a/src/lib/converters/magick.svelte.ts b/src/lib/converters/magick.svelte.ts index 9d072e1..04d13f4 100644 --- a/src/lib/converters/magick.svelte.ts +++ b/src/lib/converters/magick.svelte.ts @@ -302,7 +302,7 @@ export class MagickConverter extends Converter { // every other format handled by magick worker const conversionSettings = JSON.stringify( - Object.keys(settings).length > 0 + Object.keys(settings).length > 5 ? settings // user-provided settings : await this.getDefaultSettings(input), // use defaults if not provided ); diff --git a/src/lib/converters/mediabunny.svelte.ts b/src/lib/converters/mediabunny.svelte.ts index fe1c36e..325fc19 100644 --- a/src/lib/converters/mediabunny.svelte.ts +++ b/src/lib/converters/mediabunny.svelte.ts @@ -492,7 +492,7 @@ export class MediabunnyConverter extends Converter { }); const conversionSettings = - Object.keys(settings).length > 0 + Object.keys(settings).length > 5 ? settings // user-provided settings : await this.getDefaultSettings(file); // use defaults if not provided diff --git a/src/lib/converters/vertd.svelte.ts b/src/lib/converters/vertd.svelte.ts index 2d73716..862526a 100644 --- a/src/lib/converters/vertd.svelte.ts +++ b/src/lib/converters/vertd.svelte.ts @@ -339,6 +339,11 @@ const downloadFile = async (url: string, file: VertFile): Promise => { }); }; +// prettier-ignore +export const videoFormats = ["mp4", "mkv", "webm", "avi", "wmv", "mov", "gif", "apng", "webp", "mts", "ts", "m2ts", "mpg", "mpeg", "flv", "f4v", "vob", "m4v", "3gp", "3g2", "mxf", "ogv", "rm", "rmvb", "h264", "divx", "swf", "amv", "asf", "nut"]; +const cantEncode = ["rm", "rmvb"]; +const cantDecode = [""]; + export class VertdConverter extends Converter { public name = "vertd"; public ready = $state(false); @@ -358,36 +363,12 @@ export class VertdConverter extends Converter { private cancelledConversions = new Set(); public supportedFormats = [ - new FormatInfo("mp4", true, true), - new FormatInfo("mkv", true, true), - new FormatInfo("webm", true, true), - new FormatInfo("avi", true, true), - new FormatInfo("wmv", true, true), - new FormatInfo("mov", true, true), - new FormatInfo("gif", true, true), - new FormatInfo("apng", true, true), - new FormatInfo("webp", true, true), - new FormatInfo("mts", true, true), - new FormatInfo("ts", true, true), - new FormatInfo("m2ts", true, true), - new FormatInfo("mpg", true, true), - new FormatInfo("mpeg", true, true), - new FormatInfo("flv", true, true), - new FormatInfo("f4v", true, true), - new FormatInfo("vob", true, true), - new FormatInfo("m4v", true, true), - new FormatInfo("3gp", true, true), - new FormatInfo("3g2", true, true), - new FormatInfo("mxf", true, true), - new FormatInfo("ogv", true, true), - new FormatInfo("rm", true, false), - new FormatInfo("rmvb", true, false), - new FormatInfo("h264", true, true), - new FormatInfo("divx", true, true), - new FormatInfo("swf", true, true), - new FormatInfo("amv", true, true), - new FormatInfo("asf", true, true), - new FormatInfo("nut", true, true), + ...videoFormats + .map((f: string) => new FormatInfo(f, true, true, true, 0)) + .filter((format) => !cantEncode.includes(format.name.slice(1))) + .filter((format) => !cantDecode.includes(format.name.slice(1))), + ...cantEncode.map((f) => new FormatInfo(f, true, false, true, 0)), + ...cantDecode.map((f) => new FormatInfo(f, false, true, true, 0)), ]; // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -404,6 +385,7 @@ export class VertdConverter extends Converter { this.log("created converter"); this.log("not rly sure how to implement this :P"); this.status = "ready"; + this.log(JSON.stringify(this.supportedFormats, null, 2)); } private async getServerSizeLimit(apiUrl: string): Promise { @@ -763,7 +745,7 @@ export class VertdConverter extends Converter { let fileUpload = input; const conversionSettings = // vertd expects object not string json - Object.keys(settings).length > 0 + Object.keys(settings).length > 5 ? settings // user-provided settings : await this.getDefaultSettings(input); // use defaults if not provided