Latest repo

This commit is contained in:
Marc
2025-06-02 16:42:16 +00:00
parent 53ddf1a329
commit cde5fae175
27907 changed files with 3875388 additions and 1 deletions

708
node_modules/react-pdf/src/Document.spec.tsx generated vendored Normal file
View File

@@ -0,0 +1,708 @@
import { beforeAll, describe, expect, it, vi } from 'vitest';
import { createRef } from 'react';
import { fireEvent, getByTestId, render } from '@testing-library/react';
import { pdfjs } from './index.test.js';
import Document from './Document.js';
import DocumentContext from './DocumentContext.js';
import Page from './Page.js';
import { makeAsyncCallback, loadPDF, muteConsole, restoreConsole } from '../../../test-utils.js';
import type { PDFDocumentProxy } from 'pdfjs-dist';
import type { ScrollPageIntoViewArgs } from './shared/types.js';
import type LinkService from './LinkService.js';
const pdfFile = loadPDF('./../../__mocks__/_pdf.pdf');
const pdfFile2 = loadPDF('./../../__mocks__/_pdf2.pdf');
const OK = Symbol('OK');
function ChildInternal({
renderMode,
rotate,
}: {
renderMode?: string | null;
rotate?: number | null;
}) {
return <div data-testid="child" data-rendermode={renderMode} data-rotate={rotate} />;
}
function Child(props: React.ComponentProps<typeof ChildInternal>) {
return (
<DocumentContext.Consumer>
{(context) => <ChildInternal {...context} {...props} />}
</DocumentContext.Consumer>
);
}
async function waitForAsync() {
await new Promise((resolve) => {
setTimeout(resolve, 0);
});
}
describe('Document', () => {
// Object with basic loaded PDF information that shall match after successful loading
const desiredLoadedPdf: Partial<PDFDocumentProxy> = {};
const desiredLoadedPdf2: Partial<PDFDocumentProxy> = {};
beforeAll(async () => {
const pdf = await pdfjs.getDocument({ data: pdfFile.arrayBuffer }).promise;
desiredLoadedPdf._pdfInfo = pdf._pdfInfo;
const pdf2 = await pdfjs.getDocument({ data: pdfFile2.arrayBuffer }).promise;
desiredLoadedPdf2._pdfInfo = pdf2._pdfInfo;
});
describe('loading', () => {
it('loads a file and calls onSourceSuccess and onLoadSuccess callbacks via data URI properly', async () => {
const { func: onSourceSuccess, promise: onSourceSuccessPromise } = makeAsyncCallback(OK);
const { func: onLoadSuccess, promise: onLoadSuccessPromise } = makeAsyncCallback();
render(
<Document
file={pdfFile.dataURI}
onLoadSuccess={onLoadSuccess}
onSourceSuccess={onSourceSuccess}
/>,
);
expect.assertions(2);
await expect(onSourceSuccessPromise).resolves.toBe(OK);
await expect(onLoadSuccessPromise).resolves.toMatchObject([desiredLoadedPdf]);
});
it('loads a file and calls onSourceSuccess and onLoadSuccess callbacks via data URI properly (param object)', async () => {
const { func: onSourceSuccess, promise: onSourceSuccessPromise } = makeAsyncCallback(OK);
const { func: onLoadSuccess, promise: onLoadSuccessPromise } = makeAsyncCallback();
render(
<Document
file={{ url: pdfFile.dataURI }}
onLoadSuccess={onLoadSuccess}
onSourceSuccess={onSourceSuccess}
/>,
);
expect.assertions(2);
await expect(onSourceSuccessPromise).resolves.toBe(OK);
await expect(onLoadSuccessPromise).resolves.toMatchObject([desiredLoadedPdf]);
});
// FIXME: In Jest, it used to be worked around as described in https://github.com/facebook/jest/issues/7780
it.skip('loads a file and calls onSourceSuccess and onLoadSuccess callbacks via ArrayBuffer properly', async () => {
const { func: onSourceSuccess, promise: onSourceSuccessPromise } = makeAsyncCallback(OK);
const { func: onLoadSuccess, promise: onLoadSuccessPromise } = makeAsyncCallback();
render(
<Document
file={pdfFile.arrayBuffer}
onLoadSuccess={onLoadSuccess}
onSourceSuccess={onSourceSuccess}
/>,
);
expect.assertions(2);
await expect(onSourceSuccessPromise).resolves.toBe(OK);
await expect(onLoadSuccessPromise).resolves.toMatchObject([desiredLoadedPdf]);
});
it('loads a file and calls onSourceSuccess and onLoadSuccess callbacks via Blob properly', async () => {
const { func: onSourceSuccess, promise: onSourceSuccessPromise } = makeAsyncCallback(OK);
const { func: onLoadSuccess, promise: onLoadSuccessPromise } = makeAsyncCallback();
render(
<Document
file={pdfFile.blob}
onLoadSuccess={onLoadSuccess}
onSourceSuccess={onSourceSuccess}
/>,
);
expect.assertions(2);
await expect(onSourceSuccessPromise).resolves.toBe(OK);
await expect(onLoadSuccessPromise).resolves.toMatchObject([desiredLoadedPdf]);
});
it('loads a file and calls onSourceSuccess and onLoadSuccess callbacks via File properly', async () => {
const { func: onSourceSuccess, promise: onSourceSuccessPromise } = makeAsyncCallback(OK);
const { func: onLoadSuccess, promise: onLoadSuccessPromise } = makeAsyncCallback();
render(
<Document
file={pdfFile.file}
onLoadSuccess={onLoadSuccess}
onSourceSuccess={onSourceSuccess}
/>,
);
expect.assertions(2);
await expect(onSourceSuccessPromise).resolves.toBe(OK);
await expect(onLoadSuccessPromise).resolves.toMatchObject([desiredLoadedPdf]);
});
it('fails to load a file and calls onSourceError given invalid file source', async () => {
const { func: onSourceError, promise: onSourceErrorPromise } = makeAsyncCallback();
muteConsole();
// @ts-expect-error-next-line
render(<Document file={() => null} onSourceError={onSourceError} />);
expect.assertions(1);
const [error] = await onSourceErrorPromise;
expect(error).toMatchObject(expect.any(Error));
restoreConsole();
});
it('replaces a file properly', async () => {
const { func: onSourceSuccess, promise: onSourceSuccessPromise } = makeAsyncCallback(OK);
const { func: onLoadSuccess, promise: onLoadSuccessPromise } = makeAsyncCallback();
const { rerender } = render(
<Document
file={pdfFile.file}
onLoadSuccess={onLoadSuccess}
onSourceSuccess={onSourceSuccess}
/>,
);
expect.assertions(4);
await expect(onSourceSuccessPromise).resolves.toBe(OK);
await expect(onLoadSuccessPromise).resolves.toMatchObject([desiredLoadedPdf]);
const { func: onSourceSuccess2, promise: onSourceSuccessPromise2 } = makeAsyncCallback(OK);
const { func: onLoadSuccess2, promise: onLoadSuccessPromise2 } = makeAsyncCallback();
rerender(
<Document
file={pdfFile2.file}
onLoadSuccess={onLoadSuccess2}
onSourceSuccess={onSourceSuccess2}
/>,
);
await expect(onSourceSuccessPromise2).resolves.toBe(OK);
await expect(onLoadSuccessPromise2).resolves.toMatchObject([desiredLoadedPdf2]);
});
});
describe('rendering', () => {
it('applies className to its wrapper when given a string', () => {
const className = 'testClassName';
const { container } = render(<Document className={className} />);
const wrapper = container.querySelector('.react-pdf__Document');
expect(wrapper).toHaveClass(className);
});
it('passes container element to inputRef properly', () => {
const inputRef = createRef<HTMLDivElement>();
render(<Document inputRef={inputRef} />);
expect(inputRef.current).toBeInstanceOf(HTMLDivElement);
});
it('renders "No PDF file specified." when given nothing', () => {
const { container } = render(<Document />);
const noData = container.querySelector('.react-pdf__message');
expect(noData).toBeInTheDocument();
expect(noData).toHaveTextContent('No PDF file specified.');
});
it('renders custom no data message when given nothing and noData prop is given', () => {
const { container } = render(<Document noData="Nothing here" />);
const noData = container.querySelector('.react-pdf__message');
expect(noData).toBeInTheDocument();
expect(noData).toHaveTextContent('Nothing here');
});
it('renders custom no data message when given nothing and noData prop is given as a function', () => {
const { container } = render(<Document noData={() => 'Nothing here'} />);
const noData = container.querySelector('.react-pdf__message');
expect(noData).toBeInTheDocument();
expect(noData).toHaveTextContent('Nothing here');
});
it('renders "Loading PDF…" when loading a file', async () => {
const { container, findByText } = render(<Document file={pdfFile.file} />);
const loading = container.querySelector('.react-pdf__message');
expect(loading).toBeInTheDocument();
expect(await findByText('Loading PDF…')).toBeInTheDocument();
});
it('renders custom loading message when loading a file and loading prop is given', async () => {
const { container, findByText } = render(<Document file={pdfFile.file} loading="Loading" />);
const loading = container.querySelector('.react-pdf__message');
expect(loading).toBeInTheDocument();
expect(await findByText('Loading')).toBeInTheDocument();
});
it('renders custom loading message when loading a file and loading prop is given as a function', async () => {
const { container, findByText } = render(
<Document file={pdfFile.file} loading={() => 'Loading'} />,
);
const loading = container.querySelector('.react-pdf__message');
expect(loading).toBeInTheDocument();
expect(await findByText('Loading')).toBeInTheDocument();
});
it('renders "Failed to load PDF file." when failed to load a document', async () => {
const { func: onLoadError, promise: onLoadErrorPromise } = makeAsyncCallback();
const failingPdf = 'data:application/pdf;base64,abcdef';
muteConsole();
const { container, findByText } = render(
<Document file={failingPdf} onLoadError={onLoadError} />,
);
expect.assertions(2);
await onLoadErrorPromise;
await waitForAsync();
const error = container.querySelector('.react-pdf__message');
expect(error).toBeInTheDocument();
expect(await findByText('Failed to load PDF file.')).toBeInTheDocument();
restoreConsole();
});
it('renders custom error message when failed to load a document and error prop is given', async () => {
const { func: onLoadError, promise: onLoadErrorPromise } = makeAsyncCallback();
const failingPdf = 'data:application/pdf;base64,abcdef';
muteConsole();
const { container, findByText } = render(
<Document error="Error" file={failingPdf} onLoadError={onLoadError} />,
);
expect.assertions(2);
await onLoadErrorPromise;
await waitForAsync();
const error = container.querySelector('.react-pdf__message');
expect(error).toBeInTheDocument();
expect(await findByText('Error')).toBeInTheDocument();
restoreConsole();
});
it('renders custom error message when failed to load a document and error prop is given as a function', async () => {
const { func: onLoadError, promise: onLoadErrorPromise } = makeAsyncCallback();
const failingPdf = 'data:application/pdf;base64,abcdef';
muteConsole();
const { container, findByText } = render(
<Document error="Error" file={failingPdf} onLoadError={onLoadError} />,
);
expect.assertions(2);
await onLoadErrorPromise;
await waitForAsync();
const error = container.querySelector('.react-pdf__message');
expect(error).toBeInTheDocument();
expect(await findByText('Error')).toBeInTheDocument();
restoreConsole();
});
it('passes renderMode prop to its children', async () => {
const { func: onLoadSuccess, promise: onLoadSuccessPromise } = makeAsyncCallback();
const { container } = render(
<Document
file={pdfFile.file}
loading="Loading"
onLoadSuccess={onLoadSuccess}
renderMode="custom"
>
<Child />
</Document>,
);
expect.assertions(1);
await onLoadSuccessPromise;
const child = getByTestId(container, 'child');
expect(child.dataset.rendermode).toBe('custom');
});
it('passes rotate prop to its children', async () => {
const { func: onLoadSuccess, promise: onLoadSuccessPromise } = makeAsyncCallback();
const { container } = render(
<Document file={pdfFile.file} loading="Loading" onLoadSuccess={onLoadSuccess} rotate={90}>
<Child />
</Document>,
);
expect.assertions(1);
await onLoadSuccessPromise;
const child = getByTestId(container, 'child');
expect(child.dataset.rotate).toBe('90');
});
it('does not overwrite renderMode prop in its children when given renderMode prop to both Document and its children', async () => {
const { func: onLoadSuccess, promise: onLoadSuccessPromise } = makeAsyncCallback();
const { container } = render(
<Document
file={pdfFile.file}
loading="Loading"
onLoadSuccess={onLoadSuccess}
renderMode="canvas"
>
<Child renderMode="custom" />
</Document>,
);
expect.assertions(1);
await onLoadSuccessPromise;
const child = getByTestId(container, 'child');
expect(child.dataset.rendermode).toBe('custom');
});
it('does not overwrite rotate prop in its children when given rotate prop to both Document and its children', async () => {
const { func: onLoadSuccess, promise: onLoadSuccessPromise } = makeAsyncCallback();
const { container } = render(
<Document file={pdfFile.file} loading="Loading" onLoadSuccess={onLoadSuccess} rotate={90}>
<Child rotate={180} />
</Document>,
);
expect.assertions(1);
await onLoadSuccessPromise;
const child = getByTestId(container, 'child');
expect(child.dataset.rotate).toBe('180');
});
});
describe('viewer', () => {
it('calls onItemClick if defined', async () => {
const { func: onLoadSuccess, promise: onLoadSuccessPromise } = makeAsyncCallback();
const onItemClick = vi.fn();
const instance = createRef<{
linkService: React.RefObject<LinkService>;
pages: React.RefObject<HTMLDivElement[]>;
viewer: React.RefObject<{ scrollPageIntoView: (args: ScrollPageIntoViewArgs) => void }>;
}>();
render(
<Document
file={pdfFile.file}
onItemClick={onItemClick}
onLoadSuccess={onLoadSuccess}
ref={instance}
/>,
);
if (!instance.current) {
throw new Error('Document ref is not set');
}
if (!instance.current.viewer.current) {
throw new Error('Viewer ref is not set');
}
expect.assertions(2);
await onLoadSuccessPromise;
const dest: number[] = [];
const pageIndex = 5;
const pageNumber = 6;
// Simulate clicking on an outline item
instance.current.viewer.current.scrollPageIntoView({ dest, pageIndex, pageNumber });
expect(onItemClick).toHaveBeenCalledTimes(1);
expect(onItemClick).toHaveBeenCalledWith({ dest, pageIndex, pageNumber });
});
it('attempts to find a page and scroll it into view if onItemClick is not given', async () => {
const { func: onLoadSuccess, promise: onLoadSuccessPromise } = makeAsyncCallback();
const instance = createRef<{
linkService: React.RefObject<LinkService>;
// biome-ignore lint/suspicious/noExplicitAny: Intentional use to simplify the test
pages: React.RefObject<any[]>;
viewer: React.RefObject<{ scrollPageIntoView: (args: ScrollPageIntoViewArgs) => void }>;
}>();
render(<Document file={pdfFile.file} onLoadSuccess={onLoadSuccess} ref={instance} />);
if (!instance.current) {
throw new Error('Document ref is not set');
}
if (!instance.current.pages.current) {
throw new Error('Pages ref is not set');
}
if (!instance.current.viewer.current) {
throw new Error('Viewer ref is not set');
}
expect.assertions(1);
await onLoadSuccessPromise;
const scrollIntoView = vi.fn();
const dest: number[] = [];
const pageIndex = 5;
const pageNumber = 6;
// Register fake page in Document viewer
instance.current.pages.current[pageIndex] = { scrollIntoView };
// Simulate clicking on an outline item
instance.current.viewer.current.scrollPageIntoView({ dest, pageIndex, pageNumber });
expect(scrollIntoView).toHaveBeenCalledTimes(1);
});
});
describe('linkService', () => {
it.each`
externalLinkTarget | target
${null} | ${''}
${'_self'} | ${'_self'}
${'_blank'} | ${'_blank'}
${'_parent'} | ${'_parent'}
${'_top'} | ${'_top'}
`(
'returns externalLinkTarget = $target given externalLinkTarget prop = $externalLinkTarget',
async ({ externalLinkTarget, target }) => {
const {
func: onRenderAnnotationLayerSuccess,
promise: onRenderAnnotationLayerSuccessPromise,
} = makeAsyncCallback();
const { container } = render(
<Document externalLinkTarget={externalLinkTarget} file={pdfFile.file}>
<Page
onRenderAnnotationLayerSuccess={onRenderAnnotationLayerSuccess}
renderMode="none"
pageNumber={1}
/>
</Document>,
);
expect.assertions(1);
await onRenderAnnotationLayerSuccessPromise;
const link = container.querySelector('a') as HTMLAnchorElement;
expect(link.target).toBe(target);
},
);
it.each`
externalLinkRel | rel
${null} | ${'noopener noreferrer nofollow'}
${'noopener'} | ${'noopener'}
${'noreferrer'} | ${'noreferrer'}
${'nofollow'} | ${'nofollow'}
`(
'returns externalLinkRel = $rel given externalLinkRel prop = $externalLinkRel',
async ({ externalLinkRel, rel }) => {
const {
func: onRenderAnnotationLayerSuccess,
promise: onRenderAnnotationLayerSuccessPromise,
} = makeAsyncCallback();
const { container } = render(
<Document externalLinkRel={externalLinkRel} file={pdfFile.file}>
<Page
onRenderAnnotationLayerSuccess={onRenderAnnotationLayerSuccess}
renderMode="none"
pageNumber={1}
/>
</Document>,
);
expect.assertions(1);
await onRenderAnnotationLayerSuccessPromise;
const link = container.querySelector('a') as HTMLAnchorElement;
expect(link.rel).toBe(rel);
},
);
});
it('calls onClick callback when clicked a page (sample of mouse events family)', () => {
const onClick = vi.fn();
const { container } = render(<Document onClick={onClick} />);
const document = container.querySelector('.react-pdf__Document') as HTMLDivElement;
fireEvent.click(document);
expect(onClick).toHaveBeenCalled();
});
it('calls onTouchStart callback when touched a page (sample of touch events family)', () => {
const onTouchStart = vi.fn();
const { container } = render(<Document onTouchStart={onTouchStart} />);
const document = container.querySelector('.react-pdf__Document') as HTMLDivElement;
fireEvent.touchStart(document);
expect(onTouchStart).toHaveBeenCalled();
});
it('does not warn if file prop was memoized', () => {
const spy = vi.spyOn(globalThis.console, 'error').mockImplementation(() => {
// Intentionally empty
});
const file = { data: pdfFile.arrayBuffer };
const { rerender } = render(<Document file={file} />);
rerender(<Document file={file} />);
expect(spy).not.toHaveBeenCalled();
vi.mocked(globalThis.console.error).mockRestore();
});
it('warns if file prop was not memoized', () => {
const spy = vi.spyOn(globalThis.console, 'error').mockImplementation(() => {
// Intentionally empty
});
const { rerender } = render(<Document file={{ data: pdfFile.arrayBuffer }} />);
rerender(<Document file={{ data: pdfFile.arrayBuffer }} />);
expect(spy).toHaveBeenCalledTimes(1);
vi.mocked(globalThis.console.error).mockRestore();
});
it('does not warn if file prop was not memoized, but was changed', () => {
const spy = vi.spyOn(globalThis.console, 'error').mockImplementation(() => {
// Intentionally empty
});
const { rerender } = render(<Document file={{ data: pdfFile.arrayBuffer }} />);
rerender(<Document file={{ data: pdfFile2.arrayBuffer }} />);
expect(spy).not.toHaveBeenCalled();
vi.mocked(globalThis.console.error).mockRestore();
});
it('does not warn if options prop was memoized', () => {
const spy = vi.spyOn(globalThis.console, 'error').mockImplementation(() => {
// Intentionally empty
});
const options = {};
const { rerender } = render(<Document file={pdfFile.blob} options={options} />);
rerender(<Document file={pdfFile.blob} options={options} />);
expect(spy).not.toHaveBeenCalled();
vi.mocked(globalThis.console.error).mockRestore();
});
it('warns if options prop was not memoized', () => {
const spy = vi.spyOn(globalThis.console, 'error').mockImplementation(() => {
// Intentionally empty
});
const { rerender } = render(<Document file={pdfFile.blob} options={{}} />);
rerender(<Document file={pdfFile.blob} options={{}} />);
expect(spy).toHaveBeenCalledTimes(1);
vi.mocked(globalThis.console.error).mockRestore();
});
it('does not warn if options prop was not memoized, but was changed', () => {
const spy = vi.spyOn(globalThis.console, 'error').mockImplementation(() => {
// Intentionally empty
});
const { rerender } = render(<Document file={pdfFile.blob} options={{}} />);
rerender(<Document file={pdfFile.blob} options={{ maxImageSize: 100 }} />);
expect(spy).not.toHaveBeenCalled();
vi.mocked(globalThis.console.error).mockRestore();
});
it('does not throw an error on unmount', async () => {
const { func: onLoadProgress, promise: onLoadProgressPromise } = makeAsyncCallback();
const { unmount } = render(<Document file={pdfFile} onLoadProgress={onLoadProgress} />);
await onLoadProgressPromise;
expect(unmount).not.toThrowError();
});
});

631
node_modules/react-pdf/src/Document.tsx generated vendored Normal file
View File

