├── .eslintrc.json
├── .github
├── PULL_REQUEST_TEMPLATE.md
└── workflows
│ ├── cla.yml
│ ├── publish.yml
│ └── style_and_syntax_checks.yml
├── .gitignore
├── .nvmrc
├── .prettierignore
├── .prettierrc.js
├── README.md
├── contributingGuides
└── CLA.md
├── example
├── .eslintrc.js
├── .gitignore
├── assets
│ └── fonts
│ │ ├── ExpensifyNeue-Bold.woff
│ │ └── ExpensifyNeue-Bold.woff2
├── babel.config.json
├── package-lock.json
├── package.json
├── public
│ ├── example.gif
│ ├── example.pdf
│ ├── example_protected.pdf
│ └── index.html
├── src
│ ├── App.tsx
│ ├── index.css
│ └── index.js
├── tsconfig.json
└── webpack.config.js
├── package-lock.json
├── package.json
├── src
├── PDFPasswordForm.tsx
├── PDFPreviewer.tsx
├── PageRenderer.tsx
├── constants.ts
├── helpers.ts
├── index.ts
├── pdf.worker.d.ts
├── styles.ts
└── types.ts
└── tsconfig.json
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "root": true,
3 | "env": {
4 | "browser": true,
5 | "es2021": true,
6 | "jest": true
7 | },
8 | "parser": "@typescript-eslint/parser",
9 | "parserOptions": {
10 | "ecmaVersion": "latest",
11 | "sourceType": "module"
12 | },
13 | "extends": ["eslint:recommended", "airbnb", "plugin:@typescript-eslint/recommended", "plugin:react/jsx-runtime", "plugin:jsx-a11y/recommended", "plugin:react/recommended", "prettier"],
14 | "plugins": ["@typescript-eslint", "react", "jsx-a11y"],
15 | "ignorePatterns": ["dist", "node_modules", ".git"],
16 | "rules": {
17 | "arrow-parens": [
18 | "error",
19 | "as-needed",
20 | {
21 | "requireForBlockBody": true
22 | }
23 | ],
24 | "no-invalid-this": "error",
25 | "react/require-default-props": "off",
26 | "react/function-component-definition": [
27 | "error",
28 | {
29 | "namedComponents": "function-declaration",
30 | "unnamedComponents": "arrow-function"
31 | }
32 | ],
33 | "no-restricted-syntax": [
34 | "error",
35 | // The following four selectors prevent the usage of async/await
36 | {
37 | "selector": "AwaitExpression",
38 | "message": "async/await is not allowed"
39 | },
40 | {
41 | "selector": "FunctionDeclaration[async=true]",
42 | "message": "async functions are not allowed"
43 | },
44 | {
45 | "selector": "FunctionExpression[async=true]",
46 | "message": "async functions are not allowed"
47 | },
48 | {
49 | "selector": "ArrowFunctionExpression[async=true]",
50 | "message": "async functions are not allowed"
51 | },
52 | {
53 | "selector": "MethodDefinition[async=true]",
54 | "message": "async methods are not allowed"
55 | }
56 | ]
57 | },
58 | "overrides": [
59 | {
60 | "files": ["*.ts", "*.tsx"],
61 | "plugins": ["@typescript-eslint"],
62 | "extends": ["plugin:@typescript-eslint/recommended-type-checked", "airbnb-typescript", "prettier"],
63 | "parser": "@typescript-eslint/parser",
64 | "parserOptions": {
65 | "project": "./tsconfig.json"
66 | }
67 | }
68 | ],
69 | "settings": {
70 | "react": {
71 | "version": "detect"
72 | }
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/.github/PULL_REQUEST_TEMPLATE.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | ### Details
4 |
5 |
6 |
7 | ### Related Issues
8 |
9 |
10 |
11 | GH_LINK
12 |
13 | ### Manual Tests
14 |
15 |
18 |
19 | ### Linked PRs
20 |
21 |
24 |
--------------------------------------------------------------------------------
/.github/workflows/cla.yml:
--------------------------------------------------------------------------------
1 | name: CLA Assistant
2 |
3 | on:
4 | issue_comment:
5 | types: [created]
6 | pull_request_target:
7 | types: [opened, synchronize]
8 |
9 | jobs:
10 | CLA:
11 | uses: Expensify/GitHub-Actions/.github/workflows/cla.yml@main
12 | secrets: inherit
13 |
--------------------------------------------------------------------------------
/.github/workflows/publish.yml:
--------------------------------------------------------------------------------
1 | name: Publish package to npmjs
2 |
3 | on:
4 | push:
5 | branches: [main]
6 |
7 | # Ensure that only one instance of this workflow executes at a time.
8 | # If multiple PRs are merged in quick succession, there will only ever be one publish workflow running and one pending.
9 | concurrency: ${{ github.workflow }}
10 |
11 | jobs:
12 | publish:
13 | # os-botify[bot] will update the version on `main`, so this check is important to prevent an infinite loop
14 | if: ${{ github.actor != 'os-botify[bot]' }}
15 | uses: Expensify/GitHub-Actions/.github/workflows/npmPublish.yml@main
16 | secrets: inherit
17 | with:
18 | should_run_build: true
19 |
--------------------------------------------------------------------------------
/.github/workflows/style_and_syntax_checks.yml:
--------------------------------------------------------------------------------
1 | name: Style and Syntax Checks
2 |
3 | on:
4 | pull_request:
5 | types: [opened, synchronize]
6 |
7 | jobs:
8 | checks:
9 | runs-on: ubuntu-latest
10 | steps:
11 | - name: Checkout code
12 | # v3
13 | uses: actions/checkout@ee0669bd1cc54295c223e0bb666b733df41de1c5
14 |
15 | - name: Setup Node
16 | # v3
17 | uses: actions/setup-node@1a4442cacd436585916779262731d5b162bc6ec7
18 | with:
19 | node-version: 18
20 | cache: npm
21 |
22 | - name: Install dependencies
23 | run: npm ci
24 |
25 | - name: Run ESLint
26 | run: npx eslint src
27 |
28 | - name: Run Prettier
29 | run: npx prettier . --check
30 |
31 | - name: Run TypeScript checks
32 | run: npm run ts
33 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # OSX
2 | .DS_Store
3 |
4 | # node.js
5 | node_modules/
6 | npm-debug.log
7 | dist/
8 |
9 | # NPM file created by GitHub actions
10 | .npmrc
11 | .idea/
12 |
--------------------------------------------------------------------------------
/.nvmrc:
--------------------------------------------------------------------------------
1 | 20.10.0
2 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | # The GH actions don't seem to compile and verify themselves well when Prettier is applied to them
2 | dist
3 | package.json
4 | package-lock.json
5 | *.html
6 | *.css
7 | *.scss
8 | *.md
9 | *.yml
10 | *.yaml
11 |
--------------------------------------------------------------------------------
/.prettierrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | tabWidth: 4,
3 | singleQuote: true,
4 | trailingComma: 'all',
5 | bracketSpacing: false,
6 | arrowParens: 'always',
7 | printWidth: 190,
8 | singleAttributePerLine: true,
9 | };
10 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # `react-fast-pdf`
2 |
3 | Awesome PDF viewer, that combines the best of others into one tool.
4 |
5 | 
6 |
7 | # Features
8 |
9 | - Simple usage.
10 | - Loads/previews PDF files of any size.
11 | - Performance optimized.
12 | - Easy customized.
13 | - Support by Expensify.
14 |
15 | # Local testing
16 |
17 | Actually, for testing changes locally, we need to connect `example` to the code from `src` directory (_**NPM** by default_).
18 |
19 | Update a few fields in `example` directory:
20 | 1. `package.json`:
21 | ```diff
22 | -"react-fast-pdf": "^1.*"
23 | +"react-fast-pdf": "../src"
24 | ```
25 |
26 | 2. `tsconfig.json`:
27 | ```diff
28 | -"rootDir": "./src"
29 | +"rootDir": "../"
30 | ```
31 |
32 | 3. Run `npm i` in the directory. It should update `package-lock.json`.
33 |
34 | The final result has to look like in this [commit](https://github.com/Expensify/react-fast-pdf/pull/9/commits/29334948a936def7a1ba967aa632f6f5e951a3cb).
35 |
36 | # Deploying
37 |
38 | This repo automatically publishes to NPM when PRs are merged to main.
39 |
40 | # Contributing
41 |
42 | Right now, contributions to this library are done under https://github.com/Expensify/App. Please refer to that repo and all it's guidelines for contributing.
43 |
--------------------------------------------------------------------------------
/contributingGuides/CLA.md:
--------------------------------------------------------------------------------
1 | ## Expensify Individual Contributor License Agreement
2 |
3 | Thank you for your interest in contributing to open source software projects (“Projects”) made available by Expensify Inc or its affiliates (“Expensify”). This Individual Contributor License Agreement (“Agreement”) sets out the terms governing any source code, object code, bug fixes, configuration changes, tools, specifications, documentation, data, materials, feedback, information or other works of authorship that you submit or have submitted, in any form and in any manner, to Expensify in respect of any of the Projects (collectively “Contributions”). If you have any questions respecting this Agreement, please contact reactnative@expensify.com.
4 |
5 | You agree that the following terms apply to all of your past, present and future Contributions. Except for the licenses granted in this Agreement, you retain all of your rights, title and interest in and to your Contributions.
6 |
7 | **Copyright License.** You hereby grant, and agree to grant, to Expensify a non-exclusive, perpetual, irrevocable, worldwide, fully-paid, royalty-free, transferable copyright license to reproduce, prepare derivative works of, publicly display, publicly perform, and distribute your Contributions and such derivative works, with the right to sublicense the foregoing rights through multiple tiers of sublicensees.
8 |
9 | **Patent License.** You hereby grant, and agree to grant, to Expensify a non-exclusive, perpetual, irrevocable, worldwide, fully-paid, royalty-free, transferable patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer your Contributions, where such license applies only to those patent claims licensable by you that are necessarily infringed by your Contributions alone or by combination of your Contributions with the Project to which such Contributions were submitted, with the right to sublicense the foregoing rights through multiple tiers of sublicensees.
10 |
11 | **Moral Rights.** To the fullest extent permitted under applicable law, you hereby waive, and agree not to assert, all of your “moral rights” in or relating to your Contributions for the benefit of Expensify, its assigns, and their respective direct and indirect sublicensees.
12 |
13 | **Third Party Content/Rights.** If your Contribution includes or is based on any source code, object code, bug fixes, configuration changes, tools, specifications, documentation, data, materials, feedback, information or other works of authorship that were not authored by you (“Third Party Content”) or if you are aware of any third party intellectual property or proprietary rights associated with your Contribution (“Third Party Rights”), then you agree to include with the submission of your Contribution full details respecting such Third Party Content and Third Party Rights, including, without limitation, identification of which aspects of your Contribution contain Third Party Content or are associated with Third Party Rights, the owner/author of the Third Party Content and Third Party Rights, where you obtained the Third Party Content, and any applicable third party license terms or restrictions respecting the Third Party Content and Third Party Rights. For greater certainty, the foregoing obligations respecting the identification of Third Party Content and Third Party Rights do not apply to any portion of a Project that is incorporated into your Contribution to that same Project.
14 |
15 | **Representations.** You represent that, other than the Third Party Content and Third Party Rights identified by you in accordance with this Agreement, you are the sole author of your Contributions and are legally entitled to grant the foregoing licenses and waivers in respect of your Contributions. If your Contributions were created in the course of your employment with your past or present employer(s), you represent that such employer(s) has authorized you to make your Contributions on behalf of such employer(s) or such employer(s) has waived all of their right, title or interest in or to your Contributions.
16 |
17 | **No Obligation.** You acknowledge that Expensify is under no obligation to use or incorporate your Contributions into any of the Projects. The decision to use or incorporate your Contributions into any of the Projects will be made at the sole discretion of Expensify or its authorized delegates.
18 |
19 | **Assignment.** You agree that Expensify may assign this Agreement, and all of its rights, obligations and licenses hereunder.
20 |
21 |
--------------------------------------------------------------------------------
/example/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: true,
3 | env: {
4 | browser: true,
5 | es2021: true,
6 | jest: true,
7 | },
8 | parser: '@typescript-eslint/parser',
9 | parserOptions: {
10 | ecmaVersion: 'latest',
11 | sourceType: 'module',
12 | tsconfigRootDir: __dirname,
13 | },
14 | extends: ['eslint:recommended', 'airbnb', 'plugin:@typescript-eslint/recommended', 'plugin:react/jsx-runtime', 'plugin:jsx-a11y/recommended', 'plugin:react/recommended', 'prettier'],
15 | plugins: ['@typescript-eslint', 'react', 'jsx-a11y'],
16 | ignorePatterns: ['dist', 'node_modules', '.git'],
17 | rules: {
18 | 'arrow-parens': [
19 | 'error',
20 | 'as-needed',
21 | {
22 | requireForBlockBody: true,
23 | },
24 | ],
25 | 'no-invalid-this': 'error',
26 | 'react/function-component-definition': [
27 | 'error',
28 | {
29 | namedComponents: 'function-declaration',
30 | unnamedComponents: 'arrow-function',
31 | },
32 | ],
33 | 'no-restricted-syntax': [
34 | 'error',
35 | // The following four selectors prevent the usage of async/await
36 | {
37 | selector: 'AwaitExpression',
38 | message: 'async/await is not allowed',
39 | },
40 | {
41 | selector: 'FunctionDeclaration[async=true]',
42 | message: 'async functions are not allowed',
43 | },
44 | {
45 | selector: 'FunctionExpression[async=true]',
46 | message: 'async functions are not allowed',
47 | },
48 | {
49 | selector: 'ArrowFunctionExpression[async=true]',
50 | message: 'async functions are not allowed',
51 | },
52 | {
53 | selector: 'MethodDefinition[async=true]',
54 | message: 'async methods are not allowed',
55 | },
56 | ],
57 | },
58 | overrides: [
59 | {
60 | files: ['*.ts', '*.tsx'],
61 | plugins: ['@typescript-eslint'],
62 | extends: ['plugin:@typescript-eslint/recommended-type-checked', 'airbnb-typescript', 'prettier'],
63 | parser: '@typescript-eslint/parser',
64 | parserOptions: {
65 | project: './tsconfig.json',
66 | },
67 | },
68 | ],
69 | settings: {
70 | react: {
71 | version: 'detect',
72 | },
73 | },
74 | };
75 |
--------------------------------------------------------------------------------
/example/.gitignore:
--------------------------------------------------------------------------------
1 | # OSX
2 | .DS_Store
3 |
4 | # node.js
5 | node_modules/
6 | npm-debug.log
7 | dist/
8 |
9 |
--------------------------------------------------------------------------------
/example/assets/fonts/ExpensifyNeue-Bold.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Expensify/react-fast-pdf/de65f3f359330606ba79bf732cf72acc9ec5440c/example/assets/fonts/ExpensifyNeue-Bold.woff
--------------------------------------------------------------------------------
/example/assets/fonts/ExpensifyNeue-Bold.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Expensify/react-fast-pdf/de65f3f359330606ba79bf732cf72acc9ec5440c/example/assets/fonts/ExpensifyNeue-Bold.woff2
--------------------------------------------------------------------------------
/example/babel.config.json:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | "@babel/preset-env",
4 | [
5 | "@babel/preset-react",
6 | {
7 | "runtime": "automatic"
8 | }
9 | ],
10 | "@babel/preset-typescript"
11 | ],
12 | "sourceMaps": true
13 | }
14 |
--------------------------------------------------------------------------------
/example/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-fast-pdf-example",
3 | "version": "1.0.0",
4 | "description": "react-fast-pdf",
5 | "main": "./src/index.js",
6 | "files": [
7 | "dist",
8 | "src"
9 | ],
10 | "scripts": {
11 | "start": "webpack-dev-server --mode=development --open --hot",
12 | "build": "webpack --mode=production",
13 | "lint": "eslint src --fix",
14 | "ts": "tsc --noEmit",
15 | "prettier": "prettier --write .",
16 | "prettier-watch": "onchange \"**/*.js\" -- prettier --write --ignore-unknown {{changed}}",
17 | "test": "echo \"Error: no test specified\" && exit 1"
18 | },
19 | "repository": {
20 | "type": "git",
21 | "url": "git+https://github.com/Expensify/react-fast-pdf.git"
22 | },
23 | "keywords": [
24 | "react-fast-pdf"
25 | ],
26 | "author": "Expensify, Inc.",
27 | "license": "MIT",
28 | "bugs": {
29 | "url": "https://github.com/Expensify/react-fast-pdf/issues"
30 | },
31 | "homepage": "https://github.com/Expensify/react-fast-pdf#readme",
32 | "dependencies": {
33 | "react": "^18.2.0",
34 | "react-dom": "^18.2.0",
35 | "react-fast-pdf": "^1.0.23"
36 | },
37 | "devDependencies": {
38 | "@babel/cli": "^7.22.9",
39 | "@babel/core": "^7.22.9",
40 | "@babel/preset-env": "^7.22.9",
41 | "@babel/preset-react": "^7.22.5",
42 | "@babel/preset-typescript": "^7.22.5",
43 | "@types/react": "^18.2.17",
44 | "@types/react-dom": "^18.2.7",
45 | "@typescript-eslint/eslint-plugin": "^6.2.0",
46 | "@typescript-eslint/parser": "^6.2.0",
47 | "babel-loader": "^9.1.3",
48 | "copy-webpack-plugin": "^11.0.0",
49 | "css-loader": "^6.8.1",
50 | "eslint": "^8.46.0",
51 | "eslint-config-airbnb": "^19.0.4",
52 | "eslint-config-airbnb-typescript": "^17.1.0",
53 | "eslint-config-prettier": "^8.9.0",
54 | "eslint-plugin-import": "^2.28.0",
55 | "eslint-plugin-jsx-a11y": "^6.7.1",
56 | "eslint-plugin-react": "^7.33.1",
57 | "eslint-plugin-react-hooks": "^4.6.0",
58 | "html-webpack-plugin": "^5.5.3",
59 | "onchange": "^7.1.0",
60 | "prettier": "^3.0.0",
61 | "style-loader": "^3.3.3",
62 | "ts-loader": "^9.4.4",
63 | "typescript": "^5.1.6",
64 | "webpack": "^5.88.2",
65 | "webpack-cli": "^5.1.4",
66 | "webpack-dev-server": "^4.15.1"
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/example/public/example.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Expensify/react-fast-pdf/de65f3f359330606ba79bf732cf72acc9ec5440c/example/public/example.gif
--------------------------------------------------------------------------------
/example/public/example.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Expensify/react-fast-pdf/de65f3f359330606ba79bf732cf72acc9ec5440c/example/public/example.pdf
--------------------------------------------------------------------------------
/example/public/example_protected.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Expensify/react-fast-pdf/de65f3f359330606ba79bf732cf72acc9ec5440c/example/public/example_protected.pdf
--------------------------------------------------------------------------------
/example/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | react-fast-pdf
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/example/src/App.tsx:
--------------------------------------------------------------------------------
1 | import React, {useState} from 'react';
2 | import ReactFastPDF, {PDFPreviewer} from 'react-fast-pdf';
3 | import './index.css';
4 |
5 | function App() {
6 | const [file, setFile] = useState(null);
7 |
8 | return (
9 |
10 | Hello, I am {ReactFastPDF.PackageName}!
11 |
12 | {file ? (
13 | <>
14 |
21 |
22 |
27 | >
28 | ) : (
29 | <>
30 | Please choose a file for previewing:
31 |
32 |
33 |
40 |
41 |
48 |
49 | {
53 | const uploadedFile = event?.target?.files?.[0];
54 |
55 | if (!uploadedFile) {
56 | return;
57 | }
58 |
59 | setFile(URL.createObjectURL(uploadedFile));
60 | }}
61 | />
62 |
63 | >
64 | )}
65 |
66 | );
67 | }
68 |
69 | export default App;
70 |
--------------------------------------------------------------------------------
/example/src/index.css:
--------------------------------------------------------------------------------
1 | @font-face {
2 | font-family: ExpensifyMono-Neue;
3 | font-weight: 700;
4 | font-style: normal;
5 | src:
6 | url('../assets/fonts/ExpensifyNeue-Bold.woff2') format('woff2'),
7 | url('../assets/fonts/ExpensifyNeue-Bold.woff') format('woff');
8 | }
9 |
10 | * {
11 | color: #e7ece9;
12 | font-family: ExpensifyMono-Neue;
13 | }
14 |
15 | *:not(div) {
16 | margin: 0;
17 | }
18 |
19 | body {
20 | width: 100vw;
21 | height: 100vh;
22 | overflow: hidden;
23 | }
24 |
25 | #root {
26 | height: 100%;
27 | display: flex;
28 | }
29 |
30 | .container {
31 | padding: 16px;
32 | display: flex;
33 | flex: 1;
34 | flex-direction: column;
35 | align-items: center;
36 | background-color: #07271f;
37 | }
38 |
39 | .title {
40 | margin-bottom: 16px;
41 | }
42 |
43 | .react-pdf__Page {
44 | direction: ltr;
45 | margin: 1px auto -8px auto;
46 | background-color: transparent !important;
47 | }
48 |
49 | .buttons_container {
50 | margin-top: 16px;
51 | display: flex;
52 | }
53 |
54 | .button {
55 | margin: 0 8px;
56 | padding: 8px 16px;
57 | font-size: 18px;
58 | border: 0;
59 | border-radius: 16px;
60 | background-color: #03d47c;
61 | cursor: 'pointer';
62 | }
63 |
64 | .button_back {
65 | position: absolute;
66 | z-index: 1;
67 | left: 16px;
68 | top: 16px;
69 | }
70 |
--------------------------------------------------------------------------------
/example/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import {createRoot} from 'react-dom/client';
4 | import App from './App.tsx';
5 |
6 | const container = document.getElementById('root');
7 | const root = createRoot(container);
8 |
9 | // eslint-disable-next-line react/jsx-filename-extension
10 | root.render();
11 |
--------------------------------------------------------------------------------
/example/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://json.schemastore.org/tsconfig",
3 | "compilerOptions": {
4 | "target": "es2016" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */,
5 | "jsx": "react" /* Specify what JSX code is generated. */,
6 | "module": "commonjs" /* Specify what module code is generated. */,
7 | "rootDir": "./src" /* Specify the root folder within your source files. */,
8 | "declaration": true /* Generate .d.ts files from TypeScript and JavaScript files in your project. */,
9 | "declarationMap": true /* Create sourcemaps for d.ts files. */,
10 | "outDir": "./dist" /* Specify an output folder for all emitted files. */,
11 | "isolatedModules": true /* Ensure that each file can be safely transpiled without relying on other imports. */,
12 | "allowSyntheticDefaultImports": true /* Allow 'import x from y' when a module doesn't have a default export. */,
13 | "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */,
14 | "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */,
15 | "strict": true /* Enable all strict type-checking options. */,
16 | "skipLibCheck": true /* Skip type checking all .d.ts files. */
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/example/webpack.config.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/no-var-requires */
2 | const path = require('path');
3 | const HtmlWebpackPlugin = require('html-webpack-plugin');
4 | const CopyWebpackPlugin = require('copy-webpack-plugin');
5 |
6 | const cMapsDir = path.join(path.dirname(require.resolve('pdfjs-dist/package.json')), 'cmaps');
7 |
8 | module.exports = {
9 | entry: path.join(__dirname, 'src', 'index.js'),
10 | output: {
11 | path: path.resolve(__dirname, 'dist'),
12 | filename: 'bundle.js',
13 | },
14 | resolve: {
15 | modules: [path.join(__dirname, 'src'), 'node_modules'],
16 | alias: {
17 | react: path.join(__dirname, 'node_modules', 'react'),
18 | },
19 | extensions: ['.tsx', '.ts', '.js'],
20 | },
21 | devServer: {
22 | port: 8080,
23 | },
24 | module: {
25 | rules: [
26 | {
27 | test: /\.(js|jsx)?$/,
28 | exclude: /node_modules/,
29 | use: {
30 | loader: 'babel-loader',
31 | options: {
32 | presets: ['@babel/preset-env', '@babel/preset-react'],
33 | },
34 | },
35 | },
36 | {
37 | test: /\.(ts|tsx)$/,
38 | exclude: /node_modules/,
39 | use: 'ts-loader',
40 | },
41 | {
42 | test: /\.css$/,
43 | use: ['style-loader', 'css-loader'],
44 | },
45 | // We are importing this worker as a string by using asset/source otherwise it will default to loading via an HTTPS request later.
46 | // This causes issues if we have gone offline before the pdfjs web worker is set up as we won't be able to load it from the server.
47 | {
48 | // eslint-disable-next-line prefer-regex-literals
49 | test: new RegExp('node_modules/pdfjs-dist/build/pdf.worker.min.mjs'),
50 | type: 'asset/source',
51 | },
52 | {
53 | // eslint-disable-next-line prefer-regex-literals
54 | test: new RegExp('node_modules/pdfjs-dist/legacy/build/pdf.worker.min.mjs'),
55 | type: 'asset/source',
56 | },
57 | ],
58 | },
59 | plugins: [
60 | new HtmlWebpackPlugin({
61 | template: path.join(__dirname, 'public', 'index.html'),
62 | }),
63 | new CopyWebpackPlugin({
64 | patterns: [
65 | {
66 | from: cMapsDir,
67 | to: 'cmaps/',
68 | },
69 | ],
70 | }),
71 | ],
72 | };
73 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-fast-pdf",
3 | "version": "1.0.28",
4 | "description": "react-fast-pdf",
5 | "main": "./dist/index.js",
6 | "files": [
7 | "dist",
8 | "src"
9 | ],
10 | "scripts": {
11 | "build": "tsc",
12 | "lint": "eslint src --fix",
13 | "ts": "tsc --noEmit",
14 | "prettier": "prettier --write .",
15 | "prettier-watch": "onchange \"**/*.js\" -- prettier --write --ignore-unknown {{changed}}",
16 | "test": "echo \"Error: no test specified\" && exit 1"
17 | },
18 | "repository": {
19 | "type": "git",
20 | "url": "git+https://github.com/Expensify/react-fast-pdf.git"
21 | },
22 | "keywords": [
23 | "react-fast-pdf"
24 | ],
25 | "author": "Expensify, Inc.",
26 | "license": "MIT",
27 | "bugs": {
28 | "url": "https://github.com/Expensify/react-fast-pdf/issues"
29 | },
30 | "homepage": "https://github.com/Expensify/react-fast-pdf#readme",
31 | "dependencies": {
32 | "react-pdf": "9.2.0",
33 | "react-window": "^1.8.11"
34 | },
35 | "peerDependencies": {
36 | "lodash": "4.x",
37 | "pdfjs-dist": "4.8.69",
38 | "react": "18.x",
39 | "react-dom": "18.x"
40 | },
41 | "devDependencies": {
42 | "@babel/cli": "^7.22.9",
43 | "@babel/core": "^7.22.9",
44 | "@babel/preset-env": "^7.22.9",
45 | "@babel/preset-react": "^7.22.5",
46 | "@babel/preset-typescript": "^7.22.5",
47 | "@types/lodash": "^4.14.198",
48 | "@types/react": "^18.2.17",
49 | "@types/react-dom": "^18.2.7",
50 | "@types/react-window": "^1.8.5",
51 | "@typescript-eslint/eslint-plugin": "^6.2.0",
52 | "@typescript-eslint/parser": "^6.2.0",
53 | "babel-loader": "^9.1.3",
54 | "css-loader": "^6.8.1",
55 | "eslint": "^8.46.0",
56 | "eslint-config-airbnb": "^19.0.4",
57 | "eslint-config-airbnb-typescript": "^17.1.0",
58 | "eslint-config-prettier": "^8.9.0",
59 | "eslint-plugin-import": "^2.28.0",
60 | "eslint-plugin-jsx-a11y": "^6.7.1",
61 | "eslint-plugin-react": "^7.33.1",
62 | "eslint-plugin-react-hooks": "^4.6.0",
63 | "html-webpack-plugin": "^5.5.3",
64 | "onchange": "^7.1.0",
65 | "prettier": "^3.0.0",
66 | "style-loader": "^3.3.3",
67 | "ts-loader": "^9.4.4",
68 | "typescript": "^5.3.2",
69 | "webpack": "^5.88.2",
70 | "webpack-cli": "^5.1.4",
71 | "webpack-dev-server": "^4.15.1"
72 | },
73 | "engines": {
74 | "node": ">=20.10.0",
75 | "npm": ">=10.2.3"
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/src/PDFPasswordForm.tsx:
--------------------------------------------------------------------------------
1 | import React, {useState, useRef, useMemo, type ChangeEvent, type FormEventHandler} from 'react';
2 |
3 | import {pdfPasswordFormStyles as styles} from './styles';
4 | import {isSafari} from './helpers';
5 |
6 | type Props = {
7 | isPasswordInvalid?: boolean;
8 | onSubmit?: (password: string) => void;
9 | onPasswordChange?: (password: string) => void;
10 | onPasswordFieldFocus?: (isFocused: boolean) => void;
11 | };
12 |
13 | function PDFPasswordForm({isPasswordInvalid, onSubmit, onPasswordChange, onPasswordFieldFocus}: Props) {
14 | const [password, setPassword] = useState('');
15 | const [validationErrorText, setValidationErrorText] = useState('');
16 | const [shouldShowForm, setShouldShowForm] = useState(false);
17 |
18 | const textInputRef = useRef(null);
19 |
20 | const errorText = useMemo(() => {
21 | if (isPasswordInvalid) {
22 | return 'Incorrect password. Please try again.';
23 | }
24 |
25 | if (validationErrorText) {
26 | return validationErrorText;
27 | }
28 |
29 | return '';
30 | }, [isPasswordInvalid, validationErrorText]);
31 |
32 | const updatePassword = (event: ChangeEvent) => {
33 | const newPassword = event.target.value;
34 |
35 | setPassword(newPassword);
36 | onPasswordChange?.(newPassword);
37 | setValidationErrorText('');
38 | };
39 |
40 | const validate = () => {
41 | if (!isPasswordInvalid && password) {
42 | return true;
43 | }
44 |
45 | if (!password) {
46 | setValidationErrorText('Password required. Pleaser enter.');
47 | }
48 |
49 | return false;
50 | };
51 |
52 | const submitPassword: FormEventHandler = (e) => {
53 | e.preventDefault();
54 |
55 | if (!validate()) {
56 | return;
57 | }
58 |
59 | onSubmit?.(password);
60 | };
61 |
62 | const validateAndNotifyPasswordBlur = () => {
63 | validate();
64 |
65 | onPasswordFieldFocus?.(false);
66 | };
67 |
68 | if (!shouldShowForm) {
69 | return (
70 |
71 |
72 | This PDF is password protected.
73 |
74 | Please
75 | setShouldShowForm(true)}
79 | >
80 | enter the password
81 |
82 | to view it.
83 |
84 |
85 | );
86 | }
87 |
88 | return (
89 |
90 |
View PDF
91 |
92 |
128 |
129 | );
130 | }
131 |
132 | PDFPasswordForm.displayName = 'PDFPasswordForm';
133 |
134 | export type {Props as PDFPasswordFormProps};
135 | export default PDFPasswordForm;
136 |
--------------------------------------------------------------------------------
/src/PDFPreviewer.tsx:
--------------------------------------------------------------------------------
1 | // eslint-disable-next-line import/extensions
2 | import pdfWorkerSource from 'pdfjs-dist/build/pdf.worker.min.mjs';
3 | // eslint-disable-next-line import/extensions
4 | import pdfWorkerLegacySource from 'pdfjs-dist/legacy/build/pdf.worker.mjs';
5 | import React, {memo, useCallback, useLayoutEffect, useRef, useState} from 'react';
6 | import type {CSSProperties, ReactNode} from 'react';
7 | import times from 'lodash/times';
8 | import {VariableSizeList as List} from 'react-window';
9 | import {Document, pdfjs} from 'react-pdf';
10 | import 'react-pdf/dist/Page/AnnotationLayer.css';
11 | import 'react-pdf/dist/Page/TextLayer.css';
12 |
13 | import type {PDFDocument, PageViewport} from './types';
14 | import {pdfPreviewerStyles as styles} from './styles';
15 | import PDFPasswordForm, {type PDFPasswordFormProps} from './PDFPasswordForm';
16 | import PageRenderer from './PageRenderer';
17 | import {PAGE_BORDER, LARGE_SCREEN_SIDE_SPACING, DEFAULT_DOCUMENT_OPTIONS, DEFAULT_EXTERNAL_LINK_TARGET, PDF_PASSWORD_FORM_RESPONSES} from './constants';
18 | import {isMobileSafari, isModernSafari, setListAttributes} from './helpers';
19 |
20 | type Props = {
21 | file: string;
22 | pageMaxWidth: number;
23 | isSmallScreen: boolean;
24 | maxCanvasWidth?: number;
25 | maxCanvasHeight?: number;
26 | maxCanvasArea?: number;
27 | renderPasswordForm?: ({isPasswordInvalid, onSubmit, onPasswordChange}: Omit) => ReactNode | null;
28 | LoadingComponent?: ReactNode;
29 | ErrorComponent?: ReactNode;
30 | shouldShowErrorComponent?: boolean;
31 | onLoadError?: () => void;
32 | containerStyle?: CSSProperties;
33 | contentContainerStyle?: CSSProperties;
34 | };
35 |
36 | type OnPasswordCallback = (password: string | null) => void;
37 |
38 | const shouldUseLegacyWorker = isMobileSafari() && !isModernSafari();
39 | // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
40 | const pdfWorker = shouldUseLegacyWorker ? pdfWorkerLegacySource : pdfWorkerSource;
41 |
42 | pdfjs.GlobalWorkerOptions.workerSrc = URL.createObjectURL(new Blob([pdfWorker], {type: 'text/javascript'}));
43 |
44 | const DefaultLoadingComponent = Loading...
;
45 | const DefaultErrorComponent = Failed to load the PDF file :(
;
46 |
47 | function PDFPreviewer({
48 | file,
49 | pageMaxWidth,
50 | isSmallScreen,
51 | maxCanvasWidth,
52 | maxCanvasHeight,
53 | maxCanvasArea,
54 | LoadingComponent = DefaultLoadingComponent,
55 | ErrorComponent = DefaultErrorComponent,
56 | renderPasswordForm,
57 | containerStyle,
58 | contentContainerStyle,
59 | shouldShowErrorComponent = true,
60 | onLoadError,
61 | }: Props) {
62 | const [pageViewports, setPageViewports] = useState([]);
63 | const [numPages, setNumPages] = useState(0);
64 | const [containerWidth, setContainerWidth] = useState(0);
65 | const [containerHeight, setContainerHeight] = useState(0);
66 | const [shouldRequestPassword, setShouldRequestPassword] = useState(false);
67 | const [isPasswordInvalid, setIsPasswordInvalid] = useState(false);
68 | const containerRef = useRef(null);
69 | const onPasswordCallbackRef = useRef(null);
70 | const listRef = useRef(null);
71 |
72 | /**
73 | * Calculate the devicePixelRatio the page should be rendered with
74 | * Each platform has a different default devicePixelRatio and different canvas limits, we need to verify that
75 | * with the default devicePixelRatio it will be able to display the pdf correctly, if not we must change the devicePixelRatio.
76 | * @param {Number} width of the page
77 | * @param {Number} height of the page
78 | * @returns {Number} devicePixelRatio for this page on this platform
79 | */
80 | const getDevicePixelRatio = (width: number, height: number): number | undefined => {
81 | if (!maxCanvasWidth || !maxCanvasHeight || !maxCanvasArea) {
82 | return undefined;
83 | }
84 |
85 | const nbPixels = width * height;
86 | const ratioHeight = maxCanvasHeight / height;
87 | const ratioWidth = maxCanvasWidth / width;
88 | const ratioArea = Math.sqrt(maxCanvasArea / nbPixels);
89 | const ratio = Math.min(ratioHeight, ratioArea, ratioWidth);
90 |
91 | if (ratio > window.devicePixelRatio) {
92 | return undefined;
93 | }
94 |
95 | return ratio;
96 | };
97 |
98 | /**
99 | * Calculates a proper page width.
100 | * It depends on a screen size. Also, the app should take into account the page borders.
101 | */
102 | const calculatePageWidth = useCallback(() => {
103 | const pageWidthOnLargeScreen = Math.min(containerWidth - LARGE_SCREEN_SIDE_SPACING * 2, pageMaxWidth);
104 | const pageWidth = isSmallScreen ? containerWidth : pageWidthOnLargeScreen;
105 |
106 | return pageWidth + PAGE_BORDER * 2;
107 | }, [containerWidth, pageMaxWidth, isSmallScreen]);
108 |
109 | /**
110 | * Calculates a proper page height. The method should be called only when there are page viewports.
111 | * It is based on a ratio between the specific page viewport width and provided page width.
112 | * Also, the app should take into account the page borders.
113 | */
114 | const calculatePageHeight = useCallback(
115 | (pageIndex: number) => {
116 | if (pageViewports.length === 0) {
117 | return 0;
118 | }
119 |
120 | const pageWidth = calculatePageWidth();
121 |
122 | const {width: pageViewportWidth, height: pageViewportHeight} = pageViewports[pageIndex];
123 | const scale = pageWidth / pageViewportWidth;
124 |
125 | return pageViewportHeight * scale + PAGE_BORDER * 2;
126 | },
127 | [pageViewports, calculatePageWidth],
128 | );
129 |
130 | const estimatedPageHeight = calculatePageHeight(0);
131 | const pageWidth = calculatePageWidth();
132 |
133 | /**
134 | * Upon successful document load, combine an array of page viewports,
135 | * set the number of pages on PDF,
136 | * hide/reset PDF password form, and notify parent component that
137 | * user input is no longer required.
138 | */
139 | const onDocumentLoadSuccess = (pdf: PDFDocument) => {
140 | Promise.all(
141 | times(pdf.numPages, (index: number) => {
142 | const pageNumber = index + 1;
143 |
144 | return pdf.getPage(pageNumber).then((page) => page.getViewport({scale: 1}));
145 | }),
146 | ).then(
147 | (viewports: PageViewport[]) => {
148 | setPageViewports(viewports);
149 | setNumPages(pdf.numPages);
150 | setShouldRequestPassword(false);
151 | setIsPasswordInvalid(false);
152 | },
153 | () => {},
154 | );
155 | };
156 |
157 | /**
158 | * Initiate password challenge process. The react-pdf/Document
159 | * component calls this handler to indicate that a PDF requires a
160 | * password, or to indicate that a previously provided password was
161 | * invalid.
162 | *
163 | * The PasswordResponses constants used below were copied from react-pdf
164 | * because they're not exported in entry.webpack.
165 | */
166 | const initiatePasswordChallenge = (callback: OnPasswordCallback, reason: number) => {
167 | onPasswordCallbackRef.current = callback;
168 |
169 | if (reason === PDF_PASSWORD_FORM_RESPONSES.NEED_PASSWORD) {
170 | setShouldRequestPassword(true);
171 | } else if (reason === PDF_PASSWORD_FORM_RESPONSES.INCORRECT_PASSWORD) {
172 | setShouldRequestPassword(true);
173 | setIsPasswordInvalid(true);
174 | }
175 | };
176 |
177 | /**
178 | * Send password to react-pdf via its callback so that it can attempt to load
179 | * the PDF.
180 | */
181 | const attemptPDFLoad = (password: string) => {
182 | onPasswordCallbackRef.current?.(password);
183 | };
184 |
185 | /**
186 | * Render a form to handle password typing.
187 | * The method renders the passed or default component.
188 | */
189 | const internalRenderPasswordForm = useCallback(() => {
190 | const onSubmit = attemptPDFLoad;
191 | const onPasswordChange = () => setIsPasswordInvalid(false);
192 |
193 | if (typeof renderPasswordForm === 'function') {
194 | return renderPasswordForm({
195 | isPasswordInvalid,
196 | onSubmit,
197 | onPasswordChange,
198 | });
199 | }
200 |
201 | return (
202 |
207 | );
208 | }, [isPasswordInvalid, attemptPDFLoad, setIsPasswordInvalid, renderPasswordForm]);
209 |
210 | /**
211 | * Reset List style cache when dimensions change
212 | */
213 | useLayoutEffect(() => {
214 | if (containerWidth > 0 && containerHeight > 0) {
215 | listRef.current?.resetAfterIndex(0);
216 | }
217 | }, [containerWidth, containerHeight]);
218 |
219 | useLayoutEffect(() => {
220 | if (!containerRef.current) {
221 | return undefined;
222 | }
223 | const resizeObserver = new ResizeObserver(() => {
224 | if (!containerRef.current) {
225 | return;
226 | }
227 | setContainerWidth(containerRef.current.clientWidth);
228 | setContainerHeight(containerRef.current.clientHeight);
229 | });
230 | resizeObserver.observe(containerRef.current);
231 |
232 | return () => resizeObserver.disconnect();
233 | }, []);
234 |
235 | return (
236 |
240 |
241 |
251 | {pageViewports.length > 0 && (
252 |
263 | {PageRenderer}
264 |
265 | )}
266 |
267 |
268 |
269 | {shouldRequestPassword && internalRenderPasswordForm()}
270 |
271 | );
272 | }
273 |
274 | PDFPasswordForm.displayName = 'PDFPreviewer';
275 |
276 | export default memo(PDFPreviewer);
277 |
--------------------------------------------------------------------------------
/src/PageRenderer.tsx:
--------------------------------------------------------------------------------
1 | import React, {memo, type CSSProperties} from 'react';
2 | import {Page} from 'react-pdf';
3 | import {pdfPreviewerStyles as styles} from './styles';
4 | import {PAGE_BORDER} from './constants';
5 |
6 | type Props = {
7 | index: number;
8 | style: CSSProperties;
9 | data: {
10 | pageWidth: number;
11 | estimatedPageHeight: number;
12 | calculatePageHeight: (pageIndex: number) => number;
13 | getDevicePixelRatio: (width: number, height: number) => number | undefined;
14 | numPages: number;
15 | containerHeight: number;
16 | };
17 | };
18 |
19 | function PageRenderer({index, style, data}: Props) {
20 | const {pageWidth, estimatedPageHeight, calculatePageHeight, getDevicePixelRatio, numPages, containerHeight} = data;
21 | /**
22 | * Render a specific page based on its index.
23 | * The method includes a wrapper to apply virtualized styles.
24 | */
25 | const pageHeight = calculatePageHeight(index);
26 | const devicePixelRatio = getDevicePixelRatio(pageWidth, pageHeight);
27 | const parsedHeight = parseFloat(style.height as unknown as string);
28 | const parsedTop = parseFloat(style.top as unknown as string);
29 | const topPadding = numPages > 1 || parsedHeight > containerHeight ? parsedTop + PAGE_BORDER : (containerHeight - parsedHeight) / 2;
30 | return (
31 |
43 | );
44 | }
45 |
46 | PageRenderer.displayName = 'PageRenderer';
47 |
48 | export default memo(PageRenderer);
49 |
--------------------------------------------------------------------------------
/src/constants.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Each page has a default border. The app should take this size into account
3 | * when calculates the page width and height.
4 | */
5 | const PAGE_BORDER = 9;
6 |
7 | /**
8 | * Pages should be more narrow than the container on large screens. The app should take this size into account
9 | * when calculates the page width.
10 | */
11 | const LARGE_SCREEN_SIDE_SPACING = 40;
12 |
13 | /**
14 | * An object in which additional parameters to be passed to PDF.js can be defined.
15 | * 1. cMapUrl - The URL where the predefined Adobe CMaps are located. Include the trailing slash.
16 | * 2. cMapPacked - specifies if the Adobe CMaps are binary packed or not. The default value is `true`.
17 | */
18 | const DEFAULT_DOCUMENT_OPTIONS = {
19 | cMapUrl: 'cmaps/',
20 | cMapPacked: true,
21 | };
22 |
23 | /**
24 | * Link target for external links rendered in annotations.
25 | */
26 | const DEFAULT_EXTERNAL_LINK_TARGET = '_blank';
27 |
28 | /**
29 | * Constants for password-related error responses received from react-pdf.
30 | */
31 | const PDF_PASSWORD_FORM_RESPONSES = {
32 | NEED_PASSWORD: 1,
33 | INCORRECT_PASSWORD: 2,
34 | };
35 |
36 | export {PAGE_BORDER, LARGE_SCREEN_SIDE_SPACING, DEFAULT_DOCUMENT_OPTIONS, DEFAULT_EXTERNAL_LINK_TARGET, PDF_PASSWORD_FORM_RESPONSES};
37 |
--------------------------------------------------------------------------------
/src/helpers.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Fetch browser name from UA string
3 | */
4 | const getBrowser = (): string => {
5 | const {userAgent} = window.navigator;
6 | const match = userAgent.match(/(opera|chrome|safari|firefox|msie|trident(?=\/))/i) ?? [];
7 | let temp: RegExpMatchArray | null;
8 | let browserName = '';
9 |
10 | if (/trident/i.test(match[1])) {
11 | return 'IE';
12 | }
13 |
14 | if (match[1] && match[1].toLowerCase() === 'chrome') {
15 | temp = userAgent.match(/\b(OPR)/);
16 | if (temp !== null) {
17 | return 'Opera';
18 | }
19 |
20 | temp = userAgent.match(/\b(Edg)/);
21 | if (temp !== null) {
22 | return 'Edge';
23 | }
24 | }
25 |
26 | browserName = match[1] ?? navigator.appName;
27 |
28 | return browserName ? browserName.toLowerCase() : 'other';
29 | };
30 |
31 | /**
32 | * Checks if requesting user agent is Safari browser on a mobile device
33 | */
34 | const isMobileSafari = (): boolean => {
35 | const {userAgent} = window.navigator;
36 |
37 | return /iP(ad|od|hone)/i.test(userAgent) && /WebKit/i.test(userAgent) && !/(CriOS|FxiOS|OPiOS|mercury)/i.test(userAgent);
38 | };
39 |
40 | const isSafari = () => getBrowser() === 'safari' || isMobileSafari();
41 |
42 | const isModernSafari = (): boolean => {
43 | const version = navigator.userAgent.match(/OS (\d+_\d+)/);
44 | const iosVersion = version ? version[1].replace('_', '.') : '';
45 |
46 | return parseFloat(iosVersion) >= 18;
47 | };
48 |
49 | type ListRef = {
50 | tabIndex: number;
51 | };
52 |
53 | /**
54 | * Sets attributes to list container.
55 | * It unblocks a default scroll by keyboard of browsers.
56 | */
57 | const setListAttributes = (ref: ListRef | undefined) => {
58 | if (!ref) {
59 | return;
60 | }
61 |
62 | /**
63 | * Useful for elements that should not be navigated to directly using the "Tab" key,
64 | * but need to have keyboard focus set to them.
65 | */
66 | // eslint-disable-next-line no-param-reassign
67 | ref.tabIndex = -1;
68 | };
69 |
70 | export {getBrowser, isMobileSafari, isSafari, isModernSafari, setListAttributes};
71 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | import PDFPreviewer from './PDFPreviewer';
2 |
3 | const PACKAGE_NAME = 'react-fast-pdf';
4 |
5 | export {PDFPreviewer};
6 |
7 | export default {
8 | PackageName: PACKAGE_NAME,
9 | };
10 |
--------------------------------------------------------------------------------
/src/pdf.worker.d.ts:
--------------------------------------------------------------------------------
1 | declare module 'pdfjs-dist/build/pdf.worker.min.mjs';
2 | declare module 'pdfjs-dist/legacy/build/pdf.worker.mjs';
3 |
--------------------------------------------------------------------------------
/src/styles.ts:
--------------------------------------------------------------------------------
1 | import type {ComponentStyles} from './types';
2 |
3 | const pdfPreviewerStyles: ComponentStyles = {
4 | container: {
5 | width: '100%',
6 | height: '100%',
7 | display: 'flex',
8 | alignItems: 'center',
9 | justifyContent: 'center',
10 | },
11 | innerContainer: {
12 | width: '100%',
13 | height: '100%',
14 | /**
15 | * It's being used on Web/Desktop only to vertically center short PDFs,
16 | * while preventing the overflow of the top of long PDF files.
17 | */
18 | display: 'grid',
19 | alignItems: 'center',
20 | justifyContent: 'center',
21 | overflow: 'hidden',
22 | },
23 | invisibleContainer: {
24 | position: 'absolute',
25 | opacity: 0,
26 | zIndex: -1,
27 | },
28 | list: {
29 | overflowX: 'hidden',
30 | // There properties disable "focus" effect on list
31 | boxShadow: 'none',
32 | outline: 'none',
33 | } as const,
34 | pageWrapper: {
35 | display: 'flex',
36 | },
37 | };
38 |
39 | const pdfPasswordFormStyles: ComponentStyles = {
40 | container: {
41 | display: 'flex',
42 | flexDirection: 'column',
43 | alignItems: 'center',
44 | justifyContent: 'center',
45 | backgroundColor: '#07271f',
46 | },
47 | infoMessage: {
48 | fontSize: 18,
49 | textAlign: 'center',
50 | },
51 | infoMessageButton: {
52 | textDecoration: 'underline',
53 | cursor: 'pointer',
54 | },
55 | form: {
56 | marginTop: 16,
57 | display: 'flex',
58 | flexDirection: 'column',
59 | justifyContent: 'center',
60 | },
61 | inputLabel: {
62 | fontSize: 16,
63 | },
64 | input: {
65 | marginLeft: 16,
66 | padding: '8px 16px',
67 | fontSize: 16,
68 | border: 0,
69 | borderRadius: 16,
70 | backgroundColor: '#184e3d',
71 | outline: 'none',
72 | },
73 | errorMessage: {
74 | marginTop: 8,
75 | color: '#f25730',
76 | fontSize: 16,
77 | textAlign: 'center',
78 | },
79 | confirmButton: {
80 | marginTop: 16,
81 | padding: '8px 16px',
82 | fontSize: 18,
83 | border: 0,
84 | borderRadius: 16,
85 | backgroundColor: '#03d47c',
86 | cursor: 'pointer',
87 | },
88 | };
89 |
90 | export {pdfPreviewerStyles, pdfPasswordFormStyles};
91 |
--------------------------------------------------------------------------------
/src/types.ts:
--------------------------------------------------------------------------------
1 | import {CSSProperties} from 'react';
2 |
3 | type Page = {
4 | getViewport: ({scale}: {scale: number}) => PageViewport;
5 | };
6 |
7 | type PDFDocument = {
8 | numPages: number;
9 | getPage: (pageNumber: number) => Promise;
10 | };
11 |
12 | type PageViewport = {
13 | height: number;
14 | width: number;
15 | };
16 |
17 | type ComponentStyles = {
18 | [key: string]: CSSProperties;
19 | };
20 |
21 | export type {PDFDocument, PageViewport, ComponentStyles};
22 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://json.schemastore.org/tsconfig",
3 | "compilerOptions": {
4 | "target": "es2016" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */,
5 | "jsx": "react" /* Specify what JSX code is generated. */,
6 | "module": "nodenext" /* Specify what module code is generated. */,
7 | "rootDir": "./src" /* Specify the root folder within your source files. */,
8 | "declaration": true /* Generate .d.ts files from TypeScript and JavaScript files in your project. */,
9 | "declarationMap": true /* Create sourcemaps for d.ts files. */,
10 | "outDir": "./dist" /* Specify an output folder for all emitted files. */,
11 | "isolatedModules": true /* Ensure that each file can be safely transpiled without relying on other imports. */,
12 | "allowSyntheticDefaultImports": true /* Allow 'import x from y' when a module doesn't have a default export. */,
13 | "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */,
14 | "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */,
15 | "strict": true /* Enable all strict type-checking options. */,
16 | "skipLibCheck": true /* Skip type checking all .d.ts files. */,
17 | "moduleResolution": "nodenext"
18 | },
19 | "include": ["./src"],
20 | "exclude": ["./node_modules", "./dist"]
21 | }
22 |
--------------------------------------------------------------------------------