refactor: use signature pad, smoother signature line

BREAKING CHANGE: mark.value type changed
This commit is contained in:
enes 2024-11-08 16:58:27 +01:00
parent 4cb6f07a68
commit 7c7a222d4f
9 changed files with 85 additions and 86 deletions

13
package-lock.json generated
View File

@ -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",

View File

@ -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"
},

View File

@ -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<HTMLCanvasElement>(null)
const [drawing, setDrawing] = useState(false)
const [paths, setPaths] = useState<string[]>(value ? JSON.parse(value) : [])
const signaturePad = useRef<SignaturePad | null>(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}
></canvas>
{typeof userMark?.mark !== 'undefined' && (
<div className={styles.absolute}>

View File

@ -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 (
<svg viewBox={`0 0 ${mark.location.width} ${mark.location.height}`}>
{paths.map((path: string) => (
<path d={path} stroke="black" fill="none" />
<svg
viewBox={`0 0 ${mark.location.width} ${mark.location.height}`}
strokeLinecap="round"
>
{segments.map(([path, width]) => (
<path d={path} strokeWidth={width} stroke="black" fill="none" />
))}
</svg>
)

View File

@ -117,7 +117,9 @@ const PdfMarking = (props: PdfMarkingProps) => {
// setCurrentUserMarks(updatedCurrentUserMarks)
// }
const handleChange = (value: string) => setSelectedMarkValue(value)
const handleChange = (value: string) => {
setSelectedMarkValue(value)
}
return (
<>

View File

@ -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

View File

@ -11,3 +11,4 @@ export * from './string'
export * from './url'
export * from './utils'
export * from './zip'
export * from './const'

View File

@ -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 = `<svg xmlns="http://www.w3.org/2000/svg" width="${location.width}" height="${location.height}">${paths.map((path) => `<path d="${path}" stroke="black" fill="none" />`).join('')}</svg>`
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 {

View File

@ -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
})
})
}
}