@@ -0,0 +1,631 @@
'use client';
import { forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useRef } from 'react';
import makeEventProps from 'make-event-props';
import makeCancellable from 'make-cancellable-promise';
import clsx from 'clsx';
import invariant from 'tiny-invariant';
import warning from 'warning';
import { dequal } from 'dequal';
import * as pdfjs from 'pdfjs-dist';
import DocumentContext from './DocumentContext.js';
import Message from './Message.js';
import LinkService from './LinkService.js';
import PasswordResponses from './PasswordResponses.js';
import {
cancelRunningTask,
dataURItoByteString,
displayCORSWarning,
isArrayBuffer,
isBlob,
isBrowser,
isDataURI,
loadFromFile,
} from './shared/utils.js';
import useResolver from './shared/hooks/useResolver.js';
import type { PDFDocumentProxy } from 'pdfjs-dist';
import type { DocumentInitParameters } from 'pdfjs-dist/types/src/display/api.js';
import type { EventProps } from 'make-event-props';
import type {
ClassName,
DocumentCallback,
ExternalLinkRel,
ExternalLinkTarget,
File,
ImageResourcesPath,
NodeOrRenderer,
OnDocumentLoadError,
OnDocumentLoadProgress,
OnDocumentLoadSuccess,
OnError,
OnItemClickArgs,
OnPasswordCallback,
Options,
PasswordResponse,
RenderMode,
ScrollPageIntoViewArgs,
Source,
} from './shared/types.js';
const { PDFDataRangeTransport } = pdfjs;
type OnItemClick = (args: OnItemClickArgs) => void;
type OnPassword = (callback: OnPasswordCallback, reason: PasswordResponse) => void;
type OnSourceError = OnError;
type OnSourceSuccess = () => void;
export type DocumentProps = {
children?: React.ReactNode;
/**
* Class name(s) that will be added to rendered element along with the default `react-pdf__Document`.
*
* @example 'custom-class-name-1 custom-class-name-2'
* @example ['custom-class-name-1', 'custom-class-name-2']
*/
className?: ClassName;
/**
* What the component should display in case of an error.
*
* @default 'Failed to load PDF file.'
* @example 'An error occurred!'
* @example <p>An error occurred!</p>
* @example {this.renderError}
*/
error?: NodeOrRenderer;
/**
* Link rel for links rendered in annotations.
*
* @default 'noopener noreferrer nofollow'
*/
externalLinkRel?: ExternalLinkRel;
/**
* Link target for external links rendered in annotations.
*/
externalLinkTarget?: ExternalLinkTarget;
/**
* What PDF should be displayed.
*
* Its value can be an URL, a file (imported using `import … from …` or from file input form element), or an object with parameters (`url` - URL; `data` - data, preferably Uint8Array; `range` - PDFDataRangeTransport.
*
* **Warning**: Since equality check (`===`) is used to determine if `file` object has changed, it must be memoized by setting it in component's state, `useMemo` or other similar technique.
*
* @example 'https://example.com/sample.pdf'
* @example importedPdf
* @example { url: 'https://example.com/sample.pdf' }
*/
file?: File;
/**
* The path used to prefix the src attributes of annotation SVGs.
*
* @default ''
* @example '/public/images/'
*/
imageResourcesPath?: ImageResourcesPath;
/**
* A prop that behaves like [ref](https://reactjs.org/docs/refs-and-the-dom.html), but it's passed to main `<div>` rendered by `<Document>` component.
*
* @example (ref) => { this.myDocument = ref; }
* @example this.ref
* @example ref
*/
inputRef?: React.Ref<HTMLDivElement | null>;
/**
* What the component should display while loading.
*
* @default 'Loading PDF…'
* @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 PDF file specified.'
* @example 'Please select a file.'
* @example <p>Please select a file.</p>
* @example {this.renderNoData}
*/
noData?: NodeOrRenderer;
/**
* Function called when an outline item or a thumbnail has been clicked. Usually, you would like to use this callback to move the user wherever they requested to.
*
* @example ({ dest, pageIndex, pageNumber }) => alert('Clicked an item from page ' + pageNumber + '!')
*/
onItemClick?: OnItemClick;
/**
* Function called in case of an error while loading a document.
*
* @example (error) => alert('Error while loading document! ' + error.message)
*/
onLoadError?: OnDocumentLoadError;
/**
* Function called, potentially multiple times, as the loading progresses.
*
* @example ({ loaded, total }) => alert('Loading a document: ' + (loaded / total) * 100 + '%')
*/
onLoadProgress?: OnDocumentLoadProgress;
/**
* Function called when the document is successfully loaded.
*
* @example (pdf) => alert('Loaded a file with ' + pdf.numPages + ' pages!')
*/
onLoadSuccess?: OnDocumentLoadSuccess;
/**
* Function called when a password-protected PDF is loaded.
*
* @example (callback) => callback('s3cr3t_p4ssw0rd')
*/
onPassword?: OnPassword;
/**
* Function called in case of an error while retrieving document source from `file` prop.
*
* @example (error) => alert('Error while retrieving document source! ' + error.message)
*/
onSourceError?: OnSourceError;
/**
* Function called when document source is successfully retrieved from `file` prop.
*
* @example () => alert('Document source retrieved!')
*/
onSourceSuccess?: OnSourceSuccess;
/**
* An object in which additional parameters to be passed to PDF.js can be defined. Most notably:
* - `cMapUrl`;
* - `httpHeaders` - custom request headers, e.g. for authorization);
* - `withCredentials` - a boolean to indicate whether or not to include cookies in the request (defaults to `false`)
*
* For a full list of possible parameters, check [PDF.js documentation on DocumentInitParameters](https://mozilla.github.io/pdf.js/api/draft/module-pdfjsLib.html#~DocumentInitParameters).
*
* **Note**: Make sure to define options object outside of your React component, and use `useMemo` if you can't.
*
* @example { cMapUrl: '/cmaps/' }
*/
options?: Options;
/**
* 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;
/**
* Rotation of the document in degrees. If provided, will change rotation globally, even for the pages which were given `rotate` prop of their own. `90` = rotated to the right, `180` = upside down, `270` = rotated to the left.
*
* @example 90
*/
rotate?: number | null;
} & EventProps<DocumentCallback | false | undefined>;
const defaultOnPassword: OnPassword = (callback, reason) => {
switch (reason) {
case PasswordResponses.NEED_PASSWORD: {
const password = prompt('Enter the password to open this PDF file.');
callback(password);
break;
}
case PasswordResponses.INCORRECT_PASSWORD: {
const password = prompt('Invalid password. Please try again.');
callback(password);
break;
}
default:
}
};
function isParameterObject(file: File): file is Source {
return (
typeof file === 'object' &&
file !== null &&
('data' in file || 'range' in file || 'url' in file)
);
}
/**
* Loads a document passed using `file` prop.
*/
const Document: React.ForwardRefExoticComponent<
DocumentProps &
React.RefAttributes<{
linkService: React.RefObject<LinkService>;
pages: React.RefObject<HTMLDivElement[]>;
viewer: React.RefObject<{ scrollPageIntoView: (args: ScrollPageIntoViewArgs) => void }>;
}>
> = forwardRef(function Document(
{
children,
className,
error = 'Failed to load PDF file.',
externalLinkRel,
externalLinkTarget,
file,
inputRef,
imageResourcesPath,
loading = 'Loading PDF…',
noData = 'No PDF file specified.',
onItemClick,
onLoadError: onLoadErrorProps,
onLoadProgress,
onLoadSuccess: onLoadSuccessProps,
onPassword = defaultOnPassword,
onSourceError: onSourceErrorProps,
onSourceSuccess: onSourceSuccessProps,
options,
renderMode,
rotate,
...otherProps
},
ref,
) {
const [sourceState, sourceDispatch] = useResolver<Source | null>();
const { value: source, error: sourceError } = sourceState;
const [pdfState, pdfDispatch] = useResolver<PDFDocumentProxy>();
const { value: pdf, error: pdfError } = pdfState;
const linkService = useRef(new LinkService());
const pages = useRef<HTMLDivElement[]>([]);
const prevFile = useRef<File | undefined>(undefined);
const prevOptions = useRef<Options | undefined>(undefined);
if (file && file !== prevFile.current && isParameterObject(file)) {
warning(
!dequal(file, prevFile.current),
`File prop passed to <Document /> changed, but it's equal to previous one. This might result in unnecessary reloads. Consider memoizing the value passed to "file" prop.`,
);
prevFile.current = file;
}
// Detect non-memoized changes in options prop
if (options && options !== prevOptions.current) {
warning(
!dequal(options, prevOptions.current),
`Options prop passed to <Document /> changed, but it's equal to previous one. This might result in unnecessary reloads. Consider memoizing the value passed to "options" prop.`,
);
prevOptions.current = options;
}
const viewer = useRef({
// Handling jumping to internal links target
scrollPageIntoView: (args: ScrollPageIntoViewArgs) => {
const { dest, pageNumber, pageIndex = pageNumber - 1 } = args;
// First, check if custom handling of onItemClick was provided
if (onItemClick) {
onItemClick({ dest, pageIndex, pageNumber });
return;
}
// If not, try to look for target page within the <Document>.
const page = pages.current[pageIndex];
if (page) {
// Scroll to the page automatically
page.scrollIntoView();
return;
}
warning(
false,
`An internal link leading to page ${pageNumber} was clicked, but neither <Document> was provided with onItemClick nor it was able to find the page within itself. Either provide onItemClick to <Document> and handle navigating by yourself or ensure that all pages are rendered within <Document>.`,
);
},
});
useImperativeHandle(
ref,
() => ({
linkService,
pages,
viewer,
}),
[],
);
/**
* Called when a document source is resolved correctly
*/
function onSourceSuccess() {
if (onSourceSuccessProps) {
onSourceSuccessProps();
}
}
/**
* Called when a document source failed to be resolved correctly
*/
function onSourceError() {
if (!sourceError) {
// Impossible, but TypeScript doesn't know that
return;
}
warning(false, sourceError.toString());
if (onSourceErrorProps) {
onSourceErrorProps(sourceError);
}
}
function resetSource() {
sourceDispatch({ type: 'RESET' });
}
// biome-ignore lint/correctness/useExhaustiveDependencies: See https://github.com/biomejs/biome/issues/3080
useEffect(resetSource, [file, sourceDispatch]);
const findDocumentSource = useCallback(async (): Promise<Source | null> => {
if (!file) {
return null;
}
// File is a string
if (typeof file === 'string') {
if (isDataURI(file)) {
const fileByteString = dataURItoByteString(file);
return { data: fileByteString };
}
displayCORSWarning();
return { url: file };
}
// File is PDFDataRangeTransport
if (file instanceof PDFDataRangeTransport) {
return { range: file };
}
// File is an ArrayBuffer
if (isArrayBuffer(file)) {
return { data: file };
}
/**
* The cases below are browser-only.
* If you're running on a non-browser environment, these cases will be of no use.
*/
if (isBrowser) {
// File is a Blob
if (isBlob(file)) {
const data = await loadFromFile(file);
return { data };
}
}
// At this point, file must be an object
invariant(
typeof file === 'object',
'Invalid parameter in file, need either Uint8Array, string or a parameter object',
);
invariant(
isParameterObject(file),
'Invalid parameter object: need either .data, .range or .url',
);
// File .url is a string
if ('url' in file && typeof file.url === 'string') {
if (isDataURI(file.url)) {
const { url, ...otherParams } = file;
const fileByteString = dataURItoByteString(url);
return { data: fileByteString, ...otherParams };
}
displayCORSWarning();
}
return file;
}, [file]);
useEffect(() => {
const cancellable = makeCancellable(findDocumentSource());
cancellable.promise
.then((nextSource) => {
sourceDispatch({ type: 'RESOLVE', value: nextSource });
})
.catch((error) => {
sourceDispatch({ type: 'REJECT', error });
});
return () => {
cancelRunningTask(cancellable);
};
}, [findDocumentSource, sourceDispatch]);
// biome-ignore lint/correctness/useExhaustiveDependencies: Ommitted callbacks so they are not called every time they change
useEffect(() => {
if (typeof source === 'undefined') {
return;
}
if (source === false) {
onSourceError();
return;
}
onSourceSuccess();
}, [source]);
/**
* Called when a document is read successfully
*/
function onLoadSuccess() {
if (!pdf) {
// Impossible, but TypeScript doesn't know that
return;
}
if (onLoadSuccessProps) {
onLoadSuccessProps(pdf);
}
pages.current = new Array(pdf.numPages);
linkService.current.setDocument(pdf);
}
/**
* Called when a document failed to read successfully
*/
function onLoadError() {
if (!pdfError) {
// Impossible, but TypeScript doesn't know that
return;
}
warning(false, pdfError.toString());
if (onLoadErrorProps) {
onLoadErrorProps(pdfError);
}
}
// biome-ignore lint/correctness/useExhaustiveDependencies: useEffect intentionally triggered on source change
useEffect(
function resetDocument() {
pdfDispatch({ type: 'RESET' });
},
[pdfDispatch, source],
);
// biome-ignore lint/correctness/useExhaustiveDependencies: Ommitted callbacks so they are not called every time they change
useEffect(
function loadDocument() {
if (!source) {
return;
}
const documentInitParams: DocumentInitParameters = options
? { ...source, ...options }
: source;
const destroyable = pdfjs.getDocument(documentInitParams);
if (onLoadProgress) {
destroyable.onProgress = onLoadProgress;
}
if (onPassword) {
destroyable.onPassword = onPassword;
}
const loadingTask = destroyable;
const loadingPromise = loadingTask.promise
.then((nextPdf) => {
pdfDispatch({ type: 'RESOLVE', value: nextPdf });
})
.catch((error) => {
if (loadingTask.destroyed) {
return;
}
pdfDispatch({ type: 'REJECT', error });
});
return () => {
loadingPromise.finally(() => loadingTask.destroy());
};
},
[options, pdfDispatch, source],
);
// biome-ignore lint/correctness/useExhaustiveDependencies: Ommitted callbacks so they are not called every time they change
useEffect(() => {
if (typeof pdf === 'undefined') {
return;
}
if (pdf === false) {
onLoadError();
return;
}
onLoadSuccess();
}, [pdf]);
useEffect(
function setupLinkService() {
linkService.current.setViewer(viewer.current);
linkService.current.setExternalLinkRel(externalLinkRel);
linkService.current.setExternalLinkTarget(externalLinkTarget);
},
[externalLinkRel, externalLinkTarget],
);
const registerPage = useCallback((pageIndex: number, ref: HTMLDivElement) => {
pages.current[pageIndex] = ref;
}, []);
const unregisterPage = useCallback((pageIndex: number) => {
delete pages.current[pageIndex];
}, []);
const childContext = useMemo(
() => ({
imageResourcesPath,
linkService: linkService.current,
onItemClick,
pdf,
registerPage,
renderMode,
rotate,
unregisterPage,
}),
[imageResourcesPath, onItemClick, pdf, registerPage, renderMode, rotate, unregisterPage],
);
const eventProps = useMemo(
() => makeEventProps(otherProps, () => pdf),
// biome-ignore lint/correctness/useExhaustiveDependencies: FIXME
[otherProps, pdf],
);
function renderChildren() {
return <DocumentContext.Provider value={childContext}>{children}</DocumentContext.Provider>;
}
function renderContent() {
if (!file) {
return <Message type="no-data">{typeof noData === 'function' ? noData() : noData}</Message>;
}
if (pdf === undefined || pdf === null) {
return (
<Message type="loading">{typeof loading === 'function' ? loading() : loading}</Message>
);
}
if (pdf === false) {
return <Message type="error">{typeof error === 'function' ? error() : error}</Message>;
}
return renderChildren();
}
return (
<div
className={clsx('react-pdf__Document', className)}
// Assertion is needed for React 18 compatibility
ref={inputRef as React.Ref<HTMLDivElement>}
style={{
['--scale-factor' as string]: '1',
}}
{...eventProps}
>
{renderContent()}
</div>
);
});
export default Document;

10
node_modules/react-pdf/src/DocumentContext.tsx generated vendored Normal file
View File

@@ -0,0 +1,10 @@
'use client';
import { createContext } from 'react';
import type { DocumentContextType } from './shared/types.js';
const documentContext: React.Context<DocumentContextType> =
createContext<DocumentContextType>(null);
export default documentContext;

207
node_modules/react-pdf/src/LinkService.ts generated vendored Normal file
View File

@@ -0,0 +1,207 @@
/* Copyright 2015 Mozilla Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import invariant from 'tiny-invariant';
import type { PDFDocumentProxy } from 'pdfjs-dist';
import type {
Dest,
ResolvedDest,
ExternalLinkRel,
ExternalLinkTarget,
ScrollPageIntoViewArgs,
} from './shared/types.js';
import type { IPDFLinkService } from 'pdfjs-dist/types/web/interfaces.js';
const DEFAULT_LINK_REL = 'noopener noreferrer nofollow';
type PDFViewer = {
currentPageNumber?: number;
scrollPageIntoView: (args: ScrollPageIntoViewArgs) => void;
};
export default class LinkService implements IPDFLinkService {
externalLinkEnabled: boolean;
externalLinkRel?: ExternalLinkRel;
externalLinkTarget?: ExternalLinkTarget;
isInPresentationMode: boolean;
pdfDocument?: PDFDocumentProxy | null;
pdfViewer?: PDFViewer | null;
constructor() {
this.externalLinkEnabled = true;
this.externalLinkRel = undefined;
this.externalLinkTarget = undefined;
this.isInPresentationMode = false;
this.pdfDocument = undefined;
this.pdfViewer = undefined;
}
setDocument(pdfDocument: PDFDocumentProxy): void {
this.pdfDocument = pdfDocument;
}
setViewer(pdfViewer: PDFViewer): void {
this.pdfViewer = pdfViewer;
}
setExternalLinkRel(externalLinkRel?: ExternalLinkRel): void {
this.externalLinkRel = externalLinkRel;
}
setExternalLinkTarget(externalLinkTarget?: ExternalLinkTarget): void {
this.externalLinkTarget = externalLinkTarget;
}
setHistory(): void {
// Intentionally empty
}
get pagesCount(): number {
return this.pdfDocument ? this.pdfDocument.numPages : 0;
}
get page(): number {
invariant(this.pdfViewer, 'PDF viewer is not initialized.');
return this.pdfViewer.currentPageNumber || 0;
}
set page(value: number) {
invariant(this.pdfViewer, 'PDF viewer is not initialized.');
this.pdfViewer.currentPageNumber = value;
}
get rotation(): number {
return 0;
}
set rotation(_value) {
// Intentionally empty
}
goToDestination(dest: Dest): Promise<void> {
return new Promise<ResolvedDest | null>((resolve) => {
invariant(this.pdfDocument, 'PDF document not loaded.');
invariant(dest, 'Destination is not specified.');
if (typeof dest === 'string') {
this.pdfDocument.getDestination(dest).then(resolve);
} else if (Array.isArray(dest)) {
resolve(dest);
} else {
dest.then(resolve);
}
}).then((explicitDest) => {
invariant(Array.isArray(explicitDest), `"${explicitDest}" is not a valid destination array.`);
const destRef = explicitDest[0];
new Promise<number>((resolve) => {
invariant(this.pdfDocument, 'PDF document not loaded.');
if (destRef instanceof Object) {
this.pdfDocument
.getPageIndex(destRef)
.then((pageIndex) => {
resolve(pageIndex);
})
.catch(() => {
invariant(false, `"${destRef}" is not a valid page reference.`);
});
} else if (typeof destRef === 'number') {
resolve(destRef);
} else {
invariant(false, `"${destRef}" is not a valid destination reference.`);
}
}).then((pageIndex) => {
const pageNumber = pageIndex + 1;
invariant(this.pdfViewer, 'PDF viewer is not initialized.');
invariant(
pageNumber >= 1 && pageNumber <= this.pagesCount,
`"${pageNumber}" is not a valid page number.`,
);
this.pdfViewer.scrollPageIntoView({
dest: explicitDest,
pageIndex,
pageNumber,
});
});
});
}
navigateTo(dest: Dest): void {
this.goToDestination(dest);
}
goToPage(pageNumber: number): void {
const pageIndex = pageNumber - 1;
invariant(this.pdfViewer, 'PDF viewer is not initialized.');
invariant(
pageNumber >= 1 && pageNumber <= this.pagesCount,
`"${pageNumber}" is not a valid page number.`,
);
this.pdfViewer.scrollPageIntoView({
pageIndex,
pageNumber,
});
}
addLinkAttributes(link: HTMLAnchorElement, url: string, newWindow: boolean): void {
link.href = url;
link.rel = this.externalLinkRel || DEFAULT_LINK_REL;
link.target = newWindow ? '_blank' : this.externalLinkTarget || '';
}
getDestinationHash(): string {
return '#';
}
getAnchorUrl(): string {
return '#';
}
setHash(): void {
// Intentionally empty
}
executeNamedAction(): void {
// Intentionally empty
}
cachePageRef(): void {
// Intentionally empty
}
isPageVisible(): boolean {
return true;
}
isPageCached(): boolean {
return true;
}
executeSetOCGState(): void {
// Intentionally empty
}
}

8
node_modules/react-pdf/src/Message.tsx generated vendored Normal file
View File

@@ -0,0 +1,8 @@
type MessageProps = {
children?: React.ReactNode;
type: 'error' | 'loading' | 'no-data';
};
export default function Message({ children, type }: MessageProps): React.ReactElement {
return <div className={`react-pdf__message react-pdf__message--${type}`}>{children}</div>;
}

169
node_modules/react-pdf/src/Outline.spec.tsx generated vendored Normal file
View File

@@ -0,0 +1,169 @@
import { beforeAll, describe, expect, it } from 'vitest';
import { createRef } from 'react';
import { render, screen } from '@testing-library/react';
import { pdfjs } from './index.test.js';
import Outline from './Outline.js';
import failingPdf from '../../../__mocks__/_failing_pdf.js';
import { loadPDF, makeAsyncCallback, muteConsole, restoreConsole } from '../../../test-utils.js';
import DocumentContext from './DocumentContext.js';
import type { PDFDocumentProxy } from 'pdfjs-dist';
import type { DocumentContextType } from './shared/types.js';
type PDFOutline = Awaited<ReturnType<PDFDocumentProxy['getOutline']>>;
const pdfFile = loadPDF('./../../__mocks__/_pdf.pdf');
const pdfFile2 = loadPDF('./../../__mocks__/_pdf2.pdf');
function renderWithContext(children: React.ReactNode, context: Partial<DocumentContextType>) {
const { rerender, ...otherResult } = render(
<DocumentContext.Provider value={context as DocumentContextType}>
{children}
</DocumentContext.Provider>,
);
return {
...otherResult,
rerender: (
nextChildren: React.ReactNode,
nextContext: Partial<DocumentContextType> = context,
) =>
rerender(
<DocumentContext.Provider value={nextContext as DocumentContextType}>
{nextChildren}
</DocumentContext.Provider>,
),
};
}
describe('Outline', () => {
// Loaded PDF file
let pdf: PDFDocumentProxy;
let pdf2: PDFDocumentProxy;
// Object with basic loaded outline information that shall match after successful loading
let desiredLoadedOutline: PDFOutline;
let desiredLoadedOutline2: PDFOutline;
beforeAll(async () => {
pdf = await pdfjs.getDocument({ data: pdfFile.arrayBuffer }).promise;
pdf2 = await pdfjs.getDocument({ data: pdfFile2.arrayBuffer }).promise;
desiredLoadedOutline = await pdf.getOutline();
desiredLoadedOutline2 = await pdf2.getOutline();
});
describe('loading', () => {
it('loads an outline and calls onLoadSuccess callback properly when placed inside Document', async () => {
const { func: onLoadSuccess, promise: onLoadSuccessPromise } = makeAsyncCallback();
renderWithContext(<Outline onLoadSuccess={onLoadSuccess} />, { pdf });
expect.assertions(1);
await expect(onLoadSuccessPromise).resolves.toMatchObject([desiredLoadedOutline]);
});
it('loads an outline and calls onLoadSuccess callback properly when pdf prop is passed', async () => {
const { func: onLoadSuccess, promise: onLoadSuccessPromise } = makeAsyncCallback();
render(<Outline onLoadSuccess={onLoadSuccess} pdf={pdf} />);
expect.assertions(1);
await expect(onLoadSuccessPromise).resolves.toMatchObject([desiredLoadedOutline]);
});
it('calls onLoadError when failed to load an outline', async () => {
const { func: onLoadError, promise: onLoadErrorPromise } = makeAsyncCallback();
muteConsole();
renderWithContext(<Outline onLoadError={onLoadError} />, { pdf: failingPdf });
expect.assertions(1);
await expect(onLoadErrorPromise).resolves.toMatchObject([expect.any(Error)]);
restoreConsole();
});
it('replaces an outline properly when pdf is changed', async () => {
const { func: onLoadSuccess, promise: onLoadSuccessPromise } = makeAsyncCallback();
const { rerender } = renderWithContext(<Outline onLoadSuccess={onLoadSuccess} />, { pdf });
expect.assertions(2);
await expect(onLoadSuccessPromise).resolves.toMatchObject([desiredLoadedOutline]);
const { func: onLoadSuccess2, promise: onLoadSuccessPromise2 } = makeAsyncCallback();
rerender(<Outline onLoadSuccess={onLoadSuccess2} />, { pdf: pdf2 });
// It would have been .toMatchObject if not for the fact _pdf2.pdf has no outline
await expect(onLoadSuccessPromise2).resolves.toMatchObject([desiredLoadedOutline2]);
});
it('throws an error when placed outside Document without pdf prop passed', () => {
muteConsole();
expect(() => render(<Outline />)).toThrow();
restoreConsole();
});
});
describe('rendering', () => {
it('applies className to its wrapper when given a string', async () => {
const { func: onLoadSuccess, promise: onLoadSuccessPromise } = makeAsyncCallback();
const className = 'testClassName';
const { container } = renderWithContext(
<Outline className={className} onLoadSuccess={onLoadSuccess} />,
{ pdf },
);
expect.assertions(1);
await onLoadSuccessPromise;
const wrapper = container.querySelector('.react-pdf__Outline');
expect(wrapper).toHaveClass(className);
});
it('passes container element to inputRef properly', async () => {
const { func: onLoadSuccess, promise: onLoadSuccessPromise } = makeAsyncCallback();
const inputRef = createRef<HTMLDivElement>();
renderWithContext(<Outline inputRef={inputRef} onLoadSuccess={onLoadSuccess} />, { pdf });
expect.assertions(1);
await onLoadSuccessPromise;
expect(inputRef.current).toBeInstanceOf(HTMLDivElement);
});
it('renders OutlineItem components properly', async () => {
const { func: onLoadSuccess, promise: onLoadSuccessPromise } = makeAsyncCallback();
renderWithContext(<Outline onLoadSuccess={onLoadSuccess} />, { pdf });
expect.assertions(1);
await onLoadSuccessPromise;
const items = screen.getAllByRole('listitem');
expect(items).toHaveLength(5);
});
});
});

203
node_modules/react-pdf/src/Outline.tsx generated vendored Normal file
View File

@@ -0,0 +1,203 @@
'use client';
import { useEffect, useMemo } from 'react';
import makeCancellable from 'make-cancellable-promise';
import makeEventProps from 'make-event-props';
import clsx from 'clsx';
import invariant from 'tiny-invariant';
import warning from 'warning';
import OutlineContext from './OutlineContext.js';
import OutlineItem from './OutlineItem.js';
import { cancelRunningTask } from './shared/utils.js';
import useDocumentContext from './shared/hooks/useDocumentContext.js';
import useResolver from './shared/hooks/useResolver.js';
import type { PDFDocumentProxy } from 'pdfjs-dist';
import type { EventProps } from 'make-event-props';
import type { ClassName, OnItemClickArgs } from './shared/types.js';
type PDFOutline = Awaited<ReturnType<PDFDocumentProxy['getOutline']>>;
export type OutlineProps = {
/**
* Class name(s) that will be added to rendered element along with the default `react-pdf__Outline`.
*
* @example 'custom-class-name-1 custom-class-name-2'
* @example ['custom-class-name-1', 'custom-class-name-2']
*/
className?: ClassName;
/**
* A prop that behaves like [ref](https://reactjs.org/docs/refs-and-the-dom.html), but it's passed to main `<div>` rendered by `<Outline>` component.
*
* @example (ref) => { this.myOutline = ref; }
* @example this.ref
* @example ref
*/
inputRef?: React.Ref<HTMLDivElement>;
/**
* Function called when an outline item has been clicked. Usually, you would like to use this callback to move the user wherever they requested to.
*
* @example ({ dest, pageIndex, pageNumber }) => alert('Clicked an item from page ' + pageNumber + '!')
*/
onItemClick?: (props: OnItemClickArgs) => void;
/**
* Function called in case of an error while retrieving the outline.
*
* @example (error) => alert('Error while retrieving the outline! ' + error.message)
*/
onLoadError?: (error: Error) => void;
/**
* Function called when the outline is successfully retrieved.
*
* @example (outline) => alert('The outline has been successfully retrieved.')
*/
onLoadSuccess?: (outline: PDFOutline | null) => void;
pdf?: PDFDocumentProxy | false;
} & EventProps<PDFOutline | null | false | undefined>;
/**
* Displays an outline (table of contents).
*
* Should be placed inside `<Document />`. Alternatively, it can have `pdf` prop passed, which can be obtained from `<Document />`'s `onLoadSuccess` callback function.
*/
export default function Outline(props: OutlineProps): React.ReactElement | null {
const documentContext = useDocumentContext();
const mergedProps = { ...documentContext, ...props };
const {
className,
inputRef,
onItemClick,
onLoadError: onLoadErrorProps,
onLoadSuccess: onLoadSuccessProps,
pdf,
...otherProps
} = mergedProps;
invariant(
pdf,
'Attempted to load an outline, but no document was specified. Wrap <Outline /> in a <Document /> or pass explicit `pdf` prop.',
);
const [outlineState, outlineDispatch] = useResolver<PDFOutline | null>();
const { value: outline, error: outlineError } = outlineState;
/**
* Called when an outline is read successfully
*/
function onLoadSuccess() {
if (typeof outline === 'undefined' || outline === false) {
return;
}
if (onLoadSuccessProps) {
onLoadSuccessProps(outline);
}
}
/**
* Called when an outline failed to read successfully
*/
function onLoadError() {
if (!outlineError) {
// Impossible, but TypeScript doesn't know that
return;
}
warning(false, outlineError.toString());
if (onLoadErrorProps) {
onLoadErrorProps(outlineError);
}
}
// biome-ignore lint/correctness/useExhaustiveDependencies: useEffect intentionally triggered on pdf change
useEffect(
function resetOutline() {
outlineDispatch({ type: 'RESET' });
},
[outlineDispatch, pdf],
);
useEffect(
function loadOutline() {
if (!pdf) {
// Impossible, but TypeScript doesn't know that
return;
}
const cancellable = makeCancellable(pdf.getOutline());
const runningTask = cancellable;
cancellable.promise
.then((nextOutline) => {
outlineDispatch({ type: 'RESOLVE', value: nextOutline });
})
.catch((error) => {
outlineDispatch({ type: 'REJECT', error });
});
return () => cancelRunningTask(runningTask);
},
[outlineDispatch, pdf],
);
// biome-ignore lint/correctness/useExhaustiveDependencies: Ommitted callbacks so they are not called every time they change
useEffect(() => {
if (outline === undefined) {
return;
}
if (outline === false) {
onLoadError();
return;
}
onLoadSuccess();
}, [outline]);
const childContext = useMemo(
() => ({
onItemClick,
}),
[onItemClick],
);
const eventProps = useMemo(
() => makeEventProps(otherProps, () => outline),
// biome-ignore lint/correctness/useExhaustiveDependencies: FIXME
[otherProps, outline],
);
if (!outline) {
return null;
}
function renderOutline() {
if (!outline) {
return null;
}
return (
<ul>
{outline.map((item, itemIndex) => (
<OutlineItem
key={typeof item.dest === 'string' ? item.dest : itemIndex}
item={item}
pdf={pdf}
/>
))}
</ul>
);
}
return (
<div className={clsx('react-pdf__Outline', className)} ref={inputRef} {...eventProps}>
<OutlineContext.Provider value={childContext}>{renderOutline()}</OutlineContext.Provider>
</div>
);
}

