From 7fcbdcd73ac88fe2330bb806e0f3ef91c219f119 Mon Sep 17 00:00:00 2001 From: Maya Date: Sun, 15 Feb 2026 21:06:08 +0300 Subject: [PATCH] feat: imagemagick settings logic this took a bit lol, restructured the conversion workers a little bit - i prob broke vertd and ffmpeg audio for now --- messages/en.json | 1 + src/lib/components/functional/Dialog.svelte | 11 ++- .../functional/SettingsModal.svelte | 30 +++++-- src/lib/components/visual/Toast.svelte | 14 +++- src/lib/converters/converter.svelte.ts | 2 +- src/lib/converters/magick.svelte.ts | 30 +++---- src/lib/converters/vertd.svelte.ts | 4 +- src/lib/types/conversion-settings.ts | 2 +- src/lib/types/conversion-worker.ts | 3 +- src/lib/types/file.svelte.ts | 2 + src/lib/workers/magick.ts | 79 +++++++++++++++---- static/sw.js | 4 +- 12 files changed, 126 insertions(+), 56 deletions(-) diff --git a/messages/en.json b/messages/en.json index 279503d..16cb900 100644 --- a/messages/en.json +++ b/messages/en.json @@ -85,6 +85,7 @@ "settings": "Settings", "title": "File conversion settings", "description": "Change the conversion settings for {filename}, which is using {converter}. These settings may not be available for all formats.", + "none": "No settings available for this format.", "image": { "quality": "Quality", "depth": "Color depth", diff --git a/src/lib/components/functional/Dialog.svelte b/src/lib/components/functional/Dialog.svelte index 277cfc5..df83283 100644 --- a/src/lib/components/functional/Dialog.svelte +++ b/src/lib/components/functional/Dialog.svelte @@ -7,8 +7,11 @@ type Props = DialogType; - let { id, title, message, buttons, type, ...rest }: Props = $props(); - let additional = $derived("additional" in rest ? rest.additional : undefined); + let props: Props = $props(); + // svelte-ignore state_referenced_locally + const { id, title, message, buttons, type } = props; + // svelte-ignore state_referenced_locally + const additional = "additional" in props ? props.additional : undefined; const colors = { success: "purple", @@ -53,7 +56,9 @@
{#if typeof message === "string"} -

{message}

+

+ {message} +

{:else} {@const MessageComponent = message}
diff --git a/src/lib/components/functional/SettingsModal.svelte b/src/lib/components/functional/SettingsModal.svelte index 80e97dd..5e6d02f 100644 --- a/src/lib/components/functional/SettingsModal.svelte +++ b/src/lib/components/functional/SettingsModal.svelte @@ -6,6 +6,8 @@ import { m } from "$lib/paraglide/messages"; import type { VertFile } from "$lib/types"; import { sanitize } from "$lib/store/index.svelte"; + import { log } from "$lib/util/logger"; + import { type ConversionSettings } from "$lib/types/conversion-settings"; type Props = { file: VertFile | null; @@ -14,21 +16,32 @@ let { file, onclose }: Props = $props(); - let settings = $state>({}); + let settings = $state({}); const handleSettingChange = (key: string, value: any) => { if (!file) return; settings[key] = value; - console.log( - `Changed settings for ${file.name}: ${JSON.stringify(settings, null, 2)}`, - ); }; - const applySettings = () => { + const applySettings = async () => { onclose?.(); if (!file) return; - file.conversionSettings = { ...file.conversionSettings, ...settings }; - console.log( + const converter = file.findConverter(); + if (!converter) { + log( + ["settings", "modal"], + `No converter found for ${file.name}, cannot apply settings`, + ); + return; + } + // apply defaults, then existing settings, then new settings on top + file.conversionSettings = { + ...(await converter.getDefaultSettings()), + ...file.conversionSettings, + ...settings, + }; + log( + ["settings", "modal"], `Applied settings for ${file.name}: ${JSON.stringify(file.conversionSettings, null, 2)}`, ); }; @@ -58,7 +71,8 @@

{@html sanitize( m["convert.settings.description"]({ - converter: file.findConverter()?.name, + converter: + file.findConverter()?.name || "unknown", filename: file.name, }), )} diff --git a/src/lib/components/visual/Toast.svelte b/src/lib/components/visual/Toast.svelte index b934e55..bd36250 100644 --- a/src/lib/components/visual/Toast.svelte +++ b/src/lib/components/visual/Toast.svelte @@ -12,12 +12,18 @@ import type { ToastProps } from "$lib/util/toast.svelte"; import type { SvelteComponent } from "svelte"; import clsx from "clsx"; + import type { Toast as ToastType } from "$lib/util/toast.svelte"; - const { id, type, message, durations, ...rest }: ToastProps = $props(); + const props: { + toast: ToastType; + } = $props(); - const additional = $derived( - "additional" in rest ? rest.additional : undefined, - ); + // svelte-ignore state_referenced_locally + const { id, type, message, durations } = props.toast; + + // svelte-ignore state_referenced_locally + const additional = + "additional" in props.toast ? props.toast.additional : {}; const colors = { success: "purple", diff --git a/src/lib/converters/converter.svelte.ts b/src/lib/converters/converter.svelte.ts index 8c0448d..e59ef26 100644 --- a/src/lib/converters/converter.svelte.ts +++ b/src/lib/converters/converter.svelte.ts @@ -93,7 +93,7 @@ export class Converter { public async convert( input: VertFile, to: string, - settings?: ConversionSettings, + settings: ConversionSettings, ...args: any[] ): Promise { throw new Error("Not implemented"); diff --git a/src/lib/converters/magick.svelte.ts b/src/lib/converters/magick.svelte.ts index 3a51433..ec676c9 100644 --- a/src/lib/converters/magick.svelte.ts +++ b/src/lib/converters/magick.svelte.ts @@ -118,12 +118,13 @@ export class MagickConverter extends Converter { public async getAvailableSettings(): Promise { // images - quality/compression/quantize/interlace/depth-DPI, resize, crop, rotate, flip/flop, autoOrient?, color space/bit depth, transparency settings - + const global = Settings.instance.settings; + const quality: SettingDefinition = { key: "quality", label: m["convert.settings.image.quality"](), type: "number", - default: 100, + default: global.magickQuality ?? 100, min: 0, max: 100, }; @@ -152,6 +153,7 @@ export class MagickConverter extends Converter { // what are these even lmao { value: "auto", label: "Auto" }, { value: "srgb", label: "sRGB" }, + { value: "cmyk", label: "CMYK" }, { value: "adobe98", label: "Adobe RGB" }, { value: "prophoto", label: "ProPhoto RGB" }, { value: "displayp3", label: "Display P3" }, @@ -174,7 +176,7 @@ export class MagickConverter extends Converter { key: "metadata", label: m["convert.settings.common.metadata"](), type: "boolean", - default: true, + default: global.metadata ?? true, }; // resize, crop, rotate - prob want a ui @@ -194,17 +196,10 @@ export class MagickConverter extends Converter { public async convert( input: VertFile, to: string, + settings: ConversionSettings, // eslint-disable-next-line @typescript-eslint/no-explicit-any ...args: any[] ): Promise { - let compression: number | undefined = args.at(0); - if (!compression) { - compression = Settings.instance.settings.magickQuality ?? 100; - log( - ["converters", this.name], - `using user setting for quality: ${compression}%`, - ); - } log(["converters", this.name], `converting ${input.name} to ${to}`); // handle converting from SVG manually because magick-wasm doesn't support it @@ -216,7 +211,7 @@ export class MagickConverter extends Converter { input.to, ); if (to === ".png") return pngFile; // if target is png, return it directly - return await this.convert(pngFile, to, ...args); // otherwise, recursively convert png to user's target format + return await this.convert(pngFile, to, settings, ...args); // otherwise, recursively convert png to user's target format } catch (err) { error( ["converters", this.name], @@ -270,9 +265,11 @@ export class MagickConverter extends Converter { ]); // every other format handled by magick worker - const keepMetadata: boolean = - Settings.instance.settings.metadata ?? true; - log(["converters", this.name], `keep metadata: ${keepMetadata}`); + const conversionSettings = JSON.stringify( + Object.keys(settings).length > 0 + ? settings // user-provided settings + : await this.getDefaultSettings(), // use defaults if not provided + ); const convertMsg: WorkerMessage = { type: "convert", id: input.id, @@ -283,8 +280,7 @@ export class MagickConverter extends Converter { to: input.to, }, to, - compression, - keepMetadata, + conversionSettings, }; worker.postMessage(convertMsg); diff --git a/src/lib/converters/vertd.svelte.ts b/src/lib/converters/vertd.svelte.ts index 66969b2..93279d3 100644 --- a/src/lib/converters/vertd.svelte.ts +++ b/src/lib/converters/vertd.svelte.ts @@ -422,7 +422,7 @@ export class VertdConverter extends Converter { return defaults; } - public async convert(input: VertFile, to: string): Promise { + public async convert(input: VertFile, to: string, settings: ConversionSettings): Promise { if (to.startsWith(".")) to = to.slice(1); let fileUpload = input; @@ -440,7 +440,7 @@ export class VertdConverter extends Converter { fileUpload = await magickConverter.convert( input, ".gif", - input.conversionSettings, + settings, 100, ); this.log(`successfully converted webp to gif`); diff --git a/src/lib/types/conversion-settings.ts b/src/lib/types/conversion-settings.ts index 0a8fc7a..bb6e640 100644 --- a/src/lib/types/conversion-settings.ts +++ b/src/lib/types/conversion-settings.ts @@ -10,7 +10,7 @@ export interface SettingDefinition { min?: number; max?: number; step?: number; - options?: Array<{ value: string; label: string }>; // for select types + options?: Array<{ value: any; label: string }>; // for select types description?: string; } diff --git a/src/lib/types/conversion-worker.ts b/src/lib/types/conversion-worker.ts index 3576960..504c068 100644 --- a/src/lib/types/conversion-worker.ts +++ b/src/lib/types/conversion-worker.ts @@ -9,8 +9,7 @@ interface ConvertMessage { to: string; } | VertFile; to: string; - compression: number | null; - keepMetadata?: boolean; + conversionSettings: string; // JSON stringified ConversionSettings } interface FinishedMessage { diff --git a/src/lib/types/file.svelte.ts b/src/lib/types/file.svelte.ts index 5b25bd8..82d5b81 100644 --- a/src/lib/types/file.svelte.ts +++ b/src/lib/types/file.svelte.ts @@ -188,6 +188,7 @@ export class VertFile { const converted = await converter.convert( tempVFile, this.to, + this.conversionSettings, ); let outputExt = this.to; @@ -209,6 +210,7 @@ export class VertFile { const converted = await converter.convert( tempVFile, this.to, + this.conversionSettings, ); let outputExt = this.to; diff --git a/src/lib/workers/magick.ts b/src/lib/workers/magick.ts index aa3ab5d..4eb547b 100644 --- a/src/lib/workers/magick.ts +++ b/src/lib/workers/magick.ts @@ -1,15 +1,20 @@ import { + ColorSpace, initializeImageMagick, + MagickColor, MagickFormat, MagickImage, MagickImageCollection, MagickReadSettings, + AlphaAction, type IMagickImage, } from "@imagemagick/magick-wasm"; import { makeZip } from "client-zip"; import { parseAni } from "$lib/util/parse/ani"; import { parseIcns } from "vert-wasm"; import type { WorkerMessage } from "$lib/types"; +import type { ConversionSettings } from "$lib/types/conversion-settings"; +import { log } from "$lib/util/logger"; let magickInitialized = false; @@ -44,9 +49,6 @@ const handleMessage = async ( return { type: "error", error: "magick-wasm not initialized" }; } - const compression: number | undefined = - message.compression ?? undefined; - const keepMetadata: boolean = message.keepMetadata ?? true; if (!message.to.startsWith(".")) message.to = `.${message.to}`; message.to = message.to.toLowerCase(); if (message.to === ".jfif") message.to = ".jpeg"; @@ -55,6 +57,10 @@ const handleMessage = async ( if (from === ".jfif") from = ".jpeg"; if (from === ".fit") from = ".fits"; + console.log(JSON.stringify(message, null, 2)); + const conversionSettings = JSON.parse( + message.conversionSettings || "{}", + ) as ConversionSettings; const buffer = await message.input.file.arrayBuffer(); // special ico handling to split them all into separate images @@ -90,8 +96,7 @@ const handleMessage = async ( const output = await magickConvert( img, message.to, - keepMetadata, - compression, + conversionSettings, ); convertedImgs[i] = output; }), @@ -133,8 +138,7 @@ const handleMessage = async ( }), ), message.to, - keepMetadata, - compression, + conversionSettings, ); files.push( new File( @@ -184,8 +188,7 @@ const handleMessage = async ( const converted = await magickConvert( img, message.to, - keepMetadata, - compression, + conversionSettings, ); outputs.push(converted); break; @@ -251,8 +254,7 @@ const handleMessage = async ( const converted = await magickConvert( img, message.to, - keepMetadata, - compression, + conversionSettings, ); return { @@ -287,8 +289,7 @@ const readToEnd = async (reader: ReadableStreamDefaultReader) => { const magickConvert = async ( img: IMagickImage, to: string, - keepMetadata: boolean, - compression?: number, + conversionSettings: ConversionSettings, ) => { let fmt = to.slice(1).toUpperCase(); if (fmt === "JFIF") fmt = "JPEG"; @@ -310,10 +311,56 @@ const magickConvert = async ( const result = await new Promise((resolve, reject) => { try { - // magick-wasm automatically clamps (https://github.com/dlemstra/magick-wasm/blob/76fc6f2b0c0497d2ddc251bbf6174b4dc92ac3ea/src/magick-image.ts#L2480) - if (compression) img.quality = compression; - if (!keepMetadata) img.strip(); + // quality, depth, colorSpace, transparency, metadata + const quality = conversionSettings.quality as number; + const bitDepth = conversionSettings.depth as number; + const colorSpace = conversionSettings.colorSpace as string; + const transparency = conversionSettings.transparency as boolean; + const metadata = conversionSettings.metadata as boolean; + if (quality) img.quality = quality; + if (bitDepth) img.depth = bitDepth; + if (!metadata) img.strip(); + if (colorSpace) { + switch (colorSpace) { + case "srgb": + img.colorSpace = ColorSpace.sRGB; + break; + case "cmyk": + img.colorSpace = ColorSpace.CMYK; + break; + case "adobe98": + img.colorSpace = ColorSpace.Adobe98; + break; + case "prophoto": + img.colorSpace = ColorSpace.ProPhoto; + break; + case "displayp3": + img.colorSpace = ColorSpace.DisplayP3; + break; + case "xyz": + img.colorSpace = ColorSpace.XYZ; + break; + case "lab": + img.colorSpace = ColorSpace.Lab; + break; + case "gray": + img.colorSpace = ColorSpace.Gray; + break; + // auto is default so do nothing + } + } + if (!transparency) { + img.backgroundColor = new MagickColor(0, 0, 0, 255); // TODO: probably make it an option to set the bg colour + img.alpha(AlphaAction.Remove); + } + + log( + ["workers", "imagemagick"], + `Converting to ${fmt} with settings: ${JSON.stringify(conversionSettings)}`, + ); + + // magick-wasm automatically clamps (https://github.com/dlemstra/magick-wasm/blob/76fc6f2b0c0497d2ddc251bbf6174b4dc92ac3ea/src/magick-image.ts#L2480) img.write(fmt as unknown as MagickFormat, (o: Uint8Array) => { resolve(structuredClone(o)); }); diff --git a/static/sw.js b/static/sw.js index 47573e2..75982f7 100644 --- a/static/sw.js +++ b/static/sw.js @@ -1,7 +1,7 @@ -const CACHE_NAME = "vert-wasm-cache-v2"; // updated when workers update +const CACHE_NAME = "vert-wasm-cache-v3"; // updated when workers update const WASM_FILES = [ - "/pandoc.wasm", + "/pandoc.wasm", // from https://github.com/haskell-wasm/pandoc-wasm "https://cdn.jsdelivr.net/npm/@ffmpeg/core@0.12.10/dist/esm/ffmpeg-core.js", "https://cdn.jsdelivr.net/npm/@ffmpeg/core@0.12.10/dist/esm/ffmpeg-core.wasm", ];