mirror of https://github.com/VERT-sh/VERT.git
chore: refactor IFile to VertFile
This commit is contained in:
parent
b3d279604d
commit
6bd3efd6f0
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 });
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
});
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -1,5 +0,0 @@
|
|||
export interface IFile {
|
||||
name: string;
|
||||
extension: string;
|
||||
buffer: ArrayBuffer;
|
||||
}
|
|
@ -1,3 +1,3 @@
|
|||
export * from "./file";
|
||||
export * from "./file.svelte";
|
||||
export * from "./util";
|
||||
export * from "./conversion-worker";
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
};
|
|
@ -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,
|
||||
),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
);
|
||||
};
|
||||
},
|
||||
);
|
||||
|
|
|
@ -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"
|
||||
>
|
||||
|
|
Loading…
Reference in New Issue