9
node_modules/react-pdf/src/OutlineContext.tsx generated vendored Normal file
View File

@@ -0,0 +1,9 @@
'use client';
import { createContext } from 'react';
import type { OutlineContextType } from './shared/types.js';
const outlineContext: React.Context<OutlineContextType> = createContext<OutlineContextType>(null);
export default outlineContext;

128
node_modules/react-pdf/src/OutlineItem.spec.tsx generated vendored Normal file
View File

@@ -0,0 +1,128 @@
import { beforeAll, describe, expect, it, vi } from 'vitest';
import { fireEvent, getAllByRole, render, screen } from '@testing-library/react';
import { pdfjs } from './index.test.js';
import OutlineItem from './OutlineItem.js';
import { loadPDF, makeAsyncCallback } from '../../../test-utils.js';
import DocumentContext from './DocumentContext.js';
import OutlineContext from './OutlineContext.js';
import type { PDFDocumentProxy } from 'pdfjs-dist';
import type { DocumentContextType, OutlineContextType } from './shared/types.js';
const pdfFile = loadPDF('./../../__mocks__/_pdf.pdf');
type PDFOutline = Awaited<ReturnType<PDFDocumentProxy['getOutline']>>;
type PDFOutlineItem = PDFOutline[number];
function renderWithContext(
children: React.ReactNode,
documentContext: Partial<DocumentContextType>,
outlineContext: Partial<OutlineContextType>,
) {
const { rerender, ...otherResult } = render(
<DocumentContext.Provider value={documentContext as DocumentContextType}>
<OutlineContext.Provider value={outlineContext as OutlineContextType}>
{children}
</OutlineContext.Provider>
</DocumentContext.Provider>,
);
return {
...otherResult,
rerender: (
nextChildren: React.ReactNode,
nextDocumentContext: Partial<DocumentContextType> = documentContext,
nextOutlineContext: Partial<OutlineContextType> = outlineContext,
) =>
rerender(
<DocumentContext.Provider value={nextDocumentContext as DocumentContextType}>
<OutlineContext.Provider value={nextOutlineContext as OutlineContextType}>
{nextChildren}
</OutlineContext.Provider>
</DocumentContext.Provider>,
),
};
}
describe('OutlineItem', () => {
// Loaded PDF file
let pdf: PDFDocumentProxy;
// Object with basic loaded outline item information
let outlineItem: PDFOutlineItem;
beforeAll(async () => {
pdf = await pdfjs.getDocument({ data: pdfFile.arrayBuffer }).promise;
const outlineItems = await pdf.getOutline();
[outlineItem] = outlineItems as [PDFOutlineItem];
});
describe('rendering', () => {
it('renders an item properly', () => {
const onItemClick = vi.fn();
renderWithContext(<OutlineItem item={outlineItem} />, { pdf }, { onItemClick });
const item = screen.getAllByRole('listitem')[0];
expect(item).toHaveTextContent(outlineItem.title);
});
it("renders item's subitems properly", () => {
const onItemClick = vi.fn();
renderWithContext(<OutlineItem item={outlineItem} />, { pdf }, { onItemClick });
const item = screen.getAllByRole('listitem')[0] as HTMLElement;
const subitems = getAllByRole(item, 'listitem');
expect(subitems).toHaveLength(outlineItem.items.length);
});
it('calls onItemClick with proper arguments when clicked a link', async () => {
const { func: onItemClick, promise: onItemClickPromise } = makeAsyncCallback();
renderWithContext(<OutlineItem item={outlineItem} />, { pdf }, { onItemClick });
const item = screen.getAllByRole('listitem')[0] as HTMLElement;
const link = getAllByRole(item, 'link')[0] as HTMLAnchorElement;
fireEvent.click(link);
await onItemClickPromise;
expect(onItemClick).toHaveBeenCalled();
});
it('calls onItemClick with proper arguments multiple times when clicked a link multiple times', async () => {
const { func: onItemClick, promise: onItemClickPromise } = makeAsyncCallback();
const { rerender } = renderWithContext(
<OutlineItem item={outlineItem} />,
{ pdf },
{ onItemClick },
);
const item = screen.getAllByRole('listitem')[0] as HTMLElement;
const link = getAllByRole(item, 'link')[0] as HTMLAnchorElement;
fireEvent.click(link);
await onItemClickPromise;
expect(onItemClick).toHaveBeenCalledTimes(1);
const { func: onItemClick2, promise: onItemClickPromise2 } = makeAsyncCallback();
rerender(<OutlineItem item={outlineItem} />, { pdf }, { onItemClick: onItemClick2 });
fireEvent.click(link);
await onItemClickPromise2;
expect(onItemClick2).toHaveBeenCalledTimes(1);
});
});
});

115
node_modules/react-pdf/src/OutlineItem.tsx generated vendored Normal file
View File

@@ -0,0 +1,115 @@
import invariant from 'tiny-invariant';
import Ref from './Ref.js';
import useCachedValue from './shared/hooks/useCachedValue.js';
import useDocumentContext from './shared/hooks/useDocumentContext.js';
import useOutlineContext from './shared/hooks/useOutlineContext.js';
import type { PDFDocumentProxy } from 'pdfjs-dist';
import type { RefProxy } from 'pdfjs-dist/types/src/display/api.js';
type PDFOutline = Awaited<ReturnType<PDFDocumentProxy['getOutline']>>;
type PDFOutlineItem = PDFOutline[number];
type OutlineItemProps = {
item: PDFOutlineItem;
pdf?: PDFDocumentProxy | false;
};
export default function OutlineItem(props: OutlineItemProps): React.ReactElement {
const documentContext = useDocumentContext();
const outlineContext = useOutlineContext();
invariant(outlineContext, 'Unable to find Outline context.');
const mergedProps = { ...documentContext, ...outlineContext, ...props };
const { item, linkService, onItemClick, pdf, ...otherProps } = mergedProps;
invariant(
pdf,
'Attempted to load an outline, but no document was specified. Wrap <Outline /> in a <Document /> or pass explicit `pdf` prop.',
);
const getDestination = useCachedValue(() => {
if (typeof item.dest === 'string') {
return pdf.getDestination(item.dest);
}
return item.dest;
});
const getPageIndex = useCachedValue(async () => {
const destination = await getDestination();
if (!destination) {
throw new Error('Destination not found.');
}
const [ref] = destination as [RefProxy];
return pdf.getPageIndex(new Ref(ref));
});
const getPageNumber = useCachedValue(async () => {
const pageIndex = await getPageIndex();
return pageIndex + 1;
});
function onClick(event: React.MouseEvent<HTMLAnchorElement>) {
event.preventDefault();
invariant(
onItemClick || linkService,
'Either onItemClick callback or linkService must be defined in order to navigate to an outline item.',
);
if (onItemClick) {
Promise.all([getDestination(), getPageIndex(), getPageNumber()]).then(
([dest, pageIndex, pageNumber]) => {
onItemClick({
dest,
pageIndex,
pageNumber,
});
},
);
} else if (linkService) {
linkService.goToDestination(item.dest);
}
}
function renderSubitems() {
if (!item.items || !item.items.length) {
return null;
}
const { items: subitems } = item;
return (
<ul>
{subitems.map((subitem, subitemIndex) => (
<OutlineItem
key={typeof subitem.dest === 'string' ? subitem.dest : subitemIndex}
item={subitem}
pdf={pdf}
{...otherProps}
/>
))}
</ul>
);
}
return (
<li>
{/* biome-ignore lint/a11y/useValidAnchor: We can't provide real href here */}
<a href="#" onClick={onClick}>
{item.title}
</a>
{renderSubitems()}
</li>
);
}

978
node_modules/react-pdf/src/Page.spec.tsx generated vendored Normal file
View File

