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 Wojciech Maj Wojciech Maj Wojciech Maj Wojciech Maj ================================================ 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((_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 ================================================ [![npm](https://img.shields.io/npm/v/react-pdf.svg)](https://www.npmjs.com/package/react-pdf) ![downloads](https://img.shields.io/npm/dt/react-pdf.svg) [![CI](https://github.com/wojtekmaj/react-pdf/actions/workflows/ci.yml/badge.svg)](https://github.com/wojtekmaj/react-pdf/actions) # React-PDF Display PDFs in your React app as easily as if they were images. ## Lost? This package is used to _display_ existing PDFs. If you wish to _create_ PDFs using React, you may be looking for [@react-pdf/renderer](https://www.npmjs.com/package/@react-pdf/renderer). ## tl;dr - Install by executing `npm install react-pdf` or `yarn add react-pdf`. - Import by adding `import { Document } from 'react-pdf'`. - Use by adding ``. `file` can be a URL, base64 content, Uint8Array, and more. - Put `` components inside `` 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., ``, ``). 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.
See more examples ##### 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(); ```
#### 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., ``, ``). 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(); const [pageNumber, setPageNumber] = useState(1); function onDocumentLoadSuccess({ numPages }: { numPages: number }): void { setNumPages(numPages); } return (

Page {pageNumber} of {numPages}

); } ``` 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 ; ``` > [!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 ; ``` ### 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 ; ``` > [!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 ; ``` ### 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 ; ``` > [!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 ; ``` ## 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 |
  • String:
    `"custom-class-name-1 custom-class-name-2"`
  • Array of strings:
    `["custom-class-name-1", "custom-class-name-2"]`
| | error | What the component should display in case of an error. | `"Failed to load PDF file."` |
  • String:
    `"An error occurred!"`
  • React element:
    `

    An error occurred!

    `
  • Function:
    `this.renderError`
| | 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).
  • `"noopener"`
  • `"noreferrer"`
  • `"nofollow"`
  • `"noopener noreferrer"`
| | 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).
  • `"_self"`
  • `"_blank"`
  • `"_parent"`
  • `"_top"`
| | file | 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. | n/a |
  • URL:
    `"https://example.com/sample.pdf"`
  • File:
    `import importedPdf from '../static/sample.pdf'` and then
    `sample`
  • Parameter object:
    `{ url: 'https://example.com/sample.pdf' }`
| | 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 `
` rendered by `` component. | n/a |
  • Function:
    `(ref) => { this.myDocument = ref; }`
  • Ref created using `createRef`:
    `this.ref = createRef();`

    `inputRef={this.ref}`
  • Ref created using `useRef`:
    `const ref = useRef();`

    `inputRef={ref}`
| | loading | What the component should display while loading. | `"Loading PDF…"` |
  • String:
    `"Please wait!"`
  • React element:
    `

    Please wait!

    `
  • Function:
    `this.renderLoader`
| | noData | What the component should display in case of no data. | `"No PDF file specified."` |
  • String:
    `"Please select a file."`
  • React element:
    `

    Please select a file.

    `
  • Function:
    `this.renderNoData`
| | 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:
  • `cMapUrl`;
  • `httpHeaders` - custom request headers, e.g. for authorization);
  • `withCredentials` - a boolean to indicate whether or not to include cookies in the request (defaults to `false`)
For a full list of possible parameters, check [PDF.js documentation on DocumentInitParameters](https://mozilla.github.io/pdf.js/api/draft/module-pdfjsLib.html#~DocumentInitParameters).

**Note**: Make sure to define options object outside of your React component 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 ``. Alternatively, it can have `pdf` prop passed, which can be obtained from ``'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 `` rendered by `` component. | n/a |
  • Function:
    `(ref) => { this.myCanvas = ref; }`
  • Ref created using `createRef`:
    `this.ref = createRef();`

    `inputRef={this.ref}`
  • Ref created using `useRef`:
    `const ref = useRef();`

    `inputRef={ref}`
| | className | Class name(s) that will be added to rendered element along with the default `react-pdf__Page`. | n/a |
  • String:
    `"custom-class-name-1 custom-class-name-2"`
  • Array of strings:
    `["custom-class-name-1", "custom-class-name-2"]`
| | 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 => `${value}`)`` | | 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."` |
  • String:
    `"An error occurred!"`
  • React element:
    `

    An error occurred!

    `
  • Function:
    `this.renderError`
| | 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 `
` rendered by `` component. | n/a |
  • Function:
    `(ref) => { this.myPage = ref; }`
  • Ref created using `createRef`:
    `this.ref = createRef();`

    `inputRef={this.ref}`
  • Ref created using `useRef`:
    `const ref = useRef();`

    `inputRef={ref}`
| | loading | What the component should display while loading. | `"Loading page…"` |
  • String:
    `"Please wait!"`
  • React element:
    `

    Please wait!

    `
  • Function:
    `this.renderLoader`
| | noData | What the component should display in case of no data. | `"No page specified."` |
  • String:
    `"Please select a page."`
  • React element:
    `

    Please select a page.

    `
  • Function:
    `this.renderNoData`
| | 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 ``'s `onLoadSuccess` callback function. | (automatically obtained from parent ``) | `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 ``. Alternatively, it can have `pdf` prop passed, which can be obtained from ``'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 |
  • String:
    `"custom-class-name-1 custom-class-name-2"`
  • Array of strings:
    `["custom-class-name-1", "custom-class-name-2"]`
| | inputRef | A prop that behaves like [ref](https://reactjs.org/docs/refs-and-the-dom.html), but it's passed to main `
` rendered by `` component. | n/a |
  • Function:
    `(ref) => { this.myOutline = ref; }`
  • Ref created using `createRef`:
    `this.ref = createRef();`

    `inputRef={this.ref}`
  • Ref created using `useRef`:
    `const ref = useRef();`

    `inputRef={ref}`
| | 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 ``. Alternatively, it can have `pdf` prop passed, which can be obtained from ``'s `onLoadSuccess` callback function. #### Props Props are the same as in `` 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 |
  • String:
    `"custom-class-name-1 custom-class-name-2"`
  • Array of strings:
    `["custom-class-name-1", "custom-class-name-2"]`
| | 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
Wojciech Maj Wojciech Maj
## 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. ### 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. ### Top Contributors Thank you to all our contributors that helped on this project! ![Top Contributors](https://opencollective.com/react-pdf/contributors.svg?width=890&button=false) ================================================ 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 (
); } function Child(props: React.ComponentProps) { return ( {(context) => } ); } 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 = {}; const desiredLoadedPdf2: Partial = {}; 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( , ); 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( , ); 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( , ); 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( , ); 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( , ); 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( 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( , ); 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( , ); 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(); const wrapper = container.querySelector('.react-pdf__Document'); expect(wrapper).toHaveClass(className); }); it('passes container element to inputRef properly', async () => { const inputRef = createRef(); await render(); expect(inputRef.current).toBeInstanceOf(HTMLDivElement); }); it('renders "No PDF file specified." when given nothing', async () => { const { container } = await render(); 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(); 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( '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(); 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(); 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( '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(); 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( , ); 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( , ); 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( , ); 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( , ); 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( , ); 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( , ); 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( , ); 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( , ); 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( {({ pdf }) =>

{`This PDF has ${pdf.numPages} pages`}

}
, ); 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; pages: React.RefObject; viewer: React.RefObject<{ scrollPageIntoView: (args: ScrollPageIntoViewArgs) => void }>; }>(); await render( , ); 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; // biome-ignore lint/suspicious/noExplicitAny: Intentional use to simplify the test pages: React.RefObject; viewer: React.RefObject<{ scrollPageIntoView: (args: ScrollPageIntoViewArgs) => void }>; }>(); await render(); 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( , ); 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( , ); 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(); 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(); 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(); await rerender(); 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(); await rerender(); 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(); await rerender(); 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(); await rerender(); 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(); await rerender(); 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(); await rerender(); 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(); 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

An error occurred!

* @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 `
` rendered by `` component. * * @example (ref) => { this.myDocument = ref; } * @example this.ref * @example ref */ inputRef?: React.Ref; /** * What the component should display while loading. * * @default 'Loading PDF…' * @example 'Please wait!' * @example

Please wait!

* @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

Please select a file.

* @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; 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; pages: React.RefObject; 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(); const { value: source, error: sourceError } = sourceState; const [pdfState, pdfDispatch] = useResolver(); const { value: pdf, error: pdfError } = pdfState; const linkService = useRef(new LinkService()); const pages = useRef([]); const prevFile = useRef(undefined); const prevOptions = useRef(undefined); if (file && file !== prevFile.current && isParameterObject(file)) { warning( !dequal(file, prevFile.current), `File prop passed to 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 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 . 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 was provided with onItemClick nor it was able to find the page within itself. Either provide onItemClick to and handle navigating by yourself or ensure that all pages are rendered within .`, ); }, }); 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 => { 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 ( {resolvedChildren} ); } function renderContent() { if (!file) { return {typeof noData === 'function' ? noData() : noData}; } if (pdf === undefined || pdf === null) { return ( {typeof loading === 'function' ? loading() : loading} ); } if (pdf === false) { return {typeof error === 'function' ? error() : error}; } return renderChildren(); } return (
} {...eventProps} > {renderContent()}
); }); 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 = createContext(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 { return new Promise((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((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
{children}
; } ================================================ 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>; const pdfFile = await loadPDF('../../__mocks__/_pdf.pdf'); const pdfFile2 = await loadPDF('../../__mocks__/_pdf2.pdf'); async function renderWithContext(children: React.ReactNode, context: Partial) { const { rerender, ...otherResult } = await render( {children} , ); return { ...otherResult, rerender: async ( nextChildren: React.ReactNode, nextContext: Partial = context, ) => await rerender( {nextChildren} , ), }; } 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(, { 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(); 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(, { 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(, { pdf, }); expect.assertions(2); await expect(onLoadSuccessPromise).resolves.toMatchObject([desiredLoadedOutline]); const { func: onLoadSuccess2, promise: onLoadSuccessPromise2 } = makeAsyncCallback(); await rerender(, { 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()).rejects.toThrowError( 'Invariant failed: Attempted to load an outline, but no document was specified. Wrap in a 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( , { 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(); await renderWithContext(, { 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(, { 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>; 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 `
` rendered by `` component. * * @example (ref) => { this.myOutline = ref; } * @example this.ref * @example ref */ inputRef?: React.Ref; /** * 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; /** * Displays an outline (table of contents). * * Should be placed inside ``. Alternatively, it can have `pdf` prop passed, which can be obtained from ``'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 in a or pass explicit `pdf` prop.', ); const [outlineState, outlineDispatch] = useResolver(); 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 (
    {outline.map((item, itemIndex) => ( ))}
); } return (
{renderOutline()}
); } ================================================ FILE: packages/react-pdf/src/OutlineContext.tsx ================================================ 'use client'; import { createContext } from 'react'; import type { OutlineContextType } from './shared/types.js'; const outlineContext: React.Context = createContext(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>; type PDFOutlineItem = PDFOutline[number]; async function renderWithContext( children: React.ReactNode, documentContext: Partial, outlineContext: Partial, ) { const { rerender, ...otherResult } = await render( {children} , ); return { ...otherResult, rerender: async ( nextChildren: React.ReactNode, nextDocumentContext: Partial = documentContext, nextOutlineContext: Partial = outlineContext, ) => await rerender( {nextChildren} , ), }; } 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(, { 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(, { 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(, { 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( , { 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(, { 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>; 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 in a 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) { 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 (
    {subitems.map((subitem, subitemIndex) => ( ))}
); } return (
  • {/* biome-ignore lint/a11y/useValidAnchor: We can't provide real href here */} {item.title} {renderSubitems()}
  • ); } ================================================ 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,"); --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, pageContext: Partial, ) { const { rerender, ...otherResult } = await render( {children} , ); const customRerender = async ( nextChildren: React.ReactNode, nextDocumentContext: Partial = documentContext, nextPageContext: Partial = pageContext, ) => await rerender( {nextChildren} , ); 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( , { 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( , { 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( , { linkService, pdf, }, { onGetAnnotationsSuccess, page, }, ); expect.assertions(2); await expect(onGetAnnotationsSuccessPromise).resolves.toMatchObject([desiredAnnotations]); const { func: onGetAnnotationsSuccess2, promise: onGetAnnotationsSuccessPromise2 } = makeAsyncCallback(); await rerender( , { 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()).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( , { 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( , { 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( , { 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( `]+src="${imageResourcesPath}annotation-note.svg"`, ); const { container } = await renderWithContext( , { 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( `]+src="${imageResourcesPath}annotation-note.svg"`, ); const { container } = await renderWithContext( , { 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 in a 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(); const { value: annotations, error: annotationsError } = annotationsState; const layerElement = useRef(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 (
    ); } ================================================ 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) { const { rerender, ...otherResult } = await render( {children}, ); return { ...otherResult, rerender: async ( nextChildren: React.ReactNode, nextContext: Partial = context, ) => await rerender( {nextChildren} , ), }; } 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((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(, { 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(, { 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(, { 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(, { 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(, { 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; }; export default function Canvas(props: CanvasProps): React.ReactElement { const pageContext = usePageContext(); invariant(pageContext, 'Unable to find Page context.'); const mergedProps = { ...pageContext, ...props }; const { _className, canvasBackground, devicePixelRatio = getDevicePixelRatio(), onRenderError: onRenderErrorProps, onRenderSuccess: onRenderSuccessProps, page, renderForms, renderTextLayer, pageColors, rotate, scale, } = mergedProps; const { canvasRef } = props; invariant(page, 'Attempted to render page canvas, but no page was specified.'); const canvasElement = useRef(null); /** * Called when a page is rendered successfully. */ function onRenderSuccess() { if (!page) { // Impossible, but TypeScript doesn't know that return; } if (onRenderSuccessProps) { onRenderSuccessProps(makePageCallback(page, scale)); } } /** * Called when a page fails to render. */ function onRenderError(error: Error) { if (isAbortException(error)) { return; } warning(false, error.toString()); if (onRenderErrorProps) { onRenderErrorProps(error); } } const renderViewport = useMemo( () => page.getViewport({ scale: scale * devicePixelRatio, rotation: rotate }), [devicePixelRatio, page, rotate, scale], ); const viewport = useMemo( () => page.getViewport({ scale, rotation: rotate }), [page, rotate, scale], ); // biome-ignore lint/correctness/useExhaustiveDependencies: Omitted callbacks so they are not called every time they change useEffect( function drawPageOnCanvas() { if (!page) { return; } // Ensures the canvas will be re-rendered from scratch. Otherwise all form data will stay. page.cleanup(); const { current: canvas } = canvasElement; if (!canvas) { return; } canvas.width = renderViewport.width; canvas.height = renderViewport.height; canvas.style.width = `${Math.floor(viewport.width)}px`; canvas.style.height = `${Math.floor(viewport.height)}px`; canvas.style.visibility = 'hidden'; const renderContext: RenderParameters = { annotationMode: renderForms ? ANNOTATION_MODE.ENABLE_FORMS : ANNOTATION_MODE.ENABLE, canvas, canvasContext: canvas.getContext('2d', { alpha: false }) as CanvasRenderingContext2D, pageColors, viewport: renderViewport, }; if (canvasBackground) { renderContext.background = canvasBackground; } const cancellable = page.render(renderContext); const runningTask = cancellable; cancellable.promise .then(() => { canvas.style.visibility = ''; onRenderSuccess(); }) .catch(onRenderError); return () => cancelRunningTask(runningTask); }, [canvasBackground, page, pageColors, renderForms, renderViewport, viewport], ); const cleanup = useCallback(() => { const { current: canvas } = canvasElement; /** * Zeroing the width and height cause most browsers to release graphics * resources immediately, which can greatly reduce memory consumption. */ if (canvas) { canvas.width = 0; canvas.height = 0; } }, []); useEffect(() => cleanup, [cleanup]); return ( {renderTextLayer ? : null} ); } ================================================ FILE: packages/react-pdf/src/Page/TextLayer.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-text-layer: 1; --highlight-bg-color: rgba(180, 0, 170, 1); --highlight-selected-bg-color: rgba(0, 100, 0, 1); } @media screen and (forced-colors: active) { :root { --highlight-bg-color: Highlight; --highlight-selected-bg-color: ButtonText; } } [data-main-rotation='90'] { transform: rotate(90deg) translateY(-100%); } [data-main-rotation='180'] { transform: rotate(180deg) translate(-100%, -100%); } [data-main-rotation='270'] { transform: rotate(270deg) translateX(-100%); } .textLayer { position: absolute; text-align: initial; inset: 0; overflow: hidden; line-height: 1; text-size-adjust: none; forced-color-adjust: none; transform-origin: 0 0; z-index: 2; } .textLayer :is(span, br) { color: transparent; position: absolute; white-space: pre; cursor: text; margin: 0; transform-origin: 0 0; } /* Only necessary in Google Chrome, see issue 14205, and most unfortunately * the problem doesn't show up in "text" reference tests. */ .textLayer span.markedContent { top: 0; height: 0; } .textLayer .highlight { margin: -1px; padding: 1px; background-color: var(--highlight-bg-color); border-radius: 4px; } .textLayer .highlight.appended { position: initial; } .textLayer .highlight.begin { border-radius: 4px 0 0 4px; } .textLayer .highlight.end { border-radius: 0 4px 4px 0; } .textLayer .highlight.middle { border-radius: 0; } .textLayer .highlight.selected { background-color: var(--highlight-selected-bg-color); } /* Avoids https://github.com/mozilla/pdf.js/issues/13840 in Chrome */ .textLayer br::selection { background: transparent; } .textLayer .endOfContent { display: block; position: absolute; inset: 100% 0 0; z-index: -1; cursor: default; user-select: none; } .textLayer.selecting .endOfContent { top: 0; } .hiddenCanvasElement { position: absolute; top: 0; left: 0; width: 0; height: 0; display: none; } ================================================ FILE: packages/react-pdf/src/Page/TextLayer.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 TextLayer from './TextLayer.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 { TextContent } from 'pdfjs-dist/types/src/display/api.js'; import type { CustomTextRenderer, PageContextType } from '../shared/types.js'; const pdfFile = await loadPDF('../../__mocks__/_pdf.pdf'); const untaggedPdfFile = await loadPDF('../../__mocks__/_untagged.pdf'); async function renderWithContext(children: React.ReactNode, context: Partial) { const { rerender, ...otherResult } = await render( {children}, ); return { ...otherResult, rerender: async ( nextChildren: React.ReactNode, nextContext: Partial = context, ) => await rerender( {nextChildren} , ), }; } function getTextItems(container: HTMLElement) { const wrapper = container.firstElementChild as HTMLDivElement; return wrapper.querySelectorAll('[role="presentation"]'); } describe('TextLayer', () => { // Loaded page let page: PDFPageProxy; let page2: PDFPageProxy; // Loaded page text items let desiredTextItems: TextContent['items']; let desiredTextItems2: TextContent['items']; beforeAll(async () => { const pdf = await pdfjs.getDocument({ data: pdfFile.arrayBuffer }).promise; page = await pdf.getPage(1); const textContent = await page.getTextContent(); desiredTextItems = textContent.items; page2 = await pdf.getPage(2); const textContent2 = await page2.getTextContent(); desiredTextItems2 = textContent2.items; }); describe('loading', () => { it('loads text content and calls onGetTextSuccess callback properly', async () => { const { func: onGetTextSuccess, promise: onGetTextSuccessPromise } = makeAsyncCallback(); await renderWithContext(, { onGetTextSuccess, page, }); expect.assertions(1); await expect(onGetTextSuccessPromise).resolves.toMatchObject([{ items: desiredTextItems }]); }); it('calls onGetTextError when failed to load text content', async () => { const { func: onGetTextError, promise: onGetTextErrorPromise } = makeAsyncCallback(); muteConsole(); await renderWithContext(, { onGetTextError, page: failingPage, }); expect.assertions(1); await expect(onGetTextErrorPromise).resolves.toMatchObject([expect.any(Error)]); restoreConsole(); }); it('replaces text content properly', async () => { const { func: onGetTextSuccess, promise: onGetTextSuccessPromise } = makeAsyncCallback(); const { rerender } = await renderWithContext(, { onGetTextSuccess, page, }); expect.assertions(2); await expect(onGetTextSuccessPromise).resolves.toMatchObject([ { items: desiredTextItems, }, ]); const { func: onGetTextSuccess2, promise: onGetTextSuccessPromise2 } = makeAsyncCallback(); await rerender(, { onGetTextSuccess: onGetTextSuccess2, page: page2, }); await expect(onGetTextSuccessPromise2).resolves.toMatchObject([ { items: desiredTextItems2, }, ]); }); it('throws an error when placed outside Page', async () => { muteConsole(); await expect(render()).rejects.toThrowError( 'Invariant failed: Unable to find Page context.', ); restoreConsole(); }); }); describe('rendering', () => { it('renders text content properly', async () => { const { func: onRenderTextLayerSuccess, promise: onRenderTextLayerSuccessPromise } = makeAsyncCallback(); const { container } = await renderWithContext(, { onRenderTextLayerSuccess, page, }); expect.assertions(1); await onRenderTextLayerSuccessPromise; const textItems = getTextItems(container); expect(textItems).toHaveLength(desiredTextItems.length); }); it('renders text content properly given customTextRenderer', async () => { const { func: onRenderTextLayerSuccess, promise: onRenderTextLayerSuccessPromise } = makeAsyncCallback(); const customTextRenderer = vi.fn(); const { container } = await renderWithContext(, { customTextRenderer, onRenderTextLayerSuccess, page, }); expect.assertions(1); await onRenderTextLayerSuccessPromise; const textItems = getTextItems(container); expect(textItems).toHaveLength(desiredTextItems.length); }); it('maps textContent items to actual TextLayer children properly', async () => { const { func: onRenderTextLayerSuccess, promise: onRenderTextLayerSuccessPromise } = makeAsyncCallback(); const { container, rerender } = await renderWithContext(, { onRenderTextLayerSuccess, page, }); expect.assertions(1); await onRenderTextLayerSuccessPromise; const textItems = getTextItems(container); const { func: onRenderTextLayerSuccess2, promise: onRenderTextLayerSuccessPromise2 } = makeAsyncCallback(); const customTextRenderer: CustomTextRenderer = ({ str }: { str: string }) => str; await rerender(, { customTextRenderer, onRenderTextLayerSuccess: onRenderTextLayerSuccess2, page, }); await onRenderTextLayerSuccessPromise2; const textItems2 = getTextItems(container); expect(textItems).toEqual(textItems2); }); it('calls customTextRenderer with necessary arguments', async () => { const { func: onRenderTextLayerSuccess, promise: onRenderTextLayerSuccessPromise } = makeAsyncCallback(); const customTextRenderer = vi.fn(); const { container } = await renderWithContext(, { customTextRenderer, onRenderTextLayerSuccess, page, }); expect.assertions(3); await onRenderTextLayerSuccessPromise; const textItems = getTextItems(container); expect(textItems).toHaveLength(desiredTextItems.length); expect(customTextRenderer).toHaveBeenCalledTimes(desiredTextItems.length); expect(customTextRenderer).toHaveBeenCalledWith( expect.objectContaining({ str: expect.any(String), itemIndex: expect.any(Number), }), ); }); it('renders text content properly given customTextRenderer', async () => { const { func: onRenderTextLayerSuccess, promise: onRenderTextLayerSuccessPromise } = makeAsyncCallback(); const customTextRenderer = () => 'Test value'; const { container } = await renderWithContext(, { customTextRenderer, onRenderTextLayerSuccess, page, }); expect.assertions(1); await onRenderTextLayerSuccessPromise; expect(container).toHaveTextContent('Test value'); }); it('renders text content properly given customTextRenderer and untagged document', async () => { const { func: onRenderTextLayerSuccess, promise: onRenderTextLayerSuccessPromise } = makeAsyncCallback(); const customTextRenderer = () => 'Test value'; const untaggedDoc = await pdfjs.getDocument({ data: untaggedPdfFile.arrayBuffer }).promise; const untaggedPage = await untaggedDoc.getPage(1); const { container } = await renderWithContext(, { customTextRenderer, onRenderTextLayerSuccess, page: untaggedPage, }); expect.assertions(1); await onRenderTextLayerSuccessPromise; expect(container).toHaveTextContent('Test value'); }); it('renders HTML formatting from customTextRenderer output', async () => { const { func: onRenderTextLayerSuccess, promise: onRenderTextLayerSuccessPromise } = makeAsyncCallback(); const customTextRenderer: CustomTextRenderer = ({ str }: { str: string }) => str.replace(/ipsum/g, 'ipsum'); const { container } = await renderWithContext(, { customTextRenderer, onRenderTextLayerSuccess, page, }); expect.assertions(2); await onRenderTextLayerSuccessPromise; const highlightedText = container.querySelectorAll('mark'); expect(highlightedText.length).toBeGreaterThan(0); expect(highlightedText[0]).toHaveTextContent('ipsum'); }); it('does not render blocked tags from customTextRenderer output', async () => { const { func: onRenderTextLayerSuccess, promise: onRenderTextLayerSuccessPromise } = makeAsyncCallback(); const windowWithBlockedTagFlag = window as typeof window & { __reactPdfBlockedTagExecuted?: boolean; }; windowWithBlockedTagFlag.__reactPdfBlockedTagExecuted = false; const customTextRenderer: CustomTextRenderer = () => 'safe'; const { container } = await renderWithContext(, { customTextRenderer, onRenderTextLayerSuccess, page, }); expect.assertions(3); await onRenderTextLayerSuccessPromise; expect(container.querySelector('script')).not.toBeInTheDocument(); expect(windowWithBlockedTagFlag.__reactPdfBlockedTagExecuted).toBe(false); expect(container.querySelector('mark')).toHaveTextContent('safe'); delete windowWithBlockedTagFlag.__reactPdfBlockedTagExecuted; }); it('does not execute scripts from customTextRenderer output', async () => { const { func: onRenderTextLayerSuccess, promise: onRenderTextLayerSuccessPromise } = makeAsyncCallback(); const customTextRenderer: CustomTextRenderer = () => 'javascript:/*-->
    x
    '; const alertSpy = vi.spyOn(window, 'alert').mockImplementation(() => { // Intentionally empty }); const { container } = await renderWithContext(, { customTextRenderer, onRenderTextLayerSuccess, page, }); expect.assertions(2); await onRenderTextLayerSuccessPromise; const detailsElement = container.querySelector('details'); detailsElement?.dispatchEvent(new Event('toggle', { bubbles: true })); expect(detailsElement).not.toHaveAttribute('ontoggle'); expect(alertSpy).not.toHaveBeenCalled(); alertSpy.mockRestore(); }); }); }); ================================================ FILE: packages/react-pdf/src/Page/TextLayer.tsx ================================================ 'use client'; import { useCallback, useEffect, useLayoutEffect, 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 usePageContext from '../shared/hooks/usePageContext.js'; import useResolver from '../shared/hooks/useResolver.js'; import { cancelRunningTask, isAbortException } from '../shared/utils.js'; import type { TextContent, TextItem, TextMarkedContent } from 'pdfjs-dist/types/src/display/api.js'; function isTextItem(item: TextItem | TextMarkedContent): item is TextItem { return 'str' in item; } const BLOCKED_CUSTOM_TEXT_TAGS = new Set([ 'base', 'embed', 'iframe', 'link', 'meta', 'object', 'script', 'style', 'template', ]); const URL_ATTRIBUTES = new Set(['action', 'formaction', 'href', 'poster', 'src', 'xlink:href']); function isDangerousUrl(value: string): boolean { let normalizedValue = ''; for (const char of value) { const charCode = char.charCodeAt(0); if (charCode <= 32 || charCode === 127) { continue; } normalizedValue += char.toLowerCase(); } return normalizedValue.startsWith('javascript:') || normalizedValue.startsWith('vbscript:'); } function isElementNode(node: Node): node is Element { return node.nodeType === Node.ELEMENT_NODE; } function isHtmlElement(node: Node): node is HTMLElement { return isElementNode(node) && node instanceof HTMLElement; } function isBlockedCustomTextElement(node: Node): boolean { return isHtmlElement(node) && BLOCKED_CUSTOM_TEXT_TAGS.has(node.tagName.toLowerCase()); } function sanitizeCustomHtmlElement(element: HTMLElement): HTMLElement { const sanitizedElement = document.createElement(element.tagName.toLowerCase()); Array.from(element.attributes).forEach((attribute) => { const attributeName = attribute.name.toLowerCase(); if (attributeName.startsWith('on') || attributeName === 'srcdoc') { return; } if (URL_ATTRIBUTES.has(attributeName) && isDangerousUrl(attribute.value)) { return; } sanitizedElement.setAttribute(attribute.name, attribute.value); }); Array.from(element.childNodes).forEach((child) => { sanitizedElement.append(sanitizeCustomTextNode(child)); }); return sanitizedElement; } function sanitizeCustomTextNode(node: ChildNode): Node { if (isHtmlElement(node) && !isBlockedCustomTextElement(node)) { return sanitizeCustomHtmlElement(node); } return document.createTextNode(node.textContent ?? ''); } function renderSafeCustomText(target: Element, content: string): void { const template = document.createElement('template'); template.innerHTML = content; const sanitizedFragment = document.createDocumentFragment(); Array.from(template.content.childNodes).forEach((child) => { sanitizedFragment.append(sanitizeCustomTextNode(child)); }); target.replaceChildren(sanitizedFragment); } export default function TextLayer(): React.ReactElement { const pageContext = usePageContext(); invariant(pageContext, 'Unable to find Page context.'); const { customTextRenderer, onGetTextError, onGetTextSuccess, onRenderTextLayerError, onRenderTextLayerSuccess, page, pageIndex, pageNumber, rotate, scale, } = pageContext; invariant(page, 'Attempted to load page text content, but no page was specified.'); const [textContentState, textContentDispatch] = useResolver(); const { value: textContent, error: textContentError } = textContentState; const layerElement = useRef(null); warning( Number.parseInt( window.getComputedStyle(document.body).getPropertyValue('--react-pdf-text-layer'), 10, ) === 1, 'TextLayer styles not found. Read more: https://github.com/wojtekmaj/react-pdf#support-for-text-layer', ); /** * Called when a page text content is read successfully */ function onLoadSuccess() { if (!textContent) { // Impossible, but TypeScript doesn't know that return; } if (onGetTextSuccess) { onGetTextSuccess(textContent); } } /** * Called when a page text content failed to read successfully */ function onLoadError() { if (!textContentError) { // Impossible, but TypeScript doesn't know that return; } warning(false, textContentError.toString()); if (onGetTextError) { onGetTextError(textContentError); } } // biome-ignore lint/correctness/useExhaustiveDependencies: useEffect intentionally triggered on page change useEffect( function resetTextContent() { textContentDispatch({ type: 'RESET' }); }, [page, textContentDispatch], ); useEffect( function loadTextContent() { if (!page) { return; } const cancellable = makeCancellable(page.getTextContent()); const runningTask = cancellable; cancellable.promise .then((nextTextContent) => { textContentDispatch({ type: 'RESOLVE', value: nextTextContent }); }) .catch((error) => { textContentDispatch({ type: 'REJECT', error }); }); return () => cancelRunningTask(runningTask); }, [page, textContentDispatch], ); // biome-ignore lint/correctness/useExhaustiveDependencies: Omitted callbacks so they are not called every time they change useEffect(() => { if (textContent === undefined) { return; } if (textContent === false) { onLoadError(); return; } onLoadSuccess(); }, [textContent]); /** * Called when a text layer is rendered successfully */ const onRenderSuccess = useCallback(() => { if (onRenderTextLayerSuccess) { onRenderTextLayerSuccess(); } }, [onRenderTextLayerSuccess]); /** * Called when a text layer failed to render successfully */ const onRenderError = useCallback( (error: Error) => { if (isAbortException(error)) { return; } warning(false, error.toString()); if (onRenderTextLayerError) { onRenderTextLayerError(error); } }, [onRenderTextLayerError], ); function onMouseDown() { const layer = layerElement.current; if (!layer) { return; } layer.classList.add('selecting'); } function onMouseUp() { const layer = layerElement.current; if (!layer) { return; } layer.classList.remove('selecting'); } const viewport = useMemo( () => page.getViewport({ scale, rotation: rotate }), [page, rotate, scale], ); useLayoutEffect( function renderTextLayer() { if (!page || !textContent) { return; } const { current: layer } = layerElement; if (!layer) { return; } layer.innerHTML = ''; const textContentSource = page.streamTextContent({ includeMarkedContent: true }); const parameters = { container: layer, textContentSource, viewport, }; const cancellable = new pdfjs.TextLayer(parameters); const runningTask = cancellable; cancellable .render() .then(() => { const end = document.createElement('div'); end.className = 'endOfContent'; layer.append(end); const layerChildren = layer.querySelectorAll('[role="presentation"]'); if (customTextRenderer) { let index = 0; textContent.items.forEach((item, itemIndex) => { if (!isTextItem(item)) { return; } const child = layerChildren[index]; if (!child) { return; } const content = customTextRenderer({ pageIndex, pageNumber, itemIndex, ...item, }); renderSafeCustomText(child, content); index += item.str && item.hasEOL ? 2 : 1; }); } // Intentional immediate callback onRenderSuccess(); }) .catch(onRenderError); return () => cancelRunningTask(runningTask); }, [ customTextRenderer, onRenderError, onRenderSuccess, page, pageIndex, pageNumber, textContent, viewport, ], ); return ( // biome-ignore lint/a11y/noStaticElementInteractions: False positive caused by non interactive wrapper listening for bubbling events
    ); } ================================================ FILE: packages/react-pdf/src/Page.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 DocumentContext from './DocumentContext.js'; import { pdfjs } from './index.test.js'; import LinkService from './LinkService.js'; import Page from './Page.js'; import failingPdf from '../../../__mocks__/_failing_pdf.js'; import silentlyFailingPdf from '../../../__mocks__/_silently_failing_pdf.js'; import { loadPDF, makeAsyncCallback, muteConsole, restoreConsole } from '../../../test-utils.js'; import type { PDFDocumentProxy, PDFPageProxy } from 'pdfjs-dist'; import type { DocumentContextType, PageCallback } from './shared/types.js'; const pdfFile = await loadPDF('../../__mocks__/_pdf.pdf'); const pdfFile2 = await loadPDF('../../__mocks__/_pdf2.pdf'); const pdfFile4 = await loadPDF('../../__mocks__/_pdf4.pdf'); const pdfFile5 = await loadPDF('../../__mocks__/_pdf5.pdf'); async function renderWithContext(children: React.ReactNode, context: Partial) { const { rerender, ...otherResult } = await render( {children} , ); return { ...otherResult, rerender: async ( nextChildren: React.ReactNode, nextContext: Partial = context, ) => await rerender( {nextChildren} , ), }; } describe('Page', () => { const linkService = new LinkService(); // Loaded PDF file let pdf: PDFDocumentProxy; let pdf2: PDFDocumentProxy; let pdf4: PDFDocumentProxy; let pdf5: PDFDocumentProxy; // Object with basic loaded page information that shall match after successful loading const desiredLoadedPage: Partial = {}; const desiredLoadedPage2: Partial = {}; const desiredLoadedPage3: Partial = {}; // Callbacks used in registerPage and unregisterPage callbacks let registerPageArguments: [number, HTMLDivElement]; let unregisterPageArguments: [number]; beforeAll(async () => { pdf = await pdfjs.getDocument({ data: pdfFile.arrayBuffer }).promise; const page = await pdf.getPage(1); desiredLoadedPage._pageIndex = page._pageIndex; desiredLoadedPage._pageInfo = page._pageInfo; const page2 = await pdf.getPage(2); desiredLoadedPage2._pageIndex = page2._pageIndex; desiredLoadedPage2._pageInfo = page2._pageInfo; pdf2 = await pdfjs.getDocument({ data: pdfFile2.arrayBuffer }).promise; const page3 = await pdf2.getPage(1); desiredLoadedPage3._pageIndex = page3._pageIndex; desiredLoadedPage3._pageInfo = page3._pageInfo; registerPageArguments = [page._pageIndex, expect.any(HTMLDivElement)]; unregisterPageArguments = [page._pageIndex]; pdf4 = await pdfjs.getDocument({ data: pdfFile4.arrayBuffer }).promise; pdf5 = await pdfjs.getDocument({ data: pdfFile5.arrayBuffer }).promise; }); describe('loading', () => { it('loads a page and calls onLoadSuccess callback properly when placed inside Document', async () => { const { func: onLoadSuccess, promise: onLoadSuccessPromise } = makeAsyncCallback(); await renderWithContext(, { linkService, pdf, }); expect.assertions(1); await expect(onLoadSuccessPromise).resolves.toMatchObject([desiredLoadedPage]); }); it('loads a page and calls onLoadSuccess callback properly when pdf prop is passed', async () => { const { func: onLoadSuccess, promise: onLoadSuccessPromise } = makeAsyncCallback(); await render( , ); expect.assertions(1); await expect(onLoadSuccessPromise).resolves.toMatchObject([desiredLoadedPage]); }); it('returns all desired parameters in onLoadSuccess callback', async () => { const { func: onLoadSuccess, promise: onLoadSuccessPromise } = makeAsyncCallback<[PageCallback]>(); await renderWithContext(, { linkService, pdf, }); expect.assertions(5); const [page] = await onLoadSuccessPromise; expect(page.width).toBeDefined(); expect(page.height).toBeDefined(); expect(page.originalWidth).toBeDefined(); expect(page.originalHeight).toBeDefined(); // Example of a method that got stripped away in the past expect(page.getTextContent).toBeInstanceOf(Function); }); it('calls onLoadError when failed to load a page', async () => { const { func: onLoadError, promise: onLoadErrorPromise } = makeAsyncCallback(); muteConsole(); await renderWithContext(, { linkService, pdf: failingPdf, }); expect.assertions(1); await expect(onLoadErrorPromise).resolves.toMatchObject([expect.any(Error)]); restoreConsole(); }); it('loads page when given pageIndex', async () => { const { func: onLoadSuccess, promise: onLoadSuccessPromise } = makeAsyncCallback(); await renderWithContext(, { linkService, pdf, }); expect.assertions(1); const [page] = await onLoadSuccessPromise; expect(page).toMatchObject(desiredLoadedPage); }); it('loads page when given pageNumber', async () => { const { func: onLoadSuccess, promise: onLoadSuccessPromise } = makeAsyncCallback(); await renderWithContext(, { linkService, pdf, }); expect.assertions(1); const [page] = await onLoadSuccessPromise; expect(page).toMatchObject(desiredLoadedPage); }); it('loads page of a given number when given conflicting pageNumber and pageIndex', async () => { const { func: onLoadSuccess, promise: onLoadSuccessPromise } = makeAsyncCallback(); await renderWithContext(, { linkService, pdf, }); expect.assertions(1); const [page] = await onLoadSuccessPromise; expect(page).toMatchObject(desiredLoadedPage); }); it('calls registerPage when loaded a page', async () => { const { func: registerPage, promise: registerPagePromise } = makeAsyncCallback(); await renderWithContext(, { linkService, pdf, registerPage, }); expect.assertions(1); await expect(registerPagePromise).resolves.toMatchObject(registerPageArguments); }); it('calls unregisterPage on unmount', async () => { const { func: unregisterPage, promise: nuregisterPagePromise } = makeAsyncCallback(); const { unmount } = await renderWithContext(, { linkService, pdf, unregisterPage, }); await unmount(); expect.assertions(1); await expect(nuregisterPagePromise).resolves.toMatchObject(unregisterPageArguments); }); it('replaces a page properly when pdf is changed', async () => { const { func: onLoadSuccess, promise: onLoadSuccessPromise } = makeAsyncCallback(); const { rerender } = await renderWithContext( , { linkService, pdf, }, ); expect.assertions(2); await expect(onLoadSuccessPromise).resolves.toMatchObject([desiredLoadedPage]); const { func: onLoadSuccess2, promise: onLoadSuccessPromise2 } = makeAsyncCallback(); await rerender(, { linkService, pdf: pdf2, }); await expect(onLoadSuccessPromise2).resolves.toMatchObject([desiredLoadedPage3]); }); it('replaces a page properly when pageNumber is changed', async () => { const { func: onLoadSuccess, promise: onLoadSuccessPromise } = makeAsyncCallback(); const { rerender } = await renderWithContext( , { linkService, pdf, }, ); expect.assertions(2); await expect(onLoadSuccessPromise).resolves.toMatchObject([desiredLoadedPage]); const { func: onLoadSuccess2, promise: onLoadSuccessPromise2 } = makeAsyncCallback(); await rerender(, { linkService, pdf, }); await expect(onLoadSuccessPromise2).resolves.toMatchObject([desiredLoadedPage2]); }); it('throws an error when placed outside Document without pdf prop passed', async () => { muteConsole(); await expect(render()).rejects.toThrowError( 'Invariant failed: Attempted to load a page, but no document was specified. Wrap in a or pass explicit `pdf` prop.', ); restoreConsole(); }); }); describe('rendering', () => { it('applies className to its wrapper when given a string', async () => { const className = 'testClassName'; const { container } = await renderWithContext(, { linkService, pdf, }); const wrapper = container.querySelector('.react-pdf__Page'); expect(wrapper).toHaveClass(className); }); it('passes container element to inputRef properly', async () => { const inputRef = createRef(); await renderWithContext(, { linkService, pdf: silentlyFailingPdf, }); expect(inputRef.current).toBeInstanceOf(HTMLDivElement); }); it('passes canvas element to Canvas properly', async () => { const { func: onLoadSuccess, promise: onLoadSuccessPromise } = makeAsyncCallback(); const canvasRef = createRef(); const { container } = await renderWithContext( , { linkService, pdf, }, ); expect.assertions(1); await onLoadSuccessPromise; const pageCanvas = container.querySelector('.react-pdf__Page__canvas'); expect(canvasRef.current).toBe(pageCanvas); }); it('renders "No page specified." when given neither pageIndex nor pageNumber', async () => { muteConsole(); const { container } = await renderWithContext(, { linkService, pdf, }); const noData = container.querySelector('.react-pdf__message'); expect(noData).toBeInTheDocument(); expect(noData).toHaveTextContent('No page specified.'); restoreConsole(); }); it('renders custom no data message when given nothing and noData is given', async () => { muteConsole(); const { container } = await renderWithContext(, { linkService, pdf, }); const noData = container.querySelector('.react-pdf__message'); expect(noData).toBeInTheDocument(); expect(noData).toHaveTextContent('Nothing here'); restoreConsole(); }); it('renders custom no data message when given nothing and noData is given as a function', async () => { muteConsole(); const { container } = await renderWithContext( 'Nothing here'} />, { linkService, pdf, }); const noData = container.querySelector('.react-pdf__message'); expect(noData).toBeInTheDocument(); expect(noData).toHaveTextContent('Nothing here'); restoreConsole(); }); it('renders "Loading page…" when loading a page', () => { renderWithContext(, { linkService, pdf, }); const loading = page.getByText('Loading page…'); expect(loading).toBeInTheDocument(); }); it('renders custom loading message when loading a page and loading prop is given', () => { renderWithContext(, { linkService, pdf, }); const loading = page.getByText('Loading', { exact: true }); expect(loading).toBeInTheDocument(); }); it('renders custom loading message when loading a page and loading prop is given as a function', () => { renderWithContext( 'Loading'} pageIndex={0} />, { linkService, pdf, }); const loading = page.getByText('Loading', { exact: true }); expect(loading).toBeInTheDocument(); }); it('ignores pageIndex when given pageIndex and pageNumber', async () => { const { func: onLoadSuccess, promise: onLoadSuccessPromise } = makeAsyncCallback(); await renderWithContext(, { linkService, pdf, }); expect.assertions(1); const [page] = await onLoadSuccessPromise; expect(page).toMatchObject(desiredLoadedPage); }); it('requests page to be rendered with default rotation when given nothing', async () => { const { func: onRenderSuccess, promise: onRenderSuccessPromise } = makeAsyncCallback<[PageCallback]>(); const { container } = await renderWithContext( , { linkService, pdf, }, ); const [page] = await onRenderSuccessPromise; const pageCanvas = container.querySelector('.react-pdf__Page__canvas') as HTMLCanvasElement; const { width, height } = window.getComputedStyle(pageCanvas); const viewport = page.getViewport({ scale: 1 }); // Expect the canvas layer not to be rotated expect(Number.parseInt(width, 10)).toBe(Math.floor(viewport.width)); expect(Number.parseInt(height, 10)).toBe(Math.floor(viewport.height)); }); it('requests page to be rendered with given rotation when given rotate prop', async () => { const { func: onRenderSuccess, promise: onRenderSuccessPromise } = makeAsyncCallback<[PageCallback]>(); const rotate = 90; const { container } = await renderWithContext( , { linkService, pdf, }, ); const [page] = await onRenderSuccessPromise; const pageCanvas = container.querySelector('.react-pdf__Page__canvas') as HTMLCanvasElement; const { width, height } = window.getComputedStyle(pageCanvas); const viewport = page.getViewport({ scale: 1, rotation: rotate }); // Expect the canvas layer to be rotated expect(Number.parseInt(width, 10)).toBe(Math.floor(viewport.width)); expect(Number.parseInt(height, 10)).toBe(Math.floor(viewport.height)); }); it('requests page to be rendered in canvas mode by default', async () => { const { func: onLoadSuccess, promise: onLoadSuccessPromise } = makeAsyncCallback(); const { container } = await renderWithContext( , { linkService, pdf, }, ); expect.assertions(1); await onLoadSuccessPromise; const pageCanvas = container.querySelector('.react-pdf__Page__canvas'); expect(pageCanvas).toBeInTheDocument(); }); it('requests page not to be rendered when given renderMode = "none"', async () => { const { func: onLoadSuccess, promise: onLoadSuccessPromise } = makeAsyncCallback(); const { container } = await renderWithContext( , { linkService, pdf, }, ); expect.assertions(1); await onLoadSuccessPromise; const pageCanvas = container.querySelector('.react-pdf__Page__canvas'); expect(pageCanvas).not.toBeInTheDocument(); }); it('requests page to be rendered in canvas mode when given renderMode = "canvas"', async () => { const { func: onLoadSuccess, promise: onLoadSuccessPromise } = makeAsyncCallback(); const { container } = await renderWithContext( , { linkService, pdf, }, ); expect.assertions(1); await onLoadSuccessPromise; const pageCanvas = container.querySelector('.react-pdf__Page__canvas'); expect(pageCanvas).toBeInTheDocument(); }); it('requests page to be rendered in custom mode when given renderMode = "custom"', async () => { const { func: onLoadSuccess, promise: onLoadSuccessPromise } = makeAsyncCallback(); function CustomRenderer() { return
    ; } const { container } = await renderWithContext( , { linkService, pdf, }, ); expect.assertions(1); await onLoadSuccessPromise; const customRenderer = container.querySelector('.custom-renderer'); expect(customRenderer).toBeInTheDocument(); }); it('requests text content to be rendered by default', async () => { const { func: onLoadSuccess, promise: onLoadSuccessPromise } = makeAsyncCallback(); const { container } = await renderWithContext( , { linkService, pdf, }, ); expect.assertions(1); await onLoadSuccessPromise; const textLayer = container.querySelector('.react-pdf__Page__textContent'); expect(textLayer).toBeInTheDocument(); }); it('requests text content to be rendered when given renderTextLayer = true', async () => { const { func: onLoadSuccess, promise: onLoadSuccessPromise } = makeAsyncCallback(); const { container } = await renderWithContext( , { linkService, pdf, }, ); expect.assertions(1); await onLoadSuccessPromise; const textLayer = container.querySelector('.react-pdf__Page__textContent'); expect(textLayer).toBeInTheDocument(); }); it('does not request text content to be rendered when given renderTextLayer = false', async () => { const { func: onLoadSuccess, promise: onLoadSuccessPromise } = makeAsyncCallback(); const { container } = await renderWithContext( , { linkService, pdf, }, ); expect.assertions(1); await onLoadSuccessPromise; const textLayer = container.querySelector('.react-pdf__Page__textContent'); expect(textLayer).not.toBeInTheDocument(); }); it('renders TextLayer when given renderMode = "canvas"', async () => { const { func: onLoadSuccess, promise: onLoadSuccessPromise } = makeAsyncCallback(); const { container } = await renderWithContext( , { linkService, pdf, }, ); expect.assertions(1); await onLoadSuccessPromise; const textLayer = container.querySelector('.react-pdf__Page__textContent'); expect(textLayer).toBeInTheDocument(); }); it('renders TextLayer when given renderMode = "custom"', async () => { const { func: onLoadSuccess, promise: onLoadSuccessPromise } = makeAsyncCallback(); function CustomRenderer() { return
    ; } const { container } = await renderWithContext( , { linkService, pdf, }, ); expect.assertions(1); await onLoadSuccessPromise; const textLayer = container.querySelector('.react-pdf__Page__textContent'); expect(textLayer).toBeInTheDocument(); }); it('requests annotations to be rendered by default', async () => { const { func: onLoadSuccess, promise: onLoadSuccessPromise } = makeAsyncCallback(); const { container } = await renderWithContext( , { linkService, pdf, }, ); expect.assertions(1); await onLoadSuccessPromise; const annotationLayer = container.querySelector('.react-pdf__Page__annotations'); expect(annotationLayer).toBeInTheDocument(); }); it('requests annotations to be rendered when given renderAnnotationLayer = true', async () => { const { func: onLoadSuccess, promise: onLoadSuccessPromise } = makeAsyncCallback(); const { container } = await renderWithContext( , { linkService, pdf, }, ); expect.assertions(1); await onLoadSuccessPromise; const annotationLayer = container.querySelector('.react-pdf__Page__annotations'); expect(annotationLayer).toBeInTheDocument(); }); it('does not request annotations to be rendered when given renderAnnotationLayer = false', async () => { const { func: onLoadSuccess, promise: onLoadSuccessPromise } = makeAsyncCallback(); const { container } = await renderWithContext( , { linkService, pdf, }, ); expect.assertions(1); await onLoadSuccessPromise; const annotationLayer = container.querySelector('.react-pdf__Page__annotations'); expect(annotationLayer).not.toBeInTheDocument(); }); it('supports function as children', async () => { const { func: onLoadSuccess, promise: onLoadSuccessPromise } = makeAsyncCallback(); await renderWithContext( {({ page }) =>

    {`Page ${page.pageNumber}`}

    }
    , { linkService, pdf, }, ); expect.assertions(1); await onLoadSuccessPromise; const child = page.getByText('Page 1'); expect(child).toBeInTheDocument(); }); }); it('requests page to be rendered without forms by default', async () => { const { func: onRenderAnnotationLayerSuccess, promise: onRenderAnnotationLayerSuccessPromise } = makeAsyncCallback(); const { container } = await renderWithContext( , { linkService, pdf: pdf4, }, ); expect.assertions(1); await onRenderAnnotationLayerSuccessPromise; const textWidgetAnnotation = container.querySelector('.textWidgetAnnotation'); expect(textWidgetAnnotation).toBeFalsy(); }); it('requests page to be rendered with forms given renderForms = true', async () => { const { func: onRenderAnnotationLayerSuccess, promise: onRenderAnnotationLayerSuccessPromise } = makeAsyncCallback(); const { container } = await renderWithContext( , { linkService, pdf: pdf4, }, ); expect.assertions(1); await onRenderAnnotationLayerSuccessPromise; const textWidgetAnnotation = container.querySelector('.textWidgetAnnotation'); expect(textWidgetAnnotation).toBeTruthy(); }); it('requests page to be rendered at its original size given nothing', async () => { const { func: onLoadSuccess, promise: onLoadSuccessPromise } = makeAsyncCallback<[PageCallback]>(); await renderWithContext(, { linkService, pdf, }); expect.assertions(1); const [page] = await onLoadSuccessPromise; expect(page.width).toEqual(page.originalWidth); }); it('requests page to be rendered with a proper scale when given scale', async () => { const { func: onLoadSuccess, promise: onLoadSuccessPromise } = makeAsyncCallback<[PageCallback]>(); const scale = 1.5; await renderWithContext(, { linkService, pdf, }); expect.assertions(1); const [page] = await onLoadSuccessPromise; expect(page.width).toEqual(page.originalWidth * scale); }); it('requests page to be rendered with a proper scale when given width', async () => { const { func: onLoadSuccess, promise: onLoadSuccessPromise } = makeAsyncCallback<[PageCallback]>(); const width = 600; await renderWithContext(, { linkService, pdf, }); expect.assertions(1); const [page] = await onLoadSuccessPromise; expect(page.width).toEqual(width); }); it('requests page to be rendered with a proper scale when given width and scale (multiplies)', async () => { const { func: onLoadSuccess, promise: onLoadSuccessPromise } = makeAsyncCallback<[PageCallback]>(); const width = 600; const scale = 1.5; await renderWithContext( , { linkService, pdf, }, ); expect.assertions(1); const [page] = await onLoadSuccessPromise; expect(page.width).toBeCloseTo(width * scale); }); it('requests page to be rendered with a proper scale when given height', async () => { const { func: onLoadSuccess, promise: onLoadSuccessPromise } = makeAsyncCallback<[PageCallback]>(); const height = 850; await renderWithContext(, { linkService, pdf, }); expect.assertions(1); const [page] = await onLoadSuccessPromise; expect(page.height).toEqual(height); }); it('requests page to be rendered with a proper scale when given height and scale (multiplies)', async () => { const { func: onLoadSuccess, promise: onLoadSuccessPromise } = makeAsyncCallback<[PageCallback]>(); const height = 850; const scale = 1.5; await renderWithContext( , { linkService, pdf, }, ); expect.assertions(1); const [page] = await onLoadSuccessPromise; expect(page.height).toBeCloseTo(height * scale); }); it('requests page to be rendered with a proper scale when given width and height (ignores height)', async () => { const { func: onLoadSuccess, promise: onLoadSuccessPromise } = makeAsyncCallback<[PageCallback]>(); const width = 600; const height = 100; await renderWithContext( , { linkService, pdf, }, ); expect.assertions(2); const [page] = await onLoadSuccessPromise; expect(page.width).toEqual(width); // Expect proportions to be correct even though invalid height was provided expect(page.height).toEqual(page.originalHeight * (page.width / page.originalWidth)); }); it('requests page to be rendered with a proper scale when given width, height and scale (ignores height, multiplies)', async () => { const { func: onLoadSuccess, promise: onLoadSuccessPromise } = makeAsyncCallback<[PageCallback]>(); const width = 600; const height = 100; const scale = 1.5; await renderWithContext( , { linkService, pdf, }, ); expect.assertions(2); const [page] = await onLoadSuccessPromise; expect(page.width).toBeCloseTo(width * scale); // Expect proportions to be correct even though invalid height was provided expect(page.height).toEqual(page.originalHeight * (page.width / page.originalWidth)); }); it('calls onClick callback when clicked a page (sample of mouse events family)', async () => { const onClick = vi.fn(); const { container } = await renderWithContext(, { linkService, pdf, }); const page = container.querySelector('.react-pdf__Page') as HTMLDivElement; await userEvent.click(page); expect(onClick).toHaveBeenCalled(); }); function triggerTouchStart(element: HTMLElement) { element.dispatchEvent(new TouchEvent('touchstart', { bubbles: true, cancelable: true })); } it('calls onTouchStart callback when touched a page (sample of touch events family)', async () => { const onTouchStart = vi.fn(); const { container } = await renderWithContext(, { linkService, pdf, }); const page = container.querySelector('.react-pdf__Page') as HTMLDivElement; triggerTouchStart(page); expect(onTouchStart).toHaveBeenCalled(); }); it('handles rotated page dimensions correctly in onLoadPage callback', async () => { const { func: onLoadSuccess, promise: onLoadSuccessPromise } = makeAsyncCallback<[PageCallback]>(); const { func: onLoadSuccess2, promise: onLoadSuccessPromise2 } = makeAsyncCallback<[PageCallback]>(); await renderWithContext(, { linkService, pdf: pdf5, }); await renderWithContext(, { linkService, pdf: pdf5, }); const [page1] = await onLoadSuccessPromise; const [page2] = await onLoadSuccessPromise2; expect(page1.width).toEqual(page2.height); expect(page1.height).toEqual(page2.width); }); }); ================================================ FILE: packages/react-pdf/src/Page.tsx ================================================ 'use client'; import { useEffect, useMemo, useRef } from 'react'; import clsx from 'clsx'; import makeCancellable from 'make-cancellable-promise'; import makeEventProps from 'make-event-props'; import mergeRefs from 'merge-refs'; import invariant from 'tiny-invariant'; import warning from 'warning'; import Message from './Message.js'; import AnnotationLayer from './Page/AnnotationLayer.js'; import Canvas from './Page/Canvas.js'; import TextLayer from './Page/TextLayer.js'; import PageContext from './PageContext.js'; import useDocumentContext from './shared/hooks/useDocumentContext.js'; import useResolver from './shared/hooks/useResolver.js'; import { cancelRunningTask, isProvided, makePageCallback } from './shared/utils.js'; import type { EventProps } from 'make-event-props'; import type { PDFDocumentProxy, PDFPageProxy } from 'pdfjs-dist'; import type { ClassName, CustomRenderer, CustomTextRenderer, FilterAnnotations, NodeOrRenderer, OnGetAnnotationsError, OnGetAnnotationsSuccess, OnGetStructTreeError, OnGetStructTreeSuccess, OnGetTextError, OnGetTextSuccess, OnPageLoadError, OnPageLoadSuccess, OnRenderAnnotationLayerError, OnRenderAnnotationLayerSuccess, OnRenderError, OnRenderSuccess, OnRenderTextLayerError, OnRenderTextLayerSuccess, PageCallback, PageColors, PageContextType, PageRenderProps, RenderMode, } from './shared/types.js'; const defaultScale = 1; export type PageProps = { _className?: string; _enableRegisterUnregisterPage?: boolean; /** * Canvas background color. Any valid `canvas.fillStyle` can be used. * * @example 'transparent' */ canvasBackground?: string; /** * A prop that behaves like [ref](https://reactjs.org/docs/refs-and-the-dom.html), but it's passed to `` rendered by `` component. * * @example (ref) => { this.myCanvas = ref; } * @example this.ref * @example ref */ canvasRef?: React.Ref; children?: React.ReactNode | ((props: PageRenderProps) => React.ReactNode); /** * Class name(s) that will be added to rendered element along with the default `react-pdf__Page`. * * @example 'custom-class-name-1 custom-class-name-2' * @example ['custom-class-name-1', 'custom-class-name-2'] */ className?: ClassName; /** * Function that customizes how a page is rendered. You must set `renderMode` to `"custom"` to use this prop. * * @example MyCustomRenderer */ customRenderer?: CustomRenderer; /** * Function that customizes how a text layer is rendered. * * @example ({ str, itemIndex }) => str.replace(/ipsum/g, value => `${value}`) */ customTextRenderer?: CustomTextRenderer; /** * The ratio between physical pixels and device-independent pixels (DIPs) on the current device. * * @default window.devicePixelRatio * @example 1 */ devicePixelRatio?: number; /** * What the component should display in case of an error. * * @default 'Failed to load the page.' * @example 'An error occurred!' * @example

    An error occurred!

    * @example this.renderError */ error?: NodeOrRenderer; /** * Function to filter annotations before they are rendered. * * @example ({ annotations }) => annotations.filter(annotation => annotation.subtype === 'Text') */ filterAnnotations?: FilterAnnotations; /** * Page height. If neither `height` nor `width` are defined, page will be rendered at the size defined in PDF. If you define `width` and `height` at the same time, `height` will be ignored. If you define `height` and `scale` at the same time, the height will be multiplied by a given factor. * * @example 300 */ height?: number; /** * The path used to prefix the src attributes of annotation SVGs. * * @default '' * @example '/public/images/' */ imageResourcesPath?: string; /** * A prop that behaves like [ref](https://reactjs.org/docs/refs-and-the-dom.html), but it's passed to main `
    ` rendered by `` component. * * @example (ref) => { this.myPage = ref; } * @example this.ref * @example ref */ inputRef?: React.Ref; /** * What the component should display while loading. * * @default 'Loading page…' * @example 'Please wait!' * @example

    Please wait!

    * @example this.renderLoader */ loading?: NodeOrRenderer; /** * What the component should display in case of no data. * * @default 'No page specified.' * @example 'Please select a page.' * @example

    Please select a page.

    * @example this.renderNoData */ noData?: NodeOrRenderer; /** * Function called in case of an error while loading annotations. * * @example (error) => alert('Error while loading annotations! ' + error.message) */ onGetAnnotationsError?: OnGetAnnotationsError; /** * Function called when annotations are successfully loaded. * * @example (annotations) => alert('Now displaying ' + annotations.length + ' annotations!') */ onGetAnnotationsSuccess?: OnGetAnnotationsSuccess; /** * Function called in case of an error while loading structure tree. * * @example (error) => alert('Error while loading structure tree! ' + error.message) */ onGetStructTreeError?: OnGetStructTreeError; /** * Function called when structure tree is successfully loaded. * * @example (structTree) => alert(JSON.stringify(structTree)) */ onGetStructTreeSuccess?: OnGetStructTreeSuccess; /** * Function called in case of an error while loading text layer items. * * @example (error) => alert('Error while loading text layer items! ' + error.message) */ onGetTextError?: OnGetTextError; /** * Function called when text layer items are successfully loaded. * * @example ({ items, styles }) => alert('Now displaying ' + items.length + ' text layer items!') */ onGetTextSuccess?: OnGetTextSuccess; /** * Function called in case of an error while loading the page. * * @example (error) => alert('Error while loading page! ' + error.message) */ onLoadError?: OnPageLoadError; /** * Function called when the page is successfully loaded. * * @example (page) => alert('Now displaying a page number ' + page.pageNumber + '!') */ onLoadSuccess?: OnPageLoadSuccess; /** * Function called in case of an error while rendering the annotation layer. * * @example (error) => alert('Error while rendering annotation layer! ' + error.message) */ onRenderAnnotationLayerError?: OnRenderAnnotationLayerError; /** * Function called when annotations are successfully rendered on the screen. * * @example () => alert('Rendered the annotation layer!') */ onRenderAnnotationLayerSuccess?: OnRenderAnnotationLayerSuccess; /** * Function called in case of an error while rendering the page. * * @example (error) => alert('Error while loading page! ' + error.message) */ onRenderError?: OnRenderError; /** * Function called when the page is successfully rendered on the screen. * * @example () => alert('Rendered the page!') */ onRenderSuccess?: OnRenderSuccess; /** * Function called in case of an error while rendering the text layer. * * @example (error) => alert('Error while rendering text layer! ' + error.message) */ onRenderTextLayerError?: OnRenderTextLayerError; /** * Function called when the text layer is successfully rendered on the screen. * * @example () => alert('Rendered the text layer!') */ onRenderTextLayerSuccess?: OnRenderTextLayerSuccess; /** * Colors used to render the page. If not provided, the default colors from PDF will be used. * * @example { background: 'transparent', foreground: '#ff0000' } */ pageColors?: PageColors; /** * Which page from PDF file should be displayed, by page index. Ignored if `pageNumber` prop is provided. * * @default 0 * @example 1 */ pageIndex?: number; /** * Which page from PDF file should be displayed, by page number. If provided, `pageIndex` prop will be ignored. * * @default 1 * @example 2 */ pageNumber?: number; /** * pdf object obtained from ``'s `onLoadSuccess` callback function. * * @example pdf */ pdf?: PDFDocumentProxy | false; registerPage?: undefined; /** * Whether annotations (e.g. links) should be rendered. * * @default true * @example false */ renderAnnotationLayer?: boolean; /** * Whether forms should be rendered. `renderAnnotationLayer` prop must be set to `true`. * * @default false * @example true */ renderForms?: boolean; /** * Rendering mode of the document. Can be `"canvas"`, `"custom"` or `"none"`. If set to `"custom"`, `customRenderer` must also be provided. * * @default 'canvas' * @example 'custom' */ renderMode?: RenderMode; /** * Whether a text layer should be rendered. * * @default true * @example false */ renderTextLayer?: boolean; /** * Rotation of the page in degrees. `90` = rotated to the right, `180` = upside down, `270` = rotated to the left. * * @default 0 * @example 90 */ rotate?: number | null; /** * Page scale. * * @default 1 * @example 0.5 */ scale?: number; unregisterPage?: undefined; /** * Page width. If neither `height` nor `width` are defined, page will be rendered at the size defined in PDF. If you define `width` and `height` at the same time, `height` will be ignored. If you define `width` and `scale` at the same time, the width will be multiplied by a given factor. * * @example 300 */ width?: number; } & EventProps; /** * Displays a page. * * Should be placed inside ``. Alternatively, it can have `pdf` prop passed, which can be obtained from ``'s `onLoadSuccess` callback function, however some advanced functions like linking between pages inside a document may not be working correctly. */ export default function Page(props: PageProps): React.ReactElement { const documentContext = useDocumentContext(); const mergedProps = { ...documentContext, ...props }; const { _className = 'react-pdf__Page', _enableRegisterUnregisterPage = true, canvasBackground, canvasRef, children, className, customRenderer: CustomRenderer, customTextRenderer, devicePixelRatio, error = 'Failed to load the page.', filterAnnotations, height, inputRef, loading = 'Loading page…', noData = 'No page specified.', onGetAnnotationsError: onGetAnnotationsErrorProps, onGetAnnotationsSuccess: onGetAnnotationsSuccessProps, onGetStructTreeError: onGetStructTreeErrorProps, onGetStructTreeSuccess: onGetStructTreeSuccessProps, onGetTextError: onGetTextErrorProps, onGetTextSuccess: onGetTextSuccessProps, onLoadError: onLoadErrorProps, onLoadSuccess: onLoadSuccessProps, onRenderAnnotationLayerError: onRenderAnnotationLayerErrorProps, onRenderAnnotationLayerSuccess: onRenderAnnotationLayerSuccessProps, onRenderError: onRenderErrorProps, onRenderSuccess: onRenderSuccessProps, onRenderTextLayerError: onRenderTextLayerErrorProps, onRenderTextLayerSuccess: onRenderTextLayerSuccessProps, pageColors, pageIndex: pageIndexProps, pageNumber: pageNumberProps, pdf, registerPage, renderAnnotationLayer: renderAnnotationLayerProps = true, renderForms = false, renderMode = 'canvas', renderTextLayer: renderTextLayerProps = true, rotate: rotateProps, scale: scaleProps = defaultScale, unregisterPage, width, ...otherProps } = mergedProps; const [pageState, pageDispatch] = useResolver(); const { value: page, error: pageError } = pageState; const pageElement = useRef(null); invariant( pdf, 'Attempted to load a page, but no document was specified. Wrap in a or pass explicit `pdf` prop.', ); const pageIndex = isProvided(pageNumberProps) ? pageNumberProps - 1 : (pageIndexProps ?? null); const pageNumber = pageNumberProps ?? (isProvided(pageIndexProps) ? pageIndexProps + 1 : null); const rotate = rotateProps ?? (page ? page.rotate : null); const scale = useMemo(() => { if (!page) { return null; } // Be default, we'll render page at 100% * scale width. let pageScale = 1; // Passing scale explicitly null would cause the page not to render const scaleWithDefault = scaleProps ?? defaultScale; // If width/height is defined, calculate the scale of the page so it could be of desired width. if (width || height) { const viewport = page.getViewport({ scale: 1, rotation: rotate as number }); if (width) { pageScale = width / viewport.width; } else if (height) { pageScale = height / viewport.height; } } return scaleWithDefault * pageScale; }, [height, page, rotate, scaleProps, width]); // biome-ignore lint/correctness/useExhaustiveDependencies: useEffect intentionally triggered on pdf change useEffect( function hook() { return () => { if (!isProvided(pageIndex)) { // Impossible, but TypeScript doesn't know that return; } if (_enableRegisterUnregisterPage && unregisterPage) { unregisterPage(pageIndex); } }; }, [_enableRegisterUnregisterPage, pdf, pageIndex, unregisterPage], ); /** * Called when a page is loaded successfully */ function onLoadSuccess() { if (onLoadSuccessProps) { if (!page || !scale) { // Impossible, but TypeScript doesn't know that return; } onLoadSuccessProps(makePageCallback(page, scale)); } if (_enableRegisterUnregisterPage && registerPage) { if (!isProvided(pageIndex) || !pageElement.current) { // Impossible, but TypeScript doesn't know that return; } registerPage(pageIndex, pageElement.current); } } /** * Called when a page failed to load */ function onLoadError() { if (!pageError) { // Impossible, but TypeScript doesn't know that return; } warning(false, pageError.toString()); if (onLoadErrorProps) { onLoadErrorProps(pageError); } } // biome-ignore lint/correctness/useExhaustiveDependencies: useEffect intentionally triggered on pdf and pageIndex change useEffect( function resetPage() { pageDispatch({ type: 'RESET' }); }, [pageDispatch, pdf, pageIndex], ); useEffect( function loadPage() { if (!pdf || !pageNumber) { return; } const cancellable = makeCancellable(pdf.getPage(pageNumber)); const runningTask = cancellable; cancellable.promise .then((nextPage) => { pageDispatch({ type: 'RESOLVE', value: nextPage }); }) .catch((error) => { pageDispatch({ type: 'REJECT', error }); }); return () => cancelRunningTask(runningTask); }, [pageDispatch, pdf, pageNumber], ); // biome-ignore lint/correctness/useExhaustiveDependencies: Omitted callbacks so they are not called every time they change useEffect(() => { if (page === undefined) { return; } if (page === false) { onLoadError(); return; } onLoadSuccess(); }, [page, scale]); const childContext = useMemo( () => // Technically there cannot be page without pageIndex, pageNumber, rotate and scale, but TypeScript doesn't know that isProvided(pageIndex) && pageNumber && isProvided(rotate) && isProvided(scale) ? { _className, canvasBackground, customTextRenderer, devicePixelRatio, filterAnnotations, onGetAnnotationsError: onGetAnnotationsErrorProps, onGetAnnotationsSuccess: onGetAnnotationsSuccessProps, onGetStructTreeError: onGetStructTreeErrorProps, onGetStructTreeSuccess: onGetStructTreeSuccessProps, onGetTextError: onGetTextErrorProps, onGetTextSuccess: onGetTextSuccessProps, onRenderAnnotationLayerError: onRenderAnnotationLayerErrorProps, onRenderAnnotationLayerSuccess: onRenderAnnotationLayerSuccessProps, onRenderError: onRenderErrorProps, onRenderSuccess: onRenderSuccessProps, onRenderTextLayerError: onRenderTextLayerErrorProps, onRenderTextLayerSuccess: onRenderTextLayerSuccessProps, page, pageColors, pageIndex, pageNumber, renderForms, renderTextLayer: renderTextLayerProps, rotate, scale, } : null, [ _className, canvasBackground, customTextRenderer, devicePixelRatio, filterAnnotations, onGetAnnotationsErrorProps, onGetAnnotationsSuccessProps, onGetStructTreeErrorProps, onGetStructTreeSuccessProps, onGetTextErrorProps, onGetTextSuccessProps, onRenderAnnotationLayerErrorProps, onRenderAnnotationLayerSuccessProps, onRenderErrorProps, onRenderSuccessProps, onRenderTextLayerErrorProps, onRenderTextLayerSuccessProps, page, pageColors, pageIndex, pageNumber, renderForms, renderTextLayerProps, rotate, scale, ], ); const eventProps = useMemo( () => makeEventProps(otherProps, () => page ? (scale ? makePageCallback(page, scale) : undefined) : page, ), // biome-ignore lint/correctness/useExhaustiveDependencies: FIXME [otherProps, page, scale], ); const pageKey = `${pageIndex}@${scale}/${rotate}`; function renderMainLayer() { switch (renderMode) { case 'custom': { invariant( CustomRenderer, `renderMode was set to "custom", but no customRenderer was passed.`, ); return ; } case 'none': return null; case 'canvas': default: return ; } } function renderTextLayer() { if (!renderTextLayerProps) { return null; } return ; } function renderAnnotationLayer() { if (!renderAnnotationLayerProps) { return null; } return ; } function renderChildren() { function isFulfilledContext(context: PageContextType): context is PageRenderProps { return Boolean(context?.page); } if (!isFulfilledContext(childContext)) { // Impossible, but TypeScript doesn't know that throw new Error('page is undefined'); } const resolvedChildren = typeof children === 'function' ? children(childContext) : children; return ( {renderMainLayer()} {renderTextLayer()} {renderAnnotationLayer()} {resolvedChildren} ); } function renderContent() { if (!pageNumber) { return {typeof noData === 'function' ? noData() : noData}; } if (pdf === null || page === undefined || page === null) { return ( {typeof loading === 'function' ? loading() : loading} ); } if (pdf === false || page === false) { return {typeof error === 'function' ? error() : error}; } return renderChildren(); } return (
    , pageElement)} style={ { '--scale-round-x': '1px', '--scale-round-y': '1px', '--scale-factor': '1', '--user-unit': `${scale}`, '--total-scale-factor': 'calc(var(--scale-factor) * var(--user-unit))', backgroundColor: canvasBackground || 'white', position: 'relative', minWidth: 'min-content', minHeight: 'min-content', } as React.CSSProperties } {...eventProps} > {renderContent()}
    ); } ================================================ FILE: packages/react-pdf/src/PageContext.tsx ================================================ 'use client'; import { createContext } from 'react'; import type { PageContextType } from './shared/types.js'; const pageContext: React.Context = createContext(null); export default pageContext; ================================================ FILE: packages/react-pdf/src/PasswordResponses.ts ================================================ // As defined in https://github.com/mozilla/pdf.js/blob/d9fac3459609a807be6506fb3441b5da4b154d14/src/shared/util.js#L371-L374 const PasswordResponses = { NEED_PASSWORD: 1, INCORRECT_PASSWORD: 2, } as const; export default PasswordResponses; ================================================ FILE: packages/react-pdf/src/Ref.spec.ts ================================================ import { describe, expect, it } from 'vitest'; import Ref from './Ref.js'; describe('Ref', () => { it('returns proper reference for given num and gen', () => { const num = 1; const gen = 2; const ref = new Ref({ num, gen }); expect(ref.toString()).toBe('1R2'); }); it('returns proper reference for given num and gen when gen = 0', () => { const num = 1; const gen = 0; const ref = new Ref({ num, gen }); expect(ref.toString()).toBe('1R'); }); }); ================================================ FILE: packages/react-pdf/src/Ref.ts ================================================ export default class Ref { num: number; gen: number; constructor({ num, gen }: { num: number; gen: number }) { this.num = num; this.gen = gen; } toString(): string { let str = `${this.num}R`; if (this.gen !== 0) { str += this.gen; } return str; } } ================================================ FILE: packages/react-pdf/src/StructTree.spec.tsx ================================================ import { beforeAll, describe, expect, it } from 'vitest'; import { render } from 'vitest-browser-react'; import { pdfjs } from './index.test.js'; import PageContext from './PageContext.js'; import StructTree from './StructTree.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 { StructTreeNode } from 'pdfjs-dist/types/src/display/api.js'; import type { PageContextType } from './shared/types.js'; const pdfFile = await loadPDF('../../__mocks__/_pdf.pdf'); async function renderWithContext(children: React.ReactNode, context: Partial) { const { rerender, ...otherResult } = await render( {children}, ); return { ...otherResult, rerender: async ( nextChildren: React.ReactNode, nextContext: Partial = context, ) => await rerender( {nextChildren} , ), }; } describe('StructTree', () => { // Loaded page let page: PDFPageProxy; let page2: PDFPageProxy; // Loaded structure tree let desiredStructTree: StructTreeNode; let desiredStructTree2: StructTreeNode; beforeAll(async () => { const pdf = await pdfjs.getDocument({ data: pdfFile.arrayBuffer }).promise; page = await pdf.getPage(1); desiredStructTree = await page.getStructTree(); page2 = await pdf.getPage(2); desiredStructTree2 = await page2.getStructTree(); }); describe('loading', () => { it('loads structure tree and calls onGetStructTreeSuccess callback properly', async () => { const { func: onGetStructTreeSuccess, promise: onGetStructTreeSuccessPromise } = makeAsyncCallback(); await renderWithContext(, { onGetStructTreeSuccess, page, }); expect.assertions(1); await expect(onGetStructTreeSuccessPromise).resolves.toMatchObject([desiredStructTree]); }); it('calls onGetStructTreeError when failed to load annotations', async () => { const { func: onGetStructTreeError, promise: onGetStructTreeErrorPromise } = makeAsyncCallback(); muteConsole(); await renderWithContext(, { onGetStructTreeError, page: failingPage, }); expect.assertions(1); await expect(onGetStructTreeErrorPromise).resolves.toMatchObject([expect.any(Error)]); restoreConsole(); }); it('replaces structure tree properly when page is changed', async () => { const { func: onGetStructTreeSuccess, promise: onGetStructTreeSuccessPromise } = makeAsyncCallback(); const { rerender } = await renderWithContext(, { onGetStructTreeSuccess, page, }); expect.assertions(2); await expect(onGetStructTreeSuccessPromise).resolves.toMatchObject([desiredStructTree]); const { func: onGetStructTreeSuccess2, promise: onGetStructTreeSuccessPromise2 } = makeAsyncCallback(); await rerender(, { onGetStructTreeSuccess: onGetStructTreeSuccess2, page: page2, }); await expect(onGetStructTreeSuccessPromise2).resolves.toMatchObject([desiredStructTree2]); }); it('throws an error when placed outside Page', async () => { muteConsole(); await expect(render()).rejects.toThrowError( 'Invariant failed: Unable to find Page context.', ); restoreConsole(); }); }); describe('rendering', () => { it('renders structure tree properly', async () => { const { func: onGetStructTreeSuccess, promise: onGetStructTreeSuccessPromise } = makeAsyncCallback(); const { container } = await renderWithContext(, { onGetStructTreeSuccess, page, }); expect.assertions(1); await onGetStructTreeSuccessPromise; const wrapper = container.firstElementChild as HTMLSpanElement; expect(wrapper.outerHTML).toBe( '', ); }); }); }); ================================================ FILE: packages/react-pdf/src/StructTree.tsx ================================================ import { useEffect } from 'react'; import makeCancellable from 'make-cancellable-promise'; import invariant from 'tiny-invariant'; import warning from 'warning'; import StructTreeItem from './StructTreeItem.js'; import usePageContext from './shared/hooks/usePageContext.js'; import useResolver from './shared/hooks/useResolver.js'; import { cancelRunningTask } from './shared/utils.js'; import type { StructTreeNodeWithExtraAttributes } from './shared/types.js'; export default function StructTree(): React.ReactElement | null { const pageContext = usePageContext(); invariant(pageContext, 'Unable to find Page context.'); const { onGetStructTreeError: onGetStructTreeErrorProps, onGetStructTreeSuccess: onGetStructTreeSuccessProps, } = pageContext; const [structTreeState, structTreeDispatch] = useResolver(); const { value: structTree, error: structTreeError } = structTreeState; const { customTextRenderer, page } = pageContext; function onLoadSuccess() { if (!structTree) { // Impossible, but TypeScript doesn't know that return; } if (onGetStructTreeSuccessProps) { onGetStructTreeSuccessProps(structTree); } } function onLoadError() { if (!structTreeError) { // Impossible, but TypeScript doesn't know that return; } warning(false, structTreeError.toString()); if (onGetStructTreeErrorProps) { onGetStructTreeErrorProps(structTreeError); } } // biome-ignore lint/correctness/useExhaustiveDependencies: useEffect intentionally triggered on page change useEffect( function resetStructTree() { structTreeDispatch({ type: 'RESET' }); }, [structTreeDispatch, page], ); useEffect( function loadStructTree() { if (customTextRenderer) { // TODO: Document why this is necessary return; } if (!page) { return; } const cancellable = makeCancellable(page.getStructTree()); const runningTask = cancellable; cancellable.promise .then((nextStructTree) => { structTreeDispatch({ type: 'RESOLVE', value: nextStructTree }); }) .catch((error) => { structTreeDispatch({ type: 'REJECT', error }); }); return () => cancelRunningTask(runningTask); }, [customTextRenderer, page, structTreeDispatch], ); // biome-ignore lint/correctness/useExhaustiveDependencies: Omitted callbacks so they are not called every time they change useEffect(() => { if (structTree === undefined) { return; } if (structTree === false) { onLoadError(); return; } onLoadSuccess(); }, [structTree]); if (!structTree) { return null; } return ; } ================================================ FILE: packages/react-pdf/src/StructTreeItem.tsx ================================================ import { useMemo } from 'react'; import { getAttributes, isStructTreeNode, isStructTreeNodeWithOnlyContentChild, } from './shared/structTreeUtils.js'; import type { StructTreeContent } from 'pdfjs-dist/types/src/display/api.js'; import type { StructTreeNodeWithExtraAttributes } from './shared/types.js'; type StructTreeItemProps = { className?: string; node: StructTreeNodeWithExtraAttributes | StructTreeContent; }; export default function StructTreeItem({ className, node, }: StructTreeItemProps): React.ReactElement { const attributes = useMemo(() => getAttributes(node), [node]); const children = useMemo(() => { if (!isStructTreeNode(node)) { return null; } if (isStructTreeNodeWithOnlyContentChild(node)) { return null; } return node.children.map((child, index) => { return ( // biome-ignore lint/suspicious/noArrayIndexKey: index is stable here ); }); }, [node]); return ( {children} ); } ================================================ FILE: packages/react-pdf/src/Thumbnail.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 DocumentContext from './DocumentContext.js'; import { pdfjs } from './index.test.js'; import LinkService from './LinkService.js'; import Thumbnail from './Thumbnail.js'; import failingPdf from '../../../__mocks__/_failing_pdf.js'; import silentlyFailingPdf from '../../../__mocks__/_silently_failing_pdf.js'; import { loadPDF, makeAsyncCallback, muteConsole, restoreConsole } from '../../../test-utils.js'; import type { PDFDocumentProxy, PDFPageProxy } from 'pdfjs-dist'; import type { DocumentContextType, PageCallback } from './shared/types.js'; const pdfFile = await loadPDF('../../__mocks__/_pdf.pdf'); const pdfFile2 = await loadPDF('../../__mocks__/_pdf2.pdf'); async function renderWithContext(children: React.ReactNode, context: Partial) { const { rerender, ...otherResult } = await render( {children} , ); return { ...otherResult, rerender: async ( nextChildren: React.ReactNode, nextContext: Partial = context, ) => await rerender( {nextChildren} , ), }; } describe('Thumbnail', () => { // Loaded PDF file let pdf: PDFDocumentProxy; let pdf2: PDFDocumentProxy; // Object with basic loaded page information that shall match after successful loading const desiredLoadedThumbnail: Partial = {}; const desiredLoadedThumbnail2: Partial = {}; const desiredLoadedThumbnail3: Partial = {}; const linkService = new LinkService(); beforeAll(async () => { pdf = await pdfjs.getDocument({ data: pdfFile.arrayBuffer }).promise; const page = await pdf.getPage(1); desiredLoadedThumbnail._pageIndex = page._pageIndex; desiredLoadedThumbnail._pageInfo = page._pageInfo; const page2 = await pdf.getPage(2); desiredLoadedThumbnail2._pageIndex = page2._pageIndex; desiredLoadedThumbnail2._pageInfo = page2._pageInfo; pdf2 = await pdfjs.getDocument({ data: pdfFile2.arrayBuffer }).promise; const page3 = await pdf2.getPage(1); desiredLoadedThumbnail3._pageIndex = page3._pageIndex; desiredLoadedThumbnail3._pageInfo = page3._pageInfo; }); describe('loading', () => { it('loads a page and calls onLoadSuccess callback properly when placed inside Document', async () => { const { func: onLoadSuccess, promise: onLoadSuccessPromise } = makeAsyncCallback(); await renderWithContext(, { pdf }); expect.assertions(1); await expect(onLoadSuccessPromise).resolves.toMatchObject([desiredLoadedThumbnail]); }); it('loads a page and calls onLoadSuccess callback properly when pdf prop is passed', async () => { const { func: onLoadSuccess, promise: onLoadSuccessPromise } = makeAsyncCallback(); await render(); expect.assertions(1); await expect(onLoadSuccessPromise).resolves.toMatchObject([desiredLoadedThumbnail]); }); it('returns all desired parameters in onLoadSuccess callback', async () => { const { func: onLoadSuccess, promise: onLoadSuccessPromise } = makeAsyncCallback<[PageCallback]>(); await renderWithContext(, { pdf }); expect.assertions(5); const [page] = await onLoadSuccessPromise; expect(page.width).toBeDefined(); expect(page.height).toBeDefined(); expect(page.originalWidth).toBeDefined(); expect(page.originalHeight).toBeDefined(); // Example of a method that got stripped away in the past expect(page.getTextContent).toBeInstanceOf(Function); }); it('calls onLoadError when failed to load a page', async () => { const { func: onLoadError, promise: onLoadErrorPromise } = makeAsyncCallback(); muteConsole(); await renderWithContext(, { pdf: failingPdf, }); expect.assertions(1); await expect(onLoadErrorPromise).resolves.toMatchObject([expect.any(Error)]); restoreConsole(); }); it('loads page when given pageIndex', async () => { const { func: onLoadSuccess, promise: onLoadSuccessPromise } = makeAsyncCallback(); await renderWithContext(, { pdf }); expect.assertions(1); const [page] = await onLoadSuccessPromise; expect(page).toMatchObject(desiredLoadedThumbnail); }); it('loads page when given pageNumber', async () => { const { func: onLoadSuccess, promise: onLoadSuccessPromise } = makeAsyncCallback(); await renderWithContext(, { pdf }); expect.assertions(1); const [page] = await onLoadSuccessPromise; expect(page).toMatchObject(desiredLoadedThumbnail); }); it('loads page of a given number when given conflicting pageNumber and pageIndex', async () => { const { func: onLoadSuccess, promise: onLoadSuccessPromise } = makeAsyncCallback(); await renderWithContext( , { pdf, }, ); expect.assertions(1); const [page] = await onLoadSuccessPromise; expect(page).toMatchObject(desiredLoadedThumbnail); }); it('replaces a page properly when pdf is changed', async () => { const { func: onLoadSuccess, promise: onLoadSuccessPromise } = makeAsyncCallback(); const { rerender } = await renderWithContext( , { pdf, }, ); expect.assertions(2); await expect(onLoadSuccessPromise).resolves.toMatchObject([desiredLoadedThumbnail]); const { func: onLoadSuccess2, promise: onLoadSuccessPromise2 } = makeAsyncCallback(); await rerender(, { pdf: pdf2 }); await expect(onLoadSuccessPromise2).resolves.toMatchObject([desiredLoadedThumbnail3]); }); it('replaces a page properly when pageNumber is changed', async () => { const { func: onLoadSuccess, promise: onLoadSuccessPromise } = makeAsyncCallback(); const { rerender } = await renderWithContext( , { pdf, }, ); expect.assertions(2); await expect(onLoadSuccessPromise).resolves.toMatchObject([desiredLoadedThumbnail]); const { func: onLoadSuccess2, promise: onLoadSuccessPromise2 } = makeAsyncCallback(); await rerender(, { pdf }); await expect(onLoadSuccessPromise2).resolves.toMatchObject([desiredLoadedThumbnail2]); }); it('throws an error when placed outside Document without pdf prop passed', async () => { muteConsole(); await expect(render()).rejects.toThrowError( 'Invariant failed: Attempted to load a thumbnail, but no document was specified. Wrap in a or pass explicit `pdf` prop.', ); restoreConsole(); }); }); describe('rendering', () => { it('applies className to its wrapper when given a string', async () => { const className = 'testClassName'; const { container } = await renderWithContext( , { pdf, }, ); const wrapper = container.querySelector('.react-pdf__Thumbnail'); expect(wrapper).toHaveClass(className); }); it('passes container element to inputRef properly', async () => { const inputRef = createRef(); await renderWithContext(, { pdf: silentlyFailingPdf, }); expect(inputRef.current).toBeInstanceOf(HTMLDivElement); }); it('passes canvas element to ThumbnailCanvas properly', async () => { const { func: onLoadSuccess, promise: onLoadSuccessPromise } = makeAsyncCallback(); const canvasRef = createRef(); const { container } = await renderWithContext( , { pdf }, ); expect.assertions(1); await onLoadSuccessPromise; const pageCanvas = container.querySelector('.react-pdf__Thumbnail__page__canvas'); expect(canvasRef.current).toBe(pageCanvas); }); it('renders "No page specified." when given neither pageIndex nor pageNumber', async () => { muteConsole(); const { container } = await renderWithContext(, { pdf }); const noData = container.querySelector('.react-pdf__message'); expect(noData).toBeInTheDocument(); expect(noData).toHaveTextContent('No page specified.'); restoreConsole(); }); it('renders custom no data message when given nothing and noData is given', async () => { muteConsole(); const { container } = await renderWithContext(, { pdf }); const noData = container.querySelector('.react-pdf__message'); expect(noData).toBeInTheDocument(); expect(noData).toHaveTextContent('Nothing here'); restoreConsole(); }); it('renders custom no data message when given nothing and noData is given as a function', async () => { muteConsole(); const { container } = await renderWithContext( 'Nothing here'} />, { pdf, }); const noData = container.querySelector('.react-pdf__message'); expect(noData).toBeInTheDocument(); expect(noData).toHaveTextContent('Nothing here'); restoreConsole(); }); it('renders "Loading page…" when loading a page', () => { renderWithContext(, { pdf }); const loading = page.getByText('Loading page…'); expect(loading).toBeInTheDocument(); }); it('renders custom loading message when loading a page and loading prop is given', () => { renderWithContext(, { pdf }); const loading = page.getByText('Loading', { exact: true }); expect(loading).toBeInTheDocument(); }); it('renders custom loading message when loading a page and loading prop is given as a function', () => { renderWithContext( 'Loading'} pageIndex={0} />, { pdf }); const loading = page.getByText('Loading', { exact: true }); expect(loading).toBeInTheDocument(); }); it('ignores pageIndex when given pageIndex and pageNumber', async () => { const { func: onLoadSuccess, promise: onLoadSuccessPromise } = makeAsyncCallback(); await renderWithContext( , { pdf, }, ); expect.assertions(1); const [page] = await onLoadSuccessPromise; expect(page).toMatchObject(desiredLoadedThumbnail); }); it('requests page to be rendered with default rotation when given nothing', async () => { const { func: onRenderSuccess, promise: onRenderSuccessPromise } = makeAsyncCallback<[PageCallback]>(); const { container } = await renderWithContext( , { pdf }, ); const [page] = await onRenderSuccessPromise; const pageCanvas = container.querySelector( '.react-pdf__Thumbnail__page__canvas', ) as HTMLCanvasElement; const { width, height } = window.getComputedStyle(pageCanvas); const viewport = page.getViewport({ scale: 1 }); // Expect the canvas layer not to be rotated expect(Number.parseInt(width, 10)).toBe(Math.floor(viewport.width)); expect(Number.parseInt(height, 10)).toBe(Math.floor(viewport.height)); }); it('requests page to be rendered with given rotation when given rotate prop', async () => { const { func: onRenderSuccess, promise: onRenderSuccessPromise } = makeAsyncCallback<[PageCallback]>(); const rotate = 90; const { container } = await renderWithContext( , { pdf }, ); const [page] = await onRenderSuccessPromise; const pageCanvas = container.querySelector( '.react-pdf__Thumbnail__page__canvas', ) as HTMLCanvasElement; const { width, height } = window.getComputedStyle(pageCanvas); const viewport = page.getViewport({ scale: 1, rotation: rotate }); // Expect the canvas layer to be rotated expect(Number.parseInt(width, 10)).toBe(Math.floor(viewport.width)); expect(Number.parseInt(height, 10)).toBe(Math.floor(viewport.height)); }); it('requests page to be rendered in canvas mode by default', async () => { const { func: onLoadSuccess, promise: onLoadSuccessPromise } = makeAsyncCallback(); const { container } = await renderWithContext( , { pdf }, ); expect.assertions(1); await onLoadSuccessPromise; const pageCanvas = container.querySelector('.react-pdf__Thumbnail__page__canvas'); expect(pageCanvas).toBeInTheDocument(); }); it('requests page not to be rendered when given renderMode = "none"', async () => { const { func: onLoadSuccess, promise: onLoadSuccessPromise } = makeAsyncCallback(); const { container } = await renderWithContext( , { pdf }, ); expect.assertions(1); await onLoadSuccessPromise; const pageCanvas = container.querySelector('.react-pdf__Thumbnail__page__canvas'); expect(pageCanvas).not.toBeInTheDocument(); }); it('requests page to be rendered in canvas mode when given renderMode = "canvas"', async () => { const { func: onLoadSuccess, promise: onLoadSuccessPromise } = makeAsyncCallback(); const { container } = await renderWithContext( , { pdf }, ); expect.assertions(1); await onLoadSuccessPromise; const pageCanvas = container.querySelector('.react-pdf__Thumbnail__page__canvas'); expect(pageCanvas).toBeInTheDocument(); }); it('requests page to be rendered in custom mode when given renderMode = "custom"', async () => { const { func: onLoadSuccess, promise: onLoadSuccessPromise } = makeAsyncCallback(); function CustomRenderer() { return
    ; } const { container } = await renderWithContext( , { pdf }, ); expect.assertions(1); await onLoadSuccessPromise; const customRenderer = container.querySelector('.custom-renderer'); expect(customRenderer).toBeInTheDocument(); }); }); it('requests page to be rendered at its original size given nothing', async () => { const { func: onLoadSuccess, promise: onLoadSuccessPromise } = makeAsyncCallback<[PageCallback]>(); await renderWithContext(, { pdf }); expect.assertions(1); const [page] = await onLoadSuccessPromise; expect(page.width).toEqual(page.originalWidth); }); it('requests page to be rendered with a proper scale when given scale', async () => { const { func: onLoadSuccess, promise: onLoadSuccessPromise } = makeAsyncCallback<[PageCallback]>(); const scale = 1.5; await renderWithContext( , { pdf, }, ); expect.assertions(1); const [page] = await onLoadSuccessPromise; expect(page.width).toEqual(page.originalWidth * scale); }); it('requests page to be rendered with a proper scale when given width', async () => { const { func: onLoadSuccess, promise: onLoadSuccessPromise } = makeAsyncCallback<[PageCallback]>(); const width = 600; await renderWithContext( , { pdf, }, ); expect.assertions(1); const [page] = await onLoadSuccessPromise; expect(page.width).toEqual(width); }); it('requests page to be rendered with a proper scale when given width and scale (multiplies)', async () => { const { func: onLoadSuccess, promise: onLoadSuccessPromise } = makeAsyncCallback<[PageCallback]>(); const width = 600; const scale = 1.5; await renderWithContext( , { pdf, }, ); expect.assertions(1); const [page] = await onLoadSuccessPromise; expect(page.width).toBeCloseTo(width * scale); }); it('requests page to be rendered with a proper scale when given height', async () => { const { func: onLoadSuccess, promise: onLoadSuccessPromise } = makeAsyncCallback<[PageCallback]>(); const height = 850; await renderWithContext( , { pdf, }, ); expect.assertions(1); const [page] = await onLoadSuccessPromise; expect(page.height).toEqual(height); }); it('requests page to be rendered with a proper scale when given height and scale (multiplies)', async () => { const { func: onLoadSuccess, promise: onLoadSuccessPromise } = makeAsyncCallback<[PageCallback]>(); const height = 850; const scale = 1.5; await renderWithContext( , { pdf, }, ); expect.assertions(1); const [page] = await onLoadSuccessPromise; expect(page.height).toBeCloseTo(height * scale); }); it('requests page to be rendered with a proper scale when given width and height (ignores height)', async () => { const { func: onLoadSuccess, promise: onLoadSuccessPromise } = makeAsyncCallback<[PageCallback]>(); const width = 600; const height = 100; await renderWithContext( , { pdf, }, ); expect.assertions(2); const [page] = await onLoadSuccessPromise; expect(page.width).toEqual(width); // Expect proportions to be correct even though invalid height was provided expect(page.height).toEqual(page.originalHeight * (page.width / page.originalWidth)); }); it('requests page to be rendered with a proper scale when given width, height and scale (ignores height, multiplies)', async () => { const { func: onLoadSuccess, promise: onLoadSuccessPromise } = makeAsyncCallback<[PageCallback]>(); const width = 600; const height = 100; const scale = 1.5; await renderWithContext( , { pdf }, ); expect.assertions(2); const [page] = await onLoadSuccessPromise; expect(page.width).toBeCloseTo(width * scale); // Expect proportions to be correct even though invalid height was provided expect(page.height).toEqual(page.originalHeight * (page.width / page.originalWidth)); }); it('calls onClick callback when clicked a page (sample of mouse events family)', async () => { const onClick = vi.fn(); const { container } = await renderWithContext(, { linkService, pdf, }); const page = container.querySelector('.react-pdf__Thumbnail__page') as HTMLDivElement; await userEvent.click(page); expect(onClick).toHaveBeenCalled(); }); function triggerTouchStart(element: HTMLElement) { element.dispatchEvent(new TouchEvent('touchstart', { bubbles: true, cancelable: true })); } it('calls onTouchStart callback when touched a page (sample of touch events family)', async () => { const onTouchStart = vi.fn(); const { container } = await renderWithContext(, { linkService, pdf, }); const page = container.querySelector('.react-pdf__Thumbnail__page') as HTMLDivElement; triggerTouchStart(page); expect(onTouchStart).toHaveBeenCalled(); }); }); ================================================ FILE: packages/react-pdf/src/Thumbnail.tsx ================================================ 'use client'; import clsx from 'clsx'; import invariant from 'tiny-invariant'; import Page from './Page.js'; import useDocumentContext from './shared/hooks/useDocumentContext.js'; import { isProvided } from './shared/utils.js'; import type { PageProps } from './Page.js'; import type { ClassName, OnItemClickArgs } from './shared/types.js'; export type ThumbnailProps = Omit< PageProps, | 'className' | 'customTextRenderer' | 'onGetAnnotationsError' | 'onGetAnnotationsSuccess' | 'onGetTextError' | 'onGetTextSuccess' | 'onRenderAnnotationLayerError' | 'onRenderAnnotationLayerSuccess' | 'onRenderTextLayerError' | 'onRenderTextLayerSuccess' | 'renderAnnotationLayer' | 'renderForms' | 'renderTextLayer' > & { /** * Class name(s) that will be added to rendered element along with the default `react-pdf__Thumbnail`. * * @example 'custom-class-name-1 custom-class-name-2' * @example ['custom-class-name-1', 'custom-class-name-2'] */ className?: ClassName; /** * Function called when a thumbnail has been clicked. Usually, you would like to use this callback to move the user wherever they requested to. * * @example ({ dest, pageIndex, pageNumber }) => alert('Clicked an item from page ' + pageNumber + '!') */ onItemClick?: (args: OnItemClickArgs) => void; }; /** * Displays a thumbnail of a page. Does not render the annotation layer or the text layer. Does not register itself as a link target, so the user will not be scrolled to a Thumbnail component when clicked on an internal link (e.g. in Table of Contents). When clicked, attempts to navigate to the page clicked (similarly to a link in Outline). * * Should be placed inside ``. Alternatively, it can have `pdf` prop passed, which can be obtained from ``'s `onLoadSuccess` callback function. */ export default function Thumbnail(props: ThumbnailProps): React.ReactElement { const documentContext = useDocumentContext(); const mergedProps = { ...documentContext, ...props }; const { className, linkService, onItemClick, pageIndex: pageIndexProps, pageNumber: pageNumberProps, pdf, } = mergedProps; invariant( pdf, 'Attempted to load a thumbnail, but no document was specified. Wrap in a or pass explicit `pdf` prop.', ); const pageIndex = isProvided(pageNumberProps) ? pageNumberProps - 1 : (pageIndexProps ?? null); const pageNumber = pageNumberProps ?? (isProvided(pageIndexProps) ? pageIndexProps + 1 : null); function onClick(event: React.MouseEvent) { event.preventDefault(); if (!isProvided(pageIndex) || !pageNumber) { return; } invariant( onItemClick || linkService, 'Either onItemClick callback or linkService must be defined in order to navigate to an outline item.', ); if (onItemClick) { onItemClick({ pageIndex, pageNumber, }); } else if (linkService) { linkService.goToPage(pageNumber); } } const { className: classNameProps, onItemClick: onItemClickProps, ...pageProps } = props; return ( ); } ================================================ FILE: packages/react-pdf/src/index.spec.ts ================================================ import { describe, expect, it } from 'vitest'; import { Document, Outline, Page, pdfjs, Thumbnail } from './index.js'; describe('default entry', () => { describe('has pdfjs exported properly', () => { it('has pdfjs.version exported properly', () => { expect(typeof pdfjs.version).toBe('string'); }); it('has GlobalWorkerOptions exported properly', () => { expect(typeof pdfjs.GlobalWorkerOptions).toBe('function'); }); }); it('has Document exported properly', () => { expect(Document).toBeInstanceOf(Object); }); it('has Outline exported properly', () => { expect(Outline).toBeInstanceOf(Object); }); it('has Page exported properly', () => { expect(Page).toBeInstanceOf(Object); }); it('has Thumbnail exported properly', () => { expect(Thumbnail).toBeInstanceOf(Object); }); }); ================================================ FILE: packages/react-pdf/src/index.test.ts ================================================ import * as pdfjs from 'pdfjs-dist'; import Document from './Document.js'; import Outline from './Outline.js'; import Page from './Page.js'; import Thumbnail from './Thumbnail.js'; import useDocumentContext from './shared/hooks/useDocumentContext.js'; import useOutlineContext from './shared/hooks/useOutlineContext.js'; import usePageContext from './shared/hooks/usePageContext.js'; export type { DocumentProps } from './Document.js'; export type { OutlineProps } from './Outline.js'; export type { PageProps } from './Page.js'; export type { ThumbnailProps } from './Thumbnail.js'; import './pdf.worker.entry.js'; export { pdfjs, Document, Outline, Page, Thumbnail, useDocumentContext, useOutlineContext, usePageContext, }; ================================================ FILE: packages/react-pdf/src/index.ts ================================================ import * as pdfjs from 'pdfjs-dist'; import Document from './Document.js'; import Outline from './Outline.js'; import Page from './Page.js'; import PasswordResponses from './PasswordResponses.js'; import Thumbnail from './Thumbnail.js'; import useDocumentContext from './shared/hooks/useDocumentContext.js'; import useOutlineContext from './shared/hooks/useOutlineContext.js'; import usePageContext from './shared/hooks/usePageContext.js'; import type LinkService from './LinkService.js'; export type { DocumentProps } from './Document.js'; export type { OutlineProps } from './Outline.js'; export type { PageProps } from './Page.js'; export type { PasswordResponses as PasswordResponsesType, StructTreeNode, TextContent, TextItem, TextMarkedContent, } from './shared/types.js'; export type { ThumbnailProps } from './Thumbnail.js'; export type { LinkService }; import { displayWorkerWarning } from './shared/utils.js'; displayWorkerWarning(); pdfjs.GlobalWorkerOptions.workerSrc = 'pdf.worker.mjs'; export { pdfjs, Document, Outline, Page, Thumbnail, useDocumentContext, useOutlineContext, usePageContext, PasswordResponses, }; ================================================ FILE: packages/react-pdf/src/pdf.worker.entry.ts ================================================ /** * PDF.js worker entry file. * * This file is identical to Mozilla's pdf.worker.entry.js, with one exception being placed inside * this bundle, not theirs. */ ( (typeof window !== 'undefined' ? window : {}) as Window & typeof globalThis & { pdfjsWorker: unknown } ).pdfjsWorker = // @ts-expect-error - pdfjs-dist does not ship with types await import('pdfjs-dist/build/pdf.worker.mjs'); export {}; ================================================ FILE: packages/react-pdf/src/shared/constants.ts ================================================ // From pdfjs-dist/lib/web/struct_tree_layer_builder.js export const PDF_ROLE_TO_HTML_ROLE = { // Document level structure types Document: null, // There's a "document" role, but it doesn't make sense here. DocumentFragment: null, // Grouping level structure types Part: 'group', Sect: 'group', // XXX: There's a "section" role, but it's abstract. Div: 'group', Aside: 'note', NonStruct: 'none', // Block level structure types P: null, // H, H: 'heading', Title: null, FENote: 'note', // Sub-block level structure type Sub: 'group', // General inline level structure types Lbl: null, Span: null, Em: null, Strong: null, Link: 'link', Annot: 'note', Form: 'form', // Ruby and Warichu structure types Ruby: null, RB: null, RT: null, RP: null, Warichu: null, WT: null, WP: null, // List standard structure types L: 'list', LI: 'listitem', LBody: null, // Table standard structure types Table: 'table', TR: 'row', TH: 'columnheader', TD: 'cell', THead: 'columnheader', TBody: null, TFoot: null, // Standard structure type Caption Caption: null, // Standard structure type Figure Figure: 'figure', // Standard structure type Formula Formula: null, // standard structure type Artifact Artifact: null, }; export const HEADING_PATTERN: RegExp = /^H(\d+)$/; ================================================ FILE: packages/react-pdf/src/shared/hooks/useCachedValue.ts ================================================ 'use client'; import { useRef } from 'react'; import { isDefined } from '../utils.js'; export default function useCachedValue(getter: () => T): () => T { const ref = useRef(undefined); const currentValue = ref.current; if (isDefined(currentValue)) { return () => currentValue; } return () => { const value = getter(); ref.current = value; return value; }; } ================================================ FILE: packages/react-pdf/src/shared/hooks/useDocumentContext.ts ================================================ import { useContext } from 'react'; import DocumentContext from '../../DocumentContext.js'; import type { DocumentContextType } from '../types.js'; export default function useDocumentContext(): DocumentContextType { return useContext(DocumentContext); } ================================================ FILE: packages/react-pdf/src/shared/hooks/useOutlineContext.ts ================================================ import { useContext } from 'react'; import OutlineContext from '../../OutlineContext.js'; import type { OutlineContextType } from '../types.js'; export default function useOutlineContext(): OutlineContextType { return useContext(OutlineContext); } ================================================ FILE: packages/react-pdf/src/shared/hooks/usePageContext.ts ================================================ import { useContext } from 'react'; import PageContext from '../../PageContext.js'; import type { PageContextType } from '../types.js'; export default function usePageContext(): PageContextType { return useContext(PageContext); } ================================================ FILE: packages/react-pdf/src/shared/hooks/useResolver.ts ================================================ import { useReducer } from 'react'; type State = | { value: T; error: undefined } | { value: false; error: Error } | { value: undefined; error: undefined }; type Action = | { type: 'RESOLVE'; value: T } | { type: 'REJECT'; error: Error } | { type: 'RESET' }; function reducer(state: State, action: Action): State { switch (action.type) { case 'RESOLVE': return { value: action.value, error: undefined }; case 'REJECT': return { value: false, error: action.error }; case 'RESET': return { value: undefined, error: undefined }; default: return state; } } export default function useResolver(): [State, React.Dispatch>] { return useReducer(reducer, { value: undefined, error: undefined }); } ================================================ FILE: packages/react-pdf/src/shared/structTreeUtils.ts ================================================ import { HEADING_PATTERN, PDF_ROLE_TO_HTML_ROLE } from './constants.js'; import type { StructTreeContent, StructTreeNode } from 'pdfjs-dist/types/src/display/api.js'; import type { StructTreeNodeWithExtraAttributes } from './types.js'; type PdfRole = keyof typeof PDF_ROLE_TO_HTML_ROLE; type Attributes = React.HTMLAttributes; export function isPdfRole(role: string): role is PdfRole { return role in PDF_ROLE_TO_HTML_ROLE; } export function isStructTreeNode(node: StructTreeNode | StructTreeContent): node is StructTreeNode { return 'children' in node; } export function isStructTreeNodeWithOnlyContentChild( node: StructTreeNode | StructTreeContent, ): boolean { if (!isStructTreeNode(node)) { return false; } return node.children.length === 1 && 0 in node.children && 'id' in node.children[0]; } export function getRoleAttributes(node: StructTreeNode | StructTreeContent): Attributes { const attributes: Attributes = {}; if (isStructTreeNode(node)) { const { role } = node; const matches = role.match(HEADING_PATTERN); if (matches) { attributes.role = 'heading'; attributes['aria-level'] = Number(matches[1]); } else if (isPdfRole(role)) { const htmlRole = PDF_ROLE_TO_HTML_ROLE[role]; if (htmlRole) { attributes.role = htmlRole; } } } return attributes; } export function getBaseAttributes( node: StructTreeNodeWithExtraAttributes | StructTreeContent, ): Attributes { const attributes: Attributes = {}; if (isStructTreeNode(node)) { if (node.alt !== undefined) { attributes['aria-label'] = node.alt; } if (node.lang !== undefined) { attributes.lang = node.lang; } if (isStructTreeNodeWithOnlyContentChild(node)) { const [child] = node.children; if (child) { const childAttributes = getBaseAttributes(child); return { ...attributes, ...childAttributes, }; } } } else if ('id' in node) { attributes['aria-owns'] = node.id; } return attributes; } export function getAttributes( node: StructTreeNodeWithExtraAttributes | StructTreeContent, ): Attributes | null { if (!node) { return null; } return { ...getRoleAttributes(node), ...getBaseAttributes(node), }; } ================================================ FILE: packages/react-pdf/src/shared/types.ts ================================================ import type { PasswordResponses, PDFDataRangeTransport, PDFDocumentProxy, PDFPageProxy, } from 'pdfjs-dist'; import type { AnnotationLayerParameters } from 'pdfjs-dist/types/src/display/annotation_layer.js'; import type { DocumentInitParameters, RefProxy, StructTreeNode, TextContent, TextItem, TextMarkedContent, TypedArray, } from 'pdfjs-dist/types/src/display/api.js'; import type LinkService from '../LinkService.js'; export type { PasswordResponses, StructTreeNode, TextContent, TextItem, TextMarkedContent }; type NullableObject = { [P in keyof T]: T[P] | null }; type KeyOfUnion = T extends unknown ? keyof T : never; /* Primitive types */ export type Annotations = AnnotationLayerParameters['annotations']; export type ClassName = string | null | undefined | (string | null | undefined)[]; export type ResolvedDest = (RefProxy | number)[]; export type Dest = Promise | ResolvedDest | string | null; export type ExternalLinkRel = string; export type ExternalLinkTarget = '_self' | '_blank' | '_parent' | '_top'; export type FilterAnnotationsArgs = { annotations: Annotations; }; export type ImageResourcesPath = string; export type OnError = (error: Error) => void; export type OnItemClickArgs = { dest?: Dest; pageIndex: number; pageNumber: number; }; export type OnLoadProgressArgs = { loaded: number; total: number; }; export type RegisterPage = (pageIndex: number, ref: HTMLDivElement) => void; export type RenderMode = 'canvas' | 'custom' | 'none'; export type ScrollPageIntoViewArgs = { dest?: ResolvedDest; pageIndex?: number; pageNumber: number; }; type BinaryData = TypedArray | ArrayBuffer | number[] | string; export type Source = | { data: BinaryData | undefined } | { range: PDFDataRangeTransport } | { url: string }; export type UnregisterPage = (pageIndex: number) => void; /* Complex types */ export type CustomRenderer = React.FunctionComponent | React.ComponentClass; export type CustomTextRenderer = ( props: { pageIndex: number; pageNumber: number; itemIndex: number } & TextItem, ) => string; export type DocumentCallback = PDFDocumentProxy; export type File = string | ArrayBuffer | Blob | Source | null; export type PageCallback = PDFPageProxy & { width: number; height: number; originalWidth: number; originalHeight: number; }; export type NodeOrRenderer = React.ReactNode | (() => React.ReactNode); export type FilterAnnotations = (args: FilterAnnotationsArgs) => Annotations; export type OnDocumentLoadError = OnError; export type OnDocumentLoadProgress = (args: OnLoadProgressArgs) => void; export type OnDocumentLoadSuccess = (document: DocumentCallback) => void; export type OnGetAnnotationsError = OnError; export type OnGetAnnotationsSuccess = (annotations: Annotations) => void; export type OnGetStructTreeError = OnError; export type OnGetStructTreeSuccess = (tree: StructTreeNode) => void; export type OnGetTextError = OnError; export type OnGetTextSuccess = (textContent: TextContent) => void; export type OnPageLoadError = OnError; export type OnPageLoadSuccess = (page: PageCallback) => void; export type OnPasswordCallback = (password: string | null) => void; export type OnRenderAnnotationLayerError = (error: unknown) => void; export type OnRenderAnnotationLayerSuccess = () => void; export type OnRenderError = OnError; export type OnRenderSuccess = (page: PageCallback) => void; export type OnRenderTextLayerError = OnError; export type OnRenderTextLayerSuccess = () => void; export type PasswordResponse = (typeof PasswordResponses)[keyof typeof PasswordResponses]; export type Options = NullableObject>>; export type PageColors = { background: string; foreground: string; }; /* Context types */ export type DocumentContextType = { imageResourcesPath?: ImageResourcesPath; linkService: LinkService; onItemClick?: (args: OnItemClickArgs) => void; pdf?: PDFDocumentProxy | false; registerPage: RegisterPage; renderMode?: RenderMode; rotate?: number | null; unregisterPage: UnregisterPage; } | null; export type PageContextType = { _className?: string; canvasBackground?: string; customTextRenderer?: CustomTextRenderer; devicePixelRatio?: number; filterAnnotations?: FilterAnnotations; onGetAnnotationsError?: OnGetAnnotationsError; onGetAnnotationsSuccess?: OnGetAnnotationsSuccess; onGetStructTreeError?: OnGetStructTreeError; onGetStructTreeSuccess?: OnGetStructTreeSuccess; onGetTextError?: OnGetTextError; onGetTextSuccess?: OnGetTextSuccess; onRenderAnnotationLayerError?: OnRenderAnnotationLayerError; onRenderAnnotationLayerSuccess?: OnRenderAnnotationLayerSuccess; onRenderError?: OnRenderError; onRenderSuccess?: OnRenderSuccess; onRenderTextLayerError?: OnRenderTextLayerError; onRenderTextLayerSuccess?: OnRenderTextLayerSuccess; page: PDFPageProxy | false | undefined; pageColors?: PageColors; pageIndex: number; pageNumber: number; renderForms: boolean; renderTextLayer: boolean; rotate: number; scale: number; } | null; export type OutlineContextType = { onItemClick?: (args: OnItemClickArgs) => void; } | null; export type StructTreeNodeWithExtraAttributes = StructTreeNode & { alt?: string; lang?: string; }; export type DocumentRenderProps = Omit, 'pdf'> & { pdf: PDFDocumentProxy; }; export type PageRenderProps = Omit, 'page'> & { page: PDFPageProxy; }; ================================================ FILE: packages/react-pdf/src/shared/utils.spec.ts ================================================ import { describe, expect, it } from 'vitest'; import { dataURItoByteString, isDataURI } from './utils.js'; describe('isDataURI()', () => { it.each` input | expectedResult ${'potato'} | ${false} ${'data:,Hello%2C%20world%21'} | ${true} ${'data:text/plain;base64,SGVsbG8sIHdvcmxkIQ=='} | ${true} `('returns $expectedResult given $input', ({ input, expectedResult }) => { const result = isDataURI(input); expect(result).toBe(expectedResult); }); }); describe('dataURItoByteString()', () => { it('throws given invalid data URI', () => { expect(() => dataURItoByteString('potato')).toThrow(); }); it('returns a byte string given plain text data URI', () => { const result = dataURItoByteString('data:,Hello%2C%20world%21'); expect(result).toBe('Hello, world!'); }); it('returns a byte string given base64 data URI', () => { const result = dataURItoByteString('data:text/plain;base64,SGVsbG8sIHdvcmxkIQ=='); expect(result).toBe('Hello, world!'); }); it('returns a byte string given base64 PDF data URI', () => { const result = dataURItoByteString( 'data:application/pdf;base64,JVBERi0xLg10cmFpbGVyPDwvUm9vdDw8L1BhZ2VzPDwvS2lkc1s8PC9NZWRpYUJveFswIDAgMyAzXT4+XT4+Pj4+Pg==', ); expect(result).toBe('%PDF-1.\rtrailer<>]>>>>>>'); }); it('returns a byte string given base64 PDF data URI with filename', () => { const result = dataURItoByteString( 'data:application/pdf;filename=generated.pdf;base64,JVBERi0xLg10cmFpbGVyPDwvUm9vdDw8L1BhZ2VzPDwvS2lkc1s8PC9NZWRpYUJveFswIDAgMyAzXT4+XT4+Pj4+Pg==', ); expect(result).toBe('%PDF-1.\rtrailer<>]>>>>>>'); }); }); ================================================ FILE: packages/react-pdf/src/shared/utils.ts ================================================ import invariant from 'tiny-invariant'; import warning from 'warning'; import type { PDFPageProxy } from 'pdfjs-dist'; import type { PageCallback } from './types.js'; /** * Checks if we're running in a browser environment. */ export const isBrowser: boolean = typeof window !== 'undefined'; /** * Checks whether we're running from a local file system. */ export const isLocalFileSystem: boolean = isBrowser && window.location.protocol === 'file:'; /** * Checks whether a variable is defined. * * @param {*} variable Variable to check */ export function isDefined(variable: T | undefined): variable is T { return typeof variable !== 'undefined'; } /** * Checks whether a variable is defined and not null. * * @param {*} variable Variable to check */ export function isProvided(variable: T | null | undefined): variable is T { return isDefined(variable) && variable !== null; } /** * Checks whether a variable provided is a string. * * @param {*} variable Variable to check */ export function isString(variable: unknown): variable is string { return typeof variable === 'string'; } /** * Checks whether a variable provided is an ArrayBuffer. * * @param {*} variable Variable to check */ export function isArrayBuffer(variable: unknown): variable is ArrayBuffer { return variable instanceof ArrayBuffer; } /** * Checks whether a variable provided is a Blob. * * @param {*} variable Variable to check */ export function isBlob(variable: unknown): variable is Blob { invariant(isBrowser, 'isBlob can only be used in a browser environment'); return variable instanceof Blob; } /** * Checks whether a variable provided is a data URI. * * @param {*} variable String to check */ export function isDataURI(variable: unknown): variable is `data:${string}` { return isString(variable) && /^data:/.test(variable); } export function dataURItoByteString(dataURI: unknown): string { invariant(isDataURI(dataURI), 'Invalid data URI.'); const [headersString = '', dataString = ''] = dataURI.split(','); const headers = headersString.split(';'); if (headers.indexOf('base64') !== -1) { return atob(dataString); } return unescape(dataString); } export function getDevicePixelRatio(): number { return (isBrowser && window.devicePixelRatio) || 1; } const allowFileAccessFromFilesTip = 'On Chromium based browsers, you can use --allow-file-access-from-files flag for debugging purposes.'; export function displayCORSWarning(): void { warning( !isLocalFileSystem, `Loading PDF as base64 strings/URLs may not work on protocols other than HTTP/HTTPS. ${allowFileAccessFromFilesTip}`, ); } export function displayWorkerWarning(): void { warning( !isLocalFileSystem, `Loading PDF.js worker may not work on protocols other than HTTP/HTTPS. ${allowFileAccessFromFilesTip}`, ); } export function cancelRunningTask(runningTask?: { cancel?: () => void } | null): void { if (runningTask?.cancel) runningTask.cancel(); } export function makePageCallback(page: PDFPageProxy, scale: number): PageCallback { Object.defineProperty(page, 'width', { get() { return this.getViewport({ scale }).width; }, configurable: true, }); Object.defineProperty(page, 'height', { get() { return this.getViewport({ scale }).height; }, configurable: true, }); Object.defineProperty(page, 'originalWidth', { get() { return this.getViewport({ scale: 1 }).width; }, configurable: true, }); Object.defineProperty(page, 'originalHeight', { get() { return this.getViewport({ scale: 1 }).height; }, configurable: true, }); return page as PageCallback; } export function isAbortException(error: Error): boolean { return error.name === 'AbortException' || error.name === 'RenderingCancelledException'; } export function loadFromFile(file: Blob): Promise { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = () => { if (!reader.result) { return reject(new Error('Error while reading a file.')); } resolve(reader.result as ArrayBuffer); }; reader.onerror = (event) => { if (!event.target) { return reject(new Error('Error while reading a file.')); } const { error } = event.target; if (!error) { return reject(new Error('Error while reading a file.')); } switch (error.code) { case error.NOT_FOUND_ERR: return reject(new Error('Error while reading a file: File not found.')); case error.SECURITY_ERR: return reject(new Error('Error while reading a file: Security error.')); case error.ABORT_ERR: return reject(new Error('Error while reading a file: Aborted.')); default: return reject(new Error('Error while reading a file.')); } }; reader.readAsArrayBuffer(file); }); } ================================================ FILE: packages/react-pdf/tsconfig.build.json ================================================ { "extends": "./tsconfig.json", "compilerOptions": { "noEmit": false, "outDir": "dist", "rootDir": "src" }, "include": ["src"], "exclude": ["src/**/*.spec.ts", "src/**/*.spec.tsx"] } ================================================ FILE: packages/react-pdf/tsconfig.json ================================================ { "compilerOptions": { "declaration": true, "esModuleInterop": true, "isolatedDeclarations": true, "isolatedModules": true, "jsx": "react-jsx", "module": "nodenext", "moduleDetection": "force", "noEmit": true, "noUncheckedIndexedAccess": true, "outDir": "dist", "skipLibCheck": true, "strict": true, "target": "es2018", "verbatimModuleSyntax": true }, "exclude": ["dist"] } ================================================ FILE: packages/react-pdf/vitest.config.ts ================================================ import { playwright } from '@vitest/browser-playwright'; import { configDefaults, defineConfig } from 'vitest/config'; import type { ViteUserConfig } from 'vitest/config'; const config: ViteUserConfig = defineConfig({ test: { browser: { enabled: true, headless: true, instances: [{ browser: 'chromium' }], provider: playwright(), }, exclude: [...configDefaults.exclude, 'src/index.test.ts'], pool: 'forks', setupFiles: 'vitest.setup.ts', watch: false, }, }); export default config; ================================================ FILE: packages/react-pdf/vitest.setup.ts ================================================ document.body.style.setProperty('--react-pdf-annotation-layer', '1'); document.body.style.setProperty('--react-pdf-text-layer', '1'); ================================================ FILE: sample/next-app/.gitignore ================================================ .next dist node_modules ================================================ FILE: sample/next-app/app/Sample.css ================================================ body { margin: 0; background-color: #525659; font-family: 'Segoe UI', Tahoma, sans-serif; } .Example input, .Example button { font: inherit; } .Example header { background-color: #323639; box-shadow: 0 0 8px rgba(0, 0, 0, 0.5); padding: 20px; color: white; } .Example header h1 { font-size: inherit; margin: 0; } .Example__container { display: flex; flex-direction: column; align-items: center; margin: 10px 0; padding: 10px; } .Example__container__load { margin-top: 1em; color: white; } .Example__container__document { width: 100%; max-width: calc(100% - 2em); margin: 1em 0; } .Example__container__document .react-pdf__Document { display: flex; flex-direction: column; align-items: center; } .Example__container__document .react-pdf__Page { margin: 1em 0; box-shadow: 0 0 8px rgba(0, 0, 0, 0.5); } .Example__container__document .react-pdf__message { padding: 20px; color: white; } ================================================ FILE: sample/next-app/app/Sample.tsx ================================================ 'use client'; import { useCallback, useId, useState } from 'react'; import { useResizeObserver } from '@wojtekmaj/react-hooks'; import { Document, Page, pdfjs } from 'react-pdf'; import 'react-pdf/dist/Page/AnnotationLayer.css'; import 'react-pdf/dist/Page/TextLayer.css'; import './Sample.css'; import type { PDFDocumentProxy } from 'pdfjs-dist'; pdfjs.GlobalWorkerOptions.workerSrc = new URL( 'pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url, ).toString(); const options = { cMapUrl: '/cmaps/', standardFontDataUrl: '/standard_fonts/', wasmUrl: '/wasm/', }; const resizeObserverOptions = {}; const maxWidth = 800; type PDFFile = string | File | null; export default function Sample() { const fileId = useId(); const [file, setFile] = useState('./sample.pdf'); const [numPages, setNumPages] = useState(); const [containerRef, setContainerRef] = useState(null); const [containerWidth, setContainerWidth] = useState(); const onResize = useCallback((entries) => { const [entry] = entries; if (entry) { setContainerWidth(entry.contentRect.width); } }, []); useResizeObserver(containerRef, resizeObserverOptions, onResize); function onFileChange(event: React.ChangeEvent): void { const { files } = event.target; const nextFile = files?.[0]; if (nextFile) { setFile(nextFile); } } function onDocumentLoadSuccess({ numPages: nextNumPages }: PDFDocumentProxy): void { setNumPages(nextNumPages); } return (

    react-pdf sample page

    {' '}
    {Array.from(new Array(numPages), (_el, index) => ( ))}
    ); } ================================================ FILE: sample/next-app/app/layout.tsx ================================================ export const metadata = { title: 'react-pdf sample page', }; export default function RootLayout({ children }: { children: React.ReactNode }) { return ( {children} ); } ================================================ FILE: sample/next-app/app/page.tsx ================================================ 'use client'; import dynamic from 'next/dynamic'; const Sample = dynamic(() => import('./Sample'), { ssr: false, }); export default function Page() { return ; } ================================================ FILE: sample/next-app/next-env.d.ts ================================================ /// /// import './.next/types/routes.d.ts'; // NOTE: This file should not be edited // see https://nextjs.org/docs/app/api-reference/config/typescript for more information. ================================================ FILE: sample/next-app/next.config.ts ================================================ import type { NextConfig } from 'next'; const nextConfig: NextConfig = {}; export default nextConfig; ================================================ FILE: sample/next-app/package.json ================================================ { "name": "react-pdf-sample-page-next", "version": "4.0.0", "description": "A sample page for React-PDF.", "private": true, "type": "module", "scripts": { "build": "next build", "dev": "next dev", "preview": "next preview" }, "author": { "name": "Wojciech Maj", "email": "kontakt@wojtekmaj.pl" }, "license": "MIT", "dependencies": { "@wojtekmaj/react-hooks": "^2.0.0", "next": "^16.1.7", "react": "^19.2.0", "react-dom": "^19.2.0", "react-pdf": "latest" }, "packageManager": "yarn@4.10.3" } ================================================ FILE: sample/next-app/tsconfig.json ================================================ { "compilerOptions": { "allowJs": true, "esModuleInterop": true, "incremental": true, "isolatedModules": true, "jsx": "react-jsx", "lib": ["dom", "dom.iterable", "esnext"], "module": "esnext", "moduleDetection": "force", "moduleResolution": "bundler", "noEmit": true, "noUncheckedIndexedAccess": true, "plugins": [{ "name": "next" }], "resolveJsonModule": true, "skipLibCheck": true, "strict": true, "target": "es2018", "verbatimModuleSyntax": true }, "include": [".", ".next/types/**/*.ts", ".next/dev/types/**/*.ts"], "exclude": ["node_modules"] } ================================================ FILE: sample/next-pages/.gitignore ================================================ .next dist node_modules ================================================ FILE: sample/next-pages/empty-module.ts ================================================ export {}; ================================================ FILE: sample/next-pages/next-env.d.ts ================================================ /// /// import './.next/dev/types/routes.d.ts'; // NOTE: This file should not be edited // see https://nextjs.org/docs/pages/api-reference/config/typescript for more information. ================================================ FILE: sample/next-pages/next.config.ts ================================================ import type { NextConfig } from 'next'; const turbopackEnabled = process.env.TURBOPACK; const nextConfig: NextConfig = { experimental: turbopackEnabled ? undefined : { /** * Prevents "ESM packages (pdfjs-dist/build/pdf.worker.min.mjs) need to be imported." error in Webpack builds */ esmExternals: 'loose', }, }; export default nextConfig; ================================================ FILE: sample/next-pages/package.json ================================================ { "name": "react-pdf-sample-page-next", "version": "4.0.0", "description": "A sample page for React-PDF.", "private": true, "type": "module", "scripts": { "build": "next build", "dev": "next dev", "preview": "next preview" }, "author": { "name": "Wojciech Maj", "email": "kontakt@wojtekmaj.pl" }, "license": "MIT", "dependencies": { "@wojtekmaj/react-hooks": "^2.0.0", "next": "^16.1.7", "react": "^19.2.0", "react-dom": "^19.2.0", "react-pdf": "latest" }, "packageManager": "yarn@4.10.3" } ================================================ FILE: sample/next-pages/pages/Sample.css ================================================ body { margin: 0; background-color: #525659; font-family: 'Segoe UI', Tahoma, sans-serif; } .Example input, .Example button { font: inherit; } .Example header { background-color: #323639; box-shadow: 0 0 8px rgba(0, 0, 0, 0.5); padding: 20px; color: white; } .Example header h1 { font-size: inherit; margin: 0; } .Example__container { display: flex; flex-direction: column; align-items: center; margin: 10px 0; padding: 10px; } .Example__container__load { margin-top: 1em; color: white; } .Example__container__document { width: 100%; max-width: calc(100% - 2em); margin: 1em 0; } .Example__container__document .react-pdf__Document { display: flex; flex-direction: column; align-items: center; } .Example__container__document .react-pdf__Page { margin: 1em 0; box-shadow: 0 0 8px rgba(0, 0, 0, 0.5); } .Example__container__document .react-pdf__message { padding: 20px; color: white; } ================================================ FILE: sample/next-pages/pages/Sample.tsx ================================================ import { useCallback, useId, useState } from 'react'; import { useResizeObserver } from '@wojtekmaj/react-hooks'; import { Document, Page, pdfjs } from 'react-pdf'; import 'react-pdf/dist/Page/AnnotationLayer.css'; import 'react-pdf/dist/Page/TextLayer.css'; import type { PDFDocumentProxy } from 'pdfjs-dist'; pdfjs.GlobalWorkerOptions.workerSrc = new URL( 'pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url, ).toString(); const options = { cMapUrl: '/cmaps/', standardFontDataUrl: '/standard_fonts/', wasmUrl: '/wasm/', }; const resizeObserverOptions = {}; const maxWidth = 800; type PDFFile = string | File | null; export default function Sample() { const fileId = useId(); const [file, setFile] = useState('./sample.pdf'); const [numPages, setNumPages] = useState(); const [containerRef, setContainerRef] = useState(null); const [containerWidth, setContainerWidth] = useState(); const onResize = useCallback((entries) => { const [entry] = entries; if (entry) { setContainerWidth(entry.contentRect.width); } }, []); useResizeObserver(containerRef, resizeObserverOptions, onResize); function onFileChange(event: React.ChangeEvent): void { const { files } = event.target; const nextFile = files?.[0]; if (nextFile) { setFile(nextFile); } } function onDocumentLoadSuccess({ numPages: nextNumPages }: PDFDocumentProxy): void { setNumPages(nextNumPages); } return (

    react-pdf sample page

    {' '}
    {Array.from(new Array(numPages), (_el, index) => ( ))}
    ); } ================================================ FILE: sample/next-pages/pages/_app.tsx ================================================ import './Sample.css'; import type { AppProps } from 'next/app'; export default function App({ Component, pageProps }: AppProps) { return ; } ================================================ FILE: sample/next-pages/pages/index.tsx ================================================ import dynamic from 'next/dynamic'; const Sample = dynamic(() => import('./Sample'), { ssr: false, }); export default function Page() { return ; } ================================================ FILE: sample/next-pages/tsconfig.json ================================================ { "compilerOptions": { "allowJs": true, "esModuleInterop": true, "incremental": true, "isolatedModules": true, "jsx": "react-jsx", "lib": ["dom", "dom.iterable", "esnext"], "module": "esnext", "moduleDetection": "force", "moduleResolution": "bundler", "noEmit": true, "noUncheckedIndexedAccess": true, "plugins": [{ "name": "next" }], "resolveJsonModule": true, "skipLibCheck": true, "strict": true, "target": "es2018", "verbatimModuleSyntax": true }, "include": [".", ".next/types/**/*.ts"], "exclude": ["node_modules"] } ================================================ FILE: sample/parcel2/.gitignore ================================================ .parcel-cache dist node_modules ================================================ FILE: sample/parcel2/Sample.css ================================================ body { margin: 0; background-color: #525659; font-family: 'Segoe UI', Tahoma, sans-serif; } .Example input, .Example button { font: inherit; } .Example header { background-color: #323639; box-shadow: 0 0 8px rgba(0, 0, 0, 0.5); padding: 20px; color: white; } .Example header h1 { font-size: inherit; margin: 0; } .Example__container { display: flex; flex-direction: column; align-items: center; margin: 10px 0; padding: 10px; } .Example__container__load { margin-top: 1em; color: white; } .Example__container__document { width: 100%; max-width: calc(100% - 2em); margin: 1em 0; } .Example__container__document .react-pdf__Document { display: flex; flex-direction: column; align-items: center; } .Example__container__document .react-pdf__Page { margin: 1em 0; box-shadow: 0 0 8px rgba(0, 0, 0, 0.5); } .Example__container__document .react-pdf__message { padding: 20px; color: white; } ================================================ FILE: sample/parcel2/Sample.tsx ================================================ import { useCallback, useId, useState } from 'react'; import { useResizeObserver } from '@wojtekmaj/react-hooks'; import { pdfjs, Document, Page } from 'react-pdf'; import 'react-pdf/dist/Page/AnnotationLayer.css'; import 'react-pdf/dist/Page/TextLayer.css'; import './Sample.css'; import type { PDFDocumentProxy } from 'pdfjs-dist'; const pdfFile = new URL('./sample.pdf', import.meta.url).toString(); pdfjs.GlobalWorkerOptions.workerSrc = new URL( 'npm:pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url, ).toString(); const options = { cMapUrl: '/cmaps/', standardFontDataUrl: '/standard_fonts/', wasmUrl: '/wasm/', }; const resizeObserverOptions = {}; const maxWidth = 800; type PDFFile = string | File | null; export default function Sample() { const fileId = useId(); const [file, setFile] = useState(pdfFile); const [numPages, setNumPages] = useState(); const [containerRef, setContainerRef] = useState(null); const [containerWidth, setContainerWidth] = useState(); const onResize = useCallback((entries) => { const [entry] = entries; if (entry) { setContainerWidth(entry.contentRect.width); } }, []); useResizeObserver(containerRef, resizeObserverOptions, onResize); function onFileChange(event: React.ChangeEvent): void { const { files } = event.target; const nextFile = files?.[0]; if (nextFile) { setFile(nextFile); } } function onDocumentLoadSuccess({ numPages: nextNumPages }: PDFDocumentProxy): void { setNumPages(nextNumPages); } return (

    react-pdf sample page

    {' '}
    {Array.from(new Array(numPages), (_el, index) => ( ))}
    ); } ================================================ FILE: sample/parcel2/index.html ================================================ react-pdf sample page
    ================================================ FILE: sample/parcel2/index.tsx ================================================ import { createRoot } from 'react-dom/client'; import Sample from './Sample'; const root = document.getElementById('root'); if (!root) { throw new Error('Could not find root element'); } createRoot(root).render(); ================================================ FILE: sample/parcel2/package.json ================================================ { "name": "react-pdf-sample-page-parcel2", "version": "4.0.0", "description": "A sample page for React-PDF.", "private": true, "type": "module", "scripts": { "build": "yarn copy && parcel build index.html --public-url ./", "copy": "yarn copy-cmaps && yarn copy-standard-fonts", "copy-cmaps": "node --experimental-strip-types ./scripts/copy-cmaps.ts", "copy-standard-fonts": "node --experimental-strip-types ./scripts/copy-standard-fonts.ts", "copy-wasm": "node --experimental-strip-types ./scripts/copy-wasm.ts", "dev": "yarn copy && parcel index.html" }, "author": { "name": "Wojciech Maj", "email": "kontakt@wojtekmaj.pl" }, "license": "MIT", "dependencies": { "@wojtekmaj/react-hooks": "^2.0.0", "react": "^19.2.0", "react-dom": "^19.2.0", "react-pdf": "latest" }, "devDependencies": { "@types/node": "*", "buffer": "^5.5.0", "parcel": "^2.16.4", "process": "^0.11.10", "typescript": "^5.9.2" }, "packageManager": "yarn@4.10.3" } ================================================ FILE: sample/parcel2/scripts/copy-cmaps.ts ================================================ import fs from 'node:fs'; import path from 'node:path'; import { createRequire } from 'node:module'; const require = createRequire(import.meta.url); const cMapsDir = path.join(path.dirname(require.resolve('pdfjs-dist/package.json')), 'cmaps'); const targetDir = path.join('dist', 'cmaps'); fs.cpSync(cMapsDir, targetDir, { recursive: true }); ================================================ FILE: sample/parcel2/scripts/copy-standard-fonts.ts ================================================ import fs from 'node:fs'; import path from 'node:path'; import { createRequire } from 'node:module'; const require = createRequire(import.meta.url); const standardFontsDir = path.join( path.dirname(require.resolve('pdfjs-dist/package.json')), 'standard_fonts', ); const targetDir = path.join('dist', 'standard_fonts'); fs.cpSync(standardFontsDir, targetDir, { recursive: true }); ================================================ FILE: sample/parcel2/scripts/copy-wasm.ts ================================================ import fs from 'node:fs'; import path from 'node:path'; import { createRequire } from 'node:module'; const require = createRequire(import.meta.url); const wasmDir = path.join(path.dirname(require.resolve('pdfjs-dist/package.json')), 'wasm'); const targetDir = path.join('dist', 'wasm'); fs.cpSync(wasmDir, targetDir, { recursive: true }); ================================================ FILE: sample/parcel2/tsconfig.json ================================================ { "compilerOptions": { "isolatedModules": true, "jsx": "react-jsx", "module": "preserve", "moduleDetection": "force", "noEmit": true, "noUncheckedIndexedAccess": true, "skipLibCheck": true, "strict": true, "target": "es2018", "verbatimModuleSyntax": true } } ================================================ FILE: sample/vite/.gitignore ================================================ dist node_modules ================================================ FILE: sample/vite/Sample.css ================================================ body { margin: 0; background-color: #525659; font-family: 'Segoe UI', Tahoma, sans-serif; } .Example input, .Example button { font: inherit; } .Example header { background-color: #323639; box-shadow: 0 0 8px rgba(0, 0, 0, 0.5); padding: 20px; color: white; } .Example header h1 { font-size: inherit; margin: 0; } .Example__container { display: flex; flex-direction: column; align-items: center; margin: 10px 0; padding: 10px; } .Example__container__load { margin-top: 1em; color: white; } .Example__container__document { width: 100%; max-width: calc(100% - 2em); margin: 1em 0; } .Example__container__document .react-pdf__Document { display: flex; flex-direction: column; align-items: center; } .Example__container__document .react-pdf__Page { margin: 1em 0; box-shadow: 0 0 8px rgba(0, 0, 0, 0.5); } .Example__container__document .react-pdf__message { padding: 20px; color: white; } ================================================ FILE: sample/vite/Sample.tsx ================================================ import { useCallback, useId, useState } from 'react'; import { useResizeObserver } from '@wojtekmaj/react-hooks'; import { pdfjs, Document, Page } from 'react-pdf'; import 'react-pdf/dist/Page/AnnotationLayer.css'; import 'react-pdf/dist/Page/TextLayer.css'; import './Sample.css'; import type { PDFDocumentProxy } from 'pdfjs-dist'; pdfjs.GlobalWorkerOptions.workerSrc = new URL( 'pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url, ).toString(); const options = { cMapUrl: '/cmaps/', standardFontDataUrl: '/standard_fonts/', wasmUrl: '/wasm/', }; const resizeObserverOptions = {}; const maxWidth = 800; type PDFFile = string | File | null; export default function Sample() { const fileId = useId(); const [file, setFile] = useState('./sample.pdf'); const [numPages, setNumPages] = useState(); const [containerRef, setContainerRef] = useState(null); const [containerWidth, setContainerWidth] = useState(); const onResize = useCallback((entries) => { const [entry] = entries; if (entry) { setContainerWidth(entry.contentRect.width); } }, []); useResizeObserver(containerRef, resizeObserverOptions, onResize); function onFileChange(event: React.ChangeEvent): void { const { files } = event.target; const nextFile = files?.[0]; if (nextFile) { setFile(nextFile); } } function onDocumentLoadSuccess({ numPages: nextNumPages }: PDFDocumentProxy): void { setNumPages(nextNumPages); } return (

    react-pdf sample page

    {' '}
    {Array.from(new Array(numPages), (_el, index) => ( ))}
    ); } ================================================ FILE: sample/vite/index.html ================================================ react-pdf sample page
    ================================================ FILE: sample/vite/index.tsx ================================================ import { createRoot } from 'react-dom/client'; import Sample from './Sample.js'; const root = document.getElementById('root'); if (!root) { throw new Error('Could not find root element'); } createRoot(root).render(); ================================================ FILE: sample/vite/package.json ================================================ { "name": "react-pdf-sample-page-vite4", "version": "4.0.0", "description": "A sample page for React-PDF.", "private": true, "type": "module", "scripts": { "build": "vite build", "dev": "vite", "preview": "vite preview" }, "author": { "name": "Wojciech Maj", "email": "kontakt@wojtekmaj.pl" }, "license": "MIT", "dependencies": { "@wojtekmaj/react-hooks": "^2.0.0", "react": "^19.2.0", "react-dom": "^19.2.0", "react-pdf": "latest" }, "devDependencies": { "@vitejs/plugin-react": "^4.6.0", "vite": "^7.1.11", "vite-plugin-static-copy": "^3.1.2" }, "packageManager": "yarn@4.10.3" } ================================================ FILE: sample/vite/tsconfig.json ================================================ { "compilerOptions": { "isolatedModules": true, "jsx": "react-jsx", "module": "preserve", "moduleDetection": "force", "noEmit": true, "noUncheckedIndexedAccess": true, "skipLibCheck": true, "strict": true, "target": "es2018", "verbatimModuleSyntax": true } } ================================================ FILE: sample/vite/vite.config.ts ================================================ import path from 'node:path'; import { createRequire } from 'node:module'; import { defineConfig, normalizePath } from 'vite'; import { viteStaticCopy } from 'vite-plugin-static-copy'; const require = createRequire(import.meta.url); const cMapsDir = normalizePath( path.join(path.dirname(require.resolve('pdfjs-dist/package.json')), 'cmaps'), ); const standardFontsDir = normalizePath( path.join(path.dirname(require.resolve('pdfjs-dist/package.json')), 'standard_fonts'), ); const wasmDir = normalizePath( path.join(path.dirname(require.resolve('pdfjs-dist/package.json')), 'wasm'), ); export default defineConfig({ plugins: [ viteStaticCopy({ targets: [ { src: cMapsDir, dest: '' }, { src: standardFontsDir, dest: '' }, { src: wasmDir, dest: '' }, ], }), ], }); ================================================ FILE: sample/webpack5/.babelrc ================================================ { "presets": [ "@babel/preset-typescript", [ "@babel/preset-env", { "modules": false } ], [ "@babel/preset-react", { "runtime": "automatic" } ] ] } ================================================ FILE: sample/webpack5/.gitignore ================================================ dist node_modules ================================================ FILE: sample/webpack5/Sample.css ================================================ body { margin: 0; background-color: #525659; font-family: 'Segoe UI', Tahoma, sans-serif; } .Example input, .Example button { font: inherit; } .Example header { background-color: #323639; box-shadow: 0 0 8px rgba(0, 0, 0, 0.5); padding: 20px; color: white; } .Example header h1 { font-size: inherit; margin: 0; } .Example__container { display: flex; flex-direction: column; align-items: center; margin: 10px 0; padding: 10px; } .Example__container__load { margin-top: 1em; color: white; } .Example__container__document { width: 100%; max-width: calc(100% - 2em); margin: 1em 0; } .Example__container__document .react-pdf__Document { display: flex; flex-direction: column; align-items: center; } .Example__container__document .react-pdf__Page { margin: 1em 0; box-shadow: 0 0 8px rgba(0, 0, 0, 0.5); } .Example__container__document .react-pdf__message { padding: 20px; color: white; } ================================================ FILE: sample/webpack5/Sample.tsx ================================================ import { useCallback, useId, useState } from 'react'; import { useResizeObserver } from '@wojtekmaj/react-hooks'; import { pdfjs, Document, Page } from 'react-pdf'; import 'react-pdf/dist/Page/AnnotationLayer.css'; import 'react-pdf/dist/Page/TextLayer.css'; import './Sample.css'; import type { PDFDocumentProxy } from 'pdfjs-dist'; pdfjs.GlobalWorkerOptions.workerSrc = new URL( 'pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url, ).toString(); const options = { cMapUrl: '/cmaps/', standardFontDataUrl: '/standard_fonts/', wasmUrl: '/wasm/', }; const resizeObserverOptions = {}; const maxWidth = 800; type PDFFile = string | File | null; export default function Sample() { const fileId = useId(); const [file, setFile] = useState('./sample.pdf'); const [numPages, setNumPages] = useState(); const [containerRef, setContainerRef] = useState(null); const [containerWidth, setContainerWidth] = useState(); const onResize = useCallback((entries) => { const [entry] = entries; if (entry) { setContainerWidth(entry.contentRect.width); } }, []); useResizeObserver(containerRef, resizeObserverOptions, onResize); function onFileChange(event: React.ChangeEvent): void { const { files } = event.target; const nextFile = files?.[0]; if (nextFile) { setFile(nextFile); } } function onDocumentLoadSuccess({ numPages: nextNumPages }: PDFDocumentProxy): void { setNumPages(nextNumPages); } return (

    react-pdf sample page

    {' '}
    {Array.from(new Array(numPages), (_el, index) => ( ))}
    ); } ================================================ FILE: sample/webpack5/index.html ================================================ react-pdf sample page
    ================================================ FILE: sample/webpack5/index.tsx ================================================ import { createRoot } from 'react-dom/client'; import Sample from './Sample'; const root = document.getElementById('root'); if (!root) { throw new Error('Could not find root element'); } createRoot(root).render(); ================================================ FILE: sample/webpack5/package.json ================================================ { "name": "react-pdf-sample-page-webpack5", "version": "4.0.0", "description": "A sample page for React-PDF.", "private": true, "scripts": { "build": "NODE_ENV=production webpack", "dev": "NODE_ENV=development webpack serve" }, "author": { "name": "Wojciech Maj", "email": "kontakt@wojtekmaj.pl" }, "license": "MIT", "dependencies": { "@wojtekmaj/react-hooks": "^2.0.0", "react": "^19.2.0", "react-dom": "^19.2.0", "react-pdf": "latest" }, "devDependencies": { "@babel/core": "^7.27.4", "@babel/preset-env": "^7.27.2", "@babel/preset-react": "^7.27.1", "@babel/preset-typescript": "^7.27.1", "@types/node": "*", "babel-loader": "^9.0.1", "copy-webpack-plugin": "^14.0.0", "css-loader": "^6.0.0", "html-webpack-plugin": "^5.1.0", "style-loader": "^3.0.0", "ts-node": "^10.9.1", "typescript": "^5.9.2", "webpack": "^5.104.1", "webpack-cli": "^5.0.0", "webpack-dev-server": "^5.2.1" }, "packageManager": "yarn@4.10.3" } ================================================ FILE: sample/webpack5/tsconfig.json ================================================ { "compilerOptions": { "esModuleInterop": true, "isolatedModules": true, "jsx": "react-jsx", "module": "preserve", "moduleDetection": "force", "noEmit": true, "noUncheckedIndexedAccess": true, "skipLibCheck": true, "strict": true, "target": "es2018", "verbatimModuleSyntax": true }, "ts-node": { "compilerOptions": { "module": "commonjs", "verbatimModuleSyntax": false } } } ================================================ FILE: sample/webpack5/webpack.config.ts ================================================ import webpack from 'webpack'; import path from 'node:path'; import CopyWebpackPlugin from 'copy-webpack-plugin'; import HtmlWebpackPlugin from 'html-webpack-plugin'; import type { Configuration } from 'webpack'; import 'webpack-dev-server'; const isProduction = process.env.NODE_ENV === 'production'; const cMapsDir = path.join(path.dirname(require.resolve('pdfjs-dist/package.json')), 'cmaps'); const standardFontsDir = path.join( path.dirname(require.resolve('pdfjs-dist/package.json')), 'standard_fonts', ); const wasmDir = path.join(path.dirname(require.resolve('pdfjs-dist/package.json')), 'wasm'); const config = { mode: isProduction ? 'production' : 'development', /** * Critical: prevents "Uncaught TypeError: Object.defineProperty called on non-object" error */ devtool: 'cheap-source-map', bail: isProduction, context: path.join(__dirname), entry: { src: './index.tsx', }, output: { path: path.resolve(__dirname, 'dist'), }, resolve: { extensions: ['.js', '.jsx', '.ts', '.tsx'], }, module: { rules: [ { test: /\.(j|t)sx?$/, use: ['babel-loader'], }, { test: /\.css$/, use: ['style-loader', 'css-loader'], }, ], }, plugins: [ new webpack.DefinePlugin({ 'process.env': { NODE_ENV: JSON.stringify(process.env.NODE_ENV), }, }), new HtmlWebpackPlugin({ template: 'index.html', }), new CopyWebpackPlugin({ patterns: [ { from: './sample.pdf' }, { from: cMapsDir, to: 'cmaps/' }, { from: standardFontsDir, to: 'standard_fonts/' }, { from: wasmDir, to: 'wasm/' }, ], }), ], devServer: { compress: true, historyApiFallback: true, // respond to 404s with index.html host: 'localhost', hot: true, // enable HMR on the server port: 3000, }, } satisfies Configuration; export default config; ================================================ FILE: test/.gitignore ================================================ dist node_modules ================================================ FILE: test/AnnotationOptions.tsx ================================================ import { useId } from 'react'; import type { ExternalLinkTarget } from './shared/types.js'; type AnnotationOptionsProps = { externalLinkTarget: ExternalLinkTarget | undefined; setExternalLinkTarget: (value: ExternalLinkTarget | undefined) => void; }; export default function AnnotationOptions({ externalLinkTarget, setExternalLinkTarget, }: AnnotationOptionsProps) { const targetUnsetId = useId(); const targetSelfId = useId(); const targetBlankId = useId(); function onExternalLinkTargetChange(event: React.ChangeEvent) { const { value } = event.target; if (value === 'undefined') { setExternalLinkTarget(undefined); } else { setExternalLinkTarget(value as '_blank' | '_self'); } } return (
    Annotation options
    ); } ================================================ FILE: test/CustomRenderer.tsx ================================================ import { useEffect, useMemo, useRef } from 'react'; import { usePageContext } from 'react-pdf'; import invariant from 'tiny-invariant'; import type { RenderParameters } from 'pdfjs-dist/types/src/display/api.js'; export default function CustomRenderer() { const pageContext = usePageContext(); invariant(pageContext, 'Unable to find Page context.'); const { _className, page, rotate, scale } = pageContext; invariant(page, 'Attempted to render page canvas, but no page was specified.'); const canvasElement = useRef(null); const viewport = useMemo( () => page.getViewport({ scale, rotation: rotate }), [page, rotate, scale], ); useEffect( function drawPageOnCanvas() { if (!page) { return; } const { current: canvas } = canvasElement; if (!canvas) { return; } const renderContext: RenderParameters = { canvas, canvasContext: canvas.getContext('2d', { alpha: false }) as CanvasRenderingContext2D, viewport, }; const cancellable = page.render(renderContext); const runningTask = cancellable; cancellable.promise.catch(() => { // Intentionally empty }); return () => { runningTask.cancel(); }; }, [page, viewport], ); return ( ); } ================================================ FILE: test/LayerOptions.tsx ================================================ import { useId } from 'react'; type LayerOptionsProps = { renderAnnotationLayer: boolean; renderForms: boolean; renderTextLayer: boolean; useCustomTextRenderer: boolean; setRenderAnnotationLayer: (value: boolean) => void; setRenderForms: (value: boolean) => void; setRenderTextLayer: (value: boolean) => void; setUseCustomTextRenderer: (value: boolean) => void; }; export default function LayerOptions({ renderAnnotationLayer, renderForms, renderTextLayer, useCustomTextRenderer, setRenderAnnotationLayer, setRenderForms, setRenderTextLayer, setUseCustomTextRenderer, }: LayerOptionsProps) { const renderTextLayerId = useId(); const useCustomTextRendererId = useId(); const renderAnnotationLayerId = useId(); const renderFormsId = useId(); function onRenderAnnotationLayerChange(event: React.ChangeEvent) { setRenderAnnotationLayer(event.target.checked); } function onRenderFormsChange(event: React.ChangeEvent) { setRenderForms(event.target.checked); } function onRenderTextLayersChange(event: React.ChangeEvent) { setRenderTextLayer(event.target.checked); } function onUseCustomTextRendererChange(event: React.ChangeEvent) { setUseCustomTextRenderer(event.target.checked); } return (
    Layer options
    ); } ================================================ FILE: test/LoadingOptions.tsx ================================================ import { useId, useRef } from 'react'; import { flushSync } from 'react-dom'; import samplePDF from './test.pdf'; import type { File } from './shared/types.js'; type LoadingOptionsProps = { file: File | null; setFile: (value: File | null) => void; setRender: (value: boolean) => void; }; export default function LoadingOptions({ file, setFile, setRender }: LoadingOptionsProps) { const url = useRef(null); const fetchAndPass = useRef(null); const fileId = useId(); const urlId = useId(); const fetchAndPassId = useId(); function onFileChange(event: React.ChangeEvent): void { const { files } = event.target; if (files?.[0]) { setFile(files[0]); } else { setFile(null); } } function onURLChange(event: React.FormEvent) { event.preventDefault(); const input = url.current; const { value: nextUrl } = input as HTMLInputElement; setFile(nextUrl); } function onRequestChange(event: React.FormEvent) { event.preventDefault(); const input = fetchAndPass.current; const { value: nextFetchAndPass } = input as HTMLInputElement; fetch(nextFetchAndPass) .then((response) => response.blob()) .then(setFile); } function onUseImported() { setFile(samplePDF); } function onImportAndUnmount() { setFile(samplePDF); flushSync(() => setRender(false)); flushSync(() => setRender(true)); } function unloadFile() { setFile(null); } return (
    Load file
    ); } ================================================ FILE: test/PassingOptions.tsx ================================================ import { isDataURI } from './shared/utils.js'; import { useId } from 'react'; import type { File, PassMethod } from './shared/types.js'; type PassingOptionsProps = { file: File | null; passMethod: PassMethod | undefined; setPassMethod: (value: PassMethod | undefined) => void; }; export default function PassingOptions({ file, passMethod, setPassMethod }: PassingOptionsProps) { const passNormalId = useId(); const passObjectId = useId(); const passStringId = useId(); const passBlobId = useId(); const sourceType = (() => { if (file === null) { return 'null'; } if (typeof file === 'string') { if (isDataURI(file)) { return 'data URI'; } return 'string'; } if (typeof file === 'object') { return file.constructor.name; } return typeof file; })(); function onPassMethodChange(event: React.ChangeEvent) { const nextPassMethod = event.target.value; if (nextPassMethod === 'undefined') { setPassMethod(undefined); } else { setPassMethod(nextPassMethod as PassMethod); } } return (
    Passing options
    ); } ================================================ FILE: test/Test.css ================================================ body { margin: 0; font-family: 'Segoe UI', Tahoma, sans-serif; } .Test header { background-color: #323639; box-shadow: 0 0 8px rgba(0, 0, 0, 0.5); padding: 20px; color: white; } .Test header h1 { font-size: inherit; margin: 0; } .Test__container { display: flex; flex-direction: row; flex-wrap: wrap; align-items: flex-start; margin: 10px 0; padding: 10px; } .Test__container > * > * { margin: 10px; } .Test__container__options { display: flex; flex-basis: 400px; flex-grow: 1; flex-wrap: wrap; margin: 0; } .Test__container__options input, .Test__container__options button { font: inherit; } .Test__container__options fieldset { border: 1px solid black; flex-grow: 1; position: relative; top: -10px; } .Test__container__options fieldset legend { font-weight: 600; } .Test__container__options fieldset legend + * { margin-top: 0 !important; } .Test__container__options fieldset label { font-weight: 600; display: block; } .Test__container__options fieldset label:not(:first-of-type) { margin-top: 1em; } .Test__container__options fieldset input[type='checkbox'] + label, .Test__container__options fieldset input[type='radio'] + label { font-weight: normal; display: inline-block; margin: 0; } .Test__container__options fieldset form:not(:first-child), .Test__container__options fieldset div:not(:first-child) { margin-top: 1em; } .Test__container__options fieldset form:not(:last-child), .Test__container__options fieldset div:not(:last-child) { margin-bottom: 1em; } .Test__container__content { display: flex; max-width: 100%; flex-basis: 420px; flex-direction: column; flex-grow: 100; align-items: stretch; } .Test__container__content__toc { max-width: 100%; max-height: 16vh; margin-bottom: 20px; border: 1px solid black; overflow: auto; } .Test__container__content__toc .react-pdf__message { padding: 20px; } .Test__container__content__document { max-width: 100%; max-height: 55vh; display: flex; flex-direction: column; align-items: center; border: 1px solid black; background-color: #525659; overflow: auto; } .Test__container__content__document .react-pdf__Page { max-width: calc(100% - 2em); box-shadow: 0 0 8px rgba(0, 0, 0, 0.5); margin: 1em; } .Test__container__content__document .react-pdf__Page__textContent mark { opacity: 0.5; background: #ff0; box-shadow: 0px 2px 10px #ff0; color: transparent; white-space: pre; } .Test__container__content__document .react-pdf__message { padding: 20px; color: white; } .Test__container__content__controls { max-width: 100%; display: flex; margin-top: 1em; align-self: center; } .Test__container__content__controls span { flex-grow: 1; margin: 0 1em; text-align: center; } .Test__container__content__controls button { width: 80px; } .Test__container__content__thumbnails { display: flex; margin-top: 1em; gap: 1em; overflow: auto; } .Test__container__content__thumbnails .react-pdf__Thumbnail { border: 1px solid black; } ================================================ FILE: test/Test.tsx ================================================ import { useCallback, useEffect, useState } from 'react'; import { Document, Outline, Page, pdfjs, Thumbnail } from 'react-pdf'; import 'react-pdf/dist/Page/AnnotationLayer.css'; import 'react-pdf/dist/Page/TextLayer.css'; import './Test.css'; import AnnotationOptions from './AnnotationOptions.js'; import CustomRenderer from './CustomRenderer.js'; import LayerOptions from './LayerOptions.js'; import LoadingOptions from './LoadingOptions.js'; import PassingOptions from './PassingOptions.js'; import ViewOptions from './ViewOptions.js'; import { isArrayBuffer, isBlob, isBrowser, loadFromFile } from './shared/utils.js'; import type { PDFDocumentProxy, PDFPageProxy } from 'pdfjs-dist'; import type { ExternalLinkTarget, File, PassMethod, RenderMode } from './shared/types.js'; const { PDFDataRangeTransport } = pdfjs; pdfjs.GlobalWorkerOptions.workerSrc = new URL( 'pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url, ).toString(); const options = { cMapUrl: '/cmaps/', standardFontDataUrl: '/standard_fonts/', wasmUrl: '/wasm/', }; export function readAsDataURL(file: Blob): Promise { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = () => { if (!reader.result) { return reject(new Error('Error while reading a file.')); } resolve(reader.result as string); }; reader.onerror = (event) => { if (!event.target) { return reject(new Error('Error while reading a file.')); } const { error } = event.target; if (!error) { return reject(new Error('Error while reading a file.')); } switch (error.code) { case error.NOT_FOUND_ERR: return reject(new Error('Error while reading a file: File not found.')); case error.SECURITY_ERR: return reject(new Error('Error while reading a file: Security error.')); case error.ABORT_ERR: return reject(new Error('Error while reading a file: Aborted.')); default: return reject(new Error('Error while reading a file.')); } }; reader.readAsDataURL(file); }); } export default function Test() { const [canvasBackground, setCanvasBackground] = useState(); const [devicePixelRatio, setDevicePixelRatio] = useState(); const [displayAll, setDisplayAll] = useState(false); const [externalLinkTarget, setExternalLinkTarget] = useState(); const [file, setFile] = useState(null); const [fileForProps, setFileForProps] = useState(); const [numPages, setNumPages] = useState(); const [pageHeight, setPageHeight] = useState(); const [pageNumber, setPageNumber] = useState(); const [pageScale, setPageScale] = useState(); const [pageWidth, setPageWidth] = useState(); const [passMethod, setPassMethod] = useState(); const [renderHighContrast, setRenderHighContrast] = useState(false); const [render, setRender] = useState(true); const [renderAnnotationLayer, setRenderAnnotationLayer] = useState(true); const [renderForms, setRenderForms] = useState(true); const [renderMode, setRenderMode] = useState('canvas'); const [renderTextLayer, setRenderTextLayer] = useState(true); const [useCustomTextRenderer, setUseCustomTextRenderer] = useState(true); const [rotate, setRotate] = useState(); const onDocumentLoadProgress = useCallback((progressData: { loaded: number; total: number }) => { console.log('Loading a document: ' + (progressData.loaded / progressData.total) * 100 + '%'); }, []); const onDocumentLoadSuccess = useCallback((document: PDFDocumentProxy) => { console.log('Loaded a document', document); const { numPages: nextNumPages } = document; setNumPages(nextNumPages); setPageNumber(1); }, []); const onDocumentLoadError = useCallback((error: Error) => { console.error(error); }, []); const onPageRenderSuccess = useCallback( (page: PDFPageProxy) => console.log('Rendered a page', page), [], ); const onPageClick = useCallback( (event: React.MouseEvent, page: PDFPageProxy | false | undefined) => console.log('Clicked a page', { event, page }), [], ); const onItemClick = useCallback((args: { pageNumber: number }) => { console.log('Clicked an item', args); const { pageNumber: nextPageNumber } = args; setPageNumber(nextPageNumber); }, []); const customTextRenderer = useCallback( ({ str }: { str: string }) => str.replace(/ipsum/g, (value) => `${value}`), [], ); useEffect(() => { (async () => { const nextFileForProps = await (async () => { if (!file) { return null; } switch (passMethod) { case 'string': { if (typeof file === 'string') { return file; } if (file instanceof File || file instanceof Blob) { return readAsDataURL(file); } return file; } case 'object': { // File is a string if (typeof file === 'string') { 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)) { return { data: await loadFromFile(file) }; } } return file; } default: return file; } })(); setFileForProps(nextFileForProps); })(); }, [file, passMethod]); const changePage = useCallback( (offset: number) => setPageNumber((prevPageNumber) => (prevPageNumber || 1) + offset), [], ); const previousPage = useCallback(() => changePage(-1), [changePage]); const nextPage = useCallback(() => changePage(1), [changePage]); const documentProps = { externalLinkTarget, file: fileForProps, options, rotate, }; const pageProps = { canvasBackground, className: 'custom-classname-page', customRenderer: CustomRenderer, customTextRenderer: useCustomTextRenderer ? customTextRenderer : undefined, devicePixelRatio, height: pageHeight, onClick: onPageClick, onRenderSuccess: onPageRenderSuccess, pageColors: renderHighContrast ? { background: 'black', foreground: '#ffff00', } : undefined, renderAnnotationLayer, renderForms, renderMode, renderTextLayer, scale: pageScale, width: pageWidth, }; return (

    react-pdf test page

    , pdf: PDFDocumentProxy | false | undefined, ) => console.log('Clicked a document', { event, pdf })} onItemClick={onItemClick} onLoadError={onDocumentLoadError} onLoadProgress={onDocumentLoadProgress} onLoadSuccess={onDocumentLoadSuccess} onSourceError={onDocumentLoadError} >
    {render ? : null}
    {render ? ( displayAll ? ( Array.from(new Array(numPages), (_el, index) => ( { ref?.scrollIntoView(); } : null } pageNumber={index + 1} /> )) ) : ( ) ) : null}
    {displayAll || (
    {`Page ${pageNumber || (numPages ? 1 : '--')} of ${numPages || '--'}`}
    )}
    {Array.from(new Array(numPages), (_el, index) => ( ))}
    ); } ================================================ FILE: test/ViewOptions.tsx ================================================ import { useId, useRef } from 'react'; import type { RenderMode } from './shared/types.js'; type ViewOptionsProps = { canvasBackground?: string; devicePixelRatio?: number; displayAll: boolean; pageHeight?: number; pageScale?: number; pageWidth?: number; renderHighContrast: boolean; renderMode?: RenderMode; rotate?: number; setCanvasBackground: (value: string | undefined) => void; setDevicePixelRatio: (value: number | undefined) => void; setDisplayAll: (value: boolean) => void; setPageHeight: (value: number | undefined) => void; setPageScale: (value: number | undefined) => void; setPageWidth: (value: number | undefined) => void; setRenderHighContrast: (value: boolean) => void; setRenderMode: (value: RenderMode | undefined) => void; setRotate: React.Dispatch>; }; export default function ViewOptions({ canvasBackground, devicePixelRatio, displayAll, pageHeight, pageScale, pageWidth, renderHighContrast, renderMode, rotate, setCanvasBackground, setDevicePixelRatio, setDisplayAll, setPageHeight, setPageScale, setPageWidth, setRenderHighContrast, setRenderMode, setRotate, }: ViewOptionsProps) { const devicePixelRatioInput = useRef(null); const pageHeightInput = useRef(null); const pageWidthInput = useRef(null); const pageScaleInput = useRef(null); const canvasBackgroundId = useId(); const pageWidthId = useId(); const pageHeightId = useId(); const pageScaleId = useId(); const devicePixelRatioId = useId(); const renderCanvasId = useId(); const renderCustomId = useId(); const renderNoneId = useId(); const rotationId = useId(); const displayAllId = useId(); const renderHighContrastId = useId(); function onCanvasBackgroundChange(event: React.ChangeEvent) { setCanvasBackground(event.target.value); } function onDevicePixelRatioChange(event: React.ChangeEvent) { event.preventDefault(); const input = devicePixelRatioInput.current; const { valueAsNumber: devicePixelRatio } = input as HTMLInputElement; setDevicePixelRatio(devicePixelRatio); } function onDisplayAllChange(event: React.ChangeEvent) { setDisplayAll(event.target.checked); } function onRenderHighContrastChange(event: React.ChangeEvent) { setRenderHighContrast(event.target.checked); } function onPageHeightChange(event: React.FormEvent) { event.preventDefault(); const input = pageHeightInput.current; const { valueAsNumber: nextHeight } = input as HTMLInputElement; setPageHeight(nextHeight); } function onPageScaleChange(event: React.FormEvent) { event.preventDefault(); const input = pageScaleInput.current; const { valueAsNumber: nextScale } = input as HTMLInputElement; setPageScale(nextScale); } function onPageWidthChange(event: React.FormEvent) { event.preventDefault(); const input = pageWidthInput.current; const { valueAsNumber: nextWidth } = input as HTMLInputElement; setPageWidth(nextWidth); } function onRenderModeChange(event: React.ChangeEvent) { const { value } = event.target; setRenderMode(value as RenderMode); } function changeRotation(by: number) { setRotate((prevRotate) => ((prevRotate || 0) + by + 360) % 360); } function rotateLeft() { changeRotation(-90); } function rotateRight() { changeRotation(90); } function onChangeRotate(event: React.ChangeEvent) { const { valueAsNumber: nextRotate } = event.target; changeRotation(nextRotate - (rotate || 0)); } function resetRotation() { setRotate(undefined); } function resetHeight() { setPageHeight(undefined); } function resetScale() { setPageScale(undefined); } function resetWidth() { setPageWidth(undefined); } function resetDevicePixelRatio() { setDevicePixelRatio(undefined); } return (
    View options
       
       
       
       
         
    ); } ================================================ FILE: test/global.d.ts ================================================ declare module '*.pdf' { const src: string; export default src; } ================================================ FILE: test/index.html ================================================ react-pdf test page
    ================================================ FILE: test/index.tsx ================================================ import { StrictMode } from 'react'; import { createRoot } from 'react-dom/client'; import Test from './Test.js'; const root = document.getElementById('root'); if (!root) { throw new Error('Could not find root element'); } createRoot(root).render( , ); ================================================ FILE: test/package.json ================================================ { "name": "test", "version": "4.0.0", "description": "A test page for React-PDF.", "private": true, "type": "module", "scripts": { "build": "vite build", "dev": "vite", "format": "biome format", "lint": "biome lint", "preview": "vite preview", "test": "yarn lint && yarn tsc && yarn format", "tsc": "tsc" }, "author": { "name": "Wojciech Maj", "email": "kontakt@wojtekmaj.pl" }, "license": "MIT", "dependencies": { "react": "^19.2.0", "react-dom": "^19.2.0", "react-pdf": "workspace:packages/react-pdf", "tiny-invariant": "^1.0.0" }, "devDependencies": { "@biomejs/biome": "2.2.2", "@types/react": "^19.2.0", "@vitejs/plugin-react": "^4.6.0", "typescript": "^5.9.2", "vite": "^7.1.11", "vite-plugin-static-copy": "^3.1.2" } } ================================================ FILE: test/shared/types.ts ================================================ import type { PDFDataRangeTransport } from 'pdfjs-dist'; import type { TypedArray } from 'pdfjs-dist/types/src/display/api.js'; type BinaryData = TypedArray | ArrayBuffer | number[] | string; export type Source = | { data: BinaryData | undefined } | { range: PDFDataRangeTransport } | { url: string }; export type ExternalLinkTarget = '_self' | '_blank' | '_parent' | '_top'; export type File = string | ArrayBuffer | Blob | Source | null; export type PassMethod = 'normal' | 'object' | 'string'; export type RenderMode = 'canvas' | 'custom' | 'none'; ================================================ FILE: test/shared/utils.ts ================================================ import invariant from 'tiny-invariant'; /** * Checks if we're running in a browser environment. */ export const isBrowser = typeof window !== 'undefined'; /** * Checks whether a variable provided is a string. * * @param {*} variable Variable to check */ export function isString(variable: unknown): variable is string { return typeof variable === 'string'; } /** * Checks whether a variable provided is an ArrayBuffer. * * @param {*} variable Variable to check */ export function isArrayBuffer(variable: unknown): variable is ArrayBuffer { return variable instanceof ArrayBuffer; } /** * Checks whether a variable provided is a Blob. * * @param {*} variable Variable to check */ export function isBlob(variable: unknown): variable is Blob { invariant(isBrowser, 'isBlob can only be used in a browser environment'); return variable instanceof Blob; } /** * Checks whether a variable provided is a data URI. * * @param {*} variable String to check */ export function isDataURI(variable: unknown): variable is `data:${string}` { return isString(variable) && variable.startsWith('data:'); } export function loadFromFile(file: Blob): Promise { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = () => { if (!reader.result) { return reject(new Error('Error while reading a file.')); } resolve(reader.result as ArrayBuffer); }; reader.onerror = (event) => { if (!event.target) { return reject(new Error('Error while reading a file.')); } const { error } = event.target; if (!error) { return reject(new Error('Error while reading a file.')); } switch (error.code) { case error.NOT_FOUND_ERR: return reject(new Error('Error while reading a file: File not found.')); case error.SECURITY_ERR: return reject(new Error('Error while reading a file: Security error.')); case error.ABORT_ERR: return reject(new Error('Error while reading a file: Aborted.')); default: return reject(new Error('Error while reading a file.')); } }; reader.readAsArrayBuffer(file); }); } ================================================ FILE: test/tsconfig.json ================================================ { "compilerOptions": { "isolatedModules": true, "jsx": "react-jsx", "module": "preserve", "moduleDetection": "force", "noEmit": true, "noUncheckedIndexedAccess": true, "skipLibCheck": true, "strict": true, "target": "esnext", "verbatimModuleSyntax": true } } ================================================ FILE: test/vite.config.ts ================================================ import path from 'node:path'; import { createRequire } from 'node:module'; import { defineConfig, normalizePath } from 'vite'; import react from '@vitejs/plugin-react'; import { viteStaticCopy } from 'vite-plugin-static-copy'; const require = createRequire(import.meta.url); const cMapsDir = normalizePath( path.join(path.dirname(require.resolve('pdfjs-dist/package.json')), 'cmaps'), ); const standardFontsDir = normalizePath( path.join(path.dirname(require.resolve('pdfjs-dist/package.json')), 'standard_fonts'), ); const wasmDir = normalizePath( path.join(path.dirname(require.resolve('pdfjs-dist/package.json')), 'wasm'), ); export default defineConfig({ base: './', plugins: [ react(), viteStaticCopy({ targets: [ { src: cMapsDir, dest: '' }, { src: standardFontsDir, dest: '' }, { src: wasmDir, dest: '' }, ], }), ], server: { fs: { allow: ['..'], }, }, }); ================================================ FILE: test-utils.ts ================================================ import { vi } from 'vitest'; import { server } from 'vitest/browser'; const { readFile } = server.commands; type Func = (...args: T) => void; function makeAsyncCallbackWithoutValue(): { func: Func; promise: Promise; } { let promiseResolve: (args: T) => void; const promise = new Promise((resolve) => { promiseResolve = resolve; }); const func: Func = vi.fn((...args: T) => promiseResolve(args)); return { func, promise }; } function makeAsyncCallbackWithValue(value: T): { func: Func; promise: Promise; } { let promiseResolve: (arg: T) => void; const promise = new Promise((resolve) => { promiseResolve = resolve; }); const func: Func = vi.fn(() => promiseResolve(value)); return { func, promise }; } export function makeAsyncCallback(): ReturnType< typeof makeAsyncCallbackWithoutValue >; export function makeAsyncCallback(value?: T): ReturnType>; export function makeAsyncCallback(value?: T) { if (value === undefined) { return makeAsyncCallbackWithoutValue(); } return makeAsyncCallbackWithValue(value); } export async function loadPDF(path: string): Promise<{ raw: string; readonly arrayBuffer: ArrayBuffer; readonly blob: Blob; readonly data: Uint8Array; readonly dataURI: string; readonly file: File; }> { const raw = await readFile(path, 'binary'); // Convert binary read as string to ArrayBuffer const arrayBuffer = new Uint8Array(raw.length); for (let i = 0; i < raw.length; i += 1) { arrayBuffer[i] = raw.charCodeAt(i) & 0xff; } return { raw, get arrayBuffer() { return arrayBuffer.buffer.slice(0); }, get blob() { return new Blob([arrayBuffer], { type: 'application/pdf' }); }, get data() { return new Uint8Array(arrayBuffer); }, get dataURI() { return `data:application/pdf;base64,${btoa(raw)}`; }, get file() { return new File([arrayBuffer], 'test.pdf', { type: 'application/pdf' }); }, }; } export function muteConsole(): void { vi.spyOn(globalThis.console, 'log').mockImplementation(() => { // Intentionally empty }); vi.spyOn(globalThis.console, 'error').mockImplementation(() => { // Intentionally empty }); vi.spyOn(globalThis.console, 'warn').mockImplementation(() => { // Intentionally empty }); } export function restoreConsole(): void { vi.mocked(globalThis.console.log).mockRestore(); vi.mocked(globalThis.console.error).mockRestore(); vi.mocked(globalThis.console.warn).mockRestore(); }