268 lines
8.1 KiB
TypeScript
268 lines
8.1 KiB
TypeScript
import { captureException } from '@/infrastructure/error-reporter'
|
|
import { generatePdfCachingTransportFactory } from './pdf-caching-transport'
|
|
import { PDFJS, loadPdfDocumentFromUrl, imageResourcesPath } from './pdf-js'
|
|
import {
|
|
PDFViewer,
|
|
EventBus,
|
|
PDFLinkService,
|
|
LinkTarget,
|
|
} from 'pdfjs-dist/web/pdf_viewer.mjs'
|
|
import 'pdfjs-dist/web/pdf_viewer.css'
|
|
import browser from '@/features/source-editor/extensions/browser'
|
|
import { PDFFile } from '@ol-types/compile'
|
|
|
|
const DEFAULT_RANGE_CHUNK_SIZE = 128 * 1024 // 128K chunks
|
|
|
|
export default class PDFJSWrapper {
|
|
public readonly viewer: PDFViewer
|
|
public readonly eventBus: EventBus
|
|
private readonly linkService: PDFLinkService
|
|
private readonly pdfCachingTransportFactory: any
|
|
private url?: string
|
|
|
|
// eslint-disable-next-line no-useless-constructor
|
|
constructor(public container: HTMLDivElement) {
|
|
// create the event bus
|
|
this.eventBus = new EventBus()
|
|
|
|
// create the link service
|
|
this.linkService = new PDFLinkService({
|
|
eventBus: this.eventBus,
|
|
externalLinkTarget: LinkTarget.BLANK,
|
|
externalLinkRel: 'noopener',
|
|
})
|
|
|
|
// create the viewer
|
|
this.viewer = new PDFViewer({
|
|
container: this.container,
|
|
eventBus: this.eventBus,
|
|
imageResourcesPath,
|
|
linkService: this.linkService,
|
|
maxCanvasPixels: browser.safari ? 4096 * 4096 : 8192 * 8192, // default is 4096 * 4096, increased for better resolution at high zoom levels (but not in Safari, which struggles with large canvases)
|
|
annotationMode: PDFJS.AnnotationMode.ENABLE, // enable annotations but not forms
|
|
annotationEditorMode: PDFJS.AnnotationEditorType.DISABLE, // disable annotation editing
|
|
})
|
|
|
|
this.linkService.setViewer(this.viewer)
|
|
|
|
this.pdfCachingTransportFactory = generatePdfCachingTransportFactory()
|
|
}
|
|
|
|
// load a document from a URL
|
|
async loadDocument({
|
|
url,
|
|
pdfFile,
|
|
abortController,
|
|
handleFetchError,
|
|
}: {
|
|
url: string
|
|
pdfFile: PDFFile
|
|
abortController: AbortController
|
|
handleFetchError: (error: any) => void
|
|
}) {
|
|
this.url = url
|
|
|
|
const rangeTransport = this.pdfCachingTransportFactory({
|
|
url,
|
|
pdfFile,
|
|
abortController,
|
|
handleFetchError,
|
|
})
|
|
let rangeChunkSize = DEFAULT_RANGE_CHUNK_SIZE
|
|
if (rangeTransport && pdfFile.size < 2 * DEFAULT_RANGE_CHUNK_SIZE) {
|
|
// pdf.js disables the "bulk" download optimization when providing a
|
|
// custom range transport. Restore it by bumping the chunk size.
|
|
rangeChunkSize = pdfFile.size
|
|
}
|
|
|
|
try {
|
|
const doc = await loadPdfDocumentFromUrl(url, {
|
|
rangeChunkSize,
|
|
range: rangeTransport,
|
|
}).promise
|
|
|
|
// check that this is still the current URL
|
|
if (url !== this.url) {
|
|
return
|
|
}
|
|
|
|
// Hold the .pdfViewer element's height steady across the synchronous page-clear
|
|
// that setDocument() triggers, so the viewer doesn't visually collapse while the
|
|
// new pages are being initialised. The min-height is released on pagesinit.
|
|
const viewerEl = this.container.querySelector('.pdfViewer') as HTMLElement
|
|
const currentHeight = viewerEl.getBoundingClientRect().height
|
|
if (currentHeight > 0) {
|
|
viewerEl.style.minHeight = `${currentHeight}px`
|
|
const clearMinHeight = () => {
|
|
viewerEl.style.minHeight = ''
|
|
this.eventBus.off('pagesinit', clearMinHeight)
|
|
}
|
|
this.eventBus.on('pagesinit', clearMinHeight)
|
|
}
|
|
|
|
const setDocument = () => {
|
|
this.viewer.setDocument(doc)
|
|
this.linkService.setDocument(doc)
|
|
}
|
|
|
|
const container = this.container as typeof this.container & {
|
|
// supported since Chrome 147
|
|
startViewTransition?: (cb: () => void | Promise<void>) => {
|
|
ready: Promise<void>
|
|
}
|
|
}
|
|
|
|
if (
|
|
typeof container.startViewTransition === 'function' &&
|
|
document.visibilityState !== 'hidden'
|
|
) {
|
|
const transition = container.startViewTransition(setDocument)
|
|
|
|
transition.ready.catch(err => {
|
|
// ignore InvalidStateError, it just means the document was hidden
|
|
if (err?.name !== 'InvalidStateError') {
|
|
captureException(err, {
|
|
tags: { handler: 'pdf-preview' },
|
|
})
|
|
}
|
|
})
|
|
} else {
|
|
setDocument()
|
|
}
|
|
|
|
return doc
|
|
} catch (error: any) {
|
|
if (
|
|
!error ||
|
|
!(error instanceof PDFJS.ResponseException && error.missing === true)
|
|
) {
|
|
captureException(error, {
|
|
tags: { handler: 'pdf-preview' },
|
|
})
|
|
}
|
|
|
|
throw error
|
|
}
|
|
}
|
|
|
|
async fetchAllData() {
|
|
await this.viewer.pdfDocument?.getData()
|
|
}
|
|
|
|
// update the current scale value if the container size changes
|
|
updateOnResize() {
|
|
if (!this.isVisible()) {
|
|
return
|
|
}
|
|
|
|
// Use requestAnimationFrame to prevent errors like "ResizeObserver loop
|
|
// completed with undelivered notifications" that can occur if updating the
|
|
// viewer causes another repaint. The cost of this is that the viewer update
|
|
// lags one frame behind, but it's unlikely to matter.
|
|
// Further reading: https://github.com/WICG/resize-observer/issues/38
|
|
window.requestAnimationFrame(() => {
|
|
const currentScaleValue = this.viewer.currentScaleValue
|
|
|
|
if (
|
|
currentScaleValue === 'auto' ||
|
|
currentScaleValue === 'page-fit' ||
|
|
currentScaleValue === 'page-height' ||
|
|
currentScaleValue === 'page-width'
|
|
) {
|
|
this.viewer.currentScaleValue = currentScaleValue
|
|
}
|
|
|
|
this.viewer.update()
|
|
})
|
|
}
|
|
|
|
// get the page and offset of a click event
|
|
clickPosition(event: MouseEvent, canvas: HTMLCanvasElement, page: number) {
|
|
if (!canvas) {
|
|
return
|
|
}
|
|
|
|
const { viewport } = this.viewer.getPageView(page)
|
|
|
|
const pageRect = canvas.getBoundingClientRect()
|
|
|
|
const dx = event.clientX - pageRect.left
|
|
const dy = event.clientY - pageRect.top
|
|
|
|
const [left, top] = viewport.convertToPdfPoint(dx, dy)
|
|
|
|
return {
|
|
page,
|
|
offset: {
|
|
left,
|
|
top: viewport.viewBox[3] - top,
|
|
},
|
|
}
|
|
}
|
|
|
|
// get the current page, offset and page size
|
|
get currentPosition() {
|
|
const containerRect = this.container.getBoundingClientRect()
|
|
|
|
let pageIndex = this.viewer.currentPageNumber - 1
|
|
for (let i = pageIndex; i >= 0; i--) {
|
|
const pageView = this.viewer.getPageView(i)
|
|
if (!pageView?.div) continue
|
|
const pageRect = pageView.div.getBoundingClientRect()
|
|
if (pageRect.bottom < containerRect.top) {
|
|
pageIndex = i + 1
|
|
break
|
|
}
|
|
pageIndex = i
|
|
}
|
|
|
|
const pageView = this.viewer.getPageView(pageIndex)
|
|
const pageRect = pageView.div.getBoundingClientRect()
|
|
|
|
const dy = containerRect.top - pageRect.top
|
|
const dx = containerRect.left - pageRect.left
|
|
const [left, top] = pageView.viewport.convertToPdfPoint(dx, dy)
|
|
const [, , width, height] = pageView.viewport.viewBox
|
|
|
|
return {
|
|
page: pageIndex,
|
|
offset: { top, left },
|
|
pageSize: { height, width },
|
|
}
|
|
}
|
|
|
|
scrollToPosition(position: Record<string, any>, scale = null) {
|
|
const destArray = [
|
|
null,
|
|
{
|
|
name: 'XYZ', // 'XYZ' = scroll to the given coordinates
|
|
},
|
|
position.offset.left,
|
|
position.offset.top,
|
|
scale,
|
|
]
|
|
|
|
this.viewer.scrollPageIntoView({
|
|
pageNumber: position.page + 1,
|
|
destArray,
|
|
})
|
|
|
|
// scrollPageIntoView aligns PDF content to the container top, ignoring the page margin.
|
|
// For a top-of-document position this leaves scrollTop = marginTop (margin hidden).
|
|
// Snap back to 0 so the margin is visible, but only when we are in that margin band —
|
|
// for any real mid-document scrollTop this condition is false and we leave it untouched.
|
|
const pageIndex = this.viewer.currentPageNumber - 1
|
|
const pageView = this.viewer.getPageView(pageIndex)
|
|
if (pageView) {
|
|
const marginTop = parseFloat(getComputedStyle(pageView.div).marginTop)
|
|
if (this.viewer.container.scrollTop <= marginTop) {
|
|
this.viewer.container.scrollTop = 0
|
|
}
|
|
}
|
|
}
|
|
|
|
isVisible() {
|
|
return this.viewer.container.offsetParent !== null
|
|
}
|
|
}
|