fix: transparent images issues

This commit is contained in:
Maya 2026-05-10 16:37:16 +03:00
parent f210bb886e
commit 2f30c454dc
No known key found for this signature in database
8 changed files with 119 additions and 81 deletions

View File

@ -61,7 +61,8 @@
}, },
"image_sequence": { "image_sequence": {
"image_sequence": "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.", "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": { "external_warning": {

View File

@ -49,11 +49,16 @@
let imageSequenceFPS = $state( let imageSequenceFPS = $state(
file?.conversionSettings?.imageSequenceFPS ?? 15, file?.conversionSettings?.imageSequenceFPS ?? 15,
); );
// svelte-ignore state_referenced_locally
let imageSequenceTransparency = $state(
file?.conversionSettings?.imageSequenceTransparency ?? false,
);
$effect(() => { $effect(() => {
if (!file) return; if (!file) return;
file.conversionSettings.imageSequence = imageSequence; file.conversionSettings.imageSequence = imageSequence;
file.conversionSettings.imageSequenceFPS = imageSequenceFPS; file.conversionSettings.imageSequenceFPS = imageSequenceFPS;
file.conversionSettings.imageSequenceTransparency = imageSequenceTransparency;
}); });
const normalize = (str: string) => str.replace(/^\./, "").toLowerCase(); const normalize = (str: string) => str.replace(/^\./, "").toLowerCase();
@ -570,29 +575,52 @@
> >
{m["convert.archive_file.extract"]()} {m["convert.archive_file.extract"]()}
</button> </button>
<div class="flex items-center gap-3"> <!-- FIXME this is terrible -->
<div <div class="flex flex-col flex-wrap gap-1">
class="flex items-center gap-2 flex-1 min-w-0 h-full" <!-- first row -->
> <div class="flex items-center gap-3">
<FancyInput <div
type="checkbox" class="flex items-center gap-2 flex-1 min-w-0 h-full"
class="!w-fit" >
bind:checked={imageSequence} <FancyInput
/> type="checkbox"
<label for="extract-sequence" class="text-sm"> class="!w-fit"
{m["convert.image_sequence.image_sequence"]()} bind:checked={imageSequence}
</label> />
<label for="extract-sequence" class="text-sm">
{m[
"convert.image_sequence.image_sequence"
]()}
</label>
</div>
<div class="w-[80px] shrink-0">
<FancyInput
thin
inputClass="!h-9 !text-xs"
type="number"
extension="FPS"
placeholder="15"
bind:value={imageSequenceFPS}
disabled={!imageSequence}
/>
</div>
</div> </div>
<div class="w-[80px] shrink-0">
<FancyInput <!-- second row -->
thin <div class="flex items-center gap-3">
inputClass="!h-9 !text-xs" <div
type="number" class="flex items-center gap-2 flex-1 min-w-0 h-full"
extension="FPS" >
placeholder="15" <FancyInput
bind:value={imageSequenceFPS} type="checkbox"
disabled={!imageSequence} class="!w-fit"
/> bind:checked={imageSequenceTransparency}
/>
<label for="extract-sequence" class="text-sm">
{m["convert.image_sequence.transparency"]()}
</label>
</div>
<!-- second thing -->
</div> </div>
</div> </div>
</div> </div>

View File

@ -1,5 +1,6 @@
import { toArgs, animatedImageFormats } from "$lib/converters/ffmpeg.codecs"; import { toArgs, animatedImageFormats } from "$lib/converters/ffmpeg.codecs";
import type { ConversionSettings } from "$lib/types/conversion-settings"; import type { ConversionSettings } from "$lib/types/conversion-settings";
import { videoFormats } from "./vertd.svelte";
export function buildImageSequenceCommand( export function buildImageSequenceCommand(
outputFormat: string, outputFormat: string,
@ -19,32 +20,52 @@ export function buildImageSequenceCommand(
]; ];
const scaleFilter = "scale=trunc(iw/2)*2:trunc(ih/2)*2"; const scaleFilter = "scale=trunc(iw/2)*2:trunc(ih/2)*2";
const isAnimatedImage = animatedImageFormats.includes(outputFormat); const isAnimatedImage = animatedImageFormats.includes(outputFormat);
const enableTransparency = settings.imageSequenceTransparency ?? false;
if ( if (videoFormats.includes(outputFormat)) {
outputFormat === "mp4" || const fpsFilter = `fps=${settings.imageSequenceFPS || 15}`;
outputFormat === "mkv" || const blackCompositeFilter =
outputFormat === "mov" `color=c=black,format=rgb24[bg];` +
) { `[0:v]${fpsFilter},${scaleFilter},setsar=1[fg];` +
baseArgs.push("-vf", scaleFilter, "-pix_fmt", "yuv420p"); `[bg][fg]scale2ref[bg2][fg2];` +
} else if (outputFormat === "webm") { `[bg2][fg2]overlay=format=auto:shortest=1,setsar=1`;
baseArgs.push(
"-vf", if (outputFormat === "webm" && enableTransparency) {
scaleFilter, baseArgs.push(
"-pix_fmt", "-filter_complex",
"yuva420p", `[0:v]${fpsFilter},${scaleFilter},setsar=1`,
"-auto-alt-ref", "-pix_fmt",
"0", "yuva420p",
); "-auto-alt-ref",
"0",
);
} else {
baseArgs.push(
"-filter_complex",
blackCompositeFilter,
"-pix_fmt",
"yuv420p",
);
}
} else if (outputFormat === "gif") { } else if (outputFormat === "gif") {
const paletteuse = enableTransparency
? "[p]paletteuse=alpha_threshold=128"
: "[p]paletteuse";
baseArgs.push( baseArgs.push(
"-filter_complex", "-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) { } else if (isAnimatedImage) {
baseArgs.push("-vf", scaleFilter); 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 { } else {
baseArgs.push("-vf", scaleFilter, "-pix_fmt", "yuv420p"); const pixFmt = enableTransparency ? "yuva420p" : "yuv420p";
baseArgs.push("-vf", scaleFilter, "-pix_fmt", pixFmt);
} }
baseArgs.push("output" + to); baseArgs.push("output" + to);

View File

@ -4,9 +4,6 @@ export const CONVERSION_BITRATES = ["auto", "custom", 16, 32, 64, 96, 128, 160,
// prettier-ignore // prettier-ignore
export const SAMPLE_RATES = ["auto", "custom", 8000, 11025, 12000, 16000, 22050, 24000, 32000, 44100, 48000, 96000,] as const; 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 // prettier-ignore
export const animatedImageFormats = ["gif", "webp", "apng"]; export const animatedImageFormats = ["gif", "webp", "apng"];
@ -30,6 +27,7 @@ export const getCodecs = (
case ".m4v": case ".m4v":
case ".3gp": case ".3gp":
case ".3g2": case ".3g2":
case ".nut":
return { video: "libx264", audio: "aac" }; return { video: "libx264", audio: "aac" };
case ".wmv": case ".wmv":
return { video: "wmv2", audio: "wmav2" }; return { video: "wmv2", audio: "wmav2" };
@ -48,6 +46,14 @@ export const getCodecs = (
return { video: "mpeg2video", audio: "mp2" }; return { video: "mpeg2video", audio: "mp2" };
case ".mxf": case ".mxf":
return { video: "mpeg2video", audio: "pcm_s16le" }; 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 // audio
case ".mp3": case ".mp3":
@ -76,7 +82,7 @@ export const getCodecs = (
// animated images // animated images
case ".gif": case ".gif":
case ".webp": case ".webp":
//case ".apng": case ".apng":
return { video: ext.slice(1), audio: "none" }; return { video: ext.slice(1), audio: "none" };
default: default:

View File

@ -7,7 +7,6 @@ import { m } from "$lib/paraglide/messages";
import { Settings } from "$lib/sections/settings/index.svelte"; import { Settings } from "$lib/sections/settings/index.svelte";
import { ToastManager } from "$lib/util/toast.svelte"; import { ToastManager } from "$lib/util/toast.svelte";
import { import {
videoFormats,
getCodecs, getCodecs,
toArgs, toArgs,
lossless, lossless,
@ -19,6 +18,7 @@ import type {
SettingDefinition, SettingDefinition,
ConversionSettings, ConversionSettings,
} from "$lib/types/conversion-settings"; } from "$lib/types/conversion-settings";
import { videoFormats } from "./vertd.svelte";
// TODO: differentiate in UI? (not native formats) // TODO: differentiate in UI? (not native formats)
export class FFmpegConverter extends Converter { export class FFmpegConverter extends Converter {
@ -58,7 +58,7 @@ export class FFmpegConverter extends Converter {
new FormatInfo("m4b", true, true), new FormatInfo("m4b", true, true),
new FormatInfo("voc", true, true), new FormatInfo("voc", true, true),
new FormatInfo("weba", 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; public readonly reportsProgress = true;
@ -194,7 +194,7 @@ export class FFmpegConverter extends Converter {
if (!to.startsWith(".")) to = `.${to}`; if (!to.startsWith(".")) to = `.${to}`;
const conversionSettings = 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 ? settings
: await this.getDefaultSettings(); // use defaults if not provided : await this.getDefaultSettings(); // use defaults if not provided

View File

@ -302,7 +302,7 @@ export class MagickConverter extends Converter {
// every other format handled by magick worker // every other format handled by magick worker
const conversionSettings = JSON.stringify( const conversionSettings = JSON.stringify(
Object.keys(settings).length > 0 Object.keys(settings).length > 5
? settings // user-provided settings ? settings // user-provided settings
: await this.getDefaultSettings(input), // use defaults if not provided : await this.getDefaultSettings(input), // use defaults if not provided
); );

View File

@ -492,7 +492,7 @@ export class MediabunnyConverter extends Converter {
}); });
const conversionSettings = const conversionSettings =
Object.keys(settings).length > 0 Object.keys(settings).length > 5
? settings // user-provided settings ? settings // user-provided settings
: await this.getDefaultSettings(file); // use defaults if not provided : await this.getDefaultSettings(file); // use defaults if not provided

View File

@ -339,6 +339,11 @@ const downloadFile = async (url: string, file: VertFile): Promise<Blob> => {
}); });
}; };
// 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 { export class VertdConverter extends Converter {
public name = "vertd"; public name = "vertd";
public ready = $state(false); public ready = $state(false);
@ -358,36 +363,12 @@ export class VertdConverter extends Converter {
private cancelledConversions = new Set<string>(); private cancelledConversions = new Set<string>();
public supportedFormats = [ public supportedFormats = [
new FormatInfo("mp4", true, true), ...videoFormats
new FormatInfo("mkv", true, true), .map((f: string) => new FormatInfo(f, true, true, true, 0))
new FormatInfo("webm", true, true), .filter((format) => !cantEncode.includes(format.name.slice(1)))
new FormatInfo("avi", true, true), .filter((format) => !cantDecode.includes(format.name.slice(1))),
new FormatInfo("wmv", true, true), ...cantEncode.map((f) => new FormatInfo(f, true, false, true, 0)),
new FormatInfo("mov", true, true), ...cantDecode.map((f) => new FormatInfo(f, false, true, true, 0)),
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),
]; ];
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
@ -404,6 +385,7 @@ export class VertdConverter extends Converter {
this.log("created converter"); this.log("created converter");
this.log("not rly sure how to implement this :P"); this.log("not rly sure how to implement this :P");
this.status = "ready"; this.status = "ready";
this.log(JSON.stringify(this.supportedFormats, null, 2));
} }
private async getServerSizeLimit(apiUrl: string): Promise<number | null> { private async getServerSizeLimit(apiUrl: string): Promise<number | null> {
@ -763,7 +745,7 @@ export class VertdConverter extends Converter {
let fileUpload = input; let fileUpload = input;
const conversionSettings = // vertd expects object not string json const conversionSettings = // vertd expects object not string json
Object.keys(settings).length > 0 Object.keys(settings).length > 5
? settings // user-provided settings ? settings // user-provided settings
: await this.getDefaultSettings(input); // use defaults if not provided : await this.getDefaultSettings(input); // use defaults if not provided