diff --git a/frontend/src/core/components/tools/sign/SignSettings.tsx b/frontend/src/core/components/tools/sign/SignSettings.tsx index 910c2727e..7be7e62d4 100644 --- a/frontend/src/core/components/tools/sign/SignSettings.tsx +++ b/frontend/src/core/components/tools/sign/SignSettings.tsx @@ -1,10 +1,11 @@ -import { useState, useEffect, useMemo, useRef } from 'react'; +import { useState, useEffect, useMemo, useRef } from 'react'; import { useTranslation } from "react-i18next"; import { Stack, Button, Text, Alert, SegmentedControl, Divider, ActionIcon, Tooltip, Group, Box } from '@mantine/core'; import { SignParameters } from "@app/hooks/tools/sign/useSignParameters"; import { SuggestedToolsSection } from "@app/components/tools/shared/SuggestedToolsSection"; import { useSignature } from "@app/contexts/SignatureContext"; import { useViewer } from "@app/contexts/ViewerContext"; +import { PLACEMENT_ACTIVATION_DELAY, FILE_SWITCH_ACTIVATION_DELAY } from './signConstants'; // Import the new reusable components import { DrawingCanvas } from "@app/components/annotation/shared/DrawingCanvas"; @@ -174,12 +175,12 @@ const SignSettings = ({ case 'image': return imageSignatureData ?? null; case 'text': - return [ - (parameters.signerName ?? '').trim(), - parameters.fontSize ?? 16, - parameters.fontFamily ?? 'Helvetica', - parameters.textColor ?? '#000000', - ].join('|'); + return JSON.stringify({ + signerName: (parameters.signerName ?? '').trim(), + fontSize: parameters.fontSize ?? 16, + fontFamily: parameters.fontFamily ?? 'Helvetica', + textColor: parameters.textColor ?? '#000000', + }); default: return null; } @@ -355,7 +356,7 @@ const SignSettings = ({ if (typeof window !== 'undefined') { const timer = window.setTimeout(() => { onActivateSignaturePlacement?.(); - }, 60); + }, PLACEMENT_ACTIVATION_DELAY); return () => window.clearTimeout(timer); } @@ -392,7 +393,7 @@ const SignSettings = ({ }; if (typeof window !== 'undefined') { - const timer = window.setTimeout(trigger, 60); + const timer = window.setTimeout(trigger, PLACEMENT_ACTIVATION_DELAY); return () => window.clearTimeout(timer); } @@ -415,7 +416,7 @@ const SignSettings = ({ if (typeof window !== 'undefined') { const timer = window.setTimeout(() => { onActivateSignaturePlacement?.(); - }, 80); + }, FILE_SWITCH_ACTIVATION_DELAY); return () => window.clearTimeout(timer); } diff --git a/frontend/src/core/components/tools/sign/signConstants.ts b/frontend/src/core/components/tools/sign/signConstants.ts new file mode 100644 index 000000000..ade726a6c --- /dev/null +++ b/frontend/src/core/components/tools/sign/signConstants.ts @@ -0,0 +1,15 @@ +// Timeout delays (ms) to allow PDF viewer to complete rendering before activating placement mode +export const PLACEMENT_ACTIVATION_DELAY = 60; // Standard delay for signature changes +export const FILE_SWITCH_ACTIVATION_DELAY = 80; // Slightly longer delay when switching files + +// Signature preview sizing +export const MAX_PREVIEW_WIDTH_RATIO = 0.35; // Max preview width as percentage of container +export const MAX_PREVIEW_HEIGHT_RATIO = 0.35; // Max preview height as percentage of container +export const MAX_PREVIEW_WIDTH_REM = 15; // Absolute max width in rem +export const MAX_PREVIEW_HEIGHT_REM = 10; // Absolute max height in rem +export const MIN_SIGNATURE_DIMENSION_REM = 0.75; // Min dimension for visibility +export const OVERLAY_EDGE_PADDING_REM = 0.25; // Padding from container edges + +// Text signature padding (relative to font size) +export const HORIZONTAL_PADDING_RATIO = 0.8; +export const VERTICAL_PADDING_RATIO = 0.6; diff --git a/frontend/src/core/components/viewer/SignatureAPIBridge.tsx b/frontend/src/core/components/viewer/SignatureAPIBridge.tsx index 0077c3b18..c914c50a2 100644 --- a/frontend/src/core/components/viewer/SignatureAPIBridge.tsx +++ b/frontend/src/core/components/viewer/SignatureAPIBridge.tsx @@ -6,28 +6,42 @@ import type { SignatureAPI } from '@app/components/viewer/viewerTypes'; import type { SignParameters } from '@app/hooks/tools/sign/useSignParameters'; import { useViewer } from '@app/contexts/ViewerContext'; +// Minimum allowed width/height (in pixels) for a signature image or text stamp. +// This prevents rendering issues and ensures signatures are always visible and usable. const MIN_SIGNATURE_DIMENSION = 12; +// Use 2x oversampling to improve text rendering quality (anti-aliasing) when generating signature images. +// This provides a good balance between visual fidelity and performance/memory usage. const TEXT_OVERSAMPLE_FACTOR = 2; -const extractDataUrl = (value: unknown, depth = 0): string | undefined => { +const extractDataUrl = (value: unknown, depth = 0, visited: Set = new Set()): string | undefined => { if (!value || depth > 6) return undefined; + + // Prevent circular references + if (typeof value === 'object' && visited.has(value)) { + return undefined; + } + if (typeof value === 'string') { return value.startsWith('data:image') ? value : undefined; } - if (Array.isArray(value)) { - for (const entry of value) { - const result = extractDataUrl(entry, depth + 1); - if (result) return result; - } - return undefined; - } + if (typeof value === 'object') { - for (const key of Object.keys(value as Record)) { - const result = extractDataUrl((value as Record)[key], depth + 1); - if (result) return result; + visited.add(value); + + if (Array.isArray(value)) { + for (const entry of value) { + const result = extractDataUrl(entry, depth + 1, visited); + if (result) return result; + } + } else { + for (const key of Object.keys(value as Record)) { + const result = extractDataUrl((value as Record)[key], depth + 1, visited); + if (result) return result; + } } } + return undefined; }; diff --git a/frontend/src/core/components/viewer/SignaturePlacementOverlay.tsx b/frontend/src/core/components/viewer/SignaturePlacementOverlay.tsx index fd87ea51a..c483311fa 100644 --- a/frontend/src/core/components/viewer/SignaturePlacementOverlay.tsx +++ b/frontend/src/core/components/viewer/SignaturePlacementOverlay.tsx @@ -3,6 +3,17 @@ import { Box } from '@mantine/core'; import type { SignParameters } from '@app/hooks/tools/sign/useSignParameters'; import { buildSignaturePreview, SignaturePreview } from '@app/utils/signaturePreview'; import { useSignature } from '@app/contexts/SignatureContext'; +import { + MAX_PREVIEW_WIDTH_RATIO, + MAX_PREVIEW_HEIGHT_RATIO, + MAX_PREVIEW_WIDTH_REM, + MAX_PREVIEW_HEIGHT_REM, + MIN_SIGNATURE_DIMENSION_REM, + OVERLAY_EDGE_PADDING_REM, +} from '@app/components/tools/sign/signConstants'; + +// Convert rem to pixels using browser's base font size (typically 16px) +const remToPx = (rem: number) => rem * parseFloat(getComputedStyle(document.documentElement).fontSize); interface SignaturePlacementOverlayProps { containerRef: React.RefObject; @@ -78,8 +89,8 @@ export const SignaturePlacementOverlay: React.FC const containerWidth = container.clientWidth || 1; const containerHeight = container.clientHeight || 1; - const maxWidth = Math.min(containerWidth * 0.35, 240); - const maxHeight = Math.min(containerHeight * 0.35, 160); + const maxWidth = Math.min(containerWidth * MAX_PREVIEW_WIDTH_RATIO, remToPx(MAX_PREVIEW_WIDTH_REM)); + const maxHeight = Math.min(containerHeight * MAX_PREVIEW_HEIGHT_RATIO, remToPx(MAX_PREVIEW_HEIGHT_REM)); const scale = Math.min( 1, @@ -88,8 +99,8 @@ export const SignaturePlacementOverlay: React.FC ); return { - width: Math.max(12, preview.width * scale), - height: Math.max(12, preview.height * scale), + width: Math.max(remToPx(MIN_SIGNATURE_DIMENSION_REM), preview.width * scale), + height: Math.max(remToPx(MIN_SIGNATURE_DIMENSION_REM), preview.height * scale), }; }, [preview, containerRef]); @@ -118,9 +129,10 @@ export const SignaturePlacementOverlay: React.FC const width = scaledSize.width; const height = scaledSize.height; + const edgePadding = remToPx(OVERLAY_EDGE_PADDING_REM); - const clampedLeft = Math.max(4, Math.min(cursor.x - width / 2, containerWidth - width - 4)); - const clampedTop = Math.max(4, Math.min(cursor.y - height / 2, containerHeight - height - 4)); + const clampedLeft = Math.max(edgePadding, Math.min(cursor.x - width / 2, containerWidth - width - edgePadding)); + const clampedTop = Math.max(edgePadding, Math.min(cursor.y - height / 2, containerHeight - height - edgePadding)); return { left: clampedLeft, diff --git a/frontend/src/core/utils/signaturePreview.ts b/frontend/src/core/utils/signaturePreview.ts index 15666e31b..c5be5bc2d 100644 --- a/frontend/src/core/utils/signaturePreview.ts +++ b/frontend/src/core/utils/signaturePreview.ts @@ -1,4 +1,5 @@ import { SignParameters } from '@app/hooks/tools/sign/useSignParameters'; +import { HORIZONTAL_PADDING_RATIO, VERTICAL_PADDING_RATIO } from '@app/components/tools/sign/signConstants'; export interface SignaturePreview { dataUrl: string; @@ -33,8 +34,8 @@ export const buildSignaturePreview = async (config: SignParameters | null): Prom const fontFamily = config.fontFamily ?? 'Helvetica'; const textColor = config.textColor ?? '#000000'; - const paddingX = Math.round(fontSize * 0.8); - const paddingY = Math.round(fontSize * 0.6); + const paddingX = Math.round(fontSize * HORIZONTAL_PADDING_RATIO); + const paddingY = Math.round(fontSize * VERTICAL_PADDING_RATIO); const measureCanvas = document.createElement('canvas'); const measureCtx = measureCanvas.getContext('2d');