feat: llama pdf viewer

This commit is contained in:
Thuc Pham
2024-05-10 15:03:15 +07:00
parent 9f95019bc0
commit a440eefed5
184 changed files with 3897 additions and 46486 deletions
@@ -1,2 +1,3 @@
dist
node_modules
sample
+10
View File
@@ -0,0 +1,10 @@
{
"extends": "wojtekmaj/react",
"rules": {
"@typescript-eslint/consistent-type-definitions": "off",
"@typescript-eslint/no-use-before-define": "off",
"react-hooks/exhaustive-deps": "off",
"no-console": "off",
"@typescript-eslint/ban-ts-comment": "off"
}
}
-2
View File
@@ -1,2 +0,0 @@
# Auto detect text files and perform LF normalization
* text=auto eol=lf
-2
View File
@@ -1,2 +0,0 @@
github: wojtekmaj
open_collective: react-pdf-wojtekmaj
-65
View File
@@ -1,65 +0,0 @@
name: 🐛 Bug report
description: Something does not work the way we promised
labels:
- bug
body:
- type: checkboxes
attributes:
label: Before you start - checklist
options:
- label: I followed instructions in documentation written for my React-PDF version
required: true
- label: I have checked if this bug is not already reported
required: true
- label: I have checked if an issue is not listed in [Known issues](https://github.com/wojtekmaj/react-pdf/wiki/Known-issues)
required: true
- label: If I have a problem with PDF rendering, I checked if my PDF renders properly in [PDF.js demo](https://mozilla.github.io/pdf.js/web/viewer.html)
- type: textarea
attributes:
label: Description
description: Short description of the bug you encountered.
validations:
required: true
- type: textarea
attributes:
label: Steps to reproduce
description: |
Steps to reproduce the behavior.
Example:
1. Go to '…'
2. Click on '…'
3. Scroll down to '…'
4. See error
validations:
required: true
- type: textarea
attributes:
label: Expected behavior
description: What is the expected behavior?
validations:
required: true
- type: textarea
attributes:
label: Actual behavior
description: What is the actual behavior?
validations:
required: true
- type: textarea
attributes:
label: Additional information
description: If applicable, add screenshots (preferably with browser console open) and files you have an issue with to help explain your problem.
- type: textarea
attributes:
label: Environment
description: |
Example:
- **Browser (if applicable)**: Chrome 96, Firefox 94
- **React-PDF version**: 5.5.0
- **React version**: 17.0.0
- **Webpack version (if applicable)**: 5.50.0
value: |
- **Browser (if applicable)**:
- **React-PDF version**:
- **React version**:
- **Webpack version (if applicable)**:
@@ -1,38 +0,0 @@
name: 🚀 Feature request
description: I have a great idea for this project
labels:
- enhancement
body:
- type: checkboxes
attributes:
label: Before you start - checklist
options:
- label: I understand that React-PDF does not aim to be a fully-fledged PDF viewer and is only a tool to make one
required: true
- label: I have checked if this feature request is not already reported
required: true
- type: textarea
attributes:
label: Description
description: |
Describe what the problem is.
Example: _I'd like to add a feature that […]_
validations:
required: true
- type: textarea
attributes:
label: Proposed solution
description: |
Describe the solution you'd like.
Example:
- Add a `foo` flag that, when toggled, enables the feature.
- type: textarea
attributes:
label: Alternatives
description: Describe alternative solutions or features you've considered, if any.
- type: textarea
attributes:
label: Additional information
description: If applicable, add screenshots (preferably with browser console open) and files you have an issue with to help explain your problem.
-5
View File
@@ -1,5 +0,0 @@
blank_issues_enabled: false
contact_links:
- name: 🤔 Support question
url: https://stackoverflow.com/questions/tagged/react-pdf
about: This is a bug tracker, not a support forum. For usage questions, please use Discussions (see menu) or Stack Overflow ("Open" button) where there is a lot more people ready to help you out. Thanks!
-43
View File
@@ -122,46 +122,3 @@ jobs:
- name: Run formatting
run: yarn format
unit:
name: Unit tests (React ${{ matrix.react }})
runs-on: ubuntu-latest
strategy:
matrix:
react: [18, 19]
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Cache Yarn cache
uses: actions/cache@v4
env:
cache-name: yarn-cache
with:
path: ~/.yarn/berry/cache
key: ${{ runner.os }}-${{ env.cache-name }}-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
${{ runner.os }}-${{ env.cache-name }}
- name: Use Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Enable Corepack
run: corepack enable
- name: Install dependencies
run: yarn --immutable
- name: Override React version
if: ${{ matrix.react == 19 }}
run: |
npm pkg set resolutions.'@types/react'='npm:types-react@beta'
npm pkg set resolutions.'@types/react-dom'='npm:types-react-dom@beta'
yarn config set enableImmutableInstalls false
yarn up react@beta react-dom@beta
- name: Run tests (React ${{ matrix.react }})
run: yarn unit
-25
View File
@@ -1,25 +0,0 @@
name: Close stale issues
on:
schedule:
- cron: '0 0 * * 1' # Every Monday
workflow_dispatch:
jobs:
close-issues:
name: Close stale issues
runs-on: ubuntu-latest
steps:
- name: Close stale issues
uses: actions/stale@v8
with:
days-before-issue-stale: 90
days-before-issue-close: 14
stale-issue-label: 'stale'
stale-issue-message: 'This issue is stale because it has been open 90 days with no activity. Remove stale label or comment or this issue will be closed in 14 days.'
close-issue-message: 'This issue was closed because it has been stalled for 14 days with no activity.'
exempt-issue-labels: 'fresh'
remove-issue-stale-when-updated: true
days-before-pr-stale: -1
days-before-pr-close: -1
-2
View File
@@ -47,13 +47,11 @@ jobs:
- name: Publish with latest tag
if: github.event.release.prelease == false
run: npm publish package.tgz --tag latest --provenance
working-directory: packages/react-pdf
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
- name: Publish with next tag
if: github.event.release.prelease == true
run: npm publish package.tgz --tag next --provenance
working-directory: packages/react-pdf
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
+4
View File
@@ -33,3 +33,7 @@ yarn-error.log
**/.env
**/.env.*
!**/.env.example
# sample app
sample/*
*.tgz
-3
View File
@@ -1,3 +0,0 @@
{
"*.{css,html,js,json,jsx,md,ts,tsx,yml}": "yarn format --write"
}
-5
View File
@@ -1,5 +0,0 @@
Niklas Närhinen <niklas@narhinen.net>
Wojciech Maj <kontakt@wojtekmaj.pl>
Wojciech Maj <kontakt@wojtekmaj.pl> <wojciech.maj@motorolasolutions.com>
Wojciech Maj <kontakt@wojtekmaj.pl> <wojciech.maj@ocado.com>
Wojciech Maj <kontakt@wojtekmaj.pl> <w.maj@intive.com>
+1
View File
@@ -1,3 +1,4 @@
.cache
.yarn
yarnrc.yml
dist
-9
View File
@@ -1,9 +0,0 @@
/* eslint-disable */
//prettier-ignore
module.exports = {
name: "@yarnpkg/plugin-nolyfill",
factory: function (require) {
"use strict";var plugin=(()=>{var p=Object.defineProperty;var i=Object.getOwnPropertyDescriptor;var y=Object.getOwnPropertyNames;var n=Object.prototype.hasOwnProperty;var l=(t=>typeof require<"u"?require:typeof Proxy<"u"?new Proxy(t,{get:(r,e)=>(typeof require<"u"?require:r)[e]}):t)(function(t){if(typeof require<"u")return require.apply(this,arguments);throw new Error('Dynamic require of "'+t+'" is not supported')});var c=(t,r)=>{for(var e in r)p(t,e,{get:r[e],enumerable:!0})},g=(t,r,e,s)=>{if(r&&typeof r=="object"||typeof r=="function")for(let a of y(r))!n.call(t,a)&&a!==e&&p(t,a,{get:()=>r[a],enumerable:!(s=i(r,a))||s.enumerable});return t};var f=t=>g(p({},"__esModule",{value:!0}),t);var u={};c(u,{default:()=>m});var o=l("@yarnpkg/core"),d=["abab","array-buffer-byte-length","array-includes","array.from","array.of","array.prototype.at","array.prototype.every","array.prototype.find","array.prototype.findlast","array.prototype.findlastindex","array.prototype.flat","array.prototype.flatmap","array.prototype.flatmap","array.prototype.foreach","array.prototype.reduce","array.prototype.toreversed","array.prototype.tosorted","arraybuffer.prototype.slice","assert","asynciterator.prototype","available-typed-arrays","deep-equal","define-properties","es-aggregate-error","es-iterator-helpers","es-set-tostringtag","es6-object-assign","function-bind","function.prototype.name","get-symbol-description","globalthis","gopd","harmony-reflect","has","has-property-descriptors","has-proto","has-symbols","has-tostringtag","hasown","internal-slot","is-arguments","is-array-buffer","is-date-object","is-generator-function","is-nan","is-regex","is-shared-array-buffer","is-string","is-symbol","is-typed-array","is-weakref","isarray","iterator.prototype","jsonify","object-is","object-keys","object.assign","object.entries","object.fromentries","object.getownpropertydescriptors","object.groupby","object.hasown","object.values","promise.allsettled","promise.any","reflect.getprototypeof","reflect.ownkeys","regexp.prototype.flags","safe-array-concat","safe-regex-test","set-function-length","side-channel","string.prototype.at","string.prototype.codepointat","string.prototype.includes","string.prototype.matchall","string.prototype.padend","string.prototype.padstart","string.prototype.repeat","string.prototype.replaceall","string.prototype.split","string.prototype.startswith","string.prototype.trim","string.prototype.trimend","string.prototype.trimleft","string.prototype.trimright","string.prototype.trimstart","typed-array-buffer","typed-array-byte-length","typed-array-byte-offset","typed-array-length","typedarray","unbox-primitive","util.promisify","which-boxed-primitive","which-typed-array"],h=new Map(d.map(t=>[o.structUtils.makeIdent(null,t).identHash,o.structUtils.makeIdent("nolyfill",t)])),b={hooks:{reduceDependency:async t=>{let r=h.get(t.identHash);if(r){let e=o.structUtils.makeDescriptor(r,"latest"),s=o.structUtils.makeRange({protocol:"npm:",source:null,selector:o.structUtils.stringifyDescriptor(e),params:null});return o.structUtils.makeDescriptor(t,s)}return t}}},m=b;return f(u);})();
return plugin;
}
};
-10
View File
@@ -1,10 +0,0 @@
logFilters:
- code: YN0076
level: discard
nodeLinker: node-modules
plugins:
- checksum: 24a2171abe511205f3184779105076031ca8daefb3fecb744e3d4f3f4010785881127bfc2d73445f7f101bf200d4a5931ec75feb543e50ffbf584da53a2f62cf
path: .yarn/plugins/@yarnpkg/plugin-nolyfill.cjs
spec: 'https://raw.githubusercontent.com/wojtekmaj/yarn-plugin-nolyfill/v0.1.3/bundles/@yarnpkg/plugin-nolyfill.js'
-21
View File
@@ -1,21 +0,0 @@
MIT License
Copyright (c) 20172024 Wojciech Maj
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
-1
View File
@@ -1 +0,0 @@
packages/react-pdf/README.md
-27
View File
@@ -1,27 +0,0 @@
import type { PDFPageProxy } from 'pdfjs-dist';
export default {
cleanup: () => {
return true;
},
commonObjs: {
get: () => {
// Intentionally empty
},
},
getAnnotations: () => new Promise((resolve, reject) => reject(new Error())),
getOperatorList: () => new Promise((resolve, reject) => reject(new Error())),
getStructTree: () => new Promise<void>((resolve, reject) => reject(new Error())),
getTextContent: () => new Promise((resolve, reject) => reject(new Error())),
getViewport: () => ({
width: 600,
height: 800,
rotation: 0,
}),
render: () => ({
promise: new Promise((resolve, reject) => reject(new Error())),
cancel: () => {
// Intentionally empty
},
}),
} as unknown as PDFPageProxy;
-12
View File
@@ -1,12 +0,0 @@
import type { PDFDocumentProxy } from 'pdfjs-dist';
export default {
_pdfInfo: {
fingerprint: 'a62067476e69734bb8eb60122615dfbf',
numPages: 4,
},
getDestination: () => new Promise((resolve, reject) => reject(new Error())),
getOutline: () => new Promise((resolve, reject) => reject(new Error())),
getPage: () => new Promise((resolve, reject) => reject(new Error())),
numPages: 4,
} as unknown as PDFDocumentProxy;
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
-17
View File
@@ -1,17 +0,0 @@
import { RenderingCancelledException } from 'pdfjs-dist';
import type { PDFDocumentProxy } from 'pdfjs-dist';
export default {
_pdfInfo: {
fingerprint: 'a62067476e69734bb8eb60122615dfbf',
numPages: 4,
},
getDestination: () =>
new Promise((resolve, reject) => reject(new RenderingCancelledException('Cancelled'))),
getOutline: () =>
new Promise((resolve, reject) => reject(new RenderingCancelledException('Cancelled'))),
getPage: () =>
new Promise((resolve, reject) => reject(new RenderingCancelledException('Cancelled'))),
numPages: 4,
} as unknown as PDFDocumentProxy;
Binary file not shown.
+73 -18
View File
@@ -1,29 +1,84 @@
{
"name": "react-pdf-monorepo",
"name": "pdf-viewer",
"version": "1.0.0",
"description": "react-pdf monorepo",
"description": "React PDF viewer for LLM applications",
"type": "module",
"workspaces": [
"packages/*",
"test"
"license": "MIT",
"sideEffects": [
"*.css"
],
"main": "./dist/cjs/index.js",
"module": "./dist/esm/index.js",
"source": "./src/index.ts",
"types": "./dist/cjs/index.d.ts",
"exports": {
".": {
"import": "./dist/esm/index.js",
"require": "./dist/cjs/index.js"
}
},
"scripts": {
"build": "yarn workspace react-pdf build",
"dev": "yarn workspace react-pdf watch & yarn workspace test dev",
"format": "yarn workspaces foreach --all run format",
"lint": "yarn workspaces foreach --all run lint",
"postinstall": "husky",
"test": "yarn workspaces foreach --all run test",
"tsc": "yarn workspaces foreach --all run tsc",
"unit": "yarn workspaces foreach --all run unit"
"build": "yarn build-js && yarn copy-styles",
"build-js": "yarn build-js-esm && yarn build-js-cjs && yarn build-js-cjs-package",
"build-js-esm": "tsc --project tsconfig.build.json --outDir dist/esm",
"build-js-cjs": "tsc --project tsconfig.build.json --outDir dist/cjs --module commonjs --moduleResolution node --verbatimModuleSyntax false",
"build-js-cjs-package": "echo '{\n \"type\": \"commonjs\"\n}' > dist/cjs/package.json",
"clean": "rimraf dist",
"copy-styles": "cpy \"src/**/*.css\" dist/esm && cpy \"src/**/*.css\" dist/cjs",
"format": "prettier --check . --cache",
"format:write": "prettier --ignore-unknown --write .",
"lint": "eslint . --ext .js,.jsx,.ts,.tsx",
"prepack": "yarn clean && yarn build",
"test": "yarn lint && yarn tsc && yarn format",
"tsc": "tsc",
"watch": "yarn build-js-esm --watch & yarn build-js-cjs --watch & nodemon --watch src --ext css --exec \"yarn copy-styles\"",
"postinstall": "husky"
},
"dependencies": {
"clsx": "^2.0.0",
"react-pdf": "6.2.2",
"react-window": "1.8.9",
"@wojtekmaj/react-hooks": "1.17.2",
"react-intersection-observer": "9.5.1",
"lodash": "^4.17.21",
"lodash.debounce": "^4.0.8",
"fuse.js": "^6.6.2"
},
"devDependencies": {
"husky": "^9.0.0",
"@types/node": "*",
"@types/react": "*",
"@types/lodash": "^4.14.195",
"@types/lodash.debounce": "^4.0.7",
"cpy-cli": "^5.0.0",
"globals": "^15.1.0",
"husky": "^9.0.10",
"jsdom": "^24.0.0",
"lint-staged": "^15.0.0",
"prettier": "^3.2.0"
"nodemon": "^3.0.0",
"prettier": "^3.2.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"rimraf": "^3.0.0",
"typescript": "^5.4.2",
"eslint": "^8.56.0",
"eslint-config-wojtekmaj": "^1.0.0",
"@types/react-window": "^1.8.5"
},
"resolutions": {
"eslint-plugin-import": "npm:eslint-plugin-i@^2.28.0"
"peerDependencies": {
"@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
},
"packageManager": "yarn@4.1.1"
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
},
"files": [
"dist",
"src"
],
"lint-staged": {
"*.{css,html,js,json,jsx,md,ts,tsx,yml}": "yarn format --write"
}
}
-3
View File
@@ -1,3 +0,0 @@
{
"extends": "wojtekmaj/react"
}
-1
View File
@@ -1 +0,0 @@
dist
-21
View File
@@ -1,21 +0,0 @@
MIT License
Copyright (c) 20172024 Wojciech Maj
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
-613
View File
@@ -1,613 +0,0 @@
[![npm](https://img.shields.io/npm/v/react-pdf.svg)](https://www.npmjs.com/package/react-pdf) ![downloads](https://img.shields.io/npm/dt/react-pdf.svg) [![CI](https://github.com/wojtekmaj/react-pdf/actions/workflows/ci.yml/badge.svg)](https://github.com/wojtekmaj/react-pdf/actions)
# React-PDF
Display PDFs in your React app as easily as if they were images.
## Lost?
This package is used to _display_ existing PDFs. If you wish to _create_ PDFs using React, you may be looking for [@react-pdf/renderer](https://www.npmjs.com/package/@react-pdf/renderer).
## tl;dr
- Install by executing `npm install react-pdf` or `yarn add react-pdf`.
- Import by adding `import { Document } from 'react-pdf'`.
- Use by adding `<Document file="..." />`. `file` can be a URL, base64 content, Uint8Array, and more.
- Put `<Page />` components inside `<Document />` to render pages.
## Demo
A minimal demo page can be found in `sample` directory.
[Online demo](https://projects.wojtekmaj.pl/react-pdf/) is also available!
## Before you continue
React-PDF is under constant development. This documentation is written for React-PDF 7.x branch. If you want to see documentation for other versions of React-PDF, use dropdown on top of GitHub page to switch to an appropriate tag. Here are quick links to the newest docs from each branch:
- [v7.x](https://github.com/wojtekmaj/react-pdf/blob/v7.x/packages/react-pdf/README.md)
- [v6.x](https://github.com/wojtekmaj/react-pdf/blob/v6.x/README.md)
- [v5.x](https://github.com/wojtekmaj/react-pdf/blob/v5.x/README.md)
- [v4.x](https://github.com/wojtekmaj/react-pdf/blob/v4.x/README.md)
- [v3.x](https://github.com/wojtekmaj/react-pdf/blob/v3.x/README.md)
- [v2.x](https://github.com/wojtekmaj/react-pdf/blob/v2.x/README.md)
- [v1.x](https://github.com/wojtekmaj/react-pdf/blob/v1.x/README.md)
## Getting started
### Compatibility
#### Browser support
React-PDF supports all modern browsers. It is tested with the latest versions of Chrome, Edge, Safari, Firefox, and Opera.
The following browsers are supported out of the box in React-PDF v8 and v7:
- Chrome ≥92
- Edge ≥92
- Safari ≥15.4
- Firefox ≥90
You may extend the list of supported browsers by providing additional polyfills (e.g. for `Array.prototype.at` or `Promise.allSettled`) and either configuring your bundler to transpile `pdfjs-dist` and using [legacy PDF.js worker](#legacy-pdfjs-worker).
If you need to support older browsers, you will need to use React-PDF v6 or earlier.
#### React
To use the latest version of React-PDF, your project needs to use React 16.8 or later.
If you use an older version of React, please refer to the table below to a find suitable React-PDF version.
| React version | Newest compatible React-PDF version |
| ------------- | ----------------------------------- |
| ≥16.8 | latest |
| ≥16.3 | 5.x |
| ≥15.5 | 4.x |
#### Preact
React-PDF may be used with Preact.
### Installation
Add React-PDF to your project by executing `npm install react-pdf` or `yarn add react-pdf`.
#### Next.js
If you use Next.js, you may need to add the following to your `next.config.js`:
```diff
module.exports = {
+ webpack: (config) => {
+ config.resolve.alias.canvas = false;
+ return config;
+ },
}
```
### Configure PDF.js worker
For React-PDF to work, PDF.js worker needs to be provided. You have several options.
#### Import worker (recommended)
For most cases, the following example will work:
```ts
import { pdfjs } from 'react-pdf';
pdfjs.GlobalWorkerOptions.workerSrc = new URL(
'pdfjs-dist/build/pdf.worker.min.js',
import.meta.url,
).toString();
```
> **Note**
> In Next.js:
>
> - Using App Router, make sure to add `'use client';` to the top of the file.
> - Using Pages Router, make sure to [disable SSR](https://nextjs.org/docs/pages/building-your-application/optimizing/lazy-loading#with-no-ssr) when importing the component you're using this code in.
> **Note**
> pnpm requires an `.npmrc` file with `public-hoist-pattern[]=pdfjs-dist` for this to work.
<details>
<summary>See more examples</summary>
##### Parcel 2
For Parcel 2, you need to use a slightly different code:
```diff
pdfjs.GlobalWorkerOptions.workerSrc = new URL(
- 'pdfjs-dist/build/pdf.worker.min.js',
+ 'npm:pdfjs-dist/build/pdf.worker.min.js',
import.meta.url,
).toString();
```
</details>
#### Copy worker to public directory
You will have to make sure on your own that `pdf.worker.js` file from `pdfjs-dist/build` is copied to your project's output folder.
For example, you could use a custom script like:
```ts
import path from 'node:path';
import fs from 'node:fs';
const pdfjsDistPath = path.dirname(require.resolve('pdfjs-dist/package.json'));
const pdfWorkerPath = path.join(pdfjsDistPath, 'build', 'pdf.worker.js');
fs.copyFileSync(pdfWorkerPath, './dist/pdf.worker.js');
```
#### Use external CDN
```ts
import { pdfjs } from 'react-pdf';
pdfjs.GlobalWorkerOptions.workerSrc = `//unpkg.com/pdfjs-dist@${pdfjs.version}/build/pdf.worker.min.js`;
```
#### Legacy PDF.js worker
If you need to support older browsers, you may use legacy PDF.js worker. To do so, follow the instructions above, but replace `/build/` with `legacy/build/` in PDF.js worker import path, for example:
```diff
pdfjs.GlobalWorkerOptions.workerSrc = new URL(
- 'pdfjs-dist/build/pdf.worker.min.js',
+ 'pdfjs-dist/legacy/build/pdf.worker.min.js',
import.meta.url,
).toString();
```
or:
```diff
-pdfjs.GlobalWorkerOptions.workerSrc = `//unpkg.com/pdfjs-dist@${pdfjs.version}/build/pdf.worker.min.js`;
+pdfjs.GlobalWorkerOptions.workerSrc = `//unpkg.com/pdfjs-dist@${pdfjs.version}/legacy/build/pdf.worker.min.js`;
```
### Usage
Here's an example of basic usage:
```tsx
import { useState } from 'react';
import { Document, Page } from 'react-pdf';
function MyApp() {
const [numPages, setNumPages] = useState<number>();
const [pageNumber, setPageNumber] = useState<number>(1);
function onDocumentLoadSuccess({ numPages }: { numPages: number }): void {
setNumPages(numPages);
}
return (
<div>
<Document file="somefile.pdf" onLoadSuccess={onDocumentLoadSuccess}>
<Page pageNumber={pageNumber} />
</Document>
<p>
Page {pageNumber} of {numPages}
</p>
</div>
);
}
```
Check the [sample directory](https://github.com/wojtekmaj/react-pdf/tree/main/sample) in this repository for a full working example. For more examples and more advanced use cases, check [Recipes](https://github.com/wojtekmaj/react-pdf/wiki/Recipes) in [React-PDF Wiki](https://github.com/wojtekmaj/react-pdf/wiki/).
### Support for annotations
If you want to use annotations (e.g. links) in PDFs rendered by React-PDF, then you would need to include stylesheet necessary for annotations to be correctly displayed like so:
```ts
import 'react-pdf/dist/Page/AnnotationLayer.css';
```
### Support for text layer
If you want to use text layer in PDFs rendered by React-PDF, then you would need to include stylesheet necessary for text layer to be correctly displayed like so:
```ts
import 'react-pdf/dist/Page/TextLayer.css';
```
### Support for non-latin characters
If you want to ensure that PDFs with non-latin characters will render perfectly, or you have encountered the following warning:
```
Warning: The CMap "baseUrl" parameter must be specified, ensure that the "cMapUrl" and "cMapPacked" API parameters are provided.
```
then you would also need to include cMaps in your build and tell React-PDF where they are.
#### Copying cMaps
First, you need to copy cMaps from `pdfjs-dist` (React-PDF's dependency - it should be in your `node_modules` if you have React-PDF installed). cMaps are located in `pdfjs-dist/cmaps`.
##### Vite
Add [`vite-plugin-static-copy`](https://www.npmjs.com/package/vite-plugin-static-copy) by executing `npm install vite-plugin-static-copy --save-dev` or `yarn add vite-plugin-static-copy --dev` and add the following to your Vite config:
```diff
+import path from 'node:path';
+import { createRequire } from 'node:module';
-import { defineConfig } from 'vite';
+import { defineConfig, normalizePath } from 'vite';
+import { viteStaticCopy } from 'vite-plugin-static-copy';
+const require = createRequire(import.meta.url);
+const cMapsDir = normalizePath(
+ path.join(path.dirname(require.resolve('pdfjs-dist/package.json')), 'cmaps')
+);
export default defineConfig({
plugins: [
+ viteStaticCopy({
+ targets: [
+ {
+ src: cMapsDir,
+ dest: '',
+ },
+ ],
+ }),
]
});
```
##### Webpack
Add [`copy-webpack-plugin`](https://www.npmjs.com/package/copy-webpack-plugin) by executing `npm install copy-webpack-plugin --save-dev` or `yarn add copy-webpack-plugin --dev` and add the following to your Webpack config:
```diff
+import path from 'node:path';
+import CopyWebpackPlugin from 'copy-webpack-plugin';
+const cMapsDir = path.join(path.dirname(require.resolve('pdfjs-dist/package.json')), 'cmaps');
module.exports = {
plugins: [
+ new CopyWebpackPlugin({
+ patterns: [
+ {
+ from: cMapsDir,
+ to: 'cmaps/'
+ },
+ ],
+ }),
],
};
```
##### Other tools
If you use other bundlers, you will have to make sure on your own that cMaps are copied to your project's output folder.
For example, you could use a custom script like:
```ts
import path from 'node:path';
import fs from 'node:fs';
const cMapsDir = path.join(path.dirname(require.resolve('pdfjs-dist/package.json')), 'cmaps');
fs.cpSync(cMapsDir, 'dist/cmaps/', { recursive: true });
```
#### Setting up React-PDF
Now that you have cMaps in your build, pass required options to Document component by using `options` prop, like so:
```ts
// Outside of React component
const options = {
cMapUrl: '/cmaps/',
};
// Inside of React component
<Document options={options} />;
```
> **Note**
> Make sure to define `options` object outside of your React component, and use `useMemo` if you can't.
Alternatively, you could use cMaps from external CDN:
```tsx
// Outside of React component
import { pdfjs } from 'react-pdf';
const options = {
cMapUrl: `https://unpkg.com/pdfjs-dist@${pdfjs.version}/cmaps/`,
};
// Inside of React component
<Document options={options} />;
```
### Support for standard fonts
If you want to support PDFs using standard fonts (deprecated in PDF 1.5, but still around), ot you have encountered the following warning:
```
The standard font "baseUrl" parameter must be specified, ensure that the "standardFontDataUrl" API parameter is provided.
```
then you would also need to include standard fonts in your build and tell React-PDF where they are.
#### Copying fonts
First, you need to copy standard fonts from `pdfjs-dist` (React-PDF's dependency - it should be in your `node_modules` if you have React-PDF installed). Standard fonts are located in `pdfjs-dist/standard_fonts`.
##### Vite
Add [`vite-plugin-static-copy`](https://www.npmjs.com/package/vite-plugin-static-copy) by executing `npm install vite-plugin-static-copy --save-dev` or `yarn add vite-plugin-static-copy --dev` and add the following to your Vite config:
```diff
+import path from 'node:path';
+import { createRequire } from 'node:module';
-import { defineConfig } from 'vite';
+import { defineConfig, normalizePath } from 'vite';
+import { viteStaticCopy } from 'vite-plugin-static-copy';
+const require = createRequire(import.meta.url);
+const standardFontsDir = normalizePath(
+ path.join(path.dirname(require.resolve('pdfjs-dist/package.json')), 'standard_fonts')
+);
export default defineConfig({
plugins: [
+ viteStaticCopy({
+ targets: [
+ {
+ src: standardFontsDir,
+ dest: '',
+ },
+ ],
+ }),
]
});
```
##### Webpack
Add [`copy-webpack-plugin`](https://www.npmjs.com/package/copy-webpack-plugin) by executing `npm install copy-webpack-plugin --save-dev` or `yarn add copy-webpack-plugin --dev` and add the following to your Webpack config:
```diff
+import path from 'node:path';
+import CopyWebpackPlugin from 'copy-webpack-plugin';
+const standardFontsDir = path.join(path.dirname(require.resolve('pdfjs-dist/package.json')), 'standard_fonts');
module.exports = {
plugins: [
+ new CopyWebpackPlugin({
+ patterns: [
+ {
+ from: standardFontsDir,
+ to: 'standard_fonts/'
+ },
+ ],
+ }),
],
};
```
##### Other tools
If you use other bundlers, you will have to make sure on your own that standard fonts are copied to your project's output folder.
For example, you could use a custom script like:
```ts
import path from 'node:path';
import fs from 'node:fs';
const standardFontsDir = path.join(
path.dirname(require.resolve('pdfjs-dist/package.json')),
'standard_fonts',
);
fs.cpSync(standardFontsDir, 'dist/standard_fonts/', { recursive: true });
```
#### Setting up React-PDF
Now that you have standard fonts in your build, pass required options to Document component by using `options` prop, like so:
```tsx
// Outside of React component
const options = {
standardFontDataUrl: '/standard_fonts/',
};
// Inside of React component
<Document options={options} />;
```
> **Note**
> Make sure to define `options` object outside of your React component, and use `useMemo` if you can't.
Alternatively, you could use standard fonts from external CDN:
```tsx
// Outside of React component
import { pdfjs } from 'react-pdf';
const options = {
standardFontDataUrl: `https://unpkg.com/pdfjs-dist@${pdfjs.version}/standard_fonts`,
};
// Inside of React component
<Document options={options} />;
```
## User guide
### Document
Loads a document passed using `file` prop.
#### Props
| Prop name | Description | Default value | Example values |
| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| className | Class name(s) that will be added to rendered element along with the default `react-pdf__Document`. | n/a | <ul><li>String:<br />`"custom-class-name-1 custom-class-name-2"`</li><li>Array of strings:<br />`["custom-class-name-1", "custom-class-name-2"]`</li></ul> |
| error | What the component should display in case of an error. | `"Failed to load PDF file."` | <ul><li>String:<br />`"An error occurred!"`</li><li>React element:<br />`<p>An error occurred!</p>`</li><li>Function:<br />`this.renderError`</li></ul> |
| externalLinkRel | Link rel for links rendered in annotations. | `"noopener noreferrer nofollow"` | One of valid [values for `rel` attribute](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/a#attr-rel).<ul><li>`"noopener"`</li><li>`"noreferrer"`</li><li>`"nofollow"`</li><li>`"noopener noreferrer"`</li></ul> |
| externalLinkTarget | Link target for external links rendered in annotations. | unset, which means that default behavior will be used | One of valid [values for `target` attribute](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/a#attr-target).<ul><li>`"_self"`</li><li>`"_blank"`</li><li>`"_parent"`</li><li>`"_top"`</li></ul> |
| file | What PDF should be displayed.<br />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.<br />**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. | n/a | <ul><li>URL:<br />`"https://example.com/sample.pdf"`</li><li>File:<br />`import importedPdf from '../static/sample.pdf'` and then<br />`sample`</li><li>Parameter object:<br />`{ url: 'https://example.com/sample.pdf' }`</ul> |
| imageResourcesPath | The path used to prefix the src attributes of annotation SVGs. | n/a (pdf.js will fallback to an empty string) | `"/public/images/"` |
| inputRef | 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. | n/a | <ul><li>Function:<br />`(ref) => { this.myDocument = ref; }`</li><li>Ref created using `createRef`:<br />`this.ref = createRef();`<br />…<br />`inputRef={this.ref}`</li><li>Ref created using `useRef`:<br />`const ref = useRef();`<br />…<br />`inputRef={ref}`</li></ul> |
| loading | What the component should display while loading. | `"Loading PDF…"` | <ul><li>String:<br />`"Please wait!"`</li><li>React element:<br />`<p>Please wait!</p>`</li><li>Function:<br />`this.renderLoader`</li></ul> |
| noData | What the component should display in case of no data. | `"No PDF file specified."` | <ul><li>String:<br />`"Please select a file."`</li><li>React element:<br />`<p>Please select a file.</p>`</li><li>Function:<br />`this.renderNoData`</li></ul> |
| onItemClick | 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. | n/a | `({ dest, pageIndex, pageNumber }) => alert('Clicked an item from page ' + pageNumber + '!')` |
| onLoadError | Function called in case of an error while loading a document. | n/a | `(error) => alert('Error while loading document! ' + error.message)` |
| onLoadProgress | Function called, potentially multiple times, as the loading progresses. | n/a | `({ loaded, total }) => alert('Loading a document: ' + (loaded / total) * 100 + '%')` |
| onLoadSuccess | Function called when the document is successfully loaded. | n/a | `(pdf) => alert('Loaded a file with ' + pdf.numPages + ' pages!')` |
| onPassword | Function called when a password-protected PDF is loaded. | Function that prompts the user for password. | `(callback) => callback('s3cr3t_p4ssw0rd')` |
| onSourceError | Function called in case of an error while retrieving document source from `file` prop. | n/a | `(error) => alert('Error while retrieving document source! ' + error.message)` |
| onSourceSuccess | Function called when document source is successfully retrieved from `file` prop. | n/a | `() => alert('Document source retrieved!')` |
| options | An object in which additional parameters to be passed to PDF.js can be defined. Most notably:<ul><li>`cMapUrl`;</li><li>`httpHeaders` - custom request headers, e.g. for authorization);</li><li>`withCredentials` - a boolean to indicate whether or not to include cookies in the request (defaults to `false`)</li></ul>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).<br /><br />**Note**: Make sure to define options object outside of your React component, and use `useMemo` if you can't.<br /><br />**Note**: `isEvalSupported` is forced to `false` to prevent [arbitrary JavaScript execution upon opening a malicious PDF file](https://github.com/mozilla/pdf.js/security/advisories/GHSA-wgrm-67xf-hhpq). | n/a | `{ cMapUrl: '/cmaps/' }` |
| renderMode | Rendering mode of the document. Can be `"canvas"`, `"custom"`, `"none"` or `"svg"`. If set to `"custom"`, `customRenderer` must also be provided.<br />**Warning**: SVG render mode is deprecated and will be removed in the future. | `"canvas"` | `"custom"` |
| rotate | 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. | n/a | `90` |
### Page
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 rendering annotations and linking between pages inside a document may not be working correctly.
#### Props
| Prop name | Description | Default value | Example values |
| ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| canvasBackground | Canvas background color. Any valid `canvas.fillStyle` can be used. If you set `renderMode` to `"svg"` this prop will be ignored. | n/a | `"transparent"` |
| canvasRef | 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. If you set `renderMode` to `"svg"` this prop will be ignored. | n/a | <ul><li>Function:<br />`(ref) => { this.myCanvas = ref; }`</li><li>Ref created using `createRef`:<br />`this.ref = createRef();`<br />…<br />`inputRef={this.ref}`</li><li>Ref created using `useRef`:<br />`const ref = useRef();`<br />…<br />`inputRef={ref}`</li></ul> |
| className | Class name(s) that will be added to rendered element along with the default `react-pdf__Page`. | n/a | <ul><li>String:<br />`"custom-class-name-1 custom-class-name-2"`</li><li>Array of strings:<br />`["custom-class-name-1", "custom-class-name-2"]`</li></ul> |
| customRenderer | Function that customizes how a page is rendered. You must set `renderMode` to `"custom"` to use this prop. | n/a | `MyCustomRenderer` |
| customTextRenderer | Function that customizes how a text layer is rendered. | n/a | ``({ str, itemIndex }) => str.replace(/ipsum/g, value => `<mark>${value}</mark>`)`` |
| devicePixelRatio | The ratio between physical pixels and device-independent pixels (DIPs) on the current device. | `window.devicePixelRatio` | `1` |
| error | What the component should display in case of an error. | `"Failed to load the page."` | <ul><li>String:<br />`"An error occurred!"`</li><li>React element:<br />`<p>An error occurred!</p>`</li><li>Function:<br />`this.renderError`</li></ul> |
| height | 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. | Page's default height | `300` |
| imageResourcesPath | The path used to prefix the src attributes of annotation SVGs. | n/a (pdf.js will fallback to an empty string) | `"/public/images/"` |
| inputRef | 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. | n/a | <ul><li>Function:<br />`(ref) => { this.myPage = ref; }`</li><li>Ref created using `createRef`:<br />`this.ref = createRef();`<br />…<br />`inputRef={this.ref}`</li><li>Ref created using `useRef`:<br />`const ref = useRef();`<br />…<br />`inputRef={ref}`</li></ul> |
| loading | What the component should display while loading. | `"Loading page…"` | <ul><li>String:<br />`"Please wait!"`</li><li>React element:<br />`<p>Please wait!</p>`</li><li>Function:<br />`this.renderLoader`</li></ul> |
| noData | What the component should display in case of no data. | `"No page specified."` | <ul><li>String:<br />`"Please select a page."`</li><li>React element:<br />`<p>Please select a page.</p>`</li><li>Function:<br />`this.renderNoData`</li></ul> |
| onGetAnnotationsError | Function called in case of an error while loading annotations. | n/a | `(error) => alert('Error while loading annotations! ' + error.message)` |
| onGetAnnotationsSuccess | Function called when annotations are successfully loaded. | n/a | `(annotations) => alert('Now displaying ' + annotations.length + ' annotations!')` |
| onGetStructTreeError | Function called in case of an error while loading structure tree. | n/a | `(error) => alert('Error while loading structure tree! ' + error.message)` |
| onGetStructTreeSuccess | Function called when structure tree is successfully loaded. | n/a | `(structTree) => alert(JSON.stringify(structTree))` |
| onGetTextError | Function called in case of an error while loading text layer items. | n/a | `(error) => alert('Error while loading text layer items! ' + error.message)` |
| onGetTextSuccess | Function called when text layer items are successfully loaded. | n/a | `({ items, styles }) => alert('Now displaying ' + items.length + ' text layer items!')` |
| onLoadError | Function called in case of an error while loading the page. | n/a | `(error) => alert('Error while loading page! ' + error.message)` |
| onLoadSuccess | Function called when the page is successfully loaded. | n/a | `(page) => alert('Now displaying a page number ' + page.pageNumber + '!')` |
| onRenderAnnotationLayerError | Function called in case of an error while rendering the annotation layer. | n/a | `(error) => alert('Error while loading annotation layer! ' + error.message)` |
| onRenderAnnotationLayerSuccess | Function called when annotations are successfully rendered on the screen. | n/a | `() => alert('Rendered the annotation layer!')` |
| onRenderError | Function called in case of an error while rendering the page. | n/a | `(error) => alert('Error while loading page! ' + error.message)` |
| onRenderSuccess | Function called when the page is successfully rendered on the screen. | n/a | `() => alert('Rendered the page!')` |
| onRenderTextLayerError | Function called in case of an error while rendering the text layer. | n/a | `(error) => alert('Error while rendering text layer! ' + error.message)` |
| onRenderTextLayerSuccess | Function called when the text layer is successfully rendered on the screen. | n/a | `() => alert('Rendered the text layer!')` |
| pageIndex | Which page from PDF file should be displayed, by page index. Ignored if `pageNumber` prop is provided. | `0` | `1` |
| pageNumber | Which page from PDF file should be displayed, by page number. If provided, `pageIndex` prop will be ignored. | `1` | `2` |
| pdf | pdf object obtained from `<Document />`'s `onLoadSuccess` callback function. | (automatically obtained from parent `<Document />`) | `pdf` |
| renderAnnotationLayer | Whether annotations (e.g. links) should be rendered. | `true` | `false` |
| renderForms | Whether forms should be rendered. `renderAnnotationLayer` prop must be set to `true`. | `false` | `true` |
| renderMode | Rendering mode of the document. Can be `"canvas"`, `"custom"`, `"none"` or `"svg"`. If set to `"custom"`, `customRenderer` must also be provided.<br />**Warning**: SVG render mode is deprecated and will be removed in the future. | `"canvas"` | `"custom"` |
| renderTextLayer | Whether a text layer should be rendered. | `true` | `false` |
| rotate | Rotation of the page in degrees. `90` = rotated to the right, `180` = upside down, `270` = rotated to the left. | Page's default setting, usually `0` | `90` |
| scale | Page scale. | `1` | `0.5` |
| width | 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. | Page's default width | `300` |
### Outline
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.
#### Props
| Prop name | Description | Default value | Example values |
| ------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | ------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| className | Class name(s) that will be added to rendered element along with the default `react-pdf__Outline`. | n/a | <ul><li>String:<br />`"custom-class-name-1 custom-class-name-2"`</li><li>Array of strings:<br />`["custom-class-name-1", "custom-class-name-2"]`</li></ul> |
| inputRef | 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. | n/a | <ul><li>Function:<br />`(ref) => { this.myOutline = ref; }`</li><li>Ref created using `createRef`:<br />`this.ref = createRef();`<br />…<br />`inputRef={this.ref}`</li><li>Ref created using `useRef`:<br />`const ref = useRef();`<br />…<br />`inputRef={ref}`</li></ul> |
| onItemClick | 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. | n/a | `({ dest, pageIndex, pageNumber }) => alert('Clicked an item from page ' + pageNumber + '!')` |
| onLoadError | Function called in case of an error while retrieving the outline. | n/a | `(error) => alert('Error while retrieving the outline! ' + error.message)` |
| onLoadSuccess | Function called when the outline is successfully retrieved. | n/a | `(outline) => alert('The outline has been successfully retrieved.')` |
### Thumbnail
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.
#### Props
Props are the same as in `<Page />` component, but certain annotation layer and text layer-related props are not available:
- customTextRenderer
- onGetAnnotationsError
- onGetAnnotationsSuccess
- onGetTextError
- onGetTextSuccess
- onRenderAnnotationLayerError
- onRenderAnnotationLayerSuccess
- onRenderTextLayerError
- onRenderTextLayerSuccess
- renderAnnotationLayer
- renderForms
- renderTextLayer
On top of that, additional props are available:
| Prop name | Description | Default value | Example values |
| ----------- | -------------------------------------------------------------------------------------------------------------------------------------------- | ------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
| className | Class name(s) that will be added to rendered element along with the default `react-pdf__Thumbnail`. | n/a | <ul><li>String:<br />`"custom-class-name-1 custom-class-name-2"`</li><li>Array of strings:<br />`["custom-class-name-1", "custom-class-name-2"]`</li></ul> |
| onItemClick | Function called when a thumbnail has been clicked. Usually, you would like to use this callback to move the user wherever they requested to. | n/a | `({ dest, pageIndex, pageNumber }) => alert('Clicked an item from page ' + pageNumber + '!')` |
## Useful links
- [React-PDF Wiki](https://github.com/wojtekmaj/react-pdf/wiki/)
## License
The MIT License.
## Author
<table>
<tr>
<td >
<img src="https://avatars.githubusercontent.com/u/5426427?v=4&s=128" width="64" height="64" alt="Wojciech Maj">
</td>
<td>
<a href="https://github.com/wojtekmaj">Wojciech Maj</a>
</td>
</tr>
</table>
## Thank you
This project wouldn't be possible without the awesome work of [Niklas Närhinen](https://github.com/nnarhinen) who created its original version and without Mozilla, author of [pdf.js](http://mozilla.github.io/pdf.js). Thank you!
### Sponsors
Thank you to all our sponsors! [Become a sponsor](https://opencollective.com/react-pdf-wojtekmaj#sponsor) and get your image on our README on GitHub.
<a href="https://opencollective.com/react-pdf-wojtekmaj#sponsors" target="_blank"><img src="https://opencollective.com/react-pdf-wojtekmaj/sponsors.svg?width=890"></a>
### Backers
Thank you to all our backers! [Become a backer](https://opencollective.com/react-pdf-wojtekmaj#backer) and get your image on our README on GitHub.
<a href="https://opencollective.com/react-pdf-wojtekmaj#backers" target="_blank"><img src="https://opencollective.com/react-pdf-wojtekmaj/backers.svg?width=890"></a>
### Top Contributors
Thank you to all our contributors that helped on this project!
![Top Contributors](https://opencollective.com/react-pdf/contributors.svg?width=890&button=false)
-111
View File
@@ -1,111 +0,0 @@
{
"name": "react-pdf",
"version": "8.0.2",
"description": "Display PDFs in your React app as easily as if they were images.",
"type": "module",
"sideEffects": [
"*.css"
],
"main": "./dist/cjs/index.js",
"module": "./dist/esm/index.js",
"source": "./src/index.ts",
"types": "./dist/cjs/index.d.ts",
"exports": {
".": {
"import": "./dist/esm/index.js",
"require": "./dist/cjs/index.js"
},
"./dist/Page/AnnotationLayer.css": {
"import": "./dist/esm/Page/AnnotationLayer.css",
"require": "./dist/cjs/Page/AnnotationLayer.css"
},
"./dist/Page/TextLayer.css": {
"import": "./dist/esm/Page/TextLayer.css",
"require": "./dist/cjs/Page/TextLayer.css"
},
"./dist/cjs/Page/AnnotationLayer.css": "./dist/cjs/Page/AnnotationLayer.css",
"./dist/cjs/Page/TextLayer.css": "./dist/cjs/Page/TextLayer.css",
"./dist/esm/Page/AnnotationLayer.css": "./dist/esm/Page/AnnotationLayer.css",
"./dist/esm/Page/TextLayer.css": "./dist/esm/Page/TextLayer.css"
},
"scripts": {
"build": "yarn build-js && yarn copy-styles",
"build-js": "yarn build-js-esm && yarn build-js-cjs && yarn build-js-cjs-package",
"build-js-esm": "tsc --project tsconfig.build.json --outDir dist/esm",
"build-js-cjs": "tsc --project tsconfig.build.json --outDir dist/cjs --module commonjs --moduleResolution node --verbatimModuleSyntax false",
"build-js-cjs-package": "echo '{\n \"type\": \"commonjs\"\n}' > dist/cjs/package.json",
"clean": "rimraf dist",
"copy-styles": "cpy 'src/**/*.css' dist/esm && cpy 'src/**/*.css' dist/cjs",
"format": "prettier --check . --cache",
"lint": "eslint . --ext .js,.jsx,.ts,.tsx",
"prepack": "yarn clean && yarn build",
"test": "yarn lint && yarn tsc && yarn format && yarn unit",
"tsc": "tsc",
"unit": "vitest",
"watch": "yarn build-js-esm --watch & yarn build-js-cjs --watch & nodemon --watch src --ext css --exec \"yarn copy-styles\""
},
"keywords": [
"pdf",
"pdf-viewer",
"react"
],
"author": {
"name": "Wojciech Maj",
"email": "kontakt@wojtekmaj.pl"
},
"license": "MIT",
"dependencies": {
"clsx": "^2.0.0",
"dequal": "^2.0.3",
"make-cancellable-promise": "^1.3.1",
"make-event-props": "^1.6.0",
"merge-refs": "^1.3.0",
"pdfjs-dist": "3.11.174",
"tiny-invariant": "^1.0.0",
"warning": "^4.0.0"
},
"devDependencies": {
"@testing-library/dom": "^10.0.0",
"@testing-library/jest-dom": "^6.0.0",
"@testing-library/react": "^15.0.0",
"@types/node": "*",
"@types/react": "*",
"@types/warning": "^3.0.0",
"canvas": "^2.11.2",
"cpy-cli": "^5.0.0",
"eslint": "^8.56.0",
"eslint-config-wojtekmaj": "^1.0.0",
"jsdom": "^24.0.0",
"nodemon": "^3.0.0",
"prettier": "^3.2.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"rimraf": "^3.0.0",
"typescript": "^5.4.2",
"vitest": "^1.0.2"
},
"peerDependencies": {
"@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
},
"publishConfig": {
"access": "public",
"provenance": true
},
"files": [
"dist",
"src"
],
"repository": {
"type": "git",
"url": "git+https://github.com/wojtekmaj/react-pdf.git",
"directory": "packages/react-pdf"
},
"funding": "https://github.com/wojtekmaj/react-pdf?sponsor=1"
}
-694
View File
@@ -1,694 +0,0 @@
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';
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<{
pages: React.RefObject<Record<string, unknown>[]>;
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<{
pages: React.RefObject<Record<string, unknown>[]>;
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(global.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(global.console.error).mockRestore();
});
it('warns if file prop was not memoized', () => {
const spy = vi.spyOn(global.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(global.console.error).mockRestore();
});
it('does not warn if file prop was not memoized, but was changed', () => {
const spy = vi.spyOn(global.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(global.console.error).mockRestore();
});
it('does not warn if options prop was memoized', () => {
const spy = vi.spyOn(global.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(global.console.error).mockRestore();
});
it('warns if options prop was not memoized', () => {
const spy = vi.spyOn(global.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(global.console.error).mockRestore();
});
it('does not warn if options prop was not memoized, but was changed', () => {
const spy = vi.spyOn(global.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(global.console.error).mockRestore();
});
});
-635
View File
@@ -1,635 +0,0 @@
'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 pdfjs from './pdfjs.js';
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 { 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.
*
* **Note**: `isEvalSupported` is forced to `false` to prevent [arbitrary JavaScript execution upon opening a malicious PDF file](https://github.com/mozilla/pdf.js/security/advisories/GHSA-wgrm-67xf-hhpq).
*
* @example { cMapUrl: '/cmaps/' }
*/
options?: Options;
/**
* Rendering mode of the document. Can be `"canvas"`, `"custom"`, `"none"` or `"svg"`. If set to `"custom"`, `customRenderer` must also be provided.
*
* **Warning**: SVG render mode is deprecated and will be removed in the future.
*
* @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: {
// eslint-disable-next-line no-alert
const password = prompt('Enter the password to open this PDF file.');
callback(password);
break;
}
case PasswordResponses.INCORRECT_PASSWORD: {
// eslint-disable-next-line no-alert
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 = 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
}: DocumentProps,
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' });
}
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]);
useEffect(
() => {
if (typeof source === 'undefined') {
return;
}
if (source === false) {
onSourceError();
return;
}
onSourceSuccess();
},
// Ommitted callbacks so they are not called every time they change
// eslint-disable-next-line react-hooks/exhaustive-deps
[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);
}
}
function resetDocument() {
pdfDispatch({ type: 'RESET' });
}
useEffect(resetDocument, [pdfDispatch, source]);
function loadDocument() {
if (!source) {
return;
}
const optionsWithModifiedIsEvalSupported: Options = { ...options, isEvalSupported: false };
const documentInitParams: Source = {
...source,
...optionsWithModifiedIsEvalSupported,
};
const destroyable = pdfjs.getDocument(documentInitParams);
if (onLoadProgress) {
destroyable.onProgress = onLoadProgress;
}
if (onPassword) {
destroyable.onPassword = onPassword;
}
const loadingTask = destroyable;
loadingTask.promise
.then((nextPdf) => {
pdfDispatch({ type: 'RESOLVE', value: nextPdf });
})
.catch((error) => {
if (loadingTask.destroyed) {
return;
}
pdfDispatch({ type: 'REJECT', error });
});
return () => {
loadingTask.destroy();
};
}
useEffect(
loadDocument,
// Ommitted callbacks so they are not called every time they change
// eslint-disable-next-line react-hooks/exhaustive-deps
[options, pdfDispatch, source],
);
useEffect(
() => {
if (typeof pdf === 'undefined') {
return;
}
if (pdf === false) {
onLoadError();
return;
}
onLoadSuccess();
},
// Ommitted callbacks so they are not called every time they change
// eslint-disable-next-line react-hooks/exhaustive-deps
[pdf],
);
function setupLinkService() {
linkService.current.setViewer(viewer.current);
linkService.current.setExternalLinkRel(externalLinkRel);
linkService.current.setExternalLinkTarget(externalLinkTarget);
}
useEffect(setupLinkService, [externalLinkRel, externalLinkTarget]);
function registerPage(pageIndex: number, ref: HTMLDivElement) {
pages.current[pageIndex] = ref;
}
function unregisterPage(pageIndex: number) {
delete pages.current[pageIndex];
}
const childContext = useMemo(
() => ({
imageResourcesPath,
linkService: linkService.current,
onItemClick,
pdf,
registerPage,
renderMode,
rotate,
unregisterPage,
}),
[imageResourcesPath, onItemClick, pdf, renderMode, rotate],
);
const eventProps = useMemo(() => makeEventProps(otherProps, () => pdf), [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;
@@ -1,7 +0,0 @@
'use client';
import { createContext } from 'react';
import type { DocumentContextType } from './shared/types.js';
export default createContext<DocumentContextType>(null);
-208
View File
@@ -1,208 +0,0 @@
/* 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) {
this.pdfDocument = pdfDocument;
}
setViewer(pdfViewer: PDFViewer) {
this.pdfViewer = pdfViewer;
}
setExternalLinkRel(externalLinkRel?: ExternalLinkRel) {
this.externalLinkRel = externalLinkRel;
}
setExternalLinkTarget(externalLinkTarget?: ExternalLinkTarget) {
this.externalLinkTarget = externalLinkTarget;
}
setHistory() {
// Intentionally empty
}
get pagesCount() {
return this.pdfDocument ? this.pdfDocument.numPages : 0;
}
get page() {
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;
}
// eslint-disable-next-line @typescript-eslint/class-literal-property-style
get rotation() {
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) {
this.goToDestination(dest);
}
goToPage(pageNumber: number) {
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) {
link.href = url;
link.rel = this.externalLinkRel || DEFAULT_LINK_REL;
link.target = newWindow ? '_blank' : this.externalLinkTarget || '';
}
getDestinationHash() {
return '#';
}
getAnchorUrl() {
return '#';
}
setHash() {
// Intentionally empty
}
executeNamedAction() {
// Intentionally empty
}
cachePageRef() {
// Intentionally empty
}
isPageVisible() {
return true;
}
isPageCached() {
return true;
}
executeSetOCGState() {
// Intentionally empty
}
}
-8
View File
@@ -1,8 +0,0 @@
type MessageProps = {
children?: React.ReactNode;
type: 'error' | 'loading' | 'no-data';
};
export default function Message({ children, type }: MessageProps) {
return <div className={`react-pdf__message react-pdf__message--${type}`}>{children}</div>;
}
-169
View File
@@ -1,169 +0,0 @@
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
View File
@@ -1,203 +0,0 @@
'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) {
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);
}
}
function resetOutline() {
outlineDispatch({ type: 'RESET' });
}
useEffect(resetOutline, [outlineDispatch, pdf]);
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);
}
useEffect(loadOutline, [outlineDispatch, pdf]);
useEffect(
() => {
if (outline === undefined) {
return;
}
if (outline === false) {
onLoadError();
return;
}
onLoadSuccess();
},
// Ommitted callbacks so they are not called every time they change
// eslint-disable-next-line react-hooks/exhaustive-deps
[outline],
);
const childContext = useMemo(
() => ({
onItemClick,
}),
[onItemClick],
);
const eventProps = useMemo(
() => makeEventProps(otherProps, () => outline),
[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>
);
}
@@ -1,7 +0,0 @@
'use client';
import { createContext } from 'react';
import type { OutlineContextType } from './shared/types.js';
export default createContext<OutlineContextType>(null);
-128
View File
@@ -1,128 +0,0 @@
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
View File
@@ -1,115 +0,0 @@
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) {
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>
{/* eslint-disable-next-line jsx-a11y/anchor-is-valid */}
<a href="#" onClick={onClick}>
{item.title}
</a>
{renderSubitems()}
</li>
);
}
File diff suppressed because it is too large Load Diff
-648
View File
@@ -1,648 +0,0 @@
'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 PageCanvas from './Page/PageCanvas.js';
import PageSVG from './Page/PageSVG.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. If you set `renderMode` to `"svg"` this prop will be ignored.
*
* @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. If you set `renderMode` to `"svg"` this prop will be ignored.
*
* @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"`, `"none"` or `"svg"`. If set to `"custom"`, `customRenderer` must also be provided.
*
* **Warning**: SVG render mode is deprecated and will be removed in the future.
*
* @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) {
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]);
function hook() {
return () => {
if (!isProvided(pageIndex)) {
// Impossible, but TypeScript doesn't know that
return;
}
if (_enableRegisterUnregisterPage && unregisterPage) {
unregisterPage(pageIndex);
}
};
}
useEffect(hook, [_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);
}
}
function resetPage() {
pageDispatch({ type: 'RESET' });
}
useEffect(resetPage, [pageDispatch, pdf, pageIndex]);
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);
}
useEffect(loadPage, [pageDispatch, pdf, pageIndex, pageNumber, registerPage]);
useEffect(
() => {
if (page === undefined) {
return;
}
if (page === false) {
onLoadError();
return;
}
onLoadSuccess();
},
// Ommitted callbacks so they are not called every time they change
// eslint-disable-next-line react-hooks/exhaustive-deps
[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,
),
[otherProps, page, scale],
);
const pageKey = `${pageIndex}@${scale}/${rotate}`;
const pageKeyNoScale = `${pageIndex}/${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 'svg':
return <PageSVG key={`${pageKeyNoScale}_svg`} />;
case 'canvas':
default:
return <PageCanvas key={`${pageKey}_canvas`} canvasRef={canvasRef} />;
}
}
function renderTextLayer() {
if (!renderTextLayerProps) {
return null;
}
return <TextLayer key={`${pageKey}_text`} />;
}
function renderAnnotationLayer() {
if (!renderAnnotationLayerProps) {
return null;
}
/**
* As of now, PDF.js 2.0.943 returns warnings on unimplemented annotations in SVG mode.
* Therefore, as a fallback, we render "traditional" AnnotationLayer component.
*/
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>
);
}
@@ -1,329 +0,0 @@
/* 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);
}
.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;
}
@@ -1,356 +0,0 @@
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');
annotationLinkItems.forEach((link) => 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');
annotationLinkItems.forEach((link) => 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);
});
});
});
@@ -1,209 +0,0 @@
'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 pdfjs from '../pdfjs.js';
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 { IDownloadManager } from 'pdfjs-dist/types/web/interfaces.js';
import type { Annotations } from '../shared/types.js';
export default function AnnotationLayer() {
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(
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);
}
}
function resetAnnotations() {
annotationsDispatch({ type: 'RESET' });
}
useEffect(resetAnnotations, [annotationsDispatch, page]);
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);
};
}
useEffect(loadAnnotations, [annotationsDispatch, page, renderForms]);
useEffect(
() => {
if (annotations === undefined) {
return;
}
if (annotations === false) {
onLoadError();
return;
}
onLoadSuccess();
},
// Ommitted callbacks so they are not called every time they change
// eslint-disable-next-line react-hooks/exhaustive-deps
[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],
);
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
div: layer,
l10n: null, // TODO: Implement this
page,
viewport: clonedViewport,
};
const renderParameters = {
annotations,
annotationStorage: pdf.annotationStorage,
div: layer,
// See https://github.com/mozilla/pdf.js/issues/17029
downloadManager: null as unknown as IDownloadManager,
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?
};
}
useEffect(
renderAnnotationLayer,
// Ommitted callbacks so they are not called every time they change
// eslint-disable-next-line react-hooks/exhaustive-deps
[annotations, imageResourcesPath, linkService, page, renderForms, viewport],
);
return (
<div className={clsx('react-pdf__Page__annotations', 'annotationLayer')} ref={layerElement} />
);
}
@@ -1,141 +0,0 @@
import { beforeAll, describe, expect, it, vi } from 'vitest';
import { render } from '@testing-library/react';
import { pdfjs } from '../index.test.js';
import PageCanvas from './PageCanvas.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('PageCanvas', () => {
// 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(<PageCanvas />, {
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(<PageCanvas />, {
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(<PageCanvas 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(<PageCanvas />, {
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(<PageCanvas />, {
onGetStructTreeSuccess,
page: pageWithRendererMocked,
renderTextLayer: true,
});
expect.assertions(1);
await onGetStructTreeSuccessPromise;
const canvas = container.querySelector('canvas') as HTMLCanvasElement;
expect(canvas.children.length).toBeGreaterThan(0);
});
});
});
-177
View File
@@ -1,177 +0,0 @@
'use client';
import { useCallback, useEffect, useMemo, useRef } from 'react';
import mergeRefs from 'merge-refs';
import invariant from 'tiny-invariant';
import warning from 'warning';
import pdfjs from '../pdfjs.js';
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 PageCanvasProps = {
canvasRef?: React.Ref<HTMLCanvasElement>;
};
export default function PageCanvas(props: PageCanvasProps) {
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],
);
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);
}
useEffect(
drawPageOnCanvas,
// Ommitted callbacks so they are not called every time they change
// eslint-disable-next-line react-hooks/exhaustive-deps
[
canvasBackground,
canvasElement,
devicePixelRatio,
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;
}
}, [canvasElement]);
useEffect(() => cleanup, [cleanup]);
return (
<canvas
className={`${_className}__canvas`}
dir="ltr"
ref={mergeRefs(canvasRef, canvasElement)}
style={{
display: 'block',
userSelect: 'none',
}}
>
{renderTextLayer ? <StructTree /> : null}
</canvas>
);
}
@@ -1,88 +0,0 @@
import { Blob } from 'node:buffer';
import { beforeAll, describe, expect, it } from 'vitest';
import { render } from '@testing-library/react';
import { pdfjs } from '../index.test.js';
import PageSVG from './PageSVG.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('PageSVG', () => {
// Loaded page
let page: PDFPageProxy;
beforeAll(async () => {
const pdf = await pdfjs.getDocument({ data: pdfFile.arrayBuffer }).promise;
page = await pdf.getPage(1);
});
describe('loading', () => {
it('renders a page and calls onRenderSuccess callback properly', async () => {
const originalBlob = globalThis.Blob;
globalThis.Blob = Blob as unknown as typeof globalThis.Blob;
const { func: onRenderSuccess, promise: onRenderSuccessPromise } = makeAsyncCallback();
muteConsole();
renderWithContext(<PageSVG />, {
onRenderSuccess,
page,
scale: 1,
});
expect.assertions(1);
await expect(onRenderSuccessPromise).resolves.toMatchObject([{}]);
restoreConsole();
globalThis.Blob = originalBlob;
});
it('calls onRenderError when failed to render canvas', async () => {
const { func: onRenderError, promise: onRenderErrorPromise } = makeAsyncCallback();
muteConsole();
renderWithContext(<PageSVG />, {
onRenderError,
page: failingPage,
scale: 1,
});
expect.assertions(1);
await expect(onRenderErrorPromise).resolves.toMatchObject([expect.any(Error)]);
restoreConsole();
});
});
});
-162
View File
@@ -1,162 +0,0 @@
import { useEffect, useMemo } from 'react';
import makeCancellable from 'make-cancellable-promise';
import invariant from 'tiny-invariant';
import warning from 'warning';
import pdfjs from '../pdfjs.js';
import usePageContext from '../shared/hooks/usePageContext.js';
import useResolver from '../shared/hooks/useResolver.js';
import { cancelRunningTask, isCancelException, makePageCallback } from '../shared/utils.js';
export default function PageSVG() {
const pageContext = usePageContext();
invariant(pageContext, 'Unable to find Page context.');
const {
_className,
onRenderSuccess: onRenderSuccessProps,
onRenderError: onRenderErrorProps,
page,
rotate,
scale,
} = pageContext;
invariant(page, 'Attempted to render page SVG, but no page was specified.');
const [svgState, svgDispatch] = useResolver<SVGElement>();
const { value: svg, error: svgError } = svgState;
/**
* 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() {
if (!svgError) {
// Impossible, but TypeScript doesn't know that
return;
}
if (isCancelException(svgError)) {
return;
}
warning(false, svgError.toString());
if (onRenderErrorProps) {
onRenderErrorProps(svgError);
}
}
const viewport = useMemo(
() => page.getViewport({ scale, rotation: rotate }),
[page, rotate, scale],
);
function resetSVG() {
svgDispatch({ type: 'RESET' });
}
useEffect(resetSVG, [page, svgDispatch, viewport]);
function renderSVG() {
if (!page) {
return;
}
const cancellable = makeCancellable(page.getOperatorList());
cancellable.promise
.then((operatorList) => {
const svgGfx = new pdfjs.SVGGraphics(page.commonObjs, page.objs);
svgGfx
.getSVG(operatorList, viewport)
.then((nextSvg: unknown) => {
// See https://github.com/mozilla/pdf.js/issues/16745
if (!(nextSvg instanceof SVGElement)) {
throw new Error('getSVG returned unexpected result.');
}
svgDispatch({ type: 'RESOLVE', value: nextSvg });
})
.catch((error) => {
svgDispatch({ type: 'REJECT', error });
});
})
.catch((error) => {
svgDispatch({ type: 'REJECT', error });
});
return () => cancelRunningTask(cancellable);
}
useEffect(renderSVG, [page, svgDispatch, viewport]);
useEffect(
() => {
if (svg === undefined) {
return;
}
if (svg === false) {
onRenderError();
return;
}
onRenderSuccess();
},
// Ommitted callbacks so they are not called every time they change
// eslint-disable-next-line react-hooks/exhaustive-deps
[svg],
);
function drawPageOnContainer(element: HTMLDivElement | null) {
if (!element || !svg) {
return;
}
// Append SVG element to the main container, if this hasn't been done already
if (!element.firstElementChild) {
element.appendChild(svg);
}
const { width, height } = viewport;
svg.setAttribute('width', `${width}`);
svg.setAttribute('height', `${height}`);
}
const { width, height } = viewport;
return (
<div
className={`${_className}__svg`}
// Note: This cannot be shortened, as we need this function to be called with each render.
ref={(ref) => {
drawPageOnContainer(ref);
}}
style={{
display: 'block',
backgroundColor: 'white',
overflow: 'hidden',
width,
height,
userSelect: 'none',
}}
/>
);
}
-110
View File
@@ -1,110 +0,0 @@
/* 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 .endOfContent.active {
top: 0;
}
@@ -1,274 +0,0 @@
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');
});
});
});
-262
View File
@@ -1,262 +0,0 @@
'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 pdfjs from '../pdfjs.js';
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() {
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);
const endElement = useRef<HTMLElement | undefined>(undefined);
warning(
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);
}
}
function resetTextContent() {
textContentDispatch({ type: 'RESET' });
}
useEffect(resetTextContent, [page, textContentDispatch]);
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);
}
useEffect(loadTextContent, [page, textContentDispatch]);
useEffect(
() => {
if (textContent === undefined) {
return;
}
if (textContent === false) {
onLoadError();
return;
}
onLoadSuccess();
},
// Ommitted callbacks so they are not called every time they change
// eslint-disable-next-line react-hooks/exhaustive-deps
[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 end = endElement.current;
if (!end) {
return;
}
end.classList.add('active');
}
function onMouseUp() {
const end = endElement.current;
if (!end) {
return;
}
end.classList.remove('active');
}
const viewport = useMemo(
() => page.getViewport({ scale, rotation: rotate }),
[page, rotate, scale],
);
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 = pdfjs.renderTextLayer(parameters);
const runningTask = cancellable;
cancellable.promise
.then(() => {
const end = document.createElement('div');
end.className = 'endOfContent';
layer.append(end);
endElement.current = 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);
}
useLayoutEffect(renderTextLayer, [
customTextRenderer,
onRenderError,
onRenderSuccess,
page,
pageIndex,
pageNumber,
textContent,
viewport,
]);
return (
// eslint-disable-next-line jsx-a11y/no-static-element-interactions
<div
className={clsx('react-pdf__Page__textContent', 'textLayer')}
onMouseUp={onMouseUp}
onMouseDown={onMouseDown}
ref={layerElement}
/>
);
}
-7
View File
@@ -1,7 +0,0 @@
'use client';
import { createContext } from 'react';
import type { PageContextType } from './shared/types.js';
export default createContext<PageContextType>(null);
@@ -1,8 +0,0 @@
// 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
View File
@@ -1,18 +0,0 @@
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
View File
@@ -1,17 +0,0 @@
export default class Ref {
num: number;
gen: number;
constructor({ num, gen }: { num: number; gen: number }) {
this.num = num;
this.gen = gen;
}
toString() {
let str = `${this.num}R`;
if (this.gen !== 0) {
str += this.gen;
}
return str;
}
}
-141
View File
@@ -1,141 +0,0 @@
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>',
);
});
});
});
-108
View File
@@ -1,108 +0,0 @@
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() {
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);
}
}
function resetAnnotations() {
structTreeDispatch({ type: 'RESET' });
}
useEffect(resetAnnotations, [structTreeDispatch, page]);
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);
}
useEffect(loadStructTree, [customTextRenderer, page, structTreeDispatch]);
useEffect(
() => {
if (structTree === undefined) {
return;
}
if (structTree === false) {
onLoadError();
return;
}
onLoadSuccess();
},
// Ommitted callbacks so they are not called every time they change
// eslint-disable-next-line react-hooks/exhaustive-deps
[structTree],
);
if (!structTree) {
return null;
}
return <StructTreeItem className="react-pdf__Page__structTree structTree" node={structTree} />;
}
-42
View File
@@ -1,42 +0,0 @@
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) {
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 (
// eslint-disable-next-line react/no-array-index-key
<StructTreeItem key={index} node={child} />
);
});
}, [node]);
return (
<span className={className} {...attributes}>
{children}
</span>
);
}
-647
View File
@@ -1,647 +0,0 @@
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(parseInt(width, 10)).toBe(Math.floor(viewport.width));
expect(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(parseInt(width, 10)).toBe(Math.floor(viewport.width));
expect(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(2);
await onLoadSuccessPromise;
const pageCanvas = container.querySelector('.react-pdf__Thumbnail__page__canvas');
const pageSVG = container.querySelector('.react-pdf__Thumbnail__page__svg');
expect(pageCanvas).not.toBeInTheDocument();
expect(pageSVG).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 in SVG mode when given renderMode = "svg"', async () => {
const { func: onLoadSuccess, promise: onLoadSuccessPromise } = makeAsyncCallback();
const { container } = renderWithContext(
<Thumbnail onLoadSuccess={onLoadSuccess} pageIndex={0} renderMode="svg" />,
{ pdf },
);
expect.assertions(1);
await onLoadSuccessPromise;
const pageSVG = container.querySelector('.react-pdf__Thumbnail__page__svg');
expect(pageSVG).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();
});
});
-114
View File
@@ -1,114 +0,0 @@
'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) {
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);
}
}
// eslint-disable-next-line react/prop-types
const { className: classNameProps, onItemClick: onItemClickProps, ...pageProps } = props;
return (
/* eslint-disable-next-line jsx-a11y/anchor-is-valid */
<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
View File
@@ -1,30 +0,0 @@
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('object');
});
});
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
View File
@@ -1,28 +0,0 @@
import pdfjs from './pdfjs.js';
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
View File
@@ -1,35 +0,0 @@
import pdfjs from './pdfjs.js';
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.js';
export {
pdfjs,
Document,
Outline,
Page,
Thumbnail,
useDocumentContext,
useOutlineContext,
usePageContext,
PasswordResponses,
};
@@ -1,11 +0,0 @@
/**
* 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 = require('pdfjs-dist/build/pdf.worker');
-7
View File
@@ -1,7 +0,0 @@
import * as pdfjsModule from 'pdfjs-dist';
const pdfjs = (
'default' in pdfjsModule ? pdfjsModule['default'] : pdfjsModule
) as typeof pdfjsModule;
export default pdfjs;
@@ -1,59 +0,0 @@
// 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 = /^H(\d+)$/;
@@ -1,23 +0,0 @@
'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;
};
}
@@ -1,7 +0,0 @@
import { useContext } from 'react';
import DocumentContext from '../../DocumentContext.js';
export default function useDocumentContext() {
return useContext(DocumentContext);
}
@@ -1,7 +0,0 @@
import { useContext } from 'react';
import OutlineContext from '../../OutlineContext.js';
export default function useOutlineContext() {
return useContext(OutlineContext);
}
@@ -1,7 +0,0 @@
import { useContext } from 'react';
import PageContext from '../../PageContext.js';
export default function usePageContext() {
return useContext(PageContext);
}
@@ -1,28 +0,0 @@
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>() {
return useReducer(reducer<T>, { value: undefined, error: undefined });
}
@@ -1,95 +0,0 @@
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) {
if (!node) {
return null;
}
return {
...getRoleAttributes(node),
...getBaseAttributes(node),
};
}
-172
View File
@@ -1,172 +0,0 @@
import type {
PDFDataRangeTransport,
PDFDocumentProxy,
PDFPageProxy,
PasswordResponses,
} from 'pdfjs-dist';
import type {
BinaryData,
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' | 'svg';
export type ScrollPageIntoViewArgs = {
dest?: ResolvedDest;
pageIndex?: number;
pageNumber: number;
};
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;
};
@@ -1,49 +0,0 @@
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
View File
@@ -1,180 +0,0 @@
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 = typeof document !== 'undefined';
/**
* Checks whether we're running from a local file system.
*/
export const isLocalFileSystem = 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() {
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() {
warning(
!isLocalFileSystem,
`Loading PDF as base64 strings/URLs may not work on protocols other than HTTP/HTTPS. ${allowFileAccessFromFilesTip}`,
);
}
export function displayWorkerWarning() {
warning(
!isLocalFileSystem,
`Loading PDF.js worker may not work on protocols other than HTTP/HTTPS. ${allowFileAccessFromFilesTip}`,
);
}
export function cancelRunningTask(runningTask?: { cancel?: () => void } | null) {
if (runningTask && 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);
});
}
-11
View File
@@ -1,11 +0,0 @@
import { configDefaults, defineConfig } from 'vitest/config';
export default defineConfig({
test: {
environment: 'jsdom',
exclude: [...configDefaults.exclude, 'src/index.test.ts'],
pool: 'forks',
setupFiles: 'vitest.setup.ts',
watch: false,
},
});
-10
View File
@@ -1,10 +0,0 @@
import { afterEach } from 'vitest';
import { cleanup } from '@testing-library/react';
import '@testing-library/jest-dom/vitest';
afterEach(() => {
cleanup();
});
document.body.style.setProperty('--react-pdf-annotation-layer', '1');
document.body.style.setProperty('--react-pdf-text-layer', '1');
-6
View File
@@ -1,6 +0,0 @@
.tsimp
build
node_modules
public/cmaps
public/standard_fonts
public/pdf.worker.js
-51
View File
@@ -1,51 +0,0 @@
{
"name": "react-pdf-sample-page-create-react-app-5",
"version": "4.0.0",
"description": "A sample page for React-PDF.",
"private": true,
"type": "module",
"main": "src/index.tsx",
"scripts": {
"build": "yarn copy && react-scripts build",
"copy": "yarn copy-cmaps && yarn copy-standard-fonts && yarn copy-worker",
"copy-cmaps": "node --import tsimp/import ./scripts/copy-cmaps.ts",
"copy-standard-fonts": "node --import tsimp/import ./scripts/copy-standard-fonts.ts",
"copy-worker": "node --import tsimp/import ./scripts/copy-worker.ts",
"dev": "yarn copy && react-scripts start",
"eject": "react-scripts eject",
"test": "react-scripts test"
},
"author": {
"name": "Wojciech Maj",
"email": "kontakt@wojtekmaj.pl"
},
"license": "MIT",
"dependencies": {
"@wojtekmaj/react-hooks": "^1.20.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-pdf": "latest",
"react-scripts": "^5.0.0"
},
"devDependencies": {
"@types/node": "*",
"tsimp": "^2.0.11",
"typescript": "^5.0.0"
},
"resolutions": {
"eslint-plugin-import@^2.25.3": "npm:eslint-plugin-i@^2.28.0",
"mini-css-extract-plugin": "~2.4.0"
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
}
@@ -1,11 +0,0 @@
<!DOCTYPE html>
<html lang="en-US">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>react-pdf sample page</title>
</head>
<body>
<div id="root"></div>
</body>
</html>
Binary file not shown.
@@ -1,10 +0,0 @@
import fs from 'node:fs';
import path from 'node:path';
import { createRequire } from 'node:module';
const require = createRequire(import.meta.url);
const cMapsDir = path.join(path.dirname(require.resolve('pdfjs-dist/package.json')), 'cmaps');
const targetDir = path.join('public', 'cmaps');
fs.cpSync(cMapsDir, targetDir, { recursive: true });
@@ -1,13 +0,0 @@
import fs from 'node:fs';
import path from 'node:path';
import { createRequire } from 'node:module';
const require = createRequire(import.meta.url);
const standardFontsDir = path.join(
path.dirname(require.resolve('pdfjs-dist/package.json')),
'standard_fonts',
);
const targetDir = path.join('public', 'standard_fonts');
fs.cpSync(standardFontsDir, targetDir, { recursive: true });
@@ -1,17 +0,0 @@
import fs from 'node:fs';
import path from 'node:path';
import { createRequire } from 'node:module';
const require = createRequire(import.meta.url);
const pdfjsDistPath = path.dirname(require.resolve('pdfjs-dist/package.json'));
const pdfWorkerPath = path.join(pdfjsDistPath, 'build', 'pdf.worker.js');
const targetDir = 'public';
const targetPath = path.join(targetDir, 'pdf.worker.js');
// Ensure target directory exists
fs.mkdirSync(targetDir, { recursive: true });
// Copy file
fs.copyFileSync(pdfWorkerPath, targetPath);
-60
View File
@@ -1,60 +0,0 @@
body {
margin: 0;
background-color: #525659;
font-family:
Segoe UI,
Tahoma,
sans-serif;
}
.Example input,
.Example button {
font: inherit;
}
.Example header {
background-color: #323639;
box-shadow: 0 0 8px rgba(0, 0, 0, 0.5);
padding: 20px;
color: white;
}
.Example header h1 {
font-size: inherit;
margin: 0;
}
.Example__container {
display: flex;
flex-direction: column;
align-items: center;
margin: 10px 0;
padding: 10px;
}
.Example__container__load {
margin-top: 1em;
color: white;
}
.Example__container__document {
width: 100%;
max-width: calc(100% - 2em);
margin: 1em 0;
}
.Example__container__document .react-pdf__Document {
display: flex;
flex-direction: column;
align-items: center;
}
.Example__container__document .react-pdf__Page {
margin: 1em 0;
box-shadow: 0 0 8px rgba(0, 0, 0, 0.5);
}
.Example__container__document .react-pdf__message {
padding: 20px;
color: white;
}
-79
View File
@@ -1,79 +0,0 @@
import { useCallback, useState } from 'react';
import { useResizeObserver } from '@wojtekmaj/react-hooks';
import { pdfjs, Document, Page } from 'react-pdf';
import 'react-pdf/dist/esm/Page/AnnotationLayer.css';
import 'react-pdf/dist/esm/Page/TextLayer.css';
import './Sample.css';
import type { PDFDocumentProxy } from 'pdfjs-dist';
pdfjs.GlobalWorkerOptions.workerSrc = new URL(
'pdfjs-dist/build/pdf.worker.min.js',
import.meta.url,
).toString();
const options = {
cMapUrl: '/cmaps/',
standardFontDataUrl: '/standard_fonts/',
};
const resizeObserverOptions = {};
const maxWidth = 800;
type PDFFile = string | File | null;
export default function Sample() {
const [file, setFile] = useState<PDFFile>('./sample.pdf');
const [numPages, setNumPages] = useState<number>();
const [containerRef, setContainerRef] = useState<HTMLElement | null>(null);
const [containerWidth, setContainerWidth] = useState<number>();
const onResize = useCallback<ResizeObserverCallback>((entries) => {
const [entry] = entries;
if (entry) {
setContainerWidth(entry.contentRect.width);
}
}, []);
useResizeObserver(containerRef, resizeObserverOptions, onResize);
function onFileChange(event: React.ChangeEvent<HTMLInputElement>): void {
const { files } = event.target;
if (files && files[0]) {
setFile(files[0] || null);
}
}
function onDocumentLoadSuccess({ numPages: nextNumPages }: PDFDocumentProxy): void {
setNumPages(nextNumPages);
}
return (
<div className="Example">
<header>
<h1>react-pdf sample page</h1>
</header>
<div className="Example__container">
<div className="Example__container__load">
<label htmlFor="file">Load from file:</label>{' '}
<input onChange={onFileChange} type="file" />
</div>
<div className="Example__container__document" ref={setContainerRef}>
<Document file={file} onLoadSuccess={onDocumentLoadSuccess} options={options}>
{Array.from(new Array(numPages), (el, index) => (
<Page
key={`page_${index + 1}`}
pageNumber={index + 1}
width={containerWidth ? Math.min(containerWidth, maxWidth) : maxWidth}
/>
))}
</Document>
</div>
</div>
</div>
);
}
-11
View File
@@ -1,11 +0,0 @@
import { createRoot } from 'react-dom/client';
import Sample from './Sample';
const root = document.getElementById('root');
if (!root) {
throw new Error('Could not find root element');
}
createRoot(root).render(<Sample />);
-13
View File
@@ -1,13 +0,0 @@
{
"compilerOptions": {
"isolatedModules": true,
"jsx": "react-jsx",
"module": "preserve",
"noEmit": true,
"noUncheckedIndexedAccess": true,
"skipLibCheck": true,
"strict": true,
"target": "es2015",
"verbatimModuleSyntax": true
}
}
File diff suppressed because it is too large Load Diff
-3
View File
@@ -1,3 +0,0 @@
.next
dist
node_modules
-60
View File
@@ -1,60 +0,0 @@
body {
margin: 0;
background-color: #525659;
font-family:
Segoe UI,
Tahoma,
sans-serif;
}
.Example input,
.Example button {
font: inherit;
}
.Example header {
background-color: #323639;
box-shadow: 0 0 8px rgba(0, 0, 0, 0.5);
padding: 20px;
color: white;
}
.Example header h1 {
font-size: inherit;
margin: 0;
}
.Example__container {
display: flex;
flex-direction: column;
align-items: center;
margin: 10px 0;
padding: 10px;
}
.Example__container__load {
margin-top: 1em;
color: white;
}
.Example__container__document {
width: 100%;
max-width: calc(100% - 2em);
margin: 1em 0;
}
.Example__container__document .react-pdf__Document {
display: flex;
flex-direction: column;
align-items: center;
}
.Example__container__document .react-pdf__Page {
margin: 1em 0;
box-shadow: 0 0 8px rgba(0, 0, 0, 0.5);
}
.Example__container__document .react-pdf__message {
padding: 20px;
color: white;
}
-81
View File
@@ -1,81 +0,0 @@
'use client';
import { useCallback, useState } from 'react';
import { useResizeObserver } from '@wojtekmaj/react-hooks';
import { pdfjs, Document, Page } from 'react-pdf';
import 'react-pdf/dist/esm/Page/AnnotationLayer.css';
import 'react-pdf/dist/esm/Page/TextLayer.css';
import './Sample.css';
import type { PDFDocumentProxy } from 'pdfjs-dist';
pdfjs.GlobalWorkerOptions.workerSrc = new URL(
'pdfjs-dist/build/pdf.worker.min.js',
import.meta.url,
).toString();
const options = {
cMapUrl: '/cmaps/',
standardFontDataUrl: '/standard_fonts/',
};
const resizeObserverOptions = {};
const maxWidth = 800;
type PDFFile = string | File | null;
export default function Sample() {
const [file, setFile] = useState<PDFFile>('./sample.pdf');
const [numPages, setNumPages] = useState<number>();
const [containerRef, setContainerRef] = useState<HTMLElement | null>(null);
const [containerWidth, setContainerWidth] = useState<number>();
const onResize = useCallback<ResizeObserverCallback>((entries) => {
const [entry] = entries;
if (entry) {
setContainerWidth(entry.contentRect.width);
}
}, []);
useResizeObserver(containerRef, resizeObserverOptions, onResize);
function onFileChange(event: React.ChangeEvent<HTMLInputElement>): void {
const { files } = event.target;
if (files && files[0]) {
setFile(files[0] || null);
}
}
function onDocumentLoadSuccess({ numPages: nextNumPages }: PDFDocumentProxy): void {
setNumPages(nextNumPages);
}
return (
<div className="Example">
<header>
<h1>react-pdf sample page</h1>
</header>
<div className="Example__container">
<div className="Example__container__load">
<label htmlFor="file">Load from file:</label>{' '}
<input onChange={onFileChange} type="file" />
</div>
<div className="Example__container__document" ref={setContainerRef}>
<Document file={file} onLoadSuccess={onDocumentLoadSuccess} options={options}>
{Array.from(new Array(numPages), (el, index) => (
<Page
key={`page_${index + 1}`}
pageNumber={index + 1}
width={containerWidth ? Math.min(containerWidth, maxWidth) : maxWidth}
/>
))}
</Document>
</div>
</div>
</div>
);
}
-11
View File
@@ -1,11 +0,0 @@
export const metadata = {
title: 'react-pdf sample page',
};
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en-US">
<body>{children}</body>
</html>
);
}
-5
View File
@@ -1,5 +0,0 @@
import Sample from './Sample.js';
export default function Page() {
return <Sample />;
}
-5
View File
@@ -1,5 +0,0 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/basic-features/typescript for more information.
-19
View File
@@ -1,19 +0,0 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
webpack: (config) => {
/**
* Critical: prevents " ./node_modules/canvas/build/Release/canvas.node
* Module parse failed: Unexpected character '' (1:0)" error
*/
config.resolve.alias.canvas = false;
// You may not need this, it's just to support moduleResolution: 'node16'
config.resolve.extensionAlias = {
'.js': ['.js', '.ts', '.tsx'],
};
return config;
},
};
export default nextConfig;
-24
View File
@@ -1,24 +0,0 @@
{
"name": "react-pdf-sample-page-next",
"version": "4.0.0",
"description": "A sample page for React-PDF.",
"private": true,
"type": "module",
"scripts": {
"build": "next build",
"dev": "next dev",
"preview": "next preview"
},
"author": {
"name": "Wojciech Maj",
"email": "kontakt@wojtekmaj.pl"
},
"license": "MIT",
"dependencies": {
"@wojtekmaj/react-hooks": "^1.20.0",
"next": "^13.5.4",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-pdf": "latest"
}
}

Some files were not shown because too many files have changed in this diff Show More