Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
5e1902c
chore: add pdfjs-dist typings
fukudasjp Oct 15, 2021
824c2d7
fix: control PDF rendering tasks properly
fukudasjp Nov 9, 2021
c5e09cb
feat: add pdf text layer support
fukudasjp Oct 20, 2021
9f0dcd5
fix: apply review comments
fukudasjp Nov 16, 2021
cc06446
refactor: extract text rendering hook
fukudasjp Nov 18, 2021
de5731f
refactor: extract async func call hook
fukudasjp Nov 19, 2021
f64fa07
fix: revise how to import css from pdfjs
fukudasjp Nov 24, 2021
85e3538
Merge remote-tracking branch 'upstream/master' into feat/redner-pdf-text
fukudasjp Nov 24, 2021
3c08df9
fix: install @types/pdfjs-dist to yarn2
fukudasjp Nov 24, 2021
c722221
feat: add script to update style
fukudasjp Nov 24, 2021
69ff042
refactor: revise script for importing css
fukudasjp Nov 24, 2021
17feb71
fix: use postcss to manupulate pdfjs-web css
fukudasjp Dec 2, 2021
158b8a2
fix: add comment to the style update script
fukudasjp Dec 2, 2021
0705d2e
refactor: move useAsyncFunctionCall to utils
fukudasjp Dec 2, 2021
9e9cad5
fix: name of package script
fukudasjp Dec 2, 2021
6ba0703
Merge remote-tracking branch 'upstream/master' into feat/redner-pdf-text
fukudasjp Dec 2, 2021
052336a
fix: apply CI comment
fukudasjp Dec 2, 2021
e081c92
fix: fix broken logic
fukudasjp Dec 2, 2021
68d895d
fix: remove unused code
fukudasjp Dec 2, 2021
8c82fae
fix: apply review comments around pdfjs css
fukudasjp Dec 3, 2021
2c28bd9
fix: pdfjs typings version
fukudasjp Dec 3, 2021
032842c
fix: apply review comments
fukudasjp Dec 3, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/discovery-react-components/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
"@storybook/core": "^5.3.21",
"@storybook/react": "^5.3.21",
"@storybook/source-loader": "^5.3.21",
"@types/pdfjs-dist": "2.1.7",
"cross-env": "^7.0.3",
"css-loader": "^3.4.2",
"madge": "^5.0.1",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React from 'react';
import { storiesOf } from '@storybook/react';
import { withKnobs, radios, number } from '@storybook/addon-knobs';
import { action } from '@storybook/addon-actions';
import PdfViewer from './PdfViewer';
import { document as doc } from 'components/DocumentPreview/__fixtures__/Art Effects.pdf';

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

return <PdfViewer file={atob(doc)} page={page} scale={scale} setLoading={(): void => {}} />;
const setLoadingAction = action('setLoading');
const setRenderedTextAction = action('setRenderedText');

return (
<PdfViewer
file={atob(doc)}
page={page}
scale={scale}
setLoading={setLoadingAction}
setRenderedText={setRenderedTextAction}
/>
);
});
Original file line number Diff line number Diff line change
@@ -1,25 +1,32 @@
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,
PDFPromise,
PDFRenderTask
} from 'pdfjs-dist';
import PdfjsWorkerAsText from 'pdfjs-dist/build/pdf.worker.min.js';
import { settings } from 'carbon-components';
import useAsyncFunctionCall from 'utils/useAsyncFunctionCall';
import PdfViewerTextLayer, { PdfRenderedText } from './PdfViewerTextLayer';
import { PdfDisplayProps } from './types';

setupPdfjs();

interface Props {
type Props = PdfDisplayProps & {
className?: string;

/**
* PDF file data as base64-encoded string
*/
file: string;

/**
* Page number, starting at 1
*/
page: number;

/**
* Zoom factor, where `1` is equal to 100%
* Text layer class name
*/
scale: number;
textLayerClassName?: string;

/**
* Callback invoked with page count, once `file` has been parsed
Expand All @@ -33,88 +40,88 @@ 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,
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]
)
);

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}
/>
<PdfViewerTextLayer
className={cx(`${classNameBase}--text`, textLayerClassName)}
loadedPage={loadedPage}
scale={scale}
setRenderedText={setRenderedText}
/>
{children}
</div>
);
};

Expand All @@ -123,32 +130,36 @@ PdfViewer.defaultProps = {
scale: 1
};

function _loadPdf(data: string): Promise<any> {
function _loadPdf(data: string): PDFPromise<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;
// @ts-expect-error Upgrading pdfjs-dist and its typings would resolve the issue
PdfjsLib.GlobalWorkerOptions.workerPort = pdfjsWorker;
} else {
PdfjsLib.GlobalWorkerOptions.workerSrc = PdfjsWorkerAsText;
Expand All @@ -173,4 +184,5 @@ function getCanvasInfo(viewport: any): CanvasInfo {
return { width, height, canvasWidth, canvasHeight, canvasScale };
}

export type PdfViewerProps = Props;
export default PdfViewer;
Loading