import './vendor/pdfjs/pdf.mjs' const pdfjsLib = globalThis.pdfjsLib pdfjsLib.GlobalWorkerOptions.workerSrc = new URL('vendor/pdfjs/pdf.worker.mjs', import.meta.url).toString() const fetchText = async url => await (await fetch(new URL(url, import.meta.url))).text() // https://github.com/mozilla/pdf.js/blob/642b9a5ae67ef642b9a8808fd9efd447e8c350e2/web/text_layer_builder.css const textLayerBuilderCSS = await fetchText('vendor/pdfjs/text_layer_builder.css') // https://github.com/mozilla/pdf.js/blob/642b9a5ae67ef642b9a8808fd9efd447e8c350e2/web/annotation_layer_builder.css const annotationLayerBuilderCSS = await fetchText('vendor/pdfjs/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 }) // for some reason must use `document`'s canvas // using a canvas in `doc` results in tofu 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') await new pdfjsLib.AnnotationLayer({ page, viewport, div }).render({ annotations: await page.getAnnotations(), linkService: { goToDestination: () => {}, getDestinationHash: dest => JSON.stringify(dest), addLinkAttributes: (link, url) => link.href = url, }, }) } 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 data = new Uint8Array(await file.arrayBuffer()) const pdf = await pdfjsLib.getDocument({ data }).promise const book = { rendition: { layout: 'pre-paginated' } } const info = (await pdf.getMetadata())?.info book.metadata = { title: info?.Title, author: info?.Author, } 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 }