Convert PDF.js code to TypeScript

GitOrigin-RevId: 840bc48816d78497131b927522b4f6f7940bb0c4
This commit is contained in:
Alf Eaton 2024-09-14 16:01:15 +01:00 committed by Copybot
parent be45aec5b5
commit df6147bb4d
11 changed files with 135 additions and 164 deletions

View file

@ -15,9 +15,9 @@ const FileViewPdf: FC<{
const handleContainer = useCallback(
async (element: HTMLDivElement | null) => {
if (element) {
const { PDFJS } = await import(
'../../pdf-preview/util/pdf-js-versions'
).then(m => m.default as any)
const { loadPdfDocumentFromUrl } = await import(
'@/features/pdf-preview/util/pdf-js'
)
// bail out if loading PDF.js took too long
if (!mountedRef.current) {
@ -33,10 +33,7 @@ const FileViewPdf: FC<{
return
}
const pdf = await PDFJS.getDocument({
url: preview.url,
isEvalSupported: false,
}).promise
const pdf = await loadPdfDocumentFromUrl(preview.url).promise
// bail out if loading the PDF took too long
if (!mountedRef.current) {
@ -61,7 +58,7 @@ const FileViewPdf: FC<{
element.append(canvas)
page.render({
canvasContext: canvas.getContext('2d'),
canvasContext: canvas.getContext('2d')!,
viewport,
})
}

View file

@ -1,11 +1,4 @@
import {
memo,
MouseEventHandler,
useCallback,
useEffect,
useRef,
useState,
} from 'react'
import { memo, useCallback, useEffect, useRef, useState } from 'react'
import { debounce, throttle } from 'lodash'
import PdfViewerControlsToolbar from './pdf-viewer-controls-toolbar'
import { useProjectContext } from '../../../shared/context/project-context'
@ -22,6 +15,7 @@ import { debugConsole } from '@/utils/debugging'
import { usePdfPreviewContext } from '@/features/pdf-preview/components/pdf-preview-provider'
import usePresentationMode from '../hooks/use-presentation-mode'
import useMouseWheelZoom from '../hooks/use-mouse-wheel-zoom'
import { PDFJS } from '../util/pdf-js'
type PdfJsViewerProps = {
url: string
@ -70,20 +64,18 @@ function PdfJsViewer({ url, pdfFile }: PdfJsViewerProps) {
const handleContainer = useCallback(
parent => {
if (parent) {
const wrapper = new PDFJSWrapper(parent.firstChild)
wrapper
.init()
.then(() => {
let wrapper: PDFJSWrapper | undefined
try {
wrapper = new PDFJSWrapper(parent.firstChild)
setPdfJsWrapper(wrapper)
})
.catch(error => {
} catch (error) {
setLoadingError(true)
captureException(error)
})
}
return () => {
setPdfJsWrapper(null)
wrapper.destroy()
wrapper?.destroy().catch(debugConsole.error)
}
}
},
@ -180,7 +172,7 @@ function PdfJsViewer({ url, pdfFile }: PdfJsViewerProps) {
const handleFetchError = (err: Error) => {
if (abortController.signal.aborted) return
// The error is already logged at the call-site with additional context.
if (err instanceof pdfJsWrapper.PDFJS.MissingPDFException) {
if (err instanceof PDFJS.MissingPDFException) {
setError('rendering-error-expected')
} else {
setError('rendering-error')
@ -254,7 +246,7 @@ function PdfJsViewer({ url, pdfFile }: PdfJsViewerProps) {
if (!textLayerDiv.dataset.listeningForDoubleClick) {
textLayerDiv.dataset.listeningForDoubleClick = true
const doubleClickListener: MouseEventHandler = event => {
const doubleClickListener = (event: MouseEvent) => {
const clickPosition = pdfJsWrapper.clickPosition(
event,
textLayerDiv.closest('.page').querySelector('canvas'),
@ -355,7 +347,7 @@ function PdfJsViewer({ url, pdfFile }: PdfJsViewerProps) {
for (const highlight of highlights) {
try {
const element = buildHighlightElement(highlight, pdfJsWrapper)
const element = buildHighlightElement(highlight, pdfJsWrapper.viewer)
elements.push(element)
intersectionObserver.observe(element)
} catch (error) {

View file

@ -1,4 +1,4 @@
import { memo, Suspense } from 'react'
import { ElementType, memo, Suspense } from 'react'
import classNames from 'classnames'
import PdfLogsViewer from './pdf-logs-viewer'
import PdfViewer from './pdf-viewer'
@ -10,7 +10,10 @@ import CompileTimeWarningUpgradePrompt from './compile-time-warning-upgrade-prom
import { PdfPreviewProvider } from './pdf-preview-provider'
import importOverleafModules from '../../../../macros/import-overleaf-module.macro'
const pdfPreviewPromotions = importOverleafModules('pdfPreviewPromotions')
const pdfPreviewPromotions = importOverleafModules('pdfPreviewPromotions') as {
import: { default: ElementType }
path: string
}[]
function PdfPreviewPane() {
const { pdfUrl, hasShortCompileTimeout } = useCompileContext()

View file

@ -1,6 +1,7 @@
import { useCallback, useEffect, useRef, useState } from 'react'
import PDFJSWrapper from '../util/pdf-js-wrapper'
import { sendMB } from '@/infrastructure/event-tracking'
import { debugConsole } from '@/utils/debugging'
type StoredPDFState = {
scrollMode?: number
@ -116,7 +117,9 @@ export default function usePresentationMode(
sendMB('pdf-viewer-enter-presentation-mode')
if (pdfJsWrapper) {
pdfJsWrapper.container.parentNode.requestFullscreen()
pdfJsWrapper.container.parentElement
?.requestFullscreen()
.catch(debugConsole.error)
}
}, [pdfJsWrapper])
@ -136,8 +139,8 @@ export default function usePresentationMode(
const handleExitFullscreen = useCallback(() => {
if (pdfJsWrapper) {
pdfJsWrapper.viewer.scrollMode = storedState.current.scrollMode
pdfJsWrapper.viewer.spreadMode = storedState.current.spreadMode
pdfJsWrapper.viewer.scrollMode = storedState.current.scrollMode!
pdfJsWrapper.viewer.spreadMode = storedState.current.spreadMode!
if (storedState.current.currentScaleValue !== undefined) {
setScale(storedState.current.currentScaleValue)

View file

@ -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
}

View file

@ -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

View file

@ -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()

View file

@ -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()
async destroy() {
await this.loadDocumentTask?.destroy().catch(debugConsole.error)
this.loadDocumentTask = undefined
}
if (this.viewer) {
this.viewer.destroy()
}
await this.viewer.pdfDocument?.destroy().catch(debugConsole.error)
}
}

View file

@ -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,
})

View file

@ -140,14 +140,16 @@ export class GraphicsWidget extends WidgetType {
}
async renderPDF(view: EditorView, canvas: HTMLCanvasElement, url: string) {
const { PDFJS } = await this.importPDFJS()
const { loadPdfDocumentFromUrl } = await import(
'@/features/pdf-preview/util/pdf-js'
)
// bail out if loading PDF.js took too long
if (this.destroyed) {
return
}
const pdf = await PDFJS.getDocument({ url, isEvalSupported: false }).promise
const pdf = await loadPdfDocumentFromUrl(url).promise
const page = await pdf.getPage(1)
// bail out if loading the PDF took too long
@ -162,16 +164,10 @@ export class GraphicsWidget extends WidgetType {
canvas.style.width = width
canvas.style.maxWidth = width
page.render({
canvasContext: canvas.getContext('2d'),
canvasContext: canvas.getContext('2d')!,
viewport,
})
this.height = viewport.height
view.requestMeasure()
}
async importPDFJS(): Promise<any> {
return import('../../../../pdf-preview/util/pdf-js-versions').then(
m => m.default
)
}
}