From 7c7a222d4fac7d119270f3d6b79b75f6d60032ff Mon Sep 17 00:00:00 2001 From: enes Date: Fri, 8 Nov 2024 16:58:27 +0100 Subject: [PATCH] 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 + }) }) } }