mirror of
https://github.com/john-okeefe/foliate-js.git
synced 2026-09-09 11:29:14 -04:00
Bundlers like Vite cannot resolve directory paths (cmaps/, standard_fonts/) through the new URL(..., import.meta.url) pattern. Add optional config to makePDF, makeBook, and View.open so consumers can provide explicit URLs. When options are not provided, falls back to import.meta.url resolution (preserving existing behavior for direct module usage).
206 lines
7.0 KiB
JavaScript
206 lines
7.0 KiB
JavaScript
const pdfjsPath = (path) =>
|
|
new URL(`./vendor/pdfjs/${path}`, import.meta.url).toString();
|
|
|
|
import "./vendor/pdfjs/pdf.mjs";
|
|
const pdfjsLib = globalThis.pdfjsLib;
|
|
pdfjsLib.GlobalWorkerOptions.workerSrc = pdfjsPath("pdf.worker.mjs");
|
|
|
|
const fetchText = async (url) => await (await fetch(url)).text();
|
|
|
|
// https://raw.githubusercontent.com/mozilla/pdf.js/refs/tags/v5.5.207/web/text_layer_builder.css
|
|
const textLayerBuilderCSS = await fetchText(
|
|
pdfjsPath("text_layer_builder.css"),
|
|
);
|
|
|
|
// https://raw.githubusercontent.com/mozilla/pdf.js/refs/tags/v5.5.207/web/annotation_layer_builder.css
|
|
const annotationLayerBuilderCSS = await fetchText(
|
|
pdfjsPath("annotation_layer_builder.css"),
|
|
);
|
|
|
|
const render = async (page, doc, zoom) => {
|
|
const scale = zoom * devicePixelRatio;
|
|
doc.documentElement.style.transform = `scale(${1 / devicePixelRatio})`;
|
|
doc.documentElement.style.transformOrigin = "top left";
|
|
doc.documentElement.style.setProperty("--scale-factor", scale);
|
|
const viewport = page.getViewport({ scale });
|
|
|
|
// the canvas must be in the `PDFDocument`'s `ownerDocument`
|
|
// (`globalThis.document` by default); that's where the fonts are loaded
|
|
const canvas = document.createElement("canvas");
|
|
canvas.height = viewport.height;
|
|
canvas.width = viewport.width;
|
|
const canvasContext = canvas.getContext("2d");
|
|
await page.render({ canvasContext, viewport }).promise;
|
|
doc.querySelector("#canvas").replaceChildren(doc.adoptNode(canvas));
|
|
|
|
const container = doc.querySelector(".textLayer");
|
|
const textLayer = new pdfjsLib.TextLayer({
|
|
textContentSource: await page.streamTextContent(),
|
|
container,
|
|
viewport,
|
|
});
|
|
await textLayer.render();
|
|
|
|
// hide "offscreen" canvases appended to docuemnt when rendering text layer
|
|
// https://github.com/mozilla/pdf.js/blob/642b9a5ae67ef642b9a8808fd9efd447e8c350e2/web/pdf_viewer.css#L51-L58
|
|
for (const canvas of document.querySelectorAll(".hiddenCanvasElement"))
|
|
Object.assign(canvas.style, {
|
|
position: "absolute",
|
|
top: "0",
|
|
left: "0",
|
|
width: "0",
|
|
height: "0",
|
|
display: "none",
|
|
});
|
|
|
|
// fix text selection
|
|
// https://github.com/mozilla/pdf.js/blob/642b9a5ae67ef642b9a8808fd9efd447e8c350e2/web/text_layer_builder.js#L105-L107
|
|
const endOfContent = document.createElement("div");
|
|
endOfContent.className = "endOfContent";
|
|
container.append(endOfContent);
|
|
// TODO: this only works in Firefox; see https://github.com/mozilla/pdf.js/pull/17923
|
|
container.onpointerdown = () => container.classList.add("selecting");
|
|
container.onpointerup = () => container.classList.remove("selecting");
|
|
|
|
const div = doc.querySelector(".annotationLayer");
|
|
const linkService = {
|
|
goToDestination: () => {},
|
|
getDestinationHash: (dest) => JSON.stringify(dest),
|
|
addLinkAttributes: (link, url) => (link.href = url),
|
|
};
|
|
await new pdfjsLib.AnnotationLayer({
|
|
page,
|
|
viewport,
|
|
div,
|
|
linkService,
|
|
}).render({ annotations: await page.getAnnotations() });
|
|
};
|
|
|
|
const renderPage = async (page, getImageBlob) => {
|
|
const viewport = page.getViewport({ scale: 1 });
|
|
if (getImageBlob) {
|
|
const canvas = document.createElement("canvas");
|
|
canvas.height = viewport.height;
|
|
canvas.width = viewport.width;
|
|
const canvasContext = canvas.getContext("2d");
|
|
await page.render({ canvasContext, viewport }).promise;
|
|
return new Promise((resolve) => canvas.toBlob(resolve));
|
|
}
|
|
const src = URL.createObjectURL(
|
|
new Blob(
|
|
[
|
|
`
|
|
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=${viewport.width}, height=${viewport.height}">
|
|
<style>
|
|
html, body {
|
|
margin: 0;
|
|
padding: 0;
|
|
}
|
|
/*
|
|
https://github.com/mozilla/pdf.js/commit/bd05b255fabfc313b194bfe9a17ccded4d90fb5a
|
|
*/
|
|
:root {
|
|
--user-unit: 1;
|
|
--total-scale-factor: calc(var(--scale-factor) * var(--user-unit));
|
|
--scale-round-x: 1px;
|
|
--scale-round-y: 1px;
|
|
}
|
|
${textLayerBuilderCSS}
|
|
${annotationLayerBuilderCSS}
|
|
</style>
|
|
<div id="canvas"></div>
|
|
<div class="textLayer"></div>
|
|
<div class="annotationLayer"></div>
|
|
`,
|
|
],
|
|
{ type: "text/html" },
|
|
),
|
|
);
|
|
const onZoom = ({ doc, scale }) => render(page, doc, scale);
|
|
return { src, onZoom };
|
|
};
|
|
|
|
const makeTOCItem = (item) => ({
|
|
label: item.title,
|
|
href: JSON.stringify(item.dest),
|
|
subitems: item.items.length ? item.items.map(makeTOCItem) : null,
|
|
});
|
|
|
|
export const makePDF = async (file, options = {}) => {
|
|
const cMapUrl = options.cMapUrl ?? pdfjsPath("cmaps/");
|
|
const standardFontDataUrl = options.standardFontDataUrl ?? pdfjsPath("standard_fonts/");
|
|
if (options.workerSrc) {
|
|
pdfjsLib.GlobalWorkerOptions.workerSrc = options.workerSrc;
|
|
}
|
|
const transport = new pdfjsLib.PDFDataRangeTransport(file.size, []);
|
|
transport.requestDataRange = (begin, end) => {
|
|
file
|
|
.slice(begin, end)
|
|
.arrayBuffer()
|
|
.then((chunk) => {
|
|
transport.onDataRange(begin, chunk);
|
|
});
|
|
};
|
|
const pdf = await pdfjsLib.getDocument({
|
|
range: transport,
|
|
cMapUrl,
|
|
standardFontDataUrl,
|
|
isEvalSupported: false,
|
|
}).promise;
|
|
|
|
const book = { rendition: { layout: "pre-paginated" } };
|
|
|
|
const { metadata, info } = (await pdf.getMetadata()) ?? {};
|
|
// TODO: for better results, parse `metadata.getRaw()`
|
|
book.metadata = {
|
|
title: metadata?.get("dc:title") ?? info?.Title,
|
|
author: metadata?.get("dc:creator") ?? info?.Author,
|
|
contributor: metadata?.get("dc:contributor"),
|
|
description: metadata?.get("dc:description") ?? info?.Subject,
|
|
language: metadata?.get("dc:language"),
|
|
publisher: metadata?.get("dc:publisher"),
|
|
subject: metadata?.get("dc:subject"),
|
|
identifier: metadata?.get("dc:identifier"),
|
|
source: metadata?.get("dc:source"),
|
|
rights: metadata?.get("dc:rights"),
|
|
};
|
|
|
|
const outline = await pdf.getOutline();
|
|
book.toc = outline?.map(makeTOCItem);
|
|
|
|
const cache = new Map();
|
|
book.sections = Array.from({ length: pdf.numPages }).map((_, i) => ({
|
|
id: i,
|
|
load: async () => {
|
|
const cached = cache.get(i);
|
|
if (cached) return cached;
|
|
const url = await renderPage(await pdf.getPage(i + 1));
|
|
cache.set(i, url);
|
|
return url;
|
|
},
|
|
size: 1000,
|
|
}));
|
|
book.isExternal = (uri) => /^\w+:/i.test(uri);
|
|
book.resolveHref = async (href) => {
|
|
const parsed = JSON.parse(href);
|
|
const dest =
|
|
typeof parsed === "string" ? await pdf.getDestination(parsed) : parsed;
|
|
const index = await pdf.getPageIndex(dest[0]);
|
|
return { index };
|
|
};
|
|
book.splitTOCHref = async (href) => {
|
|
const parsed = JSON.parse(href);
|
|
const dest =
|
|
typeof parsed === "string" ? await pdf.getDestination(parsed) : parsed;
|
|
const index = await pdf.getPageIndex(dest[0]);
|
|
return [index, null];
|
|
};
|
|
book.getTOCFragment = (doc) => doc.documentElement;
|
|
book.getCover = async () => renderPage(await pdf.getPage(1), true);
|
|
book.destroy = () => pdf.destroy();
|
|
return book;
|
|
};
|