feat(frontend): add client-side cover generator via dynamic foliate-js import

New cover-generator.ts module that dynamically imports foliate-js/view.js
only when cover generation is requested, keeping it out of the main bundle.

Supports all media types:
- PDF (fixed_layout): renders page 1 to canvas via view.renderer
- EPUB/CBZ (reflowable): extracts book.cover blob from parsed metadata
- Falls back to canvas-to-JPEG conversion for non-JPEG sources
This commit is contained in:
2026-05-10 11:53:31 -04:00
parent 4a26e79a20
commit 2ef1f9580f
+82
View File
@@ -0,0 +1,82 @@
import { showToast } from "./toast";
function getToken(): string {
return localStorage.getItem("token") || "";
}
export async function generateCoverBlob(
fileUrl: string,
formatGroup: string,
): Promise<Blob | null> {
try {
const View = (await import("foliate-js/view.js")).default;
const view = new View();
const resp = await fetch(fileUrl, {
headers: { Authorization: `Bearer ${getToken()}` },
});
if (!resp.ok) {
showToast("Failed to fetch book file for cover generation", "error");
return null;
}
const blob = await resp.blob();
const file = new File([blob], "book", { type: blob.type });
const pdfOptions =
formatGroup === "fixed_layout"
? {
pdf: {
cMapUrl: "/static/vendor/pdfjs/cmaps/",
standardFontDataUrl: "/static/vendor/pdfjs/standard_fonts/",
},
}
: {};
await view.open(file, pdfOptions);
if (!view.book?.sections?.length) {
showToast("Could not read book sections", "error");
return null;
}
if (formatGroup === "fixed_layout") {
const canvas = document.createElement("canvas");
await view.renderer?.renderPage(view.book.sections[0], canvas);
return new Promise((resolve) => {
canvas.toBlob(
(b) => resolve(b),
"image/jpeg",
0.85,
);
});
}
const coverHref = view.book.cover;
if (coverHref) {
const coverBlob = await coverHref.blob();
if (coverBlob.type.startsWith("image/")) {
return coverBlob;
}
return new Promise((resolve) => {
const img = new Image();
img.onload = () => {
const c = document.createElement("canvas");
c.width = img.naturalWidth;
c.height = img.naturalHeight;
c.getContext("2d")?.drawImage(img, 0, 0);
c.toBlob((b) => resolve(b), "image/jpeg", 0.85);
};
img.onerror = () => resolve(null);
img.src = URL.createObjectURL(coverBlob);
});
}
showToast("No cover found in book file", "error");
return null;
} catch (e) {
console.error("Cover generation failed:", e);
showToast("Cover generation failed", "error");
return null;
}
}