Преглед изворни кода

Convert PDF.js code to TypeScript

GitOrigin-RevId: 840bc48816d78497131b927522b4f6f7940bb0c4
Alf Eaton пре 1 година
родитељ
комит
df6147bb4d

+ 5 - 8
services/web/frontend/js/features/file-view/components/file-view-pdf.tsx

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

+ 14 - 22
services/web/frontend/js/features/pdf-preview/components/pdf-js-viewer.tsx

@@ -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(() => {
-            setPdfJsWrapper(wrapper)
-          })
-          .catch(error => {
-            setLoadingError(true)
-            captureException(error)
-          })
+        let wrapper: PDFJSWrapper | undefined
+        try {
+          wrapper = new PDFJSWrapper(parent.firstChild)
+          setPdfJsWrapper(wrapper)
+        } 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) {

+ 5 - 2
services/web/frontend/js/features/pdf-preview/components/pdf-preview-pane.jsx → services/web/frontend/js/features/pdf-preview/components/pdf-preview-pane.tsx

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

+ 0 - 0
services/web/frontend/js/features/pdf-preview/components/pdf-viewer.jsx → services/web/frontend/js/features/pdf-preview/components/pdf-viewer.tsx


+ 6 - 3
services/web/frontend/js/features/pdf-preview/hooks/use-presentation-mode.ts

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

+ 6 - 4
services/web/frontend/js/features/pdf-preview/util/highlights.js

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

+ 6 - 5
services/web/frontend/js/features/pdf-preview/util/pdf-caching-transport.js

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

+ 0 - 38
services/web/frontend/js/features/pdf-preview/util/pdf-js-versions.js

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

+ 48 - 73
services/web/frontend/js/features/pdf-preview/util/pdf-js-wrapper.js → services/web/frontend/js/features/pdf-preview/util/pdf-js-wrapper.ts

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

+ 40 - 0
services/web/frontend/js/features/pdf-preview/util/pdf-js.ts

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

+ 5 - 9
services/web/frontend/js/features/source-editor/extensions/visual/visual-widgets/graphics.ts

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