-
Notifications
You must be signed in to change notification settings - Fork 33
feat: add text layer support to PDF viewer #237
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 11 commits
5e1902c
824c2d7
c5e09cb
9f0dcd5
cc06446
de5731f
f64fa07
85e3538
3c08df9
c722221
69ff042
17feb71
158b8a2
0705d2e
9e9cad5
6ba0703
052336a
e081c92
68d895d
8c82fae
2c28bd9
032842c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,11 +1,21 @@ | ||
| import React, { SFC, useEffect, useRef, useState, useMemo } from 'react'; | ||
| import PdfjsLib from 'pdfjs-dist'; | ||
| import React, { FC, useEffect, useRef, useMemo, useCallback } from 'react'; | ||
| import cx from 'classnames'; | ||
| import PdfjsLib, { | ||
| PDFDocumentProxy, | ||
| PDFPageProxy, | ||
| PDFPageViewport, | ||
| PDFRenderTask | ||
| } from 'pdfjs-dist'; | ||
| import PdfjsWorkerAsText from 'pdfjs-dist/build/pdf.worker.min.js'; | ||
| import { settings } from 'carbon-components'; | ||
| import PdfViewerTextLayer, { PdfRenderedText } from './PdfViewerTextLayer'; | ||
| import useAsyncFunctionCall from './useAsyncFunctionCall'; | ||
|
|
||
| setupPdfjs(); | ||
|
|
||
| interface Props { | ||
| className?: string; | ||
|
|
||
| /** | ||
| * PDF file data as base64-encoded string | ||
| */ | ||
|
|
@@ -21,6 +31,16 @@ interface Props { | |
| */ | ||
| scale: number; | ||
|
|
||
| /** | ||
| * Render text layer | ||
| */ | ||
| showTextLayer?: boolean; | ||
|
||
|
|
||
| /** | ||
| * Text layer class name. Only applicable when showTextLayer is true | ||
| */ | ||
| textLayerClassName?: string; | ||
|
|
||
| /** | ||
| * Callback invoked with page count, once `file` has been parsed | ||
| */ | ||
|
|
@@ -33,88 +53,91 @@ interface Props { | |
| * Callback which is invoked with whether to enable/disable toolbar controls | ||
| */ | ||
| setHideToolbarControls?: (disabled: boolean) => void; | ||
| /** | ||
| * Callback for text layer info | ||
| */ | ||
| setRenderedText?: (info: PdfRenderedText | null) => any; | ||
| } | ||
|
|
||
| const PdfViewer: SFC<Props> = ({ | ||
| const PdfViewer: FC<Props> = ({ | ||
| className, | ||
| file, | ||
| page, | ||
| scale, | ||
| showTextLayer, | ||
| textLayerClassName, | ||
| setPageCount, | ||
| setLoading, | ||
| setHideToolbarControls | ||
| setHideToolbarControls, | ||
| setRenderedText, | ||
| children | ||
| }) => { | ||
| const canvasRef = useRef<HTMLCanvasElement>(null); | ||
|
|
||
| // In order to prevent unnecessary re-loading, loaded file and page are stored in state | ||
| const [loadedFile, setLoadedFile] = useState<any>(null); | ||
| const [loadedPage, setLoadedPage] = useState<any>(null); | ||
|
|
||
| useEffect(() => { | ||
| let didCancel = false; | ||
|
|
||
| async function loadPdf(): Promise<void> { | ||
| if (file) { | ||
| const newPdf = await _loadPdf(file); | ||
| if (!didCancel) { | ||
| setLoadedFile(newPdf); | ||
| if (setPageCount) { | ||
| setPageCount(newPdf.numPages); | ||
| } | ||
| } | ||
| } | ||
| } | ||
| loadPdf(); | ||
|
|
||
| return (): void => { | ||
| didCancel = true; | ||
| }; | ||
| }, [file, setPageCount]); | ||
|
|
||
| useEffect(() => { | ||
| let didCancel = false; | ||
|
|
||
| async function loadPage(): Promise<void> { | ||
| if (loadedFile && page > 0) { | ||
| const newPage = await _loadPage(loadedFile, page); | ||
| if (!didCancel) { | ||
| setLoadedPage(newPage); | ||
| } | ||
| } | ||
| } | ||
| loadPage(); | ||
|
|
||
| return (): void => { | ||
| didCancel = true; | ||
| }; | ||
| }, [loadedFile, page]); | ||
| const loadedFile = useAsyncFunctionCall( | ||
| useCallback(async () => (file ? await _loadPdf(file) : null), [file]) | ||
| ); | ||
| const loadedPage = useAsyncFunctionCall( | ||
| useCallback( | ||
| async () => (loadedFile && page > 0 ? await _loadPage(loadedFile, page) : null), | ||
| [loadedFile, page] | ||
| ) | ||
| ); | ||
jhpedemonte marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| const [viewport, canvasInfo] = useMemo(() => { | ||
| const viewport = loadedPage?.getViewport({ scale }); | ||
| const canvasInfo = viewport ? getCanvasInfo(viewport) : undefined; | ||
| return [viewport, canvasInfo]; | ||
| }, [loadedPage, scale]); | ||
|
|
||
| // render page | ||
| useAsyncFunctionCall( | ||
| useCallback( | ||
| async (abortSignal: AbortSignal) => { | ||
| if (loadedPage && !(loadedPage as any).then && viewport && canvasInfo) { | ||
| const task = _renderPage(loadedPage, canvasRef.current!, viewport, canvasInfo); | ||
| abortSignal.addEventListener('abort', () => task?.cancel()); | ||
| await task?.promise; | ||
|
|
||
| setLoading(false); | ||
| } | ||
| }, | ||
| [canvasInfo, loadedPage, setLoading, viewport] | ||
| ) | ||
| ); | ||
|
|
||
| useEffect(() => { | ||
| if (loadedPage && !loadedPage.then && viewport && canvasInfo) { | ||
| _renderPage(loadedPage, canvasRef.current!, viewport, canvasInfo); | ||
| setLoading(false); | ||
| if (setPageCount && loadedFile) { | ||
| setPageCount(loadedFile.numPages); | ||
| } | ||
| }, [loadedPage, viewport, canvasInfo, setLoading]); | ||
| }, [loadedFile, setPageCount]); | ||
|
|
||
| useEffect(() => { | ||
| if (setHideToolbarControls) { | ||
| setHideToolbarControls(false); | ||
| } | ||
| }, [setHideToolbarControls]); | ||
|
|
||
| const classNameBase = `${settings.prefix}--document-preview-pdf-viewer`; | ||
| return ( | ||
| <canvas | ||
| ref={canvasRef} | ||
| className={`${settings.prefix}--document-preview-pdf-viewer`} | ||
| style={{ width: `${canvasInfo?.width ?? 0}px`, height: `${canvasInfo?.height ?? 0}px` }} | ||
| width={canvasInfo?.canvasWidth} | ||
| height={canvasInfo?.canvasHeight} | ||
| /> | ||
| <div className={cx(classNameBase, className)}> | ||
| <canvas | ||
| ref={canvasRef} | ||
| className={`${classNameBase}--canvas`} | ||
| style={{ width: `${canvasInfo?.width ?? 0}px`, height: `${canvasInfo?.height ?? 0}px` }} | ||
| width={canvasInfo?.canvasWidth} | ||
| height={canvasInfo?.canvasHeight} | ||
| /> | ||
| {showTextLayer && ( | ||
| <PdfViewerTextLayer | ||
| className={cx(`${classNameBase}--text`, textLayerClassName)} | ||
| loadedPage={loadedPage} | ||
| scale={scale} | ||
| setRenderedText={setRenderedText} | ||
| /> | ||
| )} | ||
| {children} | ||
| </div> | ||
| ); | ||
| }; | ||
|
|
||
|
|
@@ -123,32 +146,35 @@ PdfViewer.defaultProps = { | |
| scale: 1 | ||
| }; | ||
|
|
||
| function _loadPdf(data: string): Promise<any> { | ||
| function _loadPdf(data: string): Promise<PDFDocumentProxy> { | ||
| return PdfjsLib.getDocument({ data }).promise; | ||
| } | ||
|
|
||
| function _loadPage(file: any, page: number): Promise<any> { | ||
| function _loadPage(file: PDFDocumentProxy, page: number) { | ||
| return file.getPage(page); | ||
| } | ||
|
|
||
| function _renderPage( | ||
| pdfPage: any, | ||
| pdfPage: PDFPageProxy, | ||
| canvas: HTMLCanvasElement, | ||
| viewport: any, | ||
| viewport: PDFPageViewport, | ||
| canvasInfo: CanvasInfo | ||
| ): void { | ||
| ): PDFRenderTask | null { | ||
| const canvasContext = canvas.getContext('2d'); | ||
| canvasContext?.resetTransform(); | ||
| canvasContext?.scale(canvasInfo.canvasScale, canvasInfo.canvasScale); | ||
| pdfPage.render({ canvasContext, viewport }); | ||
| if (canvasContext) { | ||
| canvasContext.resetTransform(); | ||
| canvasContext.scale(canvasInfo.canvasScale, canvasInfo.canvasScale); | ||
| return pdfPage.render({ canvasContext, viewport }); | ||
| } | ||
| return null; | ||
| } | ||
|
|
||
| // set up web worker for use by PDF.js library | ||
| // @see https://stackoverflow.com/a/6454685/908343 | ||
| function setupPdfjs(): void { | ||
| if (typeof Worker !== 'undefined') { | ||
| const blob = new Blob([PdfjsWorkerAsText], { type: 'text/javascript' }); | ||
| const pdfjsWorker = new Worker(URL.createObjectURL(blob)); | ||
| const pdfjsWorker = new Worker(URL.createObjectURL(blob)) as any; | ||
| PdfjsLib.GlobalWorkerOptions.workerPort = pdfjsWorker; | ||
| } else { | ||
| PdfjsLib.GlobalWorkerOptions.workerSrc = PdfjsWorkerAsText; | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It seems that this package is now deprecated, showing: