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:
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user