@@ -0,0 +1,978 @@
import { beforeAll, describe, expect, it, vi } from 'vitest';
import { createRef } from 'react';
import { fireEvent, render } from '@testing-library/react';
import { pdfjs } from './index.test.js';
import Page from './Page.js';
import LinkService from './LinkService.js';
import failingPdf from '../../../__mocks__/_failing_pdf.js';
import silentlyFailingPdf from '../../../__mocks__/_silently_failing_pdf.js';
import { loadPDF, makeAsyncCallback, muteConsole, restoreConsole } from '../../../test-utils.js';
import DocumentContext from './DocumentContext.js';
import type { PDFDocumentProxy, PDFPageProxy } from 'pdfjs-dist';
import type { DocumentContextType, PageCallback } from './shared/types.js';
const pdfFile = loadPDF('./../../__mocks__/_pdf.pdf');
const pdfFile2 = loadPDF('./../../__mocks__/_pdf2.pdf');
const pdfFile4 = loadPDF('./../../__mocks__/_pdf4.pdf');
function renderWithContext(children: React.ReactNode, context: Partial<DocumentContextType>) {
const { rerender, ...otherResult } = render(
<DocumentContext.Provider value={context as DocumentContextType}>
{children}
</DocumentContext.Provider>,
);
return {
...otherResult,
rerender: (
nextChildren: React.ReactNode,
nextContext: Partial<DocumentContextType> = context,
) =>
rerender(
<DocumentContext.Provider value={nextContext as DocumentContextType}>
{nextChildren}
</DocumentContext.Provider>,
),
};
}
describe('Page', () => {
const linkService = new LinkService();
// Loaded PDF file
let pdf: PDFDocumentProxy;
let pdf2: PDFDocumentProxy;
let pdf4: PDFDocumentProxy;
// Object with basic loaded page information that shall match after successful loading
const desiredLoadedPage: Partial<PDFPageProxy> = {};
const desiredLoadedPage2: Partial<PDFPageProxy> = {};
const desiredLoadedPage3: Partial<PDFPageProxy> = {};
// Callbacks used in registerPage and unregisterPage callbacks
let registerPageArguments: [number, HTMLDivElement];
let unregisterPageArguments: [number];
beforeAll(async () => {
pdf = await pdfjs.getDocument({ data: pdfFile.arrayBuffer }).promise;
const page = await pdf.getPage(1);
desiredLoadedPage._pageIndex = page._pageIndex;
desiredLoadedPage._pageInfo = page._pageInfo;
const page2 = await pdf.getPage(2);
desiredLoadedPage2._pageIndex = page2._pageIndex;
desiredLoadedPage2._pageInfo = page2._pageInfo;
pdf2 = await pdfjs.getDocument({ data: pdfFile2.arrayBuffer }).promise;
const page3 = await pdf2.getPage(1);
desiredLoadedPage3._pageIndex = page3._pageIndex;
desiredLoadedPage3._pageInfo = page3._pageInfo;
registerPageArguments = [page._pageIndex, expect.any(HTMLDivElement)];
unregisterPageArguments = [page._pageIndex];
pdf4 = await pdfjs.getDocument({ data: pdfFile4.arrayBuffer }).promise;
});
describe('loading', () => {
it('loads a page and calls onLoadSuccess callback properly when placed inside Document', async () => {
const { func: onLoadSuccess, promise: onLoadSuccessPromise } = makeAsyncCallback();
renderWithContext(<Page onLoadSuccess={onLoadSuccess} pageIndex={0} />, {
linkService,
pdf,
});
expect.assertions(1);
await expect(onLoadSuccessPromise).resolves.toMatchObject([desiredLoadedPage]);
});
it('loads a page and calls onLoadSuccess callback properly when pdf prop is passed', async () => {
const { func: onLoadSuccess, promise: onLoadSuccessPromise } = makeAsyncCallback();
render(
<Page
onLoadSuccess={onLoadSuccess}
pageIndex={0}
pdf={pdf}
renderAnnotationLayer={false}
/>,
);
expect.assertions(1);
await expect(onLoadSuccessPromise).resolves.toMatchObject([desiredLoadedPage]);
});
it('returns all desired parameters in onLoadSuccess callback', async () => {
const { func: onLoadSuccess, promise: onLoadSuccessPromise } =
makeAsyncCallback<[PageCallback]>();
renderWithContext(<Page onLoadSuccess={onLoadSuccess} pageIndex={0} />, {
linkService,
pdf,
});
expect.assertions(5);
const [page] = await onLoadSuccessPromise;
expect(page.width).toBeDefined();
expect(page.height).toBeDefined();
expect(page.originalWidth).toBeDefined();
expect(page.originalHeight).toBeDefined();
// Example of a method that got stripped away in the past
expect(page.getTextContent).toBeInstanceOf(Function);
});
it('calls onLoadError when failed to load a page', async () => {
const { func: onLoadError, promise: onLoadErrorPromise } = makeAsyncCallback();
muteConsole();
renderWithContext(<Page onLoadError={onLoadError} pageIndex={0} />, {
linkService,
pdf: failingPdf,
});
expect.assertions(1);
await expect(onLoadErrorPromise).resolves.toMatchObject([expect.any(Error)]);
restoreConsole();
});
it('loads page when given pageIndex', async () => {
const { func: onLoadSuccess, promise: onLoadSuccessPromise } = makeAsyncCallback();
renderWithContext(<Page onLoadSuccess={onLoadSuccess} pageIndex={0} />, {
linkService,
pdf,
});
expect.assertions(1);
const [page] = await onLoadSuccessPromise;
expect(page).toMatchObject(desiredLoadedPage);
});
it('loads page when given pageNumber', async () => {
const { func: onLoadSuccess, promise: onLoadSuccessPromise } = makeAsyncCallback();
renderWithContext(<Page onLoadSuccess={onLoadSuccess} pageNumber={1} />, {
linkService,
pdf,
});
expect.assertions(1);
const [page] = await onLoadSuccessPromise;
expect(page).toMatchObject(desiredLoadedPage);
});
it('loads page of a given number when given conflicting pageNumber and pageIndex', async () => {
const { func: onLoadSuccess, promise: onLoadSuccessPromise } = makeAsyncCallback();
renderWithContext(<Page onLoadSuccess={onLoadSuccess} pageIndex={1} pageNumber={1} />, {
linkService,
pdf,
});
expect.assertions(1);
const [page] = await onLoadSuccessPromise;
expect(page).toMatchObject(desiredLoadedPage);
});
it('calls registerPage when loaded a page', async () => {
const { func: registerPage, promise: registerPagePromise } = makeAsyncCallback();
renderWithContext(<Page pageIndex={0} />, {
linkService,
pdf,
registerPage,
});
expect.assertions(1);
await expect(registerPagePromise).resolves.toMatchObject(registerPageArguments);
});
it('calls unregisterPage on unmount', async () => {
const { func: unregisterPage, promise: nuregisterPagePromise } = makeAsyncCallback();
const { unmount } = renderWithContext(<Page pageIndex={0} />, {
linkService,
pdf,
unregisterPage,
});
unmount();
expect.assertions(1);
await expect(nuregisterPagePromise).resolves.toMatchObject(unregisterPageArguments);
});
it('replaces a page properly when pdf is changed', async () => {
const { func: onLoadSuccess, promise: onLoadSuccessPromise } = makeAsyncCallback();
const { rerender } = renderWithContext(<Page onLoadSuccess={onLoadSuccess} pageIndex={0} />, {
linkService,
pdf,
});
expect.assertions(2);
await expect(onLoadSuccessPromise).resolves.toMatchObject([desiredLoadedPage]);
const { func: onLoadSuccess2, promise: onLoadSuccessPromise2 } = makeAsyncCallback();
rerender(<Page onLoadSuccess={onLoadSuccess2} pageIndex={0} />, {
linkService,
pdf: pdf2,
});
await expect(onLoadSuccessPromise2).resolves.toMatchObject([desiredLoadedPage3]);
});
it('replaces a page properly when pageNumber is changed', async () => {
const { func: onLoadSuccess, promise: onLoadSuccessPromise } = makeAsyncCallback();
const { rerender } = renderWithContext(<Page onLoadSuccess={onLoadSuccess} pageIndex={0} />, {
linkService,
pdf,
});
expect.assertions(2);
await expect(onLoadSuccessPromise).resolves.toMatchObject([desiredLoadedPage]);
const { func: onLoadSuccess2, promise: onLoadSuccessPromise2 } = makeAsyncCallback();
rerender(<Page onLoadSuccess={onLoadSuccess2} pageIndex={1} />, {
linkService,
pdf,
});
await expect(onLoadSuccessPromise2).resolves.toMatchObject([desiredLoadedPage2]);
});
it('throws an error when placed outside Document without pdf prop passed', () => {
muteConsole();
expect(() => render(<Page pageIndex={0} />)).toThrow();
restoreConsole();
});
});
describe('rendering', () => {
it('applies className to its wrapper when given a string', () => {
const className = 'testClassName';
const { container } = renderWithContext(<Page className={className} pageIndex={0} />, {
linkService,
pdf,
});
const wrapper = container.querySelector('.react-pdf__Page');
expect(wrapper).toHaveClass(className);
});
it('passes container element to inputRef properly', () => {
const inputRef = createRef<HTMLDivElement>();
renderWithContext(<Page inputRef={inputRef} pageIndex={1} />, {
linkService,
pdf: silentlyFailingPdf,
});
expect(inputRef.current).toBeInstanceOf(HTMLDivElement);
});
it('passes canvas element to Canvas properly', async () => {
const { func: onLoadSuccess, promise: onLoadSuccessPromise } = makeAsyncCallback();
const canvasRef = createRef<HTMLCanvasElement>();
const { container } = renderWithContext(
<Page canvasRef={canvasRef} onLoadSuccess={onLoadSuccess} pageIndex={0} />,
{
linkService,
pdf,
},
);
expect.assertions(1);
await onLoadSuccessPromise;
const pageCanvas = container.querySelector('.react-pdf__Page__canvas');
expect(canvasRef.current).toBe(pageCanvas);
});
it('renders "No page specified." when given neither pageIndex nor pageNumber', () => {
muteConsole();
const { container } = renderWithContext(<Page />, {
linkService,
pdf,
});
const noData = container.querySelector('.react-pdf__message');
expect(noData).toBeInTheDocument();
expect(noData).toHaveTextContent('No page specified.');
restoreConsole();
});
it('renders custom no data message when given nothing and noData is given', () => {
muteConsole();
const { container } = renderWithContext(<Page noData="Nothing here" />, {
linkService,
pdf,
});
const noData = container.querySelector('.react-pdf__message');
expect(noData).toBeInTheDocument();
expect(noData).toHaveTextContent('Nothing here');
restoreConsole();
});
it('renders custom no data message when given nothing and noData is given as a function', () => {
muteConsole();
const { container } = renderWithContext(<Page noData={() => 'Nothing here'} />, {
linkService,
pdf,
});
const noData = container.querySelector('.react-pdf__message');
expect(noData).toBeInTheDocument();
expect(noData).toHaveTextContent('Nothing here');
restoreConsole();
});
it('renders "Loading page…" when loading a page', async () => {
const { container } = renderWithContext(<Page pageIndex={0} />, {
linkService,
pdf,
});
const loading = container.querySelector('.react-pdf__message');
expect(loading).toBeInTheDocument();
expect(loading).toHaveTextContent('Loading page…');
});
it('renders custom loading message when loading a page and loading prop is given', async () => {
const { container } = renderWithContext(<Page loading="Loading" pageIndex={0} />, {
linkService,
pdf,
});
const loading = container.querySelector('.react-pdf__message');
expect(loading).toBeInTheDocument();
expect(loading).toHaveTextContent('Loading');
});
it('renders custom loading message when loading a page and loading prop is given as a function', async () => {
const { container } = renderWithContext(<Page loading={() => 'Loading'} pageIndex={0} />, {
linkService,
pdf,
});
const loading = container.querySelector('.react-pdf__message');
expect(loading).toBeInTheDocument();
expect(loading).toHaveTextContent('Loading');
});
it('ignores pageIndex when given pageIndex and pageNumber', async () => {
const { func: onLoadSuccess, promise: onLoadSuccessPromise } = makeAsyncCallback();
renderWithContext(<Page onLoadSuccess={onLoadSuccess} pageIndex={1} pageNumber={1} />, {
linkService,
pdf,
});
expect.assertions(1);
const [page] = await onLoadSuccessPromise;
expect(page).toMatchObject(desiredLoadedPage);
});
it('requests page to be rendered with default rotation when given nothing', async () => {
const { func: onRenderSuccess, promise: onRenderSuccessPromise } =
makeAsyncCallback<[PageCallback]>();
const { container } = renderWithContext(
<Page onRenderSuccess={onRenderSuccess} pageIndex={0} />,
{
linkService,
pdf,
},
);
const [page] = await onRenderSuccessPromise;
const pageCanvas = container.querySelector('.react-pdf__Page__canvas') as HTMLCanvasElement;
const { width, height } = window.getComputedStyle(pageCanvas);
const viewport = page.getViewport({ scale: 1 });
// Expect the canvas layer not to be rotated
expect(Number.parseInt(width, 10)).toBe(Math.floor(viewport.width));
expect(Number.parseInt(height, 10)).toBe(Math.floor(viewport.height));
});
it('requests page to be rendered with given rotation when given rotate prop', async () => {
const { func: onRenderSuccess, promise: onRenderSuccessPromise } =
makeAsyncCallback<[PageCallback]>();
const rotate = 90;
const { container } = renderWithContext(
<Page onRenderSuccess={onRenderSuccess} pageIndex={0} rotate={rotate} />,
{
linkService,
pdf,
},
);
const [page] = await onRenderSuccessPromise;
const pageCanvas = container.querySelector('.react-pdf__Page__canvas') as HTMLCanvasElement;
const { width, height } = window.getComputedStyle(pageCanvas);
const viewport = page.getViewport({ scale: 1, rotation: rotate });
// Expect the canvas layer to be rotated
expect(Number.parseInt(width, 10)).toBe(Math.floor(viewport.width));
expect(Number.parseInt(height, 10)).toBe(Math.floor(viewport.height));
});
it('requests page to be rendered in canvas mode by default', async () => {
const { func: onLoadSuccess, promise: onLoadSuccessPromise } = makeAsyncCallback();
const { container } = renderWithContext(
<Page onLoadSuccess={onLoadSuccess} pageIndex={0} />,
{
linkService,
pdf,
},
);
expect.assertions(1);
await onLoadSuccessPromise;
const pageCanvas = container.querySelector('.react-pdf__Page__canvas');
expect(pageCanvas).toBeInTheDocument();
});
it('requests page not to be rendered when given renderMode = "none"', async () => {
const { func: onLoadSuccess, promise: onLoadSuccessPromise } = makeAsyncCallback();
const { container } = renderWithContext(
<Page onLoadSuccess={onLoadSuccess} pageIndex={0} renderMode="none" />,
{
linkService,
pdf,
},
);
expect.assertions(1);
await onLoadSuccessPromise;
const pageCanvas = container.querySelector('.react-pdf__Page__canvas');
expect(pageCanvas).not.toBeInTheDocument();
});
it('requests page to be rendered in canvas mode when given renderMode = "canvas"', async () => {
const { func: onLoadSuccess, promise: onLoadSuccessPromise } = makeAsyncCallback();
const { container } = renderWithContext(
<Page onLoadSuccess={onLoadSuccess} pageIndex={0} renderMode="canvas" />,
{
linkService,
pdf,
},
);
expect.assertions(1);
await onLoadSuccessPromise;
const pageCanvas = container.querySelector('.react-pdf__Page__canvas');
expect(pageCanvas).toBeInTheDocument();
});
it('requests page to be rendered in custom mode when given renderMode = "custom"', async () => {
const { func: onLoadSuccess, promise: onLoadSuccessPromise } = makeAsyncCallback();
function CustomRenderer() {
return <div className="custom-renderer" />;
}
const { container } = renderWithContext(
<Page
customRenderer={CustomRenderer}
onLoadSuccess={onLoadSuccess}
pageIndex={0}
renderMode="custom"
/>,
{
linkService,
pdf,
},
);
expect.assertions(1);
await onLoadSuccessPromise;
const customRenderer = container.querySelector('.custom-renderer');
expect(customRenderer).toBeInTheDocument();
});
it('requests text content to be rendered by default', async () => {
const { func: onLoadSuccess, promise: onLoadSuccessPromise } = makeAsyncCallback();
const { container } = renderWithContext(
<Page onLoadSuccess={onLoadSuccess} pageIndex={0} />,
{
linkService,
pdf,
},
);
expect.assertions(1);
await onLoadSuccessPromise;
const textLayer = container.querySelector('.react-pdf__Page__textContent');
expect(textLayer).toBeInTheDocument();
});
it('requests text content to be rendered when given renderTextLayer = true', async () => {
const { func: onLoadSuccess, promise: onLoadSuccessPromise } = makeAsyncCallback();
const { container } = renderWithContext(
<Page onLoadSuccess={onLoadSuccess} pageIndex={0} renderTextLayer />,
{
linkService,
pdf,
},
);
expect.assertions(1);
await onLoadSuccessPromise;
const textLayer = container.querySelector('.react-pdf__Page__textContent');
expect(textLayer).toBeInTheDocument();
});
it('does not request text content to be rendered when given renderTextLayer = false', async () => {
const { func: onLoadSuccess, promise: onLoadSuccessPromise } = makeAsyncCallback();
const { container } = renderWithContext(
<Page onLoadSuccess={onLoadSuccess} pageIndex={0} renderTextLayer={false} />,
{
linkService,
pdf,
},
);
expect.assertions(1);
await onLoadSuccessPromise;
const textLayer = container.querySelector('.react-pdf__Page__textContent');
expect(textLayer).not.toBeInTheDocument();
});
it('renders TextLayer when given renderMode = "canvas"', async () => {
const { func: onLoadSuccess, promise: onLoadSuccessPromise } = makeAsyncCallback();
const { container } = renderWithContext(
<Page onLoadSuccess={onLoadSuccess} pageIndex={0} renderMode="canvas" renderTextLayer />,
{
linkService,
pdf,
},
);
expect.assertions(1);
await onLoadSuccessPromise;
const textLayer = container.querySelector('.react-pdf__Page__textContent');
expect(textLayer).toBeInTheDocument();
});
it('renders TextLayer when given renderMode = "custom"', async () => {
const { func: onLoadSuccess, promise: onLoadSuccessPromise } = makeAsyncCallback();
function CustomRenderer() {
return <div className="custom-renderer" />;
}
const { container } = renderWithContext(
<Page
customRenderer={CustomRenderer}
onLoadSuccess={onLoadSuccess}
pageIndex={0}
renderMode="custom"
renderTextLayer
/>,
{
linkService,
pdf,
},
);
expect.assertions(1);
await onLoadSuccessPromise;
const textLayer = container.querySelector('.react-pdf__Page__textContent');
expect(textLayer).toBeInTheDocument();
});
it('requests annotations to be rendered by default', async () => {
const { func: onLoadSuccess, promise: onLoadSuccessPromise } = makeAsyncCallback();
const { container } = renderWithContext(
<Page onLoadSuccess={onLoadSuccess} pageIndex={0} />,
{
linkService,
pdf,
},
);
expect.assertions(1);
await onLoadSuccessPromise;
const annotationLayer = container.querySelector('.react-pdf__Page__annotations');
expect(annotationLayer).toBeInTheDocument();
});
it('requests annotations to be rendered when given renderAnnotationLayer = true', async () => {
const { func: onLoadSuccess, promise: onLoadSuccessPromise } = makeAsyncCallback();
const { container } = renderWithContext(
<Page onLoadSuccess={onLoadSuccess} pageIndex={0} renderAnnotationLayer />,
{
linkService,
pdf,
},
);
expect.assertions(1);
await onLoadSuccessPromise;
const annotationLayer = container.querySelector('.react-pdf__Page__annotations');
expect(annotationLayer).toBeInTheDocument();
});
it('does not request annotations to be rendered when given renderAnnotationLayer = false', async () => {
const { func: onLoadSuccess, promise: onLoadSuccessPromise } = makeAsyncCallback();
const { container } = renderWithContext(
<Page onLoadSuccess={onLoadSuccess} pageIndex={0} renderAnnotationLayer={false} />,
{
linkService,
pdf,
},
);
expect.assertions(1);
await onLoadSuccessPromise;
const annotationLayer = container.querySelector('.react-pdf__Page__annotations');
expect(annotationLayer).not.toBeInTheDocument();
});
});
it('requests page to be rendered without forms by default', async () => {
const { func: onRenderAnnotationLayerSuccess, promise: onRenderAnnotationLayerSuccessPromise } =
makeAsyncCallback();
const { container } = renderWithContext(
<Page
onRenderAnnotationLayerSuccess={onRenderAnnotationLayerSuccess}
pageIndex={0}
renderMode="none"
/>,
{
linkService,
pdf: pdf4,
},
);
expect.assertions(1);
await onRenderAnnotationLayerSuccessPromise;
const textWidgetAnnotation = container.querySelector('.textWidgetAnnotation');
expect(textWidgetAnnotation).toBeFalsy();
});
it('requests page to be rendered with forms given renderForms = true', async () => {
const { func: onRenderAnnotationLayerSuccess, promise: onRenderAnnotationLayerSuccessPromise } =
makeAsyncCallback();
const { container } = renderWithContext(
<Page
onRenderAnnotationLayerSuccess={onRenderAnnotationLayerSuccess}
pageIndex={0}
renderForms
renderMode="none"
/>,
{
linkService,
pdf: pdf4,
},
);
expect.assertions(1);
await onRenderAnnotationLayerSuccessPromise;
const textWidgetAnnotation = container.querySelector('.textWidgetAnnotation');
expect(textWidgetAnnotation).toBeTruthy();
});
it('requests page to be rendered at its original size given nothing', async () => {
const { func: onLoadSuccess, promise: onLoadSuccessPromise } =
makeAsyncCallback<[PageCallback]>();
renderWithContext(<Page onLoadSuccess={onLoadSuccess} pageIndex={0} />, {
linkService,
pdf,
});
expect.assertions(1);
const [page] = await onLoadSuccessPromise;
expect(page.width).toEqual(page.originalWidth);
});
it('requests page to be rendered with a proper scale when given scale', async () => {
const { func: onLoadSuccess, promise: onLoadSuccessPromise } =
makeAsyncCallback<[PageCallback]>();
const scale = 1.5;
renderWithContext(<Page onLoadSuccess={onLoadSuccess} pageIndex={0} scale={scale} />, {
linkService,
pdf,
});
expect.assertions(1);
const [page] = await onLoadSuccessPromise;
expect(page.width).toEqual(page.originalWidth * scale);
});
it('requests page to be rendered with a proper scale when given width', async () => {
const { func: onLoadSuccess, promise: onLoadSuccessPromise } =
makeAsyncCallback<[PageCallback]>();
const width = 600;
renderWithContext(<Page onLoadSuccess={onLoadSuccess} pageIndex={0} width={width} />, {
linkService,
pdf,
});
expect.assertions(1);
const [page] = await onLoadSuccessPromise;
expect(page.width).toEqual(width);
});
it('requests page to be rendered with a proper scale when given width and scale (multiplies)', async () => {
const { func: onLoadSuccess, promise: onLoadSuccessPromise } =
makeAsyncCallback<[PageCallback]>();
const width = 600;
const scale = 1.5;
renderWithContext(
<Page onLoadSuccess={onLoadSuccess} pageIndex={0} scale={scale} width={width} />,
{
linkService,
pdf,
},
);
expect.assertions(1);
const [page] = await onLoadSuccessPromise;
expect(page.width).toBeCloseTo(width * scale);
});
it('requests page to be rendered with a proper scale when given height', async () => {
const { func: onLoadSuccess, promise: onLoadSuccessPromise } =
makeAsyncCallback<[PageCallback]>();
const height = 850;
renderWithContext(<Page height={height} onLoadSuccess={onLoadSuccess} pageIndex={0} />, {
linkService,
pdf,
});
expect.assertions(1);
const [page] = await onLoadSuccessPromise;
expect(page.height).toEqual(height);
});
it('requests page to be rendered with a proper scale when given height and scale (multiplies)', async () => {
const { func: onLoadSuccess, promise: onLoadSuccessPromise } =
makeAsyncCallback<[PageCallback]>();
const height = 850;
const scale = 1.5;
renderWithContext(
<Page height={height} onLoadSuccess={onLoadSuccess} pageIndex={0} scale={scale} />,
{
linkService,
pdf,
},
);
expect.assertions(1);
const [page] = await onLoadSuccessPromise;
expect(page.height).toBeCloseTo(height * scale);
});
it('requests page to be rendered with a proper scale when given width and height (ignores height)', async () => {
const { func: onLoadSuccess, promise: onLoadSuccessPromise } =
makeAsyncCallback<[PageCallback]>();
const width = 600;
const height = 100;
renderWithContext(
<Page height={height} onLoadSuccess={onLoadSuccess} pageIndex={0} width={width} />,
{
linkService,
pdf,
},
);
expect.assertions(2);
const [page] = await onLoadSuccessPromise;
expect(page.width).toEqual(width);
// Expect proportions to be correct even though invalid height was provided
expect(page.height).toEqual(page.originalHeight * (page.width / page.originalWidth));
});
it('requests page to be rendered with a proper scale when given width, height and scale (ignores height, multiplies)', async () => {
const { func: onLoadSuccess, promise: onLoadSuccessPromise } =
makeAsyncCallback<[PageCallback]>();
const width = 600;
const height = 100;
const scale = 1.5;
renderWithContext(
<Page
height={height}
onLoadSuccess={onLoadSuccess}
pageIndex={0}
scale={scale}
width={width}
/>,
{
linkService,
pdf,
},
);
expect.assertions(2);
const [page] = await onLoadSuccessPromise;
expect(page.width).toBeCloseTo(width * scale);
// Expect proportions to be correct even though invalid height was provided
expect(page.height).toEqual(page.originalHeight * (page.width / page.originalWidth));
});
it('calls onClick callback when clicked a page (sample of mouse events family)', () => {
const onClick = vi.fn();
const { container } = renderWithContext(<Page onClick={onClick} />, {
linkService,
pdf,
});
const page = container.querySelector('.react-pdf__Page') as HTMLDivElement;
fireEvent.click(page);
expect(onClick).toHaveBeenCalled();
});
it('calls onTouchStart callback when touched a page (sample of touch events family)', () => {
const onTouchStart = vi.fn();
const { container } = renderWithContext(<Page onTouchStart={onTouchStart} />, {
linkService,
pdf,
});
const page = container.querySelector('.react-pdf__Page') as HTMLDivElement;
fireEvent.touchStart(page);
expect(onTouchStart).toHaveBeenCalled();
});
});

639
node_modules/react-pdf/src/Page.tsx generated vendored Normal file
View File

@@ -0,0 +1,639 @@
'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>
);
}

333
node_modules/react-pdf/src/Page/AnnotationLayer.css generated vendored Normal file
View File

@@ -0,0 +1,333 @@
/* Copyright 2014 Mozilla Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
:root {
--react-pdf-annotation-layer: 1;
--annotation-unfocused-field-background: url("data:image/svg+xml;charset=UTF-8,<svg width='1px' height='1px' xmlns='http://www.w3.org/2000/svg'><rect width='100%' height='100%' style='fill:rgba(0, 54, 255, 0.13);'/></svg>");
--input-focus-border-color: Highlight;
--input-focus-outline: 1px solid Canvas;
--input-unfocused-border-color: transparent;
--input-disabled-border-color: transparent;
--input-hover-border-color: black;
--link-outline: none;
}
@media screen and (forced-colors: active) {
:root {
--input-focus-border-color: CanvasText;
--input-unfocused-border-color: ActiveText;
--input-disabled-border-color: GrayText;
--input-hover-border-color: Highlight;
--link-outline: 1.5px solid LinkText;
}
.annotationLayer .textWidgetAnnotation :is(input, textarea):required,
.annotationLayer .choiceWidgetAnnotation select:required,
.annotationLayer .buttonWidgetAnnotation:is(.checkBox, .radioButton) input:required {
outline: 1.5px solid selectedItem;
}
.annotationLayer .linkAnnotation:hover {
backdrop-filter: invert(100%);
}
}
.annotationLayer {
position: absolute;
top: 0;
left: 0;
pointer-events: none;
transform-origin: 0 0;
z-index: 3;
}
.annotationLayer[data-main-rotation='90'] .norotate {
transform: rotate(270deg) translateX(-100%);
}
.annotationLayer[data-main-rotation='180'] .norotate {
transform: rotate(180deg) translate(-100%, -100%);
}
.annotationLayer[data-main-rotation='270'] .norotate {
transform: rotate(90deg) translateY(-100%);
}
.annotationLayer canvas {
position: absolute;
width: 100%;
height: 100%;
}
.annotationLayer section {
position: absolute;
text-align: initial;
pointer-events: auto;
box-sizing: border-box;
margin: 0;
transform-origin: 0 0;
}
.annotationLayer .linkAnnotation {
outline: var(--link-outline);
}
.textLayer.selecting ~ .annotationLayer section {
pointer-events: none;
}
.annotationLayer :is(.linkAnnotation, .buttonWidgetAnnotation.pushButton) > a {
position: absolute;
font-size: 1em;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
.annotationLayer :is(.linkAnnotation, .buttonWidgetAnnotation.pushButton) > a:hover {
opacity: 0.2;
background: rgba(255, 255, 0, 1);
box-shadow: 0 2px 10px rgba(255, 255, 0, 1);
}
.annotationLayer .textAnnotation img {
position: absolute;
cursor: pointer;
width: 100%;
height: 100%;
top: 0;
left: 0;
}
.annotationLayer .textWidgetAnnotation :is(input, textarea),
.annotationLayer .choiceWidgetAnnotation select,
.annotationLayer .buttonWidgetAnnotation:is(.checkBox, .radioButton) input {
background-image: var(--annotation-unfocused-field-background);
border: 2px solid var(--input-unfocused-border-color);
box-sizing: border-box;
font: calc(9px * var(--scale-factor)) sans-serif;
height: 100%;
margin: 0;
vertical-align: top;
width: 100%;
}
.annotationLayer .textWidgetAnnotation :is(input, textarea):required,
.annotationLayer .choiceWidgetAnnotation select:required,
.annotationLayer .buttonWidgetAnnotation:is(.checkBox, .radioButton) input:required {
outline: 1.5px solid red;
}
.annotationLayer .choiceWidgetAnnotation select option {
padding: 0;
}
.annotationLayer .buttonWidgetAnnotation.radioButton input {
border-radius: 50%;
}
.annotationLayer .textWidgetAnnotation textarea {
resize: none;
}
.annotationLayer .textWidgetAnnotation :is(input, textarea)[disabled],
.annotationLayer .choiceWidgetAnnotation select[disabled],
.annotationLayer .buttonWidgetAnnotation:is(.checkBox, .radioButton) input[disabled] {
background: none;
border: 2px solid var(--input-disabled-border-color);
cursor: not-allowed;
}
.annotationLayer .textWidgetAnnotation :is(input, textarea):hover,
.annotationLayer .choiceWidgetAnnotation select:hover,
.annotationLayer .buttonWidgetAnnotation:is(.checkBox, .radioButton) input:hover {
border: 2px solid var(--input-hover-border-color);
}
.annotationLayer .textWidgetAnnotation :is(input, textarea):hover,
.annotationLayer .choiceWidgetAnnotation select:hover,
.annotationLayer .buttonWidgetAnnotation.checkBox input:hover {
border-radius: 2px;
}
.annotationLayer .textWidgetAnnotation :is(input, textarea):focus,
.annotationLayer .choiceWidgetAnnotation select:focus {
background: none;
border: 2px solid var(--input-focus-border-color);
border-radius: 2px;
outline: var(--input-focus-outline);
}
.annotationLayer .buttonWidgetAnnotation:is(.checkBox, .radioButton) :focus {
background-image: none;
background-color: transparent;
}
.annotationLayer .buttonWidgetAnnotation.checkBox :focus {
border: 2px solid var(--input-focus-border-color);
border-radius: 2px;
outline: var(--input-focus-outline);
}
.annotationLayer .buttonWidgetAnnotation.radioButton :focus {
border: 2px solid var(--input-focus-border-color);
outline: var(--input-focus-outline);
}
.annotationLayer .buttonWidgetAnnotation.checkBox input:checked::before,
.annotationLayer .buttonWidgetAnnotation.checkBox input:checked::after,
.annotationLayer .buttonWidgetAnnotation.radioButton input:checked::before {
background-color: CanvasText;
content: '';
display: block;
position: absolute;
}
.annotationLayer .buttonWidgetAnnotation.checkBox input:checked::before,
.annotationLayer .buttonWidgetAnnotation.checkBox input:checked::after {
height: 80%;
left: 45%;
width: 1px;
}
.annotationLayer .buttonWidgetAnnotation.checkBox input:checked::before {
transform: rotate(45deg);
}
.annotationLayer .buttonWidgetAnnotation.checkBox input:checked::after {
transform: rotate(-45deg);
}
.annotationLayer .buttonWidgetAnnotation.radioButton input:checked::before {
border-radius: 50%;
height: 50%;
left: 30%;
top: 20%;
width: 50%;
}
.annotationLayer .textWidgetAnnotation input.comb {
font-family: monospace;
padding-left: 2px;
padding-right: 0;
}
.annotationLayer .textWidgetAnnotation input.comb:focus {
/*
* Letter spacing is placed on the right side of each character. Hence, the
* letter spacing of the last character may be placed outside the visible
* area, causing horizontal scrolling. We avoid this by extending the width
* when the element has focus and revert this when it loses focus.
*/
width: 103%;
}
.annotationLayer .buttonWidgetAnnotation:is(.checkBox, .radioButton) input {
appearance: none;
}
.annotationLayer .popupTriggerArea {
height: 100%;
width: 100%;
}
.annotationLayer .fileAttachmentAnnotation .popupTriggerArea {
position: absolute;
}
.annotationLayer .popupWrapper {
position: absolute;
font-size: calc(9px * var(--scale-factor));
width: 100%;
min-width: calc(180px * var(--scale-factor));
pointer-events: none;
}
.annotationLayer .popup {
position: absolute;
max-width: calc(180px * var(--scale-factor));
background-color: rgba(255, 255, 153, 1);
box-shadow: 0 calc(2px * var(--scale-factor)) calc(5px * var(--scale-factor))
rgba(136, 136, 136, 1);
border-radius: calc(2px * var(--scale-factor));
padding: calc(6px * var(--scale-factor));
margin-left: calc(5px * var(--scale-factor));
cursor: pointer;
font: message-box;
white-space: normal;
word-wrap: break-word;
pointer-events: auto;
}
.annotationLayer .popup > * {
font-size: calc(9px * var(--scale-factor));
}
.annotationLayer .popup h1 {
display: inline-block;
}
.annotationLayer .popupDate {
display: inline-block;
margin-left: calc(5px * var(--scale-factor));
}
.annotationLayer .popupContent {
border-top: 1px solid rgba(51, 51, 51, 1);
margin-top: calc(2px * var(--scale-factor));
padding-top: calc(2px * var(--scale-factor));
}
.annotationLayer .richText > * {
white-space: pre-wrap;
font-size: calc(9px * var(--scale-factor));
}
.annotationLayer .highlightAnnotation,
.annotationLayer .underlineAnnotation,
.annotationLayer .squigglyAnnotation,
.annotationLayer .strikeoutAnnotation,
.annotationLayer .freeTextAnnotation,
.annotationLayer .lineAnnotation svg line,
.annotationLayer .squareAnnotation svg rect,
.annotationLayer .circleAnnotation svg ellipse,
.annotationLayer .polylineAnnotation svg polyline,
.annotationLayer .polygonAnnotation svg polygon,
.annotationLayer .caretAnnotation,
.annotationLayer .inkAnnotation svg polyline,
.annotationLayer .stampAnnotation,
.annotationLayer .fileAttachmentAnnotation {
cursor: pointer;
}
.annotationLayer section svg {
position: absolute;
width: 100%;
height: 100%;
top: 0;
left: 0;
}
.annotationLayer .annotationTextContent {
position: absolute;
width: 100%;
height: 100%;
opacity: 0;
color: transparent;
user-select: none;
pointer-events: none;
}
.annotationLayer .annotationTextContent span {
width: 100%;
display: inline-block;
}

