mirror of https://github.com/VERT-sh/VERT.git
feat: better worker cancellation & types
conversions should now *actually* stop and terminate when removed, instead of continuing to run in the background until finished. most notably, magick has been reworked to run a new worker for each conversion to follow ffmpeg and pandoc (& to allow individual cancellations) also fix uh, a lot of stuff relating to messages not following WorkerMessage type & types in general. i'm braindead right now but everything still works somehow, vertd is next. this took forever.
This commit is contained in:
parent
9467e58d3b
commit
55edaad4b4
|
@ -209,6 +209,7 @@
|
|||
"workers": {
|
||||
"errors": {
|
||||
"general": "Error converting {file}: {message}",
|
||||
"cancel": "Error canceling conversion for {file}: {message}",
|
||||
"magick": "Error in Magick worker, image conversion may not work as expected.",
|
||||
"ffmpeg": "Error loading ffmpeg, some features may not work.",
|
||||
"no_audio": "No audio stream found.",
|
||||
|
|
|
@ -73,6 +73,15 @@ export class Converter {
|
|||
throw new Error("Not implemented");
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel the active conversion of a file.
|
||||
* @param input The input file.
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
public async cancel(input: VertFile): Promise<void> {
|
||||
throw new Error("Not implemented");
|
||||
}
|
||||
|
||||
public async valid(): Promise<boolean> {
|
||||
return true;
|
||||
}
|
||||
|
|
|
@ -38,6 +38,8 @@ export class FFmpegConverter extends Converter {
|
|||
public name = "ffmpeg";
|
||||
public ready = $state(false);
|
||||
|
||||
private activeConversions = new Map<string, FFmpeg>();
|
||||
|
||||
public supportedFormats = [
|
||||
new FormatInfo("mp3", true, true),
|
||||
new FormatInfo("wav", true, true),
|
||||
|
@ -109,6 +111,8 @@ export class FFmpegConverter extends Converter {
|
|||
let conversionError: string | null = null;
|
||||
const ffmpeg = await this.setupFFmpeg(input);
|
||||
|
||||
this.activeConversions.set(input.id, ffmpeg);
|
||||
|
||||
// listen for errors during conversion
|
||||
const errorListener = (l: { message: string }) => {
|
||||
const msg = l.message;
|
||||
|
@ -117,7 +121,9 @@ export class FFmpegConverter extends Converter {
|
|||
msg.includes("is not supported")
|
||||
) {
|
||||
const rate = Settings.instance.settings.ffmpegCustomSampleRate;
|
||||
conversionError = m["workers.errors.invalid_rate"]({ rate });
|
||||
conversionError = m["workers.errors.invalid_rate"]({
|
||||
rate,
|
||||
});
|
||||
} else if (msg.includes("Stream map '0:a:0' matches no streams.")) {
|
||||
conversionError = m["workers.errors.no_audio"]();
|
||||
} else if (
|
||||
|
@ -181,6 +187,25 @@ export class FFmpegConverter extends Converter {
|
|||
return new VertFile(new File([outBuf], outputFileName), to);
|
||||
}
|
||||
|
||||
public async cancel(input: VertFile): Promise<void> {
|
||||
const ffmpeg = this.activeConversions.get(input.id);
|
||||
if (!ffmpeg) {
|
||||
log(
|
||||
["converters", this.name],
|
||||
`No active conversion found for file ${input.name}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
log(
|
||||
["converters", this.name],
|
||||
`Cancelling conversion for file ${input.name}`,
|
||||
);
|
||||
|
||||
ffmpeg.terminate();
|
||||
this.activeConversions.delete(input.id);
|
||||
}
|
||||
|
||||
private async setupFFmpeg(input: VertFile): Promise<FFmpeg> {
|
||||
const ffmpeg = new FFmpeg();
|
||||
|
||||
|
|
|
@ -2,22 +2,19 @@ import { browser } from "$app/environment";
|
|||
import { error, log } from "$lib/logger";
|
||||
import { m } from "$lib/paraglide/messages";
|
||||
import { addToast } from "$lib/store/ToastProvider";
|
||||
import type { OmitBetterStrict, WorkerMessage } from "$lib/types";
|
||||
import { VertFile } from "$lib/types";
|
||||
import { VertFile, type WorkerMessage } from "$lib/types";
|
||||
import MagickWorker from "$lib/workers/magick?worker&url";
|
||||
import { Converter, FormatInfo } from "./converter.svelte";
|
||||
import { imageFormats } from "./magick-automated";
|
||||
import { Settings } from "$lib/sections/settings/index.svelte";
|
||||
import magickWasm from "@imagemagick/magick-wasm/magick.wasm?url";
|
||||
|
||||
export class MagickConverter extends Converter {
|
||||
private worker: Worker = browser
|
||||
? new Worker(MagickWorker, {
|
||||
type: "module",
|
||||
})
|
||||
: null!;
|
||||
private id = 0;
|
||||
public name = "imagemagick";
|
||||
public ready = $state(false);
|
||||
public wasm: ArrayBuffer = null!;
|
||||
|
||||
private activeConversions = new Map<string, Worker>();
|
||||
|
||||
public supportedFormats = [
|
||||
// manually tested formats
|
||||
|
@ -86,23 +83,29 @@ export class MagickConverter extends Converter {
|
|||
super();
|
||||
log(["converters", this.name], `created converter`);
|
||||
if (!browser) return;
|
||||
this.initializeWasm();
|
||||
}
|
||||
|
||||
this.status = "downloading";
|
||||
|
||||
log(["converters", this.name], `loading worker @ ${MagickWorker}`);
|
||||
this.worker.onmessage = (e) => {
|
||||
const message: WorkerMessage = e.data;
|
||||
log(["converters", this.name], `received message ${message.type}`);
|
||||
if (message.type === "loaded") {
|
||||
this.status = "ready";
|
||||
} else if (message.type === "error") {
|
||||
error(
|
||||
["converters", this.name],
|
||||
`error in worker: ${message.error}`,
|
||||
private async initializeWasm() {
|
||||
try {
|
||||
this.status = "downloading";
|
||||
const response = await fetch(magickWasm);
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
`Failed to fetch WASM: ${response.status} ${response.statusText}`,
|
||||
);
|
||||
addToast("error", m["workers.errors.magick"]());
|
||||
}
|
||||
};
|
||||
|
||||
this.wasm = await response.arrayBuffer();
|
||||
this.status = "ready";
|
||||
} catch (err) {
|
||||
this.status = "error";
|
||||
error(
|
||||
["converters", this.name],
|
||||
`Failed to load ImageMagick WASM: ${err}`,
|
||||
);
|
||||
addToast("error", m["workers.errors.magick"]());
|
||||
}
|
||||
}
|
||||
|
||||
public async convert(
|
||||
|
@ -140,67 +143,137 @@ 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 msg = {
|
||||
type: "convert",
|
||||
input: {
|
||||
file: input.file,
|
||||
name: input.name,
|
||||
to: input.to,
|
||||
from: input.from,
|
||||
},
|
||||
to,
|
||||
compression,
|
||||
keepMetadata,
|
||||
} as WorkerMessage;
|
||||
const res = await this.sendMessage(msg);
|
||||
const worker = new Worker(MagickWorker, {
|
||||
type: "module",
|
||||
});
|
||||
this.activeConversions.set(input.id, worker);
|
||||
|
||||
if (res.type === "finished") {
|
||||
log(["converters", this.name], `converted ${input.name} to ${to}`);
|
||||
return new VertFile(
|
||||
new File([res.output as unknown as BlobPart], input.name),
|
||||
res.zip ? ".zip" : to,
|
||||
);
|
||||
try {
|
||||
await Promise.race([
|
||||
this.waitForMessage(worker, "ready"),
|
||||
new Promise((_, reject) =>
|
||||
setTimeout(
|
||||
() =>
|
||||
reject(
|
||||
new Error(
|
||||
"Worker ready timeout after 5 seconds",
|
||||
),
|
||||
),
|
||||
5000,
|
||||
),
|
||||
),
|
||||
]);
|
||||
|
||||
const loadMsg: WorkerMessage = {
|
||||
type: "load",
|
||||
wasm: this.wasm,
|
||||
id: input.id,
|
||||
};
|
||||
worker.postMessage(loadMsg);
|
||||
|
||||
await Promise.race([
|
||||
this.waitForMessage(worker, "loaded"),
|
||||
new Promise((_, reject) =>
|
||||
setTimeout(
|
||||
() =>
|
||||
reject(
|
||||
new Error(
|
||||
"Worker initialization timeout after 30 seconds",
|
||||
),
|
||||
),
|
||||
30000,
|
||||
),
|
||||
),
|
||||
]);
|
||||
|
||||
// every other format handled by magick worker
|
||||
const keepMetadata: boolean =
|
||||
Settings.instance.settings.metadata ?? true;
|
||||
log(["converters", this.name], `keep metadata: ${keepMetadata}`);
|
||||
const convertMsg: WorkerMessage = {
|
||||
type: "convert",
|
||||
id: input.id,
|
||||
input: {
|
||||
file: input.file,
|
||||
name: input.name,
|
||||
from: input.from,
|
||||
to: input.to,
|
||||
},
|
||||
to,
|
||||
compression,
|
||||
keepMetadata,
|
||||
};
|
||||
worker.postMessage(convertMsg);
|
||||
|
||||
const res = await this.waitForMessage(worker);
|
||||
if (res.type === "finished") {
|
||||
log(
|
||||
["converters", this.name],
|
||||
`converted ${input.name} to ${to}`,
|
||||
);
|
||||
return new VertFile(
|
||||
new File([res.output as unknown as BlobPart], input.name),
|
||||
res.zip ? ".zip" : to,
|
||||
);
|
||||
}
|
||||
|
||||
if (res.type === "error") {
|
||||
throw new Error(res.error);
|
||||
}
|
||||
|
||||
throw new Error("Unknown message type");
|
||||
} finally {
|
||||
this.activeConversions.delete(input.id);
|
||||
worker.terminate();
|
||||
}
|
||||
|
||||
if (res.type === "error") {
|
||||
throw new Error(res.error);
|
||||
}
|
||||
|
||||
throw new Error("Unknown message type");
|
||||
}
|
||||
|
||||
private sendMessage(
|
||||
message: OmitBetterStrict<WorkerMessage, "id">,
|
||||
): Promise<OmitBetterStrict<WorkerMessage, "id">> {
|
||||
const id = this.id++;
|
||||
let resolved = false;
|
||||
return new Promise((resolve) => {
|
||||
public async cancel(input: VertFile): Promise<void> {
|
||||
const worker = this.activeConversions.get(input.id);
|
||||
if (!worker) {
|
||||
log(
|
||||
["converters", this.name],
|
||||
`No active conversion found for file ${input.name}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
log(
|
||||
["converters", this.name],
|
||||
`Cancelling conversion for file ${input.name}`,
|
||||
);
|
||||
|
||||
worker.terminate();
|
||||
this.activeConversions.delete(input.id);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
private waitForMessage(worker: Worker, type?: string): Promise<any> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const onMessage = (e: MessageEvent) => {
|
||||
if (e.data.id === id) {
|
||||
this.worker.removeEventListener("message", onMessage);
|
||||
if (type && e.data.type === type) {
|
||||
worker.removeEventListener("message", onMessage);
|
||||
worker.removeEventListener("error", onError);
|
||||
resolve(e.data);
|
||||
resolved = true;
|
||||
} else if (!type) {
|
||||
worker.removeEventListener("message", onMessage);
|
||||
worker.removeEventListener("error", onError);
|
||||
resolve(e.data);
|
||||
} else if (e.data.type === "error") {
|
||||
worker.removeEventListener("message", onMessage);
|
||||
worker.removeEventListener("error", onError);
|
||||
reject(new Error(e.data.error));
|
||||
}
|
||||
};
|
||||
|
||||
setTimeout(() => {
|
||||
if (!resolved) {
|
||||
this.worker.removeEventListener("message", onMessage);
|
||||
throw new Error("Timeout");
|
||||
}
|
||||
}, 60000);
|
||||
const onError = (e: ErrorEvent) => {
|
||||
worker.removeEventListener("message", onMessage);
|
||||
worker.removeEventListener("error", onError);
|
||||
reject(new Error(`Worker error: ${e.message}`));
|
||||
};
|
||||
|
||||
this.worker.addEventListener("message", onMessage);
|
||||
const msg = { ...message, id, worker: null };
|
||||
try {
|
||||
this.worker.postMessage(msg);
|
||||
} catch (e) {
|
||||
error(["converters", this.name], e);
|
||||
}
|
||||
worker.addEventListener("message", onMessage);
|
||||
worker.addEventListener("error", onError);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -1,14 +1,17 @@
|
|||
import { VertFile } from "$lib/types";
|
||||
import { VertFile, type WorkerMessage } from "$lib/types";
|
||||
import { Converter, FormatInfo } from "./converter.svelte";
|
||||
import { browser } from "$app/environment";
|
||||
import PandocWorker from "$lib/workers/pandoc?worker&url";
|
||||
import { addToast } from "$lib/store/ToastProvider";
|
||||
import { log } from "$lib/logger";
|
||||
|
||||
export class PandocConverter extends Converter {
|
||||
public name = "pandoc";
|
||||
public ready = $state(false);
|
||||
public wasm: ArrayBuffer = null!;
|
||||
|
||||
private activeConversions = new Map<string, Worker>();
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
if (!browser) return;
|
||||
|
@ -27,17 +30,33 @@ export class PandocConverter extends Converter {
|
|||
})();
|
||||
}
|
||||
|
||||
public async convert(input: VertFile, to: string): Promise<VertFile> {
|
||||
public async convert(file: VertFile, to: string): Promise<VertFile> {
|
||||
const worker = new Worker(PandocWorker, {
|
||||
type: "module",
|
||||
});
|
||||
worker.postMessage({ type: "load", wasm: this.wasm });
|
||||
|
||||
this.activeConversions.set(file.id, worker);
|
||||
|
||||
const loadMsg: WorkerMessage = {
|
||||
type: "load",
|
||||
wasm: this.wasm,
|
||||
id: file.id,
|
||||
};
|
||||
worker.postMessage(loadMsg);
|
||||
await waitForMessage(worker, "loaded");
|
||||
worker.postMessage({
|
||||
const convertMsg: WorkerMessage = {
|
||||
type: "convert",
|
||||
to,
|
||||
file: input.file,
|
||||
});
|
||||
input: {
|
||||
file: file.file,
|
||||
name: file.name,
|
||||
from: file.from,
|
||||
to,
|
||||
},
|
||||
compression: null,
|
||||
id: file.id,
|
||||
};
|
||||
worker.postMessage(convertMsg);
|
||||
const result = await waitForMessage(worker);
|
||||
if (result.type === "error") {
|
||||
worker.terminate();
|
||||
|
@ -46,7 +65,7 @@ export class PandocConverter extends Converter {
|
|||
switch (result.errorKind) {
|
||||
case "PandocUnknownReaderError": {
|
||||
throw new Error(
|
||||
`${input.from} is not a supported input format for documents.`,
|
||||
`${file.from} is not a supported input format for documents.`,
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -73,14 +92,35 @@ export class PandocConverter extends Converter {
|
|||
else throw new Error(result.error);
|
||||
}
|
||||
}
|
||||
worker.terminate();
|
||||
|
||||
if (!to.startsWith(".")) to = `.${to}`;
|
||||
this.activeConversions.delete(file.id);
|
||||
worker.terminate();
|
||||
return new VertFile(
|
||||
new File([result.output], input.name),
|
||||
new File([result.output], file.name),
|
||||
result.isZip ? ".zip" : to,
|
||||
);
|
||||
}
|
||||
|
||||
public async cancel(input: VertFile): Promise<void> {
|
||||
const worker = this.activeConversions.get(input.id);
|
||||
if (!worker) {
|
||||
log(
|
||||
["converters", this.name],
|
||||
`No active conversion found for file ${input.name}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
log(
|
||||
["converters", this.name],
|
||||
`Cancelling conversion for file ${input.name}`,
|
||||
);
|
||||
|
||||
worker.terminate();
|
||||
this.activeConversions.delete(input.id);
|
||||
}
|
||||
|
||||
public supportedFormats = [
|
||||
new FormatInfo("docx", true, true),
|
||||
new FormatInfo("doc", true, true),
|
||||
|
|
|
@ -2,7 +2,12 @@ import { VertFile } from "./file.svelte";
|
|||
|
||||
interface ConvertMessage {
|
||||
type: "convert";
|
||||
input: VertFile;
|
||||
input: {
|
||||
file: File;
|
||||
name: string;
|
||||
from: string;
|
||||
to: string;
|
||||
} | VertFile;
|
||||
to: string;
|
||||
compression: number | null;
|
||||
keepMetadata?: boolean;
|
||||
|
@ -10,14 +15,23 @@ interface ConvertMessage {
|
|||
|
||||
interface FinishedMessage {
|
||||
type: "finished";
|
||||
output: ArrayBufferLike;
|
||||
output: ArrayBufferLike | Uint8Array;
|
||||
zip?: boolean;
|
||||
}
|
||||
|
||||
interface LoadMessage {
|
||||
type: "load";
|
||||
wasm: ArrayBuffer;
|
||||
}
|
||||
|
||||
interface LoadedMessage {
|
||||
type: "loaded";
|
||||
}
|
||||
|
||||
interface ReadyMessage {
|
||||
type: "ready";
|
||||
}
|
||||
|
||||
interface ErrorMessage {
|
||||
type: "error";
|
||||
error: string;
|
||||
|
@ -26,8 +40,10 @@ interface ErrorMessage {
|
|||
export type WorkerMessage = (
|
||||
| ConvertMessage
|
||||
| FinishedMessage
|
||||
| LoadMessage
|
||||
| LoadedMessage
|
||||
| ReadyMessage
|
||||
| ErrorMessage
|
||||
) & {
|
||||
id: number;
|
||||
id: string; // unused? rn just using file id, probably meant to be incrementing w/ every message posted?
|
||||
};
|
||||
|
|
|
@ -25,6 +25,8 @@ export class VertFile {
|
|||
|
||||
public processing = $state(false);
|
||||
|
||||
public cancelled = $state(false);
|
||||
|
||||
public converters: Converter[] = [];
|
||||
|
||||
public findConverters(supportedFormats: string[] = [this.from]) {
|
||||
|
@ -84,26 +86,51 @@ export class VertFile {
|
|||
this.result = null;
|
||||
this.progress = 0;
|
||||
this.processing = true;
|
||||
this.cancelled = false;
|
||||
let res;
|
||||
try {
|
||||
res = await converter.convert(this, this.to, ...args);
|
||||
this.result = res;
|
||||
} catch (err) {
|
||||
const castedErr = err as Error;
|
||||
error(["files"], castedErr.message);
|
||||
addToast(
|
||||
"error",
|
||||
m["workers.errors.general"]({
|
||||
file: this.file.name,
|
||||
message: castedErr.message || castedErr,
|
||||
}),
|
||||
);
|
||||
if (!this.cancelled) {
|
||||
const castedErr = err as Error;
|
||||
error(["files"], castedErr.message);
|
||||
addToast(
|
||||
"error",
|
||||
m["workers.errors.general"]({
|
||||
file: this.file.name,
|
||||
message: castedErr.message || castedErr,
|
||||
}),
|
||||
);
|
||||
}
|
||||
this.result = null;
|
||||
}
|
||||
this.processing = false;
|
||||
return res;
|
||||
}
|
||||
|
||||
public async cancel() {
|
||||
if (!this.processing) return;
|
||||
const converter = this.findConverter();
|
||||
if (!converter) throw new Error("No converter found");
|
||||
this.cancelled = true;
|
||||
try {
|
||||
await converter.cancel(this);
|
||||
this.processing = false;
|
||||
this.result = null;
|
||||
} catch (err) {
|
||||
const castedErr = err as Error;
|
||||
error(["files"], castedErr.message);
|
||||
addToast(
|
||||
"error",
|
||||
m["workers.errors.cancel"]({
|
||||
file: this.file.name,
|
||||
message: castedErr.message || castedErr,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public async download() {
|
||||
if (!this.result) throw new Error("No result found");
|
||||
|
||||
|
|
|
@ -7,38 +7,58 @@ import {
|
|||
type IMagickImage,
|
||||
} from "@imagemagick/magick-wasm";
|
||||
import { makeZip } from "client-zip";
|
||||
import wasm from "@imagemagick/magick-wasm/magick.wasm?url";
|
||||
import { parseAni } from "$lib/parse/ani";
|
||||
import { parseIcns } from "vert-wasm";
|
||||
import type { WorkerMessage } from "$lib/types";
|
||||
|
||||
const magickPromise = initializeImageMagick(new URL(wasm, import.meta.url));
|
||||
let magickInitialized = false;
|
||||
|
||||
magickPromise
|
||||
.then(() => {
|
||||
postMessage({ type: "loaded" });
|
||||
})
|
||||
.catch((error) => {
|
||||
postMessage({ type: "error", error });
|
||||
});
|
||||
self.postMessage({ type: "ready", id: "0" });
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const handleMessage = async (message: any): Promise<any> => {
|
||||
const handleMessage = async (
|
||||
message: WorkerMessage,
|
||||
): Promise<Partial<WorkerMessage>> => {
|
||||
switch (message.type) {
|
||||
case "load": {
|
||||
try {
|
||||
if (!message.wasm || !(message.wasm instanceof ArrayBuffer)) {
|
||||
throw new Error(
|
||||
`Invalid WASM data: ${typeof message.wasm}`,
|
||||
);
|
||||
}
|
||||
|
||||
const wasmBytes = new Uint8Array(message.wasm);
|
||||
|
||||
await initializeImageMagick(wasmBytes);
|
||||
magickInitialized = true;
|
||||
return { type: "loaded" };
|
||||
} catch (error) {
|
||||
return {
|
||||
type: "error",
|
||||
error: `error loading magick-wasm: ${(error as Error).message}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
case "convert": {
|
||||
const compression: number | undefined = message.compression;
|
||||
if (!magickInitialized) {
|
||||
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";
|
||||
if (message.input.from === ".jfif") message.input.from = ".jpeg";
|
||||
if (message.input.from === ".fit") message.input.from = ".fits";
|
||||
|
||||
let from = message.input.from;
|
||||
if (from === ".jfif") from = ".jpeg";
|
||||
if (from === ".fit") from = ".fits";
|
||||
|
||||
const buffer = await message.input.file.arrayBuffer();
|
||||
// only wait when we need to
|
||||
await magickPromise;
|
||||
|
||||
// special ico handling to split them all into separate images
|
||||
if (message.input.from === ".ico") {
|
||||
if (from === ".ico") {
|
||||
const imgs = MagickImageCollection.create();
|
||||
|
||||
while (true) {
|
||||
|
@ -98,7 +118,7 @@ const handleMessage = async (message: any): Promise<any> => {
|
|||
output: zipBytes,
|
||||
zip: true,
|
||||
};
|
||||
} else if (message.input.from === ".ani") {
|
||||
} else if (from === ".ani") {
|
||||
console.log("Parsing ANI file");
|
||||
try {
|
||||
const parsedAni = parseAni(new Uint8Array(buffer));
|
||||
|
@ -136,7 +156,7 @@ const handleMessage = async (message: any): Promise<any> => {
|
|||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
} else if (message.input.from === ".icns") {
|
||||
} else if (from === ".icns") {
|
||||
const icns: Uint8Array[] = parseIcns(new Uint8Array(buffer));
|
||||
if (typeof icns === "string") {
|
||||
return {
|
||||
|
@ -187,6 +207,7 @@ const handleMessage = async (message: any): Promise<any> => {
|
|||
"images.zip",
|
||||
);
|
||||
const zipBytes = await readToEnd(zip.getReader());
|
||||
|
||||
return {
|
||||
type: "finished",
|
||||
output: zipBytes,
|
||||
|
@ -197,8 +218,7 @@ const handleMessage = async (message: any): Promise<any> => {
|
|||
// build frames of animated formats (webp/gif)
|
||||
// APNG does not work on magick-wasm since it needs ffmpeg built-in (not in magick-wasm) - handle in ffmpeg
|
||||
if (
|
||||
(message.input.from === ".webp" ||
|
||||
message.input.from === ".gif") &&
|
||||
(from === ".webp" || from === ".gif") &&
|
||||
(message.to === ".gif" || message.to === ".webp")
|
||||
) {
|
||||
const collection = MagickImageCollection.create(
|
||||
|
@ -214,6 +234,7 @@ const handleMessage = async (message: any): Promise<any> => {
|
|||
});
|
||||
});
|
||||
collection.dispose();
|
||||
|
||||
return {
|
||||
type: "finished",
|
||||
output: result,
|
||||
|
@ -223,9 +244,7 @@ const handleMessage = async (message: any): Promise<any> => {
|
|||
const img = MagickImage.create(
|
||||
new Uint8Array(buffer),
|
||||
new MagickReadSettings({
|
||||
format: message.input.from
|
||||
.slice(1)
|
||||
.toUpperCase() as MagickFormat,
|
||||
format: from.slice(1).toUpperCase() as MagickFormat,
|
||||
}),
|
||||
);
|
||||
|
||||
|
@ -241,6 +260,11 @@ const handleMessage = async (message: any): Promise<any> => {
|
|||
output: converted,
|
||||
};
|
||||
}
|
||||
default:
|
||||
return {
|
||||
type: "error",
|
||||
error: `Unknown message type: ${message.type}`,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -269,14 +293,18 @@ const magickConvert = async (
|
|||
let fmt = to.slice(1).toUpperCase();
|
||||
if (fmt === "JFIF") fmt = "JPEG";
|
||||
|
||||
const result = await new Promise<Uint8Array>((resolve) => {
|
||||
// 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();
|
||||
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();
|
||||
|
||||
img.write(fmt as unknown as MagickFormat, (o: Uint8Array) => {
|
||||
resolve(structuredClone(o));
|
||||
});
|
||||
img.write(fmt as unknown as MagickFormat, (o: Uint8Array) => {
|
||||
resolve(structuredClone(o));
|
||||
});
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import type { WorkerMessage } from "$lib/types";
|
||||
import * as wasiShim from "@bjorn3/browser_wasi_shim";
|
||||
import * as zip from "client-zip";
|
||||
|
||||
|
@ -37,18 +38,19 @@ type Format =
|
|||
| ".markdown";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const handleMessage = async (message: any): Promise<any> => {
|
||||
const handleMessage = async (message: WorkerMessage): Promise<any> => {
|
||||
switch (message.type) {
|
||||
case "load": {
|
||||
wasm = message.wasm;
|
||||
postMessage({ type: "loaded" });
|
||||
postMessage({ type: "loaded", id: "0" });
|
||||
break;
|
||||
}
|
||||
|
||||
case "convert": {
|
||||
try {
|
||||
// eslint-disable-next-line prefer-const
|
||||
let { to, file }: { to: Format; file: File } = message;
|
||||
const { to: ext, input } = message;
|
||||
const file = input.file as File;
|
||||
const to = ext as Format;
|
||||
if (to === ".rtf") {
|
||||
throw new Error(
|
||||
"Converting into RTF is currently not supported.",
|
||||
|
|
|
@ -63,7 +63,7 @@
|
|||
const clipboardData = e.clipboardData;
|
||||
if (!clipboardData || !clipboardData.files.length) return;
|
||||
e.preventDefault();
|
||||
|
||||
|
||||
if (page.url.pathname !== "/jpegify/") {
|
||||
const oldLength = files.files.length;
|
||||
files.add(clipboardData.files);
|
||||
|
@ -73,8 +73,6 @@
|
|||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
onMount(() => {
|
||||
initAnimStores();
|
||||
|
||||
|
|
|
@ -184,8 +184,10 @@
|
|||
</div>
|
||||
<button
|
||||
class="flex-shrink-0 w-8 rounded-full hover:bg-panel-alt h-full flex items-center justify-center"
|
||||
onclick={() =>
|
||||
(files.files = files.files.filter((_, i) => i !== index))}
|
||||
onclick={async () => {
|
||||
await file.cancel();
|
||||
files.files = files.files.filter((_, i) => i !== index);
|
||||
}}
|
||||
>
|
||||
<XIcon size="24" class="text-muted" />
|
||||
</button>
|
||||
|
|
Loading…
Reference in New Issue