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",
"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": {

View File

@ -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"]()}
</button>
<div class="flex items-center gap-3">
<div
class="flex items-center gap-2 flex-1 min-w-0 h-full"
>
<FancyInput
type="checkbox"
class="!w-fit"
bind:checked={imageSequence}
/>
<label for="extract-sequence" class="text-sm">
{m["convert.image_sequence.image_sequence"]()}
</label>
<!-- FIXME this is terrible -->
<div class="flex flex-col flex-wrap gap-1">
<!-- first row -->
<div class="flex items-center gap-3">
<div
class="flex items-center gap-2 flex-1 min-w-0 h-full"
>
<FancyInput
type="checkbox"
class="!w-fit"
bind:checked={imageSequence}
/>
<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 class="w-[80px] shrink-0">
<FancyInput
thin
inputClass="!h-9 !text-xs"
type="number"
extension="FPS"
placeholder="15"
bind:value={imageSequenceFPS}
disabled={!imageSequence}
/>
<!-- second row -->
<div class="flex items-center gap-3">
<div
class="flex items-center gap-2 flex-1 min-w-0 h-full"
>
<FancyInput
type="checkbox"
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>

View File

@ -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);

View File

@ -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:

View File

@ -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

View File

@ -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
);

View File

@ -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

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 {
public name = "vertd";
public ready = $state(false);
@ -358,36 +363,12 @@ export class VertdConverter extends Converter {
private cancelledConversions = new Set<string>();
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<number | null> {
@ -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