Files
med-notes/.pnpm-store/v10/files/51/da99f157788664ec5196100eacb0c76f285a5ecd77fb35fda12e49e89d3f5719cc084d7a352e21f64fc4651cf7a4b17a66cb863cc76ac4353b7f84e10a5a46
2025-05-09 05:30:08 +02:00

640 lines
19 KiB
Plaintext

'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 `<canvas>` rendered by `<PageCanvas>` component.
*
* @example (ref) => { this.myCanvas = ref; }
* @example this.ref
* @example ref
*/
canvasRef?: React.Ref<HTMLCanvasElement>;
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 => `<mark>${value}</mark>`)
*/
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 <p>An error occurred!</p>
* @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 `<div>` rendered by `<Page>` component.
*
* @example (ref) => { this.myPage = ref; }
* @example this.ref
* @example ref
*/
inputRef?: React.Ref<HTMLDivElement | null>;
/**
* What the component should display while loading.
*
* @default 'Loading page…'
* @example 'Please wait!'
* @example <p>Please wait!</p>
* @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 <p>Please select a page.</p>
* @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 `<Document />`'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<PageCallback | false | undefined>;
/**
* Displays a page.
*
* Should be placed inside `<Document />`. Alternatively, it can have `pdf` prop passed, which can be obtained from `<Document />`'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<PDFPageProxy>();
const { value: page, error: pageError } = pageState;
const pageElement = useRef<HTMLDivElement>(null);
invariant(
pdf,
'Attempted to load a page, but no document was specified. Wrap <Page /> in a <Document /> 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 <CustomRenderer key={`${pageKey}_custom`} />;
}
case 'none':
return null;
case 'canvas':
default:
return <Canvas key={`${pageKey}_canvas`} canvasRef={canvasRef} />;
}
}
function renderTextLayer() {
if (!renderTextLayerProps) {
return null;
}
return <TextLayer key={`${pageKey}_text`} />;
}
function renderAnnotationLayer() {
if (!renderAnnotationLayerProps) {
return null;
}
return <AnnotationLayer key={`${pageKey}_annotations`} />;
}
function renderChildren() {
return (
<PageContext.Provider value={childContext}>
{renderMainLayer()}
{renderTextLayer()}
{renderAnnotationLayer()}
{children}
</PageContext.Provider>
);
}
function renderContent() {
if (!pageNumber) {
return <Message type="no-data">{typeof noData === 'function' ? noData() : noData}</Message>;
}
if (pdf === null || page === undefined || page === null) {
return (
<Message type="loading">{typeof loading === 'function' ? loading() : loading}</Message>
);
}
if (pdf === false || page === false) {
return <Message type="error">{typeof error === 'function' ? error() : error}</Message>;
}
return renderChildren();
}
return (
<div
className={clsx(_className, className)}
data-page-number={pageNumber}
// Assertion is needed for React 18 compatibility
ref={mergeRefs(inputRef as React.Ref<HTMLDivElement>, pageElement)}
style={{
['--scale-factor' as string]: `${scale}`,
backgroundColor: canvasBackground || 'white',
position: 'relative',
minWidth: 'min-content',
minHeight: 'min-content',
}}
{...eventProps}
>
{renderContent()}
</div>
);
}