mirror of
https://github.com/Frooodle/Stirling-PDF.git
synced 2025-09-26 17:52:59 +02:00
Only flatten current annotations
This commit is contained in:
parent
172f622c5f
commit
a22330ebf4
@ -14,8 +14,13 @@ import { Tooltip } from '../shared/Tooltip';
|
||||
import BulkSelectionPanel from '../pageEditor/BulkSelectionPanel';
|
||||
import { SearchInterface } from '../viewer/SearchInterface';
|
||||
import { ViewerContext } from '../../contexts/ViewerContext';
|
||||
import { useSignature } from '../../contexts/SignatureContext';
|
||||
|
||||
import { parseSelection } from '../../utils/bulkselection/parseSelection';
|
||||
import { PDFDocument, rgb } from 'pdf-lib';
|
||||
import { generateThumbnailWithMetadata } from '../../utils/thumbnailUtils';
|
||||
import { createProcessedFile } from '../../contexts/file/fileActions';
|
||||
import { createNewStirlingFileStub, createStirlingFile } from '../../types/fileContext';
|
||||
|
||||
|
||||
export default function RightRail() {
|
||||
@ -43,6 +48,9 @@ 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();
|
||||
|
||||
const activeFiles = selectors.getFiles();
|
||||
const filesSignature = selectors.getFilesSignature();
|
||||
|
||||
@ -98,8 +106,30 @@ export default function RightRail() {
|
||||
}
|
||||
}, [currentView, setSelectedFiles, pageEditorFunctions]);
|
||||
|
||||
const handleExportAll = useCallback(() => {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
// Use EmbedPDF export functionality for viewer mode
|
||||
viewerContext?.exportActions?.download();
|
||||
} else if (currentView === 'fileEditor') {
|
||||
|
@ -1,5 +1,6 @@
|
||||
import React, { createContext, useContext, useState, ReactNode, useRef } from 'react';
|
||||
import { SpreadMode } from '@embedpdf/plugin-spread/react';
|
||||
import { useNavigation } from './NavigationContext';
|
||||
|
||||
// Bridge API interfaces - these match what the bridges provide
|
||||
interface ScrollAPIWrapper {
|
||||
@ -208,6 +209,9 @@ export const ViewerProvider: React.FC<ViewerProviderProps> = ({ children }) => {
|
||||
// UI state - only state directly managed by this context
|
||||
const [isThumbnailSidebarVisible, setIsThumbnailSidebarVisible] = useState(false);
|
||||
|
||||
// Get current navigation state to check if we're in sign mode
|
||||
const { workbench } = useNavigation();
|
||||
|
||||
// Bridge registry - bridges register their state and APIs here
|
||||
const bridgeRefs = useRef({
|
||||
scroll: null as BridgeRef<ScrollState, ScrollAPIWrapper> | null,
|
||||
|
@ -14,6 +14,7 @@ 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';
|
||||
import { flattenSignatures } from "../utils/signatureFlattening";
|
||||
|
||||
const Sign = (props: BaseToolProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
339
frontend/src/utils/signatureFlattening.ts
Normal file
339
frontend/src/utils/signatureFlattening.ts
Normal file
@ -0,0 +1,339 @@
|
||||
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 type { SignatureAPI } from '../components/viewer/SignatureAPIBridge';
|
||||
|
||||
interface SignatureFlatteningOptions {
|
||||
signatureApiRef: React.RefObject<SignatureAPI>;
|
||||
getImageData: (id: string) => string | null;
|
||||
exportActions: {
|
||||
saveAsCopy: () => Promise<ArrayBuffer | null>;
|
||||
};
|
||||
selectors: {
|
||||
getStirlingFileStub: (fileId: string) => any;
|
||||
getFile: (fileId: string) => File | null;
|
||||
getAllFileIds: () => string[];
|
||||
};
|
||||
consumeFiles: (inputFileIds: FileId[], outputFiles: StirlingFile[], outputStubs: any[]) => Promise<void>;
|
||||
originalFile?: StirlingFile;
|
||||
}
|
||||
|
||||
export async function flattenSignatures(options: SignatureFlatteningOptions): Promise<boolean> {
|
||||
const { signatureApiRef, getImageData, exportActions, selectors, consumeFiles, originalFile } = options;
|
||||
|
||||
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...');
|
||||
|
||||
for (let pageIndex = 0; pageIndex < 10; pageIndex++) {
|
||||
try {
|
||||
const pageAnnotations = await signatureApiRef.current.getPageAnnotations(pageIndex);
|
||||
if (pageAnnotations && pageAnnotations.length > 0) {
|
||||
// Filter to only include annotations added in this session
|
||||
const sessionAnnotations = pageAnnotations.filter(annotation => {
|
||||
// Check if this annotation has stored image data (indicates it was added this session)
|
||||
const hasStoredImageData = annotation.id && getImageData(annotation.id);
|
||||
|
||||
// Also check if it has image data directly in the annotation (new signatures)
|
||||
const hasDirectImageData = annotation.imageData || annotation.appearance ||
|
||||
annotation.stampData || annotation.imageSrc ||
|
||||
annotation.contents || annotation.data;
|
||||
|
||||
const isSessionAnnotation = hasStoredImageData || (hasDirectImageData && typeof hasDirectImageData === 'string' && hasDirectImageData.startsWith('data:image'));
|
||||
|
||||
if (isSessionAnnotation) {
|
||||
console.log(`Including session annotation ${annotation.id} from page ${pageIndex + 1}`);
|
||||
} else {
|
||||
console.log(`Skipping existing annotation ${annotation.id} from page ${pageIndex + 1} (not added in this session)`);
|
||||
}
|
||||
|
||||
return isSessionAnnotation;
|
||||
});
|
||||
|
||||
if (sessionAnnotations.length > 0) {
|
||||
allAnnotations.push({pageIndex, annotations: sessionAnnotations});
|
||||
console.log(`Found ${sessionAnnotations.length} session annotations on page ${pageIndex + 1} (out of ${pageAnnotations.length} total)`);
|
||||
}
|
||||
}
|
||||
} catch (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 ONLY session annotations from EmbedPDF before export (they'll be rendered manually)
|
||||
// Leave old annotations alone - they will remain as annotations in the PDF
|
||||
if (allAnnotations.length > 0 && signatureApiRef?.current) {
|
||||
console.log('Deleting session 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 session 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
|
||||
let blob = new Blob([pdfArrayBuffer], { type: 'application/pdf' });
|
||||
|
||||
// Get the current file - try from originalFile first, then from all files
|
||||
let currentFile = originalFile;
|
||||
if (!currentFile) {
|
||||
const allFileIds = selectors.getAllFileIds();
|
||||
if (allFileIds.length > 0) {
|
||||
const fileStub = selectors.getStirlingFileStub(allFileIds[0]);
|
||||
const fileObject = selectors.getFile(allFileIds[0]);
|
||||
if (fileStub && fileObject) {
|
||||
currentFile = createStirlingFile(fileObject, allFileIds[0] as FileId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!currentFile) {
|
||||
console.error('No file available to replace');
|
||||
return false;
|
||||
}
|
||||
|
||||
let signedFile = new File([blob], currentFile.name, { type: 'application/pdf' });
|
||||
|
||||
// Step 4: 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;
|
||||
|
||||
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);
|
||||
|
||||
const rect = annotation.rect || annotation.bounds || annotation.rectangle || annotation.position;
|
||||
|
||||
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
|
||||
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
|
||||
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 storage
|
||||
if (!imageDataUrl && annotation.id) {
|
||||
const storedImageData = getImageData(annotation.id);
|
||||
if (storedImageData) {
|
||||
imageDataUrl = storedImageData;
|
||||
}
|
||||
}
|
||||
|
||||
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));
|
||||
|
||||
// Embed image in PDF based on data URL type
|
||||
let image;
|
||||
if (imageDataUrl.includes('data:image/jpeg') || imageDataUrl.includes('data:image/jpg')) {
|
||||
image = await pdfDoc.embedJpg(imageBytes);
|
||||
} else if (imageDataUrl.includes('data:image/png')) {
|
||||
image = await pdfDoc.embedPng(imageBytes);
|
||||
} else {
|
||||
image = await pdfDoc.embedPng(imageBytes);
|
||||
}
|
||||
|
||||
// Draw image on page at annotation position
|
||||
page.drawImage(image, {
|
||||
x: pdfX,
|
||||
y: pdfY,
|
||||
width: width,
|
||||
height: height,
|
||||
});
|
||||
|
||||
totalRendered++;
|
||||
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++;
|
||||
console.log(`Rendered text annotation: "${annotation.content || annotation.text}"`);
|
||||
} else if (annotation.type === 14 || annotation.type === 15) {
|
||||
// Handle ink annotations (drawn signatures)
|
||||
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
|
||||
});
|
||||
|
||||
page.drawText('Drawn Signature', {
|
||||
x: pdfX + 5,
|
||||
y: pdfY + height / 2,
|
||||
size: 10,
|
||||
color: rgb(0, 0, 0)
|
||||
});
|
||||
|
||||
totalRendered++;
|
||||
console.log(`Rendered ink annotation placeholder at (${pdfX}, ${pdfY}) size (${width}x${height})`);
|
||||
} else {
|
||||
// Handle other annotation 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++;
|
||||
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`);
|
||||
|
||||
// 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], currentFile.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[] = [currentFile.fileId];
|
||||
|
||||
const record = selectors.getStirlingFileStub(currentFile.fileId);
|
||||
if (!record) {
|
||||
console.error('No file record found for:', currentFile.fileId);
|
||||
return false;
|
||||
}
|
||||
|
||||
// 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('✓ Signature flattening completed successfully');
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
} catch (error) {
|
||||
console.error('Error flattening signatures:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user