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