feat: conversion quality

fixes #56
This commit is contained in:
Maya 2025-09-02 16:53:33 +08:00
parent e8cdb18dd5
commit 76ff9cc704
4 changed files with 57 additions and 17 deletions

View File

@ -5,6 +5,7 @@ import { browser } from "$app/environment";
import { error, log } from "$lib/logger"; import { error, log } from "$lib/logger";
import { addToast } from "$lib/store/ToastProvider"; import { addToast } from "$lib/store/ToastProvider";
import { m } from "$lib/paraglide/messages"; import { m } from "$lib/paraglide/messages";
import { Settings } from "$lib/sections/settings/index.svelte";
// TODO: differentiate in UI? (not native formats) // TODO: differentiate in UI? (not native formats)
const videoFormats = [ const videoFormats = [
@ -203,15 +204,23 @@ export class FFmpegConverter extends Converter {
const outputFormat = to.slice(1); const outputFormat = to.slice(1);
const lossless = ["flac", "alac", "wav"]; const lossless = ["flac", "alac", "wav"];
let audioBitrateArgs: string[]; const userSetting = Settings.instance.settings.ffmpegQuality;
if ( log(["converters", this.name], `using user setting for audio bitrate: ${userSetting}`);
lossless.includes(inputFormat) && let audioBitrateArgs: string[] = [];
!lossless.includes(outputFormat)
) { if (userSetting !== "auto") {
audioBitrateArgs = ["-b:a", "320k"]; // user's setting
audioBitrateArgs = ["-b:a", `${userSetting}k`];
} else { } else {
const inputBitrate = await this.detectAudioBitrate(ffmpeg); // detect bitrate of original file and use
audioBitrateArgs = inputBitrate ? ["-b:a", `${inputBitrate}k`] : []; if (lossless.includes(inputFormat) && !lossless.includes(outputFormat)) {
audioBitrateArgs = ["-b:a", "320k"];
log(["converters", this.name], `using default audio bitrate: 320k`);
} else {
const inputBitrate = await this.detectAudioBitrate(ffmpeg);
audioBitrateArgs = inputBitrate ? ["-b:a", `${inputBitrate}k`] : [];
log(["converters", this.name], `using detected audio bitrate: ${inputBitrate}k`);
}
} }
// video to audio // video to audio

View File

@ -7,6 +7,7 @@ import { VertFile } from "$lib/types";
import MagickWorker from "$lib/workers/magick?worker&url"; import MagickWorker from "$lib/workers/magick?worker&url";
import { Converter, FormatInfo } from "./converter.svelte"; import { Converter, FormatInfo } from "./converter.svelte";
import { imageFormats } from "./magick-automated"; import { imageFormats } from "./magick-automated";
import { Settings } from "$lib/sections/settings/index.svelte";
export class MagickConverter extends Converter { export class MagickConverter extends Converter {
private worker: Worker = browser private worker: Worker = browser
@ -108,7 +109,11 @@ export class MagickConverter extends Converter {
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
...args: any[] ...args: any[]
): Promise<VertFile> { ): Promise<VertFile> {
const compression: number | undefined = args.at(0); let compression: number | undefined = args.at(0);
if (compression == null) {
compression = Settings.instance.settings.magickQuality;
log(["converters", this.name], `using user setting for quality: ${compression}%`);
}
log(["converters", this.name], `converting ${input.name} to ${to}`); log(["converters", this.name], `converting ${input.name} to ${to}`);
// handle converting from SVG manually because magick-wasm doesn't support it // handle converting from SVG manually because magick-wasm doesn't support it

View File

@ -62,6 +62,7 @@
type="number" type="number"
min={1} min={1}
max={100} max={100}
extension={"%"}
/> />
</div> </div>
<div class="flex flex-col gap-2"> <div class="flex flex-col gap-2">

View File

@ -26,6 +26,7 @@ magickPromise
const handleMessage = async (message: any): Promise<any> => { const handleMessage = async (message: any): Promise<any> => {
switch (message.type) { switch (message.type) {
case "convert": { case "convert": {
const compression: number | undefined = message.compression;
if (!message.to.startsWith(".")) message.to = `.${message.to}`; if (!message.to.startsWith(".")) message.to = `.${message.to}`;
message.to = message.to.toLowerCase(); message.to = message.to.toLowerCase();
if (message.to === ".jfif") message.to = ".jpeg"; if (message.to === ".jfif") message.to = ".jpeg";
@ -66,7 +67,7 @@ const handleMessage = async (message: any): Promise<any> => {
const convertedImgs: Uint8Array[] = []; const convertedImgs: Uint8Array[] = [];
await Promise.all( await Promise.all(
imgs.map(async (img, i) => { imgs.map(async (img, i) => {
const output = await magickConvert(img, message.to); const output = await magickConvert(img, message.to, compression);
convertedImgs[i] = output; convertedImgs[i] = output;
}), }),
); );
@ -74,7 +75,10 @@ const handleMessage = async (message: any): Promise<any> => {
const zip = makeZip( const zip = makeZip(
convertedImgs.map( convertedImgs.map(
(img, i) => (img, i) =>
new File([img], `image${i}.${message.to.slice(1)}`), new File(
[new Uint8Array(img)],
`image${i}.${message.to.slice(1)}`,
),
), ),
"images.zip", "images.zip",
); );
@ -104,9 +108,13 @@ const handleMessage = async (message: any): Promise<any> => {
}), }),
), ),
message.to, message.to,
compression
); );
files.push( files.push(
new File([blob], `image${i}${message.to}`), new File(
[new Uint8Array(blob)],
`image${i}${message.to}`,
),
); );
}), }),
); );
@ -150,6 +158,7 @@ const handleMessage = async (message: any): Promise<any> => {
const converted = await magickConvert( const converted = await magickConvert(
img, img,
message.to, message.to,
compression
); );
outputs.push(converted); outputs.push(converted);
break; break;
@ -163,7 +172,10 @@ const handleMessage = async (message: any): Promise<any> => {
const zip = makeZip( const zip = makeZip(
outputs.map( outputs.map(
(img, i) => (img, i) =>
new File([img], `image${i}.${message.to.slice(1)}`), new File(
[new Uint8Array(img)],
`image${i}.${message.to.slice(1)}`,
),
), ),
"images.zip", "images.zip",
); );
@ -210,7 +222,7 @@ const handleMessage = async (message: any): Promise<any> => {
}), }),
); );
const converted = await magickConvert(img, message.to); const converted = await magickConvert(img, message.to, compression);
return { return {
type: "finished", type: "finished",
@ -228,7 +240,10 @@ const readToEnd = async (reader: ReadableStreamDefaultReader<Uint8Array>) => {
if (value) chunks.push(value); if (value) chunks.push(value);
done = d; done = d;
} }
const blob = new Blob(chunks, { type: "application/zip" }); const blob = new Blob(
chunks.map((chunk) => new Uint8Array(chunk)),
{ type: "application/zip" },
);
const arrayBuffer = await blob.arrayBuffer(); const arrayBuffer = await blob.arrayBuffer();
return new Uint8Array(arrayBuffer); return new Uint8Array(arrayBuffer);
}; };
@ -272,8 +287,12 @@ const magickToBlob = async (img: IMagickImage): Promise<Blob> => {
return; return;
} }
if (!data) {
reject(new Error("Pixel data is null"));
return;
}
const imageData = new ImageData( const imageData = new ImageData(
new Uint8ClampedArray(data?.buffer || new ArrayBuffer(0)), new Uint8ClampedArray(data),
img.width, img.width,
img.height, img.height,
); );
@ -288,7 +307,11 @@ const magickToBlob = async (img: IMagickImage): Promise<Blob> => {
); );
}; };
const magickConvert = async (img: IMagickImage, to: string) => { const magickConvert = async (
img: IMagickImage,
to: string,
compression?: number,
) => {
const intermediary = await magickToBlob(img); const intermediary = await magickToBlob(img);
const buf = new Uint8Array(await intermediary.arrayBuffer()); const buf = new Uint8Array(await intermediary.arrayBuffer());
let fmt = to.slice(1).toUpperCase(); let fmt = to.slice(1).toUpperCase();
@ -296,6 +319,8 @@ const magickConvert = async (img: IMagickImage, to: string) => {
const result = await new Promise<Uint8Array>((resolve) => { const result = await new Promise<Uint8Array>((resolve) => {
ImageMagick.read(buf, MagickFormat.Png, (image) => { ImageMagick.read(buf, MagickFormat.Png, (image) => {
// magick-wasm automatically clamps (https://github.com/dlemstra/magick-wasm/blob/76fc6f2b0c0497d2ddc251bbf6174b4dc92ac3ea/src/magick-image.ts#L2480)
if (compression) image.quality = compression;
image.write(fmt as unknown as MagickFormat, (o) => { image.write(fmt as unknown as MagickFormat, (o) => {
resolve(structuredClone(o)); resolve(structuredClone(o));
}); });