More robust

This commit is contained in:
Reece 2025-11-13 20:00:30 +00:00
parent 306117ed4b
commit 2c491c6bf4
5 changed files with 72 additions and 29 deletions

View File

@ -1,10 +1,11 @@
import { useState, useEffect, useMemo, useRef } from 'react'; import { useState, useEffect, useMemo, useRef } from 'react';
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Stack, Button, Text, Alert, SegmentedControl, Divider, ActionIcon, Tooltip, Group, Box } from '@mantine/core'; import { Stack, Button, Text, Alert, SegmentedControl, Divider, ActionIcon, Tooltip, Group, Box } from '@mantine/core';
import { SignParameters } from "@app/hooks/tools/sign/useSignParameters"; import { SignParameters } from "@app/hooks/tools/sign/useSignParameters";
import { SuggestedToolsSection } from "@app/components/tools/shared/SuggestedToolsSection"; import { SuggestedToolsSection } from "@app/components/tools/shared/SuggestedToolsSection";
import { useSignature } from "@app/contexts/SignatureContext"; import { useSignature } from "@app/contexts/SignatureContext";
import { useViewer } from "@app/contexts/ViewerContext"; import { useViewer } from "@app/contexts/ViewerContext";
import { PLACEMENT_ACTIVATION_DELAY, FILE_SWITCH_ACTIVATION_DELAY } from './signConstants';
// Import the new reusable components // Import the new reusable components
import { DrawingCanvas } from "@app/components/annotation/shared/DrawingCanvas"; import { DrawingCanvas } from "@app/components/annotation/shared/DrawingCanvas";
@ -174,12 +175,12 @@ const SignSettings = ({
case 'image': case 'image':
return imageSignatureData ?? null; return imageSignatureData ?? null;
case 'text': case 'text':
return [ return JSON.stringify({
(parameters.signerName ?? '').trim(), signerName: (parameters.signerName ?? '').trim(),
parameters.fontSize ?? 16, fontSize: parameters.fontSize ?? 16,
parameters.fontFamily ?? 'Helvetica', fontFamily: parameters.fontFamily ?? 'Helvetica',
parameters.textColor ?? '#000000', textColor: parameters.textColor ?? '#000000',
].join('|'); });
default: default:
return null; return null;
} }
@ -355,7 +356,7 @@ const SignSettings = ({
if (typeof window !== 'undefined') { if (typeof window !== 'undefined') {
const timer = window.setTimeout(() => { const timer = window.setTimeout(() => {
onActivateSignaturePlacement?.(); onActivateSignaturePlacement?.();
}, 60); }, PLACEMENT_ACTIVATION_DELAY);
return () => window.clearTimeout(timer); return () => window.clearTimeout(timer);
} }
@ -392,7 +393,7 @@ const SignSettings = ({
}; };
if (typeof window !== 'undefined') { if (typeof window !== 'undefined') {
const timer = window.setTimeout(trigger, 60); const timer = window.setTimeout(trigger, PLACEMENT_ACTIVATION_DELAY);
return () => window.clearTimeout(timer); return () => window.clearTimeout(timer);
} }
@ -415,7 +416,7 @@ const SignSettings = ({
if (typeof window !== 'undefined') { if (typeof window !== 'undefined') {
const timer = window.setTimeout(() => { const timer = window.setTimeout(() => {
onActivateSignaturePlacement?.(); onActivateSignaturePlacement?.();
}, 80); }, FILE_SWITCH_ACTIVATION_DELAY);
return () => window.clearTimeout(timer); return () => window.clearTimeout(timer);
} }

View File

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

View File

@ -6,28 +6,42 @@ import type { SignatureAPI } from '@app/components/viewer/viewerTypes';
import type { SignParameters } from '@app/hooks/tools/sign/useSignParameters'; import type { SignParameters } from '@app/hooks/tools/sign/useSignParameters';
import { useViewer } from '@app/contexts/ViewerContext'; 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; 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 TEXT_OVERSAMPLE_FACTOR = 2;
const extractDataUrl = (value: unknown, depth = 0): string | undefined => { const extractDataUrl = (value: unknown, depth = 0, visited: Set<unknown> = new Set()): string | undefined => {
if (!value || depth > 6) return undefined; if (!value || depth > 6) return undefined;
// Prevent circular references
if (typeof value === 'object' && visited.has(value)) {
return undefined;
}
if (typeof value === 'string') { if (typeof value === 'string') {
return value.startsWith('data:image') ? value : undefined; 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') { if (typeof value === 'object') {
for (const key of Object.keys(value as Record<string, unknown>)) { visited.add(value);
const result = extractDataUrl((value as Record<string, unknown>)[key], depth + 1);
if (result) return result; 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<string, unknown>)) {
const result = extractDataUrl((value as Record<string, unknown>)[key], depth + 1, visited);
if (result) return result;
}
} }
} }
return undefined; return undefined;
}; };

View File

@ -3,6 +3,17 @@ import { Box } from '@mantine/core';
import type { SignParameters } from '@app/hooks/tools/sign/useSignParameters'; import type { SignParameters } from '@app/hooks/tools/sign/useSignParameters';
import { buildSignaturePreview, SignaturePreview } from '@app/utils/signaturePreview'; import { buildSignaturePreview, SignaturePreview } from '@app/utils/signaturePreview';
import { useSignature } from '@app/contexts/SignatureContext'; 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 { interface SignaturePlacementOverlayProps {
containerRef: React.RefObject<HTMLElement | null>; containerRef: React.RefObject<HTMLElement | null>;
@ -78,8 +89,8 @@ export const SignaturePlacementOverlay: React.FC<SignaturePlacementOverlayProps>
const containerWidth = container.clientWidth || 1; const containerWidth = container.clientWidth || 1;
const containerHeight = container.clientHeight || 1; const containerHeight = container.clientHeight || 1;
const maxWidth = Math.min(containerWidth * 0.35, 240); const maxWidth = Math.min(containerWidth * MAX_PREVIEW_WIDTH_RATIO, remToPx(MAX_PREVIEW_WIDTH_REM));
const maxHeight = Math.min(containerHeight * 0.35, 160); const maxHeight = Math.min(containerHeight * MAX_PREVIEW_HEIGHT_RATIO, remToPx(MAX_PREVIEW_HEIGHT_REM));
const scale = Math.min( const scale = Math.min(
1, 1,
@ -88,8 +99,8 @@ export const SignaturePlacementOverlay: React.FC<SignaturePlacementOverlayProps>
); );
return { return {
width: Math.max(12, preview.width * scale), width: Math.max(remToPx(MIN_SIGNATURE_DIMENSION_REM), preview.width * scale),
height: Math.max(12, preview.height * scale), height: Math.max(remToPx(MIN_SIGNATURE_DIMENSION_REM), preview.height * scale),
}; };
}, [preview, containerRef]); }, [preview, containerRef]);
@ -118,9 +129,10 @@ export const SignaturePlacementOverlay: React.FC<SignaturePlacementOverlayProps>
const width = scaledSize.width; const width = scaledSize.width;
const height = scaledSize.height; 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 clampedLeft = Math.max(edgePadding, Math.min(cursor.x - width / 2, containerWidth - width - edgePadding));
const clampedTop = Math.max(4, Math.min(cursor.y - height / 2, containerHeight - height - 4)); const clampedTop = Math.max(edgePadding, Math.min(cursor.y - height / 2, containerHeight - height - edgePadding));
return { return {
left: clampedLeft, left: clampedLeft,

View File

@ -1,4 +1,5 @@
import { SignParameters } from '@app/hooks/tools/sign/useSignParameters'; import { SignParameters } from '@app/hooks/tools/sign/useSignParameters';
import { HORIZONTAL_PADDING_RATIO, VERTICAL_PADDING_RATIO } from '@app/components/tools/sign/signConstants';
export interface SignaturePreview { export interface SignaturePreview {
dataUrl: string; dataUrl: string;
@ -33,8 +34,8 @@ export const buildSignaturePreview = async (config: SignParameters | null): Prom
const fontFamily = config.fontFamily ?? 'Helvetica'; const fontFamily = config.fontFamily ?? 'Helvetica';
const textColor = config.textColor ?? '#000000'; const textColor = config.textColor ?? '#000000';
const paddingX = Math.round(fontSize * 0.8); const paddingX = Math.round(fontSize * HORIZONTAL_PADDING_RATIO);
const paddingY = Math.round(fontSize * 0.6); const paddingY = Math.round(fontSize * VERTICAL_PADDING_RATIO);
const measureCanvas = document.createElement('canvas'); const measureCanvas = document.createElement('canvas');
const measureCtx = measureCanvas.getContext('2d'); const measureCtx = measureCanvas.getContext('2d');