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.
This commit is contained in:
2026-08-13 12:43:56 -04:00
parent d9b1d5d18f
commit eb9ae699d7
2 changed files with 168 additions and 141 deletions
+136 -109
View File
@@ -1,82 +1,97 @@
const pdfjsPath = path => new URL(`./vendor/pdfjs/${path}`, import.meta.url).toString() const _defaultPdfjsPath = (path) =>
new URL(`./vendor/pdfjs/${path}`, import.meta.url).toString();
import './vendor/pdfjs/pdf.mjs' export const config = {
const pdfjsLib = globalThis.pdfjsLib pdfjsPath: _defaultPdfjsPath,
pdfjsLib.GlobalWorkerOptions.workerSrc = pdfjsPath('pdf.worker.mjs') };
const fetchText = async url => await (await fetch(url)).text() import "./vendor/pdfjs/pdf.mjs";
const pdfjsLib = globalThis.pdfjsLib;
pdfjsLib.GlobalWorkerOptions.workerSrc = _defaultPdfjsPath("pdf.worker.mjs");
// https://raw.githubusercontent.com/mozilla/pdf.js/refs/tags/v5.5.207/web/text_layer_builder.css const fetchText = async (url) => await (await fetch(url)).text();
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 textLayerBuilderCSS = await fetchText(
const annotationLayerBuilderCSS = await fetchText(pdfjsPath('annotation_layer_builder.css')) _defaultPdfjsPath("text_layer_builder.css"),
);
const annotationLayerBuilderCSS = await fetchText(
_defaultPdfjsPath("annotation_layer_builder.css"),
);
const render = async (page, doc, zoom) => { const render = async (page, doc, zoom) => {
const scale = zoom * devicePixelRatio const scale = zoom * devicePixelRatio;
doc.documentElement.style.transform = `scale(${1 / devicePixelRatio})` doc.documentElement.style.transform = `scale(${1 / devicePixelRatio})`;
doc.documentElement.style.transformOrigin = 'top left' doc.documentElement.style.transformOrigin = "top left";
doc.documentElement.style.setProperty('--scale-factor', scale) doc.documentElement.style.setProperty("--scale-factor", scale);
const viewport = page.getViewport({ scale }) const viewport = page.getViewport({ scale });
// the canvas must be in the `PDFDocument`'s `ownerDocument` // the canvas must be in the `PDFDocument`'s `ownerDocument`
// (`globalThis.document` by default); that's where the fonts are loaded // (`globalThis.document` by default); that's where the fonts are loaded
const canvas = document.createElement('canvas') const canvas = document.createElement("canvas");
canvas.height = viewport.height canvas.height = viewport.height;
canvas.width = viewport.width canvas.width = viewport.width;
const canvasContext = canvas.getContext('2d') const canvasContext = canvas.getContext("2d");
await page.render({ canvasContext, viewport }).promise await page.render({ canvasContext, viewport }).promise;
doc.querySelector('#canvas').replaceChildren(doc.adoptNode(canvas)) doc.querySelector("#canvas").replaceChildren(doc.adoptNode(canvas));
const container = doc.querySelector('.textLayer') const container = doc.querySelector(".textLayer");
const textLayer = new pdfjsLib.TextLayer({ const textLayer = new pdfjsLib.TextLayer({
textContentSource: await page.streamTextContent(), textContentSource: await page.streamTextContent(),
container, viewport, container,
}) viewport,
await textLayer.render() });
await textLayer.render();
// hide "offscreen" canvases appended to docuemnt when rendering text layer // hide "offscreen" canvases appended to docuemnt when rendering text layer
// https://github.com/mozilla/pdf.js/blob/642b9a5ae67ef642b9a8808fd9efd447e8c350e2/web/pdf_viewer.css#L51-L58 // https://github.com/mozilla/pdf.js/blob/642b9a5ae67ef642b9a8808fd9efd447e8c350e2/web/pdf_viewer.css#L51-L58
for (const canvas of document.querySelectorAll('.hiddenCanvasElement')) for (const canvas of document.querySelectorAll(".hiddenCanvasElement"))
Object.assign(canvas.style, { Object.assign(canvas.style, {
position: 'absolute', position: "absolute",
top: '0', top: "0",
left: '0', left: "0",
width: '0', width: "0",
height: '0', height: "0",
display: 'none', display: "none",
}) });
// fix text selection // fix text selection
// https://github.com/mozilla/pdf.js/blob/642b9a5ae67ef642b9a8808fd9efd447e8c350e2/web/text_layer_builder.js#L105-L107 // https://github.com/mozilla/pdf.js/blob/642b9a5ae67ef642b9a8808fd9efd447e8c350e2/web/text_layer_builder.js#L105-L107
const endOfContent = document.createElement('div') const endOfContent = document.createElement("div");
endOfContent.className = 'endOfContent' endOfContent.className = "endOfContent";
container.append(endOfContent) container.append(endOfContent);
// TODO: this only works in Firefox; see https://github.com/mozilla/pdf.js/pull/17923 // TODO: this only works in Firefox; see https://github.com/mozilla/pdf.js/pull/17923
container.onpointerdown = () => container.classList.add('selecting') container.onpointerdown = () => container.classList.add("selecting");
container.onpointerup = () => container.classList.remove('selecting') container.onpointerup = () => container.classList.remove("selecting");
const div = doc.querySelector('.annotationLayer') const div = doc.querySelector(".annotationLayer");
const linkService = { const linkService = {
goToDestination: () => {}, goToDestination: () => {},
getDestinationHash: dest => JSON.stringify(dest), getDestinationHash: (dest) => JSON.stringify(dest),
addLinkAttributes: (link, url) => link.href = url, addLinkAttributes: (link, url) => (link.href = url),
} };
await new pdfjsLib.AnnotationLayer({ page, viewport, div, linkService }) await new pdfjsLib.AnnotationLayer({
.render({ annotations: await page.getAnnotations() }) page,
} viewport,
div,
linkService,
}).render({ annotations: await page.getAnnotations() });
};
const renderPage = async (page, getImageBlob) => { const renderPage = async (page, getImageBlob) => {
const viewport = page.getViewport({ scale: 1 }) const viewport = page.getViewport({ scale: 1 });
if (getImageBlob) { if (getImageBlob) {
const canvas = document.createElement('canvas') const canvas = document.createElement("canvas");
canvas.height = viewport.height canvas.height = viewport.height;
canvas.width = viewport.width canvas.width = viewport.width;
const canvasContext = canvas.getContext('2d') const canvasContext = canvas.getContext("2d");
await page.render({ canvasContext, viewport }).promise await page.render({ canvasContext, viewport }).promise;
return new Promise(resolve => canvas.toBlob(resolve)) return new Promise((resolve) => canvas.toBlob(resolve));
} }
const src = URL.createObjectURL(new Blob([` const src = URL.createObjectURL(
new Blob(
[
`
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<meta charset="utf-8"> <meta charset="utf-8">
@@ -101,80 +116,92 @@ const renderPage = async (page, getImageBlob) => {
<div id="canvas"></div> <div id="canvas"></div>
<div class="textLayer"></div> <div class="textLayer"></div>
<div class="annotationLayer"></div> <div class="annotationLayer"></div>
`], { type: 'text/html' })) `,
const onZoom = ({ doc, scale }) => render(page, doc, scale) ],
return { src, onZoom } { type: "text/html" },
} ),
);
const onZoom = ({ doc, scale }) => render(page, doc, scale);
return { src, onZoom };
};
const makeTOCItem = item => ({ const makeTOCItem = (item) => ({
label: item.title, label: item.title,
href: JSON.stringify(item.dest), href: JSON.stringify(item.dest),
subitems: item.items.length ? item.items.map(makeTOCItem) : null, subitems: item.items.length ? item.items.map(makeTOCItem) : null,
}) });
export const makePDF = async file => { export const makePDF = async (file, options = {}) => {
const transport = new pdfjsLib.PDFDataRangeTransport(file.size, []) const cMapUrl = options.cMapUrl ?? config.pdfjsPath("cmaps/");
transport.requestDataRange = (begin, end) => { const standardFontDataUrl = options.standardFontDataUrl ?? config.pdfjsPath("standard_fonts/");
file.slice(begin, end).arrayBuffer().then(chunk => { if (options.workerSrc) {
transport.onDataRange(begin, chunk) 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({ const pdf = await pdfjsLib.getDocument({
range: transport, range: transport,
cMapUrl: pdfjsPath('cmaps/'), cMapUrl,
standardFontDataUrl: pdfjsPath('standard_fonts/'), standardFontDataUrl,
isEvalSupported: false, isEvalSupported: false,
}).promise }).promise;
const book = { rendition: { layout: 'pre-paginated' } } const book = { rendition: { layout: "pre-paginated" } };
const { metadata, info } = await pdf.getMetadata() ?? {} const { metadata, info } = (await pdf.getMetadata()) ?? {};
// TODO: for better results, parse `metadata.getRaw()` // TODO: for better results, parse `metadata.getRaw()`
book.metadata = { book.metadata = {
title: metadata?.get('dc:title') ?? info?.Title, title: metadata?.get("dc:title") ?? info?.Title,
author: metadata?.get('dc:creator') ?? info?.Author, author: metadata?.get("dc:creator") ?? info?.Author,
contributor: metadata?.get('dc:contributor'), contributor: metadata?.get("dc:contributor"),
description: metadata?.get('dc:description') ?? info?.Subject, description: metadata?.get("dc:description") ?? info?.Subject,
language: metadata?.get('dc:language'), language: metadata?.get("dc:language"),
publisher: metadata?.get('dc:publisher'), publisher: metadata?.get("dc:publisher"),
subject: metadata?.get('dc:subject'), subject: metadata?.get("dc:subject"),
identifier: metadata?.get('dc:identifier'), identifier: metadata?.get("dc:identifier"),
source: metadata?.get('dc:source'), source: metadata?.get("dc:source"),
rights: metadata?.get('dc:rights'), rights: metadata?.get("dc:rights"),
} };
const outline = await pdf.getOutline() const outline = await pdf.getOutline();
book.toc = outline?.map(makeTOCItem) book.toc = outline?.map(makeTOCItem);
const cache = new Map() const cache = new Map();
book.sections = Array.from({ length: pdf.numPages }).map((_, i) => ({ book.sections = Array.from({ length: pdf.numPages }).map((_, i) => ({
id: i, id: i,
load: async () => { load: async () => {
const cached = cache.get(i) const cached = cache.get(i);
if (cached) return cached if (cached) return cached;
const url = await renderPage(await pdf.getPage(i + 1)) const url = await renderPage(await pdf.getPage(i + 1));
cache.set(i, url) cache.set(i, url);
return url return url;
}, },
size: 1000, size: 1000,
})) }));
book.isExternal = uri => /^\w+:/i.test(uri) book.isExternal = (uri) => /^\w+:/i.test(uri);
book.resolveHref = async href => { book.resolveHref = async (href) => {
const parsed = JSON.parse(href) const parsed = JSON.parse(href);
const dest = typeof parsed === 'string' const dest =
? await pdf.getDestination(parsed) : parsed typeof parsed === "string" ? await pdf.getDestination(parsed) : parsed;
const index = await pdf.getPageIndex(dest[0]) const index = await pdf.getPageIndex(dest[0]);
return { index } return { index };
} };
book.splitTOCHref = async href => { book.splitTOCHref = async (href) => {
const parsed = JSON.parse(href) const parsed = JSON.parse(href);
const dest = typeof parsed === 'string' const dest =
? await pdf.getDestination(parsed) : parsed typeof parsed === "string" ? await pdf.getDestination(parsed) : parsed;
const index = await pdf.getPageIndex(dest[0]) const index = await pdf.getPageIndex(dest[0]);
return [index, null] return [index, null];
} };
book.getTOCFragment = doc => doc.documentElement book.getTOCFragment = (doc) => doc.documentElement;
book.getCover = async () => renderPage(await pdf.getPage(1), true) book.getCover = async () => renderPage(await pdf.getPage(1), true);
book.destroy = () => pdf.destroy() book.destroy = () => pdf.destroy();
return book return book;
} };
+4 -4
View File
@@ -76,7 +76,7 @@ const fetchFile = async url => {
return new File([await res.blob()], new URL(res.url).pathname) return new File([await res.blob()], new URL(res.url).pathname)
} }
export const makeBook = async file => { export const makeBook = async (file, options = {}) => {
if (typeof file === 'string') file = await fetchFile(file) if (typeof file === 'string') file = await fetchFile(file)
let book let book
if (file.isDirectory) { if (file.isDirectory) {
@@ -105,7 +105,7 @@ export const makeBook = async file => {
} }
else if (await isPDF(file)) { else if (await isPDF(file)) {
const { makePDF } = await import('./pdf.js') const { makePDF } = await import('./pdf.js')
book = await makePDF(file) book = await makePDF(file, options.pdf)
} }
else { else {
const { isMOBI, MOBI } = await import('./mobi.js') const { isMOBI, MOBI } = await import('./mobi.js')
@@ -230,10 +230,10 @@ export class View extends HTMLElement {
this.renderer.goTo(resolved) this.renderer.goTo(resolved)
}) })
} }
async open(book) { async open(book, options = {}) {
if (typeof book === 'string' if (typeof book === 'string'
|| typeof book.arrayBuffer === 'function' || typeof book.arrayBuffer === 'function'
|| book.isDirectory) book = await makeBook(book) || book.isDirectory) book = await makeBook(book, options)
this.book = book this.book = book
this.language = languageInfo(book.metadata?.language) this.language = languageInfo(book.metadata?.language)