diff --git a/frontend/src/components/pageEditor/PageEditor.tsx b/frontend/src/components/pageEditor/PageEditor.tsx index 21403731d..f42a16143 100644 --- a/frontend/src/components/pageEditor/PageEditor.tsx +++ b/frontend/src/components/pageEditor/PageEditor.tsx @@ -308,6 +308,26 @@ const PageEditor = ({ undoManagerRef.current.executeCommand(reorderCommand); }, [displayDocument]); + // Helper function to collect source files for multi-file export + const getSourceFiles = useCallback((): Map | null => { + const sourceFiles = new Map(); + + // Check if we have multiple files by looking at active file IDs + if (activeFileIds.length <= 1) { + return null; // Use single-file export method + } + + // Collect all source files + activeFileIds.forEach(fileId => { + const file = selectors.getFile(fileId); + if (file) { + sourceFiles.set(fileId, file); + } + }); + + return sourceFiles.size > 0 ? sourceFiles : null; + }, [activeFileIds, selectors]); + const onExportSelected = useCallback(async () => { @@ -334,11 +354,20 @@ const PageEditor = ({ // Step 3: Export with pdfExportService console.log('Exporting selected pages:', selectedPageNumbers, 'with DOM rotations applied'); - const result = await pdfExportService.exportPDF( - documentWithDOMState, - selectedPageIds, - { selectedOnly: true, filename: documentWithDOMState.name } - ); + + const sourceFiles = getSourceFiles(); + const result = sourceFiles + ? await pdfExportService.exportPDFMultiFile( + documentWithDOMState, + sourceFiles, + selectedPageIds, + { selectedOnly: true, filename: documentWithDOMState.name } + ) + : await pdfExportService.exportPDF( + documentWithDOMState, + selectedPageIds, + { selectedOnly: true, filename: documentWithDOMState.name } + ); // Step 4: Download the result pdfExportService.downloadFile(result.blob, result.filename); @@ -348,7 +377,7 @@ const PageEditor = ({ console.error('Export failed:', error); setExportLoading(false); } - }, [displayDocument, selectedPageNumbers, mergedPdfDocument, splitPositions]); + }, [displayDocument, selectedPageNumbers, mergedPdfDocument, splitPositions, getSourceFiles]); const onExportAll = useCallback(async () => { if (!displayDocument) return; @@ -370,8 +399,11 @@ const PageEditor = ({ const blobs: Blob[] = []; const filenames: string[] = []; + const sourceFiles = getSourceFiles(); for (const doc of processedDocuments) { - const result = await pdfExportService.exportPDF(doc, [], { filename: doc.name }); + const result = sourceFiles + ? await pdfExportService.exportPDFMultiFile(doc, sourceFiles, [], { filename: doc.name }) + : await pdfExportService.exportPDF(doc, [], { filename: doc.name }); blobs.push(result.blob); filenames.push(result.filename); } @@ -391,11 +423,19 @@ const PageEditor = ({ } else { // Single document - regular export console.log('Exporting as single PDF'); - const result = await pdfExportService.exportPDF( - processedDocuments, - [], - { selectedOnly: false, filename: processedDocuments.name } - ); + const sourceFiles = getSourceFiles(); + const result = sourceFiles + ? await pdfExportService.exportPDFMultiFile( + processedDocuments, + sourceFiles, + [], + { selectedOnly: false, filename: processedDocuments.name } + ) + : await pdfExportService.exportPDF( + processedDocuments, + [], + { selectedOnly: false, filename: processedDocuments.name } + ); pdfExportService.downloadFile(result.blob, result.filename); } @@ -405,7 +445,7 @@ const PageEditor = ({ console.error('Export failed:', error); setExportLoading(false); } - }, [displayDocument, mergedPdfDocument, splitPositions]); + }, [displayDocument, mergedPdfDocument, splitPositions, getSourceFiles]); // Apply DOM changes to document state using dedicated service const applyChanges = useCallback(() => { @@ -587,7 +627,7 @@ const PageEditor = ({ page={page} index={index} totalPages={displayDocument.pages.length} - originalFile={activeFileIds.length === 1 && primaryFileId ? selectors.getFile(primaryFileId) : undefined} + originalFile={(page as any).originalFileId ? selectors.getFile((page as any).originalFileId) : undefined} selectedPages={selectedPageNumbers} selectionMode={selectionMode} movingPage={movingPage} diff --git a/frontend/src/components/pageEditor/PageThumbnail.tsx b/frontend/src/components/pageEditor/PageThumbnail.tsx index f734e2b85..2cc4e986c 100644 --- a/frontend/src/components/pageEditor/PageThumbnail.tsx +++ b/frontend/src/components/pageEditor/PageThumbnail.tsx @@ -68,6 +68,18 @@ const PageThumbnail: React.FC = ({ const [thumbnailUrl, setThumbnailUrl] = useState(page.thumbnail); const { getThumbnailFromCache, requestThumbnail } = useThumbnailGeneration(); + // Calculate document aspect ratio from first non-blank page + const getDocumentAspectRatio = useCallback(() => { + // Find first non-blank page with a thumbnail to get aspect ratio + const firstRealPage = pdfDocument.pages.find(p => !p.isBlankPage && p.thumbnail); + if (firstRealPage?.thumbnail) { + // Try to get aspect ratio from an actual thumbnail image + // For now, default to A4 but could be enhanced to measure image dimensions + return '1 / 1.414'; // A4 ratio as fallback + } + return '1 / 1.414'; // Default A4 ratio + }, [pdfDocument.pages]); + // Update thumbnail URL when page prop changes useEffect(() => { if (page.thumbnail && page.thumbnail !== thumbnailUrl) { @@ -329,11 +341,20 @@ const PageThumbnail: React.FC = ({ > {page.isBlankPage ? (
+ width: '100%', + height: '100%', + display: 'flex', + alignItems: 'center', + justifyContent: 'center' + }}> +
+ ) : thumbnailUrl ? ( , + selectedPageIds: string[] = [], + options: ExportOptions = {} + ): Promise<{ blob: Blob; filename: string }> { + const { selectedOnly = false, filename } = options; + + try { + // Determine which pages to export + const pagesToExport = selectedOnly && selectedPageIds.length > 0 + ? pdfDocument.pages.filter(page => selectedPageIds.includes(page.id)) + : pdfDocument.pages; + + if (pagesToExport.length === 0) { + throw new Error('No pages to export'); + } + + const blob = await this.createMultiSourceDocument(sourceFiles, pagesToExport); + const exportFilename = this.generateFilename(filename || pdfDocument.name, selectedOnly); + + return { blob, filename: exportFilename }; + } catch (error) { + console.error('Multi-file PDF export error:', error); + throw new Error(`Failed to export PDF: ${error instanceof Error ? error.message : 'Unknown error'}`); + } + } + + /** + * Create a PDF document from multiple source files + */ + private async createMultiSourceDocument( + sourceFiles: Map, + pages: PDFPage[] + ): Promise { + const newDoc = await PDFLibDocument.create(); + + // Load all source documents once and cache them + const loadedDocs = new Map(); + + for (const [fileId, file] of sourceFiles) { + try { + const arrayBuffer = await file.arrayBuffer(); + const doc = await PDFLibDocument.load(arrayBuffer); + loadedDocs.set(fileId, doc); + } catch (error) { + console.warn(`Failed to load source file ${fileId}:`, error); + } + } + + for (const page of pages) { + if (page.isBlankPage || page.originalPageNumber === -1) { + // Create a blank page + const blankPage = newDoc.addPage(PageSizes.A4); + + // Apply rotation if needed + if (page.rotation !== 0) { + blankPage.setRotation(degrees(page.rotation)); + } + } else if (page.originalFileId && loadedDocs.has(page.originalFileId)) { + // Get the correct source document for this page + const sourceDoc = loadedDocs.get(page.originalFileId)!; + const sourcePageIndex = page.originalPageNumber - 1; + + if (sourcePageIndex >= 0 && sourcePageIndex < sourceDoc.getPageCount()) { + // Copy the page from the correct source document + const [copiedPage] = await newDoc.copyPages(sourceDoc, [sourcePageIndex]); + + // Apply rotation + if (page.rotation !== 0) { + copiedPage.setRotation(degrees(page.rotation)); + } + + newDoc.addPage(copiedPage); + } + } else { + console.warn(`Cannot find source document for page ${page.pageNumber} (fileId: ${page.originalFileId})`); + } + } + + // Set metadata + newDoc.setCreator('Stirling PDF'); + newDoc.setProducer('Stirling PDF'); + newDoc.setCreationDate(new Date()); + newDoc.setModificationDate(new Date()); + + const pdfBytes = await newDoc.save(); + return new Blob([pdfBytes], { type: 'application/pdf' }); + } + + /** + * Create a single PDF document with all operations applied (single source) */ private async createSingleDocument( sourceDoc: PDFLibDocument, diff --git a/frontend/src/types/pageEditor.ts b/frontend/src/types/pageEditor.ts index dd27ef627..32b64a933 100644 --- a/frontend/src/types/pageEditor.ts +++ b/frontend/src/types/pageEditor.ts @@ -7,6 +7,7 @@ export interface PDFPage { selected: boolean; splitAfter?: boolean; isBlankPage?: boolean; + originalFileId?: string; } export interface PDFDocument {