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( [ `
`, ], { 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) => { 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: pdfjsPath("cmaps/"), standardFontDataUrl: pdfjsPath("standard_fonts/"), 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; };