'use client'; import { useEffect, useMemo, useRef } from 'react'; import makeCancellable from 'make-cancellable-promise'; import makeEventProps from 'make-event-props'; import clsx from 'clsx'; import mergeRefs from 'merge-refs'; import invariant from 'tiny-invariant'; import warning from 'warning'; import PageContext from './PageContext.js'; import Message from './Message.js'; import Canvas from './Page/Canvas.js'; import TextLayer from './Page/TextLayer.js'; import AnnotationLayer from './Page/AnnotationLayer.js'; import { cancelRunningTask, isProvided, makePageCallback } from './shared/utils.js'; import useDocumentContext from './shared/hooks/useDocumentContext.js'; import useResolver from './shared/hooks/useResolver.js'; import type { PDFDocumentProxy, PDFPageProxy } from 'pdfjs-dist'; import type { EventProps } from 'make-event-props'; import type { ClassName, CustomRenderer, CustomTextRenderer, NodeOrRenderer, OnGetAnnotationsError, OnGetAnnotationsSuccess, OnGetStructTreeError, OnGetStructTreeSuccess, OnGetTextError, OnGetTextSuccess, OnPageLoadError, OnPageLoadSuccess, OnRenderAnnotationLayerError, OnRenderAnnotationLayerSuccess, OnRenderError, OnRenderSuccess, OnRenderTextLayerError, OnRenderTextLayerSuccess, PageCallback, RenderMode, } from './shared/types.js'; const defaultScale = 1; export type PageProps = { _className?: string; _enableRegisterUnregisterPage?: boolean; /** * Canvas background color. Any valid `canvas.fillStyle` can be used. * * @example 'transparent' */ canvasBackground?: string; /** * A prop that behaves like [ref](https://reactjs.org/docs/refs-and-the-dom.html), but it's passed to `` rendered by `` component. * * @example (ref) => { this.myCanvas = ref; } * @example this.ref * @example ref */ canvasRef?: React.Ref; children?: React.ReactNode; /** * Class name(s) that will be added to rendered element along with the default `react-pdf__Page`. * * @example 'custom-class-name-1 custom-class-name-2' * @example ['custom-class-name-1', 'custom-class-name-2'] */ className?: ClassName; /** * Function that customizes how a page is rendered. You must set `renderMode` to `"custom"` to use this prop. * * @example MyCustomRenderer */ customRenderer?: CustomRenderer; /** * Function that customizes how a text layer is rendered. * * @example ({ str, itemIndex }) => str.replace(/ipsum/g, value => `${value}`) */ customTextRenderer?: CustomTextRenderer; /** * The ratio between physical pixels and device-independent pixels (DIPs) on the current device. * * @default window.devicePixelRatio * @example 1 */ devicePixelRatio?: number; /** * What the component should display in case of an error. * * @default 'Failed to load the page.' * @example 'An error occurred!' * @example

An error occurred!

* @example this.renderError */ error?: NodeOrRenderer; /** * Page height. If neither `height` nor `width` are defined, page will be rendered at the size defined in PDF. If you define `width` and `height` at the same time, `height` will be ignored. If you define `height` and `scale` at the same time, the height will be multiplied by a given factor. * * @example 300 */ height?: number; /** * The path used to prefix the src attributes of annotation SVGs. * * @default '' * @example '/public/images/' */ imageResourcesPath?: string; /** * A prop that behaves like [ref](https://reactjs.org/docs/refs-and-the-dom.html), but it's passed to main `
` rendered by `` component. * * @example (ref) => { this.myPage = ref; } * @example this.ref * @example ref */ inputRef?: React.Ref; /** * What the component should display while loading. * * @default 'Loading page…' * @example 'Please wait!' * @example

Please wait!

* @example this.renderLoader */ loading?: NodeOrRenderer; /** * What the component should display in case of no data. * * @default 'No page specified.' * @example 'Please select a page.' * @example

Please select a page.

* @example this.renderNoData */ noData?: NodeOrRenderer; /** * Function called in case of an error while loading annotations. * * @example (error) => alert('Error while loading annotations! ' + error.message) */ onGetAnnotationsError?: OnGetAnnotationsError; /** * Function called when annotations are successfully loaded. * * @example (annotations) => alert('Now displaying ' + annotations.length + ' annotations!') */ onGetAnnotationsSuccess?: OnGetAnnotationsSuccess; /** * Function called in case of an error while loading structure tree. * * @example (error) => alert('Error while loading structure tree! ' + error.message) */ onGetStructTreeError?: OnGetStructTreeError; /** * Function called when structure tree is successfully loaded. * * @example (structTree) => alert(JSON.stringify(structTree)) */ onGetStructTreeSuccess?: OnGetStructTreeSuccess; /** * Function called in case of an error while loading text layer items. * * @example (error) => alert('Error while loading text layer items! ' + error.message) */ onGetTextError?: OnGetTextError; /** * Function called when text layer items are successfully loaded. * * @example ({ items, styles }) => alert('Now displaying ' + items.length + ' text layer items!') */ onGetTextSuccess?: OnGetTextSuccess; /** * Function called in case of an error while loading the page. * * @example (error) => alert('Error while loading page! ' + error.message) */ onLoadError?: OnPageLoadError; /** * Function called when the page is successfully loaded. * * @example (page) => alert('Now displaying a page number ' + page.pageNumber + '!') */ onLoadSuccess?: OnPageLoadSuccess; /** * Function called in case of an error while rendering the annotation layer. * * @example (error) => alert('Error while rendering annotation layer! ' + error.message) */ onRenderAnnotationLayerError?: OnRenderAnnotationLayerError; /** * Function called when annotations are successfully rendered on the screen. * * @example () => alert('Rendered the annotation layer!') */ onRenderAnnotationLayerSuccess?: OnRenderAnnotationLayerSuccess; /** * Function called in case of an error while rendering the page. * * @example (error) => alert('Error while loading page! ' + error.message) */ onRenderError?: OnRenderError; /** * Function called when the page is successfully rendered on the screen. * * @example () => alert('Rendered the page!') */ onRenderSuccess?: OnRenderSuccess; /** * Function called in case of an error while rendering the text layer. * * @example (error) => alert('Error while rendering text layer! ' + error.message) */ onRenderTextLayerError?: OnRenderTextLayerError; /** * Function called when the text layer is successfully rendered on the screen. * * @example () => alert('Rendered the text layer!') */ onRenderTextLayerSuccess?: OnRenderTextLayerSuccess; /** * Which page from PDF file should be displayed, by page index. Ignored if `pageNumber` prop is provided. * * @default 0 * @example 1 */ pageIndex?: number; /** * Which page from PDF file should be displayed, by page number. If provided, `pageIndex` prop will be ignored. * * @default 1 * @example 2 */ pageNumber?: number; /** * pdf object obtained from ``'s `onLoadSuccess` callback function. * * @example pdf */ pdf?: PDFDocumentProxy | false; registerPage?: undefined; /** * Whether annotations (e.g. links) should be rendered. * * @default true * @example false */ renderAnnotationLayer?: boolean; /** * Whether forms should be rendered. `renderAnnotationLayer` prop must be set to `true`. * * @default false * @example true */ renderForms?: boolean; /** * Rendering mode of the document. Can be `"canvas"`, `"custom"` or `"none"`. If set to `"custom"`, `customRenderer` must also be provided. * * @default 'canvas' * @example 'custom' */ renderMode?: RenderMode; /** * Whether a text layer should be rendered. * * @default true * @example false */ renderTextLayer?: boolean; /** * Rotation of the page in degrees. `90` = rotated to the right, `180` = upside down, `270` = rotated to the left. * * @default 0 * @example 90 */ rotate?: number | null; /** * Page scale. * * @default 1 * @example 0.5 */ scale?: number; unregisterPage?: undefined; /** * Page width. If neither `height` nor `width` are defined, page will be rendered at the size defined in PDF. If you define `width` and `height` at the same time, `height` will be ignored. If you define `width` and `scale` at the same time, the width will be multiplied by a given factor. * * @example 300 */ width?: number; } & EventProps; /** * Displays a page. * * Should be placed inside ``. Alternatively, it can have `pdf` prop passed, which can be obtained from ``'s `onLoadSuccess` callback function, however some advanced functions like linking between pages inside a document may not be working correctly. */ export default function Page(props: PageProps): React.ReactElement { const documentContext = useDocumentContext(); const mergedProps = { ...documentContext, ...props }; const { _className = 'react-pdf__Page', _enableRegisterUnregisterPage = true, canvasBackground, canvasRef, children, className, customRenderer: CustomRenderer, customTextRenderer, devicePixelRatio, error = 'Failed to load the page.', height, inputRef, loading = 'Loading page…', noData = 'No page specified.', onGetAnnotationsError: onGetAnnotationsErrorProps, onGetAnnotationsSuccess: onGetAnnotationsSuccessProps, onGetStructTreeError: onGetStructTreeErrorProps, onGetStructTreeSuccess: onGetStructTreeSuccessProps, onGetTextError: onGetTextErrorProps, onGetTextSuccess: onGetTextSuccessProps, onLoadError: onLoadErrorProps, onLoadSuccess: onLoadSuccessProps, onRenderAnnotationLayerError: onRenderAnnotationLayerErrorProps, onRenderAnnotationLayerSuccess: onRenderAnnotationLayerSuccessProps, onRenderError: onRenderErrorProps, onRenderSuccess: onRenderSuccessProps, onRenderTextLayerError: onRenderTextLayerErrorProps, onRenderTextLayerSuccess: onRenderTextLayerSuccessProps, pageIndex: pageIndexProps, pageNumber: pageNumberProps, pdf, registerPage, renderAnnotationLayer: renderAnnotationLayerProps = true, renderForms = false, renderMode = 'canvas', renderTextLayer: renderTextLayerProps = true, rotate: rotateProps, scale: scaleProps = defaultScale, unregisterPage, width, ...otherProps } = mergedProps; const [pageState, pageDispatch] = useResolver(); const { value: page, error: pageError } = pageState; const pageElement = useRef(null); invariant( pdf, 'Attempted to load a page, but no document was specified. Wrap in a or pass explicit `pdf` prop.', ); const pageIndex = isProvided(pageNumberProps) ? pageNumberProps - 1 : (pageIndexProps ?? null); const pageNumber = pageNumberProps ?? (isProvided(pageIndexProps) ? pageIndexProps + 1 : null); const rotate = rotateProps ?? (page ? page.rotate : null); const scale = useMemo(() => { if (!page) { return null; } // Be default, we'll render page at 100% * scale width. let pageScale = 1; // Passing scale explicitly null would cause the page not to render const scaleWithDefault = scaleProps ?? defaultScale; // If width/height is defined, calculate the scale of the page so it could be of desired width. if (width || height) { const viewport = page.getViewport({ scale: 1, rotation: rotate as number }); if (width) { pageScale = width / viewport.width; } else if (height) { pageScale = height / viewport.height; } } return scaleWithDefault * pageScale; }, [height, page, rotate, scaleProps, width]); // biome-ignore lint/correctness/useExhaustiveDependencies: useEffect intentionally triggered on pdf change useEffect( function hook() { return () => { if (!isProvided(pageIndex)) { // Impossible, but TypeScript doesn't know that return; } if (_enableRegisterUnregisterPage && unregisterPage) { unregisterPage(pageIndex); } }; }, [_enableRegisterUnregisterPage, pdf, pageIndex, unregisterPage], ); /** * Called when a page is loaded successfully */ function onLoadSuccess() { if (onLoadSuccessProps) { if (!page || !scale) { // Impossible, but TypeScript doesn't know that return; } onLoadSuccessProps(makePageCallback(page, scale)); } if (_enableRegisterUnregisterPage && registerPage) { if (!isProvided(pageIndex) || !pageElement.current) { // Impossible, but TypeScript doesn't know that return; } registerPage(pageIndex, pageElement.current); } } /** * Called when a page failed to load */ function onLoadError() { if (!pageError) { // Impossible, but TypeScript doesn't know that return; } warning(false, pageError.toString()); if (onLoadErrorProps) { onLoadErrorProps(pageError); } } // biome-ignore lint/correctness/useExhaustiveDependencies: useEffect intentionally triggered on pdf and pageIndex change useEffect( function resetPage() { pageDispatch({ type: 'RESET' }); }, [pageDispatch, pdf, pageIndex], ); useEffect( function loadPage() { if (!pdf || !pageNumber) { return; } const cancellable = makeCancellable(pdf.getPage(pageNumber)); const runningTask = cancellable; cancellable.promise .then((nextPage) => { pageDispatch({ type: 'RESOLVE', value: nextPage }); }) .catch((error) => { pageDispatch({ type: 'REJECT', error }); }); return () => cancelRunningTask(runningTask); }, [pageDispatch, pdf, pageNumber], ); // biome-ignore lint/correctness/useExhaustiveDependencies: Ommitted callbacks so they are not called every time they change useEffect(() => { if (page === undefined) { return; } if (page === false) { onLoadError(); return; } onLoadSuccess(); }, [page, scale]); const childContext = useMemo( () => // Technically there cannot be page without pageIndex, pageNumber, rotate and scale, but TypeScript doesn't know that page && isProvided(pageIndex) && pageNumber && isProvided(rotate) && isProvided(scale) ? { _className, canvasBackground, customTextRenderer, devicePixelRatio, onGetAnnotationsError: onGetAnnotationsErrorProps, onGetAnnotationsSuccess: onGetAnnotationsSuccessProps, onGetStructTreeError: onGetStructTreeErrorProps, onGetStructTreeSuccess: onGetStructTreeSuccessProps, onGetTextError: onGetTextErrorProps, onGetTextSuccess: onGetTextSuccessProps, onRenderAnnotationLayerError: onRenderAnnotationLayerErrorProps, onRenderAnnotationLayerSuccess: onRenderAnnotationLayerSuccessProps, onRenderError: onRenderErrorProps, onRenderSuccess: onRenderSuccessProps, onRenderTextLayerError: onRenderTextLayerErrorProps, onRenderTextLayerSuccess: onRenderTextLayerSuccessProps, page, pageIndex, pageNumber, renderForms, renderTextLayer: renderTextLayerProps, rotate, scale, } : null, [ _className, canvasBackground, customTextRenderer, devicePixelRatio, onGetAnnotationsErrorProps, onGetAnnotationsSuccessProps, onGetStructTreeErrorProps, onGetStructTreeSuccessProps, onGetTextErrorProps, onGetTextSuccessProps, onRenderAnnotationLayerErrorProps, onRenderAnnotationLayerSuccessProps, onRenderErrorProps, onRenderSuccessProps, onRenderTextLayerErrorProps, onRenderTextLayerSuccessProps, page, pageIndex, pageNumber, renderForms, renderTextLayerProps, rotate, scale, ], ); const eventProps = useMemo( () => makeEventProps(otherProps, () => page ? (scale ? makePageCallback(page, scale) : undefined) : page, ), // biome-ignore lint/correctness/useExhaustiveDependencies: FIXME [otherProps, page, scale], ); const pageKey = `${pageIndex}@${scale}/${rotate}`; function renderMainLayer() { switch (renderMode) { case 'custom': { invariant( CustomRenderer, `renderMode was set to "custom", but no customRenderer was passed.`, ); return ; } case 'none': return null; case 'canvas': default: return ; } } function renderTextLayer() { if (!renderTextLayerProps) { return null; } return ; } function renderAnnotationLayer() { if (!renderAnnotationLayerProps) { return null; } return ; } function renderChildren() { return ( {renderMainLayer()} {renderTextLayer()} {renderAnnotationLayer()} {children} ); } function renderContent() { if (!pageNumber) { return {typeof noData === 'function' ? noData() : noData}; } if (pdf === null || page === undefined || page === null) { return ( {typeof loading === 'function' ? loading() : loading} ); } if (pdf === false || page === false) { return {typeof error === 'function' ? error() : error}; } return renderChildren(); } return (
, pageElement)} style={{ ['--scale-factor' as string]: `${scale}`, backgroundColor: canvasBackground || 'white', position: 'relative', minWidth: 'min-content', minHeight: 'min-content', }} {...eventProps} > {renderContent()}
); }