├── .babelrc
├── .gitignore
├── LICENSE
├── README.md
├── package-lock.json
├── package.json
├── public
├── compressed.tracemonkey-pldi-09.pdf
├── index.html
├── pdf.worker.js
└── sample-annotations.json
├── sceenshot.png
├── src
├── index.js
└── pdf
│ ├── AnnotationStore.js
│ ├── PDFAnnotation.js
│ ├── PDFViewer.css
│ ├── PDFViewer.jsx
│ ├── endless
│ ├── AnnotatablePage.jsx
│ └── EndlessViewer.jsx
│ ├── index.js
│ └── paginated
│ ├── AnnotatablePage.jsx
│ └── PaginatedViewer.jsx
├── test
└── index.js
└── webpack.config.js
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | "@babel/env",
4 | "@babel/react"
5 | ],
6 | "plugins": [
7 | "@babel/transform-runtime",
8 | "@babel/proposal-class-properties"
9 | ],
10 | "env": {
11 | "production-esm": {
12 | "presets": [
13 | ["@babel/env", { "modules": false }],
14 | "@babel/react"
15 | ],
16 | "plugins": [
17 | ["@babel/transform-runtime", { "useESModules": true }],
18 | "@babel/proposal-class-properties"
19 | ]
20 | }
21 | }
22 | }
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | dist
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | BSD 3-Clause License
2 |
3 | Copyright (c) 2020, Pelagios Network
4 | All rights reserved.
5 |
6 | Redistribution and use in source and binary forms, with or without
7 | modification, are permitted provided that the following conditions are met:
8 |
9 | 1. Redistributions of source code must retain the above copyright notice, this
10 | list of conditions and the following disclaimer.
11 |
12 | 2. Redistributions in binary form must reproduce the above copyright notice,
13 | this list of conditions and the following disclaimer in the documentation
14 | and/or other materials provided with the distribution.
15 |
16 | 3. Neither the name of the copyright holder nor the names of its
17 | contributors may be used to endorse or promote products derived from
18 | this software without specific prior written permission.
19 |
20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Recogito-PDF
2 |
3 | Annotate a PDF document in React. Powered by [PDF.js](https://mozilla.github.io/pdf.js/),
4 | [RecogitoJS](https://github.com/recogito/recogito-js) and [Annotorious](https://github.com/recogito/annotorious).
5 |
6 | 
7 |
8 | ## Using the Component
9 |
10 | - Import the `PDFViewer` component and provide the `url` to the PDF file
11 | - It's recommended to set a link to `pdf.worker.js` from PDF.js (copy included in folder `public`)
12 |
13 | ```js
14 | import React from 'react';
15 | import ReactDOM from 'react-dom';
16 | import { pdfjs, PDFViewer } from '@recogito/recogito-react-pdf';
17 |
18 | pdfjs.GlobalWorkerOptions.workerSrc = 'pdf.worker.js';
19 |
20 | window.onload = function() {
21 |
22 | // Recogito init config (optional)
23 | // see https://github.com/recogito/recogito-js/wiki/API-Reference
24 | const config = { /* ... */ };
25 |
26 | // Initial annotations in W3C Web Annotation format
27 | const annotations = [ /* ... */ ];
28 |
29 | // CRUD event handlers
30 | const onCreateAnnotation = function () { /* ... */ };
31 | const onUpdateAnnotation = function () { /* ... */ };
32 | const onDeleteAnnotation = function () { /* ... */ };
33 |
34 | // Viewer mode can be "paginated" or "scrolling"
35 | const mode = "paginated";
36 |
37 | ReactDOM.render(
38 | ,
46 | document.getElementById('app')
47 | );
48 |
49 | }
50 | ```
51 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@recogito/recogito-react-pdf",
3 | "version": "0.8.9",
4 | "description": "A React component for annotating PDF, powered by PDF.js and RecogitoJS",
5 | "main": "src/index.js",
6 | "scripts": {
7 | "start": "webpack serve --mode development --open"
8 | },
9 | "repository": {
10 | "type": "git",
11 | "url": "git+https://github.com/recogito/recogito-react-pdf.git"
12 | },
13 | "keywords": [
14 | "Annotation",
15 | "PDF",
16 | "RecogitoJS"
17 | ],
18 | "author": "Rainer Simon",
19 | "license": "BSD-3-Clause",
20 | "bugs": {
21 | "url": "https://github.com/recogito/recogito-react-pdf/issues"
22 | },
23 | "homepage": "https://github.com/recogito/recogito-react-pdf#readme",
24 | "devDependencies": {
25 | "@babel/cli": "^7.8.0",
26 | "@babel/core": "^7.9.0",
27 | "@babel/plugin-proposal-class-properties": "^7.8.0",
28 | "@babel/plugin-transform-runtime": "^7.9.0",
29 | "@babel/preset-env": "^7.9.0",
30 | "@babel/preset-react": "^7.9.0",
31 | "babel-loader": "^8.2.2",
32 | "css-loader": "^5.2.7",
33 | "file-loader": "^6.2.0",
34 | "html-webpack-plugin": "^4.5.2",
35 | "react": "^17.0.2",
36 | "react-dom": "^17.0.2",
37 | "sass": "^1.35.2",
38 | "sass-loader": "^10.2.0",
39 | "style-loader": "^2.0.0"
40 | },
41 | "dependencies": {
42 | "@recogito/annotorious": "^2.7.8",
43 | "@recogito/recogito-connections": "^0.1.11",
44 | "@recogito/recogito-js": "^1.8.1",
45 | "pdfjs-dist": "2.9.359",
46 | "react-icons": "^4.2.0",
47 | "webpack": "^5.58.1",
48 | "webpack-cli": "^4.9.0",
49 | "webpack-dev-server": "^4.3.1"
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/public/compressed.tracemonkey-pldi-09.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/UltraDEV007/react-pdf-recogit/cc59889aeda30fdca7bd8655e798d4cb08359360/public/compressed.tracemonkey-pldi-09.pdf
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Recogito-PDF | Example
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/public/sample-annotations.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "type": "Annotation",
4 | "body": [
5 | {
6 | "type": "TextualBody",
7 | "value": "Test #1",
8 | "purpose": "commenting"
9 | }
10 | ],
11 | "target": {
12 | "selector": [
13 | {
14 | "type": "TextQuoteSelector",
15 | "exact": "Type Specialization"
16 | },
17 | {
18 | "type": "TextPositionSelector",
19 | "start": 25,
20 | "end": 44,
21 | "page": 1
22 | }
23 | ]
24 | },
25 | "@context": "http://www.w3.org/ns/anno.jsonld",
26 | "id": "#b5639d48-6b1d-4341-8211-a97dcb278865"
27 | },
28 | {
29 | "type": "Annotation",
30 | "body": [
31 | {
32 | "type": "TextualBody",
33 | "value": "Test #2",
34 | "purpose": "commenting"
35 | }
36 | ],
37 | "target": {
38 | "selector": [
39 | {
40 | "type": "TextQuoteSelector",
41 | "exact": "Abstract"
42 | },
43 | {
44 | "type": "TextPositionSelector",
45 | "start": 598,
46 | "end": 606,
47 | "page": 1
48 | }
49 | ]
50 | },
51 | "@context": "http://www.w3.org/ns/anno.jsonld",
52 | "id": "#6dc3dc53-056c-4178-9954-3e6deb6b4675"
53 | },
54 | {
55 | "type": "Annotation",
56 | "body": [
57 | {
58 | "type": "TextualBody",
59 | "value": "Test #3",
60 | "purpose": "commenting"
61 | }
62 | ],
63 | "target": {
64 | "selector": [
65 | {
66 | "type": "TextQuoteSelector",
67 | "exact": "Figure 1. Sample program:"
68 | },
69 | {
70 | "type": "TextPositionSelector",
71 | "start": 4012,
72 | "end": 4037,
73 | "page": 2
74 | }
75 | ]
76 | },
77 | "@context": "http://www.w3.org/ns/anno.jsonld",
78 | "id": "#068333e8-2616-4a90-883d-27c8595a806a"
79 | }
80 | ]
--------------------------------------------------------------------------------
/sceenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/UltraDEV007/react-pdf-recogit/cc59889aeda30fdca7bd8655e798d4cb08359360/sceenshot.png
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import * as pdfjs from 'pdfjs-dist/legacy/build/pdf';
2 | import PDFViewer from './pdf/PDFViewer';
3 |
4 | pdfjs.GlobalWorkerOptions.workerSrc = 'pdf.worker.js';
5 |
6 | export {
7 | pdfjs, PDFViewer
8 | }
9 |
--------------------------------------------------------------------------------
/src/pdf/AnnotationStore.js:
--------------------------------------------------------------------------------
1 | /** A minimal local annotation store **/
2 | export default class AnnotationStore {
3 |
4 | constructor() {
5 | this._annotations = [];
6 | }
7 |
8 | setAnnotations(annotations) {
9 | this._annotations = annotations;
10 | }
11 |
12 | createAnnotation(annotation) {
13 | this._annotations.push(annotation);
14 | }
15 |
16 | updateAnnotation(updated, previous) {
17 | this._annotations = this._annotations.map(a =>
18 | a.id === previous.id ? updated : a);
19 | }
20 |
21 | deleteAnnotation(annotation) {
22 | this._annotations = this._annotations.filter(a =>
23 | a.id !== annotation.id);
24 | }
25 |
26 | getAnnotations(pageNumber) {
27 | // Text annotations on this page
28 | const isOnPage = annotation => {
29 | if (annotation.target.selector) {
30 | const selectors = Array.isArray(annotation.target.selector) ?
31 | annotation.target.selector : [ annotation.target.selector ];
32 |
33 | const selectorWithPage = selectors.find(s => s.page);
34 | return selectorWithPage?.page == pageNumber;
35 | }
36 | };
37 |
38 | const annotationsOnPage = this._annotations.filter(isOnPage);
39 |
40 | // Relations linked to the given annotations
41 | const ids = new Set(annotationsOnPage.map(a => a.id));
42 | const linkedRelations = this._annotations
43 | .filter(a => !a.target.selector) // all relations
44 | .filter(a => {
45 | const from = a.target[0].id;
46 | const to = a.target[1].id;
47 |
48 | return ids.has(from) || ids.has(to);
49 | });
50 |
51 | return [...annotationsOnPage, ...linkedRelations ];
52 | }
53 |
54 | }
--------------------------------------------------------------------------------
/src/pdf/PDFAnnotation.js:
--------------------------------------------------------------------------------
1 | /** Inserts the page number into the annotation target **/
2 | export const extendTarget = (annotation, source, page) => {
3 |
4 | // Not a very robust criterion... but holds for now
5 | const isRelationAnnotation = Array.isArray(annotation.target);
6 |
7 | // Adds 'page' field to selector (unless it's a TextQuoteSelector)
8 | const extendSelector = selector => selector.type === 'TextQuoteSelector' ?
9 | selector : { ...selector, page };
10 |
11 | if (isRelationAnnotation) {
12 | // Nothing to change, just dd source
13 | return {
14 | ...annotation,
15 | target: annotation.target.map(t => ({
16 | id: t.id, source
17 | }))
18 | };
19 | } else {
20 | return Array.isArray(annotation.target.selector) ?
21 | {
22 | ...annotation,
23 | target: {
24 | source,
25 | selector: annotation.target.selector.map(extendSelector)
26 | }
27 | } : {
28 | ...annotation,
29 | target: {
30 | source,
31 | selector: extendSelector(annotation.target.selector)
32 | }
33 | }
34 | }
35 |
36 | }
37 |
38 | /** Splits annotations by type, text or image **/
39 | export const splitByType = annotations => {
40 | let text = [];
41 | let image = [];
42 |
43 | annotations.forEach(a => {
44 | if (a.target.selector) {
45 | const selectors = Array.isArray(a.target.selector) ?
46 | a.target.selector : [ a.target.selector ];
47 |
48 | const hasImageSelector =
49 | selectors.find(s => s.type === 'FragmentSelector' || s.type === 'SvgSelector');
50 |
51 | if (hasImageSelector)
52 | image.push(a);
53 | else
54 | text.push(a);
55 | } else {
56 | // Relationship
57 | text.push(a);
58 | }
59 | });
60 |
61 | return { text, image };
62 | }
--------------------------------------------------------------------------------
/src/pdf/PDFViewer.css:
--------------------------------------------------------------------------------
1 | html, body {
2 | padding:0;
3 | margin:0;
4 | }
5 |
6 | #app main {
7 | background-color:#cbcbcb;
8 | padding:110px 0 40px 0;
9 | }
10 |
11 | header {
12 | position:fixed;
13 | width:100%;
14 | padding:20px;
15 | box-shadow:0 1px 10px rgba(0, 0, 0, 0.3);
16 | background-color:#fff;
17 | z-index:3;
18 | }
19 |
20 | header button {
21 | background:none;
22 | border:none;
23 | outline:none;
24 | font-size:24px;
25 | cursor:pointer;
26 | padding:3px;
27 | border-radius:50%;
28 | width:40px;
29 | height:40px;
30 | display:inline-block;
31 | margin-right:10px;
32 | }
33 |
34 | header button span {
35 | display:flex;
36 | justify-content:center;
37 | align-items:center;
38 | }
39 |
40 | header button:hover {
41 | background:rgba(0, 0, 0, 0.08);
42 | }
43 |
44 | header button.active {
45 | background-color:orange;
46 | color:#fff;
47 | }
48 |
49 | header label {
50 | vertical-align:super;
51 | font-size:18px;
52 | padding:4px 10px;
53 | margin:0 8px;
54 | background-color:rgba(0, 0, 0, 0.08);
55 | border-radius:3px;
56 | font-family: Arial, Helvetica, sans-serif;
57 | }
58 |
59 | .pdf-viewer-container {
60 | display:flex;
61 | justify-content:center;
62 | align-items:center;
63 | flex-direction:column;
64 | }
65 |
66 | .page-container {
67 | position:relative;
68 | margin:10px 0;
69 | min-height:50vh;
70 | }
71 |
72 | .page-container .r6o-content-wrapper {
73 | position:absolute !important;
74 | top:0;
75 | left:0;
76 | right:0;
77 | bottom:0;
78 | }
79 |
80 | .page-container .textLayer {
81 | opacity:1;
82 | z-index:1;
83 | }
84 |
85 | .page-container .a9s-annotationlayer {
86 | z-index:2;
87 | pointer-events:none;
88 | }
89 |
90 | .page-container .r6o-annotation,
91 | .page-container .a9s-annotation {
92 | pointer-events:auto;
93 | }
94 |
95 | .page-container.debug .textLayer span {
96 | color:red;
97 | }
98 |
99 | .page-container .textLayer br {
100 | display:none;
101 | }
102 |
103 | .page-container .textLayer span.r6o-annotation,
104 | .page-container .textLayer span.r6o-selection {
105 | position:relative;
106 | cursor:pointer;
107 | }
108 |
109 | .page-container canvas {
110 | box-shadow:1px 1px 10px rgba(0, 0, 0, 0.1);
111 | }
112 |
113 | .r6o-connections-canvas {
114 | z-index:3;
115 | }
116 |
117 | .r6o-connections-editor {
118 | z-index:4;
119 | }
--------------------------------------------------------------------------------
/src/pdf/PDFViewer.jsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from 'react';
2 | import * as PDFJS from 'pdfjs-dist/legacy/build/pdf';
3 | import Connections from '@recogito/recogito-connections';
4 |
5 | import EndlessViewer from './endless/EndlessViewer';
6 | import PaginatedViewer from './paginated/PaginatedViewer';
7 | import Store from './AnnotationStore';
8 |
9 | import 'pdfjs-dist/web/pdf_viewer.css';
10 | import '@recogito/recogito-js/dist/recogito.min.css';
11 | import '@recogito/annotorious/dist/annotorious.min.css';
12 | import './PDFViewer.css';
13 |
14 | const store = new Store();
15 |
16 | const PDFViewer = props => {
17 |
18 | const [ pdf, setPdf ] = useState();
19 |
20 | const [ connections, setConnections ] = useState();
21 |
22 | // Load PDF on mount
23 | useEffect(() => {
24 | // Init after DOM load
25 | const conn = new Connections([], {
26 | showLabels: true,
27 | vocabulary: props.config.relationVocabulary
28 | });
29 |
30 | setConnections(conn);
31 |
32 | PDFJS.getDocument(props.url).promise
33 | .then(
34 | pdf => setPdf(pdf),
35 | error => console.error(error)
36 | );
37 |
38 | // Destroy connections layer on unmount
39 | return () => conn.destroy();
40 | }, []);
41 |
42 | useEffect(() => {
43 | store.setAnnotations(props.annotations || []);
44 | }, [ props.annotations ])
45 |
46 | const onCreateAnnotation = a => {
47 | store.createAnnotation(a);
48 | props.onCreateAnnotation && props.onCreateAnnotation(a);
49 | }
50 |
51 | const onUpdateAnnotation = (a, p) => {
52 | store.updateAnnotation(a, p);
53 | props.onUpdateAnnotation && props.onUpdateAnnotation(a, p);
54 | }
55 |
56 | const onDeleteAnnotation = a => {
57 | store.deleteAnnotation(a);
58 | props.onDeleteAnnotation && props.onDeleteAnnotation(a);
59 | }
60 |
61 | const onCancelSelected = a => {
62 | props.onCancelSelected && props.onCancelSelected(a);
63 | }
64 |
65 | return pdf ?
66 | props.mode === 'scrolling' ?
67 | :
76 |
77 |
86 |
87 | : null;
88 |
89 | }
90 |
91 | export default PDFViewer;
--------------------------------------------------------------------------------
/src/pdf/endless/AnnotatablePage.jsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useRef, useState } from 'react';
2 | import * as PDFJS from 'pdfjs-dist/legacy/build/pdf';
3 | import { Recogito } from '@recogito/recogito-js/src';
4 | import { Annotorious } from '@recogito/annotorious/src';
5 |
6 | import { extendTarget, splitByType } from '../PDFAnnotation';
7 |
8 | const AnnotatablePage = props => {
9 |
10 | const containerEl = useRef();
11 |
12 | const [ pageVisible, setPageVisible ] = useState(false)
13 | const [ isRendered, setRendered ] = useState(false);
14 |
15 | const [ anno, setAnno ] = useState();
16 |
17 | const [ recogito, setRecogito ] = useState();
18 |
19 | // Renders the PDF page, returning a promise
20 | const renderPage = () => {
21 | console.log('Rendering page ' + props.page);
22 |
23 | props.pdf.getPage(props.page).then(page => {
24 | const scale = props.scale || 1.8;
25 | const viewport = page.getViewport({ scale });
26 |
27 | // Render the image layer to a CANVAS element
28 | const canvas = document.createElement('canvas');
29 | canvas.setAttribute('class', 'imageLayer');
30 | canvas.setAttribute('data-page', props.page);
31 |
32 | canvas.height = viewport.height;
33 | canvas.width = viewport.width;
34 |
35 | containerEl.current.appendChild(canvas);
36 |
37 | const renderContext = {
38 | canvasContext: canvas.getContext('2d'),
39 | viewport
40 | };
41 |
42 | page.render(renderContext).promise.then(() => {
43 | page.getTextContent().then(textContent =>
44 | PDFJS.renderTextLayer({
45 | textContent: textContent,
46 | container: containerEl.current.querySelector('.textLayer'),
47 | viewport: viewport,
48 | textDivs: []
49 | }).promise.then(() => {
50 | setRendered(true);
51 | }));
52 | });
53 | });
54 | }
55 |
56 | const onCreateAnnotation = a => {
57 | const extended = extendTarget(a, props.url, props.page);
58 | props.onCreateAnnotation(extended);
59 | }
60 |
61 | const onUpdateAnnotation = (a, p) => {
62 | const updated = extendTarget(a, props.url, props.page);
63 | const previous = extendTarget(p, props.url, props.page);
64 | props.onUpdateAnnotation(updated, previous);
65 | }
66 |
67 | const onDeleteAnnotation = a => {
68 | const extended = extendTarget(a, props.url, props.page);
69 | props.onDeleteAnnotation(extended)
70 | }
71 |
72 | const setMode = recogito => {
73 | if (pageVisible) {
74 | const imageLayer = containerEl.current.querySelector('svg.a9s-annotationlayer');
75 |
76 | if (props.annotationMode === 'IMAGE') {
77 | if (imageLayer)
78 | imageLayer.style.pointerEvents = 'auto';
79 | } else if (props.annotationMode === 'ANNOTATION') {
80 | if (imageLayer)
81 | imageLayer.style.pointerEvents = null;
82 |
83 | recogito.setMode('ANNOTATION');
84 | } else if (props.annotationMode === 'RELATIONS') {
85 | if (imageLayer)
86 | imageLayer.style.pointerEvents = null;
87 |
88 | recogito.setMode('RELATIONS');
89 | }
90 | }
91 | }
92 |
93 | const initAnnotationLayer = () => {
94 | console.log('Creating annotation layer on page ' + props.page);
95 |
96 | const config = props.config || {};
97 |
98 | const { text, image } = splitByType(props.store.getAnnotations(props.page));
99 |
100 | const r = new Recogito({
101 | ...config,
102 | content: containerEl.current.querySelector('.textLayer'),
103 | mode: 'pre'
104 | });
105 |
106 | // Init Recogito Connections plugin
107 | props.connections.register(r);
108 |
109 | props.connections.on('createConnection', onCreateAnnotation);
110 | props.connections.on('updateConnection', onUpdateAnnotation);
111 | props.connections.on('deleteConnection', onDeleteAnnotation);
112 |
113 | r.on('createAnnotation', onCreateAnnotation);
114 | r.on('updateAnnotation', onUpdateAnnotation);
115 | r.on('deleteAnnotation', onDeleteAnnotation);
116 | r.on('cancelSelected', a => props.onCancelSelected(a));
117 | setRecogito(r);
118 |
119 | const anno = new Annotorious({
120 | ...config,
121 | image: containerEl.current.querySelector('.imageLayer')
122 | });
123 |
124 | anno.on('createAnnotation', onCreateAnnotation);
125 | anno.on('updateAnnotation', onUpdateAnnotation);
126 | anno.on('deleteAnnotation', onDeleteAnnotation);
127 | anno.on('cancelSelected', a => props.onCancelSelected(a));
128 |
129 | setAnno(anno);
130 |
131 | r.on('selectAnnotation', () => anno.selectAnnotation());
132 | anno.on('selectAnnotation', () => r.selectAnnotation());
133 |
134 | // For some reason, React is not done initializing the Image-/TextAnnotators.
135 | // This remains an unsolved mystery for now. The hack is to introduce a little
136 | // wait time until Recogito/Annotorious inits are complete.
137 | const init = () => {
138 | if (r._app.current && anno._app.current) {
139 | r.setAnnotations(text);
140 | anno.setAnnotations(image);
141 | setMode(r);
142 | } else {
143 | setTimeout(() => init(), 50);
144 | }
145 | }
146 |
147 | init();
148 | }
149 |
150 | const destroyAnnotationLayer = () => {
151 | if (recogito || anno)
152 | console.log('Destroying annotation layer on page ' + props.page);
153 |
154 | if (recogito) {
155 | props.connections.unregister(recogito);
156 | recogito.destroy();
157 | }
158 |
159 | if (anno) {
160 | anno.destroy();
161 | }
162 | }
163 |
164 | // Render on page change
165 | useEffect(() => {
166 | const onIntersect = entries => {
167 | const intersecting = entries[0].isIntersecting;
168 | setPageVisible(intersecting);
169 | }
170 |
171 | // Init intersection observer
172 | const observer = new IntersectionObserver(onIntersect, {
173 | rootMargin: '40px'
174 | });
175 |
176 | const target = containerEl.current;
177 | observer.observe(target);
178 |
179 | // First page renders instantly, all others are lazy
180 | if (props.page === 1)
181 | renderPage();
182 |
183 | return () =>
184 | observer.unobserve(target);
185 | }, []);
186 |
187 | useEffect(() => {
188 | if (isRendered) {
189 | if (pageVisible)
190 | initAnnotationLayer();
191 | else
192 | destroyAnnotationLayer();
193 | } else if (pageVisible && props.page > 1) {
194 | renderPage();
195 | }
196 | }, [ isRendered, pageVisible ]);
197 |
198 | useEffect(() => {
199 | setMode(recogito);
200 | }, [ props.annotationMode ])
201 |
202 | return (
203 |
208 | )
209 |
210 | }
211 |
212 | export default AnnotatablePage;
--------------------------------------------------------------------------------
/src/pdf/endless/EndlessViewer.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import { CgDebug, CgArrowsExpandDownRight } from 'react-icons/cg';
3 | import { RiImageEditFill } from 'react-icons/ri';
4 |
5 | import AnnotatablePage from './AnnotatablePage';
6 |
7 | const Range = maxValue =>
8 | Array.from(Array(maxValue).keys());
9 |
10 | const EndlessViewer = props => {
11 |
12 | const [ debug, setDebug ] = useState(false);
13 |
14 | const [ annotationMode, setAnnotationMode ] = useState('ANNOTATION');
15 |
16 | const onToggleRelationsMode = () => {
17 | if (annotationMode === 'RELATIONS')
18 | setAnnotationMode('ANNOTATION');
19 | else
20 | setAnnotationMode('RELATIONS');
21 | }
22 |
23 | const onToggleImageMode = () => {
24 | if (annotationMode === 'IMAGE')
25 | setAnnotationMode('ANNOTATION');
26 | else
27 | setAnnotationMode('IMAGE');
28 | }
29 |
30 | return (
31 |
32 |
33 |
38 |
39 |
46 |
47 |
54 |
55 |
56 |
57 |
58 | {Range(props.pdf.numPages).map(idx =>
59 |
73 | )}
74 |
75 |
76 |
77 | )
78 |
79 | }
80 |
81 | export default EndlessViewer;
--------------------------------------------------------------------------------
/src/pdf/index.js:
--------------------------------------------------------------------------------
1 | export { default as PDFViewer } from './PDFViewer';
--------------------------------------------------------------------------------
/src/pdf/paginated/AnnotatablePage.jsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useRef, useState } from 'react';
2 | import * as PDFJS from 'pdfjs-dist/legacy/build/pdf';
3 | import { Recogito } from '@recogito/recogito-js/src';
4 | import { Annotorious } from '@recogito/annotorious/src';
5 |
6 | import { splitByType } from '../PDFAnnotation';
7 |
8 | const AnnotatablePage = props => {
9 |
10 | const containerEl = useRef();
11 |
12 | const [ anno, setAnno ] = useState();
13 |
14 | const [ recogito, setRecogito ] = useState();
15 |
16 | // Cleanup previous Recogito instance, canvas + text layer
17 | const destroyPreviousPage = () => {
18 | // Clean up previous Recogito + Annotorious instance, if any
19 | if (recogito)
20 | recogito.destroy();
21 |
22 | if (anno)
23 | anno.destroy();
24 |
25 | const canvas = containerEl.current.querySelector('canvas');
26 | if (canvas)
27 | containerEl.current.removeChild(canvas);
28 |
29 | const textLayer = containerEl.current.querySelector('.textLayer');
30 | textLayer.innerHTML = '';
31 | }
32 |
33 | // Render on page change
34 | useEffect(() => {
35 | destroyPreviousPage();
36 |
37 | if (props.page) {
38 | const scale = props.scale || 1.8;
39 | const viewport = props.page.getViewport({ scale });
40 |
41 | const canvas = document.createElement('canvas');
42 | canvas.height = viewport.height;
43 | canvas.width = viewport.width;
44 | containerEl.current.appendChild(canvas);
45 |
46 | const renderContext = {
47 | canvasContext: canvas.getContext('2d'),
48 | viewport
49 | };
50 |
51 | props.page.render(renderContext);
52 |
53 | props.page.getTextContent().then(textContent => PDFJS.renderTextLayer({
54 | textContent: textContent,
55 | container: containerEl.current.querySelector('.textLayer'),
56 | viewport: viewport,
57 | textDivs: []
58 | }).promise.then(() => {
59 | const config = props.config || {};
60 |
61 | const { text, image } = splitByType(props.annotations);
62 |
63 | const r = new Recogito({
64 | ...config,
65 | content: containerEl.current.querySelector('.textLayer'),
66 | mode: 'pre'
67 | });
68 |
69 | r.on('createAnnotation', a => props.onCreateAnnotation(a));
70 | r.on('updateAnnotation', (a, p) => props.onUpdateAnnotation(a, p));
71 | r.on('deleteAnnotation', a => props.onDeleteAnnotation(a));
72 | r.on('cancelSelected', a => props.onCancelSelected(a));
73 |
74 | // TODO split: text annotations only
75 | r.setAnnotations(text);
76 | setRecogito(r);
77 |
78 | const anno = new Annotorious({
79 | ...config,
80 | image: canvas
81 | });
82 |
83 | anno.on('createAnnotation', a => props.onCreateAnnotation(a));
84 | anno.on('updateAnnotation', (a, p) => props.onUpdateAnnotation(a, p));
85 | anno.on('deleteAnnotation', a => props.onDeleteAnnotation(a));
86 | anno.on('cancelSelected', a => props.onCancelSelected(a));
87 |
88 | anno.setAnnotations(image);
89 | setAnno(anno);
90 |
91 | r.on('selectAnnotation', () => anno.selectAnnotation());
92 | anno.on('selectAnnotation', () => r.selectAnnotation());
93 | }));
94 | }
95 | }, [ props.page ]);
96 |
97 | useEffect(() => {
98 | // Hack
99 | if (recogito && recogito.getAnnotations() === 0) {
100 | recogito.setAnnotations(props.annotations);
101 | }
102 | }, [ props.annotations ]);
103 |
104 | useEffect(() => {
105 | if (containerEl.current) {
106 | const imageLayer = containerEl.current.querySelector('svg.a9s-annotationlayer');
107 |
108 | if (imageLayer) {
109 | if (props.annotationMode === 'IMAGE') {
110 | imageLayer.style.pointerEvents = 'auto';
111 | } else {
112 | imageLayer.style.pointerEvents = null;
113 | recogito.setMode(props.annotationMode);
114 | }
115 | }
116 | }
117 | }, [ props.annotationMode ])
118 |
119 | return (
120 |
125 | )
126 |
127 | }
128 |
129 | export default AnnotatablePage;
--------------------------------------------------------------------------------
/src/pdf/paginated/PaginatedViewer.jsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from 'react';
2 | import { CgDebug, CgChevronLeft, CgChevronRight, CgArrowsExpandDownRight } from 'react-icons/cg';
3 | import { RiImageEditFill } from 'react-icons/ri';
4 |
5 | import AnnotatablePage from './AnnotatablePage';
6 | import { extendTarget } from '../PDFAnnotation';
7 |
8 | const PaginatedViewer = props => {
9 |
10 | const [ page, setPage ] = useState();
11 |
12 | const [ debug, setDebug ] = useState(false);
13 |
14 | const [ annotationMode, setAnnotationMode ] = useState('ANNOTATION');
15 |
16 | // Render first page on mount
17 | useEffect(() => {
18 | props.pdf.getPage(1).then(setPage);
19 | }, []);
20 |
21 | const onPreviousPage = () => {
22 | const { pageNumber } = page;
23 | const prevNum = Math.max(0, pageNumber - 1);
24 | if (prevNum !== pageNumber)
25 | props.pdf.getPage(prevNum).then(page => setPage(page));
26 | }
27 |
28 | const onNextPage = () => {
29 | const { numPages } = props.pdf;
30 | const { pageNumber } = page;
31 | const nextNum = Math.min(pageNumber + 1, numPages);
32 | if (nextNum !== pageNumber)
33 | props.pdf.getPage(nextNum).then(page => setPage(page));
34 | }
35 |
36 | const onToggleRelationsMode = () => {
37 | if (annotationMode === 'RELATIONS')
38 | setAnnotationMode('ANNOTATION');
39 | else
40 | setAnnotationMode('RELATIONS');
41 | }
42 |
43 | const onToggleImageMode = () => {
44 | if (annotationMode === 'IMAGE')
45 | setAnnotationMode('ANNOTATION');
46 | else
47 | setAnnotationMode('IMAGE');
48 | }
49 |
50 | const onCreateAnnotation = a => {
51 | const extended = extendTarget(a, props.url, page.pageNumber);
52 | props.onCreateAnnotation && props.onCreateAnnotation(extended);
53 | }
54 |
55 | const onUpdateAnnotation = (a, p) => {
56 | const updated = extendTarget(a, props.url, page.pageNumber);
57 | const previous = extendTarget(p, props.url, page.pageNumber);
58 | props.onUpdateAnnotation && props.onUpdateAnnotation(updated, previous);
59 | }
60 |
61 | const onDeleteAnnotation = a => {
62 | const extended = extendTarget(a, props.url, page.pageNumber);
63 | props.onDeleteAnnotation && props.onDeleteAnnotation(extended);
64 | }
65 |
66 | return (
67 |
68 |
69 |
74 |
75 |
80 |
81 |
82 |
83 |
88 |
89 |
96 |
97 |
104 |
105 |
106 |
107 |
119 |
120 |
121 | )
122 |
123 | }
124 |
125 | export default PaginatedViewer;
--------------------------------------------------------------------------------
/test/index.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from 'react';
2 | import ReactDOM from 'react-dom';
3 | import { pdfjs, PDFViewer } from '../src';
4 |
5 | pdfjs.GlobalWorkerOptions.workerSrc = 'pdf.worker.js';
6 |
7 | const App = () => {
8 |
9 | const [ annotations, setAnnotations ] = useState();
10 |
11 | useEffect(() => {
12 | fetch('sample-annotations.json')
13 | .then(response => response.json())
14 | .then(setAnnotations);
15 | }, []);
16 |
17 | return (
18 | console.log(JSON.stringify(a))}
26 | onUpdateAnnotation={(a, b) => console.log(JSON.stringify(a, b))}
27 | onDeleteAnnotation={a => console.log(JSON.stringify(a))} />
28 | )
29 |
30 | }
31 |
32 | window.onload = function() {
33 |
34 | ReactDOM.render(
35 | ,
36 | document.getElementById('app')
37 | );
38 |
39 | }
40 |
41 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 |
3 | const HtmlWebpackPlugin = require('html-webpack-plugin');
4 |
5 | module.exports = {
6 | entry: {
7 | app: './test/index.js'
8 | },
9 | output: {
10 | filename: '[name].bundle.js',
11 | path: path.resolve(__dirname, 'dist')
12 | },
13 | resolve: {
14 | extensions: ['.js', '.jsx'],
15 | alias: {
16 | 'react': path.resolve('./node_modules/react'),
17 | 'react-dom': path.resolve('./node_modules/react-dom')
18 | }
19 | },
20 | module: {
21 | rules: [
22 | {
23 | test: /\.(js|jsx)$/,
24 | use: {
25 | loader: 'babel-loader',
26 | options: {
27 | "presets": [
28 | "@babel/preset-env",
29 | "@babel/preset-react"
30 | ],
31 | "plugins": [
32 | [
33 | "@babel/plugin-proposal-class-properties"
34 | ]
35 | ]
36 | }
37 | }
38 | },
39 | { test: /\.css$/, use: [ 'style-loader', 'css-loader'] },
40 | { test: /\.scss$/, use: [ 'style-loader', 'css-loader', 'sass-loader' ] },
41 | { test: /\.(png|jpg|gif)$/,
42 | use: [
43 | {
44 | loader: 'file-loader',
45 | options: {
46 | outputPath: 'images/',
47 | name: '[name][hash].[ext]',
48 | },
49 | },
50 | ]
51 | }
52 | ]
53 | },
54 | devtool: 'source-map',
55 | devServer: {
56 | static: './public',
57 | hot: true,
58 | port: 3000
59 | },
60 | plugins: [
61 | new HtmlWebpackPlugin({
62 | inject: 'head',
63 | template: './public/index.html'
64 | })
65 | ]
66 | };
67 |
--------------------------------------------------------------------------------