chore(git): merge pull request #258 from feat/signature-pad into staging
All checks were successful
Release to Staging / build_and_release (push) Successful in 1m38s
All checks were successful
Release to Staging / build_and_release (push) Successful in 1m38s
Reviewed-on: #258 Reviewed-by: Stixx <m@noreply.git.nostrdev.com>
This commit is contained in:
commit
760ac176e8
33
package-lock.json
generated
33
package-lock.json
generated
@ -47,7 +47,7 @@
|
||||
"react-singleton-hook": "^4.0.1",
|
||||
"react-toastify": "10.0.4",
|
||||
"redux": "5.0.1",
|
||||
"svgo": "^3.3.2",
|
||||
"signature_pad": "^5.0.4",
|
||||
"tseep": "1.2.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
@ -2214,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"
|
||||
@ -2935,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": {
|
||||
@ -3634,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",
|
||||
@ -3650,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",
|
||||
@ -3663,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"
|
||||
@ -3675,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"
|
||||
@ -3688,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",
|
||||
@ -3702,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": {
|
||||
@ -3949,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",
|
||||
@ -3976,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",
|
||||
@ -3988,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"
|
||||
@ -4003,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",
|
||||
@ -4020,9 +4032,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": {
|
||||
@ -4052,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"
|
||||
@ -5979,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": {
|
||||
@ -6560,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"
|
||||
@ -6919,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": {
|
||||
@ -7866,6 +7882,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",
|
||||
@ -7971,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"
|
||||
@ -8197,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",
|
||||
@ -8222,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"
|
||||
|
@ -57,7 +57,7 @@
|
||||
"react-singleton-hook": "^4.0.1",
|
||||
"react-toastify": "10.0.4",
|
||||
"redux": "5.0.1",
|
||||
"svgo": "^3.3.2",
|
||||
"signature_pad": "^5.0.4",
|
||||
"tseep": "1.2.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
@ -3,14 +3,13 @@ import { useAppSelector } from './hooks/store'
|
||||
import { Navigate, Route, Routes } from 'react-router-dom'
|
||||
import { AuthController } from './controllers'
|
||||
import { MainLayout } from './layouts/Main'
|
||||
import { appPrivateRoutes, appPublicRoutes } from './routes'
|
||||
import './App.scss'
|
||||
import {
|
||||
appPrivateRoutes,
|
||||
appPublicRoutes,
|
||||
privateRoutes,
|
||||
publicRoutes,
|
||||
recursiveRouteRenderer
|
||||
} from './routes'
|
||||
import './App.scss'
|
||||
} from './routes/util'
|
||||
|
||||
const App = () => {
|
||||
const authState = useAppSelector((state) => state.auth)
|
||||
|
@ -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 (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.trigger}>
|
||||
@ -84,14 +83,14 @@ const MarkFormField = ({
|
||||
</div>
|
||||
<div className={styles.inputWrapper}>
|
||||
<form onSubmit={(e) => handleFormSubmit(e)}>
|
||||
{typeof MarkInputComponent !== 'undefined' && (
|
||||
<MarkInputComponent
|
||||
value={selectedMarkValue}
|
||||
placeholder={markLabel}
|
||||
handler={handleSelectedMarkValueChange}
|
||||
userMark={selectedMark}
|
||||
/>
|
||||
)}
|
||||
<MarkInput
|
||||
markType={selectedMark.mark.type}
|
||||
key={selectedMark.id}
|
||||
value={selectedMarkValue}
|
||||
placeholder={markLabel}
|
||||
handler={handleSelectedMarkValueChange}
|
||||
userMark={selectedMark}
|
||||
/>
|
||||
<div className={styles.actionsBottom}>
|
||||
<button type="submit" className={styles.submitButton}>
|
||||
NEXT
|
||||
@ -104,7 +103,7 @@ const MarkFormField = ({
|
||||
return (
|
||||
<div className={styles.pagination} key={index}>
|
||||
<button
|
||||
className={`${styles.paginationButton} ${isDone(mark) && styles.paginationButtonDone}`}
|
||||
className={`${styles.paginationButton} ${isDone(mark) ? styles.paginationButtonDone : ''}`}
|
||||
onClick={() => handleCurrentUserMarkChange(mark)}
|
||||
>
|
||||
{mark.id}
|
||||
|
@ -1,101 +0,0 @@
|
||||
import { useRef, useState } from 'react'
|
||||
import { MarkInputProps } from '../../types/mark'
|
||||
import { getOptimizedPaths, optimizeSVG } 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'
|
||||
|
||||
export const MarkInputSignature = ({
|
||||
value,
|
||||
handler,
|
||||
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) : [])
|
||||
|
||||
function update() {
|
||||
if (location && paths) {
|
||||
if (paths.length) {
|
||||
const optimizedSvg = optimizeSVG(location, paths)
|
||||
const extractedPaths = getOptimizedPaths(optimizedSvg)
|
||||
handler(JSON.stringify(extractedPaths))
|
||||
} else {
|
||||
handler('')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handlePointerDown = (event: React.PointerEvent) => {
|
||||
const rect = event.currentTarget.getBoundingClientRect()
|
||||
const x = event.clientX - rect.left
|
||||
const y = event.clientY - rect.top
|
||||
|
||||
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
|
||||
})
|
||||
}
|
||||
|
||||
const handleReset = () => {
|
||||
setPaths([])
|
||||
setDrawing(false)
|
||||
update()
|
||||
const ctx = canvasRef.current?.getContext('2d')
|
||||
ctx?.clearRect(0, 0, canvasRef.current!.width, canvasRef.current!.height)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={styles.wrapper}>
|
||||
<div className={styles.relative}>
|
||||
<canvas
|
||||
height={location?.height}
|
||||
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}>
|
||||
<MarkRenderSignature value={value} mark={userMark.mark} />
|
||||
</div>
|
||||
)}
|
||||
<div className={styles.reset}>
|
||||
<FontAwesomeIcon size="sm" icon={faEraser} onClick={handleReset} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
@ -1,13 +0,0 @@
|
||||
import { MarkRenderProps } from '../../types/mark'
|
||||
|
||||
export const MarkRenderSignature = ({ value, mark }: MarkRenderProps) => {
|
||||
const paths = 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>
|
||||
)
|
||||
}
|
16
src/components/MarkTypeStrategy/MarkInput.tsx
Normal file
16
src/components/MarkTypeStrategy/MarkInput.tsx
Normal file
@ -0,0 +1,16 @@
|
||||
import { MarkType } from '../../types/drawing'
|
||||
import { MARK_TYPE_CONFIG, MarkInputProps } from './MarkStrategy'
|
||||
|
||||
interface MarkInputComponentProps extends MarkInputProps {
|
||||
markType: MarkType
|
||||
}
|
||||
|
||||
export const MarkInput = ({ markType, ...rest }: MarkInputComponentProps) => {
|
||||
const { input: InputComponent } = MARK_TYPE_CONFIG[markType] || {}
|
||||
|
||||
if (typeof InputComponent !== 'undefined') {
|
||||
return <InputComponent {...rest} />
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
20
src/components/MarkTypeStrategy/MarkRender.tsx
Normal file
20
src/components/MarkTypeStrategy/MarkRender.tsx
Normal file
@ -0,0 +1,20 @@
|
||||
import { MarkType } from '../../types/drawing'
|
||||
import { MARK_TYPE_CONFIG, MarkRenderProps } from './MarkStrategy'
|
||||
|
||||
interface MarkRenderComponentProps extends MarkRenderProps {
|
||||
markType: MarkType
|
||||
}
|
||||
|
||||
export const MarkRender = ({ markType, ...rest }: MarkRenderComponentProps) => {
|
||||
const { render: RenderComponent } = MARK_TYPE_CONFIG[markType] || {}
|
||||
|
||||
if (typeof RenderComponent !== 'undefined') {
|
||||
return <RenderComponent {...rest} />
|
||||
}
|
||||
|
||||
return <DefaultRenderComponent {...rest} />
|
||||
}
|
||||
|
||||
const DefaultRenderComponent = ({ value }: MarkRenderProps) => (
|
||||
<span>{value}</span>
|
||||
)
|
32
src/components/MarkTypeStrategy/MarkStrategy.tsx
Normal file
32
src/components/MarkTypeStrategy/MarkStrategy.tsx
Normal file
@ -0,0 +1,32 @@
|
||||
import { MarkType } from '../../types/drawing'
|
||||
import { CurrentUserMark, Mark } from '../../types/mark'
|
||||
import { TextStrategy } from './Text'
|
||||
import { SignatureStrategy } from './Signature'
|
||||
|
||||
export interface MarkInputProps {
|
||||
value: string
|
||||
handler: (value: string) => void
|
||||
placeholder?: string
|
||||
userMark?: CurrentUserMark
|
||||
}
|
||||
|
||||
export interface MarkRenderProps {
|
||||
value?: string
|
||||
mark: Mark
|
||||
}
|
||||
|
||||
export interface MarkStrategy {
|
||||
input: React.FC<MarkInputProps>
|
||||
render: React.FC<MarkRenderProps>
|
||||
encryptAndUpload?: (value: string, key?: string) => Promise<string>
|
||||
fetchAndDecrypt?: (value: string, key?: string) => Promise<string>
|
||||
}
|
||||
|
||||
export type MarkStrategies = {
|
||||
[key in MarkType]?: MarkStrategy
|
||||
}
|
||||
|
||||
export const MARK_TYPE_CONFIG: MarkStrategies = {
|
||||
[MarkType.TEXT]: TextStrategy,
|
||||
[MarkType.SIGNATURE]: SignatureStrategy
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
@import '../../styles/colors.scss';
|
||||
@import '../../../styles/colors.scss';
|
||||
|
||||
$padding: 5px;
|
||||
|
||||
@ -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;
|
||||
|
101
src/components/MarkTypeStrategy/Signature/Input.tsx
Normal file
101
src/components/MarkTypeStrategy/Signature/Input.tsx
Normal file
@ -0,0 +1,101 @@
|
||||
import { useCallback, useEffect, useRef } from 'react'
|
||||
import { faEraser } from '@fortawesome/free-solid-svg-icons'
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import { MarkRenderSignature } from './Render'
|
||||
import SignaturePad from 'signature_pad'
|
||||
import { SIGNATURE_PAD_OPTIONS, SIGNATURE_PAD_SIZE } from '../../../utils/const'
|
||||
import { BasicPoint } from 'signature_pad/dist/types/point'
|
||||
import { MarkInputProps } from '../MarkStrategy'
|
||||
import styles from './Input.module.scss'
|
||||
|
||||
export const MarkInputSignature = ({
|
||||
value,
|
||||
handler,
|
||||
userMark
|
||||
}: MarkInputProps) => {
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null)
|
||||
const signaturePad = useRef<SignaturePad | null>(null)
|
||||
|
||||
const update = useCallback(() => {
|
||||
const data = signaturePad.current?.toData()
|
||||
const reduced = data?.map((pg) => pg.points)
|
||||
const json = JSON.stringify(reduced)
|
||||
|
||||
if (signaturePad.current && !signaturePad.current?.isEmpty()) {
|
||||
handler(json)
|
||||
} else {
|
||||
handler('')
|
||||
}
|
||||
}, [handler])
|
||||
|
||||
useEffect(() => {
|
||||
const handleEndStroke = () => {
|
||||
update()
|
||||
}
|
||||
if (canvasRef.current) {
|
||||
if (signaturePad.current === null) {
|
||||
signaturePad.current = new SignaturePad(
|
||||
canvasRef.current,
|
||||
SIGNATURE_PAD_OPTIONS
|
||||
)
|
||||
}
|
||||
|
||||
signaturePad.current.addEventListener('endStroke', handleEndStroke)
|
||||
}
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('endStroke', handleEndStroke)
|
||||
}
|
||||
}, [update])
|
||||
|
||||
useEffect(() => {
|
||||
if (signaturePad.current) {
|
||||
if (value) {
|
||||
signaturePad.current.fromData(
|
||||
JSON.parse(value).map((p: BasicPoint[]) => ({
|
||||
points: p
|
||||
}))
|
||||
)
|
||||
} else {
|
||||
signaturePad.current?.clear()
|
||||
}
|
||||
}
|
||||
update()
|
||||
}, [update, value])
|
||||
|
||||
const handleReset = () => {
|
||||
signaturePad.current?.clear()
|
||||
update()
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.wrapper}>
|
||||
<div
|
||||
className={styles.relative}
|
||||
style={{
|
||||
width: SIGNATURE_PAD_SIZE.width,
|
||||
height: SIGNATURE_PAD_SIZE.height
|
||||
}}
|
||||
>
|
||||
<canvas
|
||||
width={SIGNATURE_PAD_SIZE.width}
|
||||
height={SIGNATURE_PAD_SIZE.height}
|
||||
ref={canvasRef}
|
||||
className={styles.canvas}
|
||||
></canvas>
|
||||
{typeof userMark?.mark !== 'undefined' && (
|
||||
<div className={styles.absolute}>
|
||||
<MarkRenderSignature
|
||||
key={userMark.mark.value}
|
||||
value={userMark.mark.value}
|
||||
mark={userMark.mark}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className={styles.reset}>
|
||||
<FontAwesomeIcon size="sm" icon={faEraser} onClick={handleReset} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
.img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
overflow: hidden;
|
||||
pointer-events: none;
|
||||
-webkit-user-select: none;
|
||||
user-select: none;
|
||||
}
|
27
src/components/MarkTypeStrategy/Signature/Render.tsx
Normal file
27
src/components/MarkTypeStrategy/Signature/Render.tsx
Normal file
@ -0,0 +1,27 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import SignaturePad from 'signature_pad'
|
||||
import { SIGNATURE_PAD_OPTIONS, SIGNATURE_PAD_SIZE } from '../../../utils'
|
||||
import { BasicPoint } from 'signature_pad/dist/types/point'
|
||||
import { MarkRenderProps } from '../MarkStrategy'
|
||||
import styles from './Render.module.scss'
|
||||
|
||||
export const MarkRenderSignature = ({ value }: MarkRenderProps) => {
|
||||
const [dataUrl, setDataUrl] = useState<string | undefined>()
|
||||
|
||||
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 ? <img src={dataUrl} className={styles.img} alt="" /> : null
|
||||
}
|
95
src/components/MarkTypeStrategy/Signature/index.tsx
Normal file
95
src/components/MarkTypeStrategy/Signature/index.tsx
Normal file
@ -0,0 +1,95 @@
|
||||
import axios from 'axios'
|
||||
import {
|
||||
decryptArrayBuffer,
|
||||
encryptArrayBuffer,
|
||||
getHash,
|
||||
isOnline,
|
||||
uploadToFileStorage
|
||||
} from '../../../utils'
|
||||
import { MarkStrategy } from '../MarkStrategy'
|
||||
import { MarkInputSignature } from './Input'
|
||||
import { MarkRenderSignature } from './Render'
|
||||
|
||||
export const SignatureStrategy: MarkStrategy = {
|
||||
input: MarkInputSignature,
|
||||
render: MarkRenderSignature,
|
||||
encryptAndUpload: async (value, encryptionKey) => {
|
||||
// Value is the stringified signature object
|
||||
// Encode it to the arrayBuffer
|
||||
const encoder = new TextEncoder()
|
||||
const uint8Array = encoder.encode(value)
|
||||
|
||||
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
|
||||
)
|
||||
|
||||
const hash = await getHash(encryptedArrayBuffer)
|
||||
if (!hash) {
|
||||
throw new Error("Can't get encrypted file hash.")
|
||||
}
|
||||
|
||||
// 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)
|
||||
console.info(`${file.name} uploaded to file storage`)
|
||||
return url
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
console.error(
|
||||
`Error occurred in uploading file ${file.name}`,
|
||||
error.message
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// TOOD: 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'
|
||||
})
|
||||
|
||||
// Verify hash
|
||||
const parts = value.split('/')
|
||||
const urlHash = parts[parts.length - 1]
|
||||
const hash = await getHash(encryptedArrayBuffer.data)
|
||||
if (hash !== urlHash) {
|
||||
// TODO: handle hash verification failing
|
||||
throw new Error('Unable to verify signature')
|
||||
}
|
||||
|
||||
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 json = decoder.decode(arrayBuffer)
|
||||
return json
|
||||
}
|
||||
|
||||
// TOOD: offline
|
||||
return value
|
||||
}
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
import { MarkInputProps } from '../../types/mark'
|
||||
import styles from '../MarkFormField/style.module.scss'
|
||||
import { MarkInputProps } from '../MarkStrategy'
|
||||
import styles from '../../MarkFormField/style.module.scss'
|
||||
|
||||
export const MarkInputText = ({
|
||||
value,
|
7
src/components/MarkTypeStrategy/Text/index.tsx
Normal file
7
src/components/MarkTypeStrategy/Text/index.tsx
Normal file
@ -0,0 +1,7 @@
|
||||
import { MarkStrategy } from '../MarkStrategy'
|
||||
import { MarkInputText } from './Input'
|
||||
|
||||
export const TextStrategy: MarkStrategy = {
|
||||
input: MarkInputText,
|
||||
render: ({ value }) => <>{value}</>
|
||||
}
|
@ -4,7 +4,7 @@ import { FONT_SIZE, FONT_TYPE, inPx } from '../../utils/pdf.ts'
|
||||
import { useScale } from '../../hooks/useScale.tsx'
|
||||
import { forwardRef } from 'react'
|
||||
import { npubToHex } from '../../utils/nostr.ts'
|
||||
import { MARK_TYPE_CONFIG } from '../getMarkComponents.tsx'
|
||||
import { MarkRender } from '../MarkTypeStrategy/MarkRender.tsx'
|
||||
|
||||
interface PdfMarkItemProps {
|
||||
userMark: CurrentUserMark
|
||||
@ -28,8 +28,6 @@ const PdfMarkItem = forwardRef<HTMLDivElement, PdfMarkItemProps>(
|
||||
const getMarkValue = () =>
|
||||
isEdited() ? selectedMarkValue : userMark.currentValue
|
||||
const { from } = useScale()
|
||||
const { render: MarkRenderComponent } =
|
||||
MARK_TYPE_CONFIG[userMark.mark.type] || {}
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
@ -50,9 +48,12 @@ const PdfMarkItem = forwardRef<HTMLDivElement, PdfMarkItemProps>(
|
||||
fontSize: inPx(from(pageWidth, FONT_SIZE))
|
||||
}}
|
||||
>
|
||||
{typeof MarkRenderComponent !== 'undefined' && (
|
||||
<MarkRenderComponent value={getMarkValue()} mark={userMark.mark} />
|
||||
)}
|
||||
<MarkRender
|
||||
key={getMarkValue()}
|
||||
markType={userMark.mark.type}
|
||||
value={getMarkValue()}
|
||||
mark={userMark.mark}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
@ -117,7 +117,9 @@ const PdfMarking = (props: PdfMarkingProps) => {
|
||||
// setCurrentUserMarks(updatedCurrentUserMarks)
|
||||
// }
|
||||
|
||||
const handleChange = (value: string) => setSelectedMarkValue(value)
|
||||
const handleChange = (value: string) => {
|
||||
setSelectedMarkValue(value)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
|
@ -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 { MarkRender } from '../MarkTypeStrategy/MarkRender.tsx'
|
||||
interface PdfPageProps {
|
||||
fileName: string
|
||||
pageIndex: number
|
||||
@ -73,7 +74,7 @@ const PdfPageItem = ({
|
||||
fontSize: inPx(from(page.width, FONT_SIZE))
|
||||
}}
|
||||
>
|
||||
{m.value}
|
||||
<MarkRender value={m.value} mark={m} markType={m.type} />
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
|
@ -1,16 +0,0 @@
|
||||
import { MarkType } from '../types/drawing'
|
||||
import { MarkConfigs } from '../types/mark'
|
||||
import { MarkInputSignature } from './MarkInputs/Signature'
|
||||
import { MarkInputText } from './MarkInputs/Text'
|
||||
import { MarkRenderSignature } from './MarkRender/Signature'
|
||||
|
||||
export const MARK_TYPE_CONFIG: MarkConfigs = {
|
||||
[MarkType.TEXT]: {
|
||||
input: MarkInputText,
|
||||
render: ({ value }) => <>{value}</>
|
||||
},
|
||||
[MarkType.SIGNATURE]: {
|
||||
input: MarkInputSignature,
|
||||
render: MarkRenderSignature
|
||||
}
|
||||
}
|
@ -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/MarkTypeStrategy/MarkStrategy'
|
||||
|
||||
/**
|
||||
* 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))
|
||||
|
@ -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/MarkTypeStrategy/MarkStrategy.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)
|
||||
|
@ -55,7 +55,7 @@ import {
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
import { upgradeAndVerifyTimestamp } from '../../utils/opentimestamps.ts'
|
||||
import _ from 'lodash'
|
||||
import { MARK_TYPE_CONFIG } from '../../components/getMarkComponents.tsx'
|
||||
import { MarkRender } from '../../components/MarkTypeStrategy/MarkRender.tsx'
|
||||
|
||||
interface PdfViewProps {
|
||||
files: CurrentUserFile[]
|
||||
@ -115,8 +115,6 @@ const SlimPdfView = ({
|
||||
alt={`page ${i} of ${file.name}`}
|
||||
/>
|
||||
{marks.map((m) => {
|
||||
const { render: MarkRenderComponent } =
|
||||
MARK_TYPE_CONFIG[m.type] || {}
|
||||
return (
|
||||
<div
|
||||
className={`file-mark ${styles.mark}`}
|
||||
@ -132,9 +130,11 @@ const SlimPdfView = ({
|
||||
fontSize: inPx(from(page.width, FONT_SIZE))
|
||||
}}
|
||||
>
|
||||
{typeof MarkRenderComponent !== 'undefined' && (
|
||||
<MarkRenderComponent value={m.value} mark={m} />
|
||||
)}
|
||||
<MarkRender
|
||||
markType={m.type}
|
||||
value={m.value}
|
||||
mark={m}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
@ -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()
|
||||
|
@ -1,16 +1,4 @@
|
||||
import { CreatePage } from '../pages/create'
|
||||
import { HomePage } from '../pages/home'
|
||||
import { LandingPage } from '../pages/landing'
|
||||
import { ProfilePage } from '../pages/profile'
|
||||
import { SettingsPage } from '../pages/settings/Settings'
|
||||
import { CacheSettingsPage } from '../pages/settings/cache'
|
||||
import { NostrLoginPage } from '../pages/settings/nostrLogin'
|
||||
import { ProfileSettingsPage } from '../pages/settings/profile'
|
||||
import { RelaysPage } from '../pages/settings/relays'
|
||||
import { SignPage } from '../pages/sign'
|
||||
import { VerifyPage } from '../pages/verify'
|
||||
import { hexToNpub } from '../utils'
|
||||
import { Route, RouteProps } from 'react-router-dom'
|
||||
|
||||
export const appPrivateRoutes = {
|
||||
homePage: '/',
|
||||
@ -39,93 +27,3 @@ export const getProfileRoute = (hexKey: string) =>
|
||||
|
||||
export const getProfileSettingsRoute = (hexKey: string) =>
|
||||
appPrivateRoutes.profileSettings.replace(':npub', hexToNpub(hexKey))
|
||||
|
||||
/**
|
||||
* Helper type allows for extending react-router-dom's **RouteProps** with generic type
|
||||
*/
|
||||
type CustomRouteProps<T> = T &
|
||||
Omit<RouteProps, 'children'> & {
|
||||
children?: Array<CustomRouteProps<T>>
|
||||
}
|
||||
|
||||
/**
|
||||
* This function maps over nested routes with optional condition for rendering
|
||||
* @param {CustomRouteProps<T>[]} routes - routes list
|
||||
* @param {RenderConditionCallbackFn} renderConditionCallbackFn - render condition callback (default true)
|
||||
*/
|
||||
export function recursiveRouteRenderer<T>(
|
||||
routes?: CustomRouteProps<T>[],
|
||||
renderConditionCallbackFn: (route: CustomRouteProps<T>) => boolean = () =>
|
||||
true
|
||||
) {
|
||||
if (!routes) return null
|
||||
|
||||
// Callback allows us to pass arbitrary conditions for each route's rendering
|
||||
// Skipping the callback will by default evaluate to true (show route)
|
||||
return routes.map((route, index) =>
|
||||
renderConditionCallbackFn(route) ? (
|
||||
<Route
|
||||
key={`${route.path}${index}`}
|
||||
path={route.path}
|
||||
element={route.element}
|
||||
>
|
||||
{recursiveRouteRenderer(route.children, renderConditionCallbackFn)}
|
||||
</Route>
|
||||
) : null
|
||||
)
|
||||
}
|
||||
|
||||
type PublicRouteProps = CustomRouteProps<{
|
||||
hiddenWhenLoggedIn?: boolean
|
||||
}>
|
||||
|
||||
export const publicRoutes: PublicRouteProps[] = [
|
||||
{
|
||||
path: appPublicRoutes.landingPage,
|
||||
hiddenWhenLoggedIn: true,
|
||||
element: <LandingPage />
|
||||
},
|
||||
{
|
||||
path: appPublicRoutes.profile,
|
||||
element: <ProfilePage />
|
||||
},
|
||||
{
|
||||
path: appPublicRoutes.verify,
|
||||
element: <VerifyPage />
|
||||
}
|
||||
]
|
||||
|
||||
export const privateRoutes = [
|
||||
{
|
||||
path: appPrivateRoutes.homePage,
|
||||
element: <HomePage />
|
||||
},
|
||||
{
|
||||
path: appPrivateRoutes.create,
|
||||
element: <CreatePage />
|
||||
},
|
||||
{
|
||||
path: `${appPrivateRoutes.sign}/:id?`,
|
||||
element: <SignPage />
|
||||
},
|
||||
{
|
||||
path: appPrivateRoutes.settings,
|
||||
element: <SettingsPage />
|
||||
},
|
||||
{
|
||||
path: appPrivateRoutes.profileSettings,
|
||||
element: <ProfileSettingsPage />
|
||||
},
|
||||
{
|
||||
path: appPrivateRoutes.cacheSettings,
|
||||
element: <CacheSettingsPage />
|
||||
},
|
||||
{
|
||||
path: appPrivateRoutes.relays,
|
||||
element: <RelaysPage />
|
||||
},
|
||||
{
|
||||
path: appPrivateRoutes.nostrLogin,
|
||||
element: <NostrLoginPage />
|
||||
}
|
||||
]
|
||||
|
103
src/routes/util.tsx
Normal file
103
src/routes/util.tsx
Normal file
@ -0,0 +1,103 @@
|
||||
import { Route, RouteProps } from 'react-router-dom'
|
||||
import { appPrivateRoutes, appPublicRoutes } from '.'
|
||||
import { CreatePage } from '../pages/create'
|
||||
import { HomePage } from '../pages/home'
|
||||
import { LandingPage } from '../pages/landing'
|
||||
import { ProfilePage } from '../pages/profile'
|
||||
import { CacheSettingsPage } from '../pages/settings/cache'
|
||||
import { NostrLoginPage } from '../pages/settings/nostrLogin'
|
||||
import { ProfileSettingsPage } from '../pages/settings/profile'
|
||||
import { RelaysPage } from '../pages/settings/relays'
|
||||
import { SettingsPage } from '../pages/settings/Settings'
|
||||
import { SignPage } from '../pages/sign'
|
||||
import { VerifyPage } from '../pages/verify'
|
||||
|
||||
/**
|
||||
* Helper type allows for extending react-router-dom's **RouteProps** with generic type
|
||||
*/
|
||||
type CustomRouteProps<T> = T &
|
||||
Omit<RouteProps, 'children'> & {
|
||||
children?: Array<CustomRouteProps<T>>
|
||||
}
|
||||
|
||||
/**
|
||||
* This function maps over nested routes with optional condition for rendering
|
||||
* @param {CustomRouteProps<T>[]} routes - routes list
|
||||
* @param {RenderConditionCallbackFn} renderConditionCallbackFn - render condition callback (default true)
|
||||
*/
|
||||
export function recursiveRouteRenderer<T>(
|
||||
routes?: CustomRouteProps<T>[],
|
||||
renderConditionCallbackFn: (route: CustomRouteProps<T>) => boolean = () =>
|
||||
true
|
||||
) {
|
||||
if (!routes) return null
|
||||
|
||||
// Callback allows us to pass arbitrary conditions for each route's rendering
|
||||
// Skipping the callback will by default evaluate to true (show route)
|
||||
return routes.map((route, index) =>
|
||||
renderConditionCallbackFn(route) ? (
|
||||
<Route
|
||||
key={`${route.path}${index}`}
|
||||
path={route.path}
|
||||
element={route.element}
|
||||
>
|
||||
{recursiveRouteRenderer(route.children, renderConditionCallbackFn)}
|
||||
</Route>
|
||||
) : null
|
||||
)
|
||||
}
|
||||
|
||||
type PublicRouteProps = CustomRouteProps<{
|
||||
hiddenWhenLoggedIn?: boolean
|
||||
}>
|
||||
|
||||
export const publicRoutes: PublicRouteProps[] = [
|
||||
{
|
||||
path: appPublicRoutes.landingPage,
|
||||
hiddenWhenLoggedIn: true,
|
||||
element: <LandingPage />
|
||||
},
|
||||
{
|
||||
path: appPublicRoutes.profile,
|
||||
element: <ProfilePage />
|
||||
},
|
||||
{
|
||||
path: appPublicRoutes.verify,
|
||||
element: <VerifyPage />
|
||||
}
|
||||
]
|
||||
|
||||
export const privateRoutes = [
|
||||
{
|
||||
path: appPrivateRoutes.homePage,
|
||||
element: <HomePage />
|
||||
},
|
||||
{
|
||||
path: appPrivateRoutes.create,
|
||||
element: <CreatePage />
|
||||
},
|
||||
{
|
||||
path: `${appPrivateRoutes.sign}/:id?`,
|
||||
element: <SignPage />
|
||||
},
|
||||
{
|
||||
path: appPrivateRoutes.settings,
|
||||
element: <SettingsPage />
|
||||
},
|
||||
{
|
||||
path: appPrivateRoutes.profileSettings,
|
||||
element: <ProfileSettingsPage />
|
||||
},
|
||||
{
|
||||
path: appPrivateRoutes.cacheSettings,
|
||||
element: <CacheSettingsPage />
|
||||
},
|
||||
{
|
||||
path: appPrivateRoutes.relays,
|
||||
element: <RelaysPage />
|
||||
},
|
||||
{
|
||||
path: appPrivateRoutes.nostrLogin,
|
||||
element: <NostrLoginPage />
|
||||
}
|
||||
]
|
@ -28,24 +28,3 @@ export interface MarkRect {
|
||||
width: number
|
||||
height: number
|
||||
}
|
||||
|
||||
export interface MarkInputProps {
|
||||
value: string
|
||||
handler: (value: string) => void
|
||||
placeholder?: string
|
||||
userMark?: CurrentUserMark
|
||||
}
|
||||
|
||||
export interface MarkRenderProps {
|
||||
value?: string
|
||||
mark: Mark
|
||||
}
|
||||
|
||||
export interface MarkConfig {
|
||||
input: React.FC<MarkInputProps>
|
||||
render?: React.FC<MarkRenderProps>
|
||||
}
|
||||
|
||||
export type MarkConfigs = {
|
||||
[key in MarkType]?: MarkConfig
|
||||
}
|
||||
|
@ -112,3 +112,13 @@ 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
|
||||
|
||||
export const SIGNATURE_PAD_SIZE = {
|
||||
width: 600,
|
||||
height: 300
|
||||
}
|
||||
|
@ -1,7 +1,11 @@
|
||||
import { MARK_TYPE_CONFIG } from '../components/MarkTypeStrategy/MarkStrategy.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,49 @@ 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
|
||||
) {
|
||||
// Fetch and decrypt the original file
|
||||
const link = m.value.split('/')
|
||||
const decrypted = await fetchAndDecrypt(m.value, encryptionKey)
|
||||
|
||||
// Save decrypted
|
||||
zip.file(
|
||||
`signatures/${link[link.length - 1]}.json`,
|
||||
new Blob([decrypted])
|
||||
)
|
||||
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)
|
||||
}
|
||||
|
||||
|
@ -11,3 +11,4 @@ export * from './string'
|
||||
export * from './url'
|
||||
export * from './utils'
|
||||
export * from './zip'
|
||||
export * from './const'
|
||||
|
@ -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,7 @@ import {
|
||||
faStamp,
|
||||
faTableCellsLarge
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
import { Config, optimize } from 'svgo'
|
||||
import { MARK_TYPE_CONFIG } from '../components/MarkTypeStrategy/MarkStrategy.tsx'
|
||||
|
||||
/**
|
||||
* Takes in an array of Marks already filtered by User.
|
||||
@ -266,22 +266,38 @@ 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
|
||||
return tuples
|
||||
}
|
||||
|
||||
export const getOptimizedPaths = (svgString: string) => {
|
||||
const regex = / d="([^"]*)"/g
|
||||
const matches = [...svgString.matchAll(regex)]
|
||||
const pathValues = matches.map((match) => match[1])
|
||||
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'
|
||||
|
||||
return pathValues
|
||||
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 {
|
||||
|
@ -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
|
||||
@ -132,17 +135,18 @@ 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])
|
||||
await embedSignaturePng(mark, pages[i], pdf)
|
||||
break
|
||||
|
||||
default:
|
||||
drawMarkText(mark, pages[i], robotoFont)
|
||||
break
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -254,18 +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 paths = JSON.parse(mark.value!)
|
||||
paths.forEach((d: string) => {
|
||||
page.drawSvgPath(d, { x, y })
|
||||
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
|
||||
})
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user