├── .gitignore ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── docs └── screenshots │ └── webviewer-custom-ui-01.png ├── package.json ├── public ├── assets │ └── favicon.ico ├── files │ └── demo.pdf ├── index.html ├── manifest.json └── serve.json ├── src ├── App.css ├── App.js ├── App.test.js ├── assets │ └── icons │ │ ├── ic_add_image_24px.svg │ │ ├── ic_annotation_add_redact_black_24px.svg │ │ ├── ic_annotation_apply_redact_black_24px.svg │ │ ├── ic_annotation_square_black_24px.svg │ │ ├── ic_chevron_left_black_24px.svg │ │ ├── ic_chevron_right_black_24px.svg │ │ ├── ic_close_black_24px.svg │ │ ├── ic_edit_page_24px.svg │ │ ├── ic_paragraph_24px.svg │ │ ├── ic_search_black_24px.svg │ │ ├── ic_select_black_24px.svg │ │ ├── ic_zoom_in_black_24px.svg │ │ └── ic_zoom_out_black_24px.svg ├── components │ └── SearchContainer │ │ ├── SearchContainer.css │ │ ├── SearchContainer.js │ │ └── index.js ├── index.css └── index.js └── tools └── copy-webviewer-files.js /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # WebViewer 4 | public/webviewer 5 | 6 | # dependencies 7 | /node_modules 8 | /.pnp 9 | .pnp.js 10 | 11 | # testing 12 | /coverage 13 | 14 | # production 15 | /build 16 | 17 | # misc 18 | .DS_Store 19 | .env.local 20 | .env.development.local 21 | .env.test.local 22 | .env.production.local 23 | package-lock.json 24 | /public/assets/webviewer/core 25 | /public/assets/webviewer/ui 26 | 27 | npm-debug.log* 28 | yarn-debug.log* 29 | yarn-error.log* 30 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to WebViewer - React sample 2 | 3 | ## Issues 4 | 1. Check existing issues (open/closed) to avoid duplicates. 5 | 2. Be clear about what the problem is. 6 | 3. Make sure to paste error output or logs. 7 | 4. Code snapshot or demos on online code editor will be very helpful. 8 | 9 | ## Pull requests 10 | 1. Fork the repository. 11 | 2. Create a branch from `master`. 12 | 3. Update the source code using style guides described below. 13 | 4. Lint your code with `npm run lint`. 14 | 5. Commit and push the changes with descriptive messages. 15 | 6. Create a pull request to `master`. 16 | 17 | \* Please note that all pull requests should be tied to an issue, and all but the most trivial pull requests should be discussed before hand. 18 | 19 | ## Style guides 20 | - Tab indentation (size of 2 spaces). 21 | - `'` instead of `"`. 22 | - Curly braces for block statements. 23 | - 1TBS brace style. 24 | - Semicolon at the end of each statement. 25 | - Object shorthand for ES6. 26 | - Parenthesis around arrow function argument. 27 | - Minimum line breaks. 28 | - No `use strict`. -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2024 Apryse Systems Inc. All rights reserved. 2 | WebViewer UI project/codebase or any derived works is only permitted in solutions with an active commercial Apryse WebViewer license. For exact licensing terms refer to the commercial WebViewer license. For licensing, pricing, or product questions, Contact [Sales](https://apryse.com/form/contact-sales). -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # WebViewer - Custom UI 2 | 3 | ⚠️ This sample has been moved to the [webviewer-samples repo](https://github.com/ApryseSDK/webviewer-samples/tree/main/webviewer-custom-ui). ⚠️ -------------------------------------------------------------------------------- /docs/screenshots/webviewer-custom-ui-01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ApryseSDK/webviewer-custom-ui/fdd383e7d9927f4f6da0977dc973eb8cc1f8c876/docs/screenshots/webviewer-custom-ui-01.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webviewer-react-sample", 3 | "version": "1.0.0", 4 | "private": true, 5 | "dependencies": { 6 | "@pdftron/webviewer": "^11.0.0", 7 | "react": "^18.3.1", 8 | "react-dom": "^18.3.1", 9 | "react-scripts": "^5.0.1" 10 | }, 11 | "devDependencies": { 12 | "btoa": "^1.2.1", 13 | "download": "^8.0.0", 14 | "fs-extra": "^11.2.0" 15 | }, 16 | "scripts": { 17 | "start": "react-scripts start", 18 | "build": "react-scripts build", 19 | "test": "react-scripts test", 20 | "eject": "react-scripts eject", 21 | "postinstall": "node tools/copy-webviewer-files.js" 22 | }, 23 | "eslintConfig": { 24 | "extends": "react-app" 25 | }, 26 | "browserslist": [ 27 | ">0.2%", 28 | "not dead", 29 | "not ie <= 11", 30 | "not op_mini all" 31 | ] 32 | } -------------------------------------------------------------------------------- /public/assets/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ApryseSDK/webviewer-custom-ui/fdd383e7d9927f4f6da0977dc973eb8cc1f8c876/public/assets/favicon.ico -------------------------------------------------------------------------------- /public/files/demo.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ApryseSDK/webviewer-custom-ui/fdd383e7d9927f4f6da0977dc973eb8cc1f8c876/public/files/demo.pdf -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 22 | React Sample 23 | 24 | 25 | 28 |
29 | 30 | 31 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /public/serve.json: -------------------------------------------------------------------------------- 1 | { 2 | "cleanUrls": false 3 | } -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --pdf-background: var(--grey-2); 3 | --tools-header-background: var(--grey-3); 4 | } 5 | 6 | .App { 7 | width: 100%; 8 | height: 100%; 9 | display: flex; 10 | flex-direction: row; 11 | overflow: hidden; 12 | } 13 | 14 | .App .header { 15 | width: 100%; 16 | height: 100px; 17 | padding: 8px 8px 8px 16px; 18 | box-sizing: border-box; 19 | background: #00a5e4; 20 | font-size: 1.2em; 21 | line-height: 44px; 22 | color: white; 23 | } 24 | 25 | #scroll-view { 26 | bottom: 0; 27 | /* leave room for 100px header */ 28 | height: 100%; 29 | width: 100%; 30 | overflow: auto; 31 | flex: 3; 32 | background-color: var(--pdf-background); 33 | } 34 | 35 | #viewer { 36 | margin: auto; 37 | } 38 | 39 | #main-column { 40 | flex-direction: row; 41 | flex: 3; 42 | } 43 | 44 | #tools { 45 | display:flex; 46 | flex-direction: row; 47 | justify-content: center; 48 | background-color: var(--tools-header-background); 49 | } 50 | 51 | #tools button { 52 | background-color: var(--tools-header-background); 53 | fill: #757575; 54 | width: fit-content; 55 | height: 35px; 56 | margin: 5px; 57 | border: none; 58 | outline: none; 59 | box-shadow: none; 60 | cursor: pointer; 61 | } 62 | 63 | #tools button:hover { 64 | background-color: #c7d2dd; 65 | border-radius: 4px; 66 | } 67 | 68 | .pageContainer { 69 | border: 1px solid #ececec; 70 | box-shadow: 5px 10px 18px #ececec; 71 | position: relative; 72 | } 73 | 74 | .flexbox-container { 75 | display: flex; 76 | } 77 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import React, { useRef, useEffect, useState } from 'react'; 2 | import SearchContainer from './components/SearchContainer'; 3 | import { ReactComponent as ZoomIn } from './assets/icons/ic_zoom_in_black_24px.svg'; 4 | import { ReactComponent as ZoomOut } from './assets/icons/ic_zoom_out_black_24px.svg'; 5 | import { ReactComponent as AnnotationRectangle } from './assets/icons/ic_annotation_square_black_24px.svg'; 6 | import { ReactComponent as AnnotationRedact } from './assets/icons/ic_annotation_add_redact_black_24px.svg'; 7 | import { ReactComponent as AnnotationApplyRedact} from './assets/icons/ic_annotation_apply_redact_black_24px.svg'; 8 | import { ReactComponent as Search } from './assets/icons/ic_search_black_24px.svg'; 9 | import { ReactComponent as Select } from './assets/icons/ic_select_black_24px.svg'; 10 | import { ReactComponent as EditContent } from './assets/icons/ic_edit_page_24px.svg'; 11 | import { ReactComponent as AddParagraph } from './assets/icons/ic_paragraph_24px.svg'; 12 | import { ReactComponent as AddImageContent } from './assets/icons/ic_add_image_24px.svg'; 13 | import './App.css'; 14 | 15 | const App = () => { 16 | const viewer = useRef(null); 17 | const scrollView = useRef(null); 18 | const searchTerm = useRef(null); 19 | const searchContainerRef = useRef(null); 20 | 21 | const [documentViewer, setDocumentViewer] = useState(null); 22 | const [annotationManager, setAnnotationManager] = useState(null); 23 | const [searchContainerOpen, setSearchContainerOpen] = useState(false); 24 | const [isInContentEditMode, setIsInContentEditMode] = useState(false); 25 | 26 | const Annotations = window.Core.Annotations; 27 | 28 | // if using a class, equivalent of componentDidMount 29 | useEffect(() => { 30 | const Core = window.Core; 31 | Core.setWorkerPath('/webviewer'); 32 | Core.enableFullPDF(); 33 | 34 | const documentViewer = new Core.DocumentViewer(); 35 | documentViewer.setScrollViewElement(scrollView.current); 36 | documentViewer.setViewerElement(viewer.current); 37 | documentViewer.enableAnnotations(); 38 | documentViewer.loadDocument('/files/demo.pdf'); 39 | 40 | setDocumentViewer(documentViewer); 41 | 42 | documentViewer.addEventListener('documentLoaded', () => { 43 | console.log('document loaded'); 44 | documentViewer.setToolMode(documentViewer.getTool(Core.Tools.ToolNames.EDIT)); 45 | setAnnotationManager(documentViewer.getAnnotationManager()); 46 | }); 47 | }, []); 48 | 49 | const zoomOut = () => { 50 | documentViewer.zoomTo(documentViewer.getZoomLevel() - 0.25); 51 | }; 52 | 53 | const zoomIn = () => { 54 | documentViewer.zoomTo(documentViewer.getZoomLevel() + 0.25); 55 | }; 56 | 57 | const startEditingContent = () => { 58 | const contentEditManager = documentViewer.getContentEditManager(); 59 | contentEditManager.startContentEditMode(); 60 | setIsInContentEditMode(true); 61 | } 62 | 63 | const endEditingContent = () => { 64 | setIsInContentEditMode(false); 65 | documentViewer.setToolMode(documentViewer.getTool(window.Core.Tools.ToolNames.EDIT)); 66 | const contentEditManager = documentViewer.getContentEditManager(); 67 | contentEditManager.endContentEditMode(); 68 | } 69 | 70 | const addParagraph = () => { 71 | if (isInContentEditMode) { 72 | const addParagraphTool = documentViewer.getTool(window.Core.Tools.ToolNames.ADD_PARAGRAPH); 73 | documentViewer.setToolMode(addParagraphTool); 74 | } else { 75 | alert('Content Edit mode is not enabled.') 76 | } 77 | }; 78 | 79 | const addImageContent = () => { 80 | if (isInContentEditMode) { 81 | const addImageContentTool = documentViewer.getTool(window.Core.Tools.ToolNames.ADD_IMAGE_CONTENT); 82 | documentViewer.setToolMode(addImageContentTool); 83 | } else { 84 | alert('Content Edit mode is not enabled.') 85 | } 86 | }; 87 | 88 | const createRectangle = () => { 89 | documentViewer.setToolMode(documentViewer.getTool(window.Core.Tools.ToolNames.RECTANGLE)); 90 | }; 91 | 92 | const selectTool = () => { 93 | documentViewer.setToolMode(documentViewer.getTool(window.Core.Tools.ToolNames.EDIT)); 94 | }; 95 | 96 | const createRedaction = () => { 97 | documentViewer.setToolMode(documentViewer.getTool(window.Core.Tools.ToolNames.REDACTION)); 98 | }; 99 | 100 | const applyRedactions = async () => { 101 | const annotationManager = documentViewer.getAnnotationManager(); 102 | annotationManager.enableRedaction(); 103 | await annotationManager.applyRedactions(); 104 | }; 105 | 106 | return ( 107 |
108 |
109 |
110 | 113 | 116 | 119 | 122 | 125 | 128 | 131 | 134 | 137 | 209 |
210 |
211 | 212 | 217 | Case sensitive 218 | 219 | 220 | 225 | Whole word 226 | 227 |
228 |
229 |
230 | 231 | 234 | 235 | 236 | 242 | 248 | 249 |
250 |
251 | { 252 | searchResults.map((result, idx) => { 253 | const { 254 | ambient_str: ambientStr, 255 | page_num: pageNum, 256 | result_str_start: resultStrStart, 257 | result_str_end: resultStrEnd, 258 | } = result; 259 | const textBeforeSearchValue = ambientStr.slice(0, resultStrStart); 260 | const searchValue = ambientStr.slice( 261 | resultStrStart, 262 | resultStrEnd, 263 | ); 264 | const textAfterSearchValue = ambientStr.slice(resultStrEnd); 265 | let pageHeader = null; 266 | if (!pageRenderTracker[pageNum]) { 267 | pageRenderTracker[pageNum] = true; 268 | pageHeader =
Page {pageNum}
269 | } 270 | return ( 271 |
272 | {pageHeader} 273 |
{documentViewer.setActiveSearchResult(result)}} 276 | > 277 | {textBeforeSearchValue} 278 | 279 | {searchValue} 280 | 281 | {textAfterSearchValue} 282 |
283 |
284 | ) 285 | }) 286 | } 287 |
288 | 289 | ); 290 | }; 291 | 292 | export default SearchContainer; 293 | -------------------------------------------------------------------------------- /src/components/SearchContainer/index.js: -------------------------------------------------------------------------------- 1 | import SearchContainer from './SearchContainer'; 2 | 3 | export default SearchContainer; 4 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --grey-1: #F8F9FA; 3 | --grey-2: #F1F3F5; 4 | --grey-3: #E7EBEE; 5 | --grey-4: #DEE2E6; 6 | --grey-5: #CFD4DA; 7 | --divider: var(--grey-5); 8 | } 9 | 10 | html, body, #root { 11 | width: 100%; 12 | height: 100%; 13 | margin: 0; 14 | padding: 0; 15 | font-family: Arial, Helvetica, sans-serif; 16 | } 17 | 18 | .webviewer { 19 | flex: 1; 20 | margin: 8px; 21 | -webkit-box-shadow: 1px 1px 10px #999; 22 | box-shadow: 1px 1px 10px #999; 23 | } 24 | 25 | .divider { 26 | width: 100%; 27 | margin: 16px 0; 28 | border-top: 1px solid var(--divider); 29 | } 30 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import { createRoot } from 'react-dom/client'; 2 | import './index.css'; 3 | import App from './App'; 4 | 5 | const root = createRoot(document.getElementById('root')); 6 | root.render(); 7 | -------------------------------------------------------------------------------- /tools/copy-webviewer-files.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs-extra'); 2 | 3 | const copyFiles = async () => { 4 | try { 5 | await fs.copy('./node_modules/@pdftron/webviewer/public/core', './public/webviewer/'); 6 | console.log('WebViewer files copied over successfully'); 7 | } catch (err) { 8 | console.error(err); 9 | } 10 | }; 11 | 12 | copyFiles(); 13 | --------------------------------------------------------------------------------