View File

@@ -0,0 +1,360 @@
import { beforeAll, describe, expect, it } from 'vitest';
import { render } from '@testing-library/react';
import { pdfjs } from '../index.test.js';
import AnnotationLayer from './AnnotationLayer.js';
import LinkService from '../LinkService.js';
import failingPage from '../../../../__mocks__/_failing_page.js';
import { loadPDF, makeAsyncCallback, muteConsole, restoreConsole } from '../../../../test-utils.js';
import DocumentContext from '../DocumentContext.js';
import PageContext from '../PageContext.js';
import type { RenderResult } from '@testing-library/react';
import type { PDFDocumentProxy, PDFPageProxy } from 'pdfjs-dist';
import type { Annotations, DocumentContextType, PageContextType } from '../shared/types.js';
const pdfFile = loadPDF('./../../__mocks__/_pdf.pdf');
const annotatedPdfFile = loadPDF('./../../__mocks__/_pdf3.pdf');
function renderWithContext(
children: React.ReactNode,
documentContext: Partial<DocumentContextType>,
pageContext: Partial<PageContextType>,
) {
const { rerender, ...otherResult } = render(
<DocumentContext.Provider value={documentContext as DocumentContextType}>
<PageContext.Provider value={pageContext as PageContextType}>{children}</PageContext.Provider>
</DocumentContext.Provider>,
);
const customRerender = (
nextChildren: React.ReactNode,
nextDocumentContext: Partial<DocumentContextType> = documentContext,
nextPageContext: Partial<PageContextType> = pageContext,
) =>
rerender(
<DocumentContext.Provider value={nextDocumentContext as DocumentContextType}>
<PageContext.Provider value={nextPageContext as PageContextType}>
{nextChildren}
</PageContext.Provider>
</DocumentContext.Provider>,
);
return {
...otherResult,
rerender: customRerender,
} as RenderResult & { rerender: typeof customRerender };
}
describe('AnnotationLayer', () => {
const linkService = new LinkService();
// Loaded PDF file
let pdf: PDFDocumentProxy;
// Loaded page
let page: PDFPageProxy;
let page2: PDFPageProxy;
// Loaded page text items
let desiredAnnotations: Annotations;
let desiredAnnotations2: Annotations;
beforeAll(async () => {
pdf = await pdfjs.getDocument({ data: pdfFile.arrayBuffer }).promise;
page = await pdf.getPage(1);
desiredAnnotations = await page.getAnnotations();
page2 = await pdf.getPage(2);
desiredAnnotations2 = await page2.getAnnotations();
});
describe('loading', () => {
it('loads annotations and calls onGetAnnotationsSuccess callback properly', async () => {
const { func: onGetAnnotationsSuccess, promise: onGetAnnotationsSuccessPromise } =
makeAsyncCallback();
renderWithContext(
<AnnotationLayer />,
{
linkService,
pdf,
},
{
onGetAnnotationsSuccess,
page,
},
);
expect.assertions(1);
await expect(onGetAnnotationsSuccessPromise).resolves.toMatchObject([desiredAnnotations]);
});
it('calls onGetAnnotationsError when failed to load annotations', async () => {
const { func: onGetAnnotationsError, promise: onGetAnnotationsErrorPromise } =
makeAsyncCallback();
muteConsole();
renderWithContext(
<AnnotationLayer />,
{
linkService,
pdf,
},
{
onGetAnnotationsError,
page: failingPage,
},
);
expect.assertions(1);
await expect(onGetAnnotationsErrorPromise).resolves.toMatchObject([expect.any(Error)]);
restoreConsole();
});
it('replaces annotations properly when page is changed', async () => {
const { func: onGetAnnotationsSuccess, promise: onGetAnnotationsSuccessPromise } =
makeAsyncCallback();
const { rerender } = renderWithContext(
<AnnotationLayer />,
{
linkService,
pdf,
},
{
onGetAnnotationsSuccess,
page,
},
);
expect.assertions(2);
await expect(onGetAnnotationsSuccessPromise).resolves.toMatchObject([desiredAnnotations]);
const { func: onGetAnnotationsSuccess2, promise: onGetAnnotationsSuccessPromise2 } =
makeAsyncCallback();
rerender(
<AnnotationLayer />,
{
linkService,
pdf,
},
{
onGetAnnotationsSuccess: onGetAnnotationsSuccess2,
page: page2,
},
);
await expect(onGetAnnotationsSuccessPromise2).resolves.toMatchObject([desiredAnnotations2]);
});
it('throws an error when placed outside Page', () => {
muteConsole();
expect(() => render(<AnnotationLayer />)).toThrow();
restoreConsole();
});
});
describe('rendering', () => {
it('renders annotations properly', async () => {
const {
func: onRenderAnnotationLayerSuccess,
promise: onRenderAnnotationLayerSuccessPromise,
} = makeAsyncCallback();
const { container } = renderWithContext(
<AnnotationLayer />,
{
linkService,
pdf,
},
{
onRenderAnnotationLayerSuccess,
page,
},
);
expect.assertions(1);
await onRenderAnnotationLayerSuccessPromise;
const wrapper = container.firstElementChild as HTMLDivElement;
const annotationItems = Array.from(wrapper.children);
expect(annotationItems).toHaveLength(desiredAnnotations.length);
});
it.each`
externalLinkTarget | target
${null} | ${''}
${'_self'} | ${'_self'}
${'_blank'} | ${'_blank'}
${'_parent'} | ${'_parent'}
${'_top'} | ${'_top'}
`(
'renders all links with target $target given externalLinkTarget = $externalLinkTarget',
async ({ externalLinkTarget, target }) => {
const {
func: onRenderAnnotationLayerSuccess,
promise: onRenderAnnotationLayerSuccessPromise,
} = makeAsyncCallback();
const customLinkService = new LinkService();
if (externalLinkTarget) {
customLinkService.setExternalLinkTarget(externalLinkTarget);
}
const { container } = renderWithContext(
<AnnotationLayer />,
{
linkService: customLinkService,
pdf,
},
{
onRenderAnnotationLayerSuccess,
page,
},
);
expect.assertions(desiredAnnotations.length);
await onRenderAnnotationLayerSuccessPromise;
const wrapper = container.firstElementChild as HTMLDivElement;
const annotationItems = Array.from(wrapper.children);
const annotationLinkItems = annotationItems
.map((item) => item.firstChild as HTMLElement)
.filter((item) => item.tagName === 'A');
for (const link of annotationLinkItems) {
expect(link.getAttribute('target')).toBe(target);
}
},
);
it.each`
externalLinkRel | rel
${null} | ${'noopener noreferrer nofollow'}
${'noopener'} | ${'noopener'}
`(
'renders all links with rel $rel given externalLinkRel = $externalLinkRel',
async ({ externalLinkRel, rel }) => {
const {
func: onRenderAnnotationLayerSuccess,
promise: onRenderAnnotationLayerSuccessPromise,
} = makeAsyncCallback();
const customLinkService = new LinkService();
if (externalLinkRel) {
customLinkService.setExternalLinkRel(externalLinkRel);
}
const { container } = renderWithContext(
<AnnotationLayer />,
{
linkService: customLinkService,
pdf,
},
{
onRenderAnnotationLayerSuccess,
page,
},
);
expect.assertions(desiredAnnotations.length);
await onRenderAnnotationLayerSuccessPromise;
const wrapper = container.firstElementChild as HTMLDivElement;
const annotationItems = Array.from(wrapper.children);
const annotationLinkItems = annotationItems
.map((item) => item.firstChild as HTMLElement)
.filter((item) => item.tagName === 'A');
for (const link of annotationLinkItems) {
expect(link.getAttribute('rel')).toBe(rel);
}
},
);
it('renders annotations with the default imageResourcesPath given no imageResourcesPath', async () => {
const pdf = await pdfjs.getDocument({ data: annotatedPdfFile.arrayBuffer }).promise;
const annotatedPage = await pdf.getPage(1);
const {
func: onRenderAnnotationLayerSuccess,
promise: onRenderAnnotationLayerSuccessPromise,
} = makeAsyncCallback();
const imageResourcesPath = '';
const desiredImageTagRegExp = new RegExp(
`<img[^>]+src="${imageResourcesPath}annotation-note.svg"`,
);
const { container } = renderWithContext(
<AnnotationLayer />,
{
linkService,
pdf,
},
{
onRenderAnnotationLayerSuccess,
page: annotatedPage,
},
);
expect.assertions(1);
await onRenderAnnotationLayerSuccessPromise;
const stringifiedAnnotationLayerNode = container.outerHTML;
expect(stringifiedAnnotationLayerNode).toMatch(desiredImageTagRegExp);
});
it('renders annotations with the specified imageResourcesPath given imageResourcesPath', async () => {
const pdf = await pdfjs.getDocument({ data: annotatedPdfFile.arrayBuffer }).promise;
const annotatedPage = await pdf.getPage(1);
const {
func: onRenderAnnotationLayerSuccess,
promise: onRenderAnnotationLayerSuccessPromise,
} = makeAsyncCallback();
const imageResourcesPath = '/public/images/';
const desiredImageTagRegExp = new RegExp(
`<img[^>]+src="${imageResourcesPath}annotation-note.svg"`,
);
const { container } = renderWithContext(
<AnnotationLayer />,
{
imageResourcesPath,
linkService,
pdf,
},
{
onRenderAnnotationLayerSuccess,
page: annotatedPage,
},
);
expect.assertions(1);
await onRenderAnnotationLayerSuccessPromise;
const stringifiedAnnotationLayerNode = container.outerHTML;
expect(stringifiedAnnotationLayerNode).toMatch(desiredImageTagRegExp);
});
});
});

204
node_modules/react-pdf/src/Page/AnnotationLayer.tsx generated vendored Normal file
View File

@@ -0,0 +1,204 @@
'use client';
import { useEffect, useMemo, useRef } from 'react';
import makeCancellable from 'make-cancellable-promise';
import clsx from 'clsx';
import invariant from 'tiny-invariant';
import warning from 'warning';
import * as pdfjs from 'pdfjs-dist';
import useDocumentContext from '../shared/hooks/useDocumentContext.js';
import usePageContext from '../shared/hooks/usePageContext.js';
import useResolver from '../shared/hooks/useResolver.js';
import { cancelRunningTask } from '../shared/utils.js';
import type { Annotations } from '../shared/types.js';
export default function AnnotationLayer(): React.ReactElement {
const documentContext = useDocumentContext();
const pageContext = usePageContext();
invariant(pageContext, 'Unable to find Page context.');
const mergedProps = { ...documentContext, ...pageContext };
const {
imageResourcesPath,
linkService,
onGetAnnotationsError: onGetAnnotationsErrorProps,
onGetAnnotationsSuccess: onGetAnnotationsSuccessProps,
onRenderAnnotationLayerError: onRenderAnnotationLayerErrorProps,
onRenderAnnotationLayerSuccess: onRenderAnnotationLayerSuccessProps,
page,
pdf,
renderForms,
rotate,
scale = 1,
} = mergedProps;
invariant(
pdf,
'Attempted to load page annotations, but no document was specified. Wrap <Page /> in a <Document /> or pass explicit `pdf` prop.',
);
invariant(page, 'Attempted to load page annotations, but no page was specified.');
invariant(linkService, 'Attempted to load page annotations, but no linkService was specified.');
const [annotationsState, annotationsDispatch] = useResolver<Annotations>();
const { value: annotations, error: annotationsError } = annotationsState;
const layerElement = useRef<HTMLDivElement>(null);
warning(
Number.parseInt(
window.getComputedStyle(document.body).getPropertyValue('--react-pdf-annotation-layer'),
10,
) === 1,
'AnnotationLayer styles not found. Read more: https://github.com/wojtekmaj/react-pdf#support-for-annotations',
);
function onLoadSuccess() {
if (!annotations) {
// Impossible, but TypeScript doesn't know that
return;
}
if (onGetAnnotationsSuccessProps) {
onGetAnnotationsSuccessProps(annotations);
}
}
function onLoadError() {
if (!annotationsError) {
// Impossible, but TypeScript doesn't know that
return;
}
warning(false, annotationsError.toString());
if (onGetAnnotationsErrorProps) {
onGetAnnotationsErrorProps(annotationsError);
}
}
// biome-ignore lint/correctness/useExhaustiveDependencies: useEffect intentionally triggered on page change
useEffect(
function resetAnnotations() {
annotationsDispatch({ type: 'RESET' });
},
[annotationsDispatch, page],
);
useEffect(
function loadAnnotations() {
if (!page) {
return;
}
const cancellable = makeCancellable(page.getAnnotations());
const runningTask = cancellable;
cancellable.promise
.then((nextAnnotations) => {
annotationsDispatch({ type: 'RESOLVE', value: nextAnnotations });
})
.catch((error) => {
annotationsDispatch({ type: 'REJECT', error });
});
return () => {
cancelRunningTask(runningTask);
};
},
[annotationsDispatch, page],
);
// biome-ignore lint/correctness/useExhaustiveDependencies: Ommitted callbacks so they are not called every time they change
useEffect(() => {
if (annotations === undefined) {
return;
}
if (annotations === false) {
onLoadError();
return;
}
onLoadSuccess();
}, [annotations]);
function onRenderSuccess() {
if (onRenderAnnotationLayerSuccessProps) {
onRenderAnnotationLayerSuccessProps();
}
}
function onRenderError(error: unknown) {
warning(false, `${error}`);
if (onRenderAnnotationLayerErrorProps) {
onRenderAnnotationLayerErrorProps(error);
}
}
const viewport = useMemo(
() => page.getViewport({ scale, rotation: rotate }),
[page, rotate, scale],
);
// biome-ignore lint/correctness/useExhaustiveDependencies: Ommitted callbacks so they are not called every time they change
useEffect(
function renderAnnotationLayer() {
if (!pdf || !page || !linkService || !annotations) {
return;
}
const { current: layer } = layerElement;
if (!layer) {
return;
}
const clonedViewport = viewport.clone({ dontFlip: true });
const annotationLayerParameters = {
accessibilityManager: null, // TODO: Implement this
annotationCanvasMap: null, // TODO: Implement this
annotationEditorUIManager: null, // TODO: Implement this
div: layer,
l10n: null, // TODO: Implement this
page,
structTreeLayer: null, // TODO: Implement this
viewport: clonedViewport,
};
const renderParameters = {
annotations,
annotationStorage: pdf.annotationStorage,
div: layer,
imageResourcesPath,
linkService,
page,
renderForms,
viewport: clonedViewport,
};
layer.innerHTML = '';
try {
new pdfjs.AnnotationLayer(annotationLayerParameters).render(renderParameters);
// Intentional immediate callback
onRenderSuccess();
} catch (error) {
onRenderError(error);
}
return () => {
// TODO: Cancel running task?
};
},
[annotations, imageResourcesPath, linkService, page, pdf, renderForms, viewport],
);
return (
<div className={clsx('react-pdf__Page__annotations', 'annotationLayer')} ref={layerElement} />
);
}

141
node_modules/react-pdf/src/Page/Canvas.spec.tsx generated vendored Normal file
View File

@@ -0,0 +1,141 @@
import { beforeAll, describe, expect, it, vi } from 'vitest';
import { render } from '@testing-library/react';
import { pdfjs } from '../index.test.js';
import Canvas from './Canvas.js';
import failingPage from '../../../../__mocks__/_failing_page.js';
import { loadPDF, makeAsyncCallback, muteConsole, restoreConsole } from '../../../../test-utils.js';
import PageContext from '../PageContext.js';
import type { PDFPageProxy } from 'pdfjs-dist';
import type { PageContextType } from '../shared/types.js';
const pdfFile = loadPDF('./../../__mocks__/_pdf.pdf');
function renderWithContext(children: React.ReactNode, context: Partial<PageContextType>) {
const { rerender, ...otherResult } = render(
<PageContext.Provider value={context as PageContextType}>{children}</PageContext.Provider>,
);
return {
...otherResult,
rerender: (nextChildren: React.ReactNode, nextContext: Partial<PageContextType> = context) =>
rerender(
<PageContext.Provider value={nextContext as PageContextType}>
{nextChildren}
</PageContext.Provider>,
),
};
}
describe('Canvas', () => {
// Loaded page
let page: PDFPageProxy;
let pageWithRendererMocked: PDFPageProxy;
beforeAll(async () => {
const pdf = await pdfjs.getDocument({ data: pdfFile.arrayBuffer }).promise;
page = await pdf.getPage(1);
pageWithRendererMocked = Object.assign(page, {
render: () => ({
promise: new Promise<void>((resolve) => resolve()),
cancel: () => {
// Intentionally empty
},
}),
});
});
describe('loading', () => {
it('renders a page and calls onRenderSuccess callback properly', async () => {
const { func: onRenderSuccess, promise: onRenderSuccessPromise } = makeAsyncCallback();
muteConsole();
renderWithContext(<Canvas />, {
onRenderSuccess,
page: pageWithRendererMocked,
scale: 1,
});
expect.assertions(1);
await expect(onRenderSuccessPromise).resolves.toMatchObject([{}]);
restoreConsole();
});
it('calls onRenderError when failed to render canvas', async () => {
const { func: onRenderError, promise: onRenderErrorPromise } = makeAsyncCallback();
muteConsole();
renderWithContext(<Canvas />, {
onRenderError,
page: failingPage,
scale: 1,
});
expect.assertions(1);
await expect(onRenderErrorPromise).resolves.toMatchObject([expect.any(Error)]);
restoreConsole();
});
});
describe('rendering', () => {
it('passes canvas element to canvasRef properly', () => {
const canvasRef = vi.fn();
renderWithContext(<Canvas canvasRef={canvasRef} />, {
page: pageWithRendererMocked,
scale: 1,
});
expect(canvasRef).toHaveBeenCalled();
expect(canvasRef).toHaveBeenCalledWith(expect.any(HTMLElement));
});
it('does not request structure tree to be rendered when renderTextLayer = false', async () => {
const { func: onRenderSuccess, promise: onRenderSuccessPromise } = makeAsyncCallback();
const { container } = renderWithContext(<Canvas />, {
onRenderSuccess,
page: pageWithRendererMocked,
renderTextLayer: false,
});
await onRenderSuccessPromise;
const structTree = container.querySelector('.react-pdf__Page__structTree');
expect(structTree).not.toBeInTheDocument();
});
it('renders StructTree when given renderTextLayer = true', async () => {
const { func: onGetStructTreeSuccess, promise: onGetStructTreeSuccessPromise } =
makeAsyncCallback();
const { container } = renderWithContext(<Canvas />, {
onGetStructTreeSuccess,
page: pageWithRendererMocked,
renderTextLayer: true,
});
expect.assertions(1);
await onGetStructTreeSuccessPromise;
const canvas = container.querySelector('canvas') as HTMLCanvasElement;
expect(canvas.children.length).toBeGreaterThan(0);
});
});
});

166
node_modules/react-pdf/src/Page/Canvas.tsx generated vendored Normal file
View File

@@ -0,0 +1,166 @@
'use client';
import { useCallback, useEffect, useMemo, useRef } from 'react';
import mergeRefs from 'merge-refs';
import invariant from 'tiny-invariant';
import warning from 'warning';
import * as pdfjs from 'pdfjs-dist';
import StructTree from '../StructTree.js';
import usePageContext from '../shared/hooks/usePageContext.js';
import {
cancelRunningTask,
getDevicePixelRatio,
isCancelException,
makePageCallback,
} from '../shared/utils.js';
import type { RenderParameters } from 'pdfjs-dist/types/src/display/api.js';
const ANNOTATION_MODE = pdfjs.AnnotationMode;
type CanvasProps = {
canvasRef?: React.Ref<HTMLCanvasElement>;
};
export default function Canvas(props: CanvasProps): React.ReactElement {
const pageContext = usePageContext();
invariant(pageContext, 'Unable to find Page context.');
const mergedProps = { ...pageContext, ...props };
const {
_className,
canvasBackground,
devicePixelRatio = getDevicePixelRatio(),
onRenderError: onRenderErrorProps,
onRenderSuccess: onRenderSuccessProps,
page,
renderForms,
renderTextLayer,
rotate,
scale,
} = mergedProps;
const { canvasRef } = props;
invariant(page, 'Attempted to render page canvas, but no page was specified.');
const canvasElement = useRef<HTMLCanvasElement>(null);
/**
* Called when a page is rendered successfully.
*/
function onRenderSuccess() {
if (!page) {
// Impossible, but TypeScript doesn't know that
return;
}
if (onRenderSuccessProps) {
onRenderSuccessProps(makePageCallback(page, scale));
}
}
/**
* Called when a page fails to render.
*/
function onRenderError(error: Error) {
if (isCancelException(error)) {
return;
}
warning(false, error.toString());
if (onRenderErrorProps) {
onRenderErrorProps(error);
}
}
const renderViewport = useMemo(
() => page.getViewport({ scale: scale * devicePixelRatio, rotation: rotate }),
[devicePixelRatio, page, rotate, scale],
);
const viewport = useMemo(
() => page.getViewport({ scale, rotation: rotate }),
[page, rotate, scale],
);
// biome-ignore lint/correctness/useExhaustiveDependencies: Ommitted callbacks so they are not called every time they change
useEffect(
function drawPageOnCanvas() {
if (!page) {
return;
}
// Ensures the canvas will be re-rendered from scratch. Otherwise all form data will stay.
page.cleanup();
const { current: canvas } = canvasElement;
if (!canvas) {
return;
}
canvas.width = renderViewport.width;
canvas.height = renderViewport.height;
canvas.style.width = `${Math.floor(viewport.width)}px`;
canvas.style.height = `${Math.floor(viewport.height)}px`;
canvas.style.visibility = 'hidden';
const renderContext: RenderParameters = {
annotationMode: renderForms ? ANNOTATION_MODE.ENABLE_FORMS : ANNOTATION_MODE.ENABLE,
canvasContext: canvas.getContext('2d', { alpha: false }) as CanvasRenderingContext2D,
viewport: renderViewport,
};
if (canvasBackground) {
renderContext.background = canvasBackground;
}
const cancellable = page.render(renderContext);
const runningTask = cancellable;
cancellable.promise
.then(() => {
canvas.style.visibility = '';
onRenderSuccess();
})
.catch(onRenderError);
return () => cancelRunningTask(runningTask);
},
[canvasBackground, page, renderForms, renderViewport, viewport],
);
const cleanup = useCallback(() => {
const { current: canvas } = canvasElement;
/**
* Zeroing the width and height cause most browsers to release graphics
* resources immediately, which can greatly reduce memory consumption.
*/
if (canvas) {
canvas.width = 0;
canvas.height = 0;
}
}, []);
useEffect(() => cleanup, [cleanup]);
return (
<canvas
className={`${_className}__canvas`}
dir="ltr"
ref={mergeRefs(canvasRef, canvasElement)}
style={{
display: 'block',
userSelect: 'none',
}}
>
{renderTextLayer ? <StructTree /> : null}
</canvas>
);
}

