Showing preview only (426K chars total). Download the full file or copy to clipboard to get everything.
Repository: wojtekmaj/react-pdf
Branch: main
Commit: 56b0ca043712
Files: 135
Total size: 393.7 KB
Directory structure:
gitextract_tr_jbi67/
├── .gitattributes
├── .github/
│ ├── FUNDING.yml
│ ├── ISSUE_TEMPLATE/
│ │ ├── Bug_report.yml
│ │ ├── Feature_request.yml
│ │ └── config.yml
│ └── workflows/
│ ├── ci.yml
│ ├── close-stale-issues.yml
│ └── publish.yml
├── .gitignore
├── .husky/
│ └── pre-commit
├── .mailmap
├── .vscode/
│ ├── extensions.json
│ └── settings.json
├── .yarn/
│ └── plugins/
│ └── @yarnpkg/
│ └── plugin-nolyfill.cjs
├── .yarnrc.yml
├── LICENSE
├── __mocks__/
│ ├── _failing_page.ts
│ ├── _failing_pdf.ts
│ └── _silently_failing_pdf.ts
├── biome.json
├── package.json
├── packages/
│ └── react-pdf/
│ ├── LICENSE
│ ├── README.md
│ ├── package.json
│ ├── src/
│ │ ├── Document.spec.tsx
│ │ ├── Document.tsx
│ │ ├── DocumentContext.tsx
│ │ ├── LinkService.ts
│ │ ├── Message.tsx
│ │ ├── Outline.spec.tsx
│ │ ├── Outline.tsx
│ │ ├── OutlineContext.tsx
│ │ ├── OutlineItem.spec.tsx
│ │ ├── OutlineItem.tsx
│ │ ├── Page/
│ │ │ ├── AnnotationLayer.css
│ │ │ ├── AnnotationLayer.spec.tsx
│ │ │ ├── AnnotationLayer.tsx
│ │ │ ├── Canvas.spec.tsx
│ │ │ ├── Canvas.tsx
│ │ │ ├── TextLayer.css
│ │ │ ├── TextLayer.spec.tsx
│ │ │ └── TextLayer.tsx
│ │ ├── Page.spec.tsx
│ │ ├── Page.tsx
│ │ ├── PageContext.tsx
│ │ ├── PasswordResponses.ts
│ │ ├── Ref.spec.ts
│ │ ├── Ref.ts
│ │ ├── StructTree.spec.tsx
│ │ ├── StructTree.tsx
│ │ ├── StructTreeItem.tsx
│ │ ├── Thumbnail.spec.tsx
│ │ ├── Thumbnail.tsx
│ │ ├── index.spec.ts
│ │ ├── index.test.ts
│ │ ├── index.ts
│ │ ├── pdf.worker.entry.ts
│ │ └── shared/
│ │ ├── constants.ts
│ │ ├── hooks/
│ │ │ ├── useCachedValue.ts
│ │ │ ├── useDocumentContext.ts
│ │ │ ├── useOutlineContext.ts
│ │ │ ├── usePageContext.ts
│ │ │ └── useResolver.ts
│ │ ├── structTreeUtils.ts
│ │ ├── types.ts
│ │ ├── utils.spec.ts
│ │ └── utils.ts
│ ├── tsconfig.build.json
│ ├── tsconfig.json
│ ├── vitest.config.ts
│ └── vitest.setup.ts
├── sample/
│ ├── next-app/
│ │ ├── .gitignore
│ │ ├── app/
│ │ │ ├── Sample.css
│ │ │ ├── Sample.tsx
│ │ │ ├── layout.tsx
│ │ │ └── page.tsx
│ │ ├── next-env.d.ts
│ │ ├── next.config.ts
│ │ ├── package.json
│ │ └── tsconfig.json
│ ├── next-pages/
│ │ ├── .gitignore
│ │ ├── empty-module.ts
│ │ ├── next-env.d.ts
│ │ ├── next.config.ts
│ │ ├── package.json
│ │ ├── pages/
│ │ │ ├── Sample.css
│ │ │ ├── Sample.tsx
│ │ │ ├── _app.tsx
│ │ │ └── index.tsx
│ │ └── tsconfig.json
│ ├── parcel2/
│ │ ├── .gitignore
│ │ ├── Sample.css
│ │ ├── Sample.tsx
│ │ ├── index.html
│ │ ├── index.tsx
│ │ ├── package.json
│ │ ├── scripts/
│ │ │ ├── copy-cmaps.ts
│ │ │ ├── copy-standard-fonts.ts
│ │ │ └── copy-wasm.ts
│ │ └── tsconfig.json
│ ├── vite/
│ │ ├── .gitignore
│ │ ├── Sample.css
│ │ ├── Sample.tsx
│ │ ├── index.html
│ │ ├── index.tsx
│ │ ├── package.json
│ │ ├── tsconfig.json
│ │ └── vite.config.ts
│ └── webpack5/
│ ├── .babelrc
│ ├── .gitignore
│ ├── Sample.css
│ ├── Sample.tsx
│ ├── index.html
│ ├── index.tsx
│ ├── package.json
│ ├── tsconfig.json
│ └── webpack.config.ts
├── test/
│ ├── .gitignore
│ ├── AnnotationOptions.tsx
│ ├── CustomRenderer.tsx
│ ├── LayerOptions.tsx
│ ├── LoadingOptions.tsx
│ ├── PassingOptions.tsx
│ ├── Test.css
│ ├── Test.tsx
│ ├── ViewOptions.tsx
│ ├── global.d.ts
│ ├── index.html
│ ├── index.tsx
│ ├── package.json
│ ├── shared/
│ │ ├── types.ts
│ │ └── utils.ts
│ ├── tsconfig.json
│ └── vite.config.ts
└── test-utils.ts
================================================
FILE CONTENTS
================================================
================================================
FILE: .gitattributes
================================================
# Auto detect text files and perform LF normalization
* text=auto eol=lf
================================================
FILE: .github/FUNDING.yml
================================================
github: wojtekmaj
open_collective: react-pdf-wojtekmaj
================================================
FILE: .github/ISSUE_TEMPLATE/Bug_report.yml
================================================
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 or, if applicable, a minimal code snippet 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**: 9.0.0
- **React version**: 18.2.0
- **Bundler name and version (if applicable)**: Vite 5.2.0
value: |
- **Browser (if applicable)**:
- **React-PDF version**:
- **React version**:
- **Bundler name and version (if applicable)**:
================================================
FILE: .github/ISSUE_TEMPLATE/Feature_request.yml
================================================
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.
================================================
FILE: .github/ISSUE_TEMPLATE/config.yml
================================================
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!
================================================
FILE: .github/workflows/ci.yml
================================================
name: CI
on:
push:
branches: ['*']
pull_request:
branches: [main]
env:
HUSKY: 0
jobs:
lint:
name: Static code analysis
runs-on: ubuntu-24.04-arm
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Setup Biome
uses: biomejs/setup-biome@v2
- name: Run tests
run: biome lint
typescript:
name: Type checking (React ${{ matrix.react }})
runs-on: ubuntu-24.04-arm
strategy:
matrix:
react: [18, 19]
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Cache Yarn cache
uses: actions/cache@v5
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@v6
with:
node-version: '24'
- name: Install Corepack
run: npm install -g corepack
- name: Install dependencies
run: yarn --immutable
env:
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: true
- name: Override React version
if: ${{ matrix.react == 18 }}
run: |
npm pkg set resolutions.'@types/react'='npm:^18.0.0'
npm pkg set resolutions.'@types/react-dom'='npm:^18.0.0'
yarn config set enableImmutableInstalls false
yarn up react@^18.0.0 react-dom@^18.0.0
- name: Build package
run: yarn build
- name: Run type checking (React ${{ matrix.react }})
run: yarn tsc
format:
name: Formatting
runs-on: ubuntu-24.04-arm
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Setup Biome
uses: biomejs/setup-biome@v2
- name: Run formatting
run: biome format
unit:
name: Unit tests (React ${{ matrix.react }})
runs-on: ubuntu-latest
strategy:
matrix:
react: [18, 19]
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Cache Yarn cache
uses: actions/cache@v5
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: Cache ~/.cache/ms-playwright
id: playwright-cache
uses: actions/cache@v5
env:
cache-name: playwright-cache
with:
path: ~/.cache/ms-playwright
key: ${{ runner.os }}-${{ env.cache-name }}-${{ hashFiles('**/yarn.lock') }}
- name: Use Node.js
uses: actions/setup-node@v6
with:
node-version: '24'
- name: Install Corepack
run: npm install -g corepack
- name: Install dependencies
run: yarn --immutable
- name: Override React version
if: ${{ matrix.react == 18 }}
run: |
npm pkg set resolutions.'@types/react'='npm:^18.0.0'
npm pkg set resolutions.'@types/react-dom'='npm:^18.0.0'
yarn config set enableImmutableInstalls false
yarn up react@^18.0.0 react-dom@^18.0.0
- name: Install Playwright browsers
if: steps.playwright-cache.outputs.cache-hit != 'true'
run: yarn workspace react-pdf playwright install chromium-headless-shell
- name: Run unit tests (React ${{ matrix.react }})
run: yarn unit
================================================
FILE: .github/workflows/close-stale-issues.yml
================================================
name: Close stale issues
on:
schedule:
- cron: '0 0 * * 1' # Every Monday
workflow_dispatch:
jobs:
close-issues:
name: Close stale issues
runs-on: ubuntu-24.04-arm
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
================================================
FILE: .github/workflows/publish.yml
================================================
name: Publish
on:
release:
types: [published]
env:
HUSKY: 0
permissions:
id-token: write
jobs:
publish:
name: Publish
runs-on: ubuntu-24.04-arm
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Cache Yarn cache
uses: actions/cache@v5
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@v6
with:
node-version: '24'
registry-url: 'https://registry.npmjs.org'
- name: Install Corepack
run: npm install -g corepack
- name: Install dependencies
run: yarn --immutable
- name: Publish with latest tag
if: github.event.release.prerelease == false
run: yarn npm publish --tag latest
working-directory: packages/react-pdf
- name: Publish with next tag
if: github.event.release.prerelease == true
run: yarn npm publish --tag next
working-directory: packages/react-pdf
================================================
FILE: .gitignore
================================================
# OS
.DS_Store
# Cache
.cache
.playwright
.tmp
*.tsbuildinfo
.eslintcache
# Yarn
.pnp.*
**/.yarn/*
!**/.yarn/patches
!**/.yarn/plugins
!**/.yarn/releases
!**/.yarn/sdks
!**/.yarn/versions
# Project-generated directories and files
__screenshots__
coverage
dist
node_modules
playwright-report
test-results
package.tgz
# Logs
npm-debug.log
yarn-error.log
# .env files
**/.env
**/.env.*
!**/.env.example
================================================
FILE: .husky/pre-commit
================================================
yarn format --staged --no-errors-on-unmatched --write
================================================
FILE: .mailmap
================================================
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>
================================================
FILE: .vscode/extensions.json
================================================
{
"recommendations": ["biomejs.biome"],
"unwantedRecommendations": ["dbaeumer.jshint", "dbaeumer.vscode-eslint", "esbenp.prettier-vscode"]
}
================================================
FILE: .vscode/settings.json
================================================
{
"editor.defaultFormatter": "biomejs.biome",
"editor.formatOnSave": true,
"search.exclude": {
"**/.yarn": true
}
}
================================================
FILE: .yarn/plugins/@yarnpkg/plugin-nolyfill.cjs
================================================
/* 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 n=Object.getOwnPropertyNames;var y=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 n(r))!y.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 m={};c(m,{default:()=>h});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","deep-equal-json","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-core-module","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","json-stable-stringify","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"],u=new Map(d.map(t=>[o.structUtils.makeIdent(null,t).identHash,o.structUtils.makeIdent("nolyfill",t)])),b={hooks:{reduceDependency:async t=>{let r=u.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}}},h=b;return f(m);})();
return plugin;
}
};
================================================
FILE: .yarnrc.yml
================================================
enableScripts: false
logFilters:
- code: YN0076
level: discard
nodeLinker: node-modules
plugins:
- checksum: 9b6f8a34bda80f025c0b223fa80836f5e931cf5c8dd83e10ccfa9e677856cf1508b063d027060f74e3ce66ee1c8a936542e85db18a30584f9b88a50379b3f514
path: .yarn/plugins/@yarnpkg/plugin-nolyfill.cjs
spec: "https://raw.githubusercontent.com/wojtekmaj/yarn-plugin-nolyfill/v1.0.1/bundles/@yarnpkg/plugin-nolyfill.js"
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2017–2026 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.
================================================
FILE: __mocks__/_failing_page.ts
================================================
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;
================================================
FILE: __mocks__/_failing_pdf.ts
================================================
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;
================================================
FILE: __mocks__/_silently_failing_pdf.ts
================================================
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;
================================================
FILE: biome.json
================================================
{
"$schema": "https://biomejs.dev/schemas/2.2.2/schema.json",
"files": {
"includes": [
"**",
"!**/.yarn",
"!**/coverage",
"!**/dist",
"!**/.pnp.cjs",
"!**/.pnp.loader.mjs"
]
},
"assist": {
"actions": {
"source": {
"organizeImports": {
"level": "on",
"options": {
"groups": [
{ "type": false, "source": ":NODE:" },
{ "type": false, "source": ["vitest", "vitest/**", "@vitest/**", "vitest-*"] },
{ "type": false, "source": ["react", "react-dom", "react-dom/**", "react-native"] },
{ "type": false, "source": [":PACKAGE:"] },
":BLANK_LINE:",
{
"type": false,
"source": [
":PATH:",
"!**/hooks/*",
"!**/use*.js",
"!**/shared/*",
"!**/utils/*",
"!**/__mocks__/*",
"!**/test-utils.js"
]
},
":BLANK_LINE:",
{ "type": false, "source": ["**/hooks/*", "**/use*.js"] },
":BLANK_LINE:",
{ "type": false, "source": ["**/shared/*", "**/utils/*"] },
":BLANK_LINE:",
{ "type": false, "source": "**/__mocks__/*" },
":BLANK_LINE:",
{ "type": false, "source": "**/test-utils.js" },
":BLANK_LINE:",
":NODE:",
":PACKAGE:",
":PATH:"
]
}
}
}
}
},
"formatter": {
"lineWidth": 100,
"indentStyle": "space"
},
"linter": {
"rules": {
"complexity": {
"noUselessSwitchCase": "off"
},
"correctness": {
"noUnusedImports": "warn",
"noUnusedVariables": {
"level": "warn",
"options": {
"ignoreRestSiblings": true
}
}
},
"suspicious": {
"noConsole": "warn"
}
}
},
"css": {
"formatter": {
"quoteStyle": "single"
}
},
"javascript": {
"formatter": {
"quoteStyle": "single"
}
},
"overrides": [
{
"includes": ["**/vite.config.ts"],
"linter": {
"rules": {
"suspicious": {
"noConsole": "off"
}
}
}
}
]
}
================================================
FILE: package.json
================================================
{
"name": "react-pdf-monorepo",
"version": "1.0.0",
"description": "react-pdf monorepo",
"type": "module",
"workspaces": [
"packages/*",
"test"
],
"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"
},
"devDependencies": {
"husky": "^9.0.0"
},
"packageManager": "yarn@4.10.3"
}
================================================
FILE: packages/react-pdf/LICENSE
================================================
MIT License
Copyright (c) 2017–2026 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.
================================================
FILE: packages/react-pdf/README.md
================================================
[](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.
- Import stylesheets for [annotations](#support-for-annotations) and [text layer](#support-for-text-layer) if applicable.
## 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 10.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:
- [v9.x](https://github.com/wojtekmaj/react-pdf/blob/v9.x/packages/react-pdf/README.md)
- [v8.x](https://github.com/wojtekmaj/react-pdf/blob/v8.x/packages/react-pdf/README.md)
- [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)
## Getting started
### Compatibility
#### Browser support
React-PDF supports the latest versions of all major modern browsers.
Browser compatibility for React-PDF primarily depends on PDF.js support. For details, refer to the [PDF.js documentation](https://github.com/mozilla/pdf.js/wiki/Frequently-Asked-Questions#faq-support).
You may extend the list of supported browsers by providing additional polyfills (e.g. `Array.prototype.at`, `Promise.allSettled` or `Promise.withResolvers`) and configuring your bundler to transpile `pdfjs-dist`.
#### React
To use the latest version of React-PDF, your project needs to use React 16.8 or later.
#### 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 prior to v15 (v15.0.0-canary.53, specifically), you may need to add the following to your `next.config.js`:
```diff
module.exports = {
+ swcMinify: false,
}
```
### 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.mjs',
import.meta.url,
).toString();
```
> [!WARNING]
> The `workerSrc` must be set in the **same module** where you use React-PDF components (e.g., `<Document>`, `<Page>`). Setting it in a separate file like `main.tsx` and then importing React-PDF in another component may cause the default value to overwrite your custom setting due to module execution order. Always configure the worker in the file where you render the PDF components.
> [!NOTE]
> In Next.js, make sure to skip SSR when importing the module you're using this code in. Here's how to do this in [Pages Router](https://nextjs.org/docs/pages/guides/lazy-loading#with-no-ssr) and [App Router](https://nextjs.org/docs/app/guides/lazy-loading#skipping-ssr).
> [!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.mjs',
+ 'npm:pdfjs-dist/build/pdf.worker.min.mjs',
import.meta.url,
).toString();
```
</details>
#### Copy worker to public directory
You will have to make sure on your own that `pdf.worker.mjs` 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.mjs');
fs.cpSync(pdfWorkerPath, './dist/pdf.worker.mjs', { recursive: true });
```
#### Use external CDN
```ts
import { pdfjs } from 'react-pdf';
pdfjs.GlobalWorkerOptions.workerSrc = `//unpkg.com/pdfjs-dist@${pdfjs.version}/build/pdf.worker.min.mjs`;
```
> [!WARNING]
> The `workerSrc` must be set in the **same module** where you use React-PDF components (e.g., `<Document>`, `<Page>`). Setting it in a separate file like `main.tsx` and then importing React-PDF in another component may cause the default value to overwrite your custom setting due to module execution order. Always configure the worker in the file where you render the PDF components.
### 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 pdfjsDistPath = path.dirname(require.resolve('pdfjs-dist/package.json'));
+const cMapsDir = normalizePath(path.join(pdfjsDistPath, '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 pdfjsDistPath = path.dirname(require.resolve('pdfjs-dist/package.json'));
+const cMapsDir = path.join(pdfjsDistPath, '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 pdfjsDistPath = path.dirname(require.resolve('pdfjs-dist/package.json'));
const cMapsDir = path.join(pdfjsDistPath, '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 or 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 JPEG 2000
If you want to ensure that JPEG 2000 images in PDFs will render, or you have encountered the following warning:
```
Warning: Unable to decode image "img_p0_1": "JpxError: OpenJPEG failed to initialize".
```
then you would also need to include wasm directory in your build and tell React-PDF where it is.
#### Copying wasm directory
First, you need to copy wasm 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/wasm`.
##### 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 pdfjsDistPath = path.dirname(require.resolve('pdfjs-dist/package.json'));
+const wasmDir = normalizePath(path.join(pdfjsDistPath, 'wasm'));
export default defineConfig({
plugins: [
+ viteStaticCopy({
+ targets: [
+ {
+ src: wasmDir,
+ 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 pdfjsDistPath = path.dirname(require.resolve('pdfjs-dist/package.json'));
+const wasmDir = path.join(pdfjsDistPath, 'wasm');
module.exports = {
plugins: [
+ new CopyWebpackPlugin({
+ patterns: [
+ {
+ from: wasmDir,
+ to: 'wasm/'
+ },
+ ],
+ }),
],
};
```
##### Other tools
If you use other bundlers, you will have to make sure on your own that wasm directory 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 wasmDir = path.join(pdfjsDistPath, 'wasm');
fs.cpSync(wasmDir, 'dist/wasm/', { recursive: true });
```
#### Setting up React-PDF
Now that you have wasm directory in your build, pass required options to Document component by using `options` prop, like so:
```ts
// Outside of React component
const options = {
wasmUrl: '/wasm/',
};
// Inside of React component
<Document options={options} />;
```
> [!NOTE]
> Make sure to define `options` object outside of your React component or use `useMemo` if you can't.
Alternatively, you could use wasm directory from external CDN:
```tsx
// Outside of React component
import { pdfjs } from 'react-pdf';
const options = {
wasmUrl: `https://unpkg.com/pdfjs-dist@${pdfjs.version}/wasm/`,
};
// 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), or 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 pdfjsDistPath = path.dirname(require.resolve('pdfjs-dist/package.json'));
const standardFontsDir = path.join(pdfjsDistPath, '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 or 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 or use `useMemo` if you can't. | n/a | `{ cMapUrl: '/cmaps/' }` |
| renderMode | Rendering mode of the document. Can be `"canvas"`, `"custom"` or `"none"`. If set to `"custom"`, `customRenderer` must also be provided. | `"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` |
| scale | Document scale. | `1` | `0.5` |
### 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. | 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 `<Canvas>` component. | 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> |
| filterAnnotations | Function to filter annotations before they are rendered. | n/a | `({ annotations }) => annotations.filter(annotation => annotation.subtype === 'Text')` |
| 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!')` |
| pageColors | Colors used to render the page. If not provided, the default colors from PDF will be used. | n/a | `{ background: 'black', foreground: '#ffff00' }` |
| 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"` or `"none"`. If set to `"custom"`, `customRenderer` must also be provided. | `"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!

================================================
FILE: packages/react-pdf/package.json
================================================
{
"name": "react-pdf",
"version": "10.4.1",
"description": "Display PDFs in your React app as easily as if they were images.",
"type": "module",
"sideEffects": [
"*.css"
],
"main": "./dist/index.js",
"source": "./src/index.ts",
"types": "./dist/index.d.ts",
"exports": {
".": "./dist/index.js",
"./*": "./*"
},
"scripts": {
"build": "yarn build-js && yarn copy-styles",
"build-js": "tsc --project tsconfig.build.json",
"clean": "node -e \"fs.rmSync('./dist', { recursive: true, force: true })\"",
"copy-styles": "cpy 'src/**/*.css' dist",
"format": "biome format",
"lint": "biome lint",
"prepack": "yarn clean && yarn build",
"test": "yarn lint && yarn tsc && yarn format && yarn unit",
"tsc": "tsc",
"unit": "vitest",
"watch": "yarn build-js --watch & node --eval \"fs.watch('src', () => child_process.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": "^2.0.0",
"make-event-props": "^2.0.0",
"merge-refs": "^2.0.0",
"pdfjs-dist": "5.4.296",
"tiny-invariant": "^1.0.0",
"warning": "^4.0.0"
},
"devDependencies": {
"@biomejs/biome": "2.2.2",
"@types/node": "*",
"@types/react": "^19.2.0",
"@types/react-dom": "^19.2.0",
"@types/warning": "^3.0.0",
"@vitest/browser-playwright": "^4.0.1",
"cpy-cli": "^5.0.0",
"playwright": "^1.55.1",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"typescript": "^5.9.2",
"vitest": "^4.0.1",
"vitest-browser-react": "^2.0.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"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
},
"publishConfig": {
"access": "public",
"provenance": true
},
"files": [
"dist/**/*",
"src/**/*",
"!**/*.spec.ts",
"!**/*.spec.tsx"
],
"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"
}
================================================
FILE: packages/react-pdf/src/Document.spec.tsx
================================================
import { beforeAll, describe, expect, it, vi } from 'vitest';
import { page, userEvent } from 'vitest/browser';
import { render } from 'vitest-browser-react';
import { createRef } from 'react';
import Document from './Document.js';
import DocumentContext from './DocumentContext.js';
import { pdfjs } from './index.test.js';
import Page from './Page.js';
import { loadPDF, makeAsyncCallback, muteConsole, restoreConsole } from '../../../test-utils.js';
import type { PDFDocumentProxy } from 'pdfjs-dist';
import type LinkService from './LinkService.js';
import type { ScrollPageIntoViewArgs } from './shared/types.js';
const pdfFile = await loadPDF('../../__mocks__/_pdf.pdf');
const pdfFile2 = await loadPDF('../../__mocks__/_pdf2.pdf');
const OK = Symbol('OK');
function ChildInternal({
renderMode,
rotate,
scale,
}: {
renderMode?: string | null;
rotate?: number | null;
scale?: number | null;
}) {
return (
<div data-testid="child" data-rendermode={renderMode} data-rotate={rotate} data-scale={scale} />
);
}
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();
await 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();
await 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]);
});
it('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();
await 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();
await 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();
await 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
await 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 } = await 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();
await 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', async () => {
const className = 'testClassName';
const { container } = await render(<Document className={className} />);
const wrapper = container.querySelector('.react-pdf__Document');
expect(wrapper).toHaveClass(className);
});
it('passes container element to inputRef properly', async () => {
const inputRef = createRef<HTMLDivElement>();
await render(<Document inputRef={inputRef} />);
expect(inputRef.current).toBeInstanceOf(HTMLDivElement);
});
it('renders "No PDF file specified." when given nothing', async () => {
const { container } = await 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', async () => {
const { container } = await 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', async () => {
const { container } = await 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 } = await render(<Document file={pdfFile.file} />);
const loading = container.querySelector('.react-pdf__message');
expect(loading).toBeInTheDocument();
await expect.element(page.getByText('Loading PDF…')).toBeInTheDocument();
});
it('renders custom loading message when loading a file and loading prop is given', async () => {
const { container } = await render(<Document file={pdfFile.file} loading="Loading" />);
const loading = container.querySelector('.react-pdf__message');
expect(loading).toBeInTheDocument();
await expect.element(page.getByText('Loading')).toBeInTheDocument();
});
it('renders custom loading message when loading a file and loading prop is given as a function', async () => {
const { container } = await render(
<Document file={pdfFile.file} loading={() => 'Loading'} />,
);
const loading = container.querySelector('.react-pdf__message');
expect(loading).toBeInTheDocument();
await expect.element(page.getByText('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 } = await render(<Document file={failingPdf} onLoadError={onLoadError} />);
expect.assertions(2);
await onLoadErrorPromise;
await waitForAsync();
const error = container.querySelector('.react-pdf__message');
expect(error).toBeInTheDocument();
await expect.element(page.getByText('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 } = await 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();
await expect.element(page.getByText('Error', { exact: true })).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 } = await 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();
await expect.element(page.getByText('Error', { exact: true })).toBeInTheDocument();
restoreConsole();
});
it('passes renderMode prop to its children', async () => {
const { func: onLoadSuccess, promise: onLoadSuccessPromise } = makeAsyncCallback();
await render(
<Document
file={pdfFile.file}
loading="Loading"
onLoadSuccess={onLoadSuccess}
renderMode="custom"
>
<Child />
</Document>,
);
expect.assertions(1);
await onLoadSuccessPromise;
const child = page.getByTestId('child').element();
expect(child.dataset.rendermode).toBe('custom');
});
it('passes rotate prop to its children', async () => {
const { func: onLoadSuccess, promise: onLoadSuccessPromise } = makeAsyncCallback();
await render(
<Document file={pdfFile.file} loading="Loading" onLoadSuccess={onLoadSuccess} rotate={90}>
<Child />
</Document>,
);
expect.assertions(1);
await onLoadSuccessPromise;
const child = page.getByTestId('child').element();
expect(child.dataset.rotate).toBe('90');
});
it('passes scale prop to its children', async () => {
const { func: onLoadSuccess, promise: onLoadSuccessPromise } = makeAsyncCallback();
await render(
<Document file={pdfFile.file} loading="Loading" onLoadSuccess={onLoadSuccess} scale={1.5}>
<Child />
</Document>,
);
expect.assertions(1);
await onLoadSuccessPromise;
const child = page.getByTestId('child').element();
expect(child.dataset.scale).toBe('1.5');
});
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();
await render(
<Document
file={pdfFile.file}
loading="Loading"
onLoadSuccess={onLoadSuccess}
renderMode="canvas"
>
<Child renderMode="custom" />
</Document>,
);
expect.assertions(1);
await onLoadSuccessPromise;
const child = page.getByTestId('child').element();
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();
await render(
<Document file={pdfFile.file} loading="Loading" onLoadSuccess={onLoadSuccess} rotate={90}>
<Child rotate={180} />
</Document>,
);
expect.assertions(1);
await onLoadSuccessPromise;
const child = page.getByTestId('child').element();
expect(child.dataset.rotate).toBe('180');
});
it('does not overwrite scale prop in its children when given scale prop to both Document and its children', async () => {
const { func: onLoadSuccess, promise: onLoadSuccessPromise } = makeAsyncCallback();
await render(
<Document file={pdfFile.file} loading="Loading" onLoadSuccess={onLoadSuccess} scale={1.5}>
<Child scale={2} />
</Document>,
);
expect.assertions(1);
await onLoadSuccessPromise;
const child = page.getByTestId('child').element();
expect(child.dataset.scale).toBe('2');
});
it('supports function as children', async () => {
const { func: onLoadSuccess, promise: onLoadSuccessPromise } = makeAsyncCallback();
await render(
<Document file={pdfFile.file} loading="Loading" onLoadSuccess={onLoadSuccess}>
{({ pdf }) => <p>{`This PDF has ${pdf.numPages} pages`}</p>}
</Document>,
);
expect.assertions(1);
await onLoadSuccessPromise;
const child = page.getByText('This PDF has 4 pages');
expect(child).toBeInTheDocument();
});
});
describe('viewer', () => {
it('calls onItemClick if defined', async () => {
const { func: onLoadSuccess, promise: onLoadSuccessPromise } = makeAsyncCallback();
const onItemClick = vi.fn();
const instance = createRef<{
linkService: React.RefObject<LinkService>;
pages: React.RefObject<HTMLDivElement[]>;
viewer: React.RefObject<{ scrollPageIntoView: (args: ScrollPageIntoViewArgs) => void }>;
}>();
await render(
<Document
file={pdfFile.file}
onItemClick={onItemClick}
onLoadSuccess={onLoadSuccess}
ref={instance}
/>,
);
if (!instance.current) {
throw new Error('Document ref is not set');
}
if (!instance.current.viewer.current) {
throw new Error('Viewer ref is not set');
}
expect.assertions(2);
await onLoadSuccessPromise;
const dest: number[] = [];
const pageIndex = 5;
const pageNumber = 6;
// Simulate clicking on an outline item
instance.current.viewer.current.scrollPageIntoView({ dest, pageIndex, pageNumber });
expect(onItemClick).toHaveBeenCalledTimes(1);
expect(onItemClick).toHaveBeenCalledWith({ dest, pageIndex, pageNumber });
});
it('attempts to find a page and scroll it into view if onItemClick is not given', async () => {
const { func: onLoadSuccess, promise: onLoadSuccessPromise } = makeAsyncCallback();
const instance = createRef<{
linkService: React.RefObject<LinkService>;
// biome-ignore lint/suspicious/noExplicitAny: Intentional use to simplify the test
pages: React.RefObject<any[]>;
viewer: React.RefObject<{ scrollPageIntoView: (args: ScrollPageIntoViewArgs) => void }>;
}>();
await 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 } = await 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 } = await 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 document (sample of mouse events family)', async () => {
const onClick = vi.fn();
const { container } = await render(<Document onClick={onClick} />);
const document = container.querySelector('.react-pdf__Document') as HTMLDivElement;
await userEvent.click(document);
expect(onClick).toHaveBeenCalled();
});
function triggerTouchStart(element: HTMLElement) {
element.dispatchEvent(new TouchEvent('touchstart', { bubbles: true, cancelable: true }));
}
it('calls onTouchStart callback when touched a document (sample of touch events family)', async () => {
const onTouchStart = vi.fn();
const { container } = await render(<Document onTouchStart={onTouchStart} />);
const document = container.querySelector('.react-pdf__Document') as HTMLDivElement;
triggerTouchStart(document);
expect(onTouchStart).toHaveBeenCalled();
});
it('does not warn if file prop was memoized', async () => {
const spy = vi.spyOn(globalThis.console, 'error').mockImplementation(() => {
// Intentionally empty
});
const file = { url: pdfFile.dataURI };
const { rerender } = await render(<Document file={file} />);
await rerender(<Document file={file} />);
expect(spy).not.toHaveBeenCalled();
vi.mocked(globalThis.console.error).mockReset();
});
it('warns if file prop was not memoized', async () => {
const spy = vi.spyOn(globalThis.console, 'error').mockImplementation(() => {
// Intentionally empty
});
const { rerender } = await render(<Document file={{ url: pdfFile.dataURI }} />);
await rerender(<Document file={{ url: pdfFile.dataURI }} />);
expect(spy).toHaveBeenCalledTimes(1);
vi.mocked(globalThis.console.error).mockReset();
});
it('does not warn if file prop was not memoized, but was changed', async () => {
const spy = vi.spyOn(globalThis.console, 'error').mockImplementation(() => {
// Intentionally empty
});
const { rerender } = await render(<Document file={{ url: pdfFile.dataURI }} />);
await rerender(<Document file={{ url: pdfFile2.dataURI }} />);
expect(spy).not.toHaveBeenCalled();
vi.mocked(globalThis.console.error).mockRestore();
});
it('does not warn if options prop was memoized', async () => {
const spy = vi.spyOn(globalThis.console, 'error').mockImplementation(() => {
// Intentionally empty
});
const options = {};
const { rerender } = await render(<Document file={pdfFile.blob} options={options} />);
await rerender(<Document file={pdfFile.blob} options={options} />);
expect(spy).not.toHaveBeenCalled();
vi.mocked(globalThis.console.error).mockRestore();
});
it('warns if options prop was not memoized', async () => {
const spy = vi.spyOn(globalThis.console, 'error').mockImplementation(() => {
// Intentionally empty
});
const { rerender } = await render(<Document file={pdfFile.blob} options={{}} />);
await rerender(<Document file={pdfFile.blob} options={{}} />);
expect(spy).toHaveBeenCalledTimes(1);
vi.mocked(globalThis.console.error).mockRestore();
});
it('does not warn if options prop was not memoized, but was changed', async () => {
const spy = vi.spyOn(globalThis.console, 'error').mockImplementation(() => {
// Intentionally empty
});
const { rerender } = await render(<Document file={pdfFile.blob} options={{}} />);
await rerender(<Document file={pdfFile.blob} options={{ maxImageSize: 100 }} />);
expect(spy).not.toHaveBeenCalled();
vi.mocked(globalThis.console.error).mockRestore();
});
it('does not throw an error on unmount', async () => {
const { func: onLoadProgress, promise: onLoadProgressPromise } = makeAsyncCallback();
const { unmount } = await render(<Document file={pdfFile} onLoadProgress={onLoadProgress} />);
await onLoadProgressPromise;
expect(unmount).not.toThrowError();
});
});
================================================
FILE: packages/react-pdf/src/Document.tsx
================================================
'use client';
import { forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useRef } from 'react';
import clsx from 'clsx';
import { dequal } from 'dequal';
import makeCancellable from 'make-cancellable-promise';
import makeEventProps from 'make-event-props';
import * as pdfjs from 'pdfjs-dist';
import invariant from 'tiny-invariant';
import warning from 'warning';
import DocumentContext from './DocumentContext.js';
import LinkService from './LinkService.js';
import Message from './Message.js';
import PasswordResponses from './PasswordResponses.js';
import useResolver from './shared/hooks/useResolver.js';
import {
cancelRunningTask,
dataURItoByteString,
displayCORSWarning,
isArrayBuffer,
isBlob,
isBrowser,
isDataURI,
loadFromFile,
} from './shared/utils.js';
import type { EventProps } from 'make-event-props';
import type { PDFDocumentProxy } from 'pdfjs-dist';
import type { DocumentInitParameters } from 'pdfjs-dist/types/src/display/api.js';
import type {
ClassName,
DocumentCallback,
DocumentContextType,
DocumentRenderProps,
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 | ((props: DocumentRenderProps) => 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);
* - `wasmUrl`;
* - `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 or use `useMemo` if you can't.
*
* @example { cMapUrl: '/cmaps/', wasmUrl: '/wasm/' }
*/
options?: Options;
/**
* Rendering mode of the document. Can be `"canvas"`, `"custom"` or `"none"``. If set to `"custom"`, `customRenderer` must also be provided.
*
* @default 'canvas'
* @example 'custom'
*/
renderMode?: RenderMode;
/**
* Rotation of the document in degrees. If provided, will change rotation globally, even for the pages which were given `rotate` prop of their own. `90` = rotated to the right, `180` = upside down, `270` = rotated to the left.
*
* @example 90
*/
rotate?: number | null;
/**
* Document scale.
*
* @default 1
* @example 0.5
*/
scale?: number;
} & EventProps<DocumentCallback | false | undefined>;
const defaultOnPassword: OnPassword = (callback, reason) => {
switch (reason) {
case PasswordResponses.NEED_PASSWORD: {
const password = prompt('Enter the password to open this PDF file.');
callback(password);
break;
}
case PasswordResponses.INCORRECT_PASSWORD: {
const password = prompt('Invalid password. Please try again.');
callback(password);
break;
}
default:
}
};
function isParameterObject(file: File): file is Source {
return (
typeof file === 'object' &&
file !== null &&
('data' in file || 'range' in file || 'url' in file)
);
}
/**
* Loads a document passed using `file` prop.
*/
const Document: React.ForwardRefExoticComponent<
DocumentProps &
React.RefAttributes<{
linkService: React.RefObject<LinkService>;
pages: React.RefObject<HTMLDivElement[]>;
viewer: React.RefObject<{ scrollPageIntoView: (args: ScrollPageIntoViewArgs) => void }>;
}>
> = forwardRef(function Document(
{
children,
className,
error = 'Failed to load PDF file.',
externalLinkRel,
externalLinkTarget,
file,
inputRef,
imageResourcesPath,
loading = 'Loading PDF…',
noData = 'No PDF file specified.',
onItemClick,
onLoadError: onLoadErrorProps,
onLoadProgress,
onLoadSuccess: onLoadSuccessProps,
onPassword = defaultOnPassword,
onSourceError: onSourceErrorProps,
onSourceSuccess: onSourceSuccessProps,
options,
renderMode,
rotate,
scale,
...otherProps
},
ref,
) {
const [sourceState, sourceDispatch] = useResolver<Source | null>();
const { value: source, error: sourceError } = sourceState;
const [pdfState, pdfDispatch] = useResolver<PDFDocumentProxy>();
const { value: pdf, error: pdfError } = pdfState;
const linkService = useRef(new LinkService());
const pages = useRef<HTMLDivElement[]>([]);
const prevFile = useRef<File | undefined>(undefined);
const prevOptions = useRef<Options | undefined>(undefined);
if (file && file !== prevFile.current && isParameterObject(file)) {
warning(
!dequal(file, prevFile.current),
`File prop passed to <Document /> changed, but it's equal to previous one. This might result in unnecessary reloads. Consider memoizing the value passed to "file" prop.`,
);
prevFile.current = file;
}
// Detect non-memoized changes in options prop
if (options && options !== prevOptions.current) {
warning(
!dequal(options, prevOptions.current),
`Options prop passed to <Document /> changed, but it's equal to previous one. This might result in unnecessary reloads. Consider memoizing the value passed to "options" prop.`,
);
prevOptions.current = options;
}
const viewer = useRef({
// Handling jumping to internal links target
scrollPageIntoView: (args: ScrollPageIntoViewArgs) => {
const { dest, pageNumber, pageIndex = pageNumber - 1 } = args;
// First, check if custom handling of onItemClick was provided
if (onItemClick) {
onItemClick({ dest, pageIndex, pageNumber });
return;
}
// If not, try to look for target page within the <Document>.
const page = pages.current[pageIndex];
if (page) {
// Scroll to the page automatically
page.scrollIntoView();
return;
}
warning(
false,
`An internal link leading to page ${pageNumber} was clicked, but neither <Document> was provided with onItemClick nor it was able to find the page within itself. Either provide onItemClick to <Document> and handle navigating by yourself or ensure that all pages are rendered within <Document>.`,
);
},
});
useImperativeHandle(
ref,
() => ({
linkService,
pages,
viewer,
}),
[],
);
/**
* Called when a document source is resolved correctly
*/
function onSourceSuccess() {
if (onSourceSuccessProps) {
onSourceSuccessProps();
}
}
/**
* Called when a document source failed to be resolved correctly
*/
function onSourceError() {
if (!sourceError) {
// Impossible, but TypeScript doesn't know that
return;
}
warning(false, sourceError.toString());
if (onSourceErrorProps) {
onSourceErrorProps(sourceError);
}
}
function resetSource() {
sourceDispatch({ type: 'RESET' });
}
// biome-ignore lint/correctness/useExhaustiveDependencies: See https://github.com/biomejs/biome/issues/3080
useEffect(resetSource, [file, sourceDispatch]);
const findDocumentSource = useCallback(async (): Promise<Source | null> => {
if (!file) {
return null;
}
// File is a string
if (typeof file === 'string') {
if (isDataURI(file)) {
const fileByteString = dataURItoByteString(file);
return { data: fileByteString };
}
displayCORSWarning();
return { url: file };
}
// File is PDFDataRangeTransport
if (file instanceof PDFDataRangeTransport) {
return { range: file };
}
// File is an ArrayBuffer
if (isArrayBuffer(file)) {
return { data: file };
}
/**
* The cases below are browser-only.
* If you're running on a non-browser environment, these cases will be of no use.
*/
if (isBrowser) {
// File is a Blob
if (isBlob(file)) {
const data = await loadFromFile(file);
return { data };
}
}
// At this point, file must be an object
invariant(
typeof file === 'object',
'Invalid parameter in file, need either Uint8Array, string or a parameter object',
);
invariant(
isParameterObject(file),
'Invalid parameter object: need either .data, .range or .url',
);
// File .url is a string
if ('url' in file && typeof file.url === 'string') {
if (isDataURI(file.url)) {
const { url, ...otherParams } = file;
const fileByteString = dataURItoByteString(url);
return { data: fileByteString, ...otherParams };
}
displayCORSWarning();
}
return file;
}, [file]);
useEffect(() => {
const cancellable = makeCancellable(findDocumentSource());
cancellable.promise
.then((nextSource) => {
sourceDispatch({ type: 'RESOLVE', value: nextSource });
})
.catch((error) => {
sourceDispatch({ type: 'REJECT', error });
});
return () => {
cancelRunningTask(cancellable);
};
}, [findDocumentSource, sourceDispatch]);
// biome-ignore lint/correctness/useExhaustiveDependencies: Omitted callbacks so they are not called every time they change
useEffect(() => {
if (typeof source === 'undefined') {
return;
}
if (source === false) {
onSourceError();
return;
}
onSourceSuccess();
}, [source]);
/**
* Called when a document is read successfully
*/
function onLoadSuccess() {
if (!pdf) {
// Impossible, but TypeScript doesn't know that
return;
}
if (onLoadSuccessProps) {
onLoadSuccessProps(pdf);
}
pages.current = new Array(pdf.numPages);
linkService.current.setDocument(pdf);
}
/**
* Called when a document failed to read successfully
*/
function onLoadError() {
if (!pdfError) {
// Impossible, but TypeScript doesn't know that
return;
}
warning(false, pdfError.toString());
if (onLoadErrorProps) {
onLoadErrorProps(pdfError);
}
}
// biome-ignore lint/correctness/useExhaustiveDependencies: useEffect intentionally triggered on source change
useEffect(
function resetDocument() {
pdfDispatch({ type: 'RESET' });
},
[pdfDispatch, source],
);
// biome-ignore lint/correctness/useExhaustiveDependencies: Omitted callbacks so they are not called every time they change
useEffect(
function loadDocument() {
if (!source) {
return;
}
const documentInitParams: DocumentInitParameters = options
? { ...source, ...options }
: source;
const destroyable = pdfjs.getDocument(documentInitParams);
if (onLoadProgress) {
destroyable.onProgress = onLoadProgress;
}
if (onPassword) {
destroyable.onPassword = onPassword;
}
const loadingTask = destroyable;
loadingTask.promise
.then((nextPdf) => {
if (loadingTask.destroyed) {
return;
}
pdfDispatch({ type: 'RESOLVE', value: nextPdf });
})
.catch((error) => {
if (loadingTask.destroyed) {
return;
}
pdfDispatch({ type: 'REJECT', error });
});
return () => {
loadingTask.destroy();
};
},
[options, pdfDispatch, source],
);
// biome-ignore lint/correctness/useExhaustiveDependencies: Omitted callbacks so they are not called every time they change
useEffect(() => {
if (typeof pdf === 'undefined') {
return;
}
if (pdf === false) {
onLoadError();
return;
}
onLoadSuccess();
}, [pdf]);
useEffect(
function setupLinkService() {
linkService.current.setViewer(viewer.current);
linkService.current.setExternalLinkRel(externalLinkRel);
linkService.current.setExternalLinkTarget(externalLinkTarget);
},
[externalLinkRel, externalLinkTarget],
);
const registerPage = useCallback((pageIndex: number, ref: HTMLDivElement) => {
pages.current[pageIndex] = ref;
}, []);
const unregisterPage = useCallback((pageIndex: number) => {
delete pages.current[pageIndex];
}, []);
const childContext = useMemo(
() => ({
imageResourcesPath,
linkService: linkService.current,
onItemClick,
pdf,
registerPage,
renderMode,
rotate,
scale,
unregisterPage,
}),
[imageResourcesPath, onItemClick, pdf, registerPage, renderMode, rotate, scale, unregisterPage],
);
const eventProps = useMemo(
() => makeEventProps(otherProps, () => pdf),
// biome-ignore lint/correctness/useExhaustiveDependencies: FIXME
[otherProps, pdf],
);
function renderChildren() {
function isFulfilledContext(context: DocumentContextType): context is DocumentRenderProps {
return Boolean(context?.pdf);
}
if (!isFulfilledContext(childContext)) {
// Impossible, but TypeScript doesn't know that
throw new Error('pdf is undefined');
}
const resolvedChildren = typeof children === 'function' ? children(childContext) : children;
return (
<DocumentContext.Provider value={childContext}>{resolvedChildren}</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>}
{...eventProps}
>
{renderContent()}
</div>
);
});
export default Document;
================================================
FILE: packages/react-pdf/src/DocumentContext.tsx
================================================
'use client';
import { createContext } from 'react';
import type { DocumentContextType } from './shared/types.js';
const documentContext: React.Context<DocumentContextType> =
createContext<DocumentContextType>(null);
export default documentContext;
================================================
FILE: packages/react-pdf/src/LinkService.ts
================================================
/* 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 { IPDFLinkService } from 'pdfjs-dist/types/web/interfaces.js';
import type {
Dest,
ExternalLinkRel,
ExternalLinkTarget,
ResolvedDest,
ScrollPageIntoViewArgs,
} from './shared/types.js';
const DEFAULT_LINK_REL = 'noopener noreferrer nofollow';
type PDFViewer = {
currentPageNumber?: number;
scrollPageIntoView: (args: ScrollPageIntoViewArgs) => void;
};
export default class LinkService implements IPDFLinkService {
externalLinkEnabled: boolean;
externalLinkRel?: ExternalLinkRel;
externalLinkTarget?: ExternalLinkTarget;
isInPresentationMode: boolean;
pdfDocument?: PDFDocumentProxy | null;
pdfViewer?: PDFViewer | null;
constructor() {
this.externalLinkEnabled = true;
this.externalLinkRel = undefined;
this.externalLinkTarget = undefined;
this.isInPresentationMode = false;
this.pdfDocument = undefined;
this.pdfViewer = undefined;
}
setDocument(pdfDocument: PDFDocumentProxy): void {
this.pdfDocument = pdfDocument;
}
setViewer(pdfViewer: PDFViewer): void {
this.pdfViewer = pdfViewer;
}
setExternalLinkRel(externalLinkRel?: ExternalLinkRel): void {
this.externalLinkRel = externalLinkRel;
}
setExternalLinkTarget(externalLinkTarget?: ExternalLinkTarget): void {
this.externalLinkTarget = externalLinkTarget;
}
setHash(): void {
// Intentionally empty
}
setHistory(): void {
// Intentionally empty
}
get pagesCount(): number {
return this.pdfDocument ? this.pdfDocument.numPages : 0;
}
get page(): number {
invariant(this.pdfViewer, 'PDF viewer is not initialized.');
return this.pdfViewer.currentPageNumber || 0;
}
set page(value: number) {
invariant(this.pdfViewer, 'PDF viewer is not initialized.');
this.pdfViewer.currentPageNumber = value;
}
get rotation(): number {
return 0;
}
set rotation(_value) {
// Intentionally empty
}
addLinkAttributes(link: HTMLAnchorElement, url: string, newWindow: boolean): void {
link.href = url;
link.rel = this.externalLinkRel || DEFAULT_LINK_REL;
link.target = newWindow ? '_blank' : this.externalLinkTarget || '';
}
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,
});
});
});
}
goToPage(pageNumber: number): void {
const pageIndex = pageNumber - 1;
invariant(this.pdfViewer, 'PDF viewer is not initialized.');
invariant(
pageNumber >= 1 && pageNumber <= this.pagesCount,
`"${pageNumber}" is not a valid page number.`,
);
this.pdfViewer.scrollPageIntoView({
pageIndex,
pageNumber,
});
}
goToXY(): void {
// Intentionally empty
}
cachePageRef(): void {
// Intentionally empty
}
getDestinationHash(): string {
return '#';
}
getAnchorUrl(): string {
return '#';
}
executeNamedAction(): void {
// Intentionally empty
}
executeSetOCGState(): void {
// Intentionally empty
}
isPageVisible(): boolean {
return true;
}
isPageCached(): boolean {
return true;
}
navigateTo(dest: Dest): void {
this.goToDestination(dest);
}
}
================================================
FILE: packages/react-pdf/src/Message.tsx
================================================
type MessageProps = {
children?: React.ReactNode;
type: 'error' | 'loading' | 'no-data';
};
export default function Message({ children, type }: MessageProps): React.ReactElement {
return <div className={`react-pdf__message react-pdf__message--${type}`}>{children}</div>;
}
================================================
FILE: packages/react-pdf/src/Outline.spec.tsx
================================================
import { beforeAll, describe, expect, it } from 'vitest';
import { page } from 'vitest/browser';
import { render } from 'vitest-browser-react';
import { createRef } from 'react';
import DocumentContext from './DocumentContext.js';
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 type { PDFDocumentProxy } from 'pdfjs-dist';
import type { DocumentContextType } from './shared/types.js';
type PDFOutline = Awaited<ReturnType<PDFDocumentProxy['getOutline']>>;
const pdfFile = await loadPDF('../../__mocks__/_pdf.pdf');
const pdfFile2 = await loadPDF('../../__mocks__/_pdf2.pdf');
async function renderWithContext(children: React.ReactNode, context: Partial<DocumentContextType>) {
const { rerender, ...otherResult } = await render(
<DocumentContext.Provider value={context as DocumentContextType}>
{children}
</DocumentContext.Provider>,
);
return {
...otherResult,
rerender: async (
nextChildren: React.ReactNode,
nextContext: Partial<DocumentContextType> = context,
) =>
await 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();
await 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();
await 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();
await 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 } = await renderWithContext(<Outline onLoadSuccess={onLoadSuccess} />, {
pdf,
});
expect.assertions(2);
await expect(onLoadSuccessPromise).resolves.toMatchObject([desiredLoadedOutline]);
const { func: onLoadSuccess2, promise: onLoadSuccessPromise2 } = makeAsyncCallback();
await 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', async () => {
muteConsole();
await expect(render(<Outline />)).rejects.toThrowError(
'Invariant failed: Attempted to load an outline, but no document was specified. Wrap <Outline /> in a <Document /> or pass explicit `pdf` prop.',
);
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 } = await 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>();
await 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();
await renderWithContext(<Outline onLoadSuccess={onLoadSuccess} />, { pdf });
expect.assertions(1);
await onLoadSuccessPromise;
const items = page.getByRole('listitem');
expect(items).toHaveLength(5);
});
});
});
================================================
FILE: packages/react-pdf/src/Outline.tsx
================================================
'use client';
import { useEffect, useMemo } from 'react';
import clsx from 'clsx';
import makeCancellable from 'make-cancellable-promise';
import makeEventProps from 'make-event-props';
import invariant from 'tiny-invariant';
import warning from 'warning';
import OutlineContext from './OutlineContext.js';
import OutlineItem from './OutlineItem.js';
import useDocumentContext from './shared/hooks/useDocumentContext.js';
import useResolver from './shared/hooks/useResolver.js';
import { cancelRunningTask } from './shared/utils.js';
import type { EventProps } from 'make-event-props';
import type { PDFDocumentProxy } from 'pdfjs-dist';
import type { ClassName, OnItemClickArgs } from './shared/types.js';
type PDFOutline = Awaited<ReturnType<PDFDocumentProxy['getOutline']>>;
export type OutlineProps = {
/**
* Class name(s) that will be added to rendered element along with the default `react-pdf__Outline`.
*
* @example 'custom-class-name-1 custom-class-name-2'
* @example ['custom-class-name-1', 'custom-class-name-2']
*/
className?: ClassName;
/**
* A prop that behaves like [ref](https://reactjs.org/docs/refs-and-the-dom.html), but it's passed to main `<div>` rendered by `<Outline>` component.
*
* @example (ref) => { this.myOutline = ref; }
* @example this.ref
* @example ref
*/
inputRef?: React.Ref<HTMLDivElement>;
/**
* Function called when an outline item has been clicked. Usually, you would like to use this callback to move the user wherever they requested to.
*
* @example ({ dest, pageIndex, pageNumber }) => alert('Clicked an item from page ' + pageNumber + '!')
*/
onItemClick?: (props: OnItemClickArgs) => void;
/**
* Function called in case of an error while retrieving the outline.
*
* @example (error) => alert('Error while retrieving the outline! ' + error.message)
*/
onLoadError?: (error: Error) => void;
/**
* Function called when the outline is successfully retrieved.
*
* @example (outline) => alert('The outline has been successfully retrieved.')
*/
onLoadSuccess?: (outline: PDFOutline | null) => void;
pdf?: PDFDocumentProxy | false;
} & EventProps<PDFOutline | null | false | undefined>;
/**
* Displays an outline (table of contents).
*
* Should be placed inside `<Document />`. Alternatively, it can have `pdf` prop passed, which can be obtained from `<Document />`'s `onLoadSuccess` callback function.
*/
export default function Outline(props: OutlineProps): React.ReactElement | null {
const documentContext = useDocumentContext();
const mergedProps = { ...documentContext, ...props };
const {
className,
inputRef,
onItemClick,
onLoadError: onLoadErrorProps,
onLoadSuccess: onLoadSuccessProps,
pdf,
...otherProps
} = mergedProps;
invariant(
pdf,
'Attempted to load an outline, but no document was specified. Wrap <Outline /> in a <Document /> or pass explicit `pdf` prop.',
);
const [outlineState, outlineDispatch] = useResolver<PDFOutline | null>();
const { value: outline, error: outlineError } = outlineState;
/**
* Called when an outline is read successfully
*/
function onLoadSuccess() {
if (typeof outline === 'undefined' || outline === false) {
return;
}
if (onLoadSuccessProps) {
onLoadSuccessProps(outline);
}
}
/**
* Called when an outline failed to read successfully
*/
function onLoadError() {
if (!outlineError) {
// Impossible, but TypeScript doesn't know that
return;
}
warning(false, outlineError.toString());
if (onLoadErrorProps) {
onLoadErrorProps(outlineError);
}
}
// biome-ignore lint/correctness/useExhaustiveDependencies: useEffect intentionally triggered on pdf change
useEffect(
function resetOutline() {
outlineDispatch({ type: 'RESET' });
},
[outlineDispatch, pdf],
);
useEffect(
function loadOutline() {
const cancellable = makeCancellable(pdf.getOutline());
const runningTask = cancellable;
cancellable.promise
.then((nextOutline) => {
outlineDispatch({ type: 'RESOLVE', value: nextOutline });
})
.catch((error) => {
outlineDispatch({ type: 'REJECT', error });
});
return () => cancelRunningTask(runningTask);
},
[outlineDispatch, pdf],
);
// biome-ignore lint/correctness/useExhaustiveDependencies: Omitted callbacks so they are not called every time they change
useEffect(() => {
if (outline === undefined) {
return;
}
if (outline === false) {
onLoadError();
return;
}
onLoadSuccess();
}, [outline]);
const childContext = useMemo(
() => ({
onItemClick,
}),
[onItemClick],
);
const eventProps = useMemo(
() => makeEventProps(otherProps, () => outline),
// biome-ignore lint/correctness/useExhaustiveDependencies: FIXME
[otherProps, outline],
);
if (!outline) {
return null;
}
function renderOutline() {
if (!outline) {
return null;
}
return (
<ul>
{outline.map((item, itemIndex) => (
<OutlineItem
key={typeof item.dest === 'string' ? item.dest : itemIndex}
item={item}
pdf={pdf}
/>
))}
</ul>
);
}
return (
<div className={clsx('react-pdf__Outline', className)} ref={inputRef} {...eventProps}>
<OutlineContext.Provider value={childContext}>{renderOutline()}</OutlineContext.Provider>
</div>
);
}
================================================
FILE: packages/react-pdf/src/OutlineContext.tsx
================================================
'use client';
import { createContext } from 'react';
import type { OutlineContextType } from './shared/types.js';
const outlineContext: React.Context<OutlineContextType> = createContext<OutlineContextType>(null);
export default outlineContext;
================================================
FILE: packages/react-pdf/src/OutlineItem.spec.tsx
================================================
import { beforeAll, describe, expect, it, vi } from 'vitest';
import { page, userEvent } from 'vitest/browser';
import { render } from 'vitest-browser-react';
import DocumentContext from './DocumentContext.js';
import { pdfjs } from './index.test.js';
import OutlineContext from './OutlineContext.js';
import OutlineItem from './OutlineItem.js';
import { loadPDF, makeAsyncCallback } from '../../../test-utils.js';
import type { PDFDocumentProxy } from 'pdfjs-dist';
import type { DocumentContextType, OutlineContextType } from './shared/types.js';
const pdfFile = await loadPDF('../../__mocks__/_pdf.pdf');
type PDFOutline = Awaited<ReturnType<PDFDocumentProxy['getOutline']>>;
type PDFOutlineItem = PDFOutline[number];
async function renderWithContext(
children: React.ReactNode,
documentContext: Partial<DocumentContextType>,
outlineContext: Partial<OutlineContextType>,
) {
const { rerender, ...otherResult } = await render(
<DocumentContext.Provider value={documentContext as DocumentContextType}>
<OutlineContext.Provider value={outlineContext as OutlineContextType}>
{children}
</OutlineContext.Provider>
</DocumentContext.Provider>,
);
return {
...otherResult,
rerender: async (
nextChildren: React.ReactNode,
nextDocumentContext: Partial<DocumentContextType> = documentContext,
nextOutlineContext: Partial<OutlineContextType> = outlineContext,
) =>
await 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', async () => {
const onItemClick = vi.fn();
await renderWithContext(<OutlineItem item={outlineItem} />, { pdf }, { onItemClick });
const item = page.getByRole('listitem').first();
expect(item).toHaveTextContent(outlineItem.title);
});
it("renders item's subitems properly", async () => {
const onItemClick = vi.fn();
await renderWithContext(<OutlineItem item={outlineItem} />, { pdf }, { onItemClick });
const item = page.getByRole('listitem').first();
const subitems = item.getByRole('listitem');
expect(subitems).toHaveLength(outlineItem.items.length);
});
it('calls onItemClick with proper arguments when clicked a link', async () => {
const { func: onItemClick, promise: onItemClickPromise } = makeAsyncCallback();
await renderWithContext(<OutlineItem item={outlineItem} />, { pdf }, { onItemClick });
const item = page.getByRole('listitem').first();
const link = item.getByRole('link').first();
await userEvent.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 } = await renderWithContext(
<OutlineItem item={outlineItem} />,
{ pdf },
{ onItemClick },
);
const item = page.getByRole('listitem').first();
const link = item.getByRole('link').first();
await userEvent.click(link);
await onItemClickPromise;
expect(onItemClick).toHaveBeenCalledTimes(1);
const { func: onItemClick2, promise: onItemClickPromise2 } = makeAsyncCallback();
await rerender(<OutlineItem item={outlineItem} />, { pdf }, { onItemClick: onItemClick2 });
await userEvent.click(link);
await onItemClickPromise2;
expect(onItemClick2).toHaveBeenCalledTimes(1);
});
});
});
================================================
FILE: packages/react-pdf/src/OutlineItem.tsx
================================================
import invariant from 'tiny-invariant';
import Ref from './Ref.js';
import useCachedValue from './shared/hooks/useCachedValue.js';
import useDocumentContext from './shared/hooks/useDocumentContext.js';
import useOutlineContext from './shared/hooks/useOutlineContext.js';
import type { PDFDocumentProxy } from 'pdfjs-dist';
import type { RefProxy } from 'pdfjs-dist/types/src/display/api.js';
type PDFOutline = Awaited<ReturnType<PDFDocumentProxy['getOutline']>>;
type PDFOutlineItem = PDFOutline[number];
type OutlineItemProps = {
item: PDFOutlineItem;
pdf?: PDFDocumentProxy | false;
};
export default function OutlineItem(props: OutlineItemProps): React.ReactElement {
const documentContext = useDocumentContext();
const outlineContext = useOutlineContext();
invariant(outlineContext, 'Unable to find Outline context.');
const mergedProps = { ...documentContext, ...outlineContext, ...props };
const { item, linkService, onItemClick, pdf, ...otherProps } = mergedProps;
invariant(
pdf,
'Attempted to load an outline, but no document was specified. Wrap <Outline /> in a <Document /> or pass explicit `pdf` prop.',
);
const getDestination = useCachedValue(() => {
if (typeof item.dest === 'string') {
return pdf.getDestination(item.dest);
}
return item.dest;
});
const getPageIndex = useCachedValue(async () => {
const destination = await getDestination();
if (!destination) {
throw new Error('Destination not found.');
}
const [ref] = destination as [RefProxy];
return pdf.getPageIndex(new Ref(ref));
});
const getPageNumber = useCachedValue(async () => {
const pageIndex = await getPageIndex();
return pageIndex + 1;
});
function onClick(event: React.MouseEvent<HTMLAnchorElement>) {
event.preventDefault();
invariant(
onItemClick || linkService,
'Either onItemClick callback or linkService must be defined in order to navigate to an outline item.',
);
if (onItemClick) {
Promise.all([getDestination(), getPageIndex(), getPageNumber()]).then(
([dest, pageIndex, pageNumber]) => {
onItemClick({
dest,
pageIndex,
pageNumber,
});
},
);
} else if (linkService) {
linkService.goToDestination(item.dest);
}
}
function renderSubitems() {
if (!item.items || !item.items.length) {
return null;
}
const { items: subitems } = item;
return (
<ul>
{subitems.map((subitem, subitemIndex) => (
<OutlineItem
key={typeof subitem.dest === 'string' ? subitem.dest : subitemIndex}
item={subitem}
pdf={pdf}
{...otherProps}
/>
))}
</ul>
);
}
return (
<li>
{/* biome-ignore lint/a11y/useValidAnchor: We can't provide real href here */}
<a href="#" onClick={onClick}>
{item.title}
</a>
{renderSubitems()}
</li>
);
}
================================================
FILE: packages/react-pdf/src/Page/AnnotationLayer.css
================================================
/* Copyright 2014 Mozilla Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
:root {
--react-pdf-annotation-layer: 1;
--annotation-unfocused-field-background: url("data:image/svg+xml;charset=UTF-8,<svg width='1px' height='1px' xmlns='http://www.w3.org/2000/svg'><rect width='100%' height='100%' style='fill:rgba(0, 54, 255, 0.13);'/></svg>");
--input-focus-border-color: Highlight;
--input-focus-outline: 1px solid Canvas;
--input-unfocused-border-color: transparent;
--input-disabled-border-color: transparent;
--input-hover-border-color: black;
--link-outline: none;
}
@media screen and (forced-colors: active) {
:root {
--input-focus-border-color: CanvasText;
--input-unfocused-border-color: ActiveText;
--input-disabled-border-color: GrayText;
--input-hover-border-color: Highlight;
--link-outline: 1.5px solid LinkText;
}
.annotationLayer .textWidgetAnnotation :is(input, textarea):required,
.annotationLayer .choiceWidgetAnnotation select:required,
.annotationLayer .buttonWidgetAnnotation:is(.checkBox, .radioButton) input:required {
outline: 1.5px solid selectedItem;
}
.annotationLayer .linkAnnotation:hover {
backdrop-filter: invert(100%);
}
}
.annotationLayer {
position: absolute;
top: 0;
left: 0;
pointer-events: none;
transform-origin: 0 0;
z-index: 3;
}
.annotationLayer[data-main-rotation='90'] .norotate {
transform: rotate(270deg) translateX(-100%);
}
.annotationLayer[data-main-rotation='180'] .norotate {
transform: rotate(180deg) translate(-100%, -100%);
}
.annotationLayer[data-main-rotation='270'] .norotate {
transform: rotate(90deg) translateY(-100%);
}
.annotationLayer canvas {
position: absolute;
width: 100%;
height: 100%;
}
.annotationLayer section {
position: absolute;
text-align: initial;
pointer-events: auto;
box-sizing: border-box;
margin: 0;
transform-origin: 0 0;
}
.annotationLayer .linkAnnotation {
outline: var(--link-outline);
}
.textLayer.selecting ~ .annotationLayer section {
pointer-events: none;
}
.annotationLayer :is(.linkAnnotation, .buttonWidgetAnnotation.pushButton) > a {
position: absolute;
font-size: 1em;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
.annotationLayer :is(.linkAnnotation, .buttonWidgetAnnotation.pushButton) > a:hover {
opacity: 0.2;
background: rgba(255, 255, 0, 1);
box-shadow: 0 2px 10px rgba(255, 255, 0, 1);
}
.annotationLayer .textAnnotation img {
position: absolute;
cursor: pointer;
width: 100%;
height: 100%;
top: 0;
left: 0;
}
.annotationLayer .textWidgetAnnotation :is(input, textarea),
.annotationLayer .choiceWidgetAnnotation select,
.annotationLayer .buttonWidgetAnnotation:is(.checkBox, .radioButton) input {
background-image: var(--annotation-unfocused-field-background);
border: 2px solid var(--input-unfocused-border-color);
box-sizing: border-box;
font: calc(9px * var(--total-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(--total-scale-factor));
width: 100%;
min-width: calc(180px * var(--total-scale-factor));
pointer-events: none;
}
.annotationLayer .popup {
position: absolute;
max-width: calc(180px * var(--total-scale-factor));
background-color: rgba(255, 255, 153, 1);
box-shadow: 0 calc(2px * var(--total-scale-factor)) calc(5px * var(--total-scale-factor))
rgba(136, 136, 136, 1);
border-radius: calc(2px * var(--total-scale-factor));
padding: calc(6px * var(--total-scale-factor));
margin-left: calc(5px * var(--total-scale-factor));
cursor: pointer;
font: message-box;
white-space: normal;
word-wrap: break-word;
pointer-events: auto;
}
.annotationLayer .popup > * {
font-size: calc(9px * var(--total-scale-factor));
}
.annotationLayer .popup h1 {
display: inline-block;
}
.annotationLayer .popupDate {
display: inline-block;
margin-left: calc(5px * var(--total-scale-factor));
}
.annotationLayer .popupContent {
border-top: 1px solid rgba(51, 51, 51, 1);
margin-top: calc(2px * var(--total-scale-factor));
padding-top: calc(2px * var(--total-scale-factor));
}
.annotationLayer .richText > * {
white-space: pre-wrap;
font-size: calc(9px * var(--total-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;
}
================================================
FILE: packages/react-pdf/src/Page/AnnotationLayer.spec.tsx
================================================
import { beforeAll, describe, expect, it } from 'vitest';
import { render } from 'vitest-browser-react';
import DocumentContext from '../DocumentContext.js';
import { pdfjs } from '../index.test.js';
import LinkService from '../LinkService.js';
import PageContext from '../PageContext.js';
import AnnotationLayer from './AnnotationLayer.js';
import failingPage from '../../../../__mocks__/_failing_page.js';
import { loadPDF, makeAsyncCallback, muteConsole, restoreConsole } from '../../../../test-utils.js';
import type { PDFDocumentProxy, PDFPageProxy } from 'pdfjs-dist';
import type { RenderResult } from 'vitest-browser-react';
import type { Annotations, DocumentContextType, PageContextType } from '../shared/types.js';
const pdfFile = await loadPDF('../../__mocks__/_pdf.pdf');
const annotatedPdfFile = await loadPDF('../../__mocks__/_pdf3.pdf');
async function renderWithContext(
children: React.ReactNode,
documentContext: Partial<DocumentContextType>,
pageContext: Partial<PageContextType>,
) {
const { rerender, ...otherResult } = await render(
<DocumentContext.Provider value={documentContext as DocumentContextType}>
<PageContext.Provider value={pageContext as PageContextType}>{children}</PageContext.Provider>
</DocumentContext.Provider>,
);
const customRerender = async (
nextChildren: React.ReactNode,
nextDocumentContext: Partial<DocumentContextType> = documentContext,
nextPageContext: Partial<PageContextType> = pageContext,
) =>
await 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();
await 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();
await 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 } = await renderWithContext(
<AnnotationLayer />,
{
linkService,
pdf,
},
{
onGetAnnotationsSuccess,
page,
},
);
expect.assertions(2);
await expect(onGetAnnotationsSuccessPromise).resolves.toMatchObject([desiredAnnotations]);
const { func: onGetAnnotationsSuccess2, promise: onGetAnnotationsSuccessPromise2 } =
makeAsyncCallback();
await rerender(
<AnnotationLayer />,
{
linkService,
pdf,
},
{
onGetAnnotationsSuccess: onGetAnnotationsSuccess2,
page: page2,
},
);
await expect(onGetAnnotationsSuccessPromise2).resolves.toMatchObject([desiredAnnotations2]);
});
it('throws an error when placed outside Page', async () => {
muteConsole();
await expect(render(<AnnotationLayer />)).rejects.toThrowError(
'Invariant failed: Unable to find Page context.',
);
restoreConsole();
});
});
describe('rendering', () => {
it('renders annotations properly', async () => {
const {
func: onRenderAnnotationLayerSuccess,
promise: onRenderAnnotationLayerSuccessPromise,
} = makeAsyncCallback();
const { container } = await 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 } = await renderWithContext(
<AnnotationLayer />,
{
linkService: customLinkService,
pdf,
},
{
onRenderAnnotationLayerSuccess,
page,
},
);
expect.assertions(desiredAnnotations.length);
await onRenderAnnotationLayerSuccessPromise;
const wrapper = container.firstElementChild as HTMLDivElement;
const annotationItems = Array.from(wrapper.children);
const annotationLinkItems = annotationItems
.map((item) => item.firstChild as HTMLElement)
.filter((item) => item.tagName === 'A');
for (const link of annotationLinkItems) {
expect(link).toHaveAttribute('target', 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 } = await renderWithContext(
<AnnotationLayer />,
{
linkService: customLinkService,
pdf,
},
{
onRenderAnnotationLayerSuccess,
page,
},
);
expect.assertions(desiredAnnotations.length);
await onRenderAnnotationLayerSuccessPromise;
const wrapper = container.firstElementChild as HTMLDivElement;
const annotationItems = Array.from(wrapper.children);
const annotationLinkItems = annotationItems
.map((item) => item.firstChild as HTMLElement)
.filter((item) => item.tagName === 'A');
for (const link of annotationLinkItems) {
expect(link).toHaveAttribute('rel', 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 } = await 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 } = await renderWithContext(
<AnnotationLayer />,
{
imageResourcesPath,
linkService,
pdf,
},
{
onRenderAnnotationLayerSuccess,
page: annotatedPage,
},
);
expect.assertions(1);
await onRenderAnnotationLayerSuccessPromise;
const stringifiedAnnotationLayerNode = container.outerHTML;
expect(stringifiedAnnotationLayerNode).toMatch(desiredImageTagRegExp);
});
});
});
================================================
FILE: packages/react-pdf/src/Page/AnnotationLayer.tsx
================================================
'use client';
import { useEffect, useMemo, useRef } from 'react';
import clsx from 'clsx';
import makeCancellable from 'make-cancellable-promise';
import * as pdfjs from 'pdfjs-dist';
import invariant from 'tiny-invariant';
import warning from 'warning';
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 { AnnotationLayerParameters } from 'pdfjs-dist/types/src/display/annotation_layer.js';
import type { Annotations } from '../shared/types.js';
export default function AnnotationLayer(): React.ReactElement {
const documentContext = useDocumentContext();
const pageContext = usePageContext();
invariant(pageContext, 'Unable to find Page context.');
const mergedProps = { ...documentContext, ...pageContext };
const {
filterAnnotations,
imageResourcesPath,
linkService,
onGetAnnotationsError: onGetAnnotationsErrorProps,
onGetAnnotationsSuccess: onGetAnnotationsSuccessProps,
onRenderAnnotationLayerError: onRenderAnnotationLayerErrorProps,
onRenderAnnotationLayerSuccess: onRenderAnnotationLayerSuccessProps,
page,
pdf,
renderForms,
rotate,
scale = 1,
} = mergedProps;
invariant(
pdf,
'Attempted to load page annotations, but no document was specified. Wrap <Page /> in a <Document /> or pass explicit `pdf` prop.',
);
invariant(page, 'Attempted to load page annotations, but no page was specified.');
invariant(linkService, 'Attempted to load page annotations, but no linkService was specified.');
const [annotationsState, annotationsDispatch] = useResolver<Annotations>();
const { value: annotations, error: annotationsError } = annotationsState;
const layerElement = useRef<HTMLDivElement>(null);
warning(
Number.parseInt(
window.getComputedStyle(document.body).getPropertyValue('--react-pdf-annotation-layer'),
10,
) === 1,
'AnnotationLayer styles not found. Read more: https://github.com/wojtekmaj/react-pdf#support-for-annotations',
);
function onLoadSuccess() {
if (!annotations) {
// Impossible, but TypeScript doesn't know that
return;
}
if (onGetAnnotationsSuccessProps) {
onGetAnnotationsSuccessProps(annotations);
}
}
function onLoadError() {
if (!annotationsError) {
// Impossible, but TypeScript doesn't know that
return;
}
warning(false, annotationsError.toString());
if (onGetAnnotationsErrorProps) {
onGetAnnotationsErrorProps(annotationsError);
}
}
// biome-ignore lint/correctness/useExhaustiveDependencies: useEffect intentionally triggered on page change
useEffect(
function resetAnnotations() {
annotationsDispatch({ type: 'RESET' });
},
[annotationsDispatch, page],
);
useEffect(
function loadAnnotations() {
if (!page) {
return;
}
const cancellable = makeCancellable(page.getAnnotations());
const runningTask = cancellable;
cancellable.promise
.then((nextAnnotations) => {
annotationsDispatch({ type: 'RESOLVE', value: nextAnnotations });
})
.catch((error) => {
annotationsDispatch({ type: 'REJECT', error });
});
return () => {
cancelRunningTask(runningTask);
};
},
[annotationsDispatch, page],
);
// biome-ignore lint/correctness/useExhaustiveDependencies: Omitted callbacks so they are not called every time they change
useEffect(() => {
if (annotations === undefined) {
return;
}
if (annotations === false) {
onLoadError();
return;
}
onLoadSuccess();
}, [annotations]);
function onRenderSuccess() {
if (onRenderAnnotationLayerSuccessProps) {
onRenderAnnotationLayerSuccessProps();
}
}
function onRenderError(error: unknown) {
warning(false, `${error}`);
if (onRenderAnnotationLayerErrorProps) {
onRenderAnnotationLayerErrorProps(error);
}
}
const viewport = useMemo(
() => page.getViewport({ scale, rotation: rotate }),
[page, rotate, scale],
);
// biome-ignore lint/correctness/useExhaustiveDependencies: Omitted callbacks so they are not called every time they change
useEffect(
function renderAnnotationLayer() {
if (!pdf || !page || !linkService || !annotations) {
return;
}
const { current: layer } = layerElement;
if (!layer) {
return;
}
const clonedViewport = viewport.clone({ dontFlip: true });
const annotationLayerParameters = {
accessibilityManager: null, // TODO: Implement this
annotationCanvasMap: null, // TODO: Implement this
annotationEditorUIManager: null, // TODO: Implement this
annotationStorage: pdf.annotationStorage,
commentManager: null, // TODO: Implement this
div: layer,
l10n: null, // TODO: Implement this
linkService,
page,
structTreeLayer: null, // TODO: Implement this
viewport: clonedViewport,
};
const renderParameters: AnnotationLayerParameters = {
annotations: filterAnnotations ? filterAnnotations({ annotations }) : annotations,
annotationStorage: pdf.annotationStorage,
div: layer,
imageResourcesPath,
linkService,
page,
renderForms,
viewport: clonedViewport,
};
layer.innerHTML = '';
try {
new pdfjs.AnnotationLayer(annotationLayerParameters).render(renderParameters);
// Intentional immediate callback
onRenderSuccess();
} catch (error) {
onRenderError(error);
}
return () => {
// TODO: Cancel running task?
};
},
[
annotations,
filterAnnotations,
imageResourcesPath,
linkService,
page,
pdf,
renderForms,
viewport,
],
);
return (
<div className={clsx('react-pdf__Page__annotations', 'annotationLayer')} ref={layerElement} />
);
}
================================================
FILE: packages/react-pdf/src/Page/Canvas.spec.tsx
================================================
import { beforeAll, describe, expect, it, vi } from 'vitest';
import { render } from 'vitest-browser-react';
import { pdfjs } from '../index.test.js';
import PageContext from '../PageContext.js';
import Canvas from './Canvas.js';
import failingPage from '../../../../__mocks__/_failing_page.js';
import { loadPDF, makeAsyncCallback, muteConsole, restoreConsole } from '../../../../test-utils.js';
import type { PDFPageProxy } from 'pdfjs-dist';
import type { PageContextType } from '../shared/types.js';
const pdfFile = await loadPDF('../../__mocks__/_pdf.pdf');
async function renderWithContext(children: React.ReactNode, context: Partial<PageContextType>) {
const { rerender, ...otherResult } = await render(
<PageContext.Provider value={context as PageContextType}>{children}</PageContext.Provider>,
);
return {
...otherResult,
rerender: async (
nextChildren: React.ReactNode,
nextContext: Partial<PageContextType> = context,
) =>
await rerender(
<PageContext.Provider value={nextContext as PageContextType}>
{nextChildren}
</PageContext.Provider>,
),
};
}
describe('Canvas', () => {
// Loaded page
let page: PDFPageProxy;
let pageWithRendererMocked: PDFPageProxy;
beforeAll(async () => {
const pdf = await pdfjs.getDocument({ data: pdfFile.arrayBuffer }).promise;
page = await pdf.getPage(1);
pageWithRendererMocked = Object.assign(page, {
render: () => ({
promise: new Promise<void>((resolve) => resolve()),
cancel: () => {
// Intentionally empty
},
}),
});
});
describe('loading', () => {
it('renders a page and calls onRenderSuccess callback properly', async () => {
const { func: onRenderSuccess, promise: onRenderSuccessPromise } = makeAsyncCallback();
muteConsole();
await renderWithContext(<Canvas />, {
onRenderSuccess,
page: pageWithRendererMocked,
scale: 1,
});
expect.assertions(1);
await expect(onRenderSuccessPromise).resolves.toMatchObject([{}]);
restoreConsole();
});
it('calls onRenderError when failed to render canvas', async () => {
const { func: onRenderError, promise: onRenderErrorPromise } = makeAsyncCallback();
muteConsole();
await renderWithContext(<Canvas />, {
onRenderError,
page: failingPage,
scale: 1,
});
expect.assertions(1);
await expect(onRenderErrorPromise).resolves.toMatchObject([expect.any(Error)]);
restoreConsole();
});
});
describe('rendering', () => {
it('passes canvas element to canvasRef properly', async () => {
const canvasRef = vi.fn();
await renderWithContext(<Canvas canvasRef={canvasRef} />, {
page: pageWithRendererMocked,
scale: 1,
});
expect(canvasRef).toHaveBeenCalled();
expect(canvasRef).toHaveBeenCalledWith(expect.any(HTMLElement));
});
it('does not request structure tree to be rendered when renderTextLayer = false', async () => {
const { func: onRenderSuccess, promise: onRenderSuccessPromise } = makeAsyncCallback();
const { container } = await renderWithContext(<Canvas />, {
onRenderSuccess,
page: pageWithRendererMocked,
renderTextLayer: false,
});
await onRenderSuccessPromise;
const structTree = container.querySelector('.react-pdf__Page__structTree');
expect(structTree).not.toBeInTheDocument();
});
it('renders StructTree when given renderTextLayer = true', async () => {
const { func: onGetStructTreeSuccess, promise: onGetStructTreeSuccessPromise } =
makeAsyncCallback();
const { container } = await renderWithContext(<Canvas />, {
onGetStructTreeSuccess,
page: pageWithRendererMocked,
renderTextLayer: true,
});
expect.assertions(1);
await onGetStructTreeSuccessPromise;
const canvas = container.querySelector('canvas') as HTMLCanvasElement;
expect(canvas.children.length).toBeGreaterThan(0);
});
});
});
================================================
FILE: packages/react-pdf/src/Page/Canvas.tsx
================================================
'use client';
import { useCallback, useEffect, useMemo, useRef } from 'react';
import mergeRefs from 'merge-refs';
import * as pdfjs from 'pdfjs-dist';
import invariant from 'tiny-invariant';
import warning from 'warning';
import StructTree from '../StructTree.js';
import usePageContext from '../shared/hooks/usePageContext.js';
import {
cancelRunningTask,
getDevicePixelRatio,
isAbortException,
makePageCallback,
} from '../shared/utils.js';
import type { RenderParameters } from 'pdfjs-dist/types/src/display/api.js';
const ANNOTATION_MODE = pdfjs.AnnotationMode;
type CanvasProps = {
canvasRef?: React.Ref<HTMLCanvasElement>;
};
export default function Canvas(props: CanvasProps): React.ReactElement {
const pageContext = usePageContext();
invariant(pageContext, 'Unable to find Page context.');
const mergedProps = { ...pag
gitextract_tr_jbi67/ ├── .gitattributes ├── .github/ │ ├── FUNDING.yml │ ├── ISSUE_TEMPLATE/ │ │ ├── Bug_report.yml │ │ ├── Feature_request.yml │ │ └── config.yml │ └── workflows/ │ ├── ci.yml │ ├── close-stale-issues.yml │ └── publish.yml ├── .gitignore ├── .husky/ │ └── pre-commit ├── .mailmap ├── .vscode/ │ ├── extensions.json │ └── settings.json ├── .yarn/ │ └── plugins/ │ └── @yarnpkg/ │ └── plugin-nolyfill.cjs ├── .yarnrc.yml ├── LICENSE ├── __mocks__/ │ ├── _failing_page.ts │ ├── _failing_pdf.ts │ └── _silently_failing_pdf.ts ├── biome.json ├── package.json ├── packages/ │ └── react-pdf/ │ ├── LICENSE │ ├── README.md │ ├── package.json │ ├── src/ │ │ ├── Document.spec.tsx │ │ ├── Document.tsx │ │ ├── DocumentContext.tsx │ │ ├── LinkService.ts │ │ ├── Message.tsx │ │ ├── Outline.spec.tsx │ │ ├── Outline.tsx │ │ ├── OutlineContext.tsx │ │ ├── OutlineItem.spec.tsx │ │ ├── OutlineItem.tsx │ │ ├── Page/ │ │ │ ├── AnnotationLayer.css │ │ │ ├── AnnotationLayer.spec.tsx │ │ │ ├── AnnotationLayer.tsx │ │ │ ├── Canvas.spec.tsx │ │ │ ├── Canvas.tsx │ │ │ ├── TextLayer.css │ │ │ ├── TextLayer.spec.tsx │ │ │ └── TextLayer.tsx │ │ ├── Page.spec.tsx │ │ ├── Page.tsx │ │ ├── PageContext.tsx │ │ ├── PasswordResponses.ts │ │ ├── Ref.spec.ts │ │ ├── Ref.ts │ │ ├── StructTree.spec.tsx │ │ ├── StructTree.tsx │ │ ├── StructTreeItem.tsx │ │ ├── Thumbnail.spec.tsx │ │ ├── Thumbnail.tsx │ │ ├── index.spec.ts │ │ ├── index.test.ts │ │ ├── index.ts │ │ ├── pdf.worker.entry.ts │ │ └── shared/ │ │ ├── constants.ts │ │ ├── hooks/ │ │ │ ├── useCachedValue.ts │ │ │ ├── useDocumentContext.ts │ │ │ ├── useOutlineContext.ts │ │ │ ├── usePageContext.ts │ │ │ └── useResolver.ts │ │ ├── structTreeUtils.ts │ │ ├── types.ts │ │ ├── utils.spec.ts │ │ └── utils.ts │ ├── tsconfig.build.json │ ├── tsconfig.json │ ├── vitest.config.ts │ └── vitest.setup.ts ├── sample/ │ ├── next-app/ │ │ ├── .gitignore │ │ ├── app/ │ │ │ ├── Sample.css │ │ │ ├── Sample.tsx │ │ │ ├── layout.tsx │ │ │ └── page.tsx │ │ ├── next-env.d.ts │ │ ├── next.config.ts │ │ ├── package.json │ │ └── tsconfig.json │ ├── next-pages/ │ │ ├── .gitignore │ │ ├── empty-module.ts │ │ ├── next-env.d.ts │ │ ├── next.config.ts │ │ ├── package.json │ │ ├── pages/ │ │ │ ├── Sample.css │ │ │ ├── Sample.tsx │ │ │ ├── _app.tsx │ │ │ └── index.tsx │ │ └── tsconfig.json │ ├── parcel2/ │ │ ├── .gitignore │ │ ├── Sample.css │ │ ├── Sample.tsx │ │ ├── index.html │ │ ├── index.tsx │ │ ├── package.json │ │ ├── scripts/ │ │ │ ├── copy-cmaps.ts │ │ │ ├── copy-standard-fonts.ts │ │ │ └── copy-wasm.ts │ │ └── tsconfig.json │ ├── vite/ │ │ ├── .gitignore │ │ ├── Sample.css │ │ ├── Sample.tsx │ │ ├── index.html │ │ ├── index.tsx │ │ ├── package.json │ │ ├── tsconfig.json │ │ └── vite.config.ts │ └── webpack5/ │ ├── .babelrc │ ├── .gitignore │ ├── Sample.css │ ├── Sample.tsx │ ├── index.html │ ├── index.tsx │ ├── package.json │ ├── tsconfig.json │ └── webpack.config.ts ├── test/ │ ├── .gitignore │ ├── AnnotationOptions.tsx │ ├── CustomRenderer.tsx │ ├── LayerOptions.tsx │ ├── LoadingOptions.tsx │ ├── PassingOptions.tsx │ ├── Test.css │ ├── Test.tsx │ ├── ViewOptions.tsx │ ├── global.d.ts │ ├── index.html │ ├── index.tsx │ ├── package.json │ ├── shared/ │ │ ├── types.ts │ │ └── utils.ts │ ├── tsconfig.json │ └── vite.config.ts └── test-utils.ts
SYMBOL INDEX (225 symbols across 50 files)
FILE: packages/react-pdf/src/Document.spec.tsx
function ChildInternal (line 22) | function ChildInternal({
function Child (line 36) | function Child(props: React.ComponentProps<typeof ChildInternal>) {
function waitForAsync (line 44) | async function waitForAsync() {
function triggerTouchStart (line 657) | function triggerTouchStart(element: HTMLElement) {
FILE: packages/react-pdf/src/Document.tsx
type OnItemClick (line 58) | type OnItemClick = (args: OnItemClickArgs) => void;
type OnPassword (line 60) | type OnPassword = (callback: OnPasswordCallback, reason: PasswordRespons...
type OnSourceError (line 62) | type OnSourceError = OnError;
type OnSourceSuccess (line 64) | type OnSourceSuccess = () => void;
type DocumentProps (line 66) | type DocumentProps = {
function isParameterObject (line 233) | function isParameterObject(file: File): file is Source {
function onSourceSuccess (line 349) | function onSourceSuccess() {
function onSourceError (line 358) | function onSourceError() {
function resetSource (line 371) | function resetSource() {
function onLoadSuccess (line 475) | function onLoadSuccess() {
function onLoadError (line 492) | function onLoadError() {
function renderChildren (line 608) | function renderChildren() {
function renderContent (line 625) | function renderContent() {
FILE: packages/react-pdf/src/LinkService.ts
constant DEFAULT_LINK_REL (line 27) | const DEFAULT_LINK_REL = 'noopener noreferrer nofollow';
type PDFViewer (line 29) | type PDFViewer = {
class LinkService (line 34) | class LinkService implements IPDFLinkService {
method constructor (line 42) | constructor() {
method setDocument (line 51) | setDocument(pdfDocument: PDFDocumentProxy): void {
method setViewer (line 55) | setViewer(pdfViewer: PDFViewer): void {
method setExternalLinkRel (line 59) | setExternalLinkRel(externalLinkRel?: ExternalLinkRel): void {
method setExternalLinkTarget (line 63) | setExternalLinkTarget(externalLinkTarget?: ExternalLinkTarget): void {
method setHash (line 67) | setHash(): void {
method setHistory (line 71) | setHistory(): void {
method pagesCount (line 75) | get pagesCount(): number {
method page (line 79) | get page(): number {
method page (line 85) | set page(value: number) {
method rotation (line 91) | get rotation(): number {
method rotation (line 95) | set rotation(_value) {
method addLinkAttributes (line 99) | addLinkAttributes(link: HTMLAnchorElement, url: string, newWindow: boo...
method goToDestination (line 105) | goToDestination(dest: Dest): Promise<void> {
method goToPage (line 159) | goToPage(pageNumber: number): void {
method goToXY (line 175) | goToXY(): void {
method cachePageRef (line 179) | cachePageRef(): void {
method getDestinationHash (line 183) | getDestinationHash(): string {
method getAnchorUrl (line 187) | getAnchorUrl(): string {
method executeNamedAction (line 191) | executeNamedAction(): void {
method executeSetOCGState (line 195) | executeSetOCGState(): void {
method isPageVisible (line 199) | isPageVisible(): boolean {
method isPageCached (line 203) | isPageCached(): boolean {
method navigateTo (line 207) | navigateTo(dest: Dest): void {
FILE: packages/react-pdf/src/Message.tsx
type MessageProps (line 1) | type MessageProps = {
function Message (line 6) | function Message({ children, type }: MessageProps): React.ReactElement {
FILE: packages/react-pdf/src/Outline.spec.tsx
type PDFOutline (line 17) | type PDFOutline = Awaited<ReturnType<PDFDocumentProxy['getOutline']>>;
function renderWithContext (line 22) | async function renderWithContext(children: React.ReactNode, context: Par...
FILE: packages/react-pdf/src/Outline.tsx
type PDFOutline (line 22) | type PDFOutline = Awaited<ReturnType<PDFDocumentProxy['getOutline']>>;
type OutlineProps (line 24) | type OutlineProps = {
function Outline (line 66) | function Outline(props: OutlineProps): React.ReactElement | null {
FILE: packages/react-pdf/src/OutlineItem.spec.tsx
type PDFOutline (line 17) | type PDFOutline = Awaited<ReturnType<PDFDocumentProxy['getOutline']>>;
type PDFOutlineItem (line 18) | type PDFOutlineItem = PDFOutline[number];
function renderWithContext (line 20) | async function renderWithContext(
FILE: packages/react-pdf/src/OutlineItem.tsx
type PDFOutline (line 12) | type PDFOutline = Awaited<ReturnType<PDFDocumentProxy['getOutline']>>;
type PDFOutlineItem (line 14) | type PDFOutlineItem = PDFOutline[number];
type OutlineItemProps (line 16) | type OutlineItemProps = {
function OutlineItem (line 21) | function OutlineItem(props: OutlineItemProps): React.ReactElement {
FILE: packages/react-pdf/src/Page.spec.tsx
function renderWithContext (line 24) | async function renderWithContext(children: React.ReactNode, context: Par...
function CustomRenderer (line 550) | function CustomRenderer() {
function CustomRenderer (line 659) | function CustomRenderer() {
function triggerTouchStart (line 996) | function triggerTouchStart(element: HTMLElement) {
FILE: packages/react-pdf/src/Page.tsx
type PageProps (line 53) | type PageProps = {
function Page (line 318) | function Page(props: PageProps): React.ReactElement {
FILE: packages/react-pdf/src/Page/AnnotationLayer.spec.tsx
function renderWithContext (line 21) | async function renderWithContext(
FILE: packages/react-pdf/src/Page/AnnotationLayer.tsx
function AnnotationLayer (line 19) | function AnnotationLayer(): React.ReactElement {
FILE: packages/react-pdf/src/Page/Canvas.spec.tsx
function renderWithContext (line 17) | async function renderWithContext(children: React.ReactNode, context: Par...
FILE: packages/react-pdf/src/Page/Canvas.tsx
constant ANNOTATION_MODE (line 22) | const ANNOTATION_MODE = pdfjs.AnnotationMode;
type CanvasProps (line 24) | type CanvasProps = {
function Canvas (line 28) | function Canvas(props: CanvasProps): React.ReactElement {
FILE: packages/react-pdf/src/Page/TextLayer.spec.tsx
function renderWithContext (line 19) | async function renderWithContext(children: React.ReactNode, context: Par...
function getTextItems (line 38) | function getTextItems(container: HTMLElement) {
FILE: packages/react-pdf/src/Page/TextLayer.tsx
function isTextItem (line 17) | function isTextItem(item: TextItem | TextMarkedContent): item is TextItem {
constant BLOCKED_CUSTOM_TEXT_TAGS (line 21) | const BLOCKED_CUSTOM_TEXT_TAGS = new Set([
constant URL_ATTRIBUTES (line 33) | const URL_ATTRIBUTES = new Set(['action', 'formaction', 'href', 'poster'...
function isDangerousUrl (line 35) | function isDangerousUrl(value: string): boolean {
function isElementNode (line 51) | function isElementNode(node: Node): node is Element {
function isHtmlElement (line 55) | function isHtmlElement(node: Node): node is HTMLElement {
function isBlockedCustomTextElement (line 59) | function isBlockedCustomTextElement(node: Node): boolean {
function sanitizeCustomHtmlElement (line 63) | function sanitizeCustomHtmlElement(element: HTMLElement): HTMLElement {
function sanitizeCustomTextNode (line 87) | function sanitizeCustomTextNode(node: ChildNode): Node {
function renderSafeCustomText (line 95) | function renderSafeCustomText(target: Element, content: string): void {
function TextLayer (line 108) | function TextLayer(): React.ReactElement {
FILE: packages/react-pdf/src/Ref.ts
class Ref (line 1) | class Ref {
method constructor (line 5) | constructor({ num, gen }: { num: number; gen: number }) {
method toString (line 10) | toString(): string {
FILE: packages/react-pdf/src/StructTree.spec.tsx
function renderWithContext (line 18) | async function renderWithContext(children: React.ReactNode, context: Par...
FILE: packages/react-pdf/src/StructTree.tsx
function StructTree (line 15) | function StructTree(): React.ReactElement | null {
FILE: packages/react-pdf/src/StructTreeItem.tsx
type StructTreeItemProps (line 12) | type StructTreeItemProps = {
function StructTreeItem (line 17) | function StructTreeItem({
FILE: packages/react-pdf/src/Thumbnail.spec.tsx
function renderWithContext (line 22) | async function renderWithContext(children: React.ReactNode, context: Par...
function CustomRenderer (line 452) | function CustomRenderer() {
function triggerTouchStart (line 649) | function triggerTouchStart(element: HTMLElement) {
FILE: packages/react-pdf/src/Thumbnail.tsx
type ThumbnailProps (line 15) | type ThumbnailProps = Omit<
function Thumbnail (line 51) | function Thumbnail(props: ThumbnailProps): React.ReactElement {
FILE: packages/react-pdf/src/shared/constants.ts
constant PDF_ROLE_TO_HTML_ROLE (line 3) | const PDF_ROLE_TO_HTML_ROLE = {
constant HEADING_PATTERN (line 59) | const HEADING_PATTERN: RegExp = /^H(\d+)$/;
FILE: packages/react-pdf/src/shared/hooks/useCachedValue.ts
function useCachedValue (line 7) | function useCachedValue<T>(getter: () => T): () => T {
FILE: packages/react-pdf/src/shared/hooks/useDocumentContext.ts
function useDocumentContext (line 7) | function useDocumentContext(): DocumentContextType {
FILE: packages/react-pdf/src/shared/hooks/useOutlineContext.ts
function useOutlineContext (line 7) | function useOutlineContext(): OutlineContextType {
FILE: packages/react-pdf/src/shared/hooks/usePageContext.ts
function usePageContext (line 7) | function usePageContext(): PageContextType {
FILE: packages/react-pdf/src/shared/hooks/useResolver.ts
type State (line 3) | type State<T> =
type Action (line 8) | type Action<T> =
function reducer (line 13) | function reducer<T>(state: State<T>, action: Action<T>): State<T> {
function useResolver (line 26) | function useResolver<T>(): [State<T>, React.Dispatch<Action<T>>] {
FILE: packages/react-pdf/src/shared/structTreeUtils.ts
type PdfRole (line 6) | type PdfRole = keyof typeof PDF_ROLE_TO_HTML_ROLE;
type Attributes (line 8) | type Attributes = React.HTMLAttributes<HTMLElement>;
function isPdfRole (line 10) | function isPdfRole(role: string): role is PdfRole {
function isStructTreeNode (line 14) | function isStructTreeNode(node: StructTreeNode | StructTreeContent): nod...
function isStructTreeNodeWithOnlyContentChild (line 18) | function isStructTreeNodeWithOnlyContentChild(
function getRoleAttributes (line 28) | function getRoleAttributes(node: StructTreeNode | StructTreeContent): At...
function getBaseAttributes (line 51) | function getBaseAttributes(
function getAttributes (line 84) | function getAttributes(
FILE: packages/react-pdf/src/shared/types.ts
type NullableObject (line 21) | type NullableObject<T extends object> = { [P in keyof T]: T[P] | null };
type KeyOfUnion (line 23) | type KeyOfUnion<T> = T extends unknown ? keyof T : never;
type Annotations (line 26) | type Annotations = AnnotationLayerParameters['annotations'];
type ClassName (line 28) | type ClassName = string | null | undefined | (string | null | undefined)[];
type ResolvedDest (line 30) | type ResolvedDest = (RefProxy | number)[];
type Dest (line 32) | type Dest = Promise<ResolvedDest> | ResolvedDest | string | null;
type ExternalLinkRel (line 34) | type ExternalLinkRel = string;
type ExternalLinkTarget (line 36) | type ExternalLinkTarget = '_self' | '_blank' | '_parent' | '_top';
type FilterAnnotationsArgs (line 38) | type FilterAnnotationsArgs = {
type ImageResourcesPath (line 42) | type ImageResourcesPath = string;
type OnError (line 44) | type OnError = (error: Error) => void;
type OnItemClickArgs (line 46) | type OnItemClickArgs = {
type OnLoadProgressArgs (line 52) | type OnLoadProgressArgs = {
type RegisterPage (line 57) | type RegisterPage = (pageIndex: number, ref: HTMLDivElement) => void;
type RenderMode (line 59) | type RenderMode = 'canvas' | 'custom' | 'none';
type ScrollPageIntoViewArgs (line 61) | type ScrollPageIntoViewArgs = {
type BinaryData (line 67) | type BinaryData = TypedArray | ArrayBuffer | number[] | string;
type Source (line 69) | type Source =
type UnregisterPage (line 74) | type UnregisterPage = (pageIndex: number) => void;
type CustomRenderer (line 77) | type CustomRenderer = React.FunctionComponent | React.ComponentClass;
type CustomTextRenderer (line 79) | type CustomTextRenderer = (
type DocumentCallback (line 83) | type DocumentCallback = PDFDocumentProxy;
type File (line 85) | type File = string | ArrayBuffer | Blob | Source | null;
type PageCallback (line 87) | type PageCallback = PDFPageProxy & {
type NodeOrRenderer (line 94) | type NodeOrRenderer = React.ReactNode | (() => React.ReactNode);
type FilterAnnotations (line 96) | type FilterAnnotations = (args: FilterAnnotationsArgs) => Annotations;
type OnDocumentLoadError (line 98) | type OnDocumentLoadError = OnError;
type OnDocumentLoadProgress (line 100) | type OnDocumentLoadProgress = (args: OnLoadProgressArgs) => void;
type OnDocumentLoadSuccess (line 102) | type OnDocumentLoadSuccess = (document: DocumentCallback) => void;
type OnGetAnnotationsError (line 104) | type OnGetAnnotationsError = OnError;
type OnGetAnnotationsSuccess (line 106) | type OnGetAnnotationsSuccess = (annotations: Annotations) => void;
type OnGetStructTreeError (line 108) | type OnGetStructTreeError = OnError;
type OnGetStructTreeSuccess (line 110) | type OnGetStructTreeSuccess = (tree: StructTreeNode) => void;
type OnGetTextError (line 112) | type OnGetTextError = OnError;
type OnGetTextSuccess (line 114) | type OnGetTextSuccess = (textContent: TextContent) => void;
type OnPageLoadError (line 116) | type OnPageLoadError = OnError;
type OnPageLoadSuccess (line 118) | type OnPageLoadSuccess = (page: PageCallback) => void;
type OnPasswordCallback (line 120) | type OnPasswordCallback = (password: string | null) => void;
type OnRenderAnnotationLayerError (line 122) | type OnRenderAnnotationLayerError = (error: unknown) => void;
type OnRenderAnnotationLayerSuccess (line 124) | type OnRenderAnnotationLayerSuccess = () => void;
type OnRenderError (line 126) | type OnRenderError = OnError;
type OnRenderSuccess (line 128) | type OnRenderSuccess = (page: PageCallback) => void;
type OnRenderTextLayerError (line 130) | type OnRenderTextLayerError = OnError;
type OnRenderTextLayerSuccess (line 132) | type OnRenderTextLayerSuccess = () => void;
type PasswordResponse (line 134) | type PasswordResponse = (typeof PasswordResponses)[keyof typeof Password...
type Options (line 136) | type Options = NullableObject<Omit<DocumentInitParameters, KeyOfUnion<So...
type PageColors (line 138) | type PageColors = {
type DocumentContextType (line 144) | type DocumentContextType = {
type PageContextType (line 155) | type PageContextType = {
type OutlineContextType (line 183) | type OutlineContextType = {
type StructTreeNodeWithExtraAttributes (line 187) | type StructTreeNodeWithExtraAttributes = StructTreeNode & {
type DocumentRenderProps (line 192) | type DocumentRenderProps = Omit<NonNullable<DocumentContextType>, 'pdf'>...
type PageRenderProps (line 196) | type PageRenderProps = Omit<NonNullable<PageContextType>, 'page'> & {
FILE: packages/react-pdf/src/shared/utils.ts
function isDefined (line 22) | function isDefined<T>(variable: T | undefined): variable is T {
function isProvided (line 31) | function isProvided<T>(variable: T | null | undefined): variable is T {
function isString (line 40) | function isString(variable: unknown): variable is string {
function isArrayBuffer (line 49) | function isArrayBuffer(variable: unknown): variable is ArrayBuffer {
function isBlob (line 58) | function isBlob(variable: unknown): variable is Blob {
function isDataURI (line 69) | function isDataURI(variable: unknown): variable is `data:${string}` {
function dataURItoByteString (line 73) | function dataURItoByteString(dataURI: unknown): string {
function getDevicePixelRatio (line 86) | function getDevicePixelRatio(): number {
function displayCORSWarning (line 93) | function displayCORSWarning(): void {
function displayWorkerWarning (line 100) | function displayWorkerWarning(): void {
function cancelRunningTask (line 107) | function cancelRunningTask(runningTask?: { cancel?: () => void } | null)...
function makePageCallback (line 111) | function makePageCallback(page: PDFPageProxy, scale: number): PageCallba...
function isAbortException (line 139) | function isAbortException(error: Error): boolean {
function loadFromFile (line 143) | function loadFromFile(file: Blob): Promise<ArrayBuffer> {
FILE: sample/next-app/app/Sample.tsx
type PDFFile (line 28) | type PDFFile = string | File | null;
function Sample (line 30) | function Sample() {
FILE: sample/next-app/app/layout.tsx
function RootLayout (line 5) | function RootLayout({ children }: { children: React.ReactNode }) {
FILE: sample/next-app/app/page.tsx
function Page (line 9) | function Page() {
FILE: sample/next-pages/pages/Sample.tsx
type PDFFile (line 24) | type PDFFile = string | File | null;
function Sample (line 26) | function Sample() {
FILE: sample/next-pages/pages/_app.tsx
function App (line 5) | function App({ Component, pageProps }: AppProps) {
FILE: sample/next-pages/pages/index.tsx
function Page (line 7) | function Page() {
FILE: sample/parcel2/Sample.tsx
type PDFFile (line 28) | type PDFFile = string | File | null;
function Sample (line 30) | function Sample() {
FILE: sample/vite/Sample.tsx
type PDFFile (line 26) | type PDFFile = string | File | null;
function Sample (line 28) | function Sample() {
FILE: sample/webpack5/Sample.tsx
type PDFFile (line 26) | type PDFFile = string | File | null;
function Sample (line 28) | function Sample() {
FILE: test-utils.ts
type Func (line 6) | type Func<T extends unknown[]> = (...args: T) => void;
function makeAsyncCallbackWithoutValue (line 8) | function makeAsyncCallbackWithoutValue<T extends unknown[]>(): {
function makeAsyncCallbackWithValue (line 21) | function makeAsyncCallbackWithValue<T>(value: T): {
function makeAsyncCallback (line 38) | function makeAsyncCallback<T>(value?: T) {
function loadPDF (line 45) | async function loadPDF(path: string): Promise<{
function muteConsole (line 81) | function muteConsole(): void {
function restoreConsole (line 93) | function restoreConsole(): void {
FILE: test/AnnotationOptions.tsx
type AnnotationOptionsProps (line 5) | type AnnotationOptionsProps = {
function AnnotationOptions (line 10) | function AnnotationOptions({
FILE: test/CustomRenderer.tsx
function CustomRenderer (line 7) | function CustomRenderer() {
FILE: test/LayerOptions.tsx
type LayerOptionsProps (line 3) | type LayerOptionsProps = {
function LayerOptions (line 14) | function LayerOptions({
FILE: test/LoadingOptions.tsx
type LoadingOptionsProps (line 8) | type LoadingOptionsProps = {
function LoadingOptions (line 14) | function LoadingOptions({ file, setFile, setRender }: LoadingOptionsProp...
FILE: test/PassingOptions.tsx
type PassingOptionsProps (line 6) | type PassingOptionsProps = {
function PassingOptions (line 12) | function PassingOptions({ file, passMethod, setPassMethod }: PassingOpti...
FILE: test/Test.tsx
function readAsDataURL (line 33) | function readAsDataURL(file: Blob): Promise<string> {
function Test (line 72) | function Test() {
FILE: test/ViewOptions.tsx
type ViewOptionsProps (line 5) | type ViewOptionsProps = {
function ViewOptions (line 26) | function ViewOptions({
FILE: test/shared/types.ts
type BinaryData (line 4) | type BinaryData = TypedArray | ArrayBuffer | number[] | string;
type Source (line 6) | type Source =
type ExternalLinkTarget (line 11) | type ExternalLinkTarget = '_self' | '_blank' | '_parent' | '_top';
type File (line 13) | type File = string | ArrayBuffer | Blob | Source | null;
type PassMethod (line 15) | type PassMethod = 'normal' | 'object' | 'string';
type RenderMode (line 17) | type RenderMode = 'canvas' | 'custom' | 'none';
FILE: test/shared/utils.ts
function isString (line 13) | function isString(variable: unknown): variable is string {
function isArrayBuffer (line 22) | function isArrayBuffer(variable: unknown): variable is ArrayBuffer {
function isBlob (line 31) | function isBlob(variable: unknown): variable is Blob {
function isDataURI (line 42) | function isDataURI(variable: unknown): variable is `data:${string}` {
function loadFromFile (line 46) | function loadFromFile(file: Blob): Promise<ArrayBuffer> {
Condensed preview — 135 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (426K chars).
[
{
"path": ".gitattributes",
"chars": 73,
"preview": "# Auto detect text files and perform LF normalization\n* text=auto eol=lf\n"
},
{
"path": ".github/FUNDING.yml",
"chars": 55,
"preview": "github: wojtekmaj\nopen_collective: react-pdf-wojtekmaj\n"
},
{
"path": ".github/ISSUE_TEMPLATE/Bug_report.yml",
"chars": 2257,
"preview": "name: 🐛 Bug report\ndescription: Something does not work the way we promised\nlabels:\n - bug\nbody:\n - type: checkboxes\n "
},
{
"path": ".github/ISSUE_TEMPLATE/Feature_request.yml",
"chars": 1252,
"preview": "name: 🚀 Feature request\ndescription: I have a great idea for this project\nlabels:\n - enhancement\nbody:\n - type: checkb"
},
{
"path": ".github/ISSUE_TEMPLATE/config.yml",
"chars": 342,
"preview": "blank_issues_enabled: false\ncontact_links:\n - name: 🤔 Support question\n url: https://stackoverflow.com/questions/tag"
},
{
"path": ".github/workflows/ci.yml",
"chars": 3573,
"preview": "name: CI\n\non:\n push:\n branches: ['*']\n pull_request:\n branches: [main]\n\nenv:\n HUSKY: 0\n\njobs:\n lint:\n name:"
},
{
"path": ".github/workflows/close-stale-issues.yml",
"chars": 832,
"preview": "name: Close stale issues\n\non:\n schedule:\n - cron: '0 0 * * 1' # Every Monday\n workflow_dispatch:\n\njobs:\n close-iss"
},
{
"path": ".github/workflows/publish.yml",
"chars": 1211,
"preview": "name: Publish\n\non:\n release:\n types: [published]\n\nenv:\n HUSKY: 0\n\npermissions:\n id-token: write\n\njobs:\n publish:\n"
},
{
"path": ".gitignore",
"chars": 405,
"preview": "# OS\n.DS_Store\n\n# Cache\n.cache\n.playwright\n.tmp\n*.tsbuildinfo\n.eslintcache\n\n# Yarn\n.pnp.*\n**/.yarn/*\n!**/.yarn/patches\n!"
},
{
"path": ".husky/pre-commit",
"chars": 54,
"preview": "yarn format --staged --no-errors-on-unmatched --write\n"
},
{
"path": ".mailmap",
"chars": 263,
"preview": "Niklas Närhinen <niklas@narhinen.net>\nWojciech Maj <kontakt@wojtekmaj.pl>\nWojciech Maj <kontakt@wojtekmaj.pl> <wojciech."
},
{
"path": ".vscode/extensions.json",
"chars": 145,
"preview": "{\n \"recommendations\": [\"biomejs.biome\"],\n \"unwantedRecommendations\": [\"dbaeumer.jshint\", \"dbaeumer.vscode-eslint\", \"es"
},
{
"path": ".vscode/settings.json",
"chars": 128,
"preview": "{\n \"editor.defaultFormatter\": \"biomejs.biome\",\n \"editor.formatOnSave\": true,\n \"search.exclude\": {\n \"**/.yarn\": tru"
},
{
"path": ".yarn/plugins/@yarnpkg/plugin-nolyfill.cjs",
"chars": 3314,
"preview": "/* eslint-disable */\n//prettier-ignore\nmodule.exports = {\nname: \"@yarnpkg/plugin-nolyfill\",\nfactory: function (require) "
},
{
"path": ".yarnrc.yml",
"chars": 422,
"preview": "enableScripts: false\n\nlogFilters:\n - code: YN0076\n level: discard\n\nnodeLinker: node-modules\n\nplugins:\n - checksum: "
},
{
"path": "LICENSE",
"chars": 1074,
"preview": "MIT License\n\nCopyright (c) 2017–2026 Wojciech Maj\n\nPermission is hereby granted, free of charge, to any person obtaining"
},
{
"path": "__mocks__/_failing_page.ts",
"chars": 767,
"preview": "import type { PDFPageProxy } from 'pdfjs-dist';\n\nexport default {\n cleanup: () => {\n return true;\n },\n commonObjs:"
},
{
"path": "__mocks__/_failing_pdf.ts",
"chars": 437,
"preview": "import type { PDFDocumentProxy } from 'pdfjs-dist';\n\nexport default {\n _pdfInfo: {\n fingerprint: 'a62067476e69734bb8"
},
{
"path": "__mocks__/_silently_failing_pdf.ts",
"chars": 607,
"preview": "import { RenderingCancelledException } from 'pdfjs-dist';\n\nimport type { PDFDocumentProxy } from 'pdfjs-dist';\n\nexport d"
},
{
"path": "biome.json",
"chars": 2400,
"preview": "{\n \"$schema\": \"https://biomejs.dev/schemas/2.2.2/schema.json\",\n \"files\": {\n \"includes\": [\n \"**\",\n \"!**/.y"
},
{
"path": "package.json",
"chars": 690,
"preview": "{\n \"name\": \"react-pdf-monorepo\",\n \"version\": \"1.0.0\",\n \"description\": \"react-pdf monorepo\",\n \"type\": \"module\",\n \"wo"
},
{
"path": "packages/react-pdf/LICENSE",
"chars": 1074,
"preview": "MIT License\n\nCopyright (c) 2017–2026 Wojciech Maj\n\nPermission is hereby granted, free of charge, to any person obtaining"
},
{
"path": "packages/react-pdf/README.md",
"chars": 71487,
"preview": "[](https://www.npmjs.com/package/react-pdf) ;\n * you may no"
},
{
"path": "packages/react-pdf/src/Message.tsx",
"chars": 280,
"preview": "type MessageProps = {\n children?: React.ReactNode;\n type: 'error' | 'loading' | 'no-data';\n};\n\nexport default function"
},
{
"path": "packages/react-pdf/src/Outline.spec.tsx",
"chars": 5837,
"preview": "import { beforeAll, describe, expect, it } from 'vitest';\nimport { page } from 'vitest/browser';\nimport { render } from "
},
{
"path": "packages/react-pdf/src/Outline.tsx",
"chars": 5564,
"preview": "'use client';\n\nimport { useEffect, useMemo } from 'react';\nimport clsx from 'clsx';\nimport makeCancellable from 'make-ca"
},
{
"path": "packages/react-pdf/src/OutlineContext.tsx",
"chars": 248,
"preview": "'use client';\n\nimport { createContext } from 'react';\n\nimport type { OutlineContextType } from './shared/types.js';\n\ncon"
},
{
"path": "packages/react-pdf/src/OutlineItem.spec.tsx",
"chars": 4231,
"preview": "import { beforeAll, describe, expect, it, vi } from 'vitest';\nimport { page, userEvent } from 'vitest/browser';\nimport {"
},
{
"path": "packages/react-pdf/src/OutlineItem.tsx",
"chars": 3010,
"preview": "import invariant from 'tiny-invariant';\n\nimport Ref from './Ref.js';\n\nimport useCachedValue from './shared/hooks/useCach"
},
{
"path": "packages/react-pdf/src/Page/AnnotationLayer.css",
"chars": 9461,
"preview": "/* Copyright 2014 Mozilla Foundation\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may no"
},
{
"path": "packages/react-pdf/src/Page/AnnotationLayer.spec.tsx",
"chars": 10711,
"preview": "import { beforeAll, describe, expect, it } from 'vitest';\nimport { render } from 'vitest-browser-react';\n\nimport Documen"
},
{
"path": "packages/react-pdf/src/Page/AnnotationLayer.tsx",
"chars": 6169,
"preview": "'use client';\n\nimport { useEffect, useMemo, useRef } from 'react';\nimport clsx from 'clsx';\nimport makeCancellable from "
},
{
"path": "packages/react-pdf/src/Page/Canvas.spec.tsx",
"chars": 4126,
"preview": "import { beforeAll, describe, expect, it, vi } from 'vitest';\nimport { render } from 'vitest-browser-react';\n\nimport { p"
},
{
"path": "packages/react-pdf/src/Page/Canvas.tsx",
"chars": 4323,
"preview": "'use client';\n\nimport { useCallback, useEffect, useMemo, useRef } from 'react';\nimport mergeRefs from 'merge-refs';\nimpo"
},
{
"path": "packages/react-pdf/src/Page/TextLayer.css",
"chars": 2557,
"preview": "/* Copyright 2014 Mozilla Foundation\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may no"
},
{
"path": "packages/react-pdf/src/Page/TextLayer.spec.tsx",
"chars": 11179,
"preview": "import { beforeAll, describe, expect, it, vi } from 'vitest';\nimport { render } from 'vitest-browser-react';\n\nimport { p"
},
{
"path": "packages/react-pdf/src/Page/TextLayer.tsx",
"chars": 8767,
"preview": "'use client';\n\nimport { useCallback, useEffect, useLayoutEffect, useMemo, useRef } from 'react';\nimport clsx from 'clsx'"
},
{
"path": "packages/react-pdf/src/Page.spec.tsx",
"chars": 31143,
"preview": "import { beforeAll, describe, expect, it, vi } from 'vitest';\nimport { page, userEvent } from 'vitest/browser';\nimport {"
},
{
"path": "packages/react-pdf/src/Page.tsx",
"chars": 20605,
"preview": "'use client';\n\nimport { useEffect, useMemo, useRef } from 'react';\nimport clsx from 'clsx';\nimport makeCancellable from "
},
{
"path": "packages/react-pdf/src/PageContext.tsx",
"chars": 233,
"preview": "'use client';\n\nimport { createContext } from 'react';\n\nimport type { PageContextType } from './shared/types.js';\n\nconst "
},
{
"path": "packages/react-pdf/src/PasswordResponses.ts",
"chars": 247,
"preview": "// As defined in https://github.com/mozilla/pdf.js/blob/d9fac3459609a807be6506fb3441b5da4b154d14/src/shared/util.js#L371"
},
{
"path": "packages/react-pdf/src/Ref.spec.ts",
"chars": 490,
"preview": "import { describe, expect, it } from 'vitest';\n\nimport Ref from './Ref.js';\n\ndescribe('Ref', () => {\n it('returns prope"
},
{
"path": "packages/react-pdf/src/Ref.ts",
"chars": 293,
"preview": "export default class Ref {\n num: number;\n gen: number;\n\n constructor({ num, gen }: { num: number; gen: number }) {\n "
},
{
"path": "packages/react-pdf/src/StructTree.spec.tsx",
"chars": 4941,
"preview": "import { beforeAll, describe, expect, it } from 'vitest';\nimport { render } from 'vitest-browser-react';\n\nimport { pdfjs"
},
{
"path": "packages/react-pdf/src/StructTree.tsx",
"chars": 2869,
"preview": "import { useEffect } from 'react';\nimport makeCancellable from 'make-cancellable-promise';\nimport invariant from 'tiny-i"
},
{
"path": "packages/react-pdf/src/StructTreeItem.tsx",
"chars": 1104,
"preview": "import { useMemo } from 'react';\n\nimport {\n getAttributes,\n isStructTreeNode,\n isStructTreeNodeWithOnlyContentChild,\n"
},
{
"path": "packages/react-pdf/src/Thumbnail.spec.tsx",
"chars": 21533,
"preview": "import { beforeAll, describe, expect, it, vi } from 'vitest';\nimport { page, userEvent } from 'vitest/browser';\nimport {"
},
{
"path": "packages/react-pdf/src/Thumbnail.tsx",
"chars": 3517,
"preview": "'use client';\n\nimport clsx from 'clsx';\nimport invariant from 'tiny-invariant';\n\nimport Page from './Page.js';\n\nimport u"
},
{
"path": "packages/react-pdf/src/index.spec.ts",
"chars": 850,
"preview": "import { describe, expect, it } from 'vitest';\n\nimport { Document, Outline, Page, pdfjs, Thumbnail } from './index.js';\n"
},
{
"path": "packages/react-pdf/src/index.test.ts",
"chars": 747,
"preview": "import * as pdfjs from 'pdfjs-dist';\n\nimport Document from './Document.js';\nimport Outline from './Outline.js';\nimport P"
},
{
"path": "packages/react-pdf/src/index.ts",
"chars": 1165,
"preview": "import * as pdfjs from 'pdfjs-dist';\n\nimport Document from './Document.js';\nimport Outline from './Outline.js';\nimport P"
},
{
"path": "packages/react-pdf/src/pdf.worker.entry.ts",
"chars": 418,
"preview": "/**\n * PDF.js worker entry file.\n *\n * This file is identical to Mozilla's pdf.worker.entry.js, with one exception being"
},
{
"path": "packages/react-pdf/src/shared/constants.ts",
"chars": 1360,
"preview": "// From pdfjs-dist/lib/web/struct_tree_layer_builder.js\n\nexport const PDF_ROLE_TO_HTML_ROLE = {\n // Document level stru"
},
{
"path": "packages/react-pdf/src/shared/hooks/useCachedValue.ts",
"chars": 412,
"preview": "'use client';\n\nimport { useRef } from 'react';\n\nimport { isDefined } from '../utils.js';\n\nexport default function useCac"
},
{
"path": "packages/react-pdf/src/shared/hooks/useDocumentContext.ts",
"chars": 259,
"preview": "import { useContext } from 'react';\n\nimport DocumentContext from '../../DocumentContext.js';\n\nimport type { DocumentCont"
},
{
"path": "packages/react-pdf/src/shared/hooks/useOutlineContext.ts",
"chars": 253,
"preview": "import { useContext } from 'react';\n\nimport OutlineContext from '../../OutlineContext.js';\n\nimport type { OutlineContext"
},
{
"path": "packages/react-pdf/src/shared/hooks/usePageContext.ts",
"chars": 235,
"preview": "import { useContext } from 'react';\n\nimport PageContext from '../../PageContext.js';\n\nimport type { PageContextType } fr"
},
{
"path": "packages/react-pdf/src/shared/hooks/useResolver.ts",
"chars": 788,
"preview": "import { useReducer } from 'react';\n\ntype State<T> =\n | { value: T; error: undefined }\n | { value: false; error: Error"
},
{
"path": "packages/react-pdf/src/shared/structTreeUtils.ts",
"chars": 2314,
"preview": "import { HEADING_PATTERN, PDF_ROLE_TO_HTML_ROLE } from './constants.js';\n\nimport type { StructTreeContent, StructTreeNod"
},
{
"path": "packages/react-pdf/src/shared/types.ts",
"chars": 5557,
"preview": "import type {\n PasswordResponses,\n PDFDataRangeTransport,\n PDFDocumentProxy,\n PDFPageProxy,\n} from 'pdfjs-dist';\nimp"
},
{
"path": "packages/react-pdf/src/shared/utils.spec.ts",
"chars": 1846,
"preview": "import { describe, expect, it } from 'vitest';\n\nimport { dataURItoByteString, isDataURI } from './utils.js';\n\ndescribe('"
},
{
"path": "packages/react-pdf/src/shared/utils.ts",
"chars": 4956,
"preview": "import invariant from 'tiny-invariant';\nimport warning from 'warning';\n\nimport type { PDFPageProxy } from 'pdfjs-dist';\n"
},
{
"path": "packages/react-pdf/tsconfig.build.json",
"chars": 205,
"preview": "{\n \"extends\": \"./tsconfig.json\",\n \"compilerOptions\": {\n \"noEmit\": false,\n \"outDir\": \"dist\",\n \"rootDir\": \"src\""
},
{
"path": "packages/react-pdf/tsconfig.json",
"chars": 436,
"preview": "{\n \"compilerOptions\": {\n \"declaration\": true,\n \"esModuleInterop\": true,\n \"isolatedDeclarations\": true,\n \"is"
},
{
"path": "packages/react-pdf/vitest.config.ts",
"chars": 537,
"preview": "import { playwright } from '@vitest/browser-playwright';\nimport { configDefaults, defineConfig } from 'vitest/config';\n\n"
},
{
"path": "packages/react-pdf/vitest.setup.ts",
"chars": 134,
"preview": "document.body.style.setProperty('--react-pdf-annotation-layer', '1');\ndocument.body.style.setProperty('--react-pdf-text-"
},
{
"path": "sample/next-app/.gitignore",
"chars": 24,
"preview": ".next\ndist\nnode_modules\n"
},
{
"path": "sample/next-app/app/Sample.css",
"chars": 947,
"preview": "body {\n margin: 0;\n background-color: #525659;\n font-family: 'Segoe UI', Tahoma, sans-serif;\n}\n\n.Example input,\n.Exam"
},
{
"path": "sample/next-app/app/Sample.tsx",
"chars": 2447,
"preview": "'use client';\n\nimport { useCallback, useId, useState } from 'react';\nimport { useResizeObserver } from '@wojtekmaj/react"
},
{
"path": "sample/next-app/app/layout.tsx",
"chars": 230,
"preview": "export const metadata = {\n title: 'react-pdf sample page',\n};\n\nexport default function RootLayout({ children }: { child"
},
{
"path": "sample/next-app/app/page.tsx",
"chars": 178,
"preview": "'use client';\n\nimport dynamic from 'next/dynamic';\n\nconst Sample = dynamic(() => import('./Sample'), {\n ssr: false,\n});"
},
{
"path": "sample/next-app/next-env.d.ts",
"chars": 247,
"preview": "/// <reference types=\"next\" />\n/// <reference types=\"next/image-types/global\" />\nimport './.next/types/routes.d.ts';\n\n//"
},
{
"path": "sample/next-app/next.config.ts",
"chars": 104,
"preview": "import type { NextConfig } from 'next';\n\nconst nextConfig: NextConfig = {};\n\nexport default nextConfig;\n"
},
{
"path": "sample/next-app/package.json",
"chars": 557,
"preview": "{\n \"name\": \"react-pdf-sample-page-next\",\n \"version\": \"4.0.0\",\n \"description\": \"A sample page for React-PDF.\",\n \"priv"
},
{
"path": "sample/next-app/tsconfig.json",
"chars": 626,
"preview": "{\n \"compilerOptions\": {\n \"allowJs\": true,\n \"esModuleInterop\": true,\n \"incremental\": true,\n \"isolatedModules"
},
{
"path": "sample/next-pages/.gitignore",
"chars": 24,
"preview": ".next\ndist\nnode_modules\n"
},
{
"path": "sample/next-pages/empty-module.ts",
"chars": 11,
"preview": "export {};\n"
},
{
"path": "sample/next-pages/next-env.d.ts",
"chars": 253,
"preview": "/// <reference types=\"next\" />\n/// <reference types=\"next/image-types/global\" />\nimport './.next/dev/types/routes.d.ts';"
},
{
"path": "sample/next-pages/next.config.ts",
"chars": 393,
"preview": "import type { NextConfig } from 'next';\n\nconst turbopackEnabled = process.env.TURBOPACK;\n\nconst nextConfig: NextConfig ="
},
{
"path": "sample/next-pages/package.json",
"chars": 557,
"preview": "{\n \"name\": \"react-pdf-sample-page-next\",\n \"version\": \"4.0.0\",\n \"description\": \"A sample page for React-PDF.\",\n \"priv"
},
{
"path": "sample/next-pages/pages/Sample.css",
"chars": 947,
"preview": "body {\n margin: 0;\n background-color: #525659;\n font-family: 'Segoe UI', Tahoma, sans-serif;\n}\n\n.Example input,\n.Exam"
},
{
"path": "sample/next-pages/pages/Sample.tsx",
"chars": 2408,
"preview": "import { useCallback, useId, useState } from 'react';\nimport { useResizeObserver } from '@wojtekmaj/react-hooks';\nimport"
},
{
"path": "sample/next-pages/pages/_app.tsx",
"chars": 174,
"preview": "import './Sample.css';\n\nimport type { AppProps } from 'next/app';\n\nexport default function App({ Component, pageProps }:"
},
{
"path": "sample/next-pages/pages/index.tsx",
"chars": 163,
"preview": "import dynamic from 'next/dynamic';\n\nconst Sample = dynamic(() => import('./Sample'), {\n ssr: false,\n});\n\nexport defaul"
},
{
"path": "sample/next-pages/tsconfig.json",
"chars": 599,
"preview": "{\n \"compilerOptions\": {\n \"allowJs\": true,\n \"esModuleInterop\": true,\n \"incremental\": true,\n \"isolatedModules"
},
{
"path": "sample/parcel2/.gitignore",
"chars": 32,
"preview": ".parcel-cache\ndist\nnode_modules\n"
},
{
"path": "sample/parcel2/Sample.css",
"chars": 947,
"preview": "body {\n margin: 0;\n background-color: #525659;\n font-family: 'Segoe UI', Tahoma, sans-serif;\n}\n\n.Example input,\n.Exam"
},
{
"path": "sample/parcel2/Sample.tsx",
"chars": 2499,
"preview": "import { useCallback, useId, useState } from 'react';\nimport { useResizeObserver } from '@wojtekmaj/react-hooks';\nimport"
},
{
"path": "sample/parcel2/index.html",
"chars": 309,
"preview": "<!DOCTYPE html>\n<html lang=\"en-US\">\n <head>\n <meta charset=\"utf-8\" />\n <meta name=\"viewport\" content=\"width=devic"
},
{
"path": "sample/parcel2/index.tsx",
"chars": 230,
"preview": "import { createRoot } from 'react-dom/client';\n\nimport Sample from './Sample';\n\nconst root = document.getElementById('ro"
},
{
"path": "sample/parcel2/package.json",
"chars": 1031,
"preview": "{\n \"name\": \"react-pdf-sample-page-parcel2\",\n \"version\": \"4.0.0\",\n \"description\": \"A sample page for React-PDF.\",\n \"p"
},
{
"path": "sample/parcel2/scripts/copy-cmaps.ts",
"chars": 346,
"preview": "import fs from 'node:fs';\nimport path from 'node:path';\nimport { createRequire } from 'node:module';\n\nconst require = cr"
},
{
"path": "sample/parcel2/scripts/copy-standard-fonts.ts",
"chars": 387,
"preview": "import fs from 'node:fs';\nimport path from 'node:path';\nimport { createRequire } from 'node:module';\n\nconst require = cr"
},
{
"path": "sample/parcel2/scripts/copy-wasm.ts",
"chars": 342,
"preview": "import fs from 'node:fs';\nimport path from 'node:path';\nimport { createRequire } from 'node:module';\n\nconst require = cr"
},
{
"path": "sample/parcel2/tsconfig.json",
"chars": 303,
"preview": "{\n \"compilerOptions\": {\n \"isolatedModules\": true,\n \"jsx\": \"react-jsx\",\n \"module\": \"preserve\",\n \"moduleDetec"
},
{
"path": "sample/vite/.gitignore",
"chars": 18,
"preview": "dist\nnode_modules\n"
},
{
"path": "sample/vite/Sample.css",
"chars": 947,
"preview": "body {\n margin: 0;\n background-color: #525659;\n font-family: 'Segoe UI', Tahoma, sans-serif;\n}\n\n.Example input,\n.Exam"
},
{
"path": "sample/vite/Sample.tsx",
"chars": 2432,
"preview": "import { useCallback, useId, useState } from 'react';\nimport { useResizeObserver } from '@wojtekmaj/react-hooks';\nimport"
},
{
"path": "sample/vite/index.html",
"chars": 309,
"preview": "<!DOCTYPE html>\n<html lang=\"en-US\">\n <head>\n <meta charset=\"utf-8\" />\n <meta name=\"viewport\" content=\"width=devic"
},
{
"path": "sample/vite/index.tsx",
"chars": 233,
"preview": "import { createRoot } from 'react-dom/client';\n\nimport Sample from './Sample.js';\n\nconst root = document.getElementById("
},
{
"path": "sample/vite/package.json",
"chars": 660,
"preview": "{\n \"name\": \"react-pdf-sample-page-vite4\",\n \"version\": \"4.0.0\",\n \"description\": \"A sample page for React-PDF.\",\n \"pri"
},
{
"path": "sample/vite/tsconfig.json",
"chars": 303,
"preview": "{\n \"compilerOptions\": {\n \"isolatedModules\": true,\n \"jsx\": \"react-jsx\",\n \"module\": \"preserve\",\n \"moduleDetec"
},
{
"path": "sample/vite/vite.config.ts",
"chars": 821,
"preview": "import path from 'node:path';\nimport { createRequire } from 'node:module';\n\nimport { defineConfig, normalizePath } from "
},
{
"path": "sample/webpack5/.babelrc",
"chars": 224,
"preview": "{\n \"presets\": [\n \"@babel/preset-typescript\",\n [\n \"@babel/preset-env\",\n {\n \"modules\": false\n "
},
{
"path": "sample/webpack5/.gitignore",
"chars": 18,
"preview": "dist\nnode_modules\n"
},
{
"path": "sample/webpack5/Sample.css",
"chars": 947,
"preview": "body {\n margin: 0;\n background-color: #525659;\n font-family: 'Segoe UI', Tahoma, sans-serif;\n}\n\n.Example input,\n.Exam"
},
{
"path": "sample/webpack5/Sample.tsx",
"chars": 2432,
"preview": "import { useCallback, useId, useState } from 'react';\nimport { useResizeObserver } from '@wojtekmaj/react-hooks';\nimport"
},
{
"path": "sample/webpack5/index.html",
"chars": 255,
"preview": "<!DOCTYPE html>\n<html lang=\"en-US\">\n <head>\n <meta charset=\"utf-8\" />\n <meta name=\"viewport\" content=\"width=devic"
},
{
"path": "sample/webpack5/index.tsx",
"chars": 230,
"preview": "import { createRoot } from 'react-dom/client';\n\nimport Sample from './Sample';\n\nconst root = document.getElementById('ro"
},
{
"path": "sample/webpack5/package.json",
"chars": 1037,
"preview": "{\n \"name\": \"react-pdf-sample-page-webpack5\",\n \"version\": \"4.0.0\",\n \"description\": \"A sample page for React-PDF.\",\n \""
},
{
"path": "sample/webpack5/tsconfig.json",
"chars": 447,
"preview": "{\n \"compilerOptions\": {\n \"esModuleInterop\": true,\n \"isolatedModules\": true,\n \"jsx\": \"react-jsx\",\n \"module\":"
},
{
"path": "sample/webpack5/webpack.config.ts",
"chars": 1930,
"preview": "import webpack from 'webpack';\nimport path from 'node:path';\n\nimport CopyWebpackPlugin from 'copy-webpack-plugin';\nimpor"
},
{
"path": "test/.gitignore",
"chars": 18,
"preview": "dist\nnode_modules\n"
},
{
"path": "test/AnnotationOptions.tsx",
"chars": 1863,
"preview": "import { useId } from 'react';\n\nimport type { ExternalLinkTarget } from './shared/types.js';\n\ntype AnnotationOptionsProp"
},
{
"path": "test/CustomRenderer.tsx",
"chars": 1486,
"preview": "import { useEffect, useMemo, useRef } from 'react';\nimport { usePageContext } from 'react-pdf';\nimport invariant from 't"
},
{
"path": "test/LayerOptions.tsx",
"chars": 2639,
"preview": "import { useId } from 'react';\n\ntype LayerOptionsProps = {\n renderAnnotationLayer: boolean;\n renderForms: boolean;\n r"
},
{
"path": "test/LoadingOptions.tsx",
"chars": 2565,
"preview": "import { useId, useRef } from 'react';\nimport { flushSync } from 'react-dom';\n\nimport samplePDF from './test.pdf';\n\nimpo"
},
{
"path": "test/PassingOptions.tsx",
"chars": 2141,
"preview": "import { isDataURI } from './shared/utils.js';\nimport { useId } from 'react';\n\nimport type { File, PassMethod } from './"
},
{
"path": "test/Test.css",
"chars": 3054,
"preview": "body {\n margin: 0;\n font-family: 'Segoe UI', Tahoma, sans-serif;\n}\n\n.Test header {\n background-color: #323639;\n box-"
},
{
"path": "test/Test.tsx",
"chars": 11505,
"preview": "import { useCallback, useEffect, useState } from 'react';\nimport { Document, Outline, Page, pdfjs, Thumbnail } from 'rea"
},
{
"path": "test/ViewOptions.tsx",
"chars": 9069,
"preview": "import { useId, useRef } from 'react';\n\nimport type { RenderMode } from './shared/types.js';\n\ntype ViewOptionsProps = {\n"
},
{
"path": "test/global.d.ts",
"chars": 70,
"preview": "declare module '*.pdf' {\n const src: string;\n export default src;\n}\n"
},
{
"path": "test/index.html",
"chars": 307,
"preview": "<!doctype html>\n<html lang=\"en-US\">\n <head>\n <meta charset=\"utf-8\" />\n <meta name=\"viewport\" content=\"width=devic"
},
{
"path": "test/index.tsx",
"chars": 301,
"preview": "import { StrictMode } from 'react';\nimport { createRoot } from 'react-dom/client';\n\nimport Test from './Test.js';\n\nconst"
},
{
"path": "test/package.json",
"chars": 830,
"preview": "{\n \"name\": \"test\",\n \"version\": \"4.0.0\",\n \"description\": \"A test page for React-PDF.\",\n \"private\": true,\n \"type\": \"m"
},
{
"path": "test/shared/types.ts",
"chars": 564,
"preview": "import type { PDFDataRangeTransport } from 'pdfjs-dist';\nimport type { TypedArray } from 'pdfjs-dist/types/src/display/a"
},
{
"path": "test/shared/utils.ts",
"chars": 2231,
"preview": "import invariant from 'tiny-invariant';\n\n/**\n * Checks if we're running in a browser environment.\n */\nexport const isBro"
},
{
"path": "test/tsconfig.json",
"chars": 303,
"preview": "{\n \"compilerOptions\": {\n \"isolatedModules\": true,\n \"jsx\": \"react-jsx\",\n \"module\": \"preserve\",\n \"moduleDetec"
},
{
"path": "test/vite.config.ts",
"chars": 945,
"preview": "import path from 'node:path';\nimport { createRequire } from 'node:module';\n\nimport { defineConfig, normalizePath } from "
},
{
"path": "test-utils.ts",
"chars": 2669,
"preview": "import { vi } from 'vitest';\nimport { server } from 'vitest/browser';\n\nconst { readFile } = server.commands;\n\ntype Func<"
}
]
About this extraction
This page contains the full source code of the wojtekmaj/react-pdf GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 135 files (393.7 KB), approximately 93.3k tokens, and a symbol index with 225 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.