books-ui: library, detail page, EPUB/PDF reader, reading status/rating/sessions migration, progress/notes APIs, owner gating and tests
This commit is contained in:
180
static/vendor/foliate-js/pdf.js
vendored
Normal file
180
static/vendor/foliate-js/pdf.js
vendored
Normal file
@@ -0,0 +1,180 @@
|
||||
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([`
|
||||
<!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;
|
||||
}
|
||||
/*
|
||||
https://github.com/mozilla/pdf.js/commit/bd05b255fabfc313b194bfe9a17ccded4d90fb5a
|
||||
*/
|
||||
:root {
|
||||
--user-unit: 1;
|
||||
--total-scale-factor: calc(var(--scale-factor) * var(--user-unit));
|
||||
--scale-round-x: 1px;
|
||||
--scale-round-y: 1px;
|
||||
}
|
||||
${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 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
|
||||
}
|
||||
Reference in New Issue
Block a user