Convert PDF.js code to TypeScript

GitOrigin-RevId: 840bc48816d78497131b927522b4f6f7940bb0c4
This commit is contained in:
Alf Eaton
2024-10-14 11:00:45 +00:00
committed by Copybot
parent be45aec5b5
commit df6147bb4d
11 changed files with 135 additions and 164 deletions
@@ -1,5 +1,7 @@
export function buildHighlightElement(highlight, wrapper) {
const pageView = wrapper.viewer.getPageView(highlight.page - 1)
import { PDFJS } from '@/features/pdf-preview/util/pdf-js'
export function buildHighlightElement(highlight, viewer) {
const pageView = viewer.getPageView(highlight.page - 1)
const viewport = pageView.viewport
@@ -12,7 +14,7 @@ export function buildHighlightElement(highlight, wrapper) {
height - highlight.v + 10, // yMax
])
const [left, top, right, bottom] = wrapper.PDFJS.Util.normalizeRect(rect)
const [left, top, right, bottom] = PDFJS.Util.normalizeRect(rect)
const element = document.createElement('div')
element.style.left = Math.floor(pageView.div.offsetLeft + left) + 'px'
@@ -27,7 +29,7 @@ export function buildHighlightElement(highlight, wrapper) {
element.style.opacity = '0'
element.style.transition = 'opacity 1s'
wrapper.viewer.viewer.append(element)
viewer.viewer?.append(element)
return element
}
@@ -1,6 +1,6 @@
import OError from '@overleaf/o-error'
import { fallbackRequest, fetchRange } from './pdf-caching'
import { captureException } from '../../../infrastructure/error-reporter'
import { captureException } from '@/infrastructure/error-reporter'
import { getPdfCachingMetrics } from './metrics'
import {
cachedUrlLookupEnabled,
@@ -9,16 +9,17 @@ import {
prefetchLargeEnabled,
trackPdfDownloadEnabled,
} from './pdf-caching-flags'
import { isNetworkError } from '../../../utils/isNetworkError'
import { isNetworkError } from '@/utils/isNetworkError'
import { debugConsole } from '@/utils/debugging'
import { PDFJS } from './pdf-js'
// 30 seconds: The shutdown grace period of a clsi pre-emp instance.
const STALE_OUTPUT_REQUEST_THRESHOLD_MS = 30 * 1000
export function generatePdfCachingTransportFactory(PDFJS) {
export function generatePdfCachingTransportFactory() {
// NOTE: The custom transport can be used for tracking download volume.
if (!enablePdfCaching && !trackPdfDownloadEnabled) {
return () => null
return () => undefined
}
const usageScore = new Map()
const cachedUrls = new Map()
@@ -180,7 +181,7 @@ export function generatePdfCachingTransportFactory(PDFJS) {
if (metrics.failedOnce) {
// Disable pdf caching once any fetch request failed.
// Be trigger-happy here until we reached a stable state of the feature.
return null
return undefined
}
// Latency is collected per preview cycle.
metrics.latencyComputeMax = 0
@@ -1,38 +0,0 @@
// To add a new version, copy and adjust one of the `importPDFJS*` functions below,
// add the variant to the "switch" statement, and add to `pdfjsVersions` in webpack.config.js
import 'core-js/stable/global-this' // polyfill for globalThis (used by pdf.js)
import 'core-js/stable/promise/all-settled' // polyfill for Promise.allSettled (used by pdf.js)
import 'core-js/stable/structured-clone' // polyfill for global.StructuredClone (used by pdf.js)
import 'core-js/stable/array/at' // polyfill for Array.prototype.at (used by pdf.js)
import { createWorker } from '@/utils/worker'
async function importPDFJS() {
const cMapUrl = '/js/pdfjs-dist/cmaps/'
const standardFontDataUrl = '/fonts/pdfjs-dist/'
const imageResourcesPath = '/images/pdfjs-dist/'
// ensure that PDF.js is loaded before importing the viewer
const PDFJS = await import('pdfjs-dist/legacy/build/pdf')
const [PDFJSViewer] = await Promise.all([
import('pdfjs-dist/legacy/web/pdf_viewer'),
import('pdfjs-dist/legacy/web/pdf_viewer.css'),
])
createWorker(() => {
PDFJS.GlobalWorkerOptions.workerPort = new Worker(
new URL('pdfjs-dist/legacy/build/pdf.worker.mjs', import.meta.url) // NOTE: .mjs extension
)
})
return {
PDFJS,
PDFJSViewer,
cMapUrl,
imageResourcesPath,
standardFontDataUrl,
}
}
export default importPDFJS()
@@ -1,81 +1,70 @@
import { captureException } from '../../../infrastructure/error-reporter'
import { captureException } from '@/infrastructure/error-reporter'
import { generatePdfCachingTransportFactory } from './pdf-caching-transport'
import { debugConsole } from '@/utils/debugging'
const params = new URLSearchParams(window.location.search)
const disableFontFace = params.get('disable-font-face') === 'true'
const disableStream = process.env.NODE_ENV !== 'test'
import { PDFJS, loadPdfDocumentFromUrl, imageResourcesPath } from './pdf-js'
import {
PDFViewer,
EventBus,
PDFLinkService,
LinkTarget,
} from 'pdfjs-dist/legacy/web/pdf_viewer.mjs'
import 'pdfjs-dist/legacy/web/pdf_viewer.css'
const DEFAULT_RANGE_CHUNK_SIZE = 128 * 1024 // 128K chunks
export default class PDFJSWrapper {
constructor(container) {
this.container = container
}
private loadDocumentTask: PDFJS.PDFDocumentLoadingTask | undefined
async init() {
const {
PDFJS,
PDFJSViewer,
cMapUrl,
imageResourcesPath,
standardFontDataUrl,
} = await import('./pdf-js-versions').then(m => {
return m.default
})
this.PDFJS = PDFJS
this.genPdfCachingTransport = generatePdfCachingTransportFactory(PDFJS)
this.PDFJSViewer = PDFJSViewer
this.cMapUrl = cMapUrl
this.standardFontDataUrl = standardFontDataUrl
this.imageResourcesPath = imageResourcesPath
public readonly viewer: PDFViewer
public readonly eventBus: EventBus
private readonly linkService: PDFLinkService
// eslint-disable-next-line no-useless-constructor
constructor(public container: HTMLDivElement) {
// create the event bus
const eventBus = new PDFJSViewer.EventBus()
this.eventBus = new EventBus()
// create the link service
const linkService = new PDFJSViewer.PDFLinkService({
eventBus,
externalLinkTarget: 2,
this.linkService = new PDFLinkService({
eventBus: this.eventBus,
externalLinkTarget: LinkTarget.BLANK,
externalLinkRel: 'noopener',
})
// create the localization
// const l10n = new PDFJSViewer.GenericL10n('en-GB') // TODO: locale mapping?
// create the viewer
const viewer = new PDFJSViewer.PDFViewer({
this.viewer = new PDFViewer({
container: this.container,
eventBus,
eventBus: this.eventBus,
imageResourcesPath,
linkService,
// l10n, // commented out since it currently breaks `aria-label` rendering in pdf pages
enableScripting: false, // default is false, but set explicitly to be sure
enableXfa: false, // default is false (2021-10-12), but set explicitly to be sure
renderInteractiveForms: false,
linkService: this.linkService,
maxCanvasPixels: 8192 * 8192, // default is 4096 * 4096, increased for better resolution at high zoom levels
annotationMode: PDFJS.AnnotationMode?.ENABLE, // enable annotations but not forms
annotationEditorMode: PDFJS.AnnotationEditorType?.DISABLE, // disable annotation editing
annotationMode: PDFJS.AnnotationMode.ENABLE, // enable annotations but not forms
annotationEditorMode: PDFJS.AnnotationEditorType.DISABLE, // disable annotation editing
})
linkService.setViewer(viewer)
this.eventBus = eventBus
this.linkService = linkService
this.viewer = viewer
this.linkService.setViewer(this.viewer)
}
// load a document from a URL
loadDocument({ url, pdfFile, abortController, handleFetchError }) {
loadDocument({
url,
pdfFile,
abortController,
handleFetchError,
}: {
url: string
pdfFile: Record<string, any>
abortController: AbortController
handleFetchError: (error: Error) => void
}) {
// cancel any previous loading task
if (this.loadDocumentTask) {
this.loadDocumentTask.destroy()
this.loadDocumentTask.destroy().catch(debugConsole.error)
this.loadDocumentTask = undefined
}
return new Promise((resolve, reject) => {
const rangeTransport = this.genPdfCachingTransport({
return new Promise<PDFJS.PDFDocumentProxy>((resolve, reject) => {
const rangeTransport = generatePdfCachingTransportFactory()({
url,
pdfFile,
abortController,
@@ -87,35 +76,25 @@ export default class PDFJSWrapper {
// custom range transport. Restore it by bumping the chunk size.
rangeChunkSize = pdfFile.size
}
this.loadDocumentTask = this.PDFJS.getDocument({
url,
cMapUrl: this.cMapUrl,
cMapPacked: true,
standardFontDataUrl: this.standardFontDataUrl,
disableFontFace,
this.loadDocumentTask = loadPdfDocumentFromUrl(url, {
rangeChunkSize,
disableAutoFetch: true,
disableStream,
isEvalSupported: false,
textLayerMode: 2, // PDFJSViewer.TextLayerMode.ENABLE,
range: rangeTransport,
})
this.loadDocumentTask.promise
.then(doc => {
if (!this.loadDocumentTask) {
if (!this.loadDocumentTask || !this.viewer) {
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) {
previousDoc.cleanup().catch(debugConsole.error)
previousDoc.destroy()
previousDoc.destroy().catch(debugConsole.error)
}
})
.catch(error => {
@@ -163,7 +142,7 @@ export default class PDFJSWrapper {
}
// get the page and offset of a click event
clickPosition(event, canvas, page) {
clickPosition(event: MouseEvent, canvas: HTMLCanvasElement, page: number) {
if (!canvas) {
return
}
@@ -205,7 +184,7 @@ export default class PDFJSWrapper {
}
}
scrollToPosition(position, scale = null) {
scrollToPosition(position: Record<string, any>, scale = null) {
const destArray = [
null,
{
@@ -239,13 +218,9 @@ export default class PDFJSWrapper {
this.loadDocumentTask = undefined
}
destroy() {
if (this.loadDocumentTask) {
this.loadDocumentTask.destroy()
this.loadDocumentTask = undefined
}
if (this.viewer) {
this.viewer.destroy()
}
async destroy() {
await this.loadDocumentTask?.destroy().catch(debugConsole.error)
this.loadDocumentTask = undefined
await this.viewer.pdfDocument?.destroy().catch(debugConsole.error)
}
}
@@ -0,0 +1,40 @@
import 'core-js/stable/global-this' // polyfill for globalThis (used by pdf.js)
import 'core-js/stable/promise/all-settled' // polyfill for Promise.allSettled (used by pdf.js)
import 'core-js/stable/structured-clone' // polyfill for global.StructuredClone (used by pdf.js)
import 'core-js/stable/array/at' // polyfill for Array.prototype.at (used by pdf.js)
import { createWorker } from '@/utils/worker'
import * as PDFJS from 'pdfjs-dist/legacy/build/pdf.mjs'
import type { DocumentInitParameters } from 'pdfjs-dist/types/src/display/api'
export { PDFJS }
createWorker(() => {
PDFJS.GlobalWorkerOptions.workerPort = new Worker(
new URL('pdfjs-dist/legacy/build/pdf.worker.mjs', import.meta.url) // NOTE: .mjs extension
)
})
export const imageResourcesPath = '/images/pdfjs-dist/'
const cMapUrl = '/js/pdfjs-dist/cmaps/'
const standardFontDataUrl = '/fonts/pdfjs-dist/'
const params = new URLSearchParams(window.location.search)
const disableFontFace = params.get('disable-font-face') === 'true'
const disableStream = process.env.NODE_ENV !== 'test'
export const loadPdfDocumentFromUrl = (
url: string,
options: Partial<DocumentInitParameters> = {}
) =>
PDFJS.getDocument({
url,
cMapUrl,
standardFontDataUrl,
disableFontFace,
disableAutoFetch: true, // only fetch the data needed for the displayed pages
disableStream,
isEvalSupported: false,
enableXfa: false, // default is false (2021-10-12), but set explicitly to be sure
...options,
})