119
node_modules/react-pdf/src/Page/TextLayer.css generated vendored Normal file
View File

@@ -0,0 +1,119 @@
/* Copyright 2014 Mozilla Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
:root {
--react-pdf-text-layer: 1;
--highlight-bg-color: rgba(180, 0, 170, 1);
--highlight-selected-bg-color: rgba(0, 100, 0, 1);
}
@media screen and (forced-colors: active) {
:root {
--highlight-bg-color: Highlight;
--highlight-selected-bg-color: ButtonText;
}
}
[data-main-rotation='90'] {
transform: rotate(90deg) translateY(-100%);
}
[data-main-rotation='180'] {
transform: rotate(180deg) translate(-100%, -100%);
}
[data-main-rotation='270'] {
transform: rotate(270deg) translateX(-100%);
}
.textLayer {
position: absolute;
text-align: initial;
inset: 0;
overflow: hidden;
line-height: 1;
text-size-adjust: none;
forced-color-adjust: none;
transform-origin: 0 0;
z-index: 2;
}
.textLayer :is(span, br) {
color: transparent;
position: absolute;
white-space: pre;
cursor: text;
margin: 0;
transform-origin: 0 0;
}
/* Only necessary in Google Chrome, see issue 14205, and most unfortunately
* the problem doesn't show up in "text" reference tests. */
.textLayer span.markedContent {
top: 0;
height: 0;
}
.textLayer .highlight {
margin: -1px;
padding: 1px;
background-color: var(--highlight-bg-color);
border-radius: 4px;
}
.textLayer .highlight.appended {
position: initial;
}
.textLayer .highlight.begin {
border-radius: 4px 0 0 4px;
}
.textLayer .highlight.end {
border-radius: 0 4px 4px 0;
}
.textLayer .highlight.middle {
border-radius: 0;
}
.textLayer .highlight.selected {
background-color: var(--highlight-selected-bg-color);
}
/* Avoids https://github.com/mozilla/pdf.js/issues/13840 in Chrome */
.textLayer br::selection {
background: transparent;
}
.textLayer .endOfContent {
display: block;
position: absolute;
inset: 100% 0 0;
z-index: -1;
cursor: default;
user-select: none;
}
.textLayer.selecting .endOfContent {
top: 0;
}
.hiddenCanvasElement {
position: absolute;
top: 0;
left: 0;
width: 0;
height: 0;
display: none;
}

274
node_modules/react-pdf/src/Page/TextLayer.spec.tsx generated vendored Normal file
View File

@@ -0,0 +1,274 @@
import { beforeAll, describe, expect, it, vi } from 'vitest';
import { render } from '@testing-library/react';
import { pdfjs } from '../index.test.js';
import TextLayer from './TextLayer.js';
import failingPage from '../../../../__mocks__/_failing_page.js';
import { loadPDF, makeAsyncCallback, muteConsole, restoreConsole } from '../../../../test-utils.js';
import PageContext from '../PageContext.js';
import type { PDFPageProxy } from 'pdfjs-dist';
import type { TextContent } from 'pdfjs-dist/types/src/display/api.js';
import type { PageContextType } from '../shared/types.js';
const pdfFile = loadPDF('./../../__mocks__/_pdf.pdf');
const untaggedPdfFile = loadPDF('./../../__mocks__/_untagged.pdf');
function renderWithContext(children: React.ReactNode, context: Partial<PageContextType>) {
const { rerender, ...otherResult } = render(
<PageContext.Provider value={context as PageContextType}>{children}</PageContext.Provider>,
);
return {
...otherResult,
rerender: (nextChildren: React.ReactNode, nextContext: Partial<PageContextType> = context) =>
rerender(
<PageContext.Provider value={nextContext as PageContextType}>
{nextChildren}
</PageContext.Provider>,
),
};
}
function getTextItems(container: HTMLElement) {
const wrapper = container.firstElementChild as HTMLDivElement;
return wrapper.querySelectorAll('[role="presentation"]');
}
describe('TextLayer', () => {
// Loaded page
let page: PDFPageProxy;
let page2: PDFPageProxy;
// Loaded page text items
let desiredTextItems: TextContent['items'];
let desiredTextItems2: TextContent['items'];
beforeAll(async () => {
const pdf = await pdfjs.getDocument({ data: pdfFile.arrayBuffer }).promise;
page = await pdf.getPage(1);
const textContent = await page.getTextContent();
desiredTextItems = textContent.items;
page2 = await pdf.getPage(2);
const textContent2 = await page2.getTextContent();
desiredTextItems2 = textContent2.items;
});
describe('loading', () => {
it('loads text content and calls onGetTextSuccess callback properly', async () => {
const { func: onGetTextSuccess, promise: onGetTextSuccessPromise } = makeAsyncCallback();
renderWithContext(<TextLayer />, {
onGetTextSuccess,
page,
});
expect.assertions(1);
await expect(onGetTextSuccessPromise).resolves.toMatchObject([{ items: desiredTextItems }]);
});
it('calls onGetTextError when failed to load text content', async () => {
const { func: onGetTextError, promise: onGetTextErrorPromise } = makeAsyncCallback();
muteConsole();
renderWithContext(<TextLayer />, {
onGetTextError,
page: failingPage,
});
expect.assertions(1);
await expect(onGetTextErrorPromise).resolves.toMatchObject([expect.any(Error)]);
restoreConsole();
});
it('replaces text content properly', async () => {
const { func: onGetTextSuccess, promise: onGetTextSuccessPromise } = makeAsyncCallback();
const { rerender } = renderWithContext(<TextLayer />, {
onGetTextSuccess,
page,
});
expect.assertions(2);
await expect(onGetTextSuccessPromise).resolves.toMatchObject([
{
items: desiredTextItems,
},
]);
const { func: onGetTextSuccess2, promise: onGetTextSuccessPromise2 } = makeAsyncCallback();
rerender(<TextLayer />, {
onGetTextSuccess: onGetTextSuccess2,
page: page2,
});
await expect(onGetTextSuccessPromise2).resolves.toMatchObject([
{
items: desiredTextItems2,
},
]);
});
it('throws an error when placed outside Page', () => {
muteConsole();
expect(() => render(<TextLayer />)).toThrow();
restoreConsole();
});
});
describe('rendering', () => {
it('renders text content properly', async () => {
const { func: onRenderTextLayerSuccess, promise: onRenderTextLayerSuccessPromise } =
makeAsyncCallback();
const { container } = renderWithContext(<TextLayer />, { onRenderTextLayerSuccess, page });
expect.assertions(1);
await onRenderTextLayerSuccessPromise;
const textItems = getTextItems(container);
expect(textItems).toHaveLength(desiredTextItems.length);
});
it('renders text content properly given customTextRenderer', async () => {
const { func: onRenderTextLayerSuccess, promise: onRenderTextLayerSuccessPromise } =
makeAsyncCallback();
const customTextRenderer = vi.fn();
const { container } = renderWithContext(<TextLayer />, {
customTextRenderer,
onRenderTextLayerSuccess,
page,
});
expect.assertions(1);
await onRenderTextLayerSuccessPromise;
const textItems = getTextItems(container);
expect(textItems).toHaveLength(desiredTextItems.length);
});
it('maps textContent items to actual TextLayer children properly', async () => {
const { func: onRenderTextLayerSuccess, promise: onRenderTextLayerSuccessPromise } =
makeAsyncCallback();
const { container, rerender } = renderWithContext(<TextLayer />, {
onRenderTextLayerSuccess,
page,
});
expect.assertions(1);
await onRenderTextLayerSuccessPromise;
const textItems = getTextItems(container);
const { func: onRenderTextLayerSuccess2, promise: onRenderTextLayerSuccessPromise2 } =
makeAsyncCallback();
const customTextRenderer = (item: { str: string }) => item.str;
rerender(<TextLayer />, {
customTextRenderer,
onRenderTextLayerSuccess: onRenderTextLayerSuccess2,
page,
});
await onRenderTextLayerSuccessPromise2;
const textItems2 = getTextItems(container);
expect(textItems).toEqual(textItems2);
});
it('calls customTextRenderer with necessary arguments', async () => {
const { func: onRenderTextLayerSuccess, promise: onRenderTextLayerSuccessPromise } =
makeAsyncCallback();
const customTextRenderer = vi.fn();
const { container } = renderWithContext(<TextLayer />, {
customTextRenderer,
onRenderTextLayerSuccess,
page,
});
expect.assertions(3);
await onRenderTextLayerSuccessPromise;
const textItems = getTextItems(container);
expect(textItems).toHaveLength(desiredTextItems.length);
expect(customTextRenderer).toHaveBeenCalledTimes(desiredTextItems.length);
expect(customTextRenderer).toHaveBeenCalledWith(
expect.objectContaining({
str: expect.any(String),
itemIndex: expect.any(Number),
}),
);
});
it('renders text content properly given customTextRenderer', async () => {
const { func: onRenderTextLayerSuccess, promise: onRenderTextLayerSuccessPromise } =
makeAsyncCallback();
const customTextRenderer = () => 'Test value';
const { container } = renderWithContext(<TextLayer />, {
customTextRenderer,
onRenderTextLayerSuccess,
page,
});
expect.assertions(1);
await onRenderTextLayerSuccessPromise;
expect(container).toHaveTextContent('Test value');
});
it('renders text content properly given customTextRenderer and untagged document', async () => {
const { func: onRenderTextLayerSuccess, promise: onRenderTextLayerSuccessPromise } =
makeAsyncCallback();
const customTextRenderer = () => 'Test value';
const untaggedDoc = await pdfjs.getDocument({ data: untaggedPdfFile.arrayBuffer }).promise;
const untaggedPage = await untaggedDoc.getPage(1);
const { container } = renderWithContext(<TextLayer />, {
customTextRenderer,
onRenderTextLayerSuccess,
page: untaggedPage,
});
expect.assertions(1);
await onRenderTextLayerSuccessPromise;
expect(container).toHaveTextContent('Test value');
});
});
});

260
node_modules/react-pdf/src/Page/TextLayer.tsx generated vendored Normal file
View File

@@ -0,0 +1,260 @@
'use client';
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef } from 'react';
import makeCancellable from 'make-cancellable-promise';
import clsx from 'clsx';
import invariant from 'tiny-invariant';
import warning from 'warning';
import * as pdfjs from 'pdfjs-dist';
import usePageContext from '../shared/hooks/usePageContext.js';
import useResolver from '../shared/hooks/useResolver.js';
import { cancelRunningTask } from '../shared/utils.js';
import type { TextContent, TextItem, TextMarkedContent } from 'pdfjs-dist/types/src/display/api.js';
function isTextItem(item: TextItem | TextMarkedContent): item is TextItem {
return 'str' in item;
}
export default function TextLayer(): React.ReactElement {
const pageContext = usePageContext();
invariant(pageContext, 'Unable to find Page context.');
const {
customTextRenderer,
onGetTextError,
onGetTextSuccess,
onRenderTextLayerError,
onRenderTextLayerSuccess,
page,
pageIndex,
pageNumber,
rotate,
scale,
} = pageContext;
invariant(page, 'Attempted to load page text content, but no page was specified.');
const [textContentState, textContentDispatch] = useResolver<TextContent>();
const { value: textContent, error: textContentError } = textContentState;
const layerElement = useRef<HTMLDivElement>(null);
warning(
Number.parseInt(
window.getComputedStyle(document.body).getPropertyValue('--react-pdf-text-layer'),
10,
) === 1,
'TextLayer styles not found. Read more: https://github.com/wojtekmaj/react-pdf#support-for-text-layer',
);
/**
* Called when a page text content is read successfully
*/
function onLoadSuccess() {
if (!textContent) {
// Impossible, but TypeScript doesn't know that
return;
}
if (onGetTextSuccess) {
onGetTextSuccess(textContent);
}
}
/**
* Called when a page text content failed to read successfully
*/
function onLoadError() {
if (!textContentError) {
// Impossible, but TypeScript doesn't know that
return;
}
warning(false, textContentError.toString());
if (onGetTextError) {
onGetTextError(textContentError);
}
}
// biome-ignore lint/correctness/useExhaustiveDependencies: useEffect intentionally triggered on page change
useEffect(
function resetTextContent() {
textContentDispatch({ type: 'RESET' });
},
[page, textContentDispatch],
);
useEffect(
function loadTextContent() {
if (!page) {
return;
}
const cancellable = makeCancellable(page.getTextContent());
const runningTask = cancellable;
cancellable.promise
.then((nextTextContent) => {
textContentDispatch({ type: 'RESOLVE', value: nextTextContent });
})
.catch((error) => {
textContentDispatch({ type: 'REJECT', error });
});
return () => cancelRunningTask(runningTask);
},
[page, textContentDispatch],
);
// biome-ignore lint/correctness/useExhaustiveDependencies: Ommitted callbacks so they are not called every time they change
useEffect(() => {
if (textContent === undefined) {
return;
}
if (textContent === false) {
onLoadError();
return;
}
onLoadSuccess();
}, [textContent]);
/**
* Called when a text layer is rendered successfully
*/
const onRenderSuccess = useCallback(() => {
if (onRenderTextLayerSuccess) {
onRenderTextLayerSuccess();
}
}, [onRenderTextLayerSuccess]);
/**
* Called when a text layer failed to render successfully
*/
const onRenderError = useCallback(
(error: Error) => {
warning(false, error.toString());
if (onRenderTextLayerError) {
onRenderTextLayerError(error);
}
},
[onRenderTextLayerError],
);
function onMouseDown() {
const layer = layerElement.current;
if (!layer) {
return;
}
layer.classList.add('selecting');
}
function onMouseUp() {
const layer = layerElement.current;
if (!layer) {
return;
}
layer.classList.remove('selecting');
}
const viewport = useMemo(
() => page.getViewport({ scale, rotation: rotate }),
[page, rotate, scale],
);
useLayoutEffect(
function renderTextLayer() {
if (!page || !textContent) {
return;
}
const { current: layer } = layerElement;
if (!layer) {
return;
}
layer.innerHTML = '';
const textContentSource = page.streamTextContent({ includeMarkedContent: true });
const parameters = {
container: layer,
textContentSource,
viewport,
};
const cancellable = new pdfjs.TextLayer(parameters);
const runningTask = cancellable;
cancellable
.render()
.then(() => {
const end = document.createElement('div');
end.className = 'endOfContent';
layer.append(end);
const layerChildren = layer.querySelectorAll('[role="presentation"]');
if (customTextRenderer) {
let index = 0;
textContent.items.forEach((item, itemIndex) => {
if (!isTextItem(item)) {
return;
}
const child = layerChildren[index];
if (!child) {
return;
}
const content = customTextRenderer({
pageIndex,
pageNumber,
itemIndex,
...item,
});
child.innerHTML = content;
index += item.str && item.hasEOL ? 2 : 1;
});
}
// Intentional immediate callback
onRenderSuccess();
})
.catch(onRenderError);
return () => cancelRunningTask(runningTask);
},
[
customTextRenderer,
onRenderError,
onRenderSuccess,
page,
pageIndex,
pageNumber,
textContent,
viewport,
],
);
return (
<div
className={clsx('react-pdf__Page__textContent', 'textLayer')}
onMouseUp={onMouseUp}
onMouseDown={onMouseDown}
ref={layerElement}
/>
);
}

9
node_modules/react-pdf/src/PageContext.tsx generated vendored Normal file
View File

@@ -0,0 +1,9 @@
'use client';
import { createContext } from 'react';
import type { PageContextType } from './shared/types.js';
const pageContext: React.Context<PageContextType> = createContext<PageContextType>(null);
export default pageContext;

8
node_modules/react-pdf/src/PasswordResponses.ts generated vendored Normal file
View File

@@ -0,0 +1,8 @@
// As defined in https://github.com/mozilla/pdf.js/blob/d9fac3459609a807be6506fb3441b5da4b154d14/src/shared/util.js#L371-L374
const PasswordResponses = {
NEED_PASSWORD: 1,
INCORRECT_PASSWORD: 2,
} as const;
export default PasswordResponses;

18
node_modules/react-pdf/src/Ref.spec.ts generated vendored Normal file
View File

@@ -0,0 +1,18 @@
import { describe, expect, it } from 'vitest';
import Ref from './Ref.js';
describe('Ref', () => {
it('returns proper reference for given num and gen', () => {
const num = 1;
const gen = 2;
const ref = new Ref({ num, gen });
expect(ref.toString()).toBe('1R2');
});
it('returns proper reference for given num and gen when gen = 0', () => {
const num = 1;
const gen = 0;
const ref = new Ref({ num, gen });
expect(ref.toString()).toBe('1R');
});
});

17
node_modules/react-pdf/src/Ref.ts generated vendored Normal file
View File

@@ -0,0 +1,17 @@
export default class Ref {
num: number;
gen: number;
constructor({ num, gen }: { num: number; gen: number }) {
this.num = num;
this.gen = gen;
}
toString(): string {
let str = `${this.num}R`;
if (this.gen !== 0) {
str += this.gen;
}
return str;
}
}

141
node_modules/react-pdf/src/StructTree.spec.tsx generated vendored Normal file
View File

@@ -0,0 +1,141 @@
import { beforeAll, describe, expect, it } from 'vitest';
import { render } from '@testing-library/react';
import { pdfjs } from './index.test.js';
import StructTree from './StructTree.js';
import failingPage from '../../../__mocks__/_failing_page.js';
import { loadPDF, makeAsyncCallback, muteConsole, restoreConsole } from '../../../test-utils.js';
import PageContext from './PageContext.js';
import type { PDFPageProxy } from 'pdfjs-dist';
import type { PageContextType } from './shared/types.js';
import type { StructTreeNode } from 'pdfjs-dist/types/src/display/api.js';
const pdfFile = loadPDF('./../../__mocks__/_pdf.pdf');
function renderWithContext(children: React.ReactNode, context: Partial<PageContextType>) {
const { rerender, ...otherResult } = render(
<PageContext.Provider value={context as PageContextType}>{children}</PageContext.Provider>,
);
return {
...otherResult,
rerender: (nextChildren: React.ReactNode, nextContext: Partial<PageContextType> = context) =>
rerender(
<PageContext.Provider value={nextContext as PageContextType}>
{nextChildren}
</PageContext.Provider>,
),
};
}
describe('StructTree', () => {
// Loaded page
let page: PDFPageProxy;
let page2: PDFPageProxy;
// Loaded structure tree
let desiredStructTree: StructTreeNode;
let desiredStructTree2: StructTreeNode;
beforeAll(async () => {
const pdf = await pdfjs.getDocument({ data: pdfFile.arrayBuffer }).promise;
page = await pdf.getPage(1);
desiredStructTree = await page.getStructTree();
page2 = await pdf.getPage(2);
desiredStructTree2 = await page2.getStructTree();
});
describe('loading', () => {
it('loads structure tree and calls onGetStructTreeSuccess callback properly', async () => {
const { func: onGetStructTreeSuccess, promise: onGetStructTreeSuccessPromise } =
makeAsyncCallback();
renderWithContext(<StructTree />, {
onGetStructTreeSuccess,
page,
});
expect.assertions(1);
await expect(onGetStructTreeSuccessPromise).resolves.toMatchObject([desiredStructTree]);
});
it('calls onGetStructTreeError when failed to load annotations', async () => {
const { func: onGetStructTreeError, promise: onGetStructTreeErrorPromise } =
makeAsyncCallback();
muteConsole();
renderWithContext(<StructTree />, {
onGetStructTreeError,
page: failingPage,
});
expect.assertions(1);
await expect(onGetStructTreeErrorPromise).resolves.toMatchObject([expect.any(Error)]);
restoreConsole();
});
it('replaces structure tree properly when page is changed', async () => {
const { func: onGetStructTreeSuccess, promise: onGetStructTreeSuccessPromise } =
makeAsyncCallback();
const { rerender } = renderWithContext(<StructTree />, {
onGetStructTreeSuccess,
page,
});
expect.assertions(2);
await expect(onGetStructTreeSuccessPromise).resolves.toMatchObject([desiredStructTree]);
const { func: onGetStructTreeSuccess2, promise: onGetStructTreeSuccessPromise2 } =
makeAsyncCallback();
rerender(<StructTree />, {
onGetStructTreeSuccess: onGetStructTreeSuccess2,
page: page2,
});
await expect(onGetStructTreeSuccessPromise2).resolves.toMatchObject([desiredStructTree2]);
});
it('throws an error when placed outside Page', () => {
muteConsole();
expect(() => render(<StructTree />)).toThrow();
restoreConsole();
});
});
describe('rendering', () => {
it('renders structure tree properly', async () => {
const { func: onGetStructTreeSuccess, promise: onGetStructTreeSuccessPromise } =
makeAsyncCallback();
const { container } = renderWithContext(<StructTree />, {
onGetStructTreeSuccess,
page,
});
expect.assertions(1);
await onGetStructTreeSuccessPromise;
const wrapper = container.firstElementChild as HTMLSpanElement;
expect(wrapper.outerHTML).toBe(
'<span class="react-pdf__Page__structTree structTree"><span><span role="heading" aria-level="1" aria-owns="p3R_mc0"></span><span aria-owns="p3R_mc1"></span><span aria-owns="p3R_mc2"></span><span role="figure" aria-owns="p3R_mc12"></span><span aria-owns="p3R_mc3"></span><span aria-owns="p3R_mc4"></span><span role="heading" aria-level="2" aria-owns="p3R_mc5"></span><span aria-owns="p3R_mc6"></span><span><span aria-owns="p3R_mc7"></span><span role="link"><span aria-owns="13R"></span><span aria-owns="p3R_mc8"></span></span><span aria-owns="p3R_mc9"></span></span><span aria-owns="p3R_mc10"></span><span aria-owns="p3R_mc11"></span></span></span>',
);
});
});
});

107
node_modules/react-pdf/src/StructTree.tsx generated vendored Normal file
View File

