diff --git a/frontend/public/locales/en-GB/translation.json b/frontend/public/locales/en-GB/translation.json index d153d39fb..c73eb9351 100644 --- a/frontend/public/locales/en-GB/translation.json +++ b/frontend/public/locales/en-GB/translation.json @@ -1623,6 +1623,7 @@ "add": "Add", "saved": "Saved Signatures", "save": "Save Signature", + "applySignatures": "Apply Signatures", "personalSigs": "Personal Signatures", "sharedSigs": "Shared Signatures", "noSavedSigs": "No saved signatures found", diff --git a/frontend/src/components/tools/sign/SignSettings.tsx b/frontend/src/components/tools/sign/SignSettings.tsx index fa3c0fb6e..1085075a6 100644 --- a/frontend/src/components/tools/sign/SignSettings.tsx +++ b/frontend/src/components/tools/sign/SignSettings.tsx @@ -91,11 +91,9 @@ const SignSettings = ({ } }; - // Handle drawing mode activation + // Handle signature mode deactivation when switching types useEffect(() => { - if (parameters.signatureType === 'draw' && onActivateDrawMode) { - onActivateDrawMode(); - } else if (parameters.signatureType !== 'draw' && onDeactivateSignature) { + if (parameters.signatureType !== 'text' && onDeactivateSignature) { onDeactivateSignature(); } }, [parameters.signatureType]); @@ -140,19 +138,14 @@ const SignSettings = ({ } }, [parameters.signatureType, parameters.signatureData, imageSignatureData]); - // Update draw settings when color or pen size changes - useEffect(() => { - if (parameters.signatureType === 'draw' && onUpdateDrawSettings) { - onUpdateDrawSettings(selectedColor, penSize); - } - }, [selectedColor, penSize, parameters.signatureType]); + // Draw settings are no longer needed since draw mode is removed return ( {/* Signature Type Selection */} onParameterChange('signatureType', value as 'image' | 'text' | 'draw' | 'canvas')} + onChange={(value) => onParameterChange('signatureType', value as 'image' | 'text' | 'canvas')} > @@ -164,9 +157,6 @@ const SignSettings = ({ {t('sign.type.text', 'Text')} - - {t('sign.type.draw', 'Draw')} - @@ -232,66 +222,15 @@ const SignSettings = ({ /> )} - {/* Direct PDF Drawing */} - {parameters.signatureType === 'draw' && ( - - - Direct PDF Drawing - - Draw signatures and annotations directly on the PDF document. - - - {/* Drawing Controls */} - - {/* Color Picker */} -
- Color -
setIsColorPickerOpen(true)} - /> -
- - {/* Pen Size */} -
- Pen Size - { - const size = parseInt(e.target.value); - if (!isNaN(size) && size >= 1 && size <= 200) { - setPenSize(size); - } - }} - min={1} - max={200} - disabled={disabled} - style={{ width: '100%', padding: '4px 8px' }} - /> -
- - - - )} {/* Instructions for placing signature */} - {(parameters.signatureType === 'canvas' || parameters.signatureType === 'image' || parameters.signatureType === 'text') && ( - - - {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 === '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 === 'canvas' && 'After drawing your signature in the canvas above, click "Update and Place" 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 === 'text' && 'After entering your name above, click anywhere on the PDF to place your signature.'} + + {/* Color Picker Modal */} - {/* Save Button */} + {/* Apply Signatures Button */} {onSave && ( )} diff --git a/frontend/src/components/viewer/SignatureAPIBridge.tsx b/frontend/src/components/viewer/SignatureAPIBridge.tsx index 857c42250..60884d09f 100644 --- a/frontend/src/components/viewer/SignatureAPIBridge.tsx +++ b/frontend/src/components/viewer/SignatureAPIBridge.tsx @@ -14,6 +14,7 @@ export interface SignatureAPI { updateDrawSettings: (color: string, size: number) => void; deactivateTools: () => void; applySignatureFromParameters: (params: SignParameters) => void; + getPageAnnotations: (pageIndex: number) => Promise; } export interface SignatureAPIBridgeProps {} @@ -344,6 +345,26 @@ export const SignatureAPIBridge = forwardRef => { + if (!annotationApi || !annotationApi.getPageAnnotations) { + console.warn('getPageAnnotations not available'); + return []; + } + + try { + const pageAnnotationsTask = annotationApi.getPageAnnotations({ pageIndex }); + if (pageAnnotationsTask && pageAnnotationsTask.toPromise) { + const annotations = await pageAnnotationsTask.toPromise(); + console.log(`Retrieved ${annotations?.length || 0} annotations from page ${pageIndex}`); + return annotations || []; + } + return []; + } catch (error) { + console.error(`Error getting annotations for page ${pageIndex}:`, error); + return []; + } + }, }), [annotationApi, signatureConfig]); diff --git a/frontend/src/tools/Sign.tsx b/frontend/src/tools/Sign.tsx index da4a23d75..26c64b13d 100644 --- a/frontend/src/tools/Sign.tsx +++ b/frontend/src/tools/Sign.tsx @@ -13,11 +13,12 @@ import { useViewer } from "../contexts/ViewerContext"; import { generateThumbnailWithMetadata } from "../utils/thumbnailUtils"; import { createNewStirlingFileStub, createStirlingFile, StirlingFileStub, StirlingFile, FileId, extractFiles } from "../types/fileContext"; import { createProcessedFile } from "../contexts/file/fileActions"; +import { PDFDocument, PDFName, PDFDict, PDFArray, rgb } from 'pdf-lib'; const Sign = (props: BaseToolProps) => { const { t } = useTranslation(); const { setWorkbench } = useNavigation(); - const { setSignatureConfig, activateDrawMode, activateSignaturePlacementMode, deactivateDrawMode, updateDrawSettings, undo, redo, isPlacementMode } = useSignature(); + const { setSignatureConfig, activateDrawMode, activateSignaturePlacementMode, deactivateDrawMode, updateDrawSettings, undo, redo, isPlacementMode, signatureApiRef, getImageData } = useSignature(); const { actions } = useFileActions(); const { consumeFiles, selectors } = useFileContext(); const { exportActions } = useViewer(); @@ -53,12 +54,58 @@ const Sign = (props: BaseToolProps) => { // Save signed files to the system - apply signatures using EmbedPDF and replace original const handleSaveToSystem = useCallback(async () => { try { - // Use EmbedPDF's saveAsCopy to apply signatures and get ArrayBuffer + // Step 1: Extract all annotations from EmbedPDF before export + const allAnnotations: Array<{pageIndex: number, annotations: any[]}> = []; + + if (signatureApiRef?.current) { + console.log('Extracting annotations from all pages...'); + + // We need to know how many pages to check - let's assume we check first few pages + // In a real implementation, we'd get the page count from somewhere + for (let pageIndex = 0; pageIndex < 10; pageIndex++) { + try { + const pageAnnotations = await signatureApiRef.current.getPageAnnotations(pageIndex); + if (pageAnnotations && pageAnnotations.length > 0) { + allAnnotations.push({pageIndex, annotations: pageAnnotations}); + console.log(`Found ${pageAnnotations.length} annotations on page ${pageIndex + 1}`); + console.log('Annotation data:', pageAnnotations); + } + } catch (e) { + // Page doesn't exist or no annotations, continue + console.log(`No annotations on page ${pageIndex + 1}:`, e); + if (pageIndex > 2) break; // Stop after checking first few pages + } + } + } + + console.log(`Total annotations found: ${allAnnotations.reduce((sum, page) => sum + page.annotations.length, 0)}`); + + // Step 2: Delete annotations from EmbedPDF before export (they'll be rendered manually) + if (allAnnotations.length > 0 && signatureApiRef?.current) { + console.log('Deleting annotations from EmbedPDF before export...'); + for (const pageData of allAnnotations) { + for (const annotation of pageData.annotations) { + try { + await signatureApiRef.current.deleteAnnotation(annotation.id, pageData.pageIndex); + console.log(`Deleted annotation ${annotation.id} from page ${pageData.pageIndex}`); + } catch (deleteError) { + console.warn(`Failed to delete annotation ${annotation.id}:`, deleteError); + } + } + } + } + + // Step 3: Use EmbedPDF's saveAsCopy to get the base PDF (now without annotations) const pdfArrayBuffer = await exportActions.saveAsCopy(); if (pdfArrayBuffer) { + console.log(`EmbedPDF exported PDF size: ${pdfArrayBuffer.byteLength} bytes`); + + // Try loading with more permissive PDF-lib options + console.log('Attempting to load PDF with PDF-lib...'); + // Convert ArrayBuffer to File - const blob = new Blob([pdfArrayBuffer], { type: 'application/pdf' }); + let blob = new Blob([pdfArrayBuffer], { type: 'application/pdf' }); // Get the current file - try from base.selectedFiles first, then from all files let originalFile = null; @@ -80,7 +127,248 @@ const Sign = (props: BaseToolProps) => { return; } - const signedFile = new File([blob], originalFile.name, { type: 'application/pdf' }); + let signedFile = new File([blob], originalFile.name, { type: 'application/pdf' }); + + // Step 3: Manually render extracted annotations onto the PDF using PDF-lib + if (allAnnotations.length > 0) { + try { + console.log('Manually rendering annotations onto PDF...'); + const pdfArrayBufferForFlattening = await signedFile.arrayBuffer(); + + // Try different loading options to handle problematic PDFs + let pdfDoc: PDFDocument; + try { + pdfDoc = await PDFDocument.load(pdfArrayBufferForFlattening, { + ignoreEncryption: true, + capNumbers: false, + throwOnInvalidObject: false + }); + console.log('✓ PDF loaded successfully with standard options'); + } catch (loadError) { + console.warn('Failed to load with standard options, trying createProxy...'); + try { + // Create a fresh PDF and copy pages instead of modifying + pdfDoc = await PDFDocument.create(); + const sourcePdf = await PDFDocument.load(pdfArrayBufferForFlattening, { + ignoreEncryption: true, + throwOnInvalidObject: false + }); + const pageIndices = sourcePdf.getPages().map((_, i) => i); + const copiedPages = await pdfDoc.copyPages(sourcePdf, pageIndices); + copiedPages.forEach(page => pdfDoc.addPage(page)); + console.log('✓ PDF loaded by creating new document and copying pages'); + } catch (copyError) { + console.error('Failed to load PDF with any method:', copyError); + throw copyError; + } + } + + const pages = pdfDoc.getPages(); + + console.log(`PDF has ${pages.length} pages`); + + let totalRendered = 0; + const annotationsToDelete: Array<{pageIndex: number, id: string}> = []; + + + for (const pageData of allAnnotations) { + const { pageIndex, annotations } = pageData; + + if (pageIndex < pages.length) { + const page = pages[pageIndex]; + const { width: pageWidth, height: pageHeight } = page.getSize(); + + for (const annotation of annotations) { + try { + console.log('Processing annotation:', annotation); + console.log('Annotation keys:', Object.keys(annotation)); + + // EmbedPDF annotations might have different property names + // Let's check for various possible rectangle properties + const rect = annotation.rect || annotation.bounds || annotation.rectangle || annotation.position; + console.log('Rect found:', rect); + + if (rect) { + // Extract original annotation position and size + const originalX = rect.origin?.x || rect.x || rect.left || 0; + const originalY = rect.origin?.y || rect.y || rect.top || 0; + const width = rect.size?.width || rect.width || 100; + const height = rect.size?.height || rect.height || 50; + + // Convert EmbedPDF coordinates to PDF-lib coordinates + // EmbedPDF uses top-left origin, PDF-lib uses bottom-left origin + const pdfX = originalX; + const pdfY = pageHeight - originalY - height; + + console.log('Signature positioning:', { + originalX, + originalY, + width, + height, + pdfX, + pdfY, + pageWidth, + pageHeight + }); + + // Try to get annotation image data - check multiple possible properties + console.log('Looking for image data in:', { + imageData: !!annotation.imageData, + appearance: !!annotation.appearance, + stampData: !!annotation.stampData, + imageSrc: !!annotation.imageSrc, + contents: !!annotation.contents, + data: !!annotation.data + }); + + let imageDataUrl = annotation.imageData || annotation.appearance || annotation.stampData || annotation.imageSrc || annotation.contents || annotation.data; + + // If no image data found directly, try to get it from our storage using annotation ID + if (!imageDataUrl && annotation.id) { + const storedImageData = getImageData(annotation.id); + if (storedImageData) { + console.log('Found stored image data for annotation:', annotation.id); + imageDataUrl = storedImageData; + } + } + + if (imageDataUrl) { + console.log('Found image data:', typeof imageDataUrl, imageDataUrl?.substring?.(0, 100)); + } + + if (imageDataUrl && typeof imageDataUrl === 'string' && imageDataUrl.startsWith('data:image')) { + try { + // Convert data URL to bytes + const base64Data = imageDataUrl.split(',')[1]; + const imageBytes = Uint8Array.from(atob(base64Data), c => c.charCodeAt(0)); + console.log(`Image data size: ${imageBytes.length} bytes`); + + // Embed image in PDF based on data URL type + let image; + if (imageDataUrl.includes('data:image/jpeg') || imageDataUrl.includes('data:image/jpg')) { + console.log('Embedding as JPEG'); + image = await pdfDoc.embedJpg(imageBytes); + } else if (imageDataUrl.includes('data:image/png')) { + console.log('Embedding as PNG'); + image = await pdfDoc.embedPng(imageBytes); + } else { + console.log('Unknown image type, trying PNG as fallback'); + image = await pdfDoc.embedPng(imageBytes); + } + + const imageDims = image.size(); + console.log(`Image dimensions: ${imageDims.width}x${imageDims.height}`); + + // Draw image on page at annotation position + page.drawImage(image, { + x: pdfX, + y: pdfY, + width: width, + height: height, + }); + + totalRendered++; + annotationsToDelete.push({pageIndex, id: annotation.id}); + console.log(`✓ SUCCESS: Rendered image annotation at (${pdfX}, ${pdfY}) size (${width}x${height})`); + } catch (imageError) { + console.error('Failed to render image annotation:', imageError); + } + } else if (annotation.content || annotation.text) { + // Handle text annotations + page.drawText(annotation.content || annotation.text, { + x: pdfX, + y: pdfY + height - 12, // Adjust for text baseline + size: 12, + color: rgb(0, 0, 0) + }); + totalRendered++; + annotationsToDelete.push({pageIndex, id: annotation.id}); + console.log(`Rendered text annotation: "${annotation.content || annotation.text}"`); + } else if (annotation.type === 14 || annotation.type === 15) { + // Handle ink annotations (drawn signatures) + // Type 14 = INK, Type 15 = could be another drawing type + console.log('Processing ink annotation:', annotation); + + // For ink annotations, we'll draw a placeholder rectangle since we can't easily reconstruct the ink paths + page.drawRectangle({ + x: pdfX, + y: pdfY, + width: width, + height: height, + borderColor: rgb(0, 0, 0), + borderWidth: 2, + color: rgb(0.9, 0.9, 0.9), // Light gray background + opacity: 0.8 + }); + + // Add text indicating this was a drawn signature + page.drawText('Drawn Signature', { + x: pdfX + 5, + y: pdfY + height / 2, + size: 10, + color: rgb(0, 0, 0) + }); + + totalRendered++; + annotationsToDelete.push({pageIndex, id: annotation.id}); + console.log(`Rendered ink annotation placeholder at (${pdfX}, ${pdfY}) size (${width}x${height})`); + } else { + // Handle other annotation types + console.log(`Unknown annotation type ${annotation.type}:`, annotation); + + // Draw a placeholder for unknown types + page.drawRectangle({ + x: pdfX, + y: pdfY, + width: width, + height: height, + borderColor: rgb(1, 0, 0), + borderWidth: 2, + color: rgb(1, 1, 0), // Yellow background + opacity: 0.5 + }); + + totalRendered++; + annotationsToDelete.push({pageIndex, id: annotation.id}); + console.log(`Rendered unknown annotation type ${annotation.type} as placeholder`); + } + } + } catch (annotationError) { + console.warn('Failed to render annotation:', annotationError); + } + } + } + } + + console.log(`Successfully rendered ${totalRendered} annotations`); + + // Annotations were already deleted from EmbedPDF before export + + // Save the PDF with rendered annotations + const flattenedPdfBytes = await pdfDoc.save({ useObjectStreams: false, addDefaultPage: false }); + console.log(`Original PDF size: ${pdfArrayBufferForFlattening.byteLength} bytes`); + console.log(`Modified PDF size: ${flattenedPdfBytes.length} bytes`); + + const arrayBuffer = new ArrayBuffer(flattenedPdfBytes.length); + const uint8View = new Uint8Array(arrayBuffer); + uint8View.set(flattenedPdfBytes); + signedFile = new File([arrayBuffer], originalFile.name, { type: 'application/pdf' }); + console.log('Manual annotation rendering completed'); + + // Verify the modified PDF can be loaded + try { + const verifyDoc = await PDFDocument.load(flattenedPdfBytes); + console.log(`✓ Verification: Modified PDF has ${verifyDoc.getPages().length} pages and can be loaded`); + } catch (verifyError) { + console.error('❌ Verification: Modified PDF cannot be loaded:', verifyError); + } + } catch (renderError) { + console.error('Failed to manually render annotations:', renderError); + console.warn('Signatures may only show as annotations'); + } + } else { + console.log('No annotations found to render'); + } // Generate thumbnail and metadata for the signed file const thumbnailResult = await generateThumbnailWithMetadata(signedFile); @@ -105,19 +393,28 @@ const Sign = (props: BaseToolProps) => { // Replace the original file with the signed version await consumeFiles(inputFileIds, [outputStirlingFile], [outputStub]); - // Reactivate the signature mode that was active before save + console.log('✓ File replaced in context, new file ID:', outputStub.id); + + // Force refresh the viewer to show the flattened PDF setTimeout(() => { - if (activeModeRef.current === 'draw') { - activateDrawMode(); - } else if (activeModeRef.current === 'placement') { - handleSignaturePlacement(); - } + // Navigate away from viewer and back to force reload + setWorkbench('fileEditor'); + setTimeout(() => { + setWorkbench('viewer'); + + // Reactivate the signature mode that was active before save + if (activeModeRef.current === 'draw') { + activateDrawMode(); + } else if (activeModeRef.current === 'placement') { + handleSignaturePlacement(); + } + }, 100); }, 200); } } catch (error) { console.error('Error saving signed document:', error); } - }, [exportActions, base.selectedFiles, selectors, consumeFiles]); + }, [exportActions, base.selectedFiles, selectors, consumeFiles, signatureApiRef, getImageData]); const getSteps = () => { const steps = [];