mirror of
https://github.com/run-llama/pdf-viewer.git
synced 2026-06-30 21:37:55 -04:00
feat: llama pdf viewer
This commit is contained in:
@@ -1,2 +1,3 @@
|
||||
dist
|
||||
node_modules
|
||||
sample
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
# Auto detect text files and perform LF normalization
|
||||
* text=auto eol=lf
|
||||
@@ -1,2 +0,0 @@
|
||||
github: wojtekmaj
|
||||
open_collective: react-pdf-wojtekmaj
|
||||
@@ -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.
|
||||
@@ -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!
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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 }}
|
||||
|
||||
@@ -33,3 +33,7 @@ yarn-error.log
|
||||
**/.env
|
||||
**/.env.*
|
||||
!**/.env.example
|
||||
|
||||
# sample app
|
||||
sample/*
|
||||
*.tgz
|
||||
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"*.{css,html,js,json,jsx,md,ts,tsx,yml}": "yarn format --write"
|
||||
}
|
||||
@@ -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,3 +1,4 @@
|
||||
.cache
|
||||
.yarn
|
||||
yarnrc.yml
|
||||
dist
|
||||
-9
@@ -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
@@ -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'
|
||||
@@ -1,21 +0,0 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2017–2024 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,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;
|
||||
@@ -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.
@@ -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
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"extends": "wojtekmaj/react"
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
dist
|
||||
@@ -1,21 +0,0 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2017–2024 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,613 +0,0 @@
|
||||
[](https://www.npmjs.com/package/react-pdf)  [](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!
|
||||
|
||||

|
||||
@@ -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"
|
||||
}
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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>;
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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',
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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>',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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} />;
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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');
|
||||
@@ -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),
|
||||
};
|
||||
}
|
||||
@@ -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]>>]>>>>>>');
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
@@ -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');
|
||||
@@ -1,6 +0,0 @@
|
||||
.tsimp
|
||||
build
|
||||
node_modules
|
||||
public/cmaps
|
||||
public/standard_fonts
|
||||
public/pdf.worker.js
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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 />);
|
||||
@@ -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
@@ -1,3 +0,0 @@
|
||||
.next
|
||||
dist
|
||||
node_modules
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
import Sample from './Sample.js';
|
||||
|
||||
export default function Page() {
|
||||
return <Sample />;
|
||||
}
|
||||
Vendored
-5
@@ -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.
|
||||
@@ -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;
|
||||
@@ -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
Reference in New Issue
Block a user