1- import React , { SFC , useEffect , useRef , useState , useMemo } from 'react' ;
2- import PdfjsLib from 'pdfjs-dist' ;
1+ import React , { FC , useEffect , useRef , useMemo , useCallback } from 'react' ;
2+ import cx from 'classnames' ;
3+ import PdfjsLib , {
4+ PDFDocumentProxy ,
5+ PDFPageProxy ,
6+ PDFPageViewport ,
7+ PDFPromise ,
8+ PDFRenderTask
9+ } from 'pdfjs-dist' ;
310import PdfjsWorkerAsText from 'pdfjs-dist/build/pdf.worker.min.js' ;
411import { settings } from 'carbon-components' ;
12+ import useAsyncFunctionCall from 'utils/useAsyncFunctionCall' ;
13+ import PdfViewerTextLayer , { PdfRenderedText } from './PdfViewerTextLayer' ;
14+ import { PdfDisplayProps } from './types' ;
515
616setupPdfjs ( ) ;
717
8- interface Props {
18+ type Props = PdfDisplayProps & {
19+ className ?: string ;
20+
921 /**
1022 * PDF file data as base64-encoded string
1123 */
1224 file : string ;
1325
1426 /**
15- * Page number, starting at 1
16- */
17- page : number ;
18-
19- /**
20- * Zoom factor, where `1` is equal to 100%
27+ * Text layer class name
2128 */
22- scale : number ;
29+ textLayerClassName ?: string ;
2330
2431 /**
2532 * Callback invoked with page count, once `file` has been parsed
@@ -33,88 +40,88 @@ interface Props {
3340 * Callback which is invoked with whether to enable/disable toolbar controls
3441 */
3542 setHideToolbarControls ?: ( disabled : boolean ) => void ;
36- }
43+ /**
44+ * Callback for text layer info
45+ */
46+ setRenderedText ?: ( info : PdfRenderedText | null ) => any ;
47+ } ;
3748
38- const PdfViewer : SFC < Props > = ( {
49+ const PdfViewer : FC < Props > = ( {
50+ className,
3951 file,
4052 page,
4153 scale,
54+ textLayerClassName,
4255 setPageCount,
4356 setLoading,
44- setHideToolbarControls
57+ setHideToolbarControls,
58+ setRenderedText,
59+ children
4560} ) => {
4661 const canvasRef = useRef < HTMLCanvasElement > ( null ) ;
4762
48- // In order to prevent unnecessary re-loading, loaded file and page are stored in state
49- const [ loadedFile , setLoadedFile ] = useState < any > ( null ) ;
50- const [ loadedPage , setLoadedPage ] = useState < any > ( null ) ;
51-
52- useEffect ( ( ) => {
53- let didCancel = false ;
54-
55- async function loadPdf ( ) : Promise < void > {
56- if ( file ) {
57- const newPdf = await _loadPdf ( file ) ;
58- if ( ! didCancel ) {
59- setLoadedFile ( newPdf ) ;
60- if ( setPageCount ) {
61- setPageCount ( newPdf . numPages ) ;
62- }
63- }
64- }
65- }
66- loadPdf ( ) ;
67-
68- return ( ) : void => {
69- didCancel = true ;
70- } ;
71- } , [ file , setPageCount ] ) ;
72-
73- useEffect ( ( ) => {
74- let didCancel = false ;
75-
76- async function loadPage ( ) : Promise < void > {
77- if ( loadedFile && page > 0 ) {
78- const newPage = await _loadPage ( loadedFile , page ) ;
79- if ( ! didCancel ) {
80- setLoadedPage ( newPage ) ;
81- }
82- }
83- }
84- loadPage ( ) ;
85-
86- return ( ) : void => {
87- didCancel = true ;
88- } ;
89- } , [ loadedFile , page ] ) ;
63+ const loadedFile = useAsyncFunctionCall (
64+ useCallback ( async ( ) => ( file ? await _loadPdf ( file ) : null ) , [ file ] )
65+ ) ;
66+ const loadedPage = useAsyncFunctionCall (
67+ useCallback (
68+ async ( ) => ( loadedFile && page > 0 ? await _loadPage ( loadedFile , page ) : null ) ,
69+ [ loadedFile , page ]
70+ )
71+ ) ;
9072
9173 const [ viewport , canvasInfo ] = useMemo ( ( ) => {
9274 const viewport = loadedPage ?. getViewport ( { scale } ) ;
9375 const canvasInfo = viewport ? getCanvasInfo ( viewport ) : undefined ;
9476 return [ viewport , canvasInfo ] ;
9577 } , [ loadedPage , scale ] ) ;
9678
79+ // render page
80+ useAsyncFunctionCall (
81+ useCallback (
82+ async ( abortSignal : AbortSignal ) => {
83+ if ( loadedPage && ! ( loadedPage as any ) . then && viewport && canvasInfo ) {
84+ const task = _renderPage ( loadedPage , canvasRef . current ! , viewport , canvasInfo ) ;
85+ abortSignal . addEventListener ( 'abort' , ( ) => task ?. cancel ( ) ) ;
86+ await task ?. promise ;
87+
88+ setLoading ( false ) ;
89+ }
90+ } ,
91+ [ canvasInfo , loadedPage , setLoading , viewport ]
92+ )
93+ ) ;
94+
9795 useEffect ( ( ) => {
98- if ( loadedPage && ! loadedPage . then && viewport && canvasInfo ) {
99- _renderPage ( loadedPage , canvasRef . current ! , viewport , canvasInfo ) ;
100- setLoading ( false ) ;
96+ if ( setPageCount && loadedFile ) {
97+ setPageCount ( loadedFile . numPages ) ;
10198 }
102- } , [ loadedPage , viewport , canvasInfo , setLoading ] ) ;
99+ } , [ loadedFile , setPageCount ] ) ;
103100
104101 useEffect ( ( ) => {
105102 if ( setHideToolbarControls ) {
106103 setHideToolbarControls ( false ) ;
107104 }
108105 } , [ setHideToolbarControls ] ) ;
109106
107+ const classNameBase = `${ settings . prefix } --document-preview-pdf-viewer` ;
110108 return (
111- < canvas
112- ref = { canvasRef }
113- className = { `${ settings . prefix } --document-preview-pdf-viewer` }
114- style = { { width : `${ canvasInfo ?. width ?? 0 } px` , height : `${ canvasInfo ?. height ?? 0 } px` } }
115- width = { canvasInfo ?. canvasWidth }
116- height = { canvasInfo ?. canvasHeight }
117- />
109+ < div className = { cx ( classNameBase , className ) } >
110+ < canvas
111+ ref = { canvasRef }
112+ className = { `${ classNameBase } --canvas` }
113+ style = { { width : `${ canvasInfo ?. width ?? 0 } px` , height : `${ canvasInfo ?. height ?? 0 } px` } }
114+ width = { canvasInfo ?. canvasWidth }
115+ height = { canvasInfo ?. canvasHeight }
116+ />
117+ < PdfViewerTextLayer
118+ className = { cx ( `${ classNameBase } --text` , textLayerClassName ) }
119+ loadedPage = { loadedPage }
120+ scale = { scale }
121+ setRenderedText = { setRenderedText }
122+ />
123+ { children }
124+ </ div >
118125 ) ;
119126} ;
120127
@@ -123,32 +130,36 @@ PdfViewer.defaultProps = {
123130 scale : 1
124131} ;
125132
126- function _loadPdf ( data : string ) : Promise < any > {
133+ function _loadPdf ( data : string ) : PDFPromise < PDFDocumentProxy > {
127134 return PdfjsLib . getDocument ( { data } ) . promise ;
128135}
129136
130- function _loadPage ( file : any , page : number ) : Promise < any > {
137+ function _loadPage ( file : PDFDocumentProxy , page : number ) {
131138 return file . getPage ( page ) ;
132139}
133140
134141function _renderPage (
135- pdfPage : any ,
142+ pdfPage : PDFPageProxy ,
136143 canvas : HTMLCanvasElement ,
137- viewport : any ,
144+ viewport : PDFPageViewport ,
138145 canvasInfo : CanvasInfo
139- ) : void {
146+ ) : PDFRenderTask | null {
140147 const canvasContext = canvas . getContext ( '2d' ) ;
141- canvasContext ?. resetTransform ( ) ;
142- canvasContext ?. scale ( canvasInfo . canvasScale , canvasInfo . canvasScale ) ;
143- pdfPage . render ( { canvasContext, viewport } ) ;
148+ if ( canvasContext ) {
149+ canvasContext . resetTransform ( ) ;
150+ canvasContext . scale ( canvasInfo . canvasScale , canvasInfo . canvasScale ) ;
151+ return pdfPage . render ( { canvasContext, viewport } ) ;
152+ }
153+ return null ;
144154}
145155
146156// set up web worker for use by PDF.js library
147157// @see https://stackoverflow.com/a/6454685/908343
148158function setupPdfjs ( ) : void {
149159 if ( typeof Worker !== 'undefined' ) {
150160 const blob = new Blob ( [ PdfjsWorkerAsText ] , { type : 'text/javascript' } ) ;
151- const pdfjsWorker = new Worker ( URL . createObjectURL ( blob ) ) ;
161+ const pdfjsWorker = new Worker ( URL . createObjectURL ( blob ) ) as any ;
162+ // @ts -expect-error Upgrading pdfjs-dist and its typings would resolve the issue
152163 PdfjsLib . GlobalWorkerOptions . workerPort = pdfjsWorker ;
153164 } else {
154165 PdfjsLib . GlobalWorkerOptions . workerSrc = PdfjsWorkerAsText ;
@@ -173,4 +184,5 @@ function getCanvasInfo(viewport: any): CanvasInfo {
173184 return { width, height, canvasWidth, canvasHeight, canvasScale } ;
174185}
175186
187+ export type PdfViewerProps = Props ;
176188export default PdfViewer ;
0 commit comments