-
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 3 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,22 @@ | ||
| import React, { SFC, useEffect, useRef, useState, useMemo } from 'react'; | ||
| import PdfjsLib from 'pdfjs-dist'; | ||
| import React, { FC, useEffect, useRef, useState, useMemo } 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, { PdfTextLayerInfo } from './PdfViewerTextLayer'; | ||
|
|
||
| const { RenderingCancelledException } = PdfjsLib as any; | ||
|
jhpedemonte marked this conversation as resolved.
Outdated
|
||
|
|
||
| setupPdfjs(); | ||
|
|
||
| interface Props { | ||
| className?: string; | ||
|
|
||
| /** | ||
| * PDF file data as base64-encoded string | ||
| */ | ||
|
|
@@ -21,6 +32,16 @@ interface Props { | |
| */ | ||
| scale: number; | ||
|
|
||
| /** | ||
| * Render text layer | ||
| */ | ||
| showTextLayer?: boolean; | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is there a reason for making this an option? Why not always show the text layer when rendering a PDF?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think that we reuse this component to render thumbnail image or tile.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm worried that this would take up a lot of resources, if we need to create many "thumbnails". We'll have to test this out and make sure we don't overwhelm the browser. SDU Annotator has a server-side process to create the thumbnails. I don't believe we will be able to make use of that, but a similar approach may be needed here.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. OK. Let's think about it when we implement thumbnails. With that, our usage is only to display a (full-page) document and I drop the option. |
||
|
|
||
| /** | ||
| * Text layer class name. Only applicable when showTextLayer is true | ||
| */ | ||
| textLayerClassName?: string; | ||
|
|
||
| /** | ||
| * Callback invoked with page count, once `file` has been parsed | ||
| */ | ||
|
|
@@ -33,21 +54,30 @@ interface Props { | |
| * Callback which is invoked with whether to enable/disable toolbar controls | ||
| */ | ||
| setHideToolbarControls?: (disabled: boolean) => void; | ||
| /** | ||
| * Callback for text layer info | ||
| */ | ||
| setTextLayerInfo?: (info: PdfTextLayerInfo | null) => any; | ||
| } | ||
|
|
||
| const PdfViewer: SFC<Props> = ({ | ||
| const PdfViewer: FC<Props> = ({ | ||
| className, | ||
| file, | ||
| page, | ||
| scale, | ||
| showTextLayer, | ||
| textLayerClassName, | ||
| setPageCount, | ||
| setLoading, | ||
| setHideToolbarControls | ||
| setHideToolbarControls, | ||
| setTextLayerInfo, | ||
| 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); | ||
| const [loadedFile, setLoadedFile] = useState<PDFDocumentProxy | null>(null); | ||
| const [loadedPage, setLoadedPage] = useState<PDFPageProxy | null>(null); | ||
|
jhpedemonte marked this conversation as resolved.
Outdated
|
||
|
|
||
| useEffect(() => { | ||
| let didCancel = false; | ||
|
|
@@ -88,33 +118,74 @@ const PdfViewer: SFC<Props> = ({ | |
| }; | ||
| }, [loadedFile, page]); | ||
|
|
||
| const [viewport, canvasInfo] = useMemo(() => { | ||
| const viewport = loadedPage?.getViewport({ scale }); | ||
| const canvasInfo = viewport ? getCanvasInfo(viewport) : undefined; | ||
| return [viewport, canvasInfo]; | ||
| }, [loadedPage, scale]); | ||
| const currentPage = useMemo(() => { | ||
|
jhpedemonte marked this conversation as resolved.
Outdated
|
||
| const isPageValid = !!loadedPage && loadedPage.pageNumber === page; | ||
| if (isPageValid) { | ||
|
jhpedemonte marked this conversation as resolved.
Outdated
|
||
| const viewport = loadedPage?.getViewport({ scale }); | ||
| const canvasInfo = viewport ? getCanvasInfo(viewport) : undefined; | ||
| return { loadedPage, viewport, canvasInfo }; | ||
|
jhpedemonte marked this conversation as resolved.
Outdated
|
||
| } | ||
| return null; | ||
| }, [loadedPage, page, scale]); | ||
|
|
||
| useEffect(() => { | ||
| if (loadedPage && !loadedPage.then && viewport && canvasInfo) { | ||
| _renderPage(loadedPage, canvasRef.current!, viewport, canvasInfo); | ||
| setLoading(false); | ||
| let didCancel = false; | ||
| let task: PDFRenderTask | null = null; | ||
|
|
||
| const { loadedPage, viewport, canvasInfo } = currentPage || {}; | ||
| if (loadedPage && !(loadedPage as any).then && viewport && canvasInfo) { | ||
| const render = async () => { | ||
| try { | ||
| task = _renderPage(loadedPage, canvasRef.current!, viewport, canvasInfo); | ||
| await task?.promise; | ||
| } catch (e) { | ||
| if (e instanceof RenderingCancelledException) { | ||
| // ignore | ||
|
jhpedemonte marked this conversation as resolved.
Outdated
|
||
| } else { | ||
| throw e; // rethrow unknown exception | ||
| } | ||
| } finally { | ||
| if (!didCancel) { | ||
| setLoading(false); | ||
| } | ||
| } | ||
| }; | ||
| render(); | ||
| } | ||
| }, [loadedPage, viewport, canvasInfo, setLoading]); | ||
| return () => { | ||
| didCancel = true; | ||
| task?.cancel(); | ||
|
jhpedemonte marked this conversation as resolved.
Outdated
|
||
| }; | ||
| }, [loadedPage, currentPage, setLoading]); | ||
|
|
||
| useEffect(() => { | ||
| if (setHideToolbarControls) { | ||
| setHideToolbarControls(false); | ||
| } | ||
| }, [setHideToolbarControls]); | ||
|
|
||
| const classNameBase = `${settings.prefix}--document-preview-pdf-viewer`; | ||
| const { loadedPage: currentLoadedPage, canvasInfo } = currentPage || {}; | ||
|
jhpedemonte marked this conversation as resolved.
Outdated
|
||
| 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={currentLoadedPage} | ||
| page={page} | ||
| scale={scale} | ||
| setTextLayerInfo={setTextLayerInfo} | ||
| /> | ||
| )} | ||
| {children} | ||
| </div> | ||
| ); | ||
| }; | ||
|
|
||
|
|
@@ -127,28 +198,31 @@ function _loadPdf(data: string): Promise<any> { | |
| 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; | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.