From 50e60d497241872b54a8e0237a92a40b50332bd2 Mon Sep 17 00:00:00 2001 From: Reece Date: Fri, 26 Sep 2025 16:27:52 +0100 Subject: [PATCH] Simple export block --- frontend/src/components/shared/RightRail.tsx | 31 +- frontend/src/contexts/SignatureContext.tsx | 16 + frontend/src/tools/Sign.tsx | 364 ++----------------- frontend/src/utils/signatureFlattening.ts | 20 +- 4 files changed, 64 insertions(+), 367 deletions(-) diff --git a/frontend/src/components/shared/RightRail.tsx b/frontend/src/components/shared/RightRail.tsx index 6a31a8265..41f289106 100644 --- a/frontend/src/components/shared/RightRail.tsx +++ b/frontend/src/components/shared/RightRail.tsx @@ -17,6 +17,7 @@ import { ViewerContext } from '../../contexts/ViewerContext'; import { useSignature } from '../../contexts/SignatureContext'; import { parseSelection } from '../../utils/bulkselection/parseSelection'; +import { flattenSignatures } from '../../utils/signatureFlattening'; import { PDFDocument, rgb } from 'pdf-lib'; import { generateThumbnailWithMetadata } from '../../utils/thumbnailUtils'; import { createProcessedFile } from '../../contexts/file/fileActions'; @@ -48,8 +49,8 @@ export default function RightRail() { const { selectedFiles, selectedFileIds, setSelectedFiles } = useFileSelection(); const { removeFiles } = useFileManagement(); - // Signature context for flattening signatures when in sign mode - const { signatureApiRef, getImageData } = useSignature(); + // Signature context for checking if signatures have been applied + const { signaturesApplied } = useSignature(); const activeFiles = selectors.getFiles(); const filesSignature = selectors.getFilesSignature(); @@ -108,26 +109,10 @@ export default function RightRail() { const handleExportAll = useCallback(async () => { if (currentView === 'viewer') { - // Check if we're in sign mode and warn about unapplied signatures - if (signatureApiRef?.current) { - // Quick check for any annotations - let hasAnnotations = false; - for (let pageIndex = 0; pageIndex < 5; pageIndex++) { - try { - const pageAnnotations = await signatureApiRef.current.getPageAnnotations(pageIndex); - if (pageAnnotations && pageAnnotations.length > 0) { - hasAnnotations = true; - break; - } - } catch (e) { - break; - } - } - - if (hasAnnotations) { - alert('You have unapplied signatures. Please use "Apply Signatures" first before exporting.'); - return; - } + // Check if signatures have been applied + if (!signaturesApplied) { + alert('You have unapplied signatures. Please use "Apply Signatures" first before exporting.'); + return; } // Use EmbedPDF export functionality for viewer mode @@ -149,7 +134,7 @@ export default function RightRail() { // Export all pages (not just selected) pageEditorFunctions?.onExportAll?.(); } - }, [currentView, activeFiles, selectedFiles, pageEditorFunctions, viewerContext]); + }, [currentView, activeFiles, selectedFiles, pageEditorFunctions, viewerContext, signaturesApplied, selectors, fileActions]); const handleCloseSelected = useCallback(() => { if (currentView !== 'fileEditor') return; diff --git a/frontend/src/contexts/SignatureContext.tsx b/frontend/src/contexts/SignatureContext.tsx index cdde395b7..b047d3bb4 100644 --- a/frontend/src/contexts/SignatureContext.tsx +++ b/frontend/src/contexts/SignatureContext.tsx @@ -9,6 +9,8 @@ interface SignatureState { signatureConfig: SignParameters | null; // Whether we're in signature placement mode isPlacementMode: boolean; + // Whether signatures have been applied (allows export) + signaturesApplied: boolean; } // Signature actions interface @@ -24,6 +26,7 @@ interface SignatureActions { redo: () => void; storeImageData: (id: string, data: string) => void; getImageData: (id: string) => string | undefined; + setSignaturesApplied: (applied: boolean) => void; } // Combined context interface @@ -39,6 +42,7 @@ const SignatureContext = createContext(undefi const initialState: SignatureState = { signatureConfig: null, isPlacementMode: false, + signaturesApplied: true, // Start as true (no signatures placed yet) }; // Provider component @@ -67,6 +71,8 @@ export const SignatureProvider: React.FC<{ children: ReactNode }> = ({ children if (signatureApiRef.current) { signatureApiRef.current.activateDrawMode(); setPlacementMode(true); + // Mark signatures as not applied when entering draw mode + setState(prev => ({ ...prev, signaturesApplied: false })); } }, [setPlacementMode]); @@ -81,6 +87,8 @@ export const SignatureProvider: React.FC<{ children: ReactNode }> = ({ children if (signatureApiRef.current) { signatureApiRef.current.activateSignaturePlacementMode(); setPlacementMode(true); + // Mark signatures as not applied when placing new signatures + setState(prev => ({ ...prev, signaturesApplied: false })); } }, [setPlacementMode]); @@ -117,6 +125,13 @@ export const SignatureProvider: React.FC<{ children: ReactNode }> = ({ children return imageDataStore.current.get(id); }, []); + const setSignaturesApplied = useCallback((applied: boolean) => { + setState(prev => ({ + ...prev, + signaturesApplied: applied, + })); + }, []); + // No auto-activation - all modes use manual buttons const contextValue: SignatureContextValue = { @@ -134,6 +149,7 @@ export const SignatureProvider: React.FC<{ children: ReactNode }> = ({ children redo, storeImageData, getImageData, + setSignaturesApplied, }; return ( diff --git a/frontend/src/tools/Sign.tsx b/frontend/src/tools/Sign.tsx index c462882be..cd9b4b44f 100644 --- a/frontend/src/tools/Sign.tsx +++ b/frontend/src/tools/Sign.tsx @@ -19,7 +19,7 @@ import { flattenSignatures } from "../utils/signatureFlattening"; const Sign = (props: BaseToolProps) => { const { t } = useTranslation(); const { setWorkbench } = useNavigation(); - const { setSignatureConfig, activateDrawMode, activateSignaturePlacementMode, deactivateDrawMode, updateDrawSettings, undo, redo, isPlacementMode, signatureApiRef, getImageData } = useSignature(); + const { setSignatureConfig, activateDrawMode, activateSignaturePlacementMode, deactivateDrawMode, updateDrawSettings, undo, redo, isPlacementMode, signatureApiRef, getImageData, setSignaturesApplied } = useSignature(); const { actions } = useFileActions(); const { consumeFiles, selectors } = useFileContext(); const { exportActions } = useViewer(); @@ -55,346 +55,40 @@ const Sign = (props: BaseToolProps) => { // Save signed files to the system - apply signatures using EmbedPDF and replace original const handleSaveToSystem = useCallback(async () => { try { - // 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 + // Get the original file + let originalFile = null; + if (base.selectedFiles.length > 0) { + originalFile = base.selectedFiles[0]; + } else { + const allFileIds = selectors.getAllFileIds(); + if (allFileIds.length > 0) { + const stirlingFile = selectors.getFile(allFileIds[0]); + if (stirlingFile) { + originalFile = stirlingFile; } } } - 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); - } - } - } + if (!originalFile) { + console.error('No file available to replace'); + return; } - // Step 3: Use EmbedPDF's saveAsCopy to get the base PDF (now without annotations) - const pdfArrayBuffer = await exportActions.saveAsCopy(); + // Use the signature flattening utility + const success = await flattenSignatures({ + signatureApiRef, + getImageData, + exportActions, + selectors, + consumeFiles, + originalFile + }); - if (pdfArrayBuffer) { - console.log(`EmbedPDF exported PDF size: ${pdfArrayBuffer.byteLength} bytes`); + if (success) { + console.log('✓ Signature flattening completed successfully'); - // Try loading with more permissive PDF-lib options - console.log('Attempting to load PDF with PDF-lib...'); - - // Convert ArrayBuffer to File - let blob = new Blob([pdfArrayBuffer], { type: 'application/pdf' }); - - // Get the current file - try from base.selectedFiles first, then from all files - let originalFile = null; - if (base.selectedFiles.length > 0) { - originalFile = base.selectedFiles[0]; - } else { - const allFileIds = selectors.getAllFileIds(); - if (allFileIds.length > 0) { - const fileStub = selectors.getStirlingFileStub(allFileIds[0]); - const fileObject = selectors.getFile(allFileIds[0]); - if (fileStub && fileObject) { - originalFile = createStirlingFile(fileObject, allFileIds[0]); - } - } - } - - if (!originalFile) { - console.error('No file available to replace'); - return; - } - - 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); - const processedFileMetadata = createProcessedFile(thumbnailResult.pageCount, thumbnailResult.thumbnail); - - // Prepare input file data for replacement - const inputFileIds: FileId[] = [originalFile.fileId]; - const inputStirlingFileStubs: StirlingFileStub[] = []; - - const record = selectors.getStirlingFileStub(originalFile.fileId); - if (record) { - inputStirlingFileStubs.push(record); - } else { - console.error('No file record found for:', originalFile.fileId); - return; - } - - // Create output stub and file - const outputStub = createNewStirlingFileStub(signedFile, undefined, thumbnailResult.thumbnail, processedFileMetadata); - const outputStirlingFile = createStirlingFile(signedFile, outputStub.id); - - // Replace the original file with the signed version - await consumeFiles(inputFileIds, [outputStirlingFile], [outputStub]); - - console.log('✓ File replaced in context, new file ID:', outputStub.id); + // Mark signatures as applied + setSignaturesApplied(true); // Force refresh the viewer to show the flattened PDF setTimeout(() => { @@ -411,11 +105,13 @@ const Sign = (props: BaseToolProps) => { } }, 100); }, 200); + } else { + console.error('Signature flattening failed'); } } catch (error) { console.error('Error saving signed document:', error); } - }, [exportActions, base.selectedFiles, selectors, consumeFiles, signatureApiRef, getImageData]); + }, [exportActions, base.selectedFiles, selectors, consumeFiles, signatureApiRef, getImageData, flattenSignatures, setWorkbench, activateDrawMode]); const getSteps = () => { const steps = []; diff --git a/frontend/src/utils/signatureFlattening.ts b/frontend/src/utils/signatureFlattening.ts index 1bcebd4db..5943046e0 100644 --- a/frontend/src/utils/signatureFlattening.ts +++ b/frontend/src/utils/signatureFlattening.ts @@ -1,21 +1,17 @@ import { PDFDocument, rgb } from 'pdf-lib'; import { generateThumbnailWithMetadata } from './thumbnailUtils'; import { createProcessedFile } from '../contexts/file/fileActions'; -import { createNewStirlingFileStub, createStirlingFile, StirlingFile, FileId } from '../types/fileContext'; +import { createNewStirlingFileStub, createStirlingFile, StirlingFile, FileId, StirlingFileStub } from '../types/fileContext'; import type { SignatureAPI } from '../components/viewer/SignatureAPIBridge'; interface SignatureFlatteningOptions { - signatureApiRef: React.RefObject; - getImageData: (id: string) => string | null; - exportActions: { + signatureApiRef: React.RefObject; + getImageData: (id: string) => string | undefined; + exportActions?: { saveAsCopy: () => Promise; }; - selectors: { - getStirlingFileStub: (fileId: string) => any; - getFile: (fileId: string) => File | null; - getAllFileIds: () => string[]; - }; - consumeFiles: (inputFileIds: FileId[], outputFiles: StirlingFile[], outputStubs: any[]) => Promise; + selectors: any; // FileContextSelectors - using any to avoid complex type matching + consumeFiles: (inputFileIds: FileId[], outputStirlingFiles: StirlingFile[], outputStirlingFileStubs: StirlingFileStub[]) => Promise; originalFile?: StirlingFile; } @@ -84,6 +80,10 @@ export async function flattenSignatures(options: SignatureFlatteningOptions): Pr } // Step 3: Use EmbedPDF's saveAsCopy to get the base PDF (now without annotations) + if (!exportActions) { + console.error('No export actions available'); + return false; + } const pdfArrayBuffer = await exportActions.saveAsCopy(); if (pdfArrayBuffer) {