Skip to content

Commit b9dc342

Browse files
authored
feat: add text layer support to PDF viewer (#237)
1 parent 2181dd7 commit b9dc342

13 files changed

Lines changed: 570 additions & 78 deletions

File tree

packages/discovery-react-components/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
"@storybook/core": "^5.3.21",
5050
"@storybook/react": "^5.3.21",
5151
"@storybook/source-loader": "^5.3.21",
52+
"@types/pdfjs-dist": "2.1.7",
5253
"cross-env": "^7.0.3",
5354
"css-loader": "^3.4.2",
5455
"madge": "^5.0.1",

packages/discovery-react-components/src/components/DocumentPreview/components/PdfViewer/PdfViewer.stories.tsx

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import React from 'react';
22
import { storiesOf } from '@storybook/react';
33
import { withKnobs, radios, number } from '@storybook/addon-knobs';
4+
import { action } from '@storybook/addon-actions';
45
import PdfViewer from './PdfViewer';
56
import { document as doc } from 'components/DocumentPreview/__fixtures__/Art Effects.pdf';
67

@@ -33,5 +34,16 @@ storiesOf('DocumentPreview/components/PdfViewer', module)
3334
const zoom = radios(zoomKnob.label, zoomKnob.options, zoomKnob.defaultValue);
3435
const scale = parseFloat(zoom);
3536

36-
return <PdfViewer file={atob(doc)} page={page} scale={scale} setLoading={(): void => {}} />;
37+
const setLoadingAction = action('setLoading');
38+
const setRenderedTextAction = action('setRenderedText');
39+
40+
return (
41+
<PdfViewer
42+
file={atob(doc)}
43+
page={page}
44+
scale={scale}
45+
setLoading={setLoadingAction}
46+
setRenderedText={setRenderedTextAction}
47+
/>
48+
);
3749
});
Lines changed: 87 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,32 @@
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';
310
import PdfjsWorkerAsText from 'pdfjs-dist/build/pdf.worker.min.js';
411
import { settings } from 'carbon-components';
12+
import useAsyncFunctionCall from 'utils/useAsyncFunctionCall';
13+
import PdfViewerTextLayer, { PdfRenderedText } from './PdfViewerTextLayer';
14+
import { PdfDisplayProps } from './types';
515

616
setupPdfjs();
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

134141
function _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
148158
function 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;
176188
export default PdfViewer;

0 commit comments

Comments
 (0)