Files
foliate-js/pdf.js
T
John Factotum 1e952102bd PDF: rerender when scale changes
The fixed-layout renderer now accepts an object for the return value of
`load()` (on the `book.sections` items). If it's an object, the `src`
is the URL of the section, and `onZoom`, if present, will be called
when the viewport is zoomed. With this you can scale the page yourself
rather than relying on the renderer's own scaling with CSS transform.

Also upgraded to PDF.js 4.6.82. And now internal links are broken...
Don't know how to fix that yet.
2024-09-28 12:12:59 +08:00

144 lines
5.2 KiB
JavaScript

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',
})
const div = doc.querySelector('.annotationLayer')
await new pdfjsLib.AnnotationLayer({ page, viewport, div }).render({
annotations: await page.getAnnotations(),
linkService: {
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([`
<!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;
}
${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 => {
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
}