mirror of
https://github.com/john-okeefe/foliate-js.git
synced 2026-09-09 11:29:14 -04:00
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.
144 lines
5.2 KiB
JavaScript
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
|
|
}
|