mirror of https://github.com/VERT-sh/VERT.git
Merge cce3b887b6 into e1c83ba4ad
This commit is contained in:
commit
2a1230aa9d
|
|
@ -22,3 +22,5 @@ vite.config.ts.timestamp-*
|
|||
|
||||
# IDE
|
||||
.idea
|
||||
|
||||
static/mupdf-wasm.wasm
|
||||
|
|
|
|||
3
bun.lock
3
bun.lock
|
|
@ -17,6 +17,7 @@
|
|||
"clsx": "^2.1.1",
|
||||
"fflate": "^0.8.2",
|
||||
"lucide-svelte": "^0.554.0",
|
||||
"mupdf": "^1.28.0",
|
||||
"music-metadata": "^11.10.1",
|
||||
"overlayscrollbars": "^2.12.0",
|
||||
"overlayscrollbars-svelte": "^0.5.5",
|
||||
|
|
@ -642,6 +643,8 @@
|
|||
|
||||
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||
|
||||
"mupdf": ["mupdf@1.28.0", "", {}, "sha512-ACUnbpECaQ5JLq04pwd89lS+0IGMest5qL5tb08g9TAR7bDtfqflHEkb2Xm3o4rvC/szguLiV+WEbW9kstj8Sg=="],
|
||||
|
||||
"music-metadata": ["music-metadata@11.10.3", "", { "dependencies": { "@borewit/text-codec": "^0.2.0", "@tokenizer/token": "^0.3.0", "content-type": "^1.0.5", "debug": "^4.4.3", "file-type": "^21.1.1", "media-typer": "^1.1.0", "strtok3": "^10.3.4", "token-types": "^6.1.1", "uint8array-extras": "^1.5.0" } }, "sha512-j0g/x4cNNZW6I5gdcPAY+GFkJY9WHTpkFDMBJKQLxJQyvSfQbXm57fTE3haGFFuOzCgtsTd4Plwc49Sn9RacDQ=="],
|
||||
|
||||
"mz": ["mz@2.7.0", "", { "dependencies": { "any-promise": "^1.0.0", "object-assign": "^4.0.1", "thenify-all": "^1.0.0" } }, "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q=="],
|
||||
|
|
|
|||
|
|
@ -9,7 +9,8 @@
|
|||
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
||||
"format": "prettier --write .",
|
||||
"lint": "prettier --check . && eslint ."
|
||||
"lint": "prettier --check . && eslint .",
|
||||
"postinstall": "cp node_modules/mupdf/dist/mupdf-wasm.wasm static/mupdf-wasm.wasm"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@inlang/paraglide-js": "^2.5.0",
|
||||
|
|
@ -51,6 +52,7 @@
|
|||
"clsx": "^2.1.1",
|
||||
"fflate": "^0.8.2",
|
||||
"lucide-svelte": "^0.554.0",
|
||||
"mupdf": "^1.28.0",
|
||||
"music-metadata": "^11.10.3",
|
||||
"overlayscrollbars": "^2.12.0",
|
||||
"overlayscrollbars-svelte": "^0.5.5",
|
||||
|
|
|
|||
|
|
@ -93,3 +93,61 @@ export class Converter {
|
|||
return this.supportedFormats.map((f) => f.name);
|
||||
}
|
||||
}
|
||||
|
||||
export interface ChainStep {
|
||||
converter: Converter;
|
||||
to: string; // intermediate format (last step uses the final target)
|
||||
}
|
||||
|
||||
export class ChainedConverter extends Converter {
|
||||
public override name: string;
|
||||
private steps: ChainStep[];
|
||||
|
||||
constructor(steps: ChainStep[]) {
|
||||
super(0);
|
||||
if (steps.length < 2) throw new Error("Chain requires at least 2 steps");
|
||||
this.steps = steps;
|
||||
this.name = steps.map((s) => s.converter.name).join("+");
|
||||
this.clearTimeout();
|
||||
this.status = this.deriveStatus();
|
||||
this.supportedFormats = [
|
||||
...steps[0].converter.supportedFormats.filter((f) => f.fromSupported),
|
||||
...steps[steps.length - 1].converter.supportedFormats.filter((f) => f.toSupported),
|
||||
];
|
||||
}
|
||||
|
||||
private deriveStatus(): WorkerStatus {
|
||||
const statuses = this.steps.map((s) => s.converter.status);
|
||||
if (statuses.some((s) => s === "error")) return "error";
|
||||
if (statuses.every((s) => s === "ready")) return "ready";
|
||||
if (statuses.some((s) => s === "downloading")) return "downloading";
|
||||
return "not-ready";
|
||||
}
|
||||
|
||||
public override async convert(input: VertFile, to: string): Promise<VertFile> {
|
||||
this.status = this.deriveStatus();
|
||||
const { VertFile: VF } = await import("$lib/types");
|
||||
let current: VertFile = input;
|
||||
|
||||
for (let i = 0; i < this.steps.length; i++) {
|
||||
const { converter, to: stepTo } = this.steps[i];
|
||||
const isLast = i === this.steps.length - 1;
|
||||
const target = isLast ? to : stepTo;
|
||||
const result = await converter.convert(current, target);
|
||||
if (!isLast) {
|
||||
current = new VF(
|
||||
new File([await result.file.arrayBuffer()], input.name.replace(/\.[^/.]+$/, target)),
|
||||
target,
|
||||
);
|
||||
} else {
|
||||
current = result;
|
||||
}
|
||||
}
|
||||
|
||||
return current;
|
||||
}
|
||||
|
||||
public override async cancel(input: VertFile): Promise<void> {
|
||||
await Promise.allSettled(this.steps.map((s) => s.converter.cancel(input)));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { FFmpegConverter } from "./ffmpeg.svelte";
|
|||
import { PandocConverter } from "./pandoc.svelte";
|
||||
import { VertdConverter } from "./vertd.svelte";
|
||||
import { MagickConverter } from "./magick.svelte";
|
||||
import { MuPDFConverter } from "./mupdf.svelte";
|
||||
import { DISABLE_ALL_EXTERNAL_REQUESTS } from "$lib/util/consts";
|
||||
|
||||
const getConverters = (): Converter[] => {
|
||||
|
|
@ -17,6 +18,7 @@ const getConverters = (): Converter[] => {
|
|||
}
|
||||
|
||||
converters.push(new PandocConverter());
|
||||
converters.push(new MuPDFConverter());
|
||||
return converters;
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,53 @@
|
|||
import { VertFile } from "$lib/types";
|
||||
import { Converter, FormatInfo } from "./converter.svelte";
|
||||
import { browser } from "$app/environment";
|
||||
import MuPDFWorker from "$lib/workers/mupdf?worker&url";
|
||||
|
||||
export class MuPDFConverter extends Converter {
|
||||
public name = "mupdf";
|
||||
|
||||
private activeConversions = new Map<string, Worker>();
|
||||
|
||||
constructor() {
|
||||
super(60);
|
||||
if (!browser) return;
|
||||
this.status = "ready";
|
||||
this.clearTimeout();
|
||||
}
|
||||
|
||||
public async convert(file: VertFile, to: string): Promise<VertFile> {
|
||||
const worker = new Worker(MuPDFWorker, { type: "module" });
|
||||
this.activeConversions.set(file.id, worker);
|
||||
|
||||
worker.postMessage({ file: file.file, to, id: file.id });
|
||||
|
||||
const result = await new Promise<{ type: string; output?: Uint8Array; error?: string }>(
|
||||
(resolve) => {
|
||||
worker.onmessage = (e) => resolve(e.data);
|
||||
worker.onerror = (e) => resolve({ type: "error", error: e.message });
|
||||
},
|
||||
);
|
||||
|
||||
worker.terminate();
|
||||
this.activeConversions.delete(file.id);
|
||||
|
||||
if (result.type === "error") throw new Error(result.error);
|
||||
|
||||
if (!to.startsWith(".")) to = `.${to}`;
|
||||
return new VertFile(new File([result.output! as BlobPart], file.name), to);
|
||||
}
|
||||
|
||||
public async cancel(input: VertFile): Promise<void> {
|
||||
const worker = this.activeConversions.get(input.id);
|
||||
if (worker) {
|
||||
worker.terminate();
|
||||
this.activeConversions.delete(input.id);
|
||||
}
|
||||
}
|
||||
|
||||
public supportedFormats = [
|
||||
new FormatInfo("pdf", true, false),
|
||||
new FormatInfo("md", false, true),
|
||||
new FormatInfo("txt", false, true),
|
||||
];
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
import { byNative, converters } from "$lib/converters";
|
||||
import type { Converter } from "$lib/converters/converter.svelte";
|
||||
import { ChainedConverter } from "$lib/converters/converter.svelte";
|
||||
import { m } from "$lib/paraglide/messages";
|
||||
import { ToastManager } from "$lib/util/toast.svelte";
|
||||
import type { Component } from "svelte";
|
||||
|
|
@ -65,7 +66,29 @@ export class VertFile {
|
|||
if (!theirFrom.isNative && !theirTo.isNative) return false;
|
||||
return true;
|
||||
});
|
||||
return converter;
|
||||
if (converter) return converter;
|
||||
|
||||
for (const a of this.converters) {
|
||||
for (const fmt of a.supportedFormats) {
|
||||
if (!fmt.toSupported) continue;
|
||||
const b = converters.find((c) => {
|
||||
const cFrom = c.supportedFormats.find(
|
||||
(f) => f.name === fmt.name && f.fromSupported,
|
||||
);
|
||||
const cTo = c.supportedFormats.find(
|
||||
(f) => f.name === this.to && f.toSupported,
|
||||
);
|
||||
return cFrom && cTo;
|
||||
});
|
||||
if (b)
|
||||
return new ChainedConverter([
|
||||
{ converter: a, to: fmt.name },
|
||||
{ converter: b, to: this.to },
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
public isLarge(): boolean {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,34 @@
|
|||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(globalThis as any).$libmupdf_wasm_Module = {
|
||||
locateFile: () => "/mupdf-wasm.wasm",
|
||||
};
|
||||
|
||||
let mupdf: typeof import("mupdf") | null = null;
|
||||
|
||||
self.onmessage = async (e: MessageEvent) => {
|
||||
const { file, to, id } = e.data as { file: File; to: string; id: string };
|
||||
try {
|
||||
if (!mupdf) mupdf = await import("mupdf");
|
||||
|
||||
const buf = new Uint8Array(await file.arrayBuffer());
|
||||
const doc = mupdf.Document.openDocument(buf, "application/pdf");
|
||||
const pageCount = doc.countPages();
|
||||
const parts: string[] = [];
|
||||
|
||||
for (let i = 0; i < pageCount; i++) {
|
||||
const page = doc.loadPage(i);
|
||||
const st = page.toStructuredText("preserve-whitespace");
|
||||
parts.push(st.asText());
|
||||
page.destroy();
|
||||
}
|
||||
doc.destroy();
|
||||
|
||||
self.postMessage({
|
||||
type: "finished",
|
||||
output: new TextEncoder().encode(parts.join("\n\n")),
|
||||
id,
|
||||
});
|
||||
} catch (err) {
|
||||
self.postMessage({ type: "error", error: String(err), id });
|
||||
}
|
||||
};
|
||||
|
|
@ -53,7 +53,7 @@ export default defineConfig(({ command }) => {
|
|||
format: "es",
|
||||
},
|
||||
optimizeDeps: {
|
||||
exclude: ["@ffmpeg/core-mt", "@ffmpeg/ffmpeg", "@ffmpeg/util"],
|
||||
exclude: ["@ffmpeg/core-mt", "@ffmpeg/ffmpeg", "@ffmpeg/util", "mupdf"],
|
||||
},
|
||||
css: {
|
||||
preprocessorOptions: {
|
||||
|
|
|
|||
Loading…
Reference in New Issue