diff --git a/README.md b/README.md index 5c1cebb..88eb460 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,8 @@ WebViewer comes with a 7-day trial without any feature limitations or trial key Before you begin, make sure your development environment includes [Node.js](https://nodejs.org/en/). +This sample requires Node version 18.0.0 or higher. To check your version, run `node -v` in a terminal/console window. + ## Install ``` diff --git a/package.json b/package.json index d1d2ca3..77b88ba 100644 --- a/package.json +++ b/package.json @@ -2,35 +2,43 @@ "name": "webviewer-react-sample", "version": "1.0.0", "private": true, - "homepage": "https://pdftron.github.io/webviewer-react-sample", "dependencies": { - "react": "^16.6.0", - "react-dom": "^16.6.0", - "react-scripts": "2.1.1", - "@pdftron/webviewer": "^10.0.0" - }, - "devDependencies": { - "btoa": "^1.2.1", - "download": "^7.1.0", - "fs-extra": "^7.0.1", - "gh-pages": "^3.2.2" + "@pdftron/webviewer": "^10.4.0", + "@testing-library/jest-dom": "^5.17.0", + "@testing-library/react": "^13.4.0", + "@testing-library/user-event": "^13.5.0", + "iltorb": "^2.4.5", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-scripts": "^5.0.1", + "web-vitals": "^2.1.4" }, "scripts": { "start": "react-scripts start", "build": "react-scripts build", "test": "react-scripts test", "eject": "react-scripts eject", - "postinstall": "node tools/copy-webviewer-files.js", - "predeploy": "npm run build", - "deploy": "gh-pages -d build" + "postinstall": "node tools/copy-webviewer-files.js" }, "eslintConfig": { - "extends": "react-app" + "extends": [ + "react-app", + "react-app/jest" + ] }, - "browserslist": [ - ">0.2%", - "not dead", - "not ie <= 11", - "not op_mini all" - ] -} \ No newline at end of file + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + }, + "devDependencies": { + "@babel/plugin-proposal-private-property-in-object": "^7.21.11" + } +} diff --git a/public/assets/favicon.ico b/public/assets/favicon.ico deleted file mode 100644 index a11777c..0000000 Binary files a/public/assets/favicon.ico and /dev/null differ diff --git a/public/files/apryse.pdf b/public/files/apryse.pdf new file mode 100644 index 0000000..37707f3 Binary files /dev/null and b/public/files/apryse.pdf differ diff --git a/public/index.html b/public/index.html index 5da44ef..3f8e9f7 100644 --- a/public/index.html +++ b/public/index.html @@ -1,16 +1,18 @@ - - - - - - - - - React Sample - - - -
- - - + + + \ No newline at end of file diff --git a/public/logo192.png b/public/logo192.png new file mode 100644 index 0000000..fc44b0a Binary files /dev/null and b/public/logo192.png differ diff --git a/public/logo512.png b/public/logo512.png new file mode 100644 index 0000000..a4e47a6 Binary files /dev/null and b/public/logo512.png differ diff --git a/public/manifest.json b/public/manifest.json index 1f2f141..080d6c7 100644 --- a/public/manifest.json +++ b/public/manifest.json @@ -6,6 +6,16 @@ "src": "favicon.ico", "sizes": "64x64 32x32 24x24 16x16", "type": "image/x-icon" + }, + { + "src": "logo192.png", + "type": "image/png", + "sizes": "192x192" + }, + { + "src": "logo512.png", + "type": "image/png", + "sizes": "512x512" } ], "start_url": ".", diff --git a/public/serve.json b/public/serve.json deleted file mode 100644 index 11214a2..0000000 --- a/public/serve.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "cleanUrls": false -} \ No newline at end of file diff --git a/src/App.js b/src/App.js index f086938..fe29f9f 100644 --- a/src/App.js +++ b/src/App.js @@ -2,36 +2,153 @@ import React, { useRef, useEffect } from 'react'; import WebViewer from '@pdftron/webviewer'; import './App.css'; +export const MIN_DEVICE_PIXEL_RATIO = 5; + + +export const createCanvas = ({ width, height }) => { + const canvas = document.createElement("canvas"); + + canvas.width = width; + canvas.height = height; + + const dpr = window.devicePixelRatio > MIN_DEVICE_PIXEL_RATIO ? window.devicePixelRatio : MIN_DEVICE_PIXEL_RATIO; + + canvas.width = width * dpr; + canvas.height = height * dpr; + const ctx = canvas.getContext("2d"); + + ctx.scale(dpr, dpr); + + return { canvas, ctx }; +}; + +/* function scalePoints(points, targetHeight, targetWidth, canvas) { +// Find the min and max coordinates +const minX = Math.min(...points.map(point => point.x)); +const minY = Math.min(...points.map(point => point.y)); +const maxX = Math.max(...points.map(point => point.x)); +const maxY = Math.max(...points.map(point => point.y)); + +// Calculate the scale factor to fit the path within the canvas +const scaleX = canvas.width / (maxX - minX); +const scaleY = canvas.height / (maxY - minY); +const scale = Math.min(scaleX, scaleY); + + return points.map((point) => ({ + x: (point.x - minX) * scale, + y: (point.y - minY) * scale, + })); +} */ + +export const prepareCanvasToAppendAnnotationImage = async ({ signatureWidget, instance }) => { + const box = { + width: 500, /* signatureWidget?.Width */ + height: 500, /* signatureWidget?.Height. commented temporarily, as I am yet to figure out how I can scale the ink annotation path points such that they would fit within box dimensions.*/ + }; + + const { canvas, ctx } = createCanvas({ + height: box.height, + width: box.width + }); + + ctx.lineWidth = 2; + ctx.strokeStyle = "#8C95A6"; + ctx.beginPath(); + ctx.rect(0, 0, box.width, box.height); + ctx.stroke(); + + + ctx.textBaseline = "top"; + + return { + stamp: new instance.Core.Annotations.StampAnnotation({ + PageNumber: signatureWidget.PageNumber, + X: signatureWidget?.X, + Y: signatureWidget?.Y - 500, + Width: box.width, + Height: box.height + }), + ctx, + canvas + }; +}; + +// https://support.apryse.com/support/tickets/61784 + const App = () => { const viewer = useRef(null); - // if using a class, equivalent of componentDidMount + const annotationRef = useRef({}); + useEffect(() => { WebViewer( { path: '/webviewer/lib', - initialDoc: '/files/PDFTRON_about.pdf', - licenseKey: 'your_license_key' // sign up to get a free trial key at https://dev.apryse.com + initialDoc: '/files/apryse.pdf', + licenseKey: 'your_license_key', // sign up to get a free trial key at https://dev.apryse.com }, viewer.current, ).then((instance) => { const { documentViewer, annotationManager, Annotations } = instance.Core; - documentViewer.addEventListener('documentLoaded', () => { - const rectangleAnnot = new Annotations.RectangleAnnotation({ - PageNumber: 1, - // values are in page coordinates with (0, 0) in the top left - X: 100, - Y: 150, - Width: 200, - Height: 50, - Author: annotationManager.getCurrentUser() - }); - - annotationManager.addAnnotation(rectangleAnnot); - // need to draw the annotation otherwise it won't show up until the page is refreshed - annotationManager.redrawAnnotation(rectangleAnnot); - }); + const onAnnotationChanged = ( + annotations, + action, + info + ) => { + if (info.imported) { + return; + } + + const [annotation] = annotations; + + if (action === "add") { + annotationRef.current = annotation; + } + + }; + + annotationManager.addEventListener("annotationChanged", onAnnotationChanged); + const signatureTool = documentViewer.getTool("AnnotationCreateSignature"); + + let signatureWidget; + let canvas; + let ctx; + let stamp; + + + const onSignatureWidgetSelected = async (...args) => { + [, signatureWidget] = args; + ({ stamp, ctx, canvas } = await prepareCanvasToAppendAnnotationImage({ + signatureWidget, + instance, + })); + }; + + const onAnnotationAdded = async annotation => { + if (signatureWidget) { + + const path = annotation.getPath(); // + // const path = scalePoints(annotation.getPath(), annotation.Height, annotation?.Width); + console.log({path}); + + ctx.strokeStyle = "#000"; + ctx.lineWidth = 2; + ctx.beginPath(); + ctx.moveTo(path[0].X, path[0].Y); + for (let i = 1; i < path.length; i++) { + ctx.lineTo(path[i].X, path[i].Y); + } + ctx.stroke(); + await stamp.setImageData(canvas.toDataURL()); + await annotationManager.addAnnotation(stamp); + } + }; + + signatureTool.addEventListener("locationSelected", onSignatureWidgetSelected); + signatureTool.addEventListener("annotationAdded", onAnnotationAdded); + + }); }, []); diff --git a/src/App.test.js b/src/App.test.js index a754b20..26c018f 100644 --- a/src/App.test.js +++ b/src/App.test.js @@ -1,9 +1,8 @@ -import React from 'react'; -import ReactDOM from 'react-dom'; +import { render, screen } from '@testing-library/react'; import App from './App'; -it('renders without crashing', () => { - const div = document.createElement('div'); - ReactDOM.render(, div); - ReactDOM.unmountComponentAtNode(div); +test('renders react sample', () => { + render(); + const linkElement = screen.getByText(/react sample/i); + expect(linkElement).toBeInTheDocument(); }); diff --git a/src/index.css b/src/index.css index 1ef84a7..2871619 100644 --- a/src/index.css +++ b/src/index.css @@ -1,14 +1,19 @@ -html, body, #root { +html, +body, +#root { width: 100%; height: 100%; margin: 0; padding: 0; font-family: Arial, Helvetica, sans-serif; + overflow: hidden; } .webviewer { flex: 1; margin: 8px; -webkit-box-shadow: 1px 1px 10px #999; - box-shadow: 1px 1px 10px #999; -} + box-shadow: 1px 1px 10px #999; + box-sizing: border-box; + height: calc(100% - 76px); +} \ No newline at end of file diff --git a/src/index.js b/src/index.js index 395b749..406f756 100644 --- a/src/index.js +++ b/src/index.js @@ -1,6 +1,17 @@ import React from 'react'; -import ReactDOM from 'react-dom'; +import ReactDOM from 'react-dom/client'; import './index.css'; import App from './App'; +import reportWebVitals from './reportWebVitals'; -ReactDOM.render(, document.getElementById('root')); +const root = ReactDOM.createRoot(document.getElementById('root')); +root.render( + + + + ); + +// If you want to start measuring performance in your app, pass a function +// to log results (for example: reportWebVitals(console.log)) +// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals +reportWebVitals(); diff --git a/src/logo.svg b/src/logo.svg new file mode 100644 index 0000000..9dfc1c0 --- /dev/null +++ b/src/logo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/reportWebVitals.js b/src/reportWebVitals.js new file mode 100644 index 0000000..5253d3a --- /dev/null +++ b/src/reportWebVitals.js @@ -0,0 +1,13 @@ +const reportWebVitals = onPerfEntry => { + if (onPerfEntry && onPerfEntry instanceof Function) { + import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { + getCLS(onPerfEntry); + getFID(onPerfEntry); + getFCP(onPerfEntry); + getLCP(onPerfEntry); + getTTFB(onPerfEntry); + }); + } +}; + +export default reportWebVitals; diff --git a/src/setupTests.js b/src/setupTests.js new file mode 100644 index 0000000..8f2609b --- /dev/null +++ b/src/setupTests.js @@ -0,0 +1,5 @@ +// jest-dom adds custom jest matchers for asserting on DOM nodes. +// allows you to do things like: +// expect(element).toHaveTextContent(/react/i) +// learn more: https://github.com/testing-library/jest-dom +import '@testing-library/jest-dom';