mirror of
https://github.com/Frooodle/Stirling-PDF.git
synced 2025-11-16 01:21:16 +01:00
Feature/v2/improve sign (#4627)
# Description of Changes <!-- Please provide a summary of the changes, including: - What was changed - Why the change was made - Any challenges encountered Closes #(issue_number) --> --- ## Checklist ### General - [ ] I have read the [Contribution Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md) - [ ] I have read the [Stirling-PDF Developer Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md) (if applicable) - [ ] I have read the [How to add new languages to Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md) (if applicable) - [ ] I have performed a self-review of my own code - [ ] My changes generate no new warnings ### Documentation - [ ] I have updated relevant docs on [Stirling-PDF's doc repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/) (if functionality has heavily changed) - [ ] I have read the section [Add New Translation Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags) (for new translation tags only) ### UI Changes (if applicable) - [ ] Screenshots or videos demonstrating the UI changes are attached (e.g., as comments or direct attachments in the PR) ### Testing (if applicable) - [ ] I have tested my changes locally. Refer to the [Testing Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing) for more details. --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: James Brunton <james@stirlingpdf.com> Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
parent
2158ee4db6
commit
b695e3900e
7
frontend/package-lock.json
generated
7
frontend/package-lock.json
generated
@ -54,6 +54,7 @@
|
|||||||
"react-dom": "^19.1.1",
|
"react-dom": "^19.1.1",
|
||||||
"react-i18next": "^15.7.3",
|
"react-i18next": "^15.7.3",
|
||||||
"react-router-dom": "^7.9.1",
|
"react-router-dom": "^7.9.1",
|
||||||
|
"signature_pad": "^5.0.4",
|
||||||
"tailwindcss": "^4.1.13",
|
"tailwindcss": "^4.1.13",
|
||||||
"web-vitals": "^5.1.0"
|
"web-vitals": "^5.1.0"
|
||||||
},
|
},
|
||||||
@ -9992,6 +9993,12 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
"node_modules/signature_pad": {
|
||||||
|
"version": "5.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/signature_pad/-/signature_pad-5.1.1.tgz",
|
||||||
|
"integrity": "sha512-BT5JJygS5BS0oV+tffPRorIud6q17bM7v/1LdQwd0o6mTqGoI25yY1NjSL99OqkekWltS4uon6p52Y8j1Zqu7g==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/slash": {
|
"node_modules/slash": {
|
||||||
"version": "5.1.0",
|
"version": "5.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/slash/-/slash-5.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/slash/-/slash-5.1.0.tgz",
|
||||||
|
|||||||
@ -50,6 +50,7 @@
|
|||||||
"react-dom": "^19.1.1",
|
"react-dom": "^19.1.1",
|
||||||
"react-i18next": "^15.7.3",
|
"react-i18next": "^15.7.3",
|
||||||
"react-router-dom": "^7.9.1",
|
"react-router-dom": "^7.9.1",
|
||||||
|
"signature_pad": "^5.0.4",
|
||||||
"tailwindcss": "^4.1.13",
|
"tailwindcss": "^4.1.13",
|
||||||
"web-vitals": "^5.1.0"
|
"web-vitals": "^5.1.0"
|
||||||
},
|
},
|
||||||
|
|||||||
@ -1819,8 +1819,16 @@
|
|||||||
"placeholder": "Enter your full name"
|
"placeholder": "Enter your full name"
|
||||||
},
|
},
|
||||||
"instructions": {
|
"instructions": {
|
||||||
"title": "How to add signature"
|
"title": "How to add signature",
|
||||||
|
"canvas": "After drawing your signature in the canvas, close the modal then click anywhere on the PDF to place it.",
|
||||||
|
"image": "After uploading your signature image above, click anywhere on the PDF to place it.",
|
||||||
|
"text": "After entering your name above, click anywhere on the PDF to place your signature."
|
||||||
},
|
},
|
||||||
|
"mode": {
|
||||||
|
"move": "Move Signature",
|
||||||
|
"place": "Place Signature"
|
||||||
|
},
|
||||||
|
"updateAndPlace": "Update and Place",
|
||||||
"activate": "Activate Signature Placement",
|
"activate": "Activate Signature Placement",
|
||||||
"deactivate": "Stop Placing Signatures",
|
"deactivate": "Stop Placing Signatures",
|
||||||
"results": {
|
"results": {
|
||||||
|
|||||||
@ -1,7 +1,8 @@
|
|||||||
import React, { useRef, useState, useCallback } from 'react';
|
import React, { useRef, useState } from 'react';
|
||||||
import { Paper, Group, Button, Modal, Stack, Text } from '@mantine/core';
|
import { Paper, Button, Modal, Stack, Text, Popover, ColorPicker as MantineColorPicker } from '@mantine/core';
|
||||||
import { ColorSwatchButton } from './ColorPicker';
|
import { ColorSwatchButton } from './ColorPicker';
|
||||||
import PenSizeSelector from '../../tools/sign/PenSizeSelector';
|
import PenSizeSelector from '../../tools/sign/PenSizeSelector';
|
||||||
|
import SignaturePad from 'signature_pad';
|
||||||
|
|
||||||
interface DrawingCanvasProps {
|
interface DrawingCanvasProps {
|
||||||
selectedColor: string;
|
selectedColor: string;
|
||||||
@ -11,6 +12,7 @@ interface DrawingCanvasProps {
|
|||||||
onPenSizeChange: (size: number) => void;
|
onPenSizeChange: (size: number) => void;
|
||||||
onPenSizeInputChange: (input: string) => void;
|
onPenSizeInputChange: (input: string) => void;
|
||||||
onSignatureDataChange: (data: string | null) => void;
|
onSignatureDataChange: (data: string | null) => void;
|
||||||
|
onDrawingComplete?: () => void;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
width?: number;
|
width?: number;
|
||||||
height?: number;
|
height?: number;
|
||||||
@ -27,411 +29,253 @@ export const DrawingCanvas: React.FC<DrawingCanvasProps> = ({
|
|||||||
onPenSizeChange,
|
onPenSizeChange,
|
||||||
onPenSizeInputChange,
|
onPenSizeInputChange,
|
||||||
onSignatureDataChange,
|
onSignatureDataChange,
|
||||||
|
onDrawingComplete,
|
||||||
disabled = false,
|
disabled = false,
|
||||||
width = 400,
|
width = 400,
|
||||||
height = 150,
|
height = 150,
|
||||||
modalWidth = 800,
|
|
||||||
modalHeight = 400,
|
|
||||||
additionalButtons
|
|
||||||
}) => {
|
}) => {
|
||||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
const previewCanvasRef = useRef<HTMLCanvasElement>(null);
|
||||||
const modalCanvasRef = useRef<HTMLCanvasElement>(null);
|
const modalCanvasRef = useRef<HTMLCanvasElement>(null);
|
||||||
const visibleModalCanvasRef = useRef<HTMLCanvasElement>(null);
|
const padRef = useRef<SignaturePad | null>(null);
|
||||||
|
const [modalOpen, setModalOpen] = useState(false);
|
||||||
|
const [colorPickerOpen, setColorPickerOpen] = useState(false);
|
||||||
|
|
||||||
const [isDrawing, setIsDrawing] = useState(false);
|
const initPad = (canvas: HTMLCanvasElement) => {
|
||||||
const [isModalDrawing, setIsModalDrawing] = useState(false);
|
if (!padRef.current) {
|
||||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
const rect = canvas.getBoundingClientRect();
|
||||||
|
canvas.width = rect.width;
|
||||||
|
canvas.height = rect.height;
|
||||||
|
|
||||||
// Drawing functions for main canvas
|
padRef.current = new SignaturePad(canvas, {
|
||||||
const startDrawing = useCallback((e: React.MouseEvent<HTMLCanvasElement>) => {
|
penColor: selectedColor,
|
||||||
if (!canvasRef.current || disabled) return;
|
minWidth: penSize * 0.5,
|
||||||
|
maxWidth: penSize * 2.5,
|
||||||
setIsDrawing(true);
|
throttle: 10,
|
||||||
const rect = canvasRef.current.getBoundingClientRect();
|
minDistance: 5,
|
||||||
const scaleX = canvasRef.current.width / rect.width;
|
velocityFilterWeight: 0.7,
|
||||||
const scaleY = canvasRef.current.height / rect.height;
|
});
|
||||||
const x = (e.clientX - rect.left) * scaleX;
|
|
||||||
const y = (e.clientY - rect.top) * scaleY;
|
|
||||||
|
|
||||||
const ctx = canvasRef.current.getContext('2d');
|
|
||||||
if (ctx) {
|
|
||||||
ctx.strokeStyle = selectedColor;
|
|
||||||
ctx.lineWidth = penSize;
|
|
||||||
ctx.lineCap = 'round';
|
|
||||||
ctx.lineJoin = 'round';
|
|
||||||
ctx.beginPath();
|
|
||||||
ctx.moveTo(x, y);
|
|
||||||
}
|
}
|
||||||
}, [disabled, selectedColor, penSize]);
|
};
|
||||||
|
|
||||||
const draw = useCallback((e: React.MouseEvent<HTMLCanvasElement>) => {
|
const openModal = () => {
|
||||||
if (!isDrawing || !canvasRef.current || disabled) return;
|
// Clear pad ref so it reinitializes
|
||||||
|
if (padRef.current) {
|
||||||
const rect = canvasRef.current.getBoundingClientRect();
|
padRef.current.off();
|
||||||
const scaleX = canvasRef.current.width / rect.width;
|
padRef.current = null;
|
||||||
const scaleY = canvasRef.current.height / rect.height;
|
|
||||||
const x = (e.clientX - rect.left) * scaleX;
|
|
||||||
const y = (e.clientY - rect.top) * scaleY;
|
|
||||||
|
|
||||||
const ctx = canvasRef.current.getContext('2d');
|
|
||||||
if (ctx) {
|
|
||||||
ctx.lineTo(x, y);
|
|
||||||
ctx.stroke();
|
|
||||||
}
|
}
|
||||||
}, [isDrawing, disabled]);
|
setModalOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
const stopDrawing = useCallback(() => {
|
const trimCanvas = (canvas: HTMLCanvasElement): string => {
|
||||||
if (!isDrawing || disabled) return;
|
const ctx = canvas.getContext('2d');
|
||||||
|
if (!ctx) return canvas.toDataURL('image/png');
|
||||||
|
|
||||||
setIsDrawing(false);
|
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
||||||
|
const pixels = imageData.data;
|
||||||
|
|
||||||
// Save canvas as signature data
|
let minX = canvas.width, minY = canvas.height, maxX = 0, maxY = 0;
|
||||||
if (canvasRef.current) {
|
|
||||||
const dataURL = canvasRef.current.toDataURL('image/png');
|
|
||||||
onSignatureDataChange(dataURL);
|
|
||||||
}
|
|
||||||
}, [isDrawing, disabled, onSignatureDataChange]);
|
|
||||||
|
|
||||||
// Modal canvas drawing functions
|
// Find bounds of non-transparent pixels
|
||||||
const startModalDrawing = useCallback((e: React.MouseEvent<HTMLCanvasElement>) => {
|
for (let y = 0; y < canvas.height; y++) {
|
||||||
if (!visibleModalCanvasRef.current || !modalCanvasRef.current) return;
|
for (let x = 0; x < canvas.width; x++) {
|
||||||
|
const alpha = pixels[(y * canvas.width + x) * 4 + 3];
|
||||||
setIsModalDrawing(true);
|
if (alpha > 0) {
|
||||||
const rect = visibleModalCanvasRef.current.getBoundingClientRect();
|
if (x < minX) minX = x;
|
||||||
const scaleX = visibleModalCanvasRef.current.width / rect.width;
|
if (x > maxX) maxX = x;
|
||||||
const scaleY = visibleModalCanvasRef.current.height / rect.height;
|
if (y < minY) minY = y;
|
||||||
const x = (e.clientX - rect.left) * scaleX;
|
if (y > maxY) maxY = y;
|
||||||
const y = (e.clientY - rect.top) * scaleY;
|
|
||||||
|
|
||||||
// Draw on both the visible modal canvas and hidden canvas
|
|
||||||
const visibleCtx = visibleModalCanvasRef.current.getContext('2d');
|
|
||||||
const hiddenCtx = modalCanvasRef.current.getContext('2d');
|
|
||||||
|
|
||||||
[visibleCtx, hiddenCtx].forEach(ctx => {
|
|
||||||
if (ctx) {
|
|
||||||
ctx.strokeStyle = selectedColor;
|
|
||||||
ctx.lineWidth = penSize;
|
|
||||||
ctx.lineCap = 'round';
|
|
||||||
ctx.lineJoin = 'round';
|
|
||||||
ctx.beginPath();
|
|
||||||
ctx.moveTo(x, y);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}, [selectedColor, penSize]);
|
|
||||||
|
|
||||||
const drawModal = useCallback((e: React.MouseEvent<HTMLCanvasElement>) => {
|
|
||||||
if (!isModalDrawing || !visibleModalCanvasRef.current || !modalCanvasRef.current) return;
|
|
||||||
|
|
||||||
const rect = visibleModalCanvasRef.current.getBoundingClientRect();
|
|
||||||
const scaleX = visibleModalCanvasRef.current.width / rect.width;
|
|
||||||
const scaleY = visibleModalCanvasRef.current.height / rect.height;
|
|
||||||
const x = (e.clientX - rect.left) * scaleX;
|
|
||||||
const y = (e.clientY - rect.top) * scaleY;
|
|
||||||
|
|
||||||
// Draw on both canvases
|
|
||||||
const visibleCtx = visibleModalCanvasRef.current.getContext('2d');
|
|
||||||
const hiddenCtx = modalCanvasRef.current.getContext('2d');
|
|
||||||
|
|
||||||
[visibleCtx, hiddenCtx].forEach(ctx => {
|
|
||||||
if (ctx) {
|
|
||||||
ctx.lineTo(x, y);
|
|
||||||
ctx.stroke();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}, [isModalDrawing]);
|
|
||||||
|
|
||||||
const stopModalDrawing = useCallback(() => {
|
|
||||||
if (!isModalDrawing) return;
|
|
||||||
setIsModalDrawing(false);
|
|
||||||
|
|
||||||
// Sync the canvases and update signature data (only when drawing stops)
|
|
||||||
if (modalCanvasRef.current) {
|
|
||||||
const dataURL = modalCanvasRef.current.toDataURL('image/png');
|
|
||||||
onSignatureDataChange(dataURL);
|
|
||||||
|
|
||||||
// Also update the small canvas display
|
|
||||||
if (canvasRef.current) {
|
|
||||||
const smallCtx = canvasRef.current.getContext('2d');
|
|
||||||
if (smallCtx) {
|
|
||||||
const img = new Image();
|
|
||||||
img.onload = () => {
|
|
||||||
smallCtx.clearRect(0, 0, canvasRef.current!.width, canvasRef.current!.height);
|
|
||||||
smallCtx.drawImage(img, 0, 0, canvasRef.current!.width, canvasRef.current!.height);
|
|
||||||
};
|
|
||||||
img.src = dataURL;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [isModalDrawing]);
|
|
||||||
|
|
||||||
// Clear canvas functions
|
const trimWidth = maxX - minX + 1;
|
||||||
const clearCanvas = useCallback(() => {
|
const trimHeight = maxY - minY + 1;
|
||||||
if (!canvasRef.current || disabled) return;
|
|
||||||
|
|
||||||
const ctx = canvasRef.current.getContext('2d');
|
// Create trimmed canvas
|
||||||
if (ctx) {
|
const trimmedCanvas = document.createElement('canvas');
|
||||||
ctx.clearRect(0, 0, canvasRef.current.width, canvasRef.current.height);
|
trimmedCanvas.width = trimWidth;
|
||||||
|
trimmedCanvas.height = trimHeight;
|
||||||
// Also clear the modal canvas if it exists
|
const trimmedCtx = trimmedCanvas.getContext('2d');
|
||||||
if (modalCanvasRef.current) {
|
if (trimmedCtx) {
|
||||||
const modalCtx = modalCanvasRef.current.getContext('2d');
|
trimmedCtx.drawImage(canvas, minX, minY, trimWidth, trimHeight, 0, 0, trimWidth, trimHeight);
|
||||||
if (modalCtx) {
|
|
||||||
modalCtx.clearRect(0, 0, modalCanvasRef.current.width, modalCanvasRef.current.height);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onSignatureDataChange(null);
|
|
||||||
}
|
|
||||||
}, [disabled]);
|
|
||||||
|
|
||||||
const clearModalCanvas = useCallback(() => {
|
|
||||||
// Clear both modal canvases (visible and hidden)
|
|
||||||
if (modalCanvasRef.current) {
|
|
||||||
const hiddenCtx = modalCanvasRef.current.getContext('2d');
|
|
||||||
if (hiddenCtx) {
|
|
||||||
hiddenCtx.clearRect(0, 0, modalCanvasRef.current.width, modalCanvasRef.current.height);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (visibleModalCanvasRef.current) {
|
return trimmedCanvas.toDataURL('image/png');
|
||||||
const visibleCtx = visibleModalCanvasRef.current.getContext('2d');
|
};
|
||||||
if (visibleCtx) {
|
|
||||||
visibleCtx.clearRect(0, 0, visibleModalCanvasRef.current.width, visibleModalCanvasRef.current.height);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Also clear the main canvas and signature data
|
const closeModal = () => {
|
||||||
if (canvasRef.current) {
|
if (padRef.current && !padRef.current.isEmpty()) {
|
||||||
const mainCtx = canvasRef.current.getContext('2d');
|
const canvas = modalCanvasRef.current;
|
||||||
if (mainCtx) {
|
if (canvas) {
|
||||||
mainCtx.clearRect(0, 0, canvasRef.current.width, canvasRef.current.height);
|
const trimmedPng = trimCanvas(canvas);
|
||||||
}
|
onSignatureDataChange(trimmedPng);
|
||||||
}
|
|
||||||
|
|
||||||
onSignatureDataChange(null);
|
// Update preview canvas with proper aspect ratio
|
||||||
}, []);
|
|
||||||
|
|
||||||
const saveModalSignature = useCallback(() => {
|
|
||||||
if (!modalCanvasRef.current) return;
|
|
||||||
|
|
||||||
const dataURL = modalCanvasRef.current.toDataURL('image/png');
|
|
||||||
onSignatureDataChange(dataURL);
|
|
||||||
|
|
||||||
// Copy to small canvas for display
|
|
||||||
if (canvasRef.current) {
|
|
||||||
const ctx = canvasRef.current.getContext('2d');
|
|
||||||
if (ctx) {
|
|
||||||
const img = new Image();
|
const img = new Image();
|
||||||
img.onload = () => {
|
img.onload = () => {
|
||||||
ctx.clearRect(0, 0, canvasRef.current!.width, canvasRef.current!.height);
|
if (previewCanvasRef.current) {
|
||||||
ctx.drawImage(img, 0, 0, canvasRef.current!.width, canvasRef.current!.height);
|
const ctx = previewCanvasRef.current.getContext('2d');
|
||||||
|
if (ctx) {
|
||||||
|
ctx.clearRect(0, 0, previewCanvasRef.current.width, previewCanvasRef.current.height);
|
||||||
|
|
||||||
|
// Calculate scaling to fit within preview canvas while maintaining aspect ratio
|
||||||
|
const scale = Math.min(
|
||||||
|
previewCanvasRef.current.width / img.width,
|
||||||
|
previewCanvasRef.current.height / img.height
|
||||||
|
);
|
||||||
|
const scaledWidth = img.width * scale;
|
||||||
|
const scaledHeight = img.height * scale;
|
||||||
|
const x = (previewCanvasRef.current.width - scaledWidth) / 2;
|
||||||
|
const y = (previewCanvasRef.current.height - scaledHeight) / 2;
|
||||||
|
|
||||||
|
ctx.drawImage(img, x, y, scaledWidth, scaledHeight);
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
img.src = dataURL;
|
img.src = trimmedPng;
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setIsModalOpen(false);
|
if (onDrawingComplete) {
|
||||||
}, []);
|
onDrawingComplete();
|
||||||
|
|
||||||
const openModal = useCallback(() => {
|
|
||||||
setIsModalOpen(true);
|
|
||||||
// Copy content to modal canvas after a brief delay
|
|
||||||
setTimeout(() => {
|
|
||||||
if (visibleModalCanvasRef.current && modalCanvasRef.current) {
|
|
||||||
const visibleCtx = visibleModalCanvasRef.current.getContext('2d');
|
|
||||||
if (visibleCtx) {
|
|
||||||
visibleCtx.strokeStyle = selectedColor;
|
|
||||||
visibleCtx.lineWidth = penSize;
|
|
||||||
visibleCtx.lineCap = 'round';
|
|
||||||
visibleCtx.lineJoin = 'round';
|
|
||||||
visibleCtx.clearRect(0, 0, visibleModalCanvasRef.current.width, visibleModalCanvasRef.current.height);
|
|
||||||
visibleCtx.drawImage(modalCanvasRef.current, 0, 0, visibleModalCanvasRef.current.width, visibleModalCanvasRef.current.height);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, 300);
|
}
|
||||||
}, [selectedColor, penSize]);
|
if (padRef.current) {
|
||||||
|
padRef.current.off();
|
||||||
|
padRef.current = null;
|
||||||
|
}
|
||||||
|
setModalOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
// Initialize canvas settings whenever color or pen size changes
|
const clear = () => {
|
||||||
React.useEffect(() => {
|
if (padRef.current) {
|
||||||
const updateCanvas = (canvas: HTMLCanvasElement | null) => {
|
padRef.current.clear();
|
||||||
if (!canvas) return;
|
}
|
||||||
const ctx = canvas.getContext('2d');
|
if (previewCanvasRef.current) {
|
||||||
|
const ctx = previewCanvasRef.current.getContext('2d');
|
||||||
if (ctx) {
|
if (ctx) {
|
||||||
ctx.strokeStyle = selectedColor;
|
ctx.clearRect(0, 0, previewCanvasRef.current.width, previewCanvasRef.current.height);
|
||||||
ctx.lineWidth = penSize;
|
|
||||||
ctx.lineCap = 'round';
|
|
||||||
ctx.lineJoin = 'round';
|
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
onSignatureDataChange(null);
|
||||||
|
};
|
||||||
|
|
||||||
updateCanvas(canvasRef.current);
|
const updatePenColor = (color: string) => {
|
||||||
updateCanvas(modalCanvasRef.current);
|
if (padRef.current) {
|
||||||
updateCanvas(visibleModalCanvasRef.current);
|
padRef.current.penColor = color;
|
||||||
}, [selectedColor, penSize]);
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const updatePenSize = (size: number) => {
|
||||||
|
if (padRef.current) {
|
||||||
|
padRef.current.minWidth = size * 0.8;
|
||||||
|
padRef.current.maxWidth = size * 1.2;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Paper withBorder p="md">
|
<Paper withBorder p="md">
|
||||||
<Stack gap="sm">
|
<Stack gap="sm">
|
||||||
<Group justify="space-between">
|
<Text fw={500}>Draw your signature</Text>
|
||||||
<Text fw={500}>Draw your signature</Text>
|
|
||||||
<Group gap="lg">
|
|
||||||
<div>
|
|
||||||
<Text size="sm" fw={500} mb="xs" ta="center">Color</Text>
|
|
||||||
<Group justify="center">
|
|
||||||
<ColorSwatchButton
|
|
||||||
color={selectedColor}
|
|
||||||
onClick={onColorSwatchClick}
|
|
||||||
/>
|
|
||||||
</Group>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<Text size="sm" fw={500} mb="xs">Pen Size</Text>
|
|
||||||
<PenSizeSelector
|
|
||||||
value={penSize}
|
|
||||||
inputValue={penSizeInput}
|
|
||||||
onValueChange={onPenSizeChange}
|
|
||||||
onInputChange={onPenSizeInputChange}
|
|
||||||
disabled={disabled}
|
|
||||||
placeholder="Size"
|
|
||||||
size="compact-sm"
|
|
||||||
style={{ width: '60px' }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div style={{ paddingTop: '24px' }}>
|
|
||||||
<Button
|
|
||||||
variant="light"
|
|
||||||
size="compact-sm"
|
|
||||||
onClick={openModal}
|
|
||||||
disabled={disabled}
|
|
||||||
>
|
|
||||||
Expand
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</Group>
|
|
||||||
</Group>
|
|
||||||
<canvas
|
<canvas
|
||||||
ref={canvasRef}
|
ref={previewCanvasRef}
|
||||||
width={width}
|
width={width}
|
||||||
height={height}
|
height={height}
|
||||||
style={{
|
style={{
|
||||||
border: '1px solid #ccc',
|
border: '1px solid #ccc',
|
||||||
borderRadius: '4px',
|
borderRadius: '4px',
|
||||||
cursor: disabled ? 'default' : 'crosshair',
|
cursor: disabled ? 'default' : 'pointer',
|
||||||
backgroundColor: '#ffffff',
|
backgroundColor: '#ffffff',
|
||||||
width: '100%',
|
width: '100%',
|
||||||
}}
|
}}
|
||||||
onMouseDown={startDrawing}
|
onClick={disabled ? undefined : openModal}
|
||||||
onMouseMove={draw}
|
|
||||||
onMouseUp={stopDrawing}
|
|
||||||
onMouseLeave={stopDrawing}
|
|
||||||
/>
|
/>
|
||||||
<Group justify="space-between">
|
<Text size="sm" c="dimmed" ta="center">
|
||||||
<div>
|
Click to open drawing canvas
|
||||||
{additionalButtons}
|
</Text>
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
variant="subtle"
|
|
||||||
color="red"
|
|
||||||
size="compact-sm"
|
|
||||||
onClick={clearCanvas}
|
|
||||||
disabled={disabled}
|
|
||||||
>
|
|
||||||
Clear
|
|
||||||
</Button>
|
|
||||||
</Group>
|
|
||||||
</Stack>
|
</Stack>
|
||||||
</Paper>
|
</Paper>
|
||||||
|
|
||||||
{/* Hidden canvas for modal synchronization */}
|
<Modal opened={modalOpen} onClose={closeModal} title="Draw Your Signature" size="auto" centered>
|
||||||
<canvas
|
|
||||||
ref={modalCanvasRef}
|
|
||||||
width={modalWidth}
|
|
||||||
height={modalHeight}
|
|
||||||
style={{ display: 'none' }}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Modal for larger signature canvas */}
|
|
||||||
<Modal
|
|
||||||
opened={isModalOpen}
|
|
||||||
onClose={() => setIsModalOpen(false)}
|
|
||||||
title="Draw Your Signature"
|
|
||||||
size="xl"
|
|
||||||
centered
|
|
||||||
>
|
|
||||||
<Stack gap="md">
|
<Stack gap="md">
|
||||||
{/* Color and Pen Size picker */}
|
<div style={{ display: 'flex', gap: '20px', alignItems: 'flex-end' }}>
|
||||||
<Paper withBorder p="sm">
|
<div>
|
||||||
<Group gap="lg" align="flex-end">
|
<Text size="sm" fw={500} mb="xs">Color</Text>
|
||||||
<div>
|
<Popover
|
||||||
<Text size="sm" fw={500} mb="xs">Color</Text>
|
opened={colorPickerOpen}
|
||||||
<ColorSwatchButton
|
onChange={setColorPickerOpen}
|
||||||
color={selectedColor}
|
position="bottom-start"
|
||||||
onClick={onColorSwatchClick}
|
withArrow
|
||||||
/>
|
withinPortal={false}
|
||||||
</div>
|
>
|
||||||
<div>
|
<Popover.Target>
|
||||||
<Text size="sm" fw={500} mb="xs">Pen Size</Text>
|
<div>
|
||||||
<PenSizeSelector
|
<ColorSwatchButton
|
||||||
value={penSize}
|
color={selectedColor}
|
||||||
inputValue={penSizeInput}
|
onClick={() => setColorPickerOpen(!colorPickerOpen)}
|
||||||
onValueChange={onPenSizeChange}
|
/>
|
||||||
onInputChange={onPenSizeInputChange}
|
</div>
|
||||||
placeholder="Size"
|
</Popover.Target>
|
||||||
size="compact-sm"
|
<Popover.Dropdown>
|
||||||
style={{ width: '60px' }}
|
<MantineColorPicker
|
||||||
/>
|
format="hex"
|
||||||
</div>
|
value={selectedColor}
|
||||||
</Group>
|
onChange={(color) => {
|
||||||
</Paper>
|
onColorSwatchClick();
|
||||||
|
updatePenColor(color);
|
||||||
|
}}
|
||||||
|
swatches={['#000000', '#0066cc', '#cc0000', '#cc6600', '#009900', '#6600cc']}
|
||||||
|
/>
|
||||||
|
</Popover.Dropdown>
|
||||||
|
</Popover>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Text size="sm" fw={500} mb="xs">Pen Size</Text>
|
||||||
|
<PenSizeSelector
|
||||||
|
value={penSize}
|
||||||
|
inputValue={penSizeInput}
|
||||||
|
onValueChange={(size) => {
|
||||||
|
onPenSizeChange(size);
|
||||||
|
updatePenSize(size);
|
||||||
|
}}
|
||||||
|
onInputChange={onPenSizeInputChange}
|
||||||
|
placeholder="Size"
|
||||||
|
size="compact-sm"
|
||||||
|
style={{ width: '60px' }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<Paper withBorder p="md">
|
<canvas
|
||||||
<canvas
|
ref={(el) => {
|
||||||
ref={visibleModalCanvasRef}
|
modalCanvasRef.current = el;
|
||||||
width={modalWidth}
|
if (el) initPad(el);
|
||||||
height={modalHeight}
|
}}
|
||||||
style={{
|
style={{
|
||||||
border: '1px solid #ccc',
|
border: '1px solid #ccc',
|
||||||
borderRadius: '4px',
|
borderRadius: '4px',
|
||||||
cursor: 'crosshair',
|
display: 'block',
|
||||||
backgroundColor: '#ffffff',
|
touchAction: 'none',
|
||||||
width: '100%',
|
backgroundColor: 'white',
|
||||||
maxWidth: `${modalWidth}px`,
|
width: '100%',
|
||||||
height: 'auto',
|
maxWidth: '800px',
|
||||||
}}
|
height: '400px',
|
||||||
onMouseDown={startModalDrawing}
|
cursor: 'crosshair',
|
||||||
onMouseMove={drawModal}
|
}}
|
||||||
onMouseUp={stopModalDrawing}
|
/>
|
||||||
onMouseLeave={stopModalDrawing}
|
|
||||||
/>
|
|
||||||
</Paper>
|
|
||||||
|
|
||||||
<Group justify="space-between">
|
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
|
||||||
<Button
|
<Button variant="subtle" color="red" onClick={clear}>
|
||||||
variant="subtle"
|
|
||||||
color="red"
|
|
||||||
onClick={clearModalCanvas}
|
|
||||||
>
|
|
||||||
Clear Canvas
|
Clear Canvas
|
||||||
</Button>
|
</Button>
|
||||||
<Group gap="sm">
|
<Button onClick={closeModal}>
|
||||||
<Button
|
Done
|
||||||
variant="subtle"
|
</Button>
|
||||||
onClick={() => setIsModalOpen(false)}
|
</div>
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
onClick={saveModalSignature}
|
|
||||||
>
|
|
||||||
Save Signature
|
|
||||||
</Button>
|
|
||||||
</Group>
|
|
||||||
</Group>
|
|
||||||
</Stack>
|
</Stack>
|
||||||
</Modal>
|
</Modal>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default DrawingCanvas;
|
export default DrawingCanvas;
|
||||||
|
|||||||
@ -48,7 +48,7 @@ export const ImageUploader: React.FC<ImageUploaderProps> = ({
|
|||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
/>
|
/>
|
||||||
<Text size="sm" c="dimmed">
|
<Text size="sm" c="dimmed">
|
||||||
{hint || t('sign.image.hint', 'Upload a PNG or JPG image of your signature')}
|
{hint || t('sign.image.hint', 'Upload an image of your signature')}
|
||||||
</Text>
|
</Text>
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { Stack, TextInput, Select, Combobox, useCombobox } from '@mantine/core';
|
import { Stack, TextInput, Select, Combobox, useCombobox, Group, Box } from '@mantine/core';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { ColorPicker } from './ColorPicker';
|
||||||
|
|
||||||
interface TextInputWithFontProps {
|
interface TextInputWithFontProps {
|
||||||
text: string;
|
text: string;
|
||||||
@ -9,6 +10,8 @@ interface TextInputWithFontProps {
|
|||||||
onFontSizeChange: (size: number) => void;
|
onFontSizeChange: (size: number) => void;
|
||||||
fontFamily: string;
|
fontFamily: string;
|
||||||
onFontFamilyChange: (family: string) => void;
|
onFontFamilyChange: (family: string) => void;
|
||||||
|
textColor?: string;
|
||||||
|
onTextColorChange?: (color: string) => void;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
label?: string;
|
label?: string;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
@ -21,6 +24,8 @@ export const TextInputWithFont: React.FC<TextInputWithFontProps> = ({
|
|||||||
onFontSizeChange,
|
onFontSizeChange,
|
||||||
fontFamily,
|
fontFamily,
|
||||||
onFontFamilyChange,
|
onFontFamilyChange,
|
||||||
|
textColor = '#000000',
|
||||||
|
onTextColorChange,
|
||||||
disabled = false,
|
disabled = false,
|
||||||
label,
|
label,
|
||||||
placeholder
|
placeholder
|
||||||
@ -28,6 +33,7 @@ export const TextInputWithFont: React.FC<TextInputWithFontProps> = ({
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [fontSizeInput, setFontSizeInput] = useState(fontSize.toString());
|
const [fontSizeInput, setFontSizeInput] = useState(fontSize.toString());
|
||||||
const fontSizeCombobox = useCombobox();
|
const fontSizeCombobox = useCombobox();
|
||||||
|
const [isColorPickerOpen, setIsColorPickerOpen] = useState(false);
|
||||||
|
|
||||||
// Sync font size input with prop changes
|
// Sync font size input with prop changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -42,7 +48,7 @@ export const TextInputWithFont: React.FC<TextInputWithFontProps> = ({
|
|||||||
{ value: 'Georgia', label: 'Georgia' },
|
{ value: 'Georgia', label: 'Georgia' },
|
||||||
];
|
];
|
||||||
|
|
||||||
const fontSizeOptions = ['8', '12', '16', '20', '24', '28', '32', '36', '40', '48'];
|
const fontSizeOptions = ['8', '12', '16', '20', '24', '28', '32', '36', '40', '48', '56', '64', '72', '80', '96', '112', '128', '144', '160', '176', '192', '200'];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack gap="sm">
|
<Stack gap="sm">
|
||||||
@ -66,61 +72,101 @@ export const TextInputWithFont: React.FC<TextInputWithFontProps> = ({
|
|||||||
allowDeselect={false}
|
allowDeselect={false}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Font Size */}
|
{/* Font Size and Color */}
|
||||||
<Combobox
|
<Group grow>
|
||||||
onOptionSubmit={(optionValue) => {
|
<Combobox
|
||||||
setFontSizeInput(optionValue);
|
onOptionSubmit={(optionValue) => {
|
||||||
const size = parseInt(optionValue);
|
setFontSizeInput(optionValue);
|
||||||
if (!isNaN(size)) {
|
const size = parseInt(optionValue);
|
||||||
onFontSizeChange(size);
|
if (!isNaN(size)) {
|
||||||
}
|
onFontSizeChange(size);
|
||||||
fontSizeCombobox.closeDropdown();
|
}
|
||||||
}}
|
fontSizeCombobox.closeDropdown();
|
||||||
store={fontSizeCombobox}
|
}}
|
||||||
withinPortal={false}
|
store={fontSizeCombobox}
|
||||||
>
|
withinPortal={false}
|
||||||
<Combobox.Target>
|
>
|
||||||
<TextInput
|
<Combobox.Target>
|
||||||
label="Font Size"
|
<TextInput
|
||||||
placeholder="Type or select font size (8-72)"
|
label="Font Size"
|
||||||
value={fontSizeInput}
|
placeholder="Type or select font size (8-200)"
|
||||||
onChange={(event) => {
|
value={fontSizeInput}
|
||||||
const value = event.currentTarget.value;
|
onChange={(event) => {
|
||||||
setFontSizeInput(value);
|
const value = event.currentTarget.value;
|
||||||
|
setFontSizeInput(value);
|
||||||
|
|
||||||
// Parse and validate the typed value in real-time
|
// Parse and validate the typed value in real-time
|
||||||
const size = parseInt(value);
|
const size = parseInt(value);
|
||||||
if (!isNaN(size) && size >= 8 && size <= 72) {
|
if (!isNaN(size) && size >= 8 && size <= 200) {
|
||||||
onFontSizeChange(size);
|
onFontSizeChange(size);
|
||||||
|
}
|
||||||
|
|
||||||
|
fontSizeCombobox.openDropdown();
|
||||||
|
fontSizeCombobox.updateSelectedOptionIndex();
|
||||||
|
}}
|
||||||
|
onClick={() => fontSizeCombobox.openDropdown()}
|
||||||
|
onFocus={() => fontSizeCombobox.openDropdown()}
|
||||||
|
onBlur={() => {
|
||||||
|
fontSizeCombobox.closeDropdown();
|
||||||
|
// Clean up invalid values on blur
|
||||||
|
const size = parseInt(fontSizeInput);
|
||||||
|
if (isNaN(size) || size < 8 || size > 200) {
|
||||||
|
setFontSizeInput(fontSize.toString());
|
||||||
|
} else {
|
||||||
|
onFontSizeChange(size);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
disabled={disabled}
|
||||||
|
/>
|
||||||
|
</Combobox.Target>
|
||||||
|
|
||||||
|
<Combobox.Dropdown>
|
||||||
|
<Combobox.Options>
|
||||||
|
{fontSizeOptions.map((size) => (
|
||||||
|
<Combobox.Option value={size} key={size}>
|
||||||
|
{size}px
|
||||||
|
</Combobox.Option>
|
||||||
|
))}
|
||||||
|
</Combobox.Options>
|
||||||
|
</Combobox.Dropdown>
|
||||||
|
</Combobox>
|
||||||
|
|
||||||
|
{/* Text Color Picker */}
|
||||||
|
{onTextColorChange && (
|
||||||
|
<Box>
|
||||||
|
<TextInput
|
||||||
|
label="Text Color"
|
||||||
|
value={textColor}
|
||||||
|
readOnly
|
||||||
|
disabled={disabled}
|
||||||
|
onClick={() => !disabled && setIsColorPickerOpen(true)}
|
||||||
|
style={{ cursor: disabled ? 'default' : 'pointer' }}
|
||||||
|
rightSection={
|
||||||
|
<Box
|
||||||
|
style={{
|
||||||
|
width: 24,
|
||||||
|
height: 24,
|
||||||
|
backgroundColor: textColor,
|
||||||
|
border: '1px solid #ccc',
|
||||||
|
borderRadius: 4,
|
||||||
|
cursor: disabled ? 'default' : 'pointer'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
}
|
}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Group>
|
||||||
|
|
||||||
fontSizeCombobox.openDropdown();
|
{/* Color Picker Modal */}
|
||||||
fontSizeCombobox.updateSelectedOptionIndex();
|
{onTextColorChange && (
|
||||||
}}
|
<ColorPicker
|
||||||
onClick={() => fontSizeCombobox.openDropdown()}
|
isOpen={isColorPickerOpen}
|
||||||
onFocus={() => fontSizeCombobox.openDropdown()}
|
onClose={() => setIsColorPickerOpen(false)}
|
||||||
onBlur={() => {
|
selectedColor={textColor}
|
||||||
fontSizeCombobox.closeDropdown();
|
onColorChange={onTextColorChange}
|
||||||
// Clean up invalid values on blur
|
/>
|
||||||
const size = parseInt(fontSizeInput);
|
)}
|
||||||
if (isNaN(size) || size < 8 || size > 72) {
|
|
||||||
setFontSizeInput(fontSize.toString());
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
disabled={disabled}
|
|
||||||
/>
|
|
||||||
</Combobox.Target>
|
|
||||||
|
|
||||||
<Combobox.Dropdown>
|
|
||||||
<Combobox.Options>
|
|
||||||
{fontSizeOptions.map((size) => (
|
|
||||||
<Combobox.Option value={size} key={size}>
|
|
||||||
{size}px
|
|
||||||
</Combobox.Option>
|
|
||||||
))}
|
|
||||||
</Combobox.Options>
|
|
||||||
</Combobox.Dropdown>
|
|
||||||
</Combobox>
|
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@ -10,6 +10,7 @@ import { useFileState, useFileContext } from '../../../contexts/FileContext';
|
|||||||
import { generateThumbnailWithMetadata } from '../../../utils/thumbnailUtils';
|
import { generateThumbnailWithMetadata } from '../../../utils/thumbnailUtils';
|
||||||
import { createProcessedFile } from '../../../contexts/file/fileActions';
|
import { createProcessedFile } from '../../../contexts/file/fileActions';
|
||||||
import { createStirlingFile, createNewStirlingFileStub } from '../../../types/fileContext';
|
import { createStirlingFile, createNewStirlingFileStub } from '../../../types/fileContext';
|
||||||
|
import { useNavigationState } from '../../../contexts/NavigationContext';
|
||||||
|
|
||||||
interface ViewerAnnotationControlsProps {
|
interface ViewerAnnotationControlsProps {
|
||||||
currentView: string;
|
currentView: string;
|
||||||
@ -25,13 +26,17 @@ export default function ViewerAnnotationControls({ currentView }: ViewerAnnotati
|
|||||||
const viewerContext = React.useContext(ViewerContext);
|
const viewerContext = React.useContext(ViewerContext);
|
||||||
|
|
||||||
// Signature context for accessing drawing API
|
// Signature context for accessing drawing API
|
||||||
const { signatureApiRef } = useSignature();
|
const { signatureApiRef, isPlacementMode } = useSignature();
|
||||||
|
|
||||||
// File state for save functionality
|
// File state for save functionality
|
||||||
const { state, selectors } = useFileState();
|
const { state, selectors } = useFileState();
|
||||||
const { actions: fileActions } = useFileContext();
|
const { actions: fileActions } = useFileContext();
|
||||||
const activeFiles = selectors.getFiles();
|
const activeFiles = selectors.getFiles();
|
||||||
|
|
||||||
|
// Check if we're in sign mode
|
||||||
|
const { selectedTool } = useNavigationState();
|
||||||
|
const isSignMode = selectedTool === 'sign';
|
||||||
|
|
||||||
// Turn off annotation mode when switching away from viewer
|
// Turn off annotation mode when switching away from viewer
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (currentView !== 'viewer' && viewerContext?.isAnnotationMode) {
|
if (currentView !== 'viewer' && viewerContext?.isAnnotationMode) {
|
||||||
@ -39,6 +44,11 @@ export default function ViewerAnnotationControls({ currentView }: ViewerAnnotati
|
|||||||
}
|
}
|
||||||
}, [currentView, viewerContext]);
|
}, [currentView, viewerContext]);
|
||||||
|
|
||||||
|
// Don't show any annotation controls in sign mode
|
||||||
|
if (isSignMode) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* Annotation Visibility Toggle */}
|
{/* Annotation Visibility Toggle */}
|
||||||
@ -50,7 +60,7 @@ export default function ViewerAnnotationControls({ currentView }: ViewerAnnotati
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
viewerContext?.toggleAnnotationsVisibility();
|
viewerContext?.toggleAnnotationsVisibility();
|
||||||
}}
|
}}
|
||||||
disabled={currentView !== 'viewer' || viewerContext?.isAnnotationMode}
|
disabled={currentView !== 'viewer' || viewerContext?.isAnnotationMode || isPlacementMode}
|
||||||
>
|
>
|
||||||
<LocalIcon
|
<LocalIcon
|
||||||
icon={viewerContext?.isAnnotationsVisible ? "visibility" : "visibility-off-rounded"}
|
icon={viewerContext?.isAnnotationsVisible ? "visibility" : "visibility-off-rounded"}
|
||||||
|
|||||||
@ -1,8 +1,9 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Stack, Button, Text, Alert, Tabs } from '@mantine/core';
|
import { Stack, Button, Text, Alert, Tabs, SegmentedControl } from '@mantine/core';
|
||||||
import { SignParameters } from "../../../hooks/tools/sign/useSignParameters";
|
import { SignParameters } from "../../../hooks/tools/sign/useSignParameters";
|
||||||
import { SuggestedToolsSection } from "../shared/SuggestedToolsSection";
|
import { SuggestedToolsSection } from "../shared/SuggestedToolsSection";
|
||||||
|
import { useSignature } from "../../../contexts/SignatureContext";
|
||||||
|
|
||||||
// Import the new reusable components
|
// Import the new reusable components
|
||||||
import { DrawingCanvas } from "../../annotation/shared/DrawingCanvas";
|
import { DrawingCanvas } from "../../annotation/shared/DrawingCanvas";
|
||||||
@ -35,12 +36,14 @@ const SignSettings = ({
|
|||||||
onSave
|
onSave
|
||||||
}: SignSettingsProps) => {
|
}: SignSettingsProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const { isPlacementMode } = useSignature();
|
||||||
|
|
||||||
// State for drawing
|
// State for drawing
|
||||||
const [selectedColor, setSelectedColor] = useState('#000000');
|
const [selectedColor, setSelectedColor] = useState('#000000');
|
||||||
const [penSize, setPenSize] = useState(2);
|
const [penSize, setPenSize] = useState(2);
|
||||||
const [penSizeInput, setPenSizeInput] = useState('2');
|
const [penSizeInput, setPenSizeInput] = useState('2');
|
||||||
const [isColorPickerOpen, setIsColorPickerOpen] = useState(false);
|
const [isColorPickerOpen, setIsColorPickerOpen] = useState(false);
|
||||||
|
const [interactionMode, setInteractionMode] = useState<'move' | 'place'>('move');
|
||||||
|
|
||||||
// State for different signature types
|
// State for different signature types
|
||||||
const [canvasSignatureData, setCanvasSignatureData] = useState<string | null>(null);
|
const [canvasSignatureData, setCanvasSignatureData] = useState<string | null>(null);
|
||||||
@ -96,20 +99,29 @@ const SignSettings = ({
|
|||||||
}
|
}
|
||||||
}, [parameters.signatureType]);
|
}, [parameters.signatureType]);
|
||||||
|
|
||||||
// Handle text signature activation
|
// Handle text signature activation (including fontSize and fontFamily changes)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (parameters.signatureType === 'text' && parameters.signerName && parameters.signerName.trim() !== '') {
|
if (parameters.signatureType === 'text' && parameters.signerName && parameters.signerName.trim() !== '') {
|
||||||
if (onActivateSignaturePlacement) {
|
if (onActivateSignaturePlacement) {
|
||||||
|
setInteractionMode('place');
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
onActivateSignaturePlacement();
|
onActivateSignaturePlacement();
|
||||||
}, 100);
|
}, 100);
|
||||||
}
|
}
|
||||||
} else if (parameters.signatureType === 'text' && (!parameters.signerName || parameters.signerName.trim() === '')) {
|
} else if (parameters.signatureType === 'text' && (!parameters.signerName || parameters.signerName.trim() === '')) {
|
||||||
if (onDeactivateSignature) {
|
if (onDeactivateSignature) {
|
||||||
|
setInteractionMode('move');
|
||||||
onDeactivateSignature();
|
onDeactivateSignature();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [parameters.signatureType, parameters.signerName, onActivateSignaturePlacement, onDeactivateSignature]);
|
}, [parameters.signatureType, parameters.signerName, parameters.fontSize, parameters.fontFamily, onActivateSignaturePlacement, onDeactivateSignature]);
|
||||||
|
|
||||||
|
// Reset to move mode when placement mode is deactivated
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isPlacementMode && interactionMode === 'place') {
|
||||||
|
setInteractionMode('move');
|
||||||
|
}
|
||||||
|
}, [isPlacementMode, interactionMode]);
|
||||||
|
|
||||||
// Handle signature data updates
|
// Handle signature data updates
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -130,12 +142,23 @@ const SignSettings = ({
|
|||||||
// Handle image signature activation - activate when image data syncs with parameters
|
// Handle image signature activation - activate when image data syncs with parameters
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (parameters.signatureType === 'image' && imageSignatureData && parameters.signatureData === imageSignatureData && onActivateSignaturePlacement) {
|
if (parameters.signatureType === 'image' && imageSignatureData && parameters.signatureData === imageSignatureData && onActivateSignaturePlacement) {
|
||||||
|
setInteractionMode('place');
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
onActivateSignaturePlacement();
|
onActivateSignaturePlacement();
|
||||||
}, 100);
|
}, 100);
|
||||||
}
|
}
|
||||||
}, [parameters.signatureType, parameters.signatureData, imageSignatureData]);
|
}, [parameters.signatureType, parameters.signatureData, imageSignatureData]);
|
||||||
|
|
||||||
|
// Handle canvas signature activation - activate when canvas data syncs with parameters
|
||||||
|
useEffect(() => {
|
||||||
|
if (parameters.signatureType === 'canvas' && canvasSignatureData && parameters.signatureData === canvasSignatureData && onActivateSignaturePlacement) {
|
||||||
|
setInteractionMode('place');
|
||||||
|
setTimeout(() => {
|
||||||
|
onActivateSignaturePlacement();
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
}, [parameters.signatureType, parameters.signatureData, canvasSignatureData]);
|
||||||
|
|
||||||
// Draw settings are no longer needed since draw mode is removed
|
// Draw settings are no longer needed since draw mode is removed
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -170,7 +193,7 @@ const SignSettings = ({
|
|||||||
hasSignatureData={!!(canvasSignatureData || imageSignatureData || (parameters.signerName && parameters.signerName.trim() !== ''))}
|
hasSignatureData={!!(canvasSignatureData || imageSignatureData || (parameters.signerName && parameters.signerName.trim() !== ''))}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
showPlaceButton={false}
|
showPlaceButton={false}
|
||||||
placeButtonText="Update and Place"
|
placeButtonText={t('sign.updateAndPlace', 'Update and Place')}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Signature Creation based on type */}
|
{/* Signature Creation based on type */}
|
||||||
@ -183,6 +206,11 @@ const SignSettings = ({
|
|||||||
onPenSizeChange={setPenSize}
|
onPenSizeChange={setPenSize}
|
||||||
onPenSizeInputChange={setPenSizeInput}
|
onPenSizeInputChange={setPenSizeInput}
|
||||||
onSignatureDataChange={handleCanvasSignatureChange}
|
onSignatureDataChange={handleCanvasSignatureChange}
|
||||||
|
onDrawingComplete={() => {
|
||||||
|
if (onActivateSignaturePlacement) {
|
||||||
|
onActivateSignaturePlacement();
|
||||||
|
}
|
||||||
|
}}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
additionalButtons={
|
additionalButtons={
|
||||||
<Button
|
<Button
|
||||||
@ -195,7 +223,7 @@ const SignSettings = ({
|
|||||||
variant="filled"
|
variant="filled"
|
||||||
disabled={disabled || !canvasSignatureData}
|
disabled={disabled || !canvasSignatureData}
|
||||||
>
|
>
|
||||||
Update and Place
|
{t('sign.updateAndPlace', 'Update and Place')}
|
||||||
</Button>
|
</Button>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
@ -216,17 +244,43 @@ const SignSettings = ({
|
|||||||
onFontSizeChange={(size) => onParameterChange('fontSize', size)}
|
onFontSizeChange={(size) => onParameterChange('fontSize', size)}
|
||||||
fontFamily={parameters.fontFamily || 'Helvetica'}
|
fontFamily={parameters.fontFamily || 'Helvetica'}
|
||||||
onFontFamilyChange={(family) => onParameterChange('fontFamily', family)}
|
onFontFamilyChange={(family) => onParameterChange('fontFamily', family)}
|
||||||
|
textColor={parameters.textColor || '#000000'}
|
||||||
|
onTextColorChange={(color) => onParameterChange('textColor', color)}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
||||||
|
{/* Interaction Mode Toggle */}
|
||||||
|
{(canvasSignatureData || imageSignatureData || (parameters.signerName && parameters.signerName.trim() !== '')) && (
|
||||||
|
<SegmentedControl
|
||||||
|
value={interactionMode}
|
||||||
|
onChange={(value) => {
|
||||||
|
setInteractionMode(value as 'move' | 'place');
|
||||||
|
if (value === 'place') {
|
||||||
|
if (onActivateSignaturePlacement) {
|
||||||
|
onActivateSignaturePlacement();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (onDeactivateSignature) {
|
||||||
|
onDeactivateSignature();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
data={[
|
||||||
|
{ label: t('sign.mode.move', 'Move Signature'), value: 'move' },
|
||||||
|
{ label: t('sign.mode.place', 'Place Signature'), value: 'place' }
|
||||||
|
]}
|
||||||
|
fullWidth
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Instructions for placing signature */}
|
{/* Instructions for placing signature */}
|
||||||
<Alert color="blue" title={t('sign.instructions.title', 'How to add signature')}>
|
<Alert color="blue" title={t('sign.instructions.title', 'How to add signature')}>
|
||||||
<Text size="sm">
|
<Text size="sm">
|
||||||
{parameters.signatureType === 'canvas' && 'After drawing your signature in the canvas above, click "Update and Place" then click anywhere on the PDF to place it.'}
|
{parameters.signatureType === 'canvas' && t('sign.instructions.canvas', 'After drawing your signature in the canvas, close the modal then click anywhere on the PDF to place it.')}
|
||||||
{parameters.signatureType === 'image' && 'After uploading your signature image above, click anywhere on the PDF to place it.'}
|
{parameters.signatureType === 'image' && t('sign.instructions.image', 'After uploading your signature image above, click anywhere on the PDF to place it.')}
|
||||||
{parameters.signatureType === 'text' && 'After entering your name above, click anywhere on the PDF to place your signature.'}
|
{parameters.signatureType === 'text' && t('sign.instructions.text', 'After entering your name above, click anywhere on the PDF to place your signature.')}
|
||||||
</Text>
|
</Text>
|
||||||
</Alert>
|
</Alert>
|
||||||
|
|
||||||
|
|||||||
@ -13,6 +13,7 @@ import { useNavigationGuard, useNavigationState } from '../../contexts/Navigatio
|
|||||||
import { useSignature } from '../../contexts/SignatureContext';
|
import { useSignature } from '../../contexts/SignatureContext';
|
||||||
import { createStirlingFilesAndStubs } from '../../services/fileStubHelpers';
|
import { createStirlingFilesAndStubs } from '../../services/fileStubHelpers';
|
||||||
import NavigationWarningModal from '../shared/NavigationWarningModal';
|
import NavigationWarningModal from '../shared/NavigationWarningModal';
|
||||||
|
import { isStirlingFile } from '../../types/fileContext';
|
||||||
|
|
||||||
export interface EmbedPdfViewerProps {
|
export interface EmbedPdfViewerProps {
|
||||||
sidebarsVisible: boolean;
|
sidebarsVisible: boolean;
|
||||||
@ -263,6 +264,7 @@ const EmbedPdfViewerContent = ({
|
|||||||
transition: 'margin-right 0.3s ease'
|
transition: 'margin-right 0.3s ease'
|
||||||
}}>
|
}}>
|
||||||
<LocalEmbedPDF
|
<LocalEmbedPDF
|
||||||
|
key={currentFile && isStirlingFile(currentFile) ? currentFile.fileId : (effectiveFile.file instanceof File ? effectiveFile.file.name : effectiveFile.url)}
|
||||||
file={effectiveFile.file}
|
file={effectiveFile.file}
|
||||||
url={effectiveFile.url}
|
url={effectiveFile.url}
|
||||||
enableAnnotations={shouldEnableAnnotations}
|
enableAnnotations={shouldEnableAnnotations}
|
||||||
|
|||||||
@ -314,7 +314,7 @@ export function LocalEmbedPDF({ file, url, enableAnnotations = false, onSignatur
|
|||||||
<CustomSearchLayer pageIndex={pageIndex} scale={scale} />
|
<CustomSearchLayer pageIndex={pageIndex} scale={scale} />
|
||||||
|
|
||||||
{/* Selection layer for text interaction */}
|
{/* Selection layer for text interaction */}
|
||||||
<SelectionLayer pageIndex={pageIndex} scale={scale} />
|
<SelectionLayer pageIndex={pageIndex} scale={scale} />
|
||||||
{/* Annotation layer for signatures (only when enabled) */}
|
{/* Annotation layer for signatures (only when enabled) */}
|
||||||
{enableAnnotations && (
|
{enableAnnotations && (
|
||||||
<AnnotationLayer
|
<AnnotationLayer
|
||||||
|
|||||||
@ -1,34 +1,30 @@
|
|||||||
import { useImperativeHandle, forwardRef, useEffect } from 'react';
|
import { useImperativeHandle, forwardRef, useEffect } from 'react';
|
||||||
import { useAnnotationCapability } from '@embedpdf/plugin-annotation/react';
|
import { useAnnotationCapability } from '@embedpdf/plugin-annotation/react';
|
||||||
import { PdfAnnotationSubtype, PdfStandardFont, PdfTextAlignment, PdfVerticalAlignment, uuidV4 } from '@embedpdf/models';
|
import { PdfAnnotationSubtype, uuidV4 } from '@embedpdf/models';
|
||||||
import { SignParameters } from '../../hooks/tools/sign/useSignParameters';
|
|
||||||
import { useSignature } from '../../contexts/SignatureContext';
|
import { useSignature } from '../../contexts/SignatureContext';
|
||||||
import { useViewer } from '../../contexts/ViewerContext';
|
|
||||||
|
|
||||||
export interface SignatureAPI {
|
export interface SignatureAPI {
|
||||||
addImageSignature: (signatureData: string, x: number, y: number, width: number, height: number, pageIndex: number) => void;
|
addImageSignature: (signatureData: string, x: number, y: number, width: number, height: number, pageIndex: number) => void;
|
||||||
addTextSignature: (text: string, x: number, y: number, pageIndex: number) => void;
|
|
||||||
activateDrawMode: () => void;
|
activateDrawMode: () => void;
|
||||||
activateSignaturePlacementMode: () => void;
|
activateSignaturePlacementMode: () => void;
|
||||||
activateDeleteMode: () => void;
|
activateDeleteMode: () => void;
|
||||||
deleteAnnotation: (annotationId: string, pageIndex: number) => void;
|
deleteAnnotation: (annotationId: string, pageIndex: number) => void;
|
||||||
updateDrawSettings: (color: string, size: number) => void;
|
updateDrawSettings: (color: string, size: number) => void;
|
||||||
deactivateTools: () => void;
|
deactivateTools: () => void;
|
||||||
applySignatureFromParameters: (params: SignParameters) => void;
|
|
||||||
getPageAnnotations: (pageIndex: number) => Promise<any[]>;
|
getPageAnnotations: (pageIndex: number) => Promise<any[]>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SignatureAPIBridge = forwardRef<SignatureAPI>(function SignatureAPIBridge(_, ref) {
|
export const SignatureAPIBridge = forwardRef<SignatureAPI>(function SignatureAPIBridge(_, ref) {
|
||||||
const { provides: annotationApi } = useAnnotationCapability();
|
const { provides: annotationApi } = useAnnotationCapability();
|
||||||
const { signatureConfig, storeImageData, isPlacementMode } = useSignature();
|
const { signatureConfig, storeImageData, isPlacementMode } = useSignature();
|
||||||
const { isAnnotationMode } = useViewer();
|
|
||||||
|
|
||||||
|
|
||||||
// Enable keyboard deletion of selected annotations - when in signature placement mode or viewer annotation mode
|
// Enable keyboard deletion of selected annotations
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!annotationApi || (!isPlacementMode && !isAnnotationMode)) return;
|
// Always enable delete key when we have annotation API and are in sign mode
|
||||||
|
if (!annotationApi || (isPlacementMode === undefined)) return;
|
||||||
|
|
||||||
const handleKeyDown = (event: KeyboardEvent) => {
|
const handleKeyDown = (event: KeyboardEvent) => {
|
||||||
if (event.key === 'Delete' || event.key === 'Backspace') {
|
if (event.key === 'Delete' || event.key === 'Backspace') {
|
||||||
const selectedAnnotation = annotationApi.getSelectedAnnotation?.();
|
const selectedAnnotation = annotationApi.getSelectedAnnotation?.();
|
||||||
|
|
||||||
@ -67,7 +63,7 @@ export const SignatureAPIBridge = forwardRef<SignatureAPI>(function SignatureAPI
|
|||||||
|
|
||||||
document.addEventListener('keydown', handleKeyDown);
|
document.addEventListener('keydown', handleKeyDown);
|
||||||
return () => document.removeEventListener('keydown', handleKeyDown);
|
return () => document.removeEventListener('keydown', handleKeyDown);
|
||||||
}, [annotationApi, storeImageData, isPlacementMode, isAnnotationMode]);
|
}, [annotationApi, storeImageData, isPlacementMode]);
|
||||||
|
|
||||||
useImperativeHandle(ref, () => ({
|
useImperativeHandle(ref, () => ({
|
||||||
addImageSignature: (signatureData: string, x: number, y: number, width: number, height: number, pageIndex: number) => {
|
addImageSignature: (signatureData: string, x: number, y: number, width: number, height: number, pageIndex: number) => {
|
||||||
@ -100,34 +96,6 @@ export const SignatureAPIBridge = forwardRef<SignatureAPI>(function SignatureAPI
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
addTextSignature: (text: string, x: number, y: number, pageIndex: number) => {
|
|
||||||
if (!annotationApi) return;
|
|
||||||
|
|
||||||
// Create text annotation for signature
|
|
||||||
annotationApi.createAnnotation(pageIndex, {
|
|
||||||
type: PdfAnnotationSubtype.FREETEXT,
|
|
||||||
rect: {
|
|
||||||
origin: { x, y },
|
|
||||||
size: { width: 200, height: 50 }
|
|
||||||
},
|
|
||||||
contents: text,
|
|
||||||
author: 'Digital Signature',
|
|
||||||
fontSize: 16,
|
|
||||||
fontColor: '#000000',
|
|
||||||
fontFamily: PdfStandardFont.Helvetica,
|
|
||||||
textAlign: PdfTextAlignment.Left,
|
|
||||||
verticalAlign: PdfVerticalAlignment.Top,
|
|
||||||
opacity: 1,
|
|
||||||
pageIndex: pageIndex,
|
|
||||||
id: uuidV4(),
|
|
||||||
created: new Date(),
|
|
||||||
customData: {
|
|
||||||
signatureText: text,
|
|
||||||
signatureType: 'text'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
activateDrawMode: () => {
|
activateDrawMode: () => {
|
||||||
if (!annotationApi) return;
|
if (!annotationApi) return;
|
||||||
|
|
||||||
@ -152,45 +120,31 @@ export const SignatureAPIBridge = forwardRef<SignatureAPI>(function SignatureAPI
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
if (signatureConfig.signatureType === 'text' && signatureConfig.signerName) {
|
if (signatureConfig.signatureType === 'text' && signatureConfig.signerName) {
|
||||||
// Try different tool names for text annotations
|
// Skip native text tools - always use stamp for consistent sizing
|
||||||
const textToolNames = ['freetext', 'text', 'textbox', 'annotation-text'];
|
const activatedTool = null;
|
||||||
let activatedTool = null;
|
|
||||||
|
|
||||||
for (const toolName of textToolNames) {
|
|
||||||
annotationApi.setActiveTool(toolName);
|
|
||||||
const tool = annotationApi.getActiveTool();
|
|
||||||
|
|
||||||
if (tool && tool.id === toolName) {
|
|
||||||
activatedTool = tool;
|
|
||||||
annotationApi.setToolDefaults(toolName, {
|
|
||||||
contents: signatureConfig.signerName,
|
|
||||||
fontSize: signatureConfig.fontSize || 16,
|
|
||||||
fontFamily: signatureConfig.fontFamily === 'Times-Roman' ? PdfStandardFont.Times_Roman :
|
|
||||||
signatureConfig.fontFamily === 'Courier' ? PdfStandardFont.Courier :
|
|
||||||
PdfStandardFont.Helvetica,
|
|
||||||
fontColor: '#000000',
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!activatedTool) {
|
if (!activatedTool) {
|
||||||
// Fallback: create a simple text image as stamp
|
// Create text image as stamp with actual pixel size matching desired display size
|
||||||
const canvas = document.createElement('canvas');
|
const canvas = document.createElement('canvas');
|
||||||
const ctx = canvas.getContext('2d');
|
const ctx = canvas.getContext('2d');
|
||||||
if (ctx) {
|
if (ctx) {
|
||||||
const fontSize = signatureConfig.fontSize || 16;
|
const baseFontSize = signatureConfig.fontSize || 16;
|
||||||
const fontFamily = signatureConfig.fontFamily || 'Helvetica';
|
const fontFamily = signatureConfig.fontFamily || 'Helvetica';
|
||||||
|
const textColor = signatureConfig.textColor || '#000000';
|
||||||
|
|
||||||
canvas.width = Math.max(200, signatureConfig.signerName.length * fontSize * 0.6);
|
// Canvas pixel size = display size (EmbedPDF uses pixel dimensions directly)
|
||||||
canvas.height = fontSize + 20;
|
canvas.width = Math.max(200, signatureConfig.signerName.length * baseFontSize * 0.6);
|
||||||
ctx.fillStyle = '#000000';
|
canvas.height = baseFontSize + 20;
|
||||||
ctx.font = `${fontSize}px ${fontFamily}`;
|
|
||||||
|
ctx.fillStyle = textColor;
|
||||||
|
ctx.font = `${baseFontSize}px ${fontFamily}`;
|
||||||
ctx.textAlign = 'left';
|
ctx.textAlign = 'left';
|
||||||
ctx.textBaseline = 'middle';
|
ctx.textBaseline = 'middle';
|
||||||
ctx.fillText(signatureConfig.signerName, 10, canvas.height / 2);
|
ctx.fillText(signatureConfig.signerName, 10, canvas.height / 2);
|
||||||
const dataURL = canvas.toDataURL();
|
const dataURL = canvas.toDataURL();
|
||||||
|
|
||||||
|
// Deactivate and reactivate to force refresh
|
||||||
|
annotationApi.setActiveTool(null);
|
||||||
annotationApi.setActiveTool('stamp');
|
annotationApi.setActiveTool('stamp');
|
||||||
const stampTool = annotationApi.getActiveTool();
|
const stampTool = annotationApi.getActiveTool();
|
||||||
if (stampTool && stampTool.id === 'stamp') {
|
if (stampTool && stampTool.id === 'stamp') {
|
||||||
@ -205,6 +159,7 @@ export const SignatureAPIBridge = forwardRef<SignatureAPI>(function SignatureAPI
|
|||||||
// Use stamp tool for image/canvas signatures
|
// Use stamp tool for image/canvas signatures
|
||||||
annotationApi.setActiveTool('stamp');
|
annotationApi.setActiveTool('stamp');
|
||||||
const activeTool = annotationApi.getActiveTool();
|
const activeTool = annotationApi.getActiveTool();
|
||||||
|
|
||||||
if (activeTool && activeTool.id === 'stamp') {
|
if (activeTool && activeTool.id === 'stamp') {
|
||||||
annotationApi.setToolDefaults('stamp', {
|
annotationApi.setToolDefaults('stamp', {
|
||||||
imageSrc: signatureConfig.signatureData,
|
imageSrc: signatureConfig.signatureData,
|
||||||
@ -267,84 +222,6 @@ export const SignatureAPIBridge = forwardRef<SignatureAPI>(function SignatureAPI
|
|||||||
annotationApi.setActiveTool(null);
|
annotationApi.setActiveTool(null);
|
||||||
},
|
},
|
||||||
|
|
||||||
applySignatureFromParameters: (params: SignParameters) => {
|
|
||||||
if (!annotationApi || !params.signaturePosition) return;
|
|
||||||
|
|
||||||
const { x, y, width, height, page } = params.signaturePosition;
|
|
||||||
|
|
||||||
switch (params.signatureType) {
|
|
||||||
case 'image':
|
|
||||||
if (params.signatureData) {
|
|
||||||
const annotationId = uuidV4();
|
|
||||||
|
|
||||||
// Store image data in our persistent store
|
|
||||||
storeImageData(annotationId, params.signatureData);
|
|
||||||
|
|
||||||
annotationApi.createAnnotation(page, {
|
|
||||||
type: PdfAnnotationSubtype.STAMP,
|
|
||||||
rect: {
|
|
||||||
origin: { x, y },
|
|
||||||
size: { width, height }
|
|
||||||
},
|
|
||||||
author: 'Digital Signature',
|
|
||||||
subject: `Digital Signature - ${params.reason || 'Document signing'}`,
|
|
||||||
pageIndex: page,
|
|
||||||
id: annotationId,
|
|
||||||
created: new Date(),
|
|
||||||
// Store image data in multiple places to ensure history captures it
|
|
||||||
imageSrc: params.signatureData,
|
|
||||||
contents: params.signatureData, // Some annotation systems use contents
|
|
||||||
data: params.signatureData, // Try data field
|
|
||||||
imageData: params.signatureData, // Try imageData field
|
|
||||||
appearance: params.signatureData // Try appearance field
|
|
||||||
});
|
|
||||||
|
|
||||||
// Switch to select mode after placing signature so it can be easily deleted
|
|
||||||
setTimeout(() => {
|
|
||||||
annotationApi.setActiveTool('select');
|
|
||||||
}, 100);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'text':
|
|
||||||
if (params.signerName) {
|
|
||||||
annotationApi.createAnnotation(page, {
|
|
||||||
type: PdfAnnotationSubtype.FREETEXT,
|
|
||||||
rect: {
|
|
||||||
origin: { x, y },
|
|
||||||
size: { width, height }
|
|
||||||
},
|
|
||||||
contents: params.signerName,
|
|
||||||
author: 'Digital Signature',
|
|
||||||
fontSize: 16,
|
|
||||||
fontColor: '#000000',
|
|
||||||
fontFamily: PdfStandardFont.Helvetica,
|
|
||||||
textAlign: PdfTextAlignment.Left,
|
|
||||||
verticalAlign: PdfVerticalAlignment.Top,
|
|
||||||
opacity: 1,
|
|
||||||
pageIndex: page,
|
|
||||||
id: uuidV4(),
|
|
||||||
created: new Date(),
|
|
||||||
customData: {
|
|
||||||
signatureText: params.signerName,
|
|
||||||
signatureType: 'text'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Switch to select mode after placing signature so it can be easily deleted
|
|
||||||
setTimeout(() => {
|
|
||||||
annotationApi.setActiveTool('select');
|
|
||||||
}, 100);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'draw':
|
|
||||||
// For draw mode, we activate the tool and let user draw
|
|
||||||
annotationApi.setActiveTool('ink');
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
getPageAnnotations: async (pageIndex: number): Promise<any[]> => {
|
getPageAnnotations: async (pageIndex: number): Promise<any[]> => {
|
||||||
if (!annotationApi || !annotationApi.getPageAnnotations) {
|
if (!annotationApi || !annotationApi.getPageAnnotations) {
|
||||||
console.warn('getPageAnnotations not available');
|
console.warn('getPageAnnotations not available');
|
||||||
|
|||||||
@ -51,8 +51,9 @@ function processFileSwap(
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Clear selections that reference removed files
|
// Clear selections that reference removed files and add new files to selection
|
||||||
const validSelectedFileIds = state.ui.selectedFileIds.filter(id => !unpinnedRemoveIds.includes(id));
|
const validSelectedFileIds = state.ui.selectedFileIds.filter(id => !unpinnedRemoveIds.includes(id));
|
||||||
|
const newSelectedFileIds = [...validSelectedFileIds, ...addedIds];
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
@ -62,7 +63,7 @@ function processFileSwap(
|
|||||||
},
|
},
|
||||||
ui: {
|
ui: {
|
||||||
...state.ui,
|
...state.ui,
|
||||||
selectedFileIds: validSelectedFileIds
|
selectedFileIds: newSelectedFileIds
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -17,6 +17,7 @@ export interface SignParameters {
|
|||||||
signerName?: string;
|
signerName?: string;
|
||||||
fontFamily?: string;
|
fontFamily?: string;
|
||||||
fontSize?: number;
|
fontSize?: number;
|
||||||
|
textColor?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DEFAULT_PARAMETERS: SignParameters = {
|
export const DEFAULT_PARAMETERS: SignParameters = {
|
||||||
@ -26,6 +27,7 @@ export const DEFAULT_PARAMETERS: SignParameters = {
|
|||||||
signerName: '',
|
signerName: '',
|
||||||
fontFamily: 'Helvetica',
|
fontFamily: 'Helvetica',
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
|
textColor: '#000000',
|
||||||
};
|
};
|
||||||
|
|
||||||
const validateSignParameters = (parameters: SignParameters): boolean => {
|
const validateSignParameters = (parameters: SignParameters): boolean => {
|
||||||
|
|||||||
@ -18,6 +18,7 @@ const Sign = (props: BaseToolProps) => {
|
|||||||
const { setSignatureConfig, activateDrawMode, activateSignaturePlacementMode, deactivateDrawMode, updateDrawSettings, undo, redo, signatureApiRef, getImageData, setSignaturesApplied } = useSignature();
|
const { setSignatureConfig, activateDrawMode, activateSignaturePlacementMode, deactivateDrawMode, updateDrawSettings, undo, redo, signatureApiRef, getImageData, setSignaturesApplied } = useSignature();
|
||||||
const { consumeFiles, selectors } = useFileContext();
|
const { consumeFiles, selectors } = useFileContext();
|
||||||
const { exportActions, getScrollState } = useViewer();
|
const { exportActions, getScrollState } = useViewer();
|
||||||
|
const { setHasUnsavedChanges, unregisterUnsavedChangesChecker } = useNavigation();
|
||||||
|
|
||||||
// Track which signature mode was active for reactivation after save
|
// Track which signature mode was active for reactivation after save
|
||||||
const activeModeRef = useRef<'draw' | 'placement' | null>(null);
|
const activeModeRef = useRef<'draw' | 'placement' | null>(null);
|
||||||
@ -38,6 +39,11 @@ const Sign = (props: BaseToolProps) => {
|
|||||||
handleSignaturePlacement();
|
handleSignaturePlacement();
|
||||||
}, [handleSignaturePlacement]);
|
}, [handleSignaturePlacement]);
|
||||||
|
|
||||||
|
const handleDeactivateSignature = useCallback(() => {
|
||||||
|
activeModeRef.current = null;
|
||||||
|
deactivateDrawMode();
|
||||||
|
}, [deactivateDrawMode]);
|
||||||
|
|
||||||
const base = useBaseTool(
|
const base = useBaseTool(
|
||||||
'sign',
|
'sign',
|
||||||
useSignParameters,
|
useSignParameters,
|
||||||
@ -45,14 +51,18 @@ const Sign = (props: BaseToolProps) => {
|
|||||||
props
|
props
|
||||||
);
|
);
|
||||||
|
|
||||||
// Open viewer when files are selected
|
const hasOpenedViewer = useRef(false);
|
||||||
|
|
||||||
|
// Open viewer when files are selected (only once)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (base.selectedFiles.length > 0) {
|
if (base.selectedFiles.length > 0 && !hasOpenedViewer.current) {
|
||||||
setWorkbench('viewer');
|
setWorkbench('viewer');
|
||||||
|
hasOpenedViewer.current = true;
|
||||||
}
|
}
|
||||||
}, [base.selectedFiles.length, setWorkbench]);
|
}, [base.selectedFiles.length, setWorkbench]);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Sync signature configuration with context
|
// Sync signature configuration with context
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setSignatureConfig(base.params.parameters);
|
setSignatureConfig(base.params.parameters);
|
||||||
@ -61,6 +71,10 @@ const Sign = (props: BaseToolProps) => {
|
|||||||
// Save signed files to the system - apply signatures using EmbedPDF and replace original
|
// Save signed files to the system - apply signatures using EmbedPDF and replace original
|
||||||
const handleSaveToSystem = useCallback(async () => {
|
const handleSaveToSystem = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
|
// Unregister unsaved changes checker to prevent warning during apply
|
||||||
|
unregisterUnsavedChangesChecker();
|
||||||
|
setHasUnsavedChanges(false);
|
||||||
|
|
||||||
// Get the original file
|
// Get the original file
|
||||||
let originalFile = null;
|
let originalFile = null;
|
||||||
if (base.selectedFiles.length > 0) {
|
if (base.selectedFiles.length > 0) {
|
||||||
@ -81,68 +95,63 @@ const Sign = (props: BaseToolProps) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Use the signature flattening utility
|
// Use the signature flattening utility
|
||||||
const success = await flattenSignatures({
|
const flattenResult = await flattenSignatures({
|
||||||
signatureApiRef,
|
signatureApiRef,
|
||||||
getImageData,
|
getImageData,
|
||||||
exportActions,
|
exportActions,
|
||||||
selectors,
|
selectors,
|
||||||
consumeFiles,
|
|
||||||
originalFile,
|
originalFile,
|
||||||
getScrollState
|
getScrollState
|
||||||
});
|
});
|
||||||
|
|
||||||
if (success) {
|
if (flattenResult) {
|
||||||
console.log('✓ Signature flattening completed successfully');
|
// Now consume the files - this triggers the viewer reload
|
||||||
|
await consumeFiles(
|
||||||
|
flattenResult.inputFileIds,
|
||||||
|
[flattenResult.outputStirlingFile],
|
||||||
|
[flattenResult.outputStub]
|
||||||
|
);
|
||||||
|
|
||||||
// Mark signatures as applied
|
// Mark signatures as applied
|
||||||
setSignaturesApplied(true);
|
setSignaturesApplied(true);
|
||||||
|
|
||||||
// Force refresh the viewer to show the flattened PDF
|
// Deactivate signature placement mode after everything completes
|
||||||
setTimeout(() => {
|
handleDeactivateSignature();
|
||||||
// Navigate away from viewer and back to force reload
|
|
||||||
setWorkbench('fileEditor');
|
|
||||||
setTimeout(() => {
|
|
||||||
setWorkbench('viewer');
|
|
||||||
|
|
||||||
// Reactivate the signature mode that was active before save
|
// File has been consumed - viewer should reload automatically via key prop
|
||||||
if (activeModeRef.current === 'draw') {
|
|
||||||
activateDrawMode();
|
|
||||||
} else if (activeModeRef.current === 'placement') {
|
|
||||||
handleSignaturePlacement();
|
|
||||||
}
|
|
||||||
}, 100);
|
|
||||||
}, 200);
|
|
||||||
} else {
|
} else {
|
||||||
console.error('Signature flattening failed');
|
console.error('Signature flattening failed');
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error saving signed document:', error);
|
console.error('Error saving signed document:', error);
|
||||||
}
|
}
|
||||||
}, [exportActions, base.selectedFiles, selectors, consumeFiles, signatureApiRef, getImageData, setWorkbench, activateDrawMode]);
|
}, [exportActions, base.selectedFiles, selectors, consumeFiles, signatureApiRef, getImageData, setWorkbench, activateDrawMode, setSignaturesApplied, getScrollState, handleDeactivateSignature, setHasUnsavedChanges, unregisterUnsavedChangesChecker]);
|
||||||
|
|
||||||
const getSteps = () => {
|
const getSteps = () => {
|
||||||
const steps = [];
|
const steps = [];
|
||||||
|
|
||||||
// Step 1: Signature Configuration - Always visible
|
// Step 1: Signature Configuration - Only visible when file is loaded
|
||||||
steps.push({
|
if (base.selectedFiles.length > 0) {
|
||||||
title: t('sign.steps.configure', 'Configure Signature'),
|
steps.push({
|
||||||
isCollapsed: false,
|
title: t('sign.steps.configure', 'Configure Signature'),
|
||||||
onCollapsedClick: undefined,
|
isCollapsed: false,
|
||||||
content: (
|
onCollapsedClick: undefined,
|
||||||
<SignSettings
|
content: (
|
||||||
parameters={base.params.parameters}
|
<SignSettings
|
||||||
onParameterChange={base.params.updateParameter}
|
parameters={base.params.parameters}
|
||||||
disabled={base.endpointLoading}
|
onParameterChange={base.params.updateParameter}
|
||||||
onActivateDrawMode={handleActivateDrawMode}
|
disabled={base.endpointLoading}
|
||||||
onActivateSignaturePlacement={handleActivateSignaturePlacement}
|
onActivateDrawMode={handleActivateDrawMode}
|
||||||
onDeactivateSignature={deactivateDrawMode}
|
onActivateSignaturePlacement={handleActivateSignaturePlacement}
|
||||||
onUpdateDrawSettings={updateDrawSettings}
|
onDeactivateSignature={handleDeactivateSignature}
|
||||||
onUndo={undo}
|
onUpdateDrawSettings={updateDrawSettings}
|
||||||
onRedo={redo}
|
onUndo={undo}
|
||||||
onSave={handleSaveToSystem}
|
onRedo={redo}
|
||||||
/>
|
onSave={handleSaveToSystem}
|
||||||
),
|
/>
|
||||||
});
|
),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return steps;
|
return steps;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { PDFDocument, rgb } from 'pdf-lib';
|
import { PDFDocument, rgb } from 'pdf-lib';
|
||||||
import { generateThumbnailWithMetadata } from './thumbnailUtils';
|
import { generateThumbnailWithMetadata } from './thumbnailUtils';
|
||||||
import { createProcessedFile } from '../contexts/file/fileActions';
|
import { createProcessedFile, createChildStub } from '../contexts/file/fileActions';
|
||||||
import { createNewStirlingFileStub, createStirlingFile, StirlingFile, FileId, StirlingFileStub } from '../types/fileContext';
|
import { createStirlingFile, StirlingFile, FileId, StirlingFileStub } from '../types/fileContext';
|
||||||
import type { SignatureAPI } from '../components/viewer/SignatureAPIBridge';
|
import type { SignatureAPI } from '../components/viewer/SignatureAPIBridge';
|
||||||
|
|
||||||
interface MinimalFileContextSelectors {
|
interface MinimalFileContextSelectors {
|
||||||
@ -17,13 +17,18 @@ interface SignatureFlatteningOptions {
|
|||||||
saveAsCopy: () => Promise<ArrayBuffer | null>;
|
saveAsCopy: () => Promise<ArrayBuffer | null>;
|
||||||
};
|
};
|
||||||
selectors: MinimalFileContextSelectors;
|
selectors: MinimalFileContextSelectors;
|
||||||
consumeFiles: (inputFileIds: FileId[], outputStirlingFiles: StirlingFile[], outputStirlingFileStubs: StirlingFileStub[]) => Promise<FileId[]>;
|
|
||||||
originalFile?: StirlingFile;
|
originalFile?: StirlingFile;
|
||||||
getScrollState: () => { currentPage: number; totalPages: number };
|
getScrollState: () => { currentPage: number; totalPages: number };
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function flattenSignatures(options: SignatureFlatteningOptions): Promise<boolean> {
|
export interface SignatureFlatteningResult {
|
||||||
const { signatureApiRef, getImageData, exportActions, selectors, consumeFiles, originalFile, getScrollState } = options;
|
inputFileIds: FileId[];
|
||||||
|
outputStirlingFile: StirlingFile;
|
||||||
|
outputStub: StirlingFileStub;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function flattenSignatures(options: SignatureFlatteningOptions): Promise<SignatureFlatteningResult | null> {
|
||||||
|
const { signatureApiRef, getImageData, exportActions, selectors, originalFile, getScrollState } = options;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Step 1: Extract all annotations from EmbedPDF before export
|
// Step 1: Extract all annotations from EmbedPDF before export
|
||||||
@ -66,8 +71,6 @@ export async function flattenSignatures(options: SignatureFlatteningOptions): Pr
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`Total annotations found: ${allAnnotations.reduce((sum, page) => sum + page.annotations.length, 0)}`);
|
|
||||||
|
|
||||||
// Step 2: Delete ONLY session annotations from EmbedPDF before export (they'll be rendered manually)
|
// Step 2: Delete ONLY session annotations from EmbedPDF before export (they'll be rendered manually)
|
||||||
// Leave old annotations alone - they will remain as annotations in the PDF
|
// Leave old annotations alone - they will remain as annotations in the PDF
|
||||||
if (allAnnotations.length > 0 && signatureApiRef?.current) {
|
if (allAnnotations.length > 0 && signatureApiRef?.current) {
|
||||||
@ -85,7 +88,7 @@ export async function flattenSignatures(options: SignatureFlatteningOptions): Pr
|
|||||||
// Step 3: Use EmbedPDF's saveAsCopy to get the base PDF (now without annotations)
|
// Step 3: Use EmbedPDF's saveAsCopy to get the base PDF (now without annotations)
|
||||||
if (!exportActions) {
|
if (!exportActions) {
|
||||||
console.error('No export actions available');
|
console.error('No export actions available');
|
||||||
return false;
|
return null;
|
||||||
}
|
}
|
||||||
const pdfArrayBuffer = await exportActions.saveAsCopy();
|
const pdfArrayBuffer = await exportActions.saveAsCopy();
|
||||||
|
|
||||||
@ -111,7 +114,7 @@ export async function flattenSignatures(options: SignatureFlatteningOptions): Pr
|
|||||||
|
|
||||||
if (!currentFile) {
|
if (!currentFile) {
|
||||||
console.error('No file available to replace');
|
console.error('No file available to replace');
|
||||||
return false;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
let signedFile = new File([blob], currentFile.name, { type: 'application/pdf' });
|
let signedFile = new File([blob], currentFile.name, { type: 'application/pdf' });
|
||||||
@ -119,7 +122,6 @@ export async function flattenSignatures(options: SignatureFlatteningOptions): Pr
|
|||||||
// Step 4: Manually render extracted annotations onto the PDF using PDF-lib
|
// Step 4: Manually render extracted annotations onto the PDF using PDF-lib
|
||||||
if (allAnnotations.length > 0) {
|
if (allAnnotations.length > 0) {
|
||||||
try {
|
try {
|
||||||
console.log('Manually rendering annotations onto PDF...');
|
|
||||||
const pdfArrayBufferForFlattening = await signedFile.arrayBuffer();
|
const pdfArrayBufferForFlattening = await signedFile.arrayBuffer();
|
||||||
|
|
||||||
// Try different loading options to handle problematic PDFs
|
// Try different loading options to handle problematic PDFs
|
||||||
@ -150,7 +152,6 @@ export async function flattenSignatures(options: SignatureFlatteningOptions): Pr
|
|||||||
|
|
||||||
const pages = pdfDoc.getPages();
|
const pages = pdfDoc.getPages();
|
||||||
|
|
||||||
|
|
||||||
for (const pageData of allAnnotations) {
|
for (const pageData of allAnnotations) {
|
||||||
const { pageIndex, annotations } = pageData;
|
const { pageIndex, annotations } = pageData;
|
||||||
|
|
||||||
@ -189,6 +190,7 @@ export async function flattenSignatures(options: SignatureFlatteningOptions): Pr
|
|||||||
|
|
||||||
if (imageDataUrl && typeof imageDataUrl === 'string' && imageDataUrl.startsWith('data:image')) {
|
if (imageDataUrl && typeof imageDataUrl === 'string' && imageDataUrl.startsWith('data:image')) {
|
||||||
try {
|
try {
|
||||||
|
|
||||||
// Convert data URL to bytes
|
// Convert data URL to bytes
|
||||||
const base64Data = imageDataUrl.split(',')[1];
|
const base64Data = imageDataUrl.split(',')[1];
|
||||||
const imageBytes = Uint8Array.from(atob(base64Data), c => c.charCodeAt(0));
|
const imageBytes = Uint8Array.from(atob(base64Data), c => c.charCodeAt(0));
|
||||||
@ -215,6 +217,7 @@ export async function flattenSignatures(options: SignatureFlatteningOptions): Pr
|
|||||||
console.error('Failed to render image annotation:', imageError);
|
console.error('Failed to render image annotation:', imageError);
|
||||||
}
|
}
|
||||||
} else if (annotation.content || annotation.text) {
|
} else if (annotation.content || annotation.text) {
|
||||||
|
console.warn('Rendering text annotation instead');
|
||||||
// Handle text annotations
|
// Handle text annotations
|
||||||
page.drawText(annotation.content || annotation.text, {
|
page.drawText(annotation.content || annotation.text, {
|
||||||
x: pdfX,
|
x: pdfX,
|
||||||
@ -287,23 +290,30 @@ export async function flattenSignatures(options: SignatureFlatteningOptions): Pr
|
|||||||
const record = selectors.getStirlingFileStub(currentFile.fileId);
|
const record = selectors.getStirlingFileStub(currentFile.fileId);
|
||||||
if (!record) {
|
if (!record) {
|
||||||
console.error('No file record found for:', currentFile.fileId);
|
console.error('No file record found for:', currentFile.fileId);
|
||||||
return false;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create output stub and file
|
// Create output stub and file as a child of the original (increments version)
|
||||||
const outputStub = createNewStirlingFileStub(signedFile, undefined, thumbnailResult.thumbnail, processedFileMetadata);
|
const outputStub = createChildStub(
|
||||||
|
record,
|
||||||
|
{ toolId: 'sign', timestamp: Date.now() },
|
||||||
|
signedFile,
|
||||||
|
thumbnailResult.thumbnail,
|
||||||
|
processedFileMetadata
|
||||||
|
);
|
||||||
const outputStirlingFile = createStirlingFile(signedFile, outputStub.id);
|
const outputStirlingFile = createStirlingFile(signedFile, outputStub.id);
|
||||||
|
|
||||||
// Replace the original file with the signed version
|
// Return the flattened file data for consumption by caller
|
||||||
await consumeFiles(inputFileIds, [outputStirlingFile], [outputStub]);
|
return {
|
||||||
|
inputFileIds,
|
||||||
console.log('✓ Signature flattening completed successfully');
|
outputStirlingFile,
|
||||||
return true;
|
outputStub
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return null;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error flattening signatures:', error);
|
console.error('Error flattening signatures:', error);
|
||||||
return false;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
6
package-lock.json
generated
Normal file
6
package-lock.json
generated
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"name": "Stirling-PDF",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user