diff --git a/packages/ui/primitives/signature-pad/signature-pad.tsx b/packages/ui/primitives/signature-pad/signature-pad.tsx index 92aa8c211..dc267b3da 100644 --- a/packages/ui/primitives/signature-pad/signature-pad.tsx +++ b/packages/ui/primitives/signature-pad/signature-pad.tsx @@ -6,7 +6,7 @@ import { useEffect, useMemo, useRef, useState } from 'react'; import { Caveat } from 'next/font/google'; import { Trans } from '@lingui/macro'; -import { Undo2 } from 'lucide-react'; +import { Undo2, Upload } from 'lucide-react'; import type { StrokeOptions } from 'perfect-freehand'; import { getStroke } from 'perfect-freehand'; @@ -33,6 +33,64 @@ const fontCaveat = Caveat({ const DPI = 2; +const isBase64Image = (value: string) => value.startsWith('data:image/png;base64,'); + +const loadImage = async (file: File | undefined): Promise => { + if (!file) { + throw new Error('No file selected'); + } + + if (!file.type.startsWith('image/')) { + throw new Error('Invalid file type'); + } + + if (file.size > 5 * 1024 * 1024) { + throw new Error('Image size should be less than 5MB'); + } + + return new Promise((resolve, reject) => { + const img = new Image(); + const objectUrl = URL.createObjectURL(file); + + img.onload = () => { + URL.revokeObjectURL(objectUrl); + resolve(img); + }; + + img.onerror = () => { + URL.revokeObjectURL(objectUrl); + reject(new Error('Failed to load image')); + }; + + img.src = objectUrl; + }); +}; + +const loadImageOntoCanvas = ( + image: HTMLImageElement, + canvas: HTMLCanvasElement, + ctx: CanvasRenderingContext2D, +): ImageData => { + const scale = Math.min((canvas.width * 0.8) / image.width, (canvas.height * 0.8) / image.height); + + const x = (canvas.width - image.width * scale) / 2; + const y = (canvas.height - image.height * scale) / 2; + + ctx.clearRect(0, 0, canvas.width, canvas.height); + + ctx.save(); + ctx.imageSmoothingEnabled = true; + ctx.imageSmoothingQuality = 'high'; + + ctx.drawImage(image, x, y, image.width * scale, image.height * scale); + + ctx.restore(); + + const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); + + return imageData; +}; + export type SignaturePadProps = Omit, 'onChange'> & { onChange?: (_signatureDataUrl: string | null) => void; containerClassName?: string; @@ -52,12 +110,15 @@ export const SignaturePad = ({ }: SignaturePadProps) => { const $el = useRef(null); const $imageData = useRef(null); + const $fileInput = useRef(null); const [isPressed, setIsPressed] = useState(false); const [lines, setLines] = useState([]); const [currentLine, setCurrentLine] = useState([]); const [selectedColor, setSelectedColor] = useState('black'); - const [typedSignature, setTypedSignature] = useState(defaultValue ?? ''); + const [typedSignature, setTypedSignature] = useState( + defaultValue && !isBase64Image(defaultValue) ? defaultValue : '', + ); const perfectFreehandOptions = useMemo(() => { const size = $el.current ? Math.min($el.current.height, $el.current.width) * 0.03 : 10; @@ -80,6 +141,14 @@ export const SignaturePad = ({ setIsPressed(true); + if (typedSignature) { + setTypedSignature(''); + if ($el.current) { + const ctx = $el.current.getContext('2d'); + ctx?.clearRect(0, 0, $el.current.width, $el.current.height); + } + } + const point = Point.fromEvent(event, DPI, $el.current); setCurrentLine([point]); @@ -193,6 +262,10 @@ export const SignaturePad = ({ $imageData.current = null; } + if ($fileInput.current) { + $fileInput.current.value = ''; + } + onChange?.(null); setTypedSignature(''); @@ -255,12 +328,30 @@ export const SignaturePad = ({ } }; + const handleImageUpload = async (event: React.ChangeEvent) => { + try { + const img = await loadImage(event.target.files?.[0]); + + if (!$el.current) return; + + const ctx = $el.current.getContext('2d'); + if (!ctx) return; + + $imageData.current = loadImageOntoCanvas(img, $el.current, ctx); + onChange?.($el.current.toDataURL()); + + setLines([]); + setCurrentLine([]); + setTypedSignature(''); + } catch (error) { + console.error(error); + } + }; + useEffect(() => { - if (typedSignature.trim() !== '') { + if (typedSignature.trim() !== '' && !isBase64Image(typedSignature)) { renderTypedSignature(); onChange?.(typedSignature); - } else { - onClearClick(); } }, [typedSignature, selectedColor]); @@ -370,6 +461,26 @@ export const SignaturePad = ({ )} +
+
$fileInput.current?.click()} + > + + + + Upload Signature + +
+
+