feat: refactor ifile (#19)

* chore: refactor IFile to VertFile

* fix: remove debug red progress thing
This commit is contained in:
nullptr 2024-11-14 11:32:15 +00:00 committed by GitHub
parent b3d279604d
commit 3f0595ccb2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 116 additions and 248 deletions

View File

@ -1,4 +1,4 @@
import type { IFile, OmitBetterStrict } from "$lib/types";
import type { VertFile } from "$lib/types";
/**
* Base class for all converters.
@ -21,10 +21,10 @@ export class Converter {
public async convert(
// eslint-disable-next-line @typescript-eslint/no-unused-vars
input: OmitBetterStrict<IFile, "extension">,
input: VertFile,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
to: string,
): Promise<IFile> {
): Promise<VertFile> {
throw new Error("Not implemented");
}
}

View File

@ -1,6 +1,5 @@
import type { IFile } from "$lib/types";
import { VertFile } from "$lib/types";
import { Converter } from "./converter.svelte";
import type { OmitBetterStrict } from "$lib/types";
import { FFmpeg } from "@ffmpeg/ffmpeg";
import { browser } from "$app/environment";
import { log } from "$lib/logger";
@ -42,19 +41,23 @@ export class FFmpegConverter extends Converter {
})();
}
public async convert(
input: OmitBetterStrict<IFile, "extension">,
to: string,
): Promise<IFile> {
public async convert(input: VertFile, to: string): Promise<VertFile> {
if (!to.startsWith(".")) to = `.${to}`;
const ffmpeg = new FFmpeg();
ffmpeg.on("progress", (progress) => {
log(
["converters", this.name],
`progress for "${input.name}": ${progress.progress * 100}%`,
);
input.progress = progress.progress * 100;
});
const baseURL =
"https://cdn.jsdelivr.net/npm/@ffmpeg/core@0.12.6/dist/esm";
await ffmpeg.load({
coreURL: `${baseURL}/ffmpeg-core.js`,
wasmURL: `${baseURL}/ffmpeg-core.wasm`,
});
const buf = new Uint8Array(input.buffer);
const buf = new Uint8Array(await input.file.arrayBuffer());
await ffmpeg.writeFile("input", buf);
log(
["converters", this.name],
@ -70,10 +73,6 @@ export class FFmpegConverter extends Converter {
`read ${input.name.split(".").slice(0, -1).join(".") + to} from ffmpeg virtual fs`,
);
ffmpeg.terminate();
return {
...input,
buffer: output.buffer,
extension: to,
};
return new VertFile(new File([output], input.name), to);
}
}

View File

@ -1,82 +0,0 @@
import type { IFile } from "$lib/types";
import { Converter } from "./converter.svelte";
import MagickWorker from "$lib/workers/magick?worker";
import { browser } from "$app/environment";
import type { WorkerMessage, OmitBetterStrict } from "$lib/types";
import { MagickFormat } from "@imagemagick/magick-wasm";
const sortFirst = [".png", ".jpeg", ".jpg", ".webp", ".gif"];
export class MagickConverter extends Converter {
private worker: Worker = browser ? new MagickWorker() : null!;
private id = 0;
public name = "imagemagick";
public ready = $state(false);
public supportedFormats = Object.keys(MagickFormat)
.map((key) => `.${key.toLowerCase()}`)
.sort((a, b) => {
const aIndex = sortFirst.indexOf(a);
const bIndex = sortFirst.indexOf(b);
if (aIndex === -1 && bIndex === -1) return a.localeCompare(b);
if (aIndex === -1) return 1;
if (bIndex === -1) return -1;
return aIndex - bIndex;
});
constructor() {
super();
if (!browser) return;
this.worker.onmessage = (e) => {
const message: WorkerMessage = e.data;
if (message.type === "loaded") this.ready = true;
};
}
public async convert(
input: OmitBetterStrict<IFile, "extension">,
to: string,
): Promise<IFile> {
const res = await this.sendMessage({
type: "convert",
input: input as unknown as IFile,
to,
});
if (res.type === "finished") {
return res.output;
}
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) => {
const onMessage = (e: MessageEvent) => {
if (e.data.id === id) {
this.worker.removeEventListener("message", onMessage);
resolve(e.data);
resolved = true;
}
};
setTimeout(() => {
if (!resolved) {
this.worker.removeEventListener("message", onMessage);
throw new Error("Timeout");
}
}, 60000);
this.worker.addEventListener("message", onMessage);
this.worker.postMessage({ ...message, id });
});
}
}

View File

@ -1,4 +1,4 @@
import type { IFile } from "$lib/types";
import { VertFile } from "$lib/types";
import { Converter } from "./converter.svelte";
import VipsWorker from "$lib/workers/vips?worker";
import { browser } from "$app/environment";
@ -39,14 +39,11 @@ export class VipsConverter extends Converter {
};
}
public async convert(
input: OmitBetterStrict<IFile, "extension">,
to: string,
): Promise<IFile> {
public async convert(input: VertFile, to: string): Promise<VertFile> {
log(["converters", this.name], `converting ${input.name} to ${to}`);
const res = await this.sendMessage({
type: "convert",
input: input as unknown as IFile,
input,
to,
});

View File

@ -1,17 +1,8 @@
import { log } from "$lib/logger";
import type { IFile } from "$lib/types";
import { VertFile } from "$lib/types";
class Files {
public files = $state<
{
file: File;
from: string;
to: string;
blobUrl: string;
id: string;
result?: (IFile & { blobUrl: string; animating: boolean }) | null;
}[]
>([]);
public files = $state<VertFile[]>([]);
}
class Theme {

View File

@ -1,14 +1,14 @@
import type { IFile } from "./file";
import { VertFile } from "./file.svelte";
interface ConvertMessage {
type: "convert";
input: IFile;
input: VertFile;
to: string;
}
interface FinishedMessage {
type: "finished";
output: IFile;
output: VertFile;
}
interface LoadedMessage {

View File

@ -0,0 +1,25 @@
export class VertFile {
public id: string = Math.random().toString(36).slice(2, 8);
public get from() {
return "." + this.file.name.split(".").pop()!;
}
public get name() {
return this.file.name;
}
public progress = $state(0);
// public result: VertFile | null = null;
public result = $state<VertFile | null>(null);
public to = $state("");
constructor(
public readonly file: File,
to: string,
public readonly blobUrl?: string,
) {
this.to = to;
}
}

View File

@ -1,5 +0,0 @@
export interface IFile {
name: string;
extension: string;
buffer: ArrayBuffer;
}

View File

@ -1,3 +1,3 @@
export * from "./file";
export * from "./file.svelte";
export * from "./util";
export * from "./conversion-worker";

View File

@ -1,77 +0,0 @@
import type { WorkerMessage, OmitBetterStrict } from "$lib/types";
import {
ImageMagick,
initializeImageMagick,
MagickFormat,
} from "@imagemagick/magick-wasm";
import wasmUrl from "@imagemagick/magick-wasm/magick.wasm?url";
const magickPromise = fetch(wasmUrl)
.then((r) => r.arrayBuffer())
.then((r) => initializeImageMagick(r));
magickPromise
.then(() => {
postMessage({ type: "loaded" });
})
.catch((error) => {
postMessage({ type: "error", error });
});
const handleMessage = async (
message: WorkerMessage,
): Promise<OmitBetterStrict<WorkerMessage, "id"> | undefined> => {
await magickPromise;
switch (message.type) {
case "convert": {
if (!message.to.startsWith(".")) message.to = `.${message.to}`;
message.to = message.to.slice(1);
// unfortunately this lib uses some hacks to dispose images when the promise is resolved
// this means we can't promisify it :(
return new Promise((resolve) => {
ImageMagick.read(
new Uint8Array(message.input.buffer),
(img) => {
const keys = Object.keys(MagickFormat);
const values = Object.values(MagickFormat);
const index = keys.findIndex(
(key) =>
key.toLowerCase() === message.to.toLowerCase(),
);
const format = values[index];
img.write(format, (output) => {
resolve({
type: "finished",
output: {
...message.input,
buffer: output,
extension: message.to,
},
});
});
img.dispose();
},
);
});
}
}
};
onmessage = async (e) => {
const message: WorkerMessage = e.data;
try {
const res = await handleMessage(message);
if (!res) return;
postMessage({
...res,
id: message.id,
});
} catch (e) {
postMessage({
type: "error",
error: e,
id: message.id,
});
}
};

View File

@ -1,4 +1,8 @@
import type { WorkerMessage, OmitBetterStrict } from "$lib/types";
import {
type WorkerMessage,
type OmitBetterStrict,
VertFile,
} from "$lib/types";
import Vips from "wasm-vips";
const vipsPromise = Vips({
@ -21,16 +25,17 @@ const handleMessage = async (
switch (message.type) {
case "convert": {
if (!message.to.startsWith(".")) message.to = `.${message.to}`;
const image = vips.Image.newFromBuffer(message.input.buffer);
const image = vips.Image.newFromBuffer(
await message.input.file.arrayBuffer(),
);
const output = image.writeToBuffer(message.to);
image.delete();
return {
type: "finished",
output: {
...message.input,
buffer: output.buffer,
extension: message.to,
},
output: new VertFile(
new File([output.buffer], message.input.name),
message.to,
),
};
}
}

View File

@ -4,6 +4,7 @@
import { converters } from "$lib/converters";
import { log } from "$lib/logger/index.js";
import { files } from "$lib/store/index.svelte";
import { VertFile } from "$lib/types/file.svelte.js";
import { Check } from "lucide-svelte";
const { data } = $props();
@ -42,31 +43,58 @@
ctx?.drawImage(img, 0, 0, canvas.width, canvas.height);
// get the blob
canvas.toBlob(
(blob) => {
resolve({
file: f,
from,
to,
blobUrl:
blob === null
? ""
: URL.createObjectURL(blob),
id: Math.random().toString(36).substring(2),
});
async (blob) => {
// resolve({
// file: f,
// from,
// to,
// blobUrl:
// blob === null
// ? ""
// : URL.createObjectURL(blob),
// id: Math.random().toString(36).substring(2),
// buffer: await f.arrayBuffer(),
// extension: from,
// name: f.name,
// result: null,
// progress: 0,
// });
resolve(
new VertFile(
new File([blob!], f.name, {
type: blob!.type,
}),
to,
URL.createObjectURL(blob!),
),
);
},
"image/jpeg",
0.75,
);
};
img.onerror = () => {
resolve({
file: f,
from,
to,
blobUrl: "",
id: Math.random().toString(36).substring(2),
});
img.onerror = async () => {
// resolve({
// file: f,
// from,
// to,
// blobUrl: "",
// id: Math.random().toString(36).substring(2),
// name: f.name,
// buffer: await f.arrayBuffer(),
// extension: from,
// result: null,
// progress: 0,
// });
resolve(
new VertFile(
new File([await f.arrayBuffer()], f.name, {
type: f.type,
}),
to,
),
);
};
},
);

View File

@ -81,25 +81,8 @@
if (!converter) throw new Error("No converter found");
const to = file.to;
processings[i] = true;
const converted = await converter.convert(
{
name: file.file.name,
buffer: await file.file.arrayBuffer(),
},
to,
);
files.files[i] = {
...file,
result: {
...converted,
blobUrl: URL.createObjectURL(
new Blob([converted.buffer], {
type: file.file.type,
}),
),
animating: true,
},
};
const converted = await converter.convert(file, to);
file.result = converted;
processings[i] = false;
})(),
);
@ -123,7 +106,7 @@
dlFiles.push({
name: file.file.name.replace(/\.[^/.]+$/, "") + file.to,
lastModified: Date.now(),
input: result.buffer,
input: await result.file.arrayBuffer(),
});
}
if (files.files.length === 0) return;
@ -257,7 +240,7 @@
);
})()}
<div
class="w-full rounded-xl"
class="w-full rounded-xl relative"
animate:flip={{ duration, easing: quintOut }}
out:blur={{
duration,
@ -267,7 +250,7 @@
>
<div
class={clsx(
"sm:h-16 sm:py-0 py-4 px-3 flex relative flex-shrink-0 items-center w-full rounded-xl",
"sm:h-16 sm:py-0 py-4 px-3 flex relative overflow-hidden flex-shrink-0 items-center w-full rounded-xl",
{
"initial-fade": !finisheds[i],
processing:
@ -279,6 +262,10 @@
? 'var(--accent-bg)'
: 'var(--fg-muted-alt)'}; transition: border 1000ms ease; transition: filter {duration}ms var(--transition), transform {duration}ms var(--transition);"
>
<!-- <div
class="absolute top-0 left-0 bg-red-500 h-full"
style="width: {file.progress}%; transition: width 500ms linear;"
></div> -->
<div
class="flex gap-8 sm:gap-0 sm:flex-row flex-col items-center justify-between w-full z-50 relative sm:h-fit h-full"
>