2024-09-14 11:01:15 -04:00
|
|
|
import { captureException } from '@/infrastructure/error-reporter'
|
2022-07-06 07:06:53 -04:00
|
|
|
import { generatePdfCachingTransportFactory } from './pdf-caching-transport'
|
2023-09-27 05:45:49 -04:00
|
|
|
import { debugConsole } from '@/utils/debugging'
|
2024-09-14 11:01:15 -04:00
|
|
|
import { PDFJS, loadPdfDocumentFromUrl, imageResourcesPath } from './pdf-js'
|
|
|
|
import {
|
|
|
|
PDFViewer,
|
|
|
|
EventBus,
|
|
|
|
PDFLinkService,
|
|
|
|
LinkTarget,
|
2024-09-15 18:01:29 -04:00
|
|
|
} from 'pdfjs-dist/web/pdf_viewer.mjs'
|
|
|
|
import 'pdfjs-dist/web/pdf_viewer.css'
|
2021-09-30 07:29:25 -04:00
|
|
|
|
2022-07-20 04:15:58 -04:00
|
|
|
const DEFAULT_RANGE_CHUNK_SIZE = 128 * 1024 // 128K chunks
|
2021-09-30 07:29:25 -04:00
|
|
|
|
|
|
|
export default class PDFJSWrapper {
|
2024-09-14 11:01:15 -04:00
|
|
|
private loadDocumentTask: PDFJS.PDFDocumentLoadingTask | undefined
|
2022-03-18 06:27:29 -04:00
|
|
|
|
2024-09-14 11:01:15 -04:00
|
|
|
public readonly viewer: PDFViewer
|
|
|
|
public readonly eventBus: EventBus
|
|
|
|
private readonly linkService: PDFLinkService
|
2024-10-17 09:29:13 -04:00
|
|
|
private readonly pdfCachingTransportFactory: any
|
2021-09-30 07:29:25 -04:00
|
|
|
|
2024-09-14 11:01:15 -04:00
|
|
|
// eslint-disable-next-line no-useless-constructor
|
|
|
|
constructor(public container: HTMLDivElement) {
|
2021-09-30 07:29:25 -04:00
|
|
|
// create the event bus
|
2024-09-14 11:01:15 -04:00
|
|
|
this.eventBus = new EventBus()
|
2021-09-30 07:29:25 -04:00
|
|
|
|
|
|
|
// create the link service
|
2024-09-14 11:01:15 -04:00
|
|
|
this.linkService = new PDFLinkService({
|
|
|
|
eventBus: this.eventBus,
|
|
|
|
externalLinkTarget: LinkTarget.BLANK,
|
2021-09-30 07:29:25 -04:00
|
|
|
externalLinkRel: 'noopener',
|
|
|
|
})
|
|
|
|
|
|
|
|
// create the viewer
|
2024-09-14 11:01:15 -04:00
|
|
|
this.viewer = new PDFViewer({
|
2022-03-18 06:27:29 -04:00
|
|
|
container: this.container,
|
2024-09-14 11:01:15 -04:00
|
|
|
eventBus: this.eventBus,
|
2021-09-30 07:29:25 -04:00
|
|
|
imageResourcesPath,
|
2024-09-14 11:01:15 -04:00
|
|
|
linkService: this.linkService,
|
2022-03-25 05:31:29 -04:00
|
|
|
maxCanvasPixels: 8192 * 8192, // default is 4096 * 4096, increased for better resolution at high zoom levels
|
2024-09-14 11:01:15 -04:00
|
|
|
annotationMode: PDFJS.AnnotationMode.ENABLE, // enable annotations but not forms
|
|
|
|
annotationEditorMode: PDFJS.AnnotationEditorType.DISABLE, // disable annotation editing
|
2021-09-30 07:29:25 -04:00
|
|
|
})
|
|
|
|
|
2024-09-14 11:01:15 -04:00
|
|
|
this.linkService.setViewer(this.viewer)
|
2024-10-17 09:29:13 -04:00
|
|
|
|
|
|
|
this.pdfCachingTransportFactory = generatePdfCachingTransportFactory()
|
2021-09-30 07:29:25 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
// load a document from a URL
|
2024-09-14 11:01:15 -04:00
|
|
|
loadDocument({
|
|
|
|
url,
|
|
|
|
pdfFile,
|
|
|
|
abortController,
|
|
|
|
handleFetchError,
|
|
|
|
}: {
|
|
|
|
url: string
|
|
|
|
pdfFile: Record<string, any>
|
|
|
|
abortController: AbortController
|
|
|
|
handleFetchError: (error: Error) => void
|
|
|
|
}) {
|
2021-10-15 05:52:07 -04:00
|
|
|
// cancel any previous loading task
|
|
|
|
if (this.loadDocumentTask) {
|
2024-09-14 11:01:15 -04:00
|
|
|
this.loadDocumentTask.destroy().catch(debugConsole.error)
|
2021-10-15 05:52:07 -04:00
|
|
|
this.loadDocumentTask = undefined
|
|
|
|
}
|
2021-10-06 04:33:11 -04:00
|
|
|
|
2024-09-14 11:01:15 -04:00
|
|
|
return new Promise<PDFJS.PDFDocumentProxy>((resolve, reject) => {
|
2024-10-17 09:29:13 -04:00
|
|
|
const rangeTransport = this.pdfCachingTransportFactory({
|
2022-08-01 07:16:29 -04:00
|
|
|
url,
|
|
|
|
pdfFile,
|
|
|
|
abortController,
|
|
|
|
handleFetchError,
|
|
|
|
})
|
2022-07-20 04:15:58 -04:00
|
|
|
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
|
|
|
|
}
|
2024-09-14 11:01:15 -04:00
|
|
|
this.loadDocumentTask = loadPdfDocumentFromUrl(url, {
|
2021-10-06 04:33:11 -04:00
|
|
|
rangeChunkSize,
|
2022-07-20 04:15:58 -04:00
|
|
|
range: rangeTransport,
|
2021-10-06 04:33:11 -04:00
|
|
|
})
|
|
|
|
|
|
|
|
this.loadDocumentTask.promise
|
|
|
|
.then(doc => {
|
2024-09-14 11:01:15 -04:00
|
|
|
if (!this.loadDocumentTask || !this.viewer) {
|
2021-10-06 04:33:11 -04:00
|
|
|
return // ignoring the response since loading task has been aborted
|
|
|
|
}
|
|
|
|
|
|
|
|
const previousDoc = this.viewer.pdfDocument
|
|
|
|
this.viewer.setDocument(doc)
|
|
|
|
this.linkService.setDocument(doc)
|
|
|
|
resolve(doc)
|
|
|
|
|
|
|
|
if (previousDoc) {
|
2023-09-27 05:45:49 -04:00
|
|
|
previousDoc.cleanup().catch(debugConsole.error)
|
2024-09-14 11:01:15 -04:00
|
|
|
previousDoc.destroy().catch(debugConsole.error)
|
2021-10-06 04:33:11 -04:00
|
|
|
}
|
|
|
|
})
|
|
|
|
.catch(error => {
|
|
|
|
if (this.loadDocumentTask) {
|
2021-11-03 10:29:54 -04:00
|
|
|
if (!error || error.name !== 'MissingPDFException') {
|
2022-08-18 04:17:12 -04:00
|
|
|
captureException(error, {
|
|
|
|
tags: { handler: 'pdf-preview' },
|
|
|
|
})
|
2021-10-15 05:51:27 -04:00
|
|
|
}
|
|
|
|
|
2021-10-06 04:33:11 -04:00
|
|
|
reject(error)
|
|
|
|
}
|
|
|
|
})
|
|
|
|
.finally(() => {
|
|
|
|
this.loadDocumentTask = undefined
|
|
|
|
})
|
|
|
|
})
|
2021-09-30 07:29:25 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
// update the current scale value if the container size changes
|
|
|
|
updateOnResize() {
|
2022-01-19 06:54:52 -05:00
|
|
|
if (!this.isVisible()) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2023-09-05 10:46:51 -04:00
|
|
|
// 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' ||
|
2024-08-20 09:51:51 -04:00
|
|
|
currentScaleValue === 'page-height' ||
|
2023-09-05 10:46:51 -04:00
|
|
|
currentScaleValue === 'page-width'
|
|
|
|
) {
|
|
|
|
this.viewer.currentScaleValue = currentScaleValue
|
|
|
|
}
|
2021-09-30 07:29:25 -04:00
|
|
|
|
2023-09-05 10:46:51 -04:00
|
|
|
this.viewer.update()
|
|
|
|
})
|
2021-09-30 07:29:25 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
// get the page and offset of a click event
|
2024-09-14 11:01:15 -04:00
|
|
|
clickPosition(event: MouseEvent, canvas: HTMLCanvasElement, page: number) {
|
2024-07-29 08:29:59 -04:00
|
|
|
if (!canvas) {
|
2022-08-04 05:39:31 -04:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2024-07-29 08:29:59 -04:00
|
|
|
const { viewport } = this.viewer.getPageView(page)
|
|
|
|
|
|
|
|
const pageRect = canvas.getBoundingClientRect()
|
2021-09-30 07:29:25 -04:00
|
|
|
|
|
|
|
const dx = event.clientX - pageRect.left
|
|
|
|
const dy = event.clientY - pageRect.top
|
|
|
|
|
|
|
|
const [left, top] = viewport.convertToPdfPoint(dx, dy)
|
|
|
|
|
|
|
|
return {
|
2024-07-29 08:29:59 -04:00
|
|
|
page,
|
2021-09-30 07:29:25 -04:00
|
|
|
offset: {
|
|
|
|
left,
|
|
|
|
top: viewport.viewBox[3] - top,
|
|
|
|
},
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// get the current page, offset and page size
|
|
|
|
get currentPosition() {
|
|
|
|
const pageIndex = this.viewer.currentPageNumber - 1
|
|
|
|
const pageView = this.viewer.getPageView(pageIndex)
|
|
|
|
const pageRect = pageView.div.getBoundingClientRect()
|
|
|
|
|
|
|
|
const containerRect = this.container.getBoundingClientRect()
|
|
|
|
const dy = containerRect.top - pageRect.top
|
2021-12-07 04:19:10 -05:00
|
|
|
const dx = containerRect.left - pageRect.left
|
|
|
|
const [left, top] = pageView.viewport.convertToPdfPoint(dx, dy)
|
2021-09-30 07:29:25 -04:00
|
|
|
const [, , width, height] = pageView.viewport.viewBox
|
|
|
|
|
|
|
|
return {
|
|
|
|
page: pageIndex,
|
2021-12-07 04:19:10 -05:00
|
|
|
offset: { top, left },
|
2021-09-30 07:29:25 -04:00
|
|
|
pageSize: { height, width },
|
|
|
|
}
|
|
|
|
}
|
2021-10-06 04:33:11 -04:00
|
|
|
|
2024-09-14 11:01:15 -04:00
|
|
|
scrollToPosition(position: Record<string, any>, scale = null) {
|
2021-10-15 05:39:56 -04:00
|
|
|
const destArray = [
|
|
|
|
null,
|
|
|
|
{
|
|
|
|
name: 'XYZ', // 'XYZ' = scroll to the given coordinates
|
|
|
|
},
|
|
|
|
position.offset.left,
|
|
|
|
position.offset.top,
|
2021-10-21 06:31:51 -04:00
|
|
|
scale,
|
2021-10-15 05:39:56 -04:00
|
|
|
]
|
|
|
|
|
|
|
|
this.viewer.scrollPageIntoView({
|
|
|
|
pageNumber: position.page + 1,
|
|
|
|
destArray,
|
|
|
|
})
|
2021-12-02 06:59:41 -05:00
|
|
|
|
2023-03-16 06:35:04 -04:00
|
|
|
// scroll the page left and down by an extra few pixels to account for the pdf.js viewer page border
|
|
|
|
const pageIndex = this.viewer.currentPageNumber - 1
|
|
|
|
const pageView = this.viewer.getPageView(pageIndex)
|
|
|
|
const offset = parseFloat(getComputedStyle(pageView.div).borderWidth)
|
2021-12-02 06:59:41 -05:00
|
|
|
this.viewer.container.scrollBy({
|
2023-03-16 06:35:04 -04:00
|
|
|
top: -offset,
|
|
|
|
left: -offset,
|
2021-12-02 06:59:41 -05:00
|
|
|
})
|
2021-10-15 05:39:56 -04:00
|
|
|
}
|
|
|
|
|
2022-01-10 08:58:31 -05:00
|
|
|
isVisible() {
|
|
|
|
return this.viewer.container.offsetParent !== null
|
|
|
|
}
|
|
|
|
|
2021-10-06 04:33:11 -04:00
|
|
|
abortDocumentLoading() {
|
|
|
|
this.loadDocumentTask = undefined
|
|
|
|
}
|
|
|
|
|
2024-09-14 11:01:15 -04:00
|
|
|
async destroy() {
|
|
|
|
await this.loadDocumentTask?.destroy().catch(debugConsole.error)
|
|
|
|
this.loadDocumentTask = undefined
|
|
|
|
await this.viewer.pdfDocument?.destroy().catch(debugConsole.error)
|
2021-10-06 04:33:11 -04:00
|
|
|
}
|
2021-09-30 07:29:25 -04:00
|
|
|
}
|