Feature/v2/saved signatures (#4899)

Add Saved signature functionality
This commit is contained in:
Reece Browne
2025-11-19 17:30:01 +00:00
committed by GitHub
parent 0f73a1cf13
commit dd1c653301
11 changed files with 951 additions and 91 deletions
@@ -2,8 +2,9 @@ import { useImperativeHandle, forwardRef, useEffect, useRef } from 'react';
import { useHistoryCapability } from '@embedpdf/plugin-history/react';
import { useAnnotationCapability } from '@embedpdf/plugin-annotation/react';
import { useSignature } from '@app/contexts/SignatureContext';
import { uuidV4 } from '@embedpdf/models';
import { PdfAnnotationSubtype, uuidV4 } from '@embedpdf/models';
import type { HistoryAPI } from '@app/components/viewer/viewerTypes';
import { ANNOTATION_RECREATION_DELAY_MS, ANNOTATION_VERIFICATION_DELAY_MS } from '@app/constants/app';
export const HistoryAPIBridge = forwardRef<HistoryAPI>(function HistoryAPIBridge(_, ref) {
const { provides: historyApi } = useHistoryCapability();
@@ -19,14 +20,14 @@ export const HistoryAPIBridge = forwardRef<HistoryAPI>(function HistoryAPIBridge
const annotation = event.annotation;
// Store image data for all STAMP annotations immediately when created or modified
if (annotation && annotation.type === 13 && annotation.id && annotation.imageSrc) {
if (annotation && annotation.type === PdfAnnotationSubtype.STAMP && annotation.id && annotation.imageSrc) {
const storedImageData = getImageData(annotation.id);
if (!storedImageData) {
storeImageData(annotation.id, annotation.imageSrc);
}
}
if (annotation && annotation.type === 13 && annotation.id) {
if (annotation && annotation.type === PdfAnnotationSubtype.STAMP && annotation.id) {
// Prevent infinite loops when we recreate annotations
if (restoringIds.current.has(annotation.id)) {
restoringIds.current.delete(annotation.id);
@@ -59,7 +60,7 @@ export const HistoryAPIBridge = forwardRef<HistoryAPI>(function HistoryAPIBridge
data: storedImageData,
appearance: storedImageData,
});
}, 50);
}, ANNOTATION_RECREATION_DELAY_MS);
} catch (restoreError) {
console.error('HistoryAPI: Failed to restore cropped signature:', restoreError);
}
@@ -70,7 +71,7 @@ export const HistoryAPIBridge = forwardRef<HistoryAPI>(function HistoryAPIBridge
// Handle annotation restoration after undo operations
if (event.type === 'create' && event.committed) {
// Check if this is a STAMP annotation (signature) that might need image data restoration
if (annotation && annotation.type === 13 && annotation.id) {
if (annotation && annotation.type === PdfAnnotationSubtype.STAMP && annotation.id) {
getImageData(annotation.id);
// Delay the check to allow the annotation to be fully created
@@ -103,12 +104,12 @@ export const HistoryAPIBridge = forwardRef<HistoryAPI>(function HistoryAPIBridge
// Small delay to ensure deletion completes
setTimeout(() => {
annotationApi.createAnnotation(event.pageIndex, restoredData);
}, 50);
}, ANNOTATION_RECREATION_DELAY_MS);
} catch (error) {
console.error('HistoryAPI: Failed to restore annotation:', error);
}
}
}, 100);
}, ANNOTATION_VERIFICATION_DELAY_MS);
}
}
};
@@ -43,23 +43,32 @@ export function PdfViewerToolbar({
// Register for immediate scroll updates and sync with actual scroll state
useEffect(() => {
registerImmediateScrollUpdate((currentPage, _totalPages) => {
const unregister = registerImmediateScrollUpdate((currentPage, _totalPages) => {
setPageInput(currentPage);
});
setPageInput(scrollState.currentPage);
}, [registerImmediateScrollUpdate]);
return () => {
unregister?.();
};
}, [registerImmediateScrollUpdate, scrollState.currentPage]);
// Register for immediate zoom updates and sync with actual zoom state
useEffect(() => {
registerImmediateZoomUpdate(setDisplayZoomPercent);
const unregister = registerImmediateZoomUpdate(setDisplayZoomPercent);
setDisplayZoomPercent(zoomState.zoomPercent || 140);
}, [zoomState.zoomPercent, registerImmediateZoomUpdate]);
return () => {
unregister?.();
};
}, [registerImmediateZoomUpdate, zoomState.zoomPercent]);
useEffect(() => {
registerImmediateSpreadUpdate((_mode, isDual) => {
const unregister = registerImmediateSpreadUpdate((_mode, isDual) => {
setIsDualPageActive(isDual);
});
setIsDualPageActive(spreadState.isDualPage);
return () => {
unregister?.();
};
}, [registerImmediateSpreadUpdate, spreadState.isDualPage]);
const handleZoomOut = () => {
@@ -1,4 +1,4 @@
import { useImperativeHandle, forwardRef, useEffect, useCallback, useRef } from 'react';
import { useImperativeHandle, forwardRef, useEffect, useCallback, useRef, useState } from 'react';
import { useAnnotationCapability } from '@embedpdf/plugin-annotation/react';
import { PdfAnnotationSubtype, uuidV4 } from '@embedpdf/models';
import { useSignature } from '@app/contexts/SignatureContext';
@@ -14,6 +14,14 @@ const MIN_SIGNATURE_DIMENSION = 12;
// This provides a good balance between visual fidelity and performance/memory usage.
const TEXT_OVERSAMPLE_FACTOR = 2;
type TextStampImageResult = {
dataUrl: string;
pixelWidth: number;
pixelHeight: number;
displayWidth: number;
displayHeight: number;
};
const extractDataUrl = (value: unknown, depth = 0, visited: Set<unknown> = new Set()): string | undefined => {
if (!value || depth > 6) return undefined;
@@ -48,7 +56,7 @@ const extractDataUrl = (value: unknown, depth = 0, visited: Set<unknown> = new S
const createTextStampImage = (
config: SignParameters,
displaySize?: { width: number; height: number } | null
): { dataUrl: string; pixelWidth: number; pixelHeight: number; displayWidth: number; displayHeight: number } | null => {
): TextStampImageResult | null => {
const text = (config.signerName ?? '').trim();
if (!text) {
return null;
@@ -123,10 +131,20 @@ const createTextStampImage = (
export const SignatureAPIBridge = forwardRef<SignatureAPI>(function SignatureAPIBridge(_, ref) {
const { provides: annotationApi } = useAnnotationCapability();
const { signatureConfig, storeImageData, isPlacementMode, placementPreviewSize } = useSignature();
const { getZoomState } = useViewer();
const currentZoom = getZoomState()?.currentZoom ?? 1;
const { getZoomState, registerImmediateZoomUpdate } = useViewer();
const [currentZoom, setCurrentZoom] = useState(() => getZoomState()?.currentZoom ?? 1);
const lastStampImageRef = useRef<string | null>(null);
useEffect(() => {
setCurrentZoom(getZoomState()?.currentZoom ?? 1);
const unregister = registerImmediateZoomUpdate(percent => {
setCurrentZoom(Math.max(percent / 100, 0.01));
});
return () => {
unregister?.();
};
}, [getZoomState, registerImmediateZoomUpdate]);
const cssToPdfSize = useCallback(
(size: { width: number; height: number }) => {
const zoom = currentZoom || 1;
@@ -328,7 +346,7 @@ export const SignatureAPIBridge = forwardRef<SignatureAPI>(function SignatureAPI
if (pageAnnotationsTask) {
pageAnnotationsTask.toPromise().then((pageAnnotations: any) => {
const annotation = pageAnnotations?.find((ann: any) => ann.id === annotationId);
if (annotation && annotation.type === 13 && annotation.imageSrc) {
if (annotation && annotation.type === PdfAnnotationSubtype.STAMP && annotation.imageSrc) {
// Store image data before deletion
storeImageData(annotationId, annotation.imageSrc);
}