diff --git a/package-lock.json b/package-lock.json index 965e01e..366bbd3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -30,6 +30,8 @@ "lodash": "4.17.21", "mui-file-input": "4.0.4", "nostr-tools": "2.7.0", + "pdf-lib": "^1.17.1", + "pdfjs-dist": "^4.4.168", "react": "^18.2.0", "react-dnd": "16.0.1", "react-dnd-html5-backend": "16.0.1", @@ -44,6 +46,7 @@ "@types/crypto-js": "^4.2.2", "@types/file-saver": "2.0.7", "@types/lodash": "4.14.202", + "@types/pdfjs-dist": "^2.10.378", "@types/react": "^18.2.56", "@types/react-dom": "^18.2.19", "@typescript-eslint/eslint-plugin": "^7.0.2", @@ -1258,6 +1261,46 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@mapbox/node-pre-gyp": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz", + "integrity": "sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==", + "optional": true, + "dependencies": { + "detect-libc": "^2.0.0", + "https-proxy-agent": "^5.0.0", + "make-dir": "^3.1.0", + "node-fetch": "^2.6.7", + "nopt": "^5.0.0", + "npmlog": "^5.0.1", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "tar": "^6.1.11" + }, + "bin": { + "node-pre-gyp": "bin/node-pre-gyp" + } + }, + "node_modules/@mapbox/node-pre-gyp/node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "optional": true, + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, "node_modules/@mui/base": { "version": "5.0.0-beta.37", "resolved": "https://registry.npmjs.org/@mui/base/-/base-5.0.0-beta.37.tgz", @@ -1699,6 +1742,24 @@ } } }, + "node_modules/@pdf-lib/standard-fonts": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@pdf-lib/standard-fonts/-/standard-fonts-1.0.0.tgz", + "integrity": "sha512-hU30BK9IUN/su0Mn9VdlVKsWBS6GyhVfqjwl1FjZN4TxP6cCw0jP2w7V3Hf5uX7M0AZJ16vey9yE0ny7Sa59ZA==", + "license": "MIT", + "dependencies": { + "pako": "^1.0.6" + } + }, + "node_modules/@pdf-lib/upng": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@pdf-lib/upng/-/upng-1.0.1.tgz", + "integrity": "sha512-dQK2FUMQtowVP00mtIksrlZhdFXQZPC+taih1q4CvPZ5vqdxR/LKBaFg0oAfzd1GlHZXXSPdQfzQnt+ViGvEIQ==", + "license": "MIT", + "dependencies": { + "pako": "^1.0.10" + } + }, "node_modules/@popperjs/core": { "version": "2.11.8", "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", @@ -2152,6 +2213,16 @@ "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==" }, + "node_modules/@types/pdfjs-dist": { + "version": "2.10.378", + "resolved": "https://registry.npmjs.org/@types/pdfjs-dist/-/pdfjs-dist-2.10.378.tgz", + "integrity": "sha512-TRdIPqdsvKmPla44kVy4jv5Nt5vjMfVjbIEke1CRULIrwKNRC4lIiZvNYDJvbUMNCFPNIUcOKhXTyMJrX18IMA==", + "deprecated": "This is a stub types definition. pdfjs-dist provides its own type definitions, so you do not need this installed.", + "dev": true, + "dependencies": { + "pdfjs-dist": "*" + } + }, "node_modules/@types/prop-types": { "version": "15.7.11", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.11.tgz", @@ -2415,6 +2486,12 @@ "vite": "^4.2.0 || ^5.0.0" } }, + "node_modules/abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "optional": true + }, "node_modules/acorn": { "version": "8.11.3", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", @@ -2445,6 +2522,18 @@ "node": ">=0.4.0" } }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "optional": true, + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, "node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -2465,7 +2554,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, + "devOptional": true, "engines": { "node": ">=8" } @@ -2494,6 +2583,40 @@ "node": ">= 8" } }, + "node_modules/aproba": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", + "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==", + "optional": true + }, + "node_modules/are-we-there-yet": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz", + "integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==", + "deprecated": "This package is no longer supported.", + "optional": true, + "dependencies": { + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/are-we-there-yet/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "optional": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/arg": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", @@ -2548,7 +2671,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true + "devOptional": true }, "node_modules/binary-extensions": { "version": "2.2.0", @@ -2661,6 +2784,21 @@ } ] }, + "node_modules/canvas": { + "version": "2.11.2", + "resolved": "https://registry.npmjs.org/canvas/-/canvas-2.11.2.tgz", + "integrity": "sha512-ItanGBMrmRV7Py2Z+Xhs7cT+FNt5K0vPL4p9EZ/UX/Mu7hFbkxSjKF2KVtPwX7UYWp7dRKnrTvReflgrItJbdw==", + "hasInstallScript": true, + "optional": true, + "dependencies": { + "@mapbox/node-pre-gyp": "^1.0.0", + "nan": "^2.17.0", + "simple-get": "^3.0.3" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/chalk": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", @@ -2710,6 +2848,15 @@ "node": ">= 6" } }, + "node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "optional": true, + "engines": { + "node": ">=10" + } + }, "node_modules/clsx": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.0.tgz", @@ -2731,6 +2878,15 @@ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" }, + "node_modules/color-support": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", + "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", + "optional": true, + "bin": { + "color-support": "bin.js" + } + }, "node_modules/combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -2746,7 +2902,13 @@ "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true + "devOptional": true + }, + "node_modules/console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==", + "optional": true }, "node_modules/convert-source-map": { "version": "2.0.0", @@ -2848,6 +3010,18 @@ } } }, + "node_modules/decompress-response": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-4.2.1.tgz", + "integrity": "sha512-jOSne2qbyE+/r8G1VU+G/82LBs2Fs4LAsTiLSHOCOMZQl2OKZ6i8i4IyHemTe+/yIXOtTcRQMzPcgyhoFlqPkw==", + "optional": true, + "dependencies": { + "mimic-response": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -2862,6 +3036,21 @@ "node": ">=0.4.0" } }, + "node_modules/delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", + "optional": true + }, + "node_modules/detect-libc": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", + "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==", + "optional": true, + "engines": { + "node": ">=8" + } + }, "node_modules/diff": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", @@ -2928,6 +3117,12 @@ "integrity": "sha512-oCglfs8yYKs9RQjJFOHonSnhikPK3y+0SvSYc/YpYJV//6rqc0/hbwd0c7vgK4vrl6y2gJAwjkhkSGWK+z4KRA==", "dev": true }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "optional": true + }, "node_modules/error-ex": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", @@ -3537,11 +3732,41 @@ "node": ">=12.20.0" } }, + "node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "optional": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/fs-minipass/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "optional": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fs-minipass/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "optional": true + }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true + "devOptional": true }, "node_modules/fsevents": { "version": "2.3.3", @@ -3565,6 +3790,27 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/gauge": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz", + "integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==", + "deprecated": "This package is no longer supported.", + "optional": true, + "dependencies": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.2", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.1", + "object-assign": "^4.1.1", + "signal-exit": "^3.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.2" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -3578,7 +3824,7 @@ "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "dev": true, + "devOptional": true, "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -3610,7 +3856,7 @@ "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, + "devOptional": true, "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -3620,7 +3866,7 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, + "devOptional": true, "dependencies": { "brace-expansion": "^1.1.7" }, @@ -3677,6 +3923,12 @@ "node": ">=4" } }, + "node_modules/has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==", + "optional": true + }, "node_modules/hasown": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.1.tgz", @@ -3701,6 +3953,19 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "optional": true, + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/idb": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/idb/-/idb-8.0.0.tgz", @@ -3763,7 +4028,7 @@ "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "dev": true, + "devOptional": true, "dependencies": { "once": "^1.3.0", "wrappy": "1" @@ -3811,6 +4076,15 @@ "node": ">=0.10.0" } }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "optional": true, + "engines": { + "node": ">=8" + } + }, "node_modules/is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", @@ -4021,6 +4295,30 @@ "yallist": "^3.0.2" } }, + "node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "optional": true, + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "optional": true, + "bin": { + "semver": "bin/semver.js" + } + }, "node_modules/make-error": { "version": "1.3.6", "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", @@ -4068,6 +4366,18 @@ "node": ">= 0.6" } }, + "node_modules/mimic-response": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-2.1.0.tgz", + "integrity": "sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA==", + "optional": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/minimatch": { "version": "9.0.3", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", @@ -4083,6 +4393,58 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "optional": true, + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minizlib/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "optional": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minizlib/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "optional": true + }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "optional": true, + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -4109,6 +4471,12 @@ } } }, + "node_modules/nan": { + "version": "2.20.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.20.0.tgz", + "integrity": "sha512-bk3gXBZDGILuuo/6sKtr0DQmSThYHLtNCdSdXk9YkxD/jK6X2vmCyyXBBxyqZ4XcnzTyYEAThfX3DCEnLf6igw==", + "optional": true + }, "node_modules/nanoid": { "version": "3.3.7", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", @@ -4189,6 +4557,21 @@ "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==", "dev": true }, + "node_modules/nopt": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", + "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", + "optional": true, + "dependencies": { + "abbrev": "1" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -4239,6 +4622,19 @@ "integrity": "sha512-78BTryCLcLYv96ONU8Ws3Q1JzjlAt+43pWQhIl86xZmWeegYCNLPml7yQ+gG3vR6V5h4XGj+TxO+SS5dsThQIA==", "optional": true }, + "node_modules/npmlog": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz", + "integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==", + "deprecated": "This package is no longer supported.", + "optional": true, + "dependencies": { + "are-we-there-yet": "^2.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^3.0.0", + "set-blocking": "^2.0.0" + } + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -4251,7 +4647,7 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, + "devOptional": true, "dependencies": { "wrappy": "1" } @@ -4349,7 +4745,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "dev": true, + "devOptional": true, "engines": { "node": ">=0.10.0" } @@ -4376,6 +4772,39 @@ "node": ">=8" } }, + "node_modules/path2d": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/path2d/-/path2d-0.2.0.tgz", + "integrity": "sha512-KdPAykQX6kmLSOO6Jpu2KNcCED7CKjmaBNGGNuctOsG0hgYO1OdYQaan6cYXJiG0WmXOwZZPILPBimu5QAIw3A==", + "optional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/pdf-lib": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/pdf-lib/-/pdf-lib-1.17.1.tgz", + "integrity": "sha512-V/mpyJAoTsN4cnP31vc0wfNA1+p20evqqnap0KLoRUN0Yk/p3wN52DOEsL4oBFcLdb76hlpKPtzJIgo67j/XLw==", + "license": "MIT", + "dependencies": { + "@pdf-lib/standard-fonts": "^1.0.0", + "@pdf-lib/upng": "^1.0.1", + "pako": "^1.0.11", + "tslib": "^1.11.1" + } + }, + "node_modules/pdfjs-dist": { + "version": "4.4.168", + "resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-4.4.168.tgz", + "integrity": "sha512-MbkAjpwka/dMHaCfQ75RY1FXX3IewBVu6NGZOcxerRFlaBiIkZmUoR0jotX5VUzYZEXAGzSFtknWs5xRKliXPA==", + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "canvas": "^2.11.2", + "path2d": "^0.2.0" + } + }, "node_modules/picocolors": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", @@ -4774,7 +5203,7 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "dev": true, + "devOptional": true, "dependencies": { "glob": "^7.1.3" }, @@ -4877,7 +5306,7 @@ "version": "7.6.0", "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", - "dev": true, + "devOptional": true, "dependencies": { "lru-cache": "^6.0.0" }, @@ -4892,7 +5321,7 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, + "devOptional": true, "dependencies": { "yallist": "^4.0.0" }, @@ -4904,7 +5333,13 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true + "devOptional": true + }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "optional": true }, "node_modules/setimmediate": { "version": "1.0.5", @@ -4932,6 +5367,43 @@ "node": ">=8" } }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "optional": true + }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "optional": true + }, + "node_modules/simple-get": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-3.1.1.tgz", + "integrity": "sha512-CQ5LTKGfCpvE1K0n2us+kuMPbk/q0EKl82s4aheV9oXjFEz6W/Y7oQFVJuU6QG77hRT4Ghb5RURteF5vnWjupA==", + "optional": true, + "dependencies": { + "decompress-response": "^4.2.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, "node_modules/slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", @@ -4966,11 +5438,25 @@ "safe-buffer": "~5.1.0" } }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "optional": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, + "devOptional": true, "dependencies": { "ansi-regex": "^5.0.1" }, @@ -5017,6 +5503,29 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/tar": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "optional": true, + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/tar/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "optional": true + }, "node_modules/text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", @@ -5043,6 +5552,12 @@ "node": ">=8.0" } }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "optional": true + }, "node_modules/ts-api-utils": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.2.1.tgz", @@ -5134,6 +5649,12 @@ "resolved": "https://registry.npmjs.org/tseep/-/tseep-1.2.1.tgz", "integrity": "sha512-VFnsNcPGC4qFJ1nxbIPSjTmtRZOhlqLmtwRqtLVos8mbRHki8HO9cy9Z1e89EiWyxFmq6LBviI9TQjijxw/mEw==" }, + "node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "license": "0BSD" + }, "node_modules/tstl": { "version": "2.5.13", "resolved": "https://registry.npmjs.org/tstl/-/tstl-2.5.13.tgz", @@ -5361,6 +5882,12 @@ "node": ">= 8" } }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "optional": true + }, "node_modules/websocket": { "version": "1.0.34", "resolved": "https://registry.npmjs.org/websocket/-/websocket-1.0.34.tgz", @@ -5399,6 +5926,16 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "optional": true, + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -5414,11 +5951,20 @@ "node": ">= 8" } }, + "node_modules/wide-align": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", + "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", + "optional": true, + "dependencies": { + "string-width": "^1.0.2 || 2 || 3 || 4" + } + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true + "devOptional": true }, "node_modules/yaeti": { "version": "0.0.6", diff --git a/package.json b/package.json index f38c7c3..0041c32 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,8 @@ "lodash": "4.17.21", "mui-file-input": "4.0.4", "nostr-tools": "2.7.0", + "pdf-lib": "^1.17.1", + "pdfjs-dist": "^4.4.168", "react": "^18.2.0", "react-dnd": "16.0.1", "react-dnd-html5-backend": "16.0.1", @@ -50,6 +52,7 @@ "@types/crypto-js": "^4.2.2", "@types/file-saver": "2.0.7", "@types/lodash": "4.14.202", + "@types/pdfjs-dist": "^2.10.378", "@types/react": "^18.2.56", "@types/react-dom": "^18.2.19", "@typescript-eslint/eslint-plugin": "^7.0.2", diff --git a/src/components/DrawPDFFields/index.tsx b/src/components/DrawPDFFields/index.tsx new file mode 100644 index 0000000..148e70f --- /dev/null +++ b/src/components/DrawPDFFields/index.tsx @@ -0,0 +1,495 @@ +import { AccessTime, CalendarMonth, ExpandMore, Gesture, PictureAsPdf, Badge, Work, Close } from '@mui/icons-material' +import { Box, Typography, Accordion, AccordionDetails, AccordionSummary, CircularProgress, FormControl, InputLabel, MenuItem, Select } from '@mui/material' +import styles from './style.module.scss' +import { useEffect, useState } from 'react' + +import * as PDFJS from "pdfjs-dist"; +import { ProfileMetadata, User } from '../../types'; +import { PdfFile, DrawTool, MouseState, PdfPage, DrawnField, MarkType } from '../../types/drawing'; +import { truncate } from 'lodash'; +import { hexToNpub } from '../../utils'; +import { toPdfFiles } from '../../utils/pdf.ts' +PDFJS.GlobalWorkerOptions.workerSrc = 'node_modules/pdfjs-dist/build/pdf.worker.mjs'; + +interface Props { + selectedFiles: File[] + users: User[] + metadata: { [key: string]: ProfileMetadata } + onDrawFieldsChange: (pdfFiles: PdfFile[]) => void +} + +export const DrawPDFFields = (props: Props) => { + const { selectedFiles } = props + + const [pdfFiles, setPdfFiles] = useState([]) + const [parsingPdf, setParsingPdf] = useState(false) + const [showDrawToolBox, setShowDrawToolBox] = useState(false) + + const [selectedTool, setSelectedTool] = useState() + const [toolbox] = useState([ + { + identifier: MarkType.SIGNATURE, + icon: , + label: 'Signature', + active: false + + }, + { + identifier: MarkType.FULLNAME, + icon: , + label: 'Full Name', + active: true + }, + { + identifier: MarkType.JOBTITLE, + icon: , + label: 'Job Title', + active: false + }, + { + identifier: MarkType.DATE, + icon: , + label: 'Date', + active: false + }, + { + identifier: MarkType.DATETIME, + icon: , + label: 'Datetime', + active: false + }, + ]) + + const [mouseState, setMouseState] = useState({ + clicked: false + }) + + useEffect(() => { + if (selectedFiles) { + setParsingPdf(true) + + parsePdfPages().finally(() => { + setParsingPdf(false) + }) + } + }, [selectedFiles]) + + useEffect(() => { + if (pdfFiles) props.onDrawFieldsChange(pdfFiles) + }, [pdfFiles]) + + /** + * Drawing events + */ + useEffect(() => { + // window.addEventListener('mousedown', onMouseDown); + window.addEventListener('mouseup', onMouseUp); + + return () => { + // window.removeEventListener('mousedown', onMouseDown); + window.removeEventListener('mouseup', onMouseUp); + } + }, []) + + const refreshPdfFiles = () => { + setPdfFiles([...pdfFiles]) + } + + /** + * Fired only when left click and mouse over pdf page + * Creates new drawnElement and pushes in the array + * It is re rendered and visible right away + * + * @param event Mouse event + * @param page PdfPage where press happened + */ + const onMouseDown = (event: any, page: PdfPage) => { + // Proceed only if left click + if (event.button !== 0) return + + // Only allow drawing if mouse is not over other drawn element + const isOverPdfImageWrapper = event.target.tagName === 'IMG' + + if (!selectedTool || !isOverPdfImageWrapper) { + return + } + + const { mouseX, mouseY } = getMouseCoordinates(event) + + const newField: DrawnField = { + left: mouseX, + top: mouseY, + width: 0, + height: 0, + counterpart: '', + type: selectedTool.identifier + } + + page.drawnFields.push(newField) + + setMouseState((prev) => { + return { + ...prev, + clicked: true + } + }) + } + + /** + * Drawing is finished, resets all the variables used to draw + * @param event Mouse event + */ + const onMouseUp = () => { + setMouseState((prev) => { + return { + ...prev, + clicked: false, + dragging: false, + resizing: false + } + }) + } + + /** + * After {@link onMouseDown} create an drawing element, this function gets called on every pixel moved + * which alters the newly created drawing element, resizing it while mouse move + * @param event Mouse event + * @param page PdfPage where moving is happening + */ + const onMouseMove = (event: any, page: PdfPage) => { + if (mouseState.clicked && selectedTool) { + const lastElementIndex = page.drawnFields.length -1 + const lastDrawnField = page.drawnFields[lastElementIndex] + + const { mouseX, mouseY } = getMouseCoordinates(event) + + const width = mouseX - lastDrawnField.left + const height = mouseY - lastDrawnField.top + + lastDrawnField.width = width + lastDrawnField.height = height + + const currentDrawnFields = page.drawnFields + + currentDrawnFields[lastElementIndex] = lastDrawnField + + refreshPdfFiles() + } + } + + /** + * Fired when event happens on the drawn element which will be moved + * mouse coordinates relative to drawn element will be stored + * so when we start moving, offset can be calculated + * mouseX - offsetX + * mouseY - offsetY + * + * @param event Mouse event + * @param drawnField Which we are moving + */ + const onDrawnFieldMouseDown = (event: any) => { + event.stopPropagation() + + // Proceed only if left click + if (event.button !== 0) return + + const drawingRectangleCoords = getMouseCoordinates(event) + + setMouseState({ + dragging: true, + clicked: false, + coordsInWrapper: { + mouseX: drawingRectangleCoords.mouseX, + mouseY: drawingRectangleCoords.mouseY + } + }) + } + + /** + * Moves the drawnElement by the mouse position (mouse can grab anywhere on the drawn element) + * @param event Mouse event + * @param drawnField which we are moving + */ + const onDranwFieldMouseMove = (event: any, drawnField: DrawnField) => { + if (mouseState.dragging) { + const { mouseX, mouseY, rect } = getMouseCoordinates(event, event.target.parentNode) + const coordsOffset = mouseState.coordsInWrapper + + if (coordsOffset) { + let left = mouseX - coordsOffset.mouseX + let top = mouseY - coordsOffset.mouseY + + const rightLimit = rect.width - drawnField.width - 3 + const bottomLimit = rect.height - drawnField.height - 3 + + if (left < 0) left = 0 + if (top < 0) top = 0 + if (left > rightLimit) left = rightLimit + if (top > bottomLimit) top = bottomLimit + + drawnField.left = left + drawnField.top = top + + refreshPdfFiles() + } + } + } + + /** + * Fired when clicked on the resize handle, sets the state for a resize action + * @param event Mouse event + * @param drawnField which we are resizing + */ + const onResizeHandleMouseDown = (event: any) => { + // Proceed only if left click + if (event.button !== 0) return + + event.stopPropagation() + + setMouseState({ + resizing: true + }) + } + + /** + * Resizes the drawn element by the mouse position + * @param event Mouse event + * @param drawnField which we are resizing + */ + const onResizeHandleMouseMove = (event: any, drawnField: DrawnField) => { + if (mouseState.resizing) { + const { mouseX, mouseY } = getMouseCoordinates(event, event.target.parentNode.parentNode) + + const width = mouseX - drawnField.left + const height = mouseY - drawnField.top + + drawnField.width = width + drawnField.height = height + + refreshPdfFiles() + } + } + + /** + * Removes the drawn element using the indexes in the params + * @param event Mouse event + * @param pdfFileIndex pdf file index + * @param pdfPageIndex pdf page index + * @param drawnFileIndex drawn file index + */ + const onRemoveHandleMouseDown = (event: any, pdfFileIndex: number, pdfPageIndex: number, drawnFileIndex: number) => { + event.stopPropagation() + + pdfFiles[pdfFileIndex].pages[pdfPageIndex].drawnFields.splice(drawnFileIndex, 1) + } + + /** + * Used to stop mouse click propagating to the parent elements + * so select can work properly + * @param event Mouse event + */ + const onUserSelectHandleMouseDown = (event: any) => { + event.stopPropagation() + } + + /** + * Gets the mouse coordinates relative to a element in the `event` param + * @param event MouseEvent + * @param customTarget mouse coordinates relative to this element, if not provided + * event.target will be used + */ + const getMouseCoordinates = (event: any, customTarget?: any) => { + const target = customTarget ? customTarget : event.target + const rect = target.getBoundingClientRect(); + const mouseX = event.clientX - rect.left; //x position within the element. + const mouseY = event.clientY - rect.top; //y position within the element. + + return { + mouseX, + mouseY, + rect + } + } + + /** + * Reads the pdf binary files and converts it's pages to images + * creates the pdfFiles object and sets to a state + */ + const parsePdfPages = async () => { + const pdfFiles: PdfFile[] = await toPdfFiles(selectedFiles); + + setPdfFiles(pdfFiles) + } + + /** + * + * @returns if expanded pdf accordion is present + */ + const hasExpandedPdf = () => { + return !!pdfFiles.filter(pdfFile => !!pdfFile.expanded).length + } + + const handleAccordionExpandChange = (expanded: boolean, pdfFile: PdfFile) => { + pdfFile.expanded = expanded + + refreshPdfFiles() + setShowDrawToolBox(hasExpandedPdf()) + } + + /** + * Changes the drawing tool + * @param drawTool to draw with + */ + const handleToolSelect = (drawTool: DrawTool) => { + // If clicked on the same tool, unselect + if (drawTool.identifier === selectedTool?.identifier) { + setSelectedTool(null) + return + } + + setSelectedTool(drawTool) + } + + /** + * Renders the pdf pages and drawing elements + */ + const getPdfPages = (pdfFile: PdfFile, pdfFileIndex: number) => { + return ( + + {pdfFile.pages.map((page, pdfPageIndex: number) => { + return ( +
{onMouseMove(event, page)}} + onMouseDown={(event) => {onMouseDown(event, page)}} + > + + + {page.drawnFields.map((drawnField, drawnFieldIndex: number) => { + return ( +
{ onDranwFieldMouseMove(event, drawnField)}} + className={styles.drawingRectangle} + style={{ + left: `${drawnField.left}px`, + top: `${drawnField.top}px`, + width: `${drawnField.width}px`, + height: `${drawnField.height}px`, + pointerEvents: mouseState.clicked ? 'none' : 'all' + }} + > + {onResizeHandleMouseMove(event, drawnField)}} + className={styles.resizeHandle} + > + {onRemoveHandleMouseDown(event, pdfFileIndex, pdfPageIndex, drawnFieldIndex)}} + className={styles.removeHandle} + > + + +
+ + Counterpart + + +
+
+ ) + })} +
+ ) + })} +
+ ) + } + + if (parsingPdf) { + return ( + + + + ) + } + + if (!pdfFiles.length) { + return '' + } + + return ( + + + Draw fields on the PDFs: + + {pdfFiles.map((pdfFile, pdfFileIndex: number) => { + return ( + {handleAccordionExpandChange(expanded, pdfFile)}}> + } + aria-controls={`panel${pdfFileIndex}-content`} + id={`panel${pdfFileIndex}header`} + > + + {pdfFile.file.name} + + + {getPdfPages(pdfFile, pdfFileIndex)} + + + ) + })} + + + {showDrawToolBox && ( + + + {toolbox.filter(drawTool => drawTool.active).map((drawTool: DrawTool, index: number) => { + return ( + {handleToolSelect(drawTool)}} className={`${styles.toolItem} ${selectedTool?.identifier === drawTool.identifier ? styles.selected : ''}`}> + { drawTool.icon } + { drawTool.label } + + ) + })} + + + )} + + ) +} diff --git a/src/components/DrawPDFFields/style.module.scss b/src/components/DrawPDFFields/style.module.scss new file mode 100644 index 0000000..e3e7856 --- /dev/null +++ b/src/components/DrawPDFFields/style.module.scss @@ -0,0 +1,113 @@ +.pdfFieldItem { + background: white; + padding: 10px; + border-radius: 4px; + cursor: pointer; +} + +.drawToolBoxContainer { + position: fixed; + bottom: 0; + left: 0; + right: 0; + display: flex; + justify-content: center; + z-index: 50; + + .drawToolBox { + display: flex; + gap: 10px; + min-width: 100px; + background-color: white; + padding: 15px; + box-shadow: 0 0 10px 1px #0000003b; + border-radius: 4px; + + .toolItem { + display: flex; + flex-direction: column; + align-items: center; + border: 1px solid rgba(0, 0, 0, 0.137); + padding: 5px; + cursor: pointer; + user-select: none; + + &.selected { + border-color: #01aaad; + color: #01aaad; + } + + &:not(.selected) { + &:hover { + border-color: #01aaad79; + } + } + + } + } +} + +.pdfImageWrapper { + position: relative; + user-select: none; + + &.drawing { + cursor: crosshair; + } +} + +.drawingRectangle { + position: absolute; + border: 1px solid #01aaad; + z-index: 50; + background-color: #01aaad4b; + cursor: pointer; + display: flex; + justify-content: center; + align-items: center; + + &.nonEditable { + cursor: default; + visibility: hidden; + } + + .resizeHandle { + position: absolute; + right: -5px; + bottom: -5px; + width: 10px; + height: 10px; + background-color: #fff; + border: 1px solid rgb(160, 160, 160); + border-radius: 50%; + cursor: pointer; + } + + .removeHandle { + position: absolute; + display: flex; + justify-content: center; + align-items: center; + top: -30px; + width: 20px; + height: 20px; + background-color: #fff; + border: 1px solid rgb(160, 160, 160); + border-radius: 50%; + color: #E74C3C; + font-size: 10px; + cursor: pointer; + } + + .userSelect { + position: absolute; + display: flex; + justify-content: center; + align-items: center; + bottom: -60px; + min-width: 170px; + min-height: 30px; + background: #fff; + padding: 5px 0; + } +} \ No newline at end of file diff --git a/src/components/PDFView/PdfItem.tsx b/src/components/PDFView/PdfItem.tsx new file mode 100644 index 0000000..eb5ceff --- /dev/null +++ b/src/components/PDFView/PdfItem.tsx @@ -0,0 +1,35 @@ +import { PdfFile } from '../../types/drawing.ts' +import { CurrentUserMark } from '../../types/mark.ts' +import PdfPageItem from './PdfPageItem.tsx'; + +interface PdfItemProps { + pdfFile: PdfFile + currentUserMarks: CurrentUserMark[] + handleMarkClick: (id: number) => void + selectedMarkValue: string + selectedMark: CurrentUserMark | null +} + +/** + * Responsible for displaying pages of a single Pdf File. + */ +const PdfItem = ({ pdfFile, currentUserMarks, handleMarkClick, selectedMarkValue, selectedMark }: PdfItemProps) => { + const filterByPage = (marks: CurrentUserMark[], page: number): CurrentUserMark[] => { + return marks.filter((m) => m.mark.location.page === page); + } + return ( + pdfFile.pages.map((page, i) => { + return ( + + ) + })) +} + +export default PdfItem \ No newline at end of file diff --git a/src/components/PDFView/PdfMarkItem.tsx b/src/components/PDFView/PdfMarkItem.tsx new file mode 100644 index 0000000..7a2b24b --- /dev/null +++ b/src/components/PDFView/PdfMarkItem.tsx @@ -0,0 +1,37 @@ +import { CurrentUserMark } from '../../types/mark.ts' +import styles from '../DrawPDFFields/style.module.scss' +import { inPx } from '../../utils/pdf.ts' + +interface PdfMarkItemProps { + userMark: CurrentUserMark + handleMarkClick: (id: number) => void + selectedMarkValue: string + selectedMark: CurrentUserMark | null +} + +/** + * Responsible for display an individual Pdf Mark. + */ +const PdfMarkItem = ({ selectedMark, handleMarkClick, selectedMarkValue, userMark }: PdfMarkItemProps) => { + const { location } = userMark.mark; + const handleClick = () => handleMarkClick(userMark.mark.id); + const getMarkValue = () => ( + selectedMark?.mark.id === userMark.mark.id + ? selectedMarkValue + : userMark.mark.value + ) + return ( +
{getMarkValue()}
+ ) +} + +export default PdfMarkItem \ No newline at end of file diff --git a/src/components/PDFView/PdfMarking.tsx b/src/components/PDFView/PdfMarking.tsx new file mode 100644 index 0000000..a6dc1a4 --- /dev/null +++ b/src/components/PDFView/PdfMarking.tsx @@ -0,0 +1,100 @@ +import PdfView from './index.tsx' +import MarkFormField from '../../pages/sign/MarkFormField.tsx' +import { PdfFile } from '../../types/drawing.ts' +import { CurrentUserMark, Mark } from '../../types/mark.ts' +import React, { useState, useEffect } from 'react' +import { + findNextCurrentUserMark, + isCurrentUserMarksComplete, + updateCurrentUserMarks, +} from '../../utils' +import { EMPTY } from '../../utils/const.ts' +import { Container } from '../Container' +import styles from '../../pages/sign/style.module.scss' + +interface PdfMarkingProps { + files: { pdfFile: PdfFile, filename: string, hash: string | null }[], + currentUserMarks: CurrentUserMark[], + setIsReadyToSign: (isReadyToSign: boolean) => void, + setCurrentUserMarks: (currentUserMarks: CurrentUserMark[]) => void, + setUpdatedMarks: (markToUpdate: Mark) => void +} + +/** + * Top-level component responsible for displaying Pdfs, Pages, and Marks, + * as well as tracking if the document is ready to be signed. + * @param props + * @constructor + */ +const PdfMarking = (props: PdfMarkingProps) => { + const { + files, + currentUserMarks, + setIsReadyToSign, + setCurrentUserMarks, + setUpdatedMarks + } = props + const [selectedMark, setSelectedMark] = useState(null) + const [selectedMarkValue, setSelectedMarkValue] = useState("") + + useEffect(() => { + setSelectedMark(findNextCurrentUserMark(currentUserMarks) || null) + }, [currentUserMarks]) + + const handleMarkClick = (id: number) => { + const nextMark = currentUserMarks.find((mark) => mark.mark.id === id); + setSelectedMark(nextMark!); + setSelectedMarkValue(nextMark?.mark.value ?? EMPTY); + } + + const handleSubmit = (event: React.FormEvent) => { + event.preventDefault(); + if (!selectedMarkValue || !selectedMark) return; + + const updatedMark: CurrentUserMark = { + ...selectedMark, + mark: { + ...selectedMark.mark, + value: selectedMarkValue + }, + isCompleted: true + } + + setSelectedMarkValue(EMPTY) + const updatedCurrentUserMarks = updateCurrentUserMarks(currentUserMarks, updatedMark); + setCurrentUserMarks(updatedCurrentUserMarks) + setSelectedMark(findNextCurrentUserMark(updatedCurrentUserMarks) || null) + console.log(isCurrentUserMarksComplete(updatedCurrentUserMarks)) + setIsReadyToSign(isCurrentUserMarksComplete(updatedCurrentUserMarks)) + setUpdatedMarks(updatedMark.mark) + } + + const handleChange = (event: React.ChangeEvent) => setSelectedMarkValue(event.target.value) + + return ( + <> + + { + currentUserMarks?.length > 0 && ( + )} + { + selectedMark !== null && ( + + )} + + + ) +} + +export default PdfMarking \ No newline at end of file diff --git a/src/components/PDFView/PdfPageItem.tsx b/src/components/PDFView/PdfPageItem.tsx new file mode 100644 index 0000000..d289a6e --- /dev/null +++ b/src/components/PDFView/PdfPageItem.tsx @@ -0,0 +1,46 @@ +import styles from '../DrawPDFFields/style.module.scss' +import { PdfPage } from '../../types/drawing.ts' +import { CurrentUserMark } from '../../types/mark.ts' +import PdfMarkItem from './PdfMarkItem.tsx' +interface PdfPageProps { + page: PdfPage + currentUserMarks: CurrentUserMark[] + handleMarkClick: (id: number) => void + selectedMarkValue: string + selectedMark: CurrentUserMark | null +} + +/** + * Responsible for rendering a single Pdf Page and its Marks + */ +const PdfPageItem = ({ page, currentUserMarks, handleMarkClick, selectedMarkValue, selectedMark }: PdfPageProps) => { + return ( +
+ + { + currentUserMarks.map((m, i) => ( + + ))} +
+ ) +} + +export default PdfPageItem \ No newline at end of file diff --git a/src/components/PDFView/index.tsx b/src/components/PDFView/index.tsx new file mode 100644 index 0000000..14834a3 --- /dev/null +++ b/src/components/PDFView/index.tsx @@ -0,0 +1,42 @@ +import { PdfFile } from '../../types/drawing.ts' +import { Box } from '@mui/material' +import PdfItem from './PdfItem.tsx' +import { CurrentUserMark } from '../../types/mark.ts' + +interface PdfViewProps { + files: { pdfFile: PdfFile, filename: string, hash: string | null }[] + currentUserMarks: CurrentUserMark[] + handleMarkClick: (id: number) => void + selectedMarkValue: string + selectedMark: CurrentUserMark | null +} + +/** + * Responsible for rendering Pdf files. + */ +const PdfView = ({ files, currentUserMarks, handleMarkClick, selectedMarkValue, selectedMark }: PdfViewProps) => { + const filterByFile = (currentUserMarks: CurrentUserMark[], hash: string): CurrentUserMark[] => { + return currentUserMarks.filter((currentUserMark) => currentUserMark.mark.pdfFileHash === hash) + } + return ( + + { + files.map(({ pdfFile, hash }, i) => { + if (!hash) return + return ( + + ) + }) + } + + ) +} + +export default PdfView; \ No newline at end of file diff --git a/src/components/PDFView/style.module.scss b/src/components/PDFView/style.module.scss new file mode 100644 index 0000000..2e6e519 --- /dev/null +++ b/src/components/PDFView/style.module.scss @@ -0,0 +1,16 @@ +.imageWrapper { + display: flex; + justify-content: center; + align-items: center; + width: 100%; /* Adjust as needed */ + height: 100%; /* Adjust as needed */ + overflow: hidden; /* Ensure no overflow */ + border: 1px solid #ccc; /* Optional: for visual debugging */ + background-color: #e0f7fa; /* Optional: for visual debugging */ +} + +.image { + max-width: 100%; + max-height: 100%; + object-fit: contain; /* Ensure the image fits within the container */ +} \ No newline at end of file diff --git a/src/pages/create/index.tsx b/src/pages/create/index.tsx index b16c6d3..830f9ef 100644 --- a/src/pages/create/index.tsx +++ b/src/pages/create/index.tsx @@ -59,7 +59,11 @@ import { updateUsersAppData, uploadToFileStorage } from '../../utils' +import { Container } from '../../components/Container' import styles from './style.module.scss' +import { PdfFile } from '../../types/drawing' +import { DrawPDFFields } from '../../components/DrawPDFFields' +import { Mark } from '../../types/mark.ts' export const CreatePage = () => { const navigate = useNavigate() @@ -84,6 +88,46 @@ export const CreatePage = () => { const nostrController = NostrController.getInstance() + const [metadata, setMetadata] = useState<{ [key: string]: ProfileMetadata }>( + {} + ) + const [drawnPdfs, setDrawnPdfs] = useState([]) + + useEffect(() => { + users.forEach((user) => { + if (!(user.pubkey in metadata)) { + const metadataController = new MetadataController() + + const handleMetadataEvent = (event: Event) => { + const metadataContent = + metadataController.extractProfileMetadataContent(event) + if (metadataContent) + setMetadata((prev) => ({ + ...prev, + [user.pubkey]: metadataContent + })) + } + + metadataController.on(user.pubkey, (kind: number, event: Event) => { + if (kind === kinds.Metadata) { + handleMetadataEvent(event) + } + }) + + metadataController + .findMetadata(user.pubkey) + .then((metadataEvent) => { + if (metadataEvent) handleMetadataEvent(metadataEvent) + }) + .catch((err) => { + console.error( + `error occurred in finding metadata for: ${user.pubkey}`, + err + ) + }) + } + }) + }, []) // Set up event listener for authentication event nostrController.on('nsecbunker-auth', (url) => { setAuthUrl(url) @@ -297,6 +341,31 @@ export const CreatePage = () => { return fileHashes } + const createMarks = (fileHashes: { [key: string]: string }) : Mark[] => { + return drawnPdfs.flatMap((drawnPdf) => { + const fileHash = fileHashes[drawnPdf.file.name]; + return drawnPdf.pages.flatMap((page, index) => { + return page.drawnFields.map((drawnField) => { + return { + type: drawnField.type, + location: { + page: index, + top: drawnField.top, + left: drawnField.left, + height: drawnField.height, + width: drawnField.width, + }, + npub: drawnField.counterpart, + pdfFileHash: fileHash + } + }) + }) + }) + .map((mark, index) => { + return {...mark, id: index } + }); + } + // Handle errors during zip file generation const handleZipError = (err: any) => { console.log('Error in zip:>> ', err) @@ -309,15 +378,13 @@ export const CreatePage = () => { const generateZipFile = async (zip: JSZip): Promise => { setLoadingSpinnerDesc('Generating zip file') - const arraybuffer = await zip + return await zip .generateAsync({ type: 'arraybuffer', compression: 'DEFLATE', compressionOptions: { level: 6 } }) .catch(handleZipError) - - return arraybuffer } // Encrypt the zip file with the generated encryption key @@ -364,15 +431,13 @@ export const CreatePage = () => { if (!arraybuffer) return null - const finalZipFile = new File( + return new File( [new Blob([arraybuffer])], `${unixNow}.sigit.zip`, { type: 'application/zip' } ) - - return finalZipFile } // Handle errors during file upload @@ -394,14 +459,12 @@ export const CreatePage = () => { type: 'application/sigit' }) - const fileUrl = await uploadToFileStorage(file) + return await uploadToFileStorage(file) .then((url) => { toast.success('files.zip uploaded to file storage') return url }) .catch(handleUploadError) - - return fileUrl } // Manage offline scenarios for signing or viewing the file @@ -425,15 +488,13 @@ export const CreatePage = () => { zip.file(file.name, file) }) - const arraybuffer = await zip + return await zip .generateAsync({ type: 'arraybuffer', compression: 'DEFLATE', compressionOptions: { level: 6 } }) .catch(handleZipError) - - return arraybuffer } const generateCreateSignature = async ( @@ -444,11 +505,13 @@ export const CreatePage = () => { ) => { const signers = users.filter((user) => user.role === UserRole.signer) const viewers = users.filter((user) => user.role === UserRole.viewer) + const markConfig = createMarks(fileHashes) const content: CreateSignatureEventContent = { signers: signers.map((signer) => hexToNpub(signer.pubkey)), viewers: viewers.map((viewer) => hexToNpub(viewer.pubkey)), fileHashes, + markConfig, zipUrl, title } @@ -482,11 +545,9 @@ export const CreatePage = () => { : viewers.map((viewer) => viewer.pubkey) ).filter((receiver) => receiver !== usersPubkey) - const promises = receivers.map((receiver) => + return receivers.map((receiver) => sendNotification(receiver, meta) ) - - return promises } const handleCreate = async () => { @@ -615,6 +676,10 @@ export const CreatePage = () => { } } + const onDrawFieldsChange = (pdfFiles: PdfFile[]) => { + setDrawnPdfs(pdfFiles) + } + if (authUrl) { return (