Files
foliate-js/pdf.js
T
john-okeefe 74c317d58c Allow explicit PDF.js resource paths for bundler compatibility
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).
2026-05-09 21:03:39 -04:00

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;
};