@react-pdf-viewer/core
Version:
A React component to view a PDF document
830 lines (753 loc) • 24 kB
TypeScript
/**
* A React component to view a PDF document
*
* @see https://react-pdf-viewer.dev
* @license https://react-pdf-viewer.dev/license
* @copyright 2019-2023 Nguyen Huu Phuoc <me@phuoc.ng>
*/
import * as React from 'react';
// Types
export interface Offset {
left: number;
top: number;
}
export interface OpenFile {
data: PdfJs.FileData;
name: string;
}
export interface Rect {
height: number;
width: number;
}
export interface PageLayout {
buildPageStyles?: ({
numPages,
pageIndex,
scrollMode,
viewMode,
}: {
numPages: number;
pageIndex: number;
scrollMode: ScrollMode;
viewMode: ViewMode;
}) => React.CSSProperties;
transformSize?: ({ numPages, pageIndex, size }: { numPages: number; pageIndex: number; size: Rect }) => Rect;
}
export interface PageSize {
pageHeight: number;
pageWidth: number;
rotation: number;
}
export interface Plugin {
install?(pluginFunctions: PluginFunctions): void;
renderPageLayer?(props: PluginRenderPageLayer): React.ReactElement;
renderViewer?(props: RenderViewer): Slot;
uninstall?(pluginFunctions: PluginFunctions): void;
onAnnotationLayerRender?(props: PluginOnAnnotationLayerRender): void;
onCanvasLayerRender?(props: PluginOnCanvasLayerRender): void;
onDocumentLoad?(props: PluginOnDocumentLoad): void;
onTextLayerRender?(props: PluginOnTextLayerRender): void;
onViewerStateChange?(viewerState: ViewerState): ViewerState;
}
export type DestinationOffsetFromViewport = (viewportWidth: number, viewportHeight: number) => number;
export interface Destination {
bottomOffset: number | DestinationOffsetFromViewport;
label?: string;
leftOffset: number | DestinationOffsetFromViewport;
pageIndex: number;
scaleTo?: number | SpecialZoomLevel;
}
export interface PluginFunctions {
enterFullScreenMode(target: HTMLElement): void;
exitFullScreenMode(): void;
getPagesContainer(): HTMLElement;
getViewerState(): ViewerState;
jumpToDestination(destination: Destination): Promise<void>;
jumpToNextDestination(): Promise<void>;
jumpToPreviousDestination(): Promise<void>;
jumpToNextPage(): Promise<void>;
jumpToPreviousPage(): Promise<void>;
jumpToPage(pageIndex: number): Promise<void>;
openFile(file: File): void;
rotate(direction: RotateDirection): void;
rotatePage(pageIndex: number, direction: RotateDirection): void;
setViewerState(viewerState: ViewerState): void;
switchScrollMode(scrollMode: ScrollMode): void;
switchViewMode(viewMode: ViewMode): void;
zoom(scale: number | SpecialZoomLevel): void;
}
export interface PluginOnDocumentLoad {
doc: PdfJs.PdfDocument;
file: OpenFile;
}
export interface PluginOnTextLayerRender {
ele: HTMLElement;
pageIndex: number;
scale: number;
status: LayerRenderStatus;
}
export interface PluginOnAnnotationLayerRender {
annotations: PdfJs.Annotation[];
container: HTMLElement;
pageIndex: number;
scale: number;
rotation: number;
}
// Invoked when the canvas layer is rendered
export interface PluginOnCanvasLayerRender {
ele: HTMLCanvasElement;
pageIndex: number;
rotation: number;
scale: number;
status: LayerRenderStatus;
}
export interface PluginRenderPageLayer {
canvasLayerRef: React.MutableRefObject<HTMLCanvasElement>;
// Is the canvas layer rendered completely?
canvasLayerRendered: boolean;
doc: PdfJs.PdfDocument;
height: number;
pageIndex: number;
rotation: number;
scale: number;
textLayerRef: React.MutableRefObject<HTMLDivElement>;
// Is the text layer rendered completely?
textLayerRendered: boolean;
width: number;
}
export interface RenderPageProps {
annotationLayer: Slot;
canvasLayer: Slot;
// Is the canvas layer rendered completely?
canvasLayerRendered: boolean;
doc: PdfJs.PdfDocument;
height: number;
pageIndex: number;
rotation: number;
scale: number;
svgLayer: Slot;
textLayer: Slot;
// Is the text layer rendered completely?
textLayerRendered: boolean;
width: number;
// Mark as the page rendered completely
markRendered(pageIndex: number): void;
onRotatePage(direction: RotateDirection): void;
}
export type RenderPage = (props: RenderPageProps) => React.ReactElement;
export interface RenderViewer {
containerRef: React.RefObject<HTMLDivElement>;
doc: PdfJs.PdfDocument;
pagesContainerRef: React.RefObject<HTMLDivElement>;
// The rotation for each page
pagesRotation: Map<number, number>;
pageSizes: PageSize[];
rotation: number;
slot: Slot;
themeContext: ThemeContextProps;
openFile(file: File): void;
// Jump to given page
// `page` is zero-index based
jumpToPage(page: number): void;
rotate(direction: RotateDirection): void;
rotatePage(pageIndex: number, direction: RotateDirection): void;
switchScrollMode(scrollMode: ScrollMode): void;
switchViewMode(viewMode: ViewMode): void;
zoom(level: number | SpecialZoomLevel): void;
}
export interface Slot {
attrs?: SlotAttr;
children?: React.ReactNode;
subSlot?: Slot;
}
export interface SlotAttr extends React.HTMLAttributes<HTMLDivElement> {
'data-testid'?: string;
ref?: React.MutableRefObject<HTMLDivElement | null>;
}
export type Toggle = (status?: ToggleStatus) => void;
export interface ViewerState {
// The current opened file. It can be changed from outside, such as user drags and drops an external file
// or user opens a file from toolbar
file: OpenFile;
fullScreenMode: FullScreenMode;
// The current page index
pageIndex: number;
// Size of page
pageHeight: number;
pageWidth: number;
// The rotation for each page
pagesRotation: Map<number, number>;
// The last page which is rotated
rotatedPage?: number;
rotation: number;
// The current zoom level
scale: number;
// The current scroll mode
scrollMode: ScrollMode;
// The current view mode
viewMode: ViewMode;
}
export interface VisibilityChanged {
isVisible: boolean;
ratio: number;
}
// Structs
export enum AnnotationType {
Text = 1,
Link = 2,
FreeText = 3,
Line = 4,
Square = 5,
Circle = 6,
Polygon = 7,
Polyline = 8,
Highlight = 9,
Underline = 10,
Squiggly = 11,
StrikeOut = 12,
Stamp = 13,
Caret = 14,
Ink = 15,
Popup = 16,
FileAttachment = 17,
}
export enum FullScreenMode {
Normal = 'Normal',
// Start entering the full screen mode
Entering = 'Entering',
Entered = 'Entered',
Exitting = 'Exitting',
Exited = 'Exited',
}
export enum LayerRenderStatus {
PreRender,
DidRender,
}
export enum PageMode {
Attachments = 'UseAttachments',
Bookmarks = 'UseOutlines',
ContentGroup = 'UseOC',
Default = 'UserNone',
FullScreen = 'FullScreen',
Thumbnails = 'UseThumbs',
}
export enum Position {
TopLeft = 'TOP_LEFT',
TopCenter = 'TOP_CENTER',
TopRight = 'TOP_RIGHT',
RightTop = 'RIGHT_TOP',
RightCenter = 'RIGHT_CENTER',
RightBottom = 'RIGHT_BOTTOM',
BottomLeft = 'BOTTOM_LEFT',
BottomCenter = 'BOTTOM_CENTER',
BottomRight = 'BOTTOM_RIGHT',
LeftTop = 'LEFT_TOP',
LeftCenter = 'LEFT_CENTER',
LeftBottom = 'LEFT_BOTTOM',
}
export enum RotateDirection {
Backward = 'Backward',
Forward = 'Forward',
}
export enum ScrollMode {
Page = 'Page',
Horizontal = 'Horizontal',
Vertical = 'Vertical',
Wrapped = 'Wrapped',
}
export enum SpecialZoomLevel {
ActualSize = 'ActualSize',
PageFit = 'PageFit',
PageWidth = 'PageWidth',
}
export enum ViewMode {
DualPage = 'DualPage',
DualPageWithCover = 'DualPageWithCover',
SinglePage = 'SinglePage',
}
export enum ToggleStatus {
Close = 'Close',
Open = 'Open',
Toggle = 'Toggle',
}
// Components
export type RenderContent = (toggle: Toggle) => React.ReactNode;
export type RenderTarget = (toggle: Toggle, opened: boolean) => React.ReactNode;
export interface ButtonProps {
children?: React.ReactNode;
testId?: string;
onClick(): void;
}
export class Button extends React.Component<ButtonProps> {}
export interface LazyRenderProps {
attrs?: React.HTMLAttributes<HTMLDivElement>;
children?: React.ReactNode;
testId?: string;
}
export class LazyRender extends React.Component<LazyRenderProps> {}
export class Menu extends React.Component<{
children?: React.ReactNode;
}> {}
export class MenuDivider extends React.Component {}
export interface MenuItemProps {
checked?: boolean;
children?: React.ReactNode;
icon?: React.ReactElement;
isDisabled?: boolean;
testId?: string;
onClick(): void;
}
export class MenuItem extends React.Component<MenuItemProps> {}
export interface MinimalButtonProps {
ariaLabel?: string;
ariaKeyShortcuts?: string;
children?: React.ReactNode;
isDisabled?: boolean;
isSelected?: boolean;
testId?: string;
onClick(): void;
}
export class MinimalButton extends React.Component<MinimalButtonProps> {}
export interface PrimaryButtonProps {
children?: React.ReactNode;
testId?: string;
onClick(): void;
}
export class PrimaryButton extends React.Component<PrimaryButtonProps> {}
export interface ProgressBarProps {
progress: number;
}
export class ProgressBar extends React.Component<ProgressBarProps> {}
export class Separator extends React.Component {}
export interface SpinnerProps {
size?: string;
testId?: string;
}
export class Spinner extends React.Component<SpinnerProps> {}
export interface SplitterSize {
firstHalfPercentage: number;
firstHalfSize: number;
secondHalfPercentage: number;
secondHalfSize: number;
}
export interface SplitterProps {
constrain?(size: SplitterSize): boolean;
}
export class Splitter extends React.Component<SplitterProps> {}
export interface TextBoxProps {
ariaLabel?: string;
autoFocus?: boolean;
placeholder?: string;
testId?: string;
type?: 'text' | 'password';
value?: string;
onChange: (value: string) => void;
onKeyDown?: (e: React.KeyboardEvent) => void;
}
export class TextBox extends React.Component<TextBoxProps> {}
export interface IconProps {
children?: React.ReactNode;
// If this option is `true`, the icon will not be flipped
ignoreDirection?: boolean;
size?: number;
}
export class Icon extends React.Component<IconProps> {}
export interface ModalProps {
ariaControlsSuffix?: string;
closeOnClickOutside: boolean;
closeOnEscape: boolean;
content: RenderContent;
isOpened?: boolean;
target?: RenderTarget;
}
export class Modal extends React.Component<ModalProps> {}
export interface PopoverProps {
ariaControlsSuffix?: string;
ariaHasPopup?: 'dialog' | 'menu';
closeOnClickOutside: boolean;
closeOnEscape: boolean;
content: RenderContent;
lockScroll?: boolean;
offset: Offset;
position: Position;
target?: RenderTarget;
}
export class Popover extends React.Component<PopoverProps> {}
export type RenderTooltipContent = () => React.ReactNode;
export interface TooltipProps {
ariaControlsSuffix?: string;
content: RenderTooltipContent;
offset: Offset;
position: Position;
target: React.ReactElement;
}
export class Tooltip extends React.Component<TooltipProps> {}
// Store
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type StoreState = Record<string, any>;
export type StoreKey<T extends StoreState> = string & keyof T;
export type StoreHandler<T> = (params: T) => void;
export interface Store<T extends StoreState> {
subscribe<K extends StoreKey<T>>(eventName: K, handler: StoreHandler<NonNullable<T[K]>>): void;
unsubscribe<K extends StoreKey<T>>(eventName: K, handler: StoreHandler<NonNullable<T[K]>>): void;
update<K extends StoreKey<T>>(eventName: K, params: T[K]): void;
updateCurrentValue<K extends StoreKey<T>>(eventName: K, updater: (currentValue: T[K]) => T[K]): void;
get<K extends StoreKey<T>>(eventName: K): T[K] | undefined;
}
export function createStore<T extends StoreState>(initialState?: T): Store<T>;
// Contexts
export interface LocalizationMap {
[key: string]: string | LocalizationMap;
}
export interface LocalizationContextProps {
l10n: LocalizationMap;
setL10n(l10n: LocalizationMap): void;
}
export const LocalizationContext: React.Context<LocalizationContextProps>;
export enum TextDirection {
RightToLeft = 'RTL',
LeftToRight = 'LTR',
}
export interface ThemeContextProps {
currentTheme: string;
direction?: TextDirection;
setCurrentTheme: (theme: string) => void;
}
export const ThemeContext: React.Context<ThemeContextProps>;
// Viewer
// The character maps that can be downloaded from
// https://github.com/mozilla/pdfjs-dist/tree/master/cmaps
export interface CharacterMap {
isCompressed: boolean;
url: string;
}
// Represents the error in case the document can't be loaded
export interface LoadError {
message?: string;
// Some possible values for `name` are
// - AbortException
// - FormatError
// - InvalidPDFException
// - MissingPDFException
// - PasswordException
// - UnexpectedResponseException
// - UnknownErrorException
name?: string;
}
export type RenderError = (error: LoadError) => React.ReactElement;
// Invoked when the document asks for password
export interface DocumentAskPasswordEvent {
verifyPassword: (password: string) => void;
}
// Customize the view of protected document
export enum PasswordStatus {
RequiredPassword = 'RequiredPassword',
WrongPassword = 'WrongPassword',
}
export interface RenderProtectedViewProps {
passwordStatus: PasswordStatus;
verifyPassword: (password: string) => void;
}
export type RenderProtectedView = (renderProps: RenderProtectedViewProps) => React.ReactElement;
// Invoked when the document is loaded successfully
export interface DocumentLoadEvent {
doc: PdfJs.PdfDocument;
file: OpenFile;
}
// Invoked when users change the current page
export interface PageChangeEvent {
// zero-based page index
currentPage: number;
doc: PdfJs.PdfDocument;
}
// Invoked when users zoom the document
export interface ZoomEvent {
doc: PdfJs.PdfDocument;
scale: number;
}
// Invoked when users rotate the document
export interface RotateEvent {
direction: RotateDirection;
doc: PdfJs.PdfDocument;
rotation: number;
}
// Invoked when users rotate a page of the document
export interface RotatePageEvent {
direction: RotateDirection;
doc: PdfJs.PdfDocument;
pageIndex: number;
rotation: number;
}
export interface ThemeProps {
direction?: TextDirection;
theme?: string;
}
export interface VisiblePagesRange {
endPage: number;
numPages: number;
startPage: number;
}
export type SetRenderRange = (visiblePagesRange: VisiblePagesRange) => { endPage: number; startPage: number };
export interface ViewerProps {
characterMap?: CharacterMap;
// The default zoom level
// If it's not set, the initial zoom level will be calculated based on the dimesion of page and the container width
// So that, the document will fit best within the container
defaultScale?: number | SpecialZoomLevel;
// Enable smooth scroll
enableSmoothScroll?: boolean;
fileUrl: string | Uint8Array;
// Additional authentication headers
httpHeaders?: Record<string, string | string[]>;
// The page (zero-index based) that will be displayed initially
initialPage?: number;
// The initial rotation, must be divisible by 90
initialRotation?: number;
pageLayout?: PageLayout;
// Plugins
plugins?: Plugin[];
localization?: LocalizationMap;
renderError?: RenderError;
renderLoader?(percentages: number): React.ReactElement;
renderPage?: RenderPage;
renderProtectedView?: RenderProtectedView;
scrollMode?: ScrollMode;
setRenderRange?: SetRenderRange;
// Theme
theme?: string | ThemeProps;
transformGetDocumentParams?(options: PdfJs.GetDocumentParams): PdfJs.GetDocumentParams;
viewMode?: ViewMode;
// Indicate the cross-site requests should be made with credentials such as cookie and authorization headers.
// The default value is `false`
withCredentials?: boolean;
onDocumentAskPassword?(e: DocumentAskPasswordEvent): void;
onDocumentLoad?(e: DocumentLoadEvent): void;
onPageChange?(e: PageChangeEvent): void;
onRotate?(e: RotateEvent): void;
onRotatePage?(e: RotatePageEvent): void;
// Invoked after switching to `theme`
onSwitchTheme?(theme: string): void;
onZoom?(e: ZoomEvent): void;
}
export class Viewer extends React.Component<ViewerProps> {}
export interface WorkerProps {
children?: React.ReactNode;
workerUrl: string;
}
export class Worker extends React.Component<WorkerProps> {}
// Hooks
export function useDebounceCallback<T extends unknown[]>(callback: (...args: T) => void, wait: number): void;
export interface UseIntersectionObserverProps {
once?: boolean;
threshold?: number | number[];
onVisibilityChanged(params: VisibilityChanged): void;
}
export function useIntersectionObserver(props: UseIntersectionObserverProps): React.MutableRefObject<HTMLDivElement>;
export function useIsomorphicLayoutEffect(effect: React.EffectCallback, deps?: React.DependencyList): void;
export function useIsMounted(): React.MutableRefObject<boolean>;
export function usePrevious<T>(value: T): T;
export interface UseRenderQueue {
getHighestPriorityPage: () => number;
isInRange: (pageIndex: number) => boolean;
markNotRendered: () => void;
markRendered: (pageIndex: number) => void;
markRendering: (pageIndex: number) => void;
setOutOfRange: (pageIndex: number) => void;
setRange: (startIndex: number, endIndex: number) => void;
setVisibility: (pageIndex: number, visibility: number) => void;
}
export function useRenderQueue({ doc }: { doc: PdfJs.PdfDocument }): UseRenderQueue;
// Utils
export function chunk<T>(arr: T[], size: number): T[][];
export function classNames(classes: { [clazz: string]: boolean }): string;
export function getPage(doc: PdfJs.PdfDocument, pageIndex: number): Promise<PdfJs.Page>;
export interface JumpToDestination {
bottomOffset: number;
leftOffset: number;
pageIndex: number;
scaleTo: number | SpecialZoomLevel;
}
export function getDestination(doc: PdfJs.PdfDocument, dest: PdfJs.OutlineDestinationType): Promise<JumpToDestination>;
export function isFullScreenEnabled(): boolean;
export function isMac(): boolean;
// Vendors
// pdfjs namespace
export declare namespace PdfJs {
type FileData = string | Uint8Array;
// Worker
interface PDFWorkerConstructorParams {
name: string;
}
class PDFWorker {
destroyed: boolean;
constructor(params: PDFWorkerConstructorParams);
destroy(): void;
}
// Document
interface PdfDocument {
numPages: number;
getAttachments(): Promise<{ [filename: string]: Attachment }>;
getData(): Promise<Uint8Array>;
getDestination(dest: string): Promise<OutlineDestination>;
getDownloadInfo(): Promise<{ length: number }>;
getMetadata(): Promise<MetaData>;
getOutline(): Promise<Outline[]>;
getPage(pageIndex: number): Promise<Page>;
getPageIndex(ref: OutlineRef): Promise<number>;
getPageLabels(): Promise<string[] | null>;
getPageMode(): Promise<PageMode>;
}
interface GetDocumentParams {
data?: FileData;
cMapUrl?: string;
cMapPacked?: boolean;
httpHeaders?: Record<string, string | string[]>;
url?: string;
withCredentials?: boolean;
worker?: PDFWorker;
}
// Viewport
interface ViewPortParams {
rotation?: number;
scale: number;
}
interface ViewPortCloneParams {
dontFlip: boolean;
}
interface ViewPort {
height: number;
rotation: number;
transform: number[];
width: number;
clone(params: ViewPortCloneParams): ViewPort;
convertToViewportPoint(x: number, y: number): [number, number];
}
interface PageTextContent {
items: PageTextItem[];
}
interface PageTextItem {
str: string;
}
// Render task
interface PageRenderTask {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
promise: Promise<any>;
cancel(): void;
}
// Render page
interface PageRenderParams {
canvasContext: CanvasRenderingContext2D;
// Should be 'print' when printing
intent?: string;
transform?: number[];
viewport: ViewPort;
}
interface Page {
getTextContent(): Promise<PageTextContent>;
getViewport(params: ViewPortParams): ViewPort;
render(params: PageRenderParams): PageRenderTask;
}
// Attachment
interface Attachment {
content: Uint8Array;
filename: string;
}
// Metadata
interface MetaData {
contentDispositionFilename?: string;
info: MetaDataInfo;
}
interface MetaDataInfo {
Author: string;
CreationDate: string;
Creator: string;
Keywords: string;
ModDate: string;
PDFFormatVersion: string;
Producer: string;
Subject: string;
Title: string;
}
// Outline
type OutlineDestinationType = string | OutlineDestination;
interface Outline {
bold?: boolean;
color?: number[];
count?: undefined | number;
dest?: OutlineDestinationType;
italic?: boolean;
items: Outline[];
newWindow?: boolean;
title: string;
unsafeUrl?: string;
url?: string;
}
type OutlineDestination = [
// The first item is used to indicate the destination page
OutlineRef | number,
OutlineDestinationName,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
...any[]
];
interface OutlineDestinationName {
name: string; // Can be 'WYZ', 'Fit', ...
}
interface OutlineRef {
gen: number;
num: number;
}
interface AnnotationPoint {
x: number;
y: number;
}
interface Annotation {
annotationType: number;
color?: Uint8ClampedArray;
dest: OutlineDestinationType;
hasAppearance: boolean;
id: string;
rect: number[];
subtype: string;
// Border style
borderStyle: {
dashArray: number[];
horizontalCornerRadius: number;
style: number;
verticalCornerRadius: number;
width: number;
};
// For annotation that has a popup
hasPopup?: boolean;
contents?: string;
contentsObj?: {
dir: string;
str: string;
};
modificationDate?: string;
quadPoints?: AnnotationPoint[][];
title?: string;
titleObj?: {
dir: string;
str: string;
};
// Parent annotation
parentId?: string;
parentType?: string;
// File attachment annotation
file?: Attachment;
// Ink annotation
inkLists?: AnnotationPoint[][];
// Line annotation
lineCoordinates: number[];
// Link annotation
// `action` can be `FirstPage`, `PrevPage`, `NextPage`, `LastPage`, `GoBack`, `GoForward`
action?: string;
unsafeUrl?: string;
url?: string;
newWindow?: boolean;
// Polyline annotation
vertices?: AnnotationPoint[];
// Text annotation
name?: string;
}
}