From 7c7a222d4fac7d119270f3d6b79b75f6d60032ff Mon Sep 17 00:00:00 2001 From: enes Date: Fri, 8 Nov 2024 16:58:27 +0100 Subject: [PATCH 01/11] refactor: use signature pad, smoother signature line BREAKING CHANGE: mark.value type changed --- package-lock.json | 13 +++- package.json | 1 + src/components/MarkInputs/Signature.tsx | 89 ++++++++++--------------- src/components/MarkRender/Signature.tsx | 11 +-- src/components/PDFView/PdfMarking.tsx | 4 +- src/utils/const.ts | 5 ++ src/utils/index.ts | 1 + src/utils/mark.ts | 29 ++++---- src/utils/pdf.ts | 18 +++-- 9 files changed, 85 insertions(+), 86 deletions(-) diff --git a/package-lock.json b/package-lock.json index a156375..71ab84b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -47,6 +47,7 @@ "react-singleton-hook": "^4.0.1", "react-toastify": "10.0.4", "redux": "5.0.1", + "signature_pad": "^5.0.4", "svgo": "^3.3.2", "tseep": "1.2.1" }, @@ -4020,9 +4021,9 @@ "dev": true }, "node_modules/elliptic": { - "version": "6.5.7", - "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.7.tgz", - "integrity": "sha512-ESVCtTwiA+XhY3wyh24QqRGBoP3rEdDUl3EDUUo9tft074fi19IrdpH7hLCMMP3CIj7jb3W96rn8lt/BqIlt5Q==", + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.6.0.tgz", + "integrity": "sha512-dpwoQcLc/2WLQvJvLRHKZ+f9FgOdjnq11rurqwekGQygGPsYSK29OMMD2WalatiqQ+XGFDglTNixpPfI+lpaAA==", "dev": true, "license": "MIT", "dependencies": { @@ -7866,6 +7867,12 @@ "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", "optional": true }, + "node_modules/signature_pad": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/signature_pad/-/signature_pad-5.0.4.tgz", + "integrity": "sha512-nngOixbwLAUOuH3QnZwlgwmynQblxmo4iWacKFwfymJfiY+Qt+9icNtcIe/okqXKun4hJ5QTFmHyC7dmv6lf2w==", + "license": "MIT" + }, "node_modules/simple-concat": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", diff --git a/package.json b/package.json index 3eaa670..02e1946 100644 --- a/package.json +++ b/package.json @@ -57,6 +57,7 @@ "react-singleton-hook": "^4.0.1", "react-toastify": "10.0.4", "redux": "5.0.1", + "signature_pad": "^5.0.4", "svgo": "^3.3.2", "tseep": "1.2.1" }, diff --git a/src/components/MarkInputs/Signature.tsx b/src/components/MarkInputs/Signature.tsx index 106a847..641a3ab 100644 --- a/src/components/MarkInputs/Signature.tsx +++ b/src/components/MarkInputs/Signature.tsx @@ -1,10 +1,13 @@ -import { useRef, useState } from 'react' +import { useEffect, useRef } from 'react' import { MarkInputProps } from '../../types/mark' -import { getOptimizedPaths, optimizeSVG } from '../../utils' +import { getOptimizedPathsWithStrokeWidth } from '../../utils' import { faEraser } from '@fortawesome/free-solid-svg-icons' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import styles from './Signature.module.scss' import { MarkRenderSignature } from '../MarkRender/Signature' +import SignaturePad from 'signature_pad' +import { Config, optimize } from 'svgo' +import { SIGNATURE_PAD_OPTIONS } from '../../utils/const' export const MarkInputSignature = ({ value, @@ -12,64 +15,44 @@ export const MarkInputSignature = ({ userMark }: MarkInputProps) => { const location = userMark?.mark.location - const canvasRef = useRef(null) - const [drawing, setDrawing] = useState(false) - const [paths, setPaths] = useState(value ? JSON.parse(value) : []) + const signaturePad = useRef(null) - function update() { - if (location && paths) { - if (paths.length) { - const optimizedSvg = optimizeSVG(location, paths) - const extractedPaths = getOptimizedPaths(optimizedSvg) - handler(JSON.stringify(extractedPaths)) - } else { - handler('') - } + const update = () => { + if (signaturePad.current && signaturePad.current?.toData()) { + const svg = signaturePad.current.toSVG() + const optimizedSvg = optimize(svg, { + multipass: true, // Optimize multiple times if needed + floatPrecision: 2 // Adjust precision + } as Config).data + const extractedSegments = getOptimizedPathsWithStrokeWidth(optimizedSvg) + handler(JSON.stringify(extractedSegments)) + } else { + handler('') } } - const handlePointerDown = (event: React.PointerEvent) => { - const rect = event.currentTarget.getBoundingClientRect() - const x = event.clientX - rect.left - const y = event.clientY - rect.top + useEffect(() => { + const handleEndStroke = () => { + update() + } + if (location && canvasRef.current && signaturePad.current === null) { + signaturePad.current = new SignaturePad( + canvasRef.current, + SIGNATURE_PAD_OPTIONS + ) + signaturePad.current.addEventListener('endStroke', handleEndStroke) + } - const ctx = canvasRef.current?.getContext('2d') - ctx?.beginPath() - ctx?.moveTo(x, y) - setPaths([...paths, `M ${x} ${y}`]) - setDrawing(true) - } - const handlePointerUp = () => { - setDrawing(false) - update() - const ctx = canvasRef.current?.getContext('2d') - ctx?.clearRect(0, 0, canvasRef.current!.width, canvasRef.current!.height) - } - const handlePointerMove = (event: React.PointerEvent) => { - if (!drawing) return - const ctx = canvasRef.current?.getContext('2d') - const rect = canvasRef.current?.getBoundingClientRect() - const x = event.clientX - rect!.left - const y = event.clientY - rect!.top - - ctx?.lineTo(x, y) - ctx?.stroke() - - // Collect the path data - setPaths((prevPaths) => { - const newPaths = [...prevPaths] - newPaths[newPaths.length - 1] += ` L ${x} ${y}` - return newPaths - }) - } + return () => { + window.removeEventListener('endStroke', handleEndStroke) + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []) const handleReset = () => { - setPaths([]) - setDrawing(false) + signaturePad.current?.clear() update() - const ctx = canvasRef.current?.getContext('2d') - ctx?.clearRect(0, 0, canvasRef.current!.width, canvasRef.current!.height) } return ( @@ -81,10 +64,6 @@ export const MarkInputSignature = ({ width={location?.width} ref={canvasRef} className={styles.canvas} - onPointerDown={handlePointerDown} - onPointerUp={handlePointerUp} - onPointerMove={handlePointerMove} - onPointerOut={handlePointerUp} > {typeof userMark?.mark !== 'undefined' && (
diff --git a/src/components/MarkRender/Signature.tsx b/src/components/MarkRender/Signature.tsx index 6236edc..cee90fd 100644 --- a/src/components/MarkRender/Signature.tsx +++ b/src/components/MarkRender/Signature.tsx @@ -1,12 +1,15 @@ import { MarkRenderProps } from '../../types/mark' export const MarkRenderSignature = ({ value, mark }: MarkRenderProps) => { - const paths = value ? JSON.parse(value) : [] + const segments: string[][] = value ? JSON.parse(value) : [] return ( - - {paths.map((path: string) => ( - + + {segments.map(([path, width]) => ( + ))} ) diff --git a/src/components/PDFView/PdfMarking.tsx b/src/components/PDFView/PdfMarking.tsx index 3de1464..e11ee65 100644 --- a/src/components/PDFView/PdfMarking.tsx +++ b/src/components/PDFView/PdfMarking.tsx @@ -117,7 +117,9 @@ const PdfMarking = (props: PdfMarkingProps) => { // setCurrentUserMarks(updatedCurrentUserMarks) // } - const handleChange = (value: string) => setSelectedMarkValue(value) + const handleChange = (value: string) => { + setSelectedMarkValue(value) + } return ( <> diff --git a/src/utils/const.ts b/src/utils/const.ts index 38f138e..43aae1d 100644 --- a/src/utils/const.ts +++ b/src/utils/const.ts @@ -112,3 +112,8 @@ export const MOST_COMMON_MEDIA_TYPES = new Map([ ['3g2', 'video/3gpp2'], // 3GPP2 audio/video container ['7z', 'application/x-7z-compressed'] // 7-zip archive ]) + +export const SIGNATURE_PAD_OPTIONS = { + minWidth: 0.5, + maxWidth: 3 +} as const diff --git a/src/utils/index.ts b/src/utils/index.ts index accc008..791c39b 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -11,3 +11,4 @@ export * from './string' export * from './url' export * from './utils' export * from './zip' +export * from './const' diff --git a/src/utils/mark.ts b/src/utils/mark.ts index 4eca3a8..a5eb974 100644 --- a/src/utils/mark.ts +++ b/src/utils/mark.ts @@ -1,4 +1,4 @@ -import { CurrentUserMark, Mark, MarkLocation } from '../types/mark.ts' +import { CurrentUserMark, Mark } from '../types/mark.ts' import { hexToNpub } from './nostr.ts' import { Meta, SignedEventContent } from '../types' import { Event } from 'nostr-tools' @@ -24,7 +24,6 @@ import { faStamp, faTableCellsLarge } from '@fortawesome/free-solid-svg-icons' -import { Config, optimize } from 'svgo' /** * Takes in an array of Marks already filtered by User. @@ -266,22 +265,18 @@ export const getToolboxLabelByMarkType = (markType: MarkType) => { return DEFAULT_TOOLBOX.find((t) => t.identifier === markType)?.label } -export const optimizeSVG = (location: MarkLocation, paths: string[]) => { - const svgContent = `${paths.map((path) => ``).join('')}` - const optimizedSVG = optimize(svgContent, { - multipass: true, // Optimize multiple times if needed - floatPrecision: 2 // Adjust precision - } as Config) +export const getOptimizedPathsWithStrokeWidth = (svgString: string) => { + const parser = new DOMParser() + const xmlDoc = parser.parseFromString(svgString, 'image/svg+xml') + const paths = xmlDoc.querySelectorAll('path') + const tuples: string[][] = [] + paths.forEach((path) => { + const d = path.getAttribute('d') ?? '' + const strokeWidth = path.getAttribute('stroke-width') ?? '' + tuples.push([d, strokeWidth]) + }) - return optimizedSVG.data -} - -export const getOptimizedPaths = (svgString: string) => { - const regex = / d="([^"]*)"/g - const matches = [...svgString.matchAll(regex)] - const pathValues = matches.map((match) => match[1]) - - return pathValues + return tuples } export { diff --git a/src/utils/pdf.ts b/src/utils/pdf.ts index b66bc73..41e1d02 100644 --- a/src/utils/pdf.ts +++ b/src/utils/pdf.ts @@ -1,5 +1,5 @@ import { MarkType, PdfPage } from '../types/drawing.ts' -import { PDFDocument, PDFFont, PDFPage, rgb } from 'pdf-lib' +import { LineCapStyle, PDFDocument, PDFFont, PDFPage, rgb } from 'pdf-lib' import { Mark } from '../types/mark.ts' import * as PDFJS from 'pdfjs-dist' import PDFJSWorker from 'pdfjs-dist/build/pdf.worker.min.mjs?worker' @@ -132,7 +132,8 @@ export const addMarks = async ( for (let i = 0; i < pages.length; i++) { if (marksPerPage && Object.hasOwn(marksPerPage, i)) { - marksPerPage[i]?.forEach((mark) => { + for (let j = 0; j < marksPerPage[i].length; j++) { + const mark = marksPerPage[i][j] switch (mark.type) { case MarkType.SIGNATURE: drawSignatureText(mark, pages[i]) @@ -142,7 +143,7 @@ export const addMarks = async ( drawMarkText(mark, pages[i], robotoFont) break } - }) + } } } @@ -263,9 +264,14 @@ const drawSignatureText = (mark: Mark, page: PDFPage) => { const y = height - location.top if (hasValue(mark)) { - const paths = JSON.parse(mark.value!) - paths.forEach((d: string) => { - page.drawSvgPath(d, { x, y }) + const segments: string[][] = JSON.parse(mark.value!) + segments.forEach(([d, w]) => { + page.drawSvgPath(d, { + x, + y, + borderWidth: parseFloat(w), + borderLineCap: LineCapStyle.Round + }) }) } } From 6fd5014302457d1d22fb9f166d3db24bef781824 Mon Sep 17 00:00:00 2001 From: enes Date: Fri, 8 Nov 2024 17:33:55 +0100 Subject: [PATCH 02/11] refactor(signature): fixed pad size, scale to fit mark --- src/components/MarkInputs/Signature.module.scss | 2 +- src/components/MarkInputs/Signature.tsx | 14 ++++++++++---- src/components/MarkRender/Signature.tsx | 8 ++++++-- src/utils/const.ts | 5 +++++ 4 files changed, 22 insertions(+), 7 deletions(-) diff --git a/src/components/MarkInputs/Signature.module.scss b/src/components/MarkInputs/Signature.module.scss index fe8deec..fb49bab 100644 --- a/src/components/MarkInputs/Signature.module.scss +++ b/src/components/MarkInputs/Signature.module.scss @@ -11,10 +11,10 @@ $padding: 5px; .relative { position: relative; + outline: 1px solid black; } .canvas { - outline: 1px solid black; background-color: $body-background-color; cursor: crosshair; diff --git a/src/components/MarkInputs/Signature.tsx b/src/components/MarkInputs/Signature.tsx index 641a3ab..ed0ebff 100644 --- a/src/components/MarkInputs/Signature.tsx +++ b/src/components/MarkInputs/Signature.tsx @@ -7,7 +7,7 @@ import styles from './Signature.module.scss' import { MarkRenderSignature } from '../MarkRender/Signature' import SignaturePad from 'signature_pad' import { Config, optimize } from 'svgo' -import { SIGNATURE_PAD_OPTIONS } from '../../utils/const' +import { SIGNATURE_PAD_OPTIONS, SIGNATURE_PAD_SIZE } from '../../utils/const' export const MarkInputSignature = ({ value, @@ -58,10 +58,16 @@ export const MarkInputSignature = ({ return ( <>
-
+
diff --git a/src/components/MarkRender/Signature.tsx b/src/components/MarkRender/Signature.tsx index cee90fd..1c8279f 100644 --- a/src/components/MarkRender/Signature.tsx +++ b/src/components/MarkRender/Signature.tsx @@ -1,12 +1,16 @@ import { MarkRenderProps } from '../../types/mark' +import { SIGNATURE_PAD_SIZE } from '../../utils' +import styles from './Signature.module.scss' -export const MarkRenderSignature = ({ value, mark }: MarkRenderProps) => { +export const MarkRenderSignature = ({ value }: MarkRenderProps) => { const segments: string[][] = value ? JSON.parse(value) : [] return ( {segments.map(([path, width]) => ( diff --git a/src/utils/const.ts b/src/utils/const.ts index 43aae1d..bf38404 100644 --- a/src/utils/const.ts +++ b/src/utils/const.ts @@ -117,3 +117,8 @@ export const SIGNATURE_PAD_OPTIONS = { minWidth: 0.5, maxWidth: 3 } as const + +export const SIGNATURE_PAD_SIZE = { + width: 600, + height: 300 +} From 7fa9a008fa8642367b7b7415b738ff918ca2e9bb Mon Sep 17 00:00:00 2001 From: enes Date: Fri, 8 Nov 2024 17:34:22 +0100 Subject: [PATCH 03/11] refactor(signature): stretch svg render --- src/components/MarkRender/Signature.module.scss | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 src/components/MarkRender/Signature.module.scss diff --git a/src/components/MarkRender/Signature.module.scss b/src/components/MarkRender/Signature.module.scss new file mode 100644 index 0000000..74be1a1 --- /dev/null +++ b/src/components/MarkRender/Signature.module.scss @@ -0,0 +1,4 @@ +.svg { + width: 100%; + height: 100%; +} From 3f081c1632378955a9b2821297c0354c7a197aed Mon Sep 17 00:00:00 2001 From: enes Date: Mon, 11 Nov 2024 16:21:22 +0100 Subject: [PATCH 04/11] refactor(signature): use reduced point group data --- src/components/MarkFormField/index.tsx | 3 +- src/components/MarkInputs/Signature.tsx | 99 ++++++++++--------- .../MarkRender/Signature.module.scss | 7 +- src/components/MarkRender/Signature.tsx | 35 ++++--- 4 files changed, 84 insertions(+), 60 deletions(-) diff --git a/src/components/MarkFormField/index.tsx b/src/components/MarkFormField/index.tsx index 7328065..fef3f17 100644 --- a/src/components/MarkFormField/index.tsx +++ b/src/components/MarkFormField/index.tsx @@ -86,6 +86,7 @@ const MarkFormField = ({
handleFormSubmit(e)}> {typeof MarkInputComponent !== 'undefined' && (
-
- - {typeof userMark?.mark !== 'undefined' && ( -
- -
- )} -
- +
+
+ + {typeof userMark?.mark !== 'undefined' && ( +
+
+ )} +
+
- +
) } diff --git a/src/components/MarkRender/Signature.module.scss b/src/components/MarkRender/Signature.module.scss index 74be1a1..d09d6d2 100644 --- a/src/components/MarkRender/Signature.module.scss +++ b/src/components/MarkRender/Signature.module.scss @@ -1,4 +1,9 @@ -.svg { +.img { width: 100%; height: 100%; + object-fit: contain; + overflow: hidden; + pointer-events: none; + -webkit-user-select: none; + user-select: none; } diff --git a/src/components/MarkRender/Signature.tsx b/src/components/MarkRender/Signature.tsx index 1c8279f..1f90c9b 100644 --- a/src/components/MarkRender/Signature.tsx +++ b/src/components/MarkRender/Signature.tsx @@ -1,20 +1,27 @@ +import { useEffect, useState } from 'react' +import SignaturePad from 'signature_pad' import { MarkRenderProps } from '../../types/mark' -import { SIGNATURE_PAD_SIZE } from '../../utils' +import { SIGNATURE_PAD_OPTIONS, SIGNATURE_PAD_SIZE } from '../../utils' +import { BasicPoint } from 'signature_pad/dist/types/point' import styles from './Signature.module.scss' export const MarkRenderSignature = ({ value }: MarkRenderProps) => { - const segments: string[][] = value ? JSON.parse(value) : [] + const [dataUrl, setDataUrl] = useState() - return ( - - {segments.map(([path, width]) => ( - - ))} - - ) + useEffect(() => { + if (value) { + const canvas = document.createElement('canvas') + canvas.width = SIGNATURE_PAD_SIZE.width + canvas.height = SIGNATURE_PAD_SIZE.height + const pad = new SignaturePad(canvas, SIGNATURE_PAD_OPTIONS) + pad.fromData( + JSON.parse(value).map((p: BasicPoint[]) => ({ + points: p + })) + ) + setDataUrl(canvas.toDataURL('image/webp')) + } + }, [value]) + + return dataUrl ? : null } From 9551750cbe0d84abc983e8746dcf67aedf99c525 Mon Sep 17 00:00:00 2001 From: enes Date: Fri, 15 Nov 2024 17:51:11 +0100 Subject: [PATCH 05/11] feat(signature): signature pad encrypt, upload, fetch, decrypt, render, add to pdf --- package-lock.json | 20 ++++- package.json | 1 - src/components/PDFView/PdfPageItem.tsx | 6 +- src/components/getMarkComponents.tsx | 78 ++++++++++++++++- src/hooks/useSigitMeta.tsx | 34 +++++++- src/pages/sign/index.tsx | 67 ++++++++++++++- src/pages/verify/index.tsx | 111 ++++++++++++------------- src/types/mark.ts | 4 +- src/utils/file.ts | 44 +++++++++- src/utils/mark.ts | 21 +++++ src/utils/pdf.ts | 51 ++++++++---- 11 files changed, 352 insertions(+), 85 deletions(-) diff --git a/package-lock.json b/package-lock.json index 71ab84b..a03e759 100644 --- a/package-lock.json +++ b/package-lock.json @@ -48,7 +48,6 @@ "react-toastify": "10.0.4", "redux": "5.0.1", "signature_pad": "^5.0.4", - "svgo": "^3.3.2", "tseep": "1.2.1" }, "devDependencies": { @@ -2215,6 +2214,7 @@ "version": "0.2.0", "resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz", "integrity": "sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==", + "dev": true, "license": "ISC", "engines": { "node": ">=10.13.0" @@ -2936,6 +2936,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "dev": true, "license": "ISC" }, "node_modules/brace-expansion": { @@ -3635,6 +3636,7 @@ "version": "5.1.0", "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz", "integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==", + "dev": true, "license": "BSD-2-Clause", "dependencies": { "boolbase": "^1.0.0", @@ -3651,6 +3653,7 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.3.1.tgz", "integrity": "sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==", + "dev": true, "license": "MIT", "dependencies": { "mdn-data": "2.0.30", @@ -3664,6 +3667,7 @@ "version": "6.1.0", "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", + "dev": true, "license": "BSD-2-Clause", "engines": { "node": ">= 6" @@ -3676,6 +3680,7 @@ "version": "5.0.5", "resolved": "https://registry.npmjs.org/csso/-/csso-5.0.5.tgz", "integrity": "sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==", + "dev": true, "license": "MIT", "dependencies": { "css-tree": "~2.2.0" @@ -3689,6 +3694,7 @@ "version": "2.2.1", "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.2.1.tgz", "integrity": "sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA==", + "dev": true, "license": "MIT", "dependencies": { "mdn-data": "2.0.28", @@ -3703,6 +3709,7 @@ "version": "2.0.28", "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.28.tgz", "integrity": "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==", + "dev": true, "license": "CC0-1.0" }, "node_modules/csstype": { @@ -3950,6 +3957,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "dev": true, "license": "MIT", "dependencies": { "domelementtype": "^2.3.0", @@ -3977,6 +3985,7 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "dev": true, "funding": [ { "type": "github", @@ -3989,6 +3998,7 @@ "version": "5.0.3", "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "dev": true, "license": "BSD-2-Clause", "dependencies": { "domelementtype": "^2.3.0" @@ -4004,6 +4014,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz", "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==", + "dev": true, "license": "BSD-2-Clause", "dependencies": { "dom-serializer": "^2.0.0", @@ -4053,6 +4064,7 @@ "version": "4.5.0", "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, "license": "BSD-2-Clause", "engines": { "node": ">=0.12" @@ -5980,6 +5992,7 @@ "version": "2.0.30", "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz", "integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==", + "dev": true, "license": "CC0-1.0" }, "node_modules/merge-stream": { @@ -6561,6 +6574,7 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dev": true, "license": "BSD-2-Clause", "dependencies": { "boolbase": "^1.0.0" @@ -6920,6 +6934,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz", "integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==", + "dev": true, "license": "ISC" }, "node_modules/picomatch": { @@ -7978,6 +7993,7 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" @@ -8204,6 +8220,7 @@ "version": "3.3.2", "resolved": "https://registry.npmjs.org/svgo/-/svgo-3.3.2.tgz", "integrity": "sha512-OoohrmuUlBs8B8o6MB2Aevn+pRIH9zDALSR+6hhqVfa6fRwG/Qw9VUMSMW9VNg2CFc/MTIfabtdOVl9ODIJjpw==", + "dev": true, "license": "MIT", "dependencies": { "@trysound/sax": "0.2.0", @@ -8229,6 +8246,7 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "dev": true, "license": "MIT", "engines": { "node": ">= 10" diff --git a/package.json b/package.json index 02e1946..ab49da0 100644 --- a/package.json +++ b/package.json @@ -58,7 +58,6 @@ "react-toastify": "10.0.4", "redux": "5.0.1", "signature_pad": "^5.0.4", - "svgo": "^3.3.2", "tseep": "1.2.1" }, "devDependencies": { diff --git a/src/components/PDFView/PdfPageItem.tsx b/src/components/PDFView/PdfPageItem.tsx index 5d4be7d..9f56550 100644 --- a/src/components/PDFView/PdfPageItem.tsx +++ b/src/components/PDFView/PdfPageItem.tsx @@ -6,6 +6,7 @@ import { useEffect, useRef } from 'react' import pdfViewStyles from './style.module.scss' import { FONT_SIZE, FONT_TYPE, inPx } from '../../utils/pdf.ts' import { useScale } from '../../hooks/useScale.tsx' +import { MARK_TYPE_CONFIG } from '../getMarkComponents.tsx' interface PdfPageProps { fileName: string pageIndex: number @@ -60,6 +61,7 @@ const PdfPageItem = ({ /> ))} {otherUserMarks.map((m, i) => { + const { render: MarkRenderComponent } = MARK_TYPE_CONFIG[m.type] || {} return (
- {m.value} + {typeof MarkRenderComponent !== 'undefined' && ( + + )}
) })} diff --git a/src/components/getMarkComponents.tsx b/src/components/getMarkComponents.tsx index 507f388..e81bdd4 100644 --- a/src/components/getMarkComponents.tsx +++ b/src/components/getMarkComponents.tsx @@ -1,8 +1,17 @@ +import { toast } from 'react-toastify' import { MarkType } from '../types/drawing' import { MarkConfigs } from '../types/mark' +import { + decryptArrayBuffer, + encryptArrayBuffer, + getHash, + isOnline, + uploadToFileStorage +} from '../utils' import { MarkInputSignature } from './MarkInputs/Signature' import { MarkInputText } from './MarkInputs/Text' import { MarkRenderSignature } from './MarkRender/Signature' +import axios from 'axios' export const MARK_TYPE_CONFIG: MarkConfigs = { [MarkType.TEXT]: { @@ -11,6 +20,73 @@ export const MARK_TYPE_CONFIG: MarkConfigs = { }, [MarkType.SIGNATURE]: { input: MarkInputSignature, - render: MarkRenderSignature + render: MarkRenderSignature, + encryptAndUpload: async (value, encryptionKey) => { + // Value is the stringified signature object + // Encode it as text to the arrayBuffer + const encoder = new TextEncoder() + const uint8Array = encoder.encode(value) + const hash = await getHash(uint8Array) + + if (!hash) { + throw new Error("Can't get file hash.") + } + + if (!encryptionKey) { + throw new Error('Signature requires an encryption key') + } + + // Encrypt the file contents with the same encryption key from the create signature + const encryptedArrayBuffer = await encryptArrayBuffer( + uint8Array, + encryptionKey + ) + + // Create the encrypted json file from array buffer and hash + const file = new File([encryptedArrayBuffer], `${hash}.json`) + + if (await isOnline()) { + try { + const url = await uploadToFileStorage(file) + toast.success('files.zip uploaded to file storage') + return url + } catch (error) { + if (error instanceof Error) { + toast.error(error.message || 'Error occurred in uploading file') + } + } + } else { + // Handle offline? + } + + return value + }, + fetchAndDecrypt: async (value, encryptionKey) => { + if (!encryptionKey) { + throw new Error('Signature requires an encryption key') + } + + const encryptedArrayBuffer = await axios.get(value, { + responseType: 'arraybuffer' + }) + + const arrayBuffer = await decryptArrayBuffer( + encryptedArrayBuffer.data, + encryptionKey + ).catch((err) => { + console.log('err in decryption:>> ', err) + return null + }) + + if (arrayBuffer) { + // decode json + const decoder = new TextDecoder() + const value = decoder.decode(arrayBuffer) + return value + } + + // Handle offline? + return value + } } } diff --git a/src/hooks/useSigitMeta.tsx b/src/hooks/useSigitMeta.tsx index 8f79dc6..d37ae1e 100644 --- a/src/hooks/useSigitMeta.tsx +++ b/src/hooks/useSigitMeta.tsx @@ -21,6 +21,7 @@ import { Event } from 'nostr-tools' import store from '../store/store' import { NostrController } from '../controllers' import { MetaParseError } from '../types/errors/MetaParseError' +import { MARK_TYPE_CONFIG } from '../components/getMarkComponents' /** * Flattened interface that combines properties `Meta`, `CreateSignatureEventContent`, @@ -142,6 +143,7 @@ export const useSigitMeta = (meta: Meta): FlatMeta => { setMarkConfig(markConfig) setZipUrl(zipUrl) + let encryptionKey: string | null = null if (meta.keys) { const { sender, keys } = meta.keys // Retrieve the user's public key from the state @@ -162,6 +164,7 @@ export const useSigitMeta = (meta: Meta): FlatMeta => { return null }) + encryptionKey = decrypted setEncryptionKey(decrypted) } } @@ -206,13 +209,40 @@ export const useSigitMeta = (meta: Meta): FlatMeta => { } } - parsedSignatureEventsMap.forEach((event, npub) => { + for (const [npub, event] of parsedSignatureEventsMap) { const isValidSignature = verifyEvent(event) if (isValidSignature) { // get the signature of prev signer from the content of current signers signedEvent const prevSignersSig = getPrevSignerSig(npub) + try { const obj: SignedEventContent = JSON.parse(event.content) + + // Signature object can include values that need to be fetched and decrypted + for (let i = 0; i < obj.marks.length; i++) { + const m = obj.marks[i] + + try { + const { fetchAndDecrypt } = MARK_TYPE_CONFIG[m.type] || {} + if ( + typeof fetchAndDecrypt === 'function' && + m.value && + encryptionKey + ) { + const decrypted = await fetchAndDecrypt( + m.value, + encryptionKey + ) + obj.marks[i].value = decrypted + } + } catch (error) { + console.error( + `Error during mark fetchAndDecrypt phase`, + error + ) + } + } + parsedSignatureEventsMap.set(npub, { ...event, parsedContent: obj @@ -228,7 +258,7 @@ export const useSigitMeta = (meta: Meta): FlatMeta => { signerStatusMap.set(npub as `npub1${string}`, SignStatus.Invalid) } } - }) + } signers .filter((s) => !parsedSignatureEventsMap.has(s)) diff --git a/src/pages/sign/index.tsx b/src/pages/sign/index.tsx index 0a022eb..847ce2a 100644 --- a/src/pages/sign/index.tsx +++ b/src/pages/sign/index.tsx @@ -33,7 +33,8 @@ import { signEventForMetaFile, updateUsersAppData, findOtherUserMarks, - timeout + timeout, + processMarks } from '../../utils' import { Container } from '../../components/Container' import { DisplayMeta } from './internal/displayMeta' @@ -54,6 +55,7 @@ import { } from '../../utils/file.ts' import { ARRAY_BUFFER, DEFLATE } from '../../utils/const.ts' import { generateTimestamp } from '../../utils/opentimestamps.ts' +import { MARK_TYPE_CONFIG } from '../../components/getMarkComponents.tsx' enum SignedStatus { Fully_Signed, @@ -237,6 +239,43 @@ export const SignPage = () => { const signedMarks = extractMarksFromSignedMeta(meta) const currentUserMarks = getCurrentUserMarks(metaMarks, signedMarks) const otherUserMarks = findOtherUserMarks(signedMarks, usersPubkey!) + + if (meta.keys) { + for (let i = 0; i < otherUserMarks.length; i++) { + const m = otherUserMarks[i] + const { sender, keys } = meta.keys + const usersNpub = hexToNpub(usersPubkey) + if (usersNpub in keys) { + const encryptionKey = await nostrController + .nip04Decrypt(sender, keys[usersNpub]) + .catch((err) => { + console.log( + 'An error occurred in decrypting encryption key', + err + ) + return null + }) + + try { + const { fetchAndDecrypt } = MARK_TYPE_CONFIG[m.type] || {} + if ( + typeof fetchAndDecrypt === 'function' && + m.value && + encryptionKey + ) { + const decrypted = await fetchAndDecrypt( + m.value, + encryptionKey + ) + otherUserMarks[i].value = decrypted + } + } catch (error) { + console.error(`Error during mark fetchAndDecrypt phase`, error) + } + } + } + } + setOtherUserMarks(otherUserMarks) setCurrentUserMarks(currentUserMarks) setIsMarksCompleted(isCurrentUserMarksComplete(currentUserMarks)) @@ -248,6 +287,7 @@ export const SignPage = () => { if (meta) { handleUpdatedMeta(meta) } + // eslint-disable-next-line react-hooks/exhaustive-deps }, [meta, usersPubkey]) const handleDownload = async () => { @@ -552,8 +592,8 @@ export const SignPage = () => { setIsLoading(true) setLoadingSpinnerDesc('Signing nostr event') - - const prevSig = getPrevSignersSig(hexToNpub(usersPubkey!)) + const usersNpub = hexToNpub(usersPubkey!) + const prevSig = getPrevSignersSig(usersNpub) if (!prevSig) { setIsLoading(false) toast.error('Previous signature is invalid') @@ -562,7 +602,26 @@ export const SignPage = () => { const marks = getSignerMarksForMeta() || [] - const signedEvent = await signEventForMeta({ prevSig, marks }) + let encryptionKey: string | undefined + if (meta.keys) { + const { sender, keys } = meta.keys + encryptionKey = await nostrController + .nip04Decrypt(sender, keys[usersNpub]) + .catch((err) => { + // Log and display an error message if decryption fails + console.log('An error occurred in decrypting encryption key', err) + toast.error('An error occurred in decrypting encryption key') + return undefined + }) + } + + const processedMarks = await processMarks(marks, encryptionKey) + + const signedEvent = await signEventForMeta({ + prevSig, + marks: processedMarks + }) + if (!signedEvent) return const updatedMeta = updateMetaSignatures(meta, signedEvent) diff --git a/src/pages/verify/index.tsx b/src/pages/verify/index.tsx index 6ce4c83..7eafc78 100644 --- a/src/pages/verify/index.tsx +++ b/src/pages/verify/index.tsx @@ -367,82 +367,77 @@ export const VerifyPage = () => { setIsLoading(true) setLoadingSpinnerDesc('Fetching file from file server') - axios - .get(zipUrl, { + try { + const res = await axios.get(zipUrl, { responseType: 'arraybuffer' }) - .then(async (res) => { - const fileName = zipUrl.split('/').pop() - const file = new File([res.data], fileName!) - const encryptedArrayBuffer = await file.arrayBuffer() - const arrayBuffer = await decryptArrayBuffer( - encryptedArrayBuffer, - encryptionKey - ).catch((err) => { - console.log('err in decryption:>> ', err) + const fileName = zipUrl.split('/').pop() + const file = new File([res.data], fileName!) + + const encryptedArrayBuffer = await file.arrayBuffer() + const arrayBuffer = await decryptArrayBuffer( + encryptedArrayBuffer, + encryptionKey + ).catch((err) => { + console.log('err in decryption:>> ', err) + toast.error(err.message || 'An error occurred in decrypting file.') + return null + }) + + if (arrayBuffer) { + const zip = await JSZip.loadAsync(arrayBuffer).catch((err) => { + console.log('err in loading zip file :>> ', err) toast.error( - err.message || 'An error occurred in decrypting file.' + err.message || 'An error occurred in loading zip file.' ) return null }) - if (arrayBuffer) { - const zip = await JSZip.loadAsync(arrayBuffer).catch((err) => { - console.log('err in loading zip file :>> ', err) - toast.error( - err.message || 'An error occurred in loading zip file.' - ) - return null - }) + if (!zip) return - if (!zip) return + const files: { [fileName: string]: SigitFile } = {} + const fileHashes: { [key: string]: string | null } = {} + const fileNames = Object.values(zip.files).map( + (entry) => entry.name + ) - const files: { [fileName: string]: SigitFile } = {} - const fileHashes: { [key: string]: string | null } = {} - const fileNames = Object.values(zip.files).map( - (entry) => entry.name + // generate hashes for all entries in files folder of zipArchive + // these hashes can be used to verify the originality of files + for (const fileName of fileNames) { + const arrayBuffer = await readContentOfZipEntry( + zip, + fileName, + 'arraybuffer' ) - // generate hashes for all entries in files folder of zipArchive - // these hashes can be used to verify the originality of files - for (const fileName of fileNames) { - const arrayBuffer = await readContentOfZipEntry( - zip, - fileName, - 'arraybuffer' + if (arrayBuffer) { + files[fileName] = await convertToSigitFile( + arrayBuffer, + fileName! ) + const hash = await getHash(arrayBuffer) - if (arrayBuffer) { - files[fileName] = await convertToSigitFile( - arrayBuffer, - fileName! - ) - const hash = await getHash(arrayBuffer) - - if (hash) { - fileHashes[fileName.replace(/^files\//, '')] = hash - } - } else { - fileHashes[fileName.replace(/^files\//, '')] = null + if (hash) { + fileHashes[fileName.replace(/^files\//, '')] = hash } + } else { + fileHashes[fileName.replace(/^files\//, '')] = null } - - setCurrentFileHashes(fileHashes) - setFiles(files) - - setIsLoading(false) } - }) - .catch((err) => { - console.error(`error occurred in getting file from ${zipUrl}`, err) - toast.error( - err.message || `error occurred in getting file from ${zipUrl}` - ) - }) - .finally(() => { + + setCurrentFileHashes(fileHashes) + setFiles(files) setIsLoading(false) - }) + } + } catch (err) { + const message = `error occurred in getting file from ${zipUrl}` + console.error(message, err) + if (err instanceof Error) toast.error(err.message) + else toast.error(message) + } finally { + setIsLoading(false) + } } processSigit() diff --git a/src/types/mark.ts b/src/types/mark.ts index ec1c162..0a54d3e 100644 --- a/src/types/mark.ts +++ b/src/types/mark.ts @@ -43,7 +43,9 @@ export interface MarkRenderProps { export interface MarkConfig { input: React.FC - render?: React.FC + render: React.FC + encryptAndUpload?: (value: string, key?: string) => Promise + fetchAndDecrypt?: (value: string, key?: string) => Promise } export type MarkConfigs = { diff --git a/src/utils/file.ts b/src/utils/file.ts index c08d5e7..38dd0f1 100644 --- a/src/utils/file.ts +++ b/src/utils/file.ts @@ -1,7 +1,11 @@ +import { MARK_TYPE_CONFIG } from '../components/getMarkComponents.tsx' +import { NostrController } from '../controllers/NostrController.ts' +import store from '../store/store.ts' import { Meta } from '../types' import { PdfPage } from '../types/drawing.ts' import { MOST_COMMON_MEDIA_TYPES } from './const.ts' import { extractMarksFromSignedMeta } from './mark.ts' +import { hexToNpub } from './nostr.ts' import { addMarks, groupMarksByFileNamePage, @@ -21,7 +25,45 @@ export const getZipWithFiles = async ( for (const [fileName, file] of Object.entries(files)) { // Handle PDF Files, add marks if (file.isPdf && fileName in marksByFileNamePage) { - const blob = await addMarks(file, marksByFileNamePage[fileName]) + const marksToAdd = marksByFileNamePage[fileName] + if (meta.keys) { + for (let i = 0; i < marks.length; i++) { + const m = marks[i] + const { sender, keys } = meta.keys + const usersPubkey = store.getState().auth.usersPubkey! + const usersNpub = hexToNpub(usersPubkey) + if (usersNpub in keys) { + const encryptionKey = await NostrController.getInstance() + .nip04Decrypt(sender, keys[usersNpub]) + .catch((err) => { + console.log( + 'An error occurred in decrypting encryption key', + err + ) + return null + }) + + try { + const { fetchAndDecrypt } = MARK_TYPE_CONFIG[m.type] || {} + if ( + typeof fetchAndDecrypt === 'function' && + m.value && + encryptionKey + ) { + // TODO + // extract draw with proper values + // save both pdf with marking and original hash files for signature + // ... + const decrypted = await fetchAndDecrypt(m.value, encryptionKey) + marks[i].value = decrypted + } + } catch (error) { + console.error(`Error during mark fetchAndDecrypt phase`, error) + } + } + } + } + const blob = await addMarks(file, marksToAdd) zip.file(`marked/${fileName}`, blob) } diff --git a/src/utils/mark.ts b/src/utils/mark.ts index a5eb974..061f3f0 100644 --- a/src/utils/mark.ts +++ b/src/utils/mark.ts @@ -24,6 +24,7 @@ import { faStamp, faTableCellsLarge } from '@fortawesome/free-solid-svg-icons' +import { MARK_TYPE_CONFIG } from '../components/getMarkComponents.tsx' /** * Takes in an array of Marks already filtered by User. @@ -279,6 +280,26 @@ export const getOptimizedPathsWithStrokeWidth = (svgString: string) => { return tuples } +export const processMarks = async (marks: Mark[], encryptionKey?: string) => { + const _marks = [...marks] + for (let i = 0; i < _marks.length; i++) { + const mark = _marks[i] + const hasProcess = + mark.type in MARK_TYPE_CONFIG && + typeof MARK_TYPE_CONFIG[mark.type]?.encryptAndUpload === 'function' + + if (hasProcess) { + const value = mark.value! + const processFn = MARK_TYPE_CONFIG[mark.type]?.encryptAndUpload + if (processFn) { + mark.value = await processFn(value, encryptionKey) + } + } + } + + return _marks +} + export { getCurrentUserMarks, filterMarksByPubkey, diff --git a/src/utils/pdf.ts b/src/utils/pdf.ts index 41e1d02..88cdc67 100644 --- a/src/utils/pdf.ts +++ b/src/utils/pdf.ts @@ -1,5 +1,5 @@ import { MarkType, PdfPage } from '../types/drawing.ts' -import { LineCapStyle, PDFDocument, PDFFont, PDFPage, rgb } from 'pdf-lib' +import { PDFDocument, PDFFont, PDFPage, rgb } from 'pdf-lib' import { Mark } from '../types/mark.ts' import * as PDFJS from 'pdfjs-dist' import PDFJSWorker from 'pdfjs-dist/build/pdf.worker.min.mjs?worker' @@ -11,6 +11,9 @@ if (!PDFJS.GlobalWorkerOptions.workerPort) { import fontkit from '@pdf-lib/fontkit' import defaultFont from '../assets/fonts/roboto-regular.ttf' +import { BasicPoint } from 'signature_pad/dist/types/point' +import SignaturePad from 'signature_pad' +import { SIGNATURE_PAD_OPTIONS, SIGNATURE_PAD_SIZE } from './const.ts' /** * Defined font size used when generating a PDF. Currently it is difficult to fully @@ -136,7 +139,7 @@ export const addMarks = async ( const mark = marksPerPage[i][j] switch (mark.type) { case MarkType.SIGNATURE: - drawSignatureText(mark, pages[i]) + await embedSignaturePng(mark, pages[i], pdf) break default: @@ -255,23 +258,41 @@ async function embedFont(pdf: PDFDocument) { return embeddedFont } -const drawSignatureText = (mark: Mark, page: PDFPage) => { +const embedSignaturePng = async ( + mark: Mark, + page: PDFPage, + pdf: PDFDocument +) => { const { location } = mark const { height } = page.getSize() - // Convert the mark location origin (top, left) to PDF origin (bottom, left) - const x = location.left - const y = height - location.top - if (hasValue(mark)) { - const segments: string[][] = JSON.parse(mark.value!) - segments.forEach(([d, w]) => { - page.drawSvgPath(d, { - x, - y, - borderWidth: parseFloat(w), - borderLineCap: LineCapStyle.Round - }) + const data = JSON.parse(mark.value!).map((p: BasicPoint[]) => ({ + points: p + })) + const canvas = document.createElement('canvas') + canvas.width = SIGNATURE_PAD_SIZE.width + canvas.height = SIGNATURE_PAD_SIZE.height + const pad = new SignaturePad(canvas, SIGNATURE_PAD_OPTIONS) + pad.fromData(data) + const signatureImage = await pdf.embedPng(pad.toDataURL()) + + const scaled = signatureImage.scaleToFit(location.width, location.height) + + // Convert the mark location origin (top, left) to PDF origin (bottom, left) + // and center the image + const x = location.left + (location.width - scaled.width) / 2 + const y = + height - + location.top - + location.height + + (location.height - scaled.height) / 2 + + page.drawImage(signatureImage, { + x, + y, + width: scaled.width, + height: scaled.height }) } } From cdf26b6614fc33f840a20596a064b20cc503275a Mon Sep 17 00:00:00 2001 From: enes Date: Fri, 15 Nov 2024 18:04:40 +0100 Subject: [PATCH 06/11] feat(signature): export signature files --- src/utils/file.ts | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/src/utils/file.ts b/src/utils/file.ts index 38dd0f1..ec118df 100644 --- a/src/utils/file.ts +++ b/src/utils/file.ts @@ -1,3 +1,4 @@ +import axios from 'axios' import { MARK_TYPE_CONFIG } from '../components/getMarkComponents.tsx' import { NostrController } from '../controllers/NostrController.ts' import store from '../store/store.ts' @@ -50,11 +51,22 @@ export const getZipWithFiles = async ( m.value && encryptionKey ) { - // TODO - // extract draw with proper values - // save both pdf with marking and original hash files for signature - // ... + // Save the original file + const encryptedArrayBuffer = await axios.get(m.value, { + responseType: 'arraybuffer' + }) + const link = m.value.split('/') + zip.file( + `blossom/encrypted/${link[link.length - 1]}`, + new Blob([encryptedArrayBuffer.data]) + ) const decrypted = await fetchAndDecrypt(m.value, encryptionKey) + + // Save decrypted + zip.file( + `blossom/decrypted/${link[link.length - 1]}.json`, + new Blob([decrypted]) + ) marks[i].value = decrypted } } catch (error) { From a1c308727f2786b48cb083bf0544a358ab211c2c Mon Sep 17 00:00:00 2001 From: enes Date: Mon, 18 Nov 2024 13:39:54 +0100 Subject: [PATCH 07/11] fix(signature): force re-render on value change --- src/components/MarkInputs/Signature.tsx | 6 +++++- src/components/PDFView/PdfMarkItem.tsx | 6 +++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/components/MarkInputs/Signature.tsx b/src/components/MarkInputs/Signature.tsx index 4fadede..d60a8a1 100644 --- a/src/components/MarkInputs/Signature.tsx +++ b/src/components/MarkInputs/Signature.tsx @@ -85,7 +85,11 @@ export const MarkInputSignature = ({ > {typeof userMark?.mark !== 'undefined' && (
- +
)}
diff --git a/src/components/PDFView/PdfMarkItem.tsx b/src/components/PDFView/PdfMarkItem.tsx index 3aeef44..d850fbc 100644 --- a/src/components/PDFView/PdfMarkItem.tsx +++ b/src/components/PDFView/PdfMarkItem.tsx @@ -51,7 +51,11 @@ const PdfMarkItem = forwardRef( }} > {typeof MarkRenderComponent !== 'undefined' && ( - + )}
) From f72ad37ec01efc5b45be7993574746ce06734733 Mon Sep 17 00:00:00 2001 From: enes Date: Mon, 18 Nov 2024 13:49:39 +0100 Subject: [PATCH 08/11] refactor(signature): save only decrypted signature files on export --- src/utils/file.ts | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/src/utils/file.ts b/src/utils/file.ts index ec118df..8686d09 100644 --- a/src/utils/file.ts +++ b/src/utils/file.ts @@ -1,4 +1,3 @@ -import axios from 'axios' import { MARK_TYPE_CONFIG } from '../components/getMarkComponents.tsx' import { NostrController } from '../controllers/NostrController.ts' import store from '../store/store.ts' @@ -51,20 +50,13 @@ export const getZipWithFiles = async ( m.value && encryptionKey ) { - // Save the original file - const encryptedArrayBuffer = await axios.get(m.value, { - responseType: 'arraybuffer' - }) + // Fetch and decrypt the original file const link = m.value.split('/') - zip.file( - `blossom/encrypted/${link[link.length - 1]}`, - new Blob([encryptedArrayBuffer.data]) - ) const decrypted = await fetchAndDecrypt(m.value, encryptionKey) // Save decrypted zip.file( - `blossom/decrypted/${link[link.length - 1]}.json`, + `signatures/${link[link.length - 1]}.json`, new Blob([decrypted]) ) marks[i].value = decrypted From be146fa0fa9538a7eac899354443031ed6b40d55 Mon Sep 17 00:00:00 2001 From: enes Date: Mon, 18 Nov 2024 17:20:20 +0100 Subject: [PATCH 09/11] refactor(signature): apply strategy pattern and make it easier to expand with new tools --- src/components/MarkFormField/index.tsx | 22 ++--- src/components/MarkTypeStrategy/MarkInput.tsx | 16 ++++ .../MarkTypeStrategy/MarkRender.tsx | 20 ++++ .../MarkTypeStrategy/MarkStrategy.tsx | 32 +++++++ .../Signature/Input.module.scss} | 2 +- .../Signature/Input.tsx} | 8 +- .../Signature/Render.module.scss} | 0 .../Signature/Render.tsx} | 6 +- .../MarkTypeStrategy/Signature/index.tsx | 86 +++++++++++++++++ .../Text/Input.tsx} | 4 +- .../MarkTypeStrategy/Text/index.tsx | 7 ++ src/components/PDFView/PdfMarkItem.tsx | 17 ++-- src/components/PDFView/PdfPageItem.tsx | 7 +- src/components/getMarkComponents.tsx | 92 ------------------- src/hooks/useSigitMeta.tsx | 2 +- src/pages/sign/index.tsx | 2 +- src/pages/verify/index.tsx | 12 +-- src/types/mark.ts | 23 ----- src/utils/file.ts | 2 +- src/utils/mark.ts | 2 +- 20 files changed, 200 insertions(+), 162 deletions(-) create mode 100644 src/components/MarkTypeStrategy/MarkInput.tsx create mode 100644 src/components/MarkTypeStrategy/MarkRender.tsx create mode 100644 src/components/MarkTypeStrategy/MarkStrategy.tsx rename src/components/{MarkInputs/Signature.module.scss => MarkTypeStrategy/Signature/Input.module.scss} (94%) rename src/components/{MarkInputs/Signature.tsx => MarkTypeStrategy/Signature/Input.tsx} (93%) rename src/components/{MarkRender/Signature.module.scss => MarkTypeStrategy/Signature/Render.module.scss} (100%) rename src/components/{MarkRender/Signature.tsx => MarkTypeStrategy/Signature/Render.tsx} (89%) create mode 100644 src/components/MarkTypeStrategy/Signature/index.tsx rename src/components/{MarkInputs/Text.tsx => MarkTypeStrategy/Text/Input.tsx} (72%) create mode 100644 src/components/MarkTypeStrategy/Text/index.tsx delete mode 100644 src/components/getMarkComponents.tsx diff --git a/src/components/MarkFormField/index.tsx b/src/components/MarkFormField/index.tsx index fef3f17..18bbf31 100644 --- a/src/components/MarkFormField/index.tsx +++ b/src/components/MarkFormField/index.tsx @@ -7,7 +7,7 @@ import { isCurrentValueLast } from '../../utils' import React, { useState } from 'react' -import { MARK_TYPE_CONFIG } from '../getMarkComponents.tsx' +import { MarkInput } from '../MarkTypeStrategy/MarkInput.tsx' interface MarkFormFieldProps { currentUserMarks: CurrentUserMark[] @@ -52,8 +52,7 @@ const MarkFormField = ({ } const toggleActions = () => setDisplayActions(!displayActions) const markLabel = getToolboxLabelByMarkType(selectedMark.mark.type) - const { input: MarkInputComponent } = - MARK_TYPE_CONFIG[selectedMark.mark.type] || {} + return (
@@ -84,15 +83,14 @@ const MarkFormField = ({
handleFormSubmit(e)}> - {typeof MarkInputComponent !== 'undefined' && ( - - )} +