mirror of
https://github.com/Frooodle/Stirling-PDF.git
synced 2025-11-16 01:21:16 +01:00
More robust
This commit is contained in:
parent
306117ed4b
commit
2c491c6bf4
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
15
frontend/src/core/components/tools/sign/signConstants.ts
Normal file
15
frontend/src/core/components/tools/sign/signConstants.ts
Normal 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;
|
||||||
@ -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;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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');
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user