Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,5 @@ vite.config.ts.timestamp-*

# IDE
.idea

static/mupdf-wasm.wasm
3 changes: 3 additions & 0 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down
58 changes: 58 additions & 0 deletions src/lib/converters/converter.svelte.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)));
}
}
2 changes: 2 additions & 0 deletions src/lib/converters/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[] => {
Expand All @@ -17,6 +18,7 @@ const getConverters = (): Converter[] => {
}

converters.push(new PandocConverter());
converters.push(new MuPDFConverter());
return converters;
};

Expand Down
53 changes: 53 additions & 0 deletions src/lib/converters/mupdf.svelte.ts
Original file line number Diff line number Diff line change
@@ -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),
];
}
25 changes: 24 additions & 1 deletion src/lib/types/file.svelte.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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 {
Expand Down
34 changes: 34 additions & 0 deletions src/lib/workers/mupdf.ts
Original file line number Diff line number Diff line change
@@ -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 });
}
};
2 changes: 1 addition & 1 deletion vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down