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 { 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);
}

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 { 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<unknown> = 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<string, unknown>)) {
const result = extractDataUrl((value as Record<string, unknown>)[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<string, unknown>)) {
const result = extractDataUrl((value as Record<string, unknown>)[key], depth + 1, visited);
if (result) return result;
}
}
}
return undefined;
};

View File

@ -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<HTMLElement | null>;
@ -78,8 +89,8 @@ export const SignaturePlacementOverlay: React.FC<SignaturePlacementOverlayProps>
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<SignaturePlacementOverlayProps>
);
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<SignaturePlacementOverlayProps>
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,

View File

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