mirror of https://github.com/VERT-sh/VERT.git
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
This commit is contained in:
parent
33cd998ab8
commit
7fcbdcd73a
|
|
@ -85,6 +85,7 @@
|
|||
"settings": "Settings",
|
||||
"title": "File conversion settings",
|
||||
"description": "Change the conversion settings for <b>{filename}</b>, which is using <b>{converter}</b>. These settings may not be available for all formats.",
|
||||
"none": "No settings available for this format.",
|
||||
"image": {
|
||||
"quality": "Quality",
|
||||
"depth": "Color depth",
|
||||
|
|
|
|||
|
|
@ -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 @@
|
|||
</div>
|
||||
<div class="flex flex-col gap-1 w-full">
|
||||
{#if typeof message === "string"}
|
||||
<p class="text-sm font-normal text-muted whitespace-pre-wrap">{message}</p>
|
||||
<p class="text-sm font-normal text-muted whitespace-pre-wrap">
|
||||
{message}
|
||||
</p>
|
||||
{:else}
|
||||
{@const MessageComponent = message}
|
||||
<div class="text-sm font-normal text-muted">
|
||||
|
|
|
|||
|
|
@ -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<Record<string, any>>({});
|
||||
let settings = $state<ConversionSettings>({});
|
||||
|
||||
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 @@
|
|||
<p class="text-base">
|
||||
{@html sanitize(
|
||||
m["convert.settings.description"]({
|
||||
converter: file.findConverter()?.name,
|
||||
converter:
|
||||
file.findConverter()?.name || "unknown",
|
||||
filename: file.name,
|
||||
}),
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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<unknown>;
|
||||
} = $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",
|
||||
|
|
|
|||
|
|
@ -93,7 +93,7 @@ export class Converter {
|
|||
public async convert(
|
||||
input: VertFile,
|
||||
to: string,
|
||||
settings?: ConversionSettings,
|
||||
settings: ConversionSettings,
|
||||
...args: any[]
|
||||
): Promise<VertFile> {
|
||||
throw new Error("Not implemented");
|
||||
|
|
|
|||
|
|
@ -118,12 +118,13 @@ export class MagickConverter extends Converter {
|
|||
|
||||
public async getAvailableSettings(): Promise<SettingDefinition[]> {
|
||||
// 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<VertFile> {
|
||||
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);
|
||||
|
||||
|
|
|
|||
|
|
@ -422,7 +422,7 @@ export class VertdConverter extends Converter {
|
|||
return defaults;
|
||||
}
|
||||
|
||||
public async convert(input: VertFile, to: string): Promise<VertFile> {
|
||||
public async convert(input: VertFile, to: string, settings: ConversionSettings): Promise<VertFile> {
|
||||
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`);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -9,8 +9,7 @@ interface ConvertMessage {
|
|||
to: string;
|
||||
} | VertFile;
|
||||
to: string;
|
||||
compression: number | null;
|
||||
keepMetadata?: boolean;
|
||||
conversionSettings: string; // JSON stringified ConversionSettings
|
||||
}
|
||||
|
||||
interface FinishedMessage {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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<Uint8Array>) => {
|
|||
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<Uint8Array>((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));
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
];
|
||||
|
|
|
|||
Loading…
Reference in New Issue