@@ -0,0 +1,107 @@
import { useEffect } from 'react';
import makeCancellable from 'make-cancellable-promise';
import invariant from 'tiny-invariant';
import warning from 'warning';
import StructTreeItem from './StructTreeItem.js';
import usePageContext from './shared/hooks/usePageContext.js';
import useResolver from './shared/hooks/useResolver.js';
import { cancelRunningTask } from './shared/utils.js';
import type { StructTreeNodeWithExtraAttributes } from './shared/types.js';
export default function StructTree(): React.ReactElement | null {
const pageContext = usePageContext();
invariant(pageContext, 'Unable to find Page context.');
const {
onGetStructTreeError: onGetStructTreeErrorProps,
onGetStructTreeSuccess: onGetStructTreeSuccessProps,
} = pageContext;
const [structTreeState, structTreeDispatch] = useResolver<StructTreeNodeWithExtraAttributes>();
const { value: structTree, error: structTreeError } = structTreeState;
const { customTextRenderer, page } = pageContext;
function onLoadSuccess() {
if (!structTree) {
// Impossible, but TypeScript doesn't know that
return;
}
if (onGetStructTreeSuccessProps) {
onGetStructTreeSuccessProps(structTree);
}
}
function onLoadError() {
if (!structTreeError) {
// Impossible, but TypeScript doesn't know that
return;
}
warning(false, structTreeError.toString());
if (onGetStructTreeErrorProps) {
onGetStructTreeErrorProps(structTreeError);
}
}
// biome-ignore lint/correctness/useExhaustiveDependencies: useEffect intentionally triggered on page change
useEffect(
function resetStructTree() {
structTreeDispatch({ type: 'RESET' });
},
[structTreeDispatch, page],
);
useEffect(
function loadStructTree() {
if (customTextRenderer) {
// TODO: Document why this is necessary
return;
}
if (!page) {
return;
}
const cancellable = makeCancellable(page.getStructTree());
const runningTask = cancellable;
cancellable.promise
.then((nextStructTree) => {
structTreeDispatch({ type: 'RESOLVE', value: nextStructTree });
})
.catch((error) => {
structTreeDispatch({ type: 'REJECT', error });
});
return () => cancelRunningTask(runningTask);
},
[customTextRenderer, page, structTreeDispatch],
);
// biome-ignore lint/correctness/useExhaustiveDependencies: Ommitted callbacks so they are not called every time they change
useEffect(() => {
if (structTree === undefined) {
return;
}
if (structTree === false) {
onLoadError();
return;
}
onLoadSuccess();
}, [structTree]);
if (!structTree) {
return null;
}
return <StructTreeItem className="react-pdf__Page__structTree structTree" node={structTree} />;
}

45
node_modules/react-pdf/src/StructTreeItem.tsx generated vendored Normal file
View File

@@ -0,0 +1,45 @@
import { useMemo } from 'react';
import {
getAttributes,
isStructTreeNode,
isStructTreeNodeWithOnlyContentChild,
} from './shared/structTreeUtils.js';
import type { StructTreeContent } from 'pdfjs-dist/types/src/display/api.js';
import type { StructTreeNodeWithExtraAttributes } from './shared/types.js';
type StructTreeItemProps = {
className?: string;
node: StructTreeNodeWithExtraAttributes | StructTreeContent;
};
export default function StructTreeItem({
className,
node,
}: StructTreeItemProps): React.ReactElement {
const attributes = useMemo(() => getAttributes(node), [node]);
const children = useMemo(() => {
if (!isStructTreeNode(node)) {
return null;
}
if (isStructTreeNodeWithOnlyContentChild(node)) {
return null;
}
return node.children.map((child, index) => {
return (
// biome-ignore lint/suspicious/noArrayIndexKey: index is stable here
<StructTreeItem key={index} node={child} />
);
});
}, [node]);
return (
<span className={className} {...attributes}>
{children}
</span>
);
}

628
node_modules/react-pdf/src/Thumbnail.spec.tsx generated vendored Normal file
View File

@@ -0,0 +1,628 @@
import { beforeAll, describe, expect, it, vi } from 'vitest';
import { createRef } from 'react';
import { fireEvent, render } from '@testing-library/react';
import { pdfjs } from './index.test.js';
import Thumbnail from './Thumbnail.js';
import failingPdf from '../../../__mocks__/_failing_pdf.js';
import silentlyFailingPdf from '../../../__mocks__/_silently_failing_pdf.js';
import { loadPDF, makeAsyncCallback, muteConsole, restoreConsole } from '../../../test-utils.js';
import DocumentContext from './DocumentContext.js';
import type { PDFDocumentProxy, PDFPageProxy } from 'pdfjs-dist';
import type { DocumentContextType, PageCallback } from './shared/types.js';
const pdfFile = loadPDF('./../../__mocks__/_pdf.pdf');
const pdfFile2 = loadPDF('./../../__mocks__/_pdf2.pdf');
function renderWithContext(children: React.ReactNode, context: Partial<DocumentContextType>) {
const { rerender, ...otherResult } = render(
<DocumentContext.Provider value={context as DocumentContextType}>
{children}
</DocumentContext.Provider>,
);
return {
...otherResult,
rerender: (
nextChildren: React.ReactNode,
nextContext: Partial<DocumentContextType> = context,
) =>
rerender(
<DocumentContext.Provider value={nextContext as DocumentContextType}>
{nextChildren}
</DocumentContext.Provider>,
),
};
}
describe('Thumbnail', () => {
// Loaded PDF file
let pdf: PDFDocumentProxy;
let pdf2: PDFDocumentProxy;
// Object with basic loaded page information that shall match after successful loading
const desiredLoadedThumbnail: Partial<PDFPageProxy> = {};
const desiredLoadedThumbnail2: Partial<PDFPageProxy> = {};
const desiredLoadedThumbnail3: Partial<PDFPageProxy> = {};
beforeAll(async () => {
pdf = await pdfjs.getDocument({ data: pdfFile.arrayBuffer }).promise;
const page = await pdf.getPage(1);
desiredLoadedThumbnail._pageIndex = page._pageIndex;
desiredLoadedThumbnail._pageInfo = page._pageInfo;
const page2 = await pdf.getPage(2);
desiredLoadedThumbnail2._pageIndex = page2._pageIndex;
desiredLoadedThumbnail2._pageInfo = page2._pageInfo;
pdf2 = await pdfjs.getDocument({ data: pdfFile2.arrayBuffer }).promise;
const page3 = await pdf2.getPage(1);
desiredLoadedThumbnail3._pageIndex = page3._pageIndex;
desiredLoadedThumbnail3._pageInfo = page3._pageInfo;
});
describe('loading', () => {
it('loads a page and calls onLoadSuccess callback properly when placed inside Document', async () => {
const { func: onLoadSuccess, promise: onLoadSuccessPromise } = makeAsyncCallback();
renderWithContext(<Thumbnail onLoadSuccess={onLoadSuccess} pageIndex={0} />, { pdf });
expect.assertions(1);
await expect(onLoadSuccessPromise).resolves.toMatchObject([desiredLoadedThumbnail]);
});
it('loads a page and calls onLoadSuccess callback properly when pdf prop is passed', async () => {
const { func: onLoadSuccess, promise: onLoadSuccessPromise } = makeAsyncCallback();
render(<Thumbnail onLoadSuccess={onLoadSuccess} pageIndex={0} pdf={pdf} />);
expect.assertions(1);
await expect(onLoadSuccessPromise).resolves.toMatchObject([desiredLoadedThumbnail]);
});
it('returns all desired parameters in onLoadSuccess callback', async () => {
const { func: onLoadSuccess, promise: onLoadSuccessPromise } =
makeAsyncCallback<[PageCallback]>();
renderWithContext(<Thumbnail onLoadSuccess={onLoadSuccess} pageIndex={0} />, { pdf });
expect.assertions(5);
const [page] = await onLoadSuccessPromise;
expect(page.width).toBeDefined();
expect(page.height).toBeDefined();
expect(page.originalWidth).toBeDefined();
expect(page.originalHeight).toBeDefined();
// Example of a method that got stripped away in the past
expect(page.getTextContent).toBeInstanceOf(Function);
});
it('calls onLoadError when failed to load a page', async () => {
const { func: onLoadError, promise: onLoadErrorPromise } = makeAsyncCallback();
muteConsole();
renderWithContext(<Thumbnail onLoadError={onLoadError} pageIndex={0} />, { pdf: failingPdf });
expect.assertions(1);
await expect(onLoadErrorPromise).resolves.toMatchObject([expect.any(Error)]);
restoreConsole();
});
it('loads page when given pageIndex', async () => {
const { func: onLoadSuccess, promise: onLoadSuccessPromise } = makeAsyncCallback();
renderWithContext(<Thumbnail onLoadSuccess={onLoadSuccess} pageIndex={0} />, { pdf });
expect.assertions(1);
const [page] = await onLoadSuccessPromise;
expect(page).toMatchObject(desiredLoadedThumbnail);
});
it('loads page when given pageNumber', async () => {
const { func: onLoadSuccess, promise: onLoadSuccessPromise } = makeAsyncCallback();
renderWithContext(<Thumbnail onLoadSuccess={onLoadSuccess} pageNumber={1} />, { pdf });
expect.assertions(1);
const [page] = await onLoadSuccessPromise;
expect(page).toMatchObject(desiredLoadedThumbnail);
});
it('loads page of a given number when given conflicting pageNumber and pageIndex', async () => {
const { func: onLoadSuccess, promise: onLoadSuccessPromise } = makeAsyncCallback();
renderWithContext(<Thumbnail onLoadSuccess={onLoadSuccess} pageIndex={1} pageNumber={1} />, {
pdf,
});
expect.assertions(1);
const [page] = await onLoadSuccessPromise;
expect(page).toMatchObject(desiredLoadedThumbnail);
});
it('replaces a page properly when pdf is changed', async () => {
const { func: onLoadSuccess, promise: onLoadSuccessPromise } = makeAsyncCallback();
const { rerender } = renderWithContext(
<Thumbnail onLoadSuccess={onLoadSuccess} pageIndex={0} />,
{
pdf,
},
);
expect.assertions(2);
await expect(onLoadSuccessPromise).resolves.toMatchObject([desiredLoadedThumbnail]);
const { func: onLoadSuccess2, promise: onLoadSuccessPromise2 } = makeAsyncCallback();
rerender(<Thumbnail onLoadSuccess={onLoadSuccess2} pageIndex={0} />, { pdf: pdf2 });
await expect(onLoadSuccessPromise2).resolves.toMatchObject([desiredLoadedThumbnail3]);
});
it('replaces a page properly when pageNumber is changed', async () => {
const { func: onLoadSuccess, promise: onLoadSuccessPromise } = makeAsyncCallback();
const { rerender } = renderWithContext(
<Thumbnail onLoadSuccess={onLoadSuccess} pageIndex={0} />,
{
pdf,
},
);
expect.assertions(2);
await expect(onLoadSuccessPromise).resolves.toMatchObject([desiredLoadedThumbnail]);
const { func: onLoadSuccess2, promise: onLoadSuccessPromise2 } = makeAsyncCallback();
rerender(<Thumbnail onLoadSuccess={onLoadSuccess2} pageIndex={1} />, { pdf });
await expect(onLoadSuccessPromise2).resolves.toMatchObject([desiredLoadedThumbnail2]);
});
it('throws an error when placed outside Document without pdf prop passed', () => {
muteConsole();
expect(() => render(<Thumbnail pageIndex={0} />)).toThrow();
restoreConsole();
});
});
describe('rendering', () => {
it('applies className to its wrapper when given a string', () => {
const className = 'testClassName';
const { container } = renderWithContext(<Thumbnail className={className} pageIndex={0} />, {
pdf,
});
const wrapper = container.querySelector('.react-pdf__Thumbnail');
expect(wrapper).toHaveClass(className);
});
it('passes container element to inputRef properly', () => {
const inputRef = createRef<HTMLDivElement>();
renderWithContext(<Thumbnail inputRef={inputRef} pageIndex={1} />, {
pdf: silentlyFailingPdf,
});
expect(inputRef.current).toBeInstanceOf(HTMLDivElement);
});
it('passes canvas element to ThumbnailCanvas properly', async () => {
const { func: onLoadSuccess, promise: onLoadSuccessPromise } = makeAsyncCallback();
const canvasRef = createRef<HTMLCanvasElement>();
const { container } = renderWithContext(
<Thumbnail canvasRef={canvasRef} onLoadSuccess={onLoadSuccess} pageIndex={0} />,
{ pdf },
);
expect.assertions(1);
await onLoadSuccessPromise;
const pageCanvas = container.querySelector('.react-pdf__Thumbnail__page__canvas');
expect(canvasRef.current).toBe(pageCanvas);
});
it('renders "No page specified." when given neither pageIndex nor pageNumber', () => {
muteConsole();
const { container } = renderWithContext(<Thumbnail />, { pdf });
const noData = container.querySelector('.react-pdf__message');
expect(noData).toBeInTheDocument();
expect(noData).toHaveTextContent('No page specified.');
restoreConsole();
});
it('renders custom no data message when given nothing and noData is given', () => {
muteConsole();
const { container } = renderWithContext(<Thumbnail noData="Nothing here" />, { pdf });
const noData = container.querySelector('.react-pdf__message');
expect(noData).toBeInTheDocument();
expect(noData).toHaveTextContent('Nothing here');
restoreConsole();
});
it('renders custom no data message when given nothing and noData is given as a function', () => {
muteConsole();
const { container } = renderWithContext(<Thumbnail noData={() => 'Nothing here'} />, { pdf });
const noData = container.querySelector('.react-pdf__message');
expect(noData).toBeInTheDocument();
expect(noData).toHaveTextContent('Nothing here');
restoreConsole();
});
it('renders "Loading page…" when loading a page', async () => {
const { container } = renderWithContext(<Thumbnail pageIndex={0} />, { pdf });
const loading = container.querySelector('.react-pdf__message');
expect(loading).toBeInTheDocument();
expect(loading).toHaveTextContent('Loading page…');
});
it('renders custom loading message when loading a page and loading prop is given', async () => {
const { container } = renderWithContext(<Thumbnail loading="Loading" pageIndex={0} />, {
pdf,
});
const loading = container.querySelector('.react-pdf__message');
expect(loading).toBeInTheDocument();
expect(loading).toHaveTextContent('Loading');
});
it('renders custom loading message when loading a page and loading prop is given as a function', async () => {
const { container } = renderWithContext(
<Thumbnail loading={() => 'Loading'} pageIndex={0} />,
{
pdf,
},
);
const loading = container.querySelector('.react-pdf__message');
expect(loading).toBeInTheDocument();
expect(loading).toHaveTextContent('Loading');
});
it('ignores pageIndex when given pageIndex and pageNumber', async () => {
const { func: onLoadSuccess, promise: onLoadSuccessPromise } = makeAsyncCallback();
renderWithContext(<Thumbnail onLoadSuccess={onLoadSuccess} pageIndex={1} pageNumber={1} />, {
pdf,
});
expect.assertions(1);
const [page] = await onLoadSuccessPromise;
expect(page).toMatchObject(desiredLoadedThumbnail);
});
it('requests page to be rendered with default rotation when given nothing', async () => {
const { func: onRenderSuccess, promise: onRenderSuccessPromise } =
makeAsyncCallback<[PageCallback]>();
const { container } = renderWithContext(
<Thumbnail onRenderSuccess={onRenderSuccess} pageIndex={0} />,
{ pdf },
);
const [page] = await onRenderSuccessPromise;
const pageCanvas = container.querySelector(
'.react-pdf__Thumbnail__page__canvas',
) as HTMLCanvasElement;
const { width, height } = window.getComputedStyle(pageCanvas);
const viewport = page.getViewport({ scale: 1 });
// Expect the canvas layer not to be rotated
expect(Number.parseInt(width, 10)).toBe(Math.floor(viewport.width));
expect(Number.parseInt(height, 10)).toBe(Math.floor(viewport.height));
});
it('requests page to be rendered with given rotation when given rotate prop', async () => {
const { func: onRenderSuccess, promise: onRenderSuccessPromise } =
makeAsyncCallback<[PageCallback]>();
const rotate = 90;
const { container } = renderWithContext(
<Thumbnail onRenderSuccess={onRenderSuccess} pageIndex={0} rotate={rotate} />,
{ pdf },
);
const [page] = await onRenderSuccessPromise;
const pageCanvas = container.querySelector(
'.react-pdf__Thumbnail__page__canvas',
) as HTMLCanvasElement;
const { width, height } = window.getComputedStyle(pageCanvas);
const viewport = page.getViewport({ scale: 1, rotation: rotate });
// Expect the canvas layer to be rotated
expect(Number.parseInt(width, 10)).toBe(Math.floor(viewport.width));
expect(Number.parseInt(height, 10)).toBe(Math.floor(viewport.height));
});
it('requests page to be rendered in canvas mode by default', async () => {
const { func: onLoadSuccess, promise: onLoadSuccessPromise } = makeAsyncCallback();
const { container } = renderWithContext(
<Thumbnail onLoadSuccess={onLoadSuccess} pageIndex={0} />,
{ pdf },
);
expect.assertions(1);
await onLoadSuccessPromise;
const pageCanvas = container.querySelector('.react-pdf__Thumbnail__page__canvas');
expect(pageCanvas).toBeInTheDocument();
});
it('requests page not to be rendered when given renderMode = "none"', async () => {
const { func: onLoadSuccess, promise: onLoadSuccessPromise } = makeAsyncCallback();
const { container } = renderWithContext(
<Thumbnail onLoadSuccess={onLoadSuccess} pageIndex={0} renderMode="none" />,
{ pdf },
);
expect.assertions(1);
await onLoadSuccessPromise;
const pageCanvas = container.querySelector('.react-pdf__Thumbnail__page__canvas');
expect(pageCanvas).not.toBeInTheDocument();
});
it('requests page to be rendered in canvas mode when given renderMode = "canvas"', async () => {
const { func: onLoadSuccess, promise: onLoadSuccessPromise } = makeAsyncCallback();
const { container } = renderWithContext(
<Thumbnail onLoadSuccess={onLoadSuccess} pageIndex={0} renderMode="canvas" />,
{ pdf },
);
expect.assertions(1);
await onLoadSuccessPromise;
const pageCanvas = container.querySelector('.react-pdf__Thumbnail__page__canvas');
expect(pageCanvas).toBeInTheDocument();
});
it('requests page to be rendered in custom mode when given renderMode = "custom"', async () => {
const { func: onLoadSuccess, promise: onLoadSuccessPromise } = makeAsyncCallback();
function CustomRenderer() {
return <div className="custom-renderer" />;
}
const { container } = renderWithContext(
<Thumbnail
customRenderer={CustomRenderer}
onLoadSuccess={onLoadSuccess}
pageIndex={0}
renderMode="custom"
/>,
{ pdf },
);
expect.assertions(1);
await onLoadSuccessPromise;
const customRenderer = container.querySelector('.custom-renderer');
expect(customRenderer).toBeInTheDocument();
});
});
it('requests page to be rendered at its original size given nothing', async () => {
const { func: onLoadSuccess, promise: onLoadSuccessPromise } =
makeAsyncCallback<[PageCallback]>();
renderWithContext(<Thumbnail onLoadSuccess={onLoadSuccess} pageIndex={0} />, { pdf });
expect.assertions(1);
const [page] = await onLoadSuccessPromise;
expect(page.width).toEqual(page.originalWidth);
});
it('requests page to be rendered with a proper scale when given scale', async () => {
const { func: onLoadSuccess, promise: onLoadSuccessPromise } =
makeAsyncCallback<[PageCallback]>();
const scale = 1.5;
renderWithContext(<Thumbnail onLoadSuccess={onLoadSuccess} pageIndex={0} scale={scale} />, {
pdf,
});
expect.assertions(1);
const [page] = await onLoadSuccessPromise;
expect(page.width).toEqual(page.originalWidth * scale);
});
it('requests page to be rendered with a proper scale when given width', async () => {
const { func: onLoadSuccess, promise: onLoadSuccessPromise } =
makeAsyncCallback<[PageCallback]>();
const width = 600;
renderWithContext(<Thumbnail onLoadSuccess={onLoadSuccess} pageIndex={0} width={width} />, {
pdf,
});
expect.assertions(1);
const [page] = await onLoadSuccessPromise;
expect(page.width).toEqual(width);
});
it('requests page to be rendered with a proper scale when given width and scale (multiplies)', async () => {
const { func: onLoadSuccess, promise: onLoadSuccessPromise } =
makeAsyncCallback<[PageCallback]>();
const width = 600;
const scale = 1.5;
renderWithContext(
<Thumbnail onLoadSuccess={onLoadSuccess} pageIndex={0} scale={scale} width={width} />,
{
pdf,
},
);
expect.assertions(1);
const [page] = await onLoadSuccessPromise;
expect(page.width).toBeCloseTo(width * scale);
});
it('requests page to be rendered with a proper scale when given height', async () => {
const { func: onLoadSuccess, promise: onLoadSuccessPromise } =
makeAsyncCallback<[PageCallback]>();
const height = 850;
renderWithContext(<Thumbnail height={height} onLoadSuccess={onLoadSuccess} pageIndex={0} />, {
pdf,
});
expect.assertions(1);
const [page] = await onLoadSuccessPromise;
expect(page.height).toEqual(height);
});
it('requests page to be rendered with a proper scale when given height and scale (multiplies)', async () => {
const { func: onLoadSuccess, promise: onLoadSuccessPromise } =
makeAsyncCallback<[PageCallback]>();
const height = 850;
const scale = 1.5;
renderWithContext(
<Thumbnail height={height} onLoadSuccess={onLoadSuccess} pageIndex={0} scale={scale} />,
{
pdf,
},
);
expect.assertions(1);
const [page] = await onLoadSuccessPromise;
expect(page.height).toBeCloseTo(height * scale);
});
it('requests page to be rendered with a proper scale when given width and height (ignores height)', async () => {
const { func: onLoadSuccess, promise: onLoadSuccessPromise } =
makeAsyncCallback<[PageCallback]>();
const width = 600;
const height = 100;
renderWithContext(
<Thumbnail height={height} onLoadSuccess={onLoadSuccess} pageIndex={0} width={width} />,
{
pdf,
},
);
expect.assertions(2);
const [page] = await onLoadSuccessPromise;
expect(page.width).toEqual(width);
// Expect proportions to be correct even though invalid height was provided
expect(page.height).toEqual(page.originalHeight * (page.width / page.originalWidth));
});
it('requests page to be rendered with a proper scale when given width, height and scale (ignores height, multiplies)', async () => {
const { func: onLoadSuccess, promise: onLoadSuccessPromise } =
makeAsyncCallback<[PageCallback]>();
const width = 600;
const height = 100;
const scale = 1.5;
renderWithContext(
<Thumbnail
height={height}
onLoadSuccess={onLoadSuccess}
pageIndex={0}
scale={scale}
width={width}
/>,
{ pdf },
);
expect.assertions(2);
const [page] = await onLoadSuccessPromise;
expect(page.width).toBeCloseTo(width * scale);
// Expect proportions to be correct even though invalid height was provided
expect(page.height).toEqual(page.originalHeight * (page.width / page.originalWidth));
});
it('calls onTouchStart callback when touched a page (sample of touch events family)', () => {
const onTouchStart = vi.fn();
const { container } = renderWithContext(<Thumbnail onTouchStart={onTouchStart} />, { pdf });
const page = container.querySelector('.react-pdf__Thumbnail__page') as HTMLDivElement;
fireEvent.touchStart(page);
expect(onTouchStart).toHaveBeenCalled();
});
});

112
node_modules/react-pdf/src/Thumbnail.tsx generated vendored Normal file
View File

@@ -0,0 +1,112 @@
'use client';
import clsx from 'clsx';
import invariant from 'tiny-invariant';
import Page from './Page.js';
import { isProvided } from './shared/utils.js';
import useDocumentContext from './shared/hooks/useDocumentContext.js';
import type { PageProps } from './Page.js';
import type { ClassName, OnItemClickArgs } from './shared/types.js';
export type ThumbnailProps = Omit<
PageProps,
| 'className'
| 'customTextRenderer'
| 'onGetAnnotationsError'
| 'onGetAnnotationsSuccess'
| 'onGetTextError'
| 'onGetTextSuccess'
| 'onRenderAnnotationLayerError'
| 'onRenderAnnotationLayerSuccess'
| 'onRenderTextLayerError'
| 'onRenderTextLayerSuccess'
| 'renderAnnotationLayer'
| 'renderForms'
| 'renderTextLayer'
> & {
/**
* Class name(s) that will be added to rendered element along with the default `react-pdf__Thumbnail`.
*
* @example 'custom-class-name-1 custom-class-name-2'
* @example ['custom-class-name-1', 'custom-class-name-2']
*/
className?: ClassName;
/**
* Function called when a thumbnail has been clicked. Usually, you would like to use this callback to move the user wherever they requested to.
*
* @example ({ dest, pageIndex, pageNumber }) => alert('Clicked an item from page ' + pageNumber + '!')
*/
onItemClick?: (args: OnItemClickArgs) => void;
};
/**
* Displays a thumbnail of a page. Does not render the annotation layer or the text layer. Does not register itself as a link target, so the user will not be scrolled to a Thumbnail component when clicked on an internal link (e.g. in Table of Contents). When clicked, attempts to navigate to the page clicked (similarly to a link in Outline).
*
* Should be placed inside `<Document />`. Alternatively, it can have `pdf` prop passed, which can be obtained from `<Document />`'s `onLoadSuccess` callback function.
*/
export default function Thumbnail(props: ThumbnailProps): React.ReactElement {
const documentContext = useDocumentContext();
const mergedProps = { ...documentContext, ...props };
const {
className,
linkService,
onItemClick,
pageIndex: pageIndexProps,
pageNumber: pageNumberProps,
pdf,
} = mergedProps;
invariant(
pdf,
'Attempted to load a thumbnail, but no document was specified. Wrap <Thumbnail /> in a <Document /> or pass explicit `pdf` prop.',
);
const pageIndex = isProvided(pageNumberProps) ? pageNumberProps - 1 : (pageIndexProps ?? null);
const pageNumber = pageNumberProps ?? (isProvided(pageIndexProps) ? pageIndexProps + 1 : null);
function onClick(event: React.MouseEvent<HTMLAnchorElement>) {
event.preventDefault();
if (!isProvided(pageIndex) || !pageNumber) {
return;
}
invariant(
onItemClick || linkService,
'Either onItemClick callback or linkService must be defined in order to navigate to an outline item.',
);
if (onItemClick) {
onItemClick({
pageIndex,
pageNumber,
});
} else if (linkService) {
linkService.goToPage(pageNumber);
}
}
const { className: classNameProps, onItemClick: onItemClickProps, ...pageProps } = props;
return (
<a
className={clsx('react-pdf__Thumbnail', className)}
href={pageNumber ? '#' : undefined}
onClick={onClick}
>
<Page
{...pageProps}
_className="react-pdf__Thumbnail__page"
_enableRegisterUnregisterPage={false}
renderAnnotationLayer={false}
renderTextLayer={false}
/>
</a>
);
}

30
node_modules/react-pdf/src/index.spec.ts generated vendored Normal file
View File

@@ -0,0 +1,30 @@
import { describe, expect, it } from 'vitest';
import { pdfjs, Document, Outline, Page, Thumbnail } from './index.js';
describe('default entry', () => {
describe('has pdfjs exported properly', () => {
it('has pdfjs.version exported properly', () => {
expect(typeof pdfjs.version).toBe('string');
});
it('has GlobalWorkerOptions exported properly', () => {
expect(typeof pdfjs.GlobalWorkerOptions).toBe('function');
});
});
it('has Document exported properly', () => {
expect(Document).toBeInstanceOf(Object);
});
it('has Outline exported properly', () => {
expect(Outline).toBeInstanceOf(Object);
});
it('has Page exported properly', () => {
expect(Page).toBeInstanceOf(Object);
});
it('has Thumbnail exported properly', () => {
expect(Thumbnail).toBeInstanceOf(Object);
});
});

28
node_modules/react-pdf/src/index.test.ts generated vendored Normal file
View File

@@ -0,0 +1,28 @@
import * as pdfjs from 'pdfjs-dist';
import Document from './Document.js';
import Outline from './Outline.js';
import Page from './Page.js';
import Thumbnail from './Thumbnail.js';
import useDocumentContext from './shared/hooks/useDocumentContext.js';
import useOutlineContext from './shared/hooks/useOutlineContext.js';
import usePageContext from './shared/hooks/usePageContext.js';
export type { DocumentProps } from './Document.js';
export type { OutlineProps } from './Outline.js';
export type { PageProps } from './Page.js';
export type { ThumbnailProps } from './Thumbnail.js';
import './pdf.worker.entry.js';
export {
pdfjs,
Document,
Outline,
Page,
Thumbnail,
useDocumentContext,
useOutlineContext,
usePageContext,
};

35
node_modules/react-pdf/src/index.ts generated vendored Normal file
View File

@@ -0,0 +1,35 @@
import * as pdfjs from 'pdfjs-dist';
import Document from './Document.js';
import Outline from './Outline.js';
import Page from './Page.js';
import Thumbnail from './Thumbnail.js';
import useDocumentContext from './shared/hooks/useDocumentContext.js';
import useOutlineContext from './shared/hooks/useOutlineContext.js';
import usePageContext from './shared/hooks/usePageContext.js';
import PasswordResponses from './PasswordResponses.js';
export type { DocumentProps } from './Document.js';
export type { OutlineProps } from './Outline.js';
export type { PageProps } from './Page.js';
export type { ThumbnailProps } from './Thumbnail.js';
import { displayWorkerWarning } from './shared/utils.js';
displayWorkerWarning();
pdfjs.GlobalWorkerOptions.workerSrc = 'pdf.worker.mjs';
export {
pdfjs,
Document,
Outline,
Page,
Thumbnail,
useDocumentContext,
useOutlineContext,
usePageContext,
PasswordResponses,
};

15
node_modules/react-pdf/src/pdf.worker.entry.ts generated vendored Normal file
View File

@@ -0,0 +1,15 @@
/**
* PDF.js worker entry file.
*
* This file is identical to Mozilla's pdf.worker.entry.js, with one exception being placed inside
* this bundle, not theirs.
*/
(
(typeof window !== 'undefined' ? window : {}) as Window &
typeof globalThis & { pdfjsWorker: unknown }
).pdfjsWorker =
// @ts-expect-error - pdfjs-dist does not ship with types
await import('pdfjs-dist/build/pdf.worker.mjs');
export {};

59
node_modules/react-pdf/src/shared/constants.ts generated vendored Normal file
View File

@@ -0,0 +1,59 @@
// From pdfjs-dist/lib/web/struct_tree_layer_builder.js
export const PDF_ROLE_TO_HTML_ROLE = {
// Document level structure types
Document: null, // There's a "document" role, but it doesn't make sense here.
DocumentFragment: null,
// Grouping level structure types
Part: 'group',
Sect: 'group', // XXX: There's a "section" role, but it's abstract.
Div: 'group',
Aside: 'note',
NonStruct: 'none',
// Block level structure types
P: null,
// H<n>,
H: 'heading',
Title: null,
FENote: 'note',
// Sub-block level structure type
Sub: 'group',
// General inline level structure types
Lbl: null,
Span: null,
Em: null,
Strong: null,
Link: 'link',
Annot: 'note',
Form: 'form',
// Ruby and Warichu structure types
Ruby: null,
RB: null,
RT: null,
RP: null,
Warichu: null,
WT: null,
WP: null,
// List standard structure types
L: 'list',
LI: 'listitem',
LBody: null,
// Table standard structure types
Table: 'table',
TR: 'row',
TH: 'columnheader',
TD: 'cell',
THead: 'columnheader',
TBody: null,
TFoot: null,
// Standard structure type Caption
Caption: null,
// Standard structure type Figure
Figure: 'figure',
// Standard structure type Formula
Formula: null,
// standard structure type Artifact
Artifact: null,
};
export const HEADING_PATTERN: RegExp = /^H(\d+)$/;

View File

@@ -0,0 +1,23 @@
'use client';
import { useRef } from 'react';
import { isDefined } from '../utils.js';
export default function useCachedValue<T>(getter: () => T): () => T {
const ref = useRef<T | undefined>(undefined);
const currentValue = ref.current;
if (isDefined(currentValue)) {
return () => currentValue;
}
return () => {
const value = getter();
ref.current = value;
return value;
};
}

View File

@@ -0,0 +1,9 @@
import { useContext } from 'react';
import DocumentContext from '../../DocumentContext.js';
import type { DocumentContextType } from '../types.js';
export default function useDocumentContext(): DocumentContextType {
return useContext(DocumentContext);
}

View File

@@ -0,0 +1,9 @@
import { useContext } from 'react';
import OutlineContext from '../../OutlineContext.js';
import type { OutlineContextType } from '../types.js';
export default function useOutlineContext(): OutlineContextType {
return useContext(OutlineContext);
}

View File

@@ -0,0 +1,9 @@
import { useContext } from 'react';
import PageContext from '../../PageContext.js';
import type { PageContextType } from '../types.js';
export default function usePageContext(): PageContextType {
return useContext(PageContext);
}

28
node_modules/react-pdf/src/shared/hooks/useResolver.ts generated vendored Normal file
View File

@@ -0,0 +1,28 @@
import { useReducer } from 'react';
type State<T> =
| { value: T; error: undefined }
| { value: false; error: Error }
| { value: undefined; error: undefined };
type Action<T> =
| { type: 'RESOLVE'; value: T }
| { type: 'REJECT'; error: Error }
| { type: 'RESET' };
function reducer<T>(state: State<T>, action: Action<T>): State<T> {
switch (action.type) {
case 'RESOLVE':
return { value: action.value, error: undefined };
case 'REJECT':
return { value: false, error: action.error };
case 'RESET':
return { value: undefined, error: undefined };
default:
return state;
}
}
export default function useResolver<T>(): [State<T>, React.Dispatch<Action<T>>] {
return useReducer(reducer<T>, { value: undefined, error: undefined });
}

97
node_modules/react-pdf/src/shared/structTreeUtils.ts generated vendored Normal file
View File

@@ -0,0 +1,97 @@
import { HEADING_PATTERN, PDF_ROLE_TO_HTML_ROLE } from './constants.js';
import type { StructTreeContent, StructTreeNode } from 'pdfjs-dist/types/src/display/api.js';
import type { StructTreeNodeWithExtraAttributes } from './types.js';
type PdfRole = keyof typeof PDF_ROLE_TO_HTML_ROLE;
type Attributes = React.HTMLAttributes<HTMLElement>;
export function isPdfRole(role: string): role is PdfRole {
return role in PDF_ROLE_TO_HTML_ROLE;
}
export function isStructTreeNode(node: StructTreeNode | StructTreeContent): node is StructTreeNode {
return 'children' in node;
}
export function isStructTreeNodeWithOnlyContentChild(
node: StructTreeNode | StructTreeContent,
): boolean {
if (!isStructTreeNode(node)) {
return false;
}
return node.children.length === 1 && 0 in node.children && 'id' in node.children[0];
}
export function getRoleAttributes(node: StructTreeNode | StructTreeContent): Attributes {
const attributes: Attributes = {};
if (isStructTreeNode(node)) {
const { role } = node;
const matches = role.match(HEADING_PATTERN);
if (matches) {
attributes.role = 'heading';
attributes['aria-level'] = Number(matches[1]);
} else if (isPdfRole(role)) {
const htmlRole = PDF_ROLE_TO_HTML_ROLE[role];
if (htmlRole) {
attributes.role = htmlRole;
}
}
}
return attributes;
}
export function getBaseAttributes(
node: StructTreeNodeWithExtraAttributes | StructTreeContent,
): Attributes {
const attributes: Attributes = {};
if (isStructTreeNode(node)) {
if (node.alt !== undefined) {
attributes['aria-label'] = node.alt;
}
if (node.lang !== undefined) {
attributes.lang = node.lang;
}
if (isStructTreeNodeWithOnlyContentChild(node)) {
const [child] = node.children;
if (child) {
const childAttributes = getBaseAttributes(child);
return {
...attributes,
...childAttributes,
};
}
}
} else {
if ('id' in node) {
attributes['aria-owns'] = node.id;
}
}
return attributes;
}
export function getAttributes(
node: StructTreeNodeWithExtraAttributes | StructTreeContent,
): Attributes | null {
if (!node) {
return null;
}
return {
...getRoleAttributes(node),
...getBaseAttributes(node),
};
}

174
node_modules/react-pdf/src/shared/types.ts generated vendored Normal file
View File

@@ -0,0 +1,174 @@
import type {
PDFDataRangeTransport,
PDFDocumentProxy,
PDFPageProxy,
PasswordResponses,
} from 'pdfjs-dist';
import type {
TypedArray,
DocumentInitParameters,
RefProxy,
StructTreeNode,
TextContent,
TextItem,
} from 'pdfjs-dist/types/src/display/api.js';
import type { AnnotationLayerParameters } from 'pdfjs-dist/types/src/display/annotation_layer.js';
import type LinkService from '../LinkService.js';
type NullableObject<T extends object> = { [P in keyof T]: T[P] | null };
type KeyOfUnion<T> = T extends unknown ? keyof T : never;
/* Primitive types */
export type Annotations = AnnotationLayerParameters['annotations'];
export type ClassName = string | null | undefined | (string | null | undefined)[];
export type ResolvedDest = (RefProxy | number)[];
export type Dest = Promise<ResolvedDest> | ResolvedDest | string | null;
export type ExternalLinkRel = string;
export type ExternalLinkTarget = '_self' | '_blank' | '_parent' | '_top';
export type ImageResourcesPath = string;
export type OnError = (error: Error) => void;
export type OnItemClickArgs = {
dest?: Dest;
pageIndex: number;
pageNumber: number;
};
export type OnLoadProgressArgs = {
loaded: number;
total: number;
};
export type RegisterPage = (pageIndex: number, ref: HTMLDivElement) => void;
export type RenderMode = 'canvas' | 'custom' | 'none';
export type ScrollPageIntoViewArgs = {
dest?: ResolvedDest;
pageIndex?: number;
pageNumber: number;
};
type BinaryData = TypedArray | ArrayBuffer | number[] | string;
export type Source =
| { data: BinaryData | undefined }
| { range: PDFDataRangeTransport }
| { url: string };
export type UnregisterPage = (pageIndex: number) => void;
/* Complex types */
export type CustomRenderer = React.FunctionComponent | React.ComponentClass;
export type CustomTextRenderer = (
props: { pageIndex: number; pageNumber: number; itemIndex: number } & TextItem,
) => string;
export type DocumentCallback = PDFDocumentProxy;
export type File = string | ArrayBuffer | Blob | Source | null;
export type PageCallback = PDFPageProxy & {
width: number;
height: number;
originalWidth: number;
originalHeight: number;
};
export type NodeOrRenderer = React.ReactNode | (() => React.ReactNode);
export type OnDocumentLoadError = OnError;
export type OnDocumentLoadProgress = (args: OnLoadProgressArgs) => void;
export type OnDocumentLoadSuccess = (document: DocumentCallback) => void;
export type OnGetAnnotationsError = OnError;
export type OnGetAnnotationsSuccess = (annotations: Annotations) => void;
export type OnGetStructTreeError = OnError;
export type OnGetStructTreeSuccess = (tree: StructTreeNode) => void;
export type OnGetTextError = OnError;
export type OnGetTextSuccess = (textContent: TextContent) => void;
export type OnPageLoadError = OnError;
export type OnPageLoadSuccess = (page: PageCallback) => void;
export type OnPasswordCallback = (password: string | null) => void;
export type OnRenderAnnotationLayerError = (error: unknown) => void;
export type OnRenderAnnotationLayerSuccess = () => void;
export type OnRenderError = OnError;
export type OnRenderSuccess = (page: PageCallback) => void;
export type OnRenderTextLayerError = OnError;
export type OnRenderTextLayerSuccess = () => void;
export type PasswordResponse = (typeof PasswordResponses)[keyof typeof PasswordResponses];
export type Options = NullableObject<Omit<DocumentInitParameters, KeyOfUnion<Source>>>;
/* Context types */
export type DocumentContextType = {
imageResourcesPath?: ImageResourcesPath;
linkService: LinkService;
onItemClick?: (args: OnItemClickArgs) => void;
pdf?: PDFDocumentProxy | false;
registerPage: RegisterPage;
renderMode?: RenderMode;
rotate?: number | null;
unregisterPage: UnregisterPage;
} | null;
export type PageContextType = {
_className?: string;
canvasBackground?: string;
customTextRenderer?: CustomTextRenderer;
devicePixelRatio?: number;
onGetAnnotationsError?: OnGetAnnotationsError;
onGetAnnotationsSuccess?: OnGetAnnotationsSuccess;
onGetStructTreeError?: OnGetStructTreeError;
onGetStructTreeSuccess?: OnGetStructTreeSuccess;
onGetTextError?: OnGetTextError;
onGetTextSuccess?: OnGetTextSuccess;
onRenderAnnotationLayerError?: OnRenderAnnotationLayerError;
onRenderAnnotationLayerSuccess?: OnRenderAnnotationLayerSuccess;
onRenderError?: OnRenderError;
onRenderSuccess?: OnRenderSuccess;
onRenderTextLayerError?: OnRenderTextLayerError;
onRenderTextLayerSuccess?: OnRenderTextLayerSuccess;
page: PDFPageProxy | false | undefined;
pageIndex: number;
pageNumber: number;
renderForms: boolean;
renderTextLayer: boolean;
rotate: number;
scale: number;
} | null;
export type OutlineContextType = {
onItemClick?: (args: OnItemClickArgs) => void;
} | null;
export type StructTreeNodeWithExtraAttributes = StructTreeNode & {
alt?: string;
lang?: string;
};

49
node_modules/react-pdf/src/shared/utils.spec.ts generated vendored Normal file
View File

@@ -0,0 +1,49 @@
import { describe, expect, it } from 'vitest';
import { isDataURI, dataURItoByteString } from './utils.js';
describe('isDataURI()', () => {
it.each`
input | expectedResult
${'potato'} | ${false}
${'data:,Hello%2C%20world%21'} | ${true}
${'data:text/plain;base64,SGVsbG8sIHdvcmxkIQ=='} | ${true}
`('returns $expectedResult given $input', ({ input, expectedResult }) => {
const result = isDataURI(input);
expect(result).toBe(expectedResult);
});
});
describe('dataURItoByteString()', () => {
it('throws given invalid data URI', () => {
expect(() => dataURItoByteString('potato')).toThrow();
});
it('returns a byte string given plain text data URI', () => {
const result = dataURItoByteString('data:,Hello%2C%20world%21');
expect(result).toBe('Hello, world!');
});
it('returns a byte string given base64 data URI', () => {
const result = dataURItoByteString('data:text/plain;base64,SGVsbG8sIHdvcmxkIQ==');
expect(result).toBe('Hello, world!');
});
it('returns a byte string given base64 PDF data URI', () => {
const result = dataURItoByteString(
'data:application/pdf;base64,JVBERi0xLg10cmFpbGVyPDwvUm9vdDw8L1BhZ2VzPDwvS2lkc1s8PC9NZWRpYUJveFswIDAgMyAzXT4+XT4+Pj4+Pg==',
);
expect(result).toBe('%PDF-1.\rtrailer<</Root<</Pages<</Kids[<</MediaBox[0 0 3 3]>>]>>>>>>');
});
it('returns a byte string given base64 PDF data URI with filename', () => {
const result = dataURItoByteString(
'data:application/pdf;filename=generated.pdf;base64,JVBERi0xLg10cmFpbGVyPDwvUm9vdDw8L1BhZ2VzPDwvS2lkc1s8PC9NZWRpYUJveFswIDAgMyAzXT4+XT4+Pj4+Pg==',
);
expect(result).toBe('%PDF-1.\rtrailer<</Root<</Pages<</Kids[<</MediaBox[0 0 3 3]>>]>>>>>>');
});
});

180
node_modules/react-pdf/src/shared/utils.ts generated vendored Normal file
View File

@@ -0,0 +1,180 @@
import invariant from 'tiny-invariant';
import warning from 'warning';
import type { PDFPageProxy } from 'pdfjs-dist';
import type { PageCallback } from './types.js';
/**
* Checks if we're running in a browser environment.
*/
export const isBrowser: boolean = typeof window !== 'undefined';
/**
* Checks whether we're running from a local file system.
*/
export const isLocalFileSystem: boolean = isBrowser && window.location.protocol === 'file:';
/**
* Checks whether a variable is defined.
*
* @param {*} variable Variable to check
*/
export function isDefined<T>(variable: T | undefined): variable is T {
return typeof variable !== 'undefined';
}
/**
* Checks whether a variable is defined and not null.
*
* @param {*} variable Variable to check
*/
export function isProvided<T>(variable: T | null | undefined): variable is T {
return isDefined(variable) && variable !== null;
}
/**
* Checks whether a variable provided is a string.
*
* @param {*} variable Variable to check
*/
export function isString(variable: unknown): variable is string {
return typeof variable === 'string';
}
/**
* Checks whether a variable provided is an ArrayBuffer.
*
* @param {*} variable Variable to check
*/
export function isArrayBuffer(variable: unknown): variable is ArrayBuffer {
return variable instanceof ArrayBuffer;
}
/**
* Checks whether a variable provided is a Blob.
*
* @param {*} variable Variable to check
*/
export function isBlob(variable: unknown): variable is Blob {
invariant(isBrowser, 'isBlob can only be used in a browser environment');
return variable instanceof Blob;
}
/**
* Checks whether a variable provided is a data URI.
*
* @param {*} variable String to check
*/
export function isDataURI(variable: unknown): variable is `data:${string}` {
return isString(variable) && /^data:/.test(variable);
}
export function dataURItoByteString(dataURI: unknown): string {
invariant(isDataURI(dataURI), 'Invalid data URI.');
const [headersString = '', dataString = ''] = dataURI.split(',');
const headers = headersString.split(';');
if (headers.indexOf('base64') !== -1) {
return atob(dataString);
}
return unescape(dataString);
}
export function getDevicePixelRatio(): number {
return (isBrowser && window.devicePixelRatio) || 1;
}
const allowFileAccessFromFilesTip =
'On Chromium based browsers, you can use --allow-file-access-from-files flag for debugging purposes.';
export function displayCORSWarning(): void {
warning(
!isLocalFileSystem,
`Loading PDF as base64 strings/URLs may not work on protocols other than HTTP/HTTPS. ${allowFileAccessFromFilesTip}`,
);
}
export function displayWorkerWarning(): void {
warning(
!isLocalFileSystem,
`Loading PDF.js worker may not work on protocols other than HTTP/HTTPS. ${allowFileAccessFromFilesTip}`,
);
}
export function cancelRunningTask(runningTask?: { cancel?: () => void } | null): void {
if (runningTask?.cancel) runningTask.cancel();
}
export function makePageCallback(page: PDFPageProxy, scale: number): PageCallback {
Object.defineProperty(page, 'width', {
get() {
return this.view[2] * scale;
},
configurable: true,
});
Object.defineProperty(page, 'height', {
get() {
return this.view[3] * scale;
},
configurable: true,
});
Object.defineProperty(page, 'originalWidth', {
get() {
return this.view[2];
},
configurable: true,
});
Object.defineProperty(page, 'originalHeight', {
get() {
return this.view[3];
},
configurable: true,
});
return page as PageCallback;
}
export function isCancelException(error: Error): boolean {
return error.name === 'RenderingCancelledException';
}
export function loadFromFile(file: Blob): Promise<ArrayBuffer> {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => {
if (!reader.result) {
return reject(new Error('Error while reading a file.'));
}
resolve(reader.result as ArrayBuffer);
};
reader.onerror = (event) => {
if (!event.target) {
return reject(new Error('Error while reading a file.'));
}
const { error } = event.target;
if (!error) {
return reject(new Error('Error while reading a file.'));
}
switch (error.code) {
case error.NOT_FOUND_ERR:
return reject(new Error('Error while reading a file: File not found.'));
case error.SECURITY_ERR:
return reject(new Error('Error while reading a file: Security error.'));
case error.ABORT_ERR:
return reject(new Error('Error while reading a file: Aborted.'));
default:
return reject(new Error('Error while reading a file.'));
}
};
reader.readAsArrayBuffer(file);
});
}