Files
foliate-js/pdf.js
T
john-okeefe eb9ae699d7 Restore config.pdfjsPath override and options plumbing
Reverts the pdf.js/view.js API change that removed the exported
'config' (pdfjsPath) and the makePDF/open options parameter.
bookhoard overrides config.pdfjsPath to point at its own served
/static/vendor/pdfjs/ assets, so the override + options must stay.
Keep the unrelated external-link href fix in view.js.
2026-08-13 12:43:56 -04:00

208 lines
6.9 KiB
JavaScript

const _defaultPdfjsPath = (path) =>
new URL(`./vendor/pdfjs/${path}`, import.meta.url).toString();
export const config = {
pdfjsPath: _defaultPdfjsPath,
};
import "./vendor/pdfjs/pdf.mjs";
const pdfjsLib = globalThis.pdfjsLib;
pdfjsLib.GlobalWorkerOptions.workerSrc = _defaultPdfjsPath("pdf.worker.mjs");
const fetchText = async (url) => await (await fetch(url)).text();
const textLayerBuilderCSS = await fetchText(
_defaultPdfjsPath("text_layer_builder.css"),
);
const annotationLayerBuilderCSS = await fetchText(
_defaultPdfjsPath("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 ?? config.pdfjsPath("cmaps/");
const standardFontDataUrl = options.standardFontDataUrl ?? config.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;
};