From abed82cc6b9d996d2ceab47e39438b23935e758b Mon Sep 17 00:00:00 2001 From: Reece Browne Date: Fri, 22 Aug 2025 15:39:30 +0100 Subject: [PATCH] Page editor continued improvements --- .../src/components/pageEditor/PageEditor.tsx | 174 ++++++++++++------ .../components/pageEditor/PageThumbnail.tsx | 29 +-- frontend/src/hooks/usePDFProcessor.ts | 1 + .../services/documentManipulationService.ts | 162 ++++++++++++++++ frontend/src/services/pdfExportService.ts | 100 ++-------- frontend/src/types/pageEditor.ts | 3 +- 6 files changed, 314 insertions(+), 155 deletions(-) create mode 100644 frontend/src/services/documentManipulationService.ts diff --git a/frontend/src/components/pageEditor/PageEditor.tsx b/frontend/src/components/pageEditor/PageEditor.tsx index 03ff3ba60..ea4f82e0b 100644 --- a/frontend/src/components/pageEditor/PageEditor.tsx +++ b/frontend/src/components/pageEditor/PageEditor.tsx @@ -11,6 +11,7 @@ import { PDFDocument, PDFPage } from "../../types/pageEditor"; import { ProcessedFile as EnhancedProcessedFile } from "../../types/processing"; import { useUndoRedo } from "../../hooks/useUndoRedo"; import { pdfExportService } from "../../services/pdfExportService"; +import { documentManipulationService } from "../../services/documentManipulationService"; import { enhancedPDFProcessingService } from "../../services/enhancedPDFProcessingService"; import { fileProcessingService } from "../../services/fileProcessingService"; import { pdfProcessingService } from "../../services/pdfProcessingService"; @@ -42,7 +43,7 @@ class RotatePageCommand extends DOMCommand { } execute(): void { - // Find the page thumbnail and rotate it directly in the DOM + // Only update DOM for immediate visual feedback const pageElement = document.querySelector(`[data-page-id="${this.pageId}"]`); if (pageElement) { const img = pageElement.querySelector('img'); @@ -55,6 +56,7 @@ class RotatePageCommand extends DOMCommand { } undo(): void { + // Only update DOM const pageElement = document.querySelector(`[data-page-id="${this.pageId}"]`); if (pageElement) { const img = pageElement.querySelector('img'); @@ -128,6 +130,7 @@ export interface PageEditorProps { showExportPreview: (selectedOnly: boolean) => void; onExportSelected: () => void; onExportAll: () => void; + applyChanges: () => void; exportLoading: boolean; selectionMode: boolean; selectedPages: number[]; @@ -267,8 +270,8 @@ const PageEditor = ({ thumbnail: page.thumbnail || null, rotation: page.rotation || 0, selected: false, - splitBefore: page.splitBefore || false, - originalPageNumber: page.pageNumber, + splitAfter: page.splitAfter || false, + originalPageNumber: page.originalPageNumber || page.pageNumber || pageIndex + 1, originalFileId: fileId, })); } else if (processedFile?.totalPages) { @@ -282,7 +285,7 @@ const PageEditor = ({ rotation: 0, thumbnail: null, // Will be generated later selected: false, - splitBefore: false, + splitAfter: false, })); } @@ -442,9 +445,30 @@ const PageEditor = ({ } class ToggleSplitCommand { - constructor(public pageId: string) {} + constructor(public pageIds: string[]) {} execute() { - console.log('Toggle split:', this.pageId); + if (!displayDocument) return; + + console.log('Toggle split:', this.pageIds); + + // Create new pages array with toggled split markers + const newPages = displayDocument.pages.map(page => { + if (this.pageIds.includes(page.id)) { + return { + ...page, + splitAfter: !page.splitAfter + }; + } + return page; + }); + + // Update the document with new split markers + const updatedDocument: PDFDocument = { + ...displayDocument, + pages: newPages, + }; + + setEditedDocument(updatedDocument); } } @@ -484,8 +508,21 @@ const PageEditor = ({ }, [selectedPageNumbers]); const handleSplit = useCallback(() => { - console.log('Split at selected pages:', selectedPageNumbers); - }, [selectedPageNumbers]); + if (!displayDocument || selectedPageNumbers.length === 0) return; + + console.log('Toggle split markers at selected pages:', selectedPageNumbers); + + // Get page IDs for selected pages + const selectedPageIds = selectedPageNumbers.map(pageNum => { + const page = displayDocument.pages.find(p => p.pageNumber === pageNum); + return page?.id || ''; + }).filter(id => id); + + if (selectedPageIds.length > 0) { + const command = new ToggleSplitCommand(selectedPageIds); + command.execute(); + } + }, [selectedPageNumbers, displayDocument]); const handleReorderPages = useCallback((sourcePageNumber: number, targetIndex: number, selectedPages?: number[]) => { if (!displayDocument) return; @@ -535,6 +572,9 @@ const PageEditor = ({ totalPages: newPages.length, }; + console.log('Reordered document page numbers:', newPages.map(p => p.pageNumber)); + console.log('Reordered document page IDs:', newPages.map(p => p.id)); + // Update the edited document state setEditedDocument(reorderedDocument); @@ -542,42 +582,23 @@ const PageEditor = ({ }, [displayDocument]); - // Helper function to read DOM state and update document with current rotations - const updateDocumentWithDOMState = useCallback((pdfDocument: PDFDocument): PDFDocument => { - const updatedPages = pdfDocument.pages.map(page => { - // Find the DOM element for this page - const pageElement = document.querySelector(`[data-page-id="${page.id}"]`); - if (pageElement) { - const img = pageElement.querySelector('img'); - if (img && img.style.rotate) { - // Parse rotation from DOM (e.g., "90deg" -> 90) - const rotationMatch = img.style.rotate.match(/-?\d+/); - const domRotation = rotationMatch ? parseInt(rotationMatch[0]) : 0; - - return { - ...page, - rotation: domRotation // Update page rotation from DOM state - }; - } - } - return page; - }); - - return { - ...pdfDocument, - pages: updatedPages - }; - }, []); const onExportSelected = useCallback(async () => { if (!displayDocument || selectedPageNumbers.length === 0) return; setExportLoading(true); try { - // Step 1: Update document with current DOM state (rotations) - const documentWithDOMState = updateDocumentWithDOMState(displayDocument); + // Step 1: Apply DOM changes to document state first + console.log('Applying DOM changes before export...'); + const processedDocuments = documentManipulationService.applyDOMChangesToDocument( + mergedPdfDocument || displayDocument, // Original order + displayDocument // Current display order (includes reordering) + ); - // Step 2: Get page IDs for selected pages + // For selected pages export, we work with the first document (or single document) + const documentWithDOMState = Array.isArray(processedDocuments) ? processedDocuments[0] : processedDocuments; + + // Step 2: Convert selected page numbers to page IDs from the document with DOM state const selectedPageIds = selectedPageNumbers.map(pageNum => { const page = documentWithDOMState.pages.find(p => p.pageNumber === pageNum); return page?.id || ''; @@ -592,35 +613,61 @@ const PageEditor = ({ ); // Step 4: Download the result - if ('blob' in result) { - pdfExportService.downloadFile(result.blob, result.filename); - } + pdfExportService.downloadFile(result.blob, result.filename); setExportLoading(false); } catch (error) { console.error('Export failed:', error); setExportLoading(false); } - }, [displayDocument, selectedPageNumbers, updateDocumentWithDOMState]); + }, [displayDocument, selectedPageNumbers, mergedPdfDocument]); const onExportAll = useCallback(async () => { if (!displayDocument) return; setExportLoading(true); try { - // Step 1: Update document with current DOM state (rotations) - const documentWithDOMState = updateDocumentWithDOMState(displayDocument); - - // Step 2: Export all pages with pdfExportService - console.log('Exporting all pages with DOM rotations applied'); - const result = await pdfExportService.exportPDF( - documentWithDOMState, - [], - { selectedOnly: false, filename: documentWithDOMState.name } + // Step 1: Apply DOM changes to document state first + console.log('Applying DOM changes before export...'); + const processedDocuments = documentManipulationService.applyDOMChangesToDocument( + mergedPdfDocument || displayDocument, // Original order + displayDocument // Current display order (includes reordering) ); + + // Step 2: Check if we have multiple documents (splits) or single document + if (Array.isArray(processedDocuments)) { + // Multiple documents (splits) - export as ZIP + console.log('Exporting multiple split documents:', processedDocuments.length); + const blobs: Blob[] = []; + const filenames: string[] = []; + + for (const doc of processedDocuments) { + const result = await pdfExportService.exportPDF(doc, [], { filename: doc.name }); + blobs.push(result.blob); + filenames.push(result.filename); + } + + // Create ZIP file + const JSZip = await import('jszip'); + const zip = new JSZip.default(); + + blobs.forEach((blob, index) => { + zip.file(filenames[index], blob); + }); + + const zipBlob = await zip.generateAsync({ type: 'blob' }); + const zipFilename = displayDocument.name.replace(/\.pdf$/i, '_split.zip'); + + pdfExportService.downloadFile(zipBlob, zipFilename); + } else { + // Single document - regular export + console.log('Exporting as single PDF'); + const result = await pdfExportService.exportPDF( + processedDocuments, + [], + { selectedOnly: false, filename: processedDocuments.name } + ); - // Step 3: Download the result - if ('blob' in result) { pdfExportService.downloadFile(result.blob, result.filename); } @@ -629,7 +676,25 @@ const PageEditor = ({ console.error('Export failed:', error); setExportLoading(false); } - }, [displayDocument, updateDocumentWithDOMState]); + }, [displayDocument, mergedPdfDocument]); + + // Apply DOM changes to document state using dedicated service + const applyChanges = useCallback(() => { + if (!displayDocument) return; + + // Pass current display document (which includes reordering) to get both reordering AND DOM changes + const processedDocuments = documentManipulationService.applyDOMChangesToDocument( + mergedPdfDocument || displayDocument, // Original order + displayDocument // Current display order (includes reordering) + ); + + // For apply changes, we only set the first document if it's an array (splits shouldn't affect document state) + const documentToSet = Array.isArray(processedDocuments) ? processedDocuments[0] : processedDocuments; + setEditedDocument(documentToSet); + + console.log('Changes applied to document'); + }, [displayDocument, mergedPdfDocument]); + const closePdf = useCallback(() => { actions.clearAllFiles(); @@ -665,6 +730,7 @@ const PageEditor = ({ showExportPreview: handleExportPreview, onExportSelected, onExportAll, + applyChanges, exportLoading, selectionMode, selectedPages: selectedPageNumbers, @@ -673,7 +739,7 @@ const PageEditor = ({ } }, [ onFunctionsReady, handleUndo, handleRedo, handleRotate, handleDelete, handleSplit, - handleExportPreview, onExportSelected, onExportAll, exportLoading, selectionMode, selectedPageNumbers, closePdf + handleExportPreview, onExportSelected, onExportAll, applyChanges, exportLoading, selectionMode, selectedPageNumbers, closePdf ]); // Display all pages - use edited or original document diff --git a/frontend/src/components/pageEditor/PageThumbnail.tsx b/frontend/src/components/pageEditor/PageThumbnail.tsx index a49025b8a..1ad353dbf 100644 --- a/frontend/src/components/pageEditor/PageThumbnail.tsx +++ b/frontend/src/components/pageEditor/PageThumbnail.tsx @@ -194,9 +194,14 @@ const PageThumbnail: React.FC = ({ const handleSplit = useCallback((e: React.MouseEvent) => { e.stopPropagation(); - console.log('Split at page:', page.pageNumber); - onSetStatus(`Split marker toggled for page ${page.pageNumber}`); - }, [page.pageNumber, onSetStatus]); + + // Create a command to toggle split marker + const command = new ToggleSplitCommand([page.id]); + onExecuteCommand(command); + + const action = page.splitAfter ? 'removed' : 'added'; + onSetStatus(`Split marker ${action} after page ${page.pageNumber}`); + }, [page.pageNumber, page.id, page.splitAfter, onExecuteCommand, onSetStatus, ToggleSplitCommand]); return (
= ({ - {index > 0 && ( - + {index < totalPages - 1 && ( + = ({
- {/* Split indicator */} - {page.splitBefore && ( + {/* Split indicator - shows where document will be split */} + {page.splitAfter && (
p.pageNumber)); + console.log('Current display order:', currentDisplayOrder?.pages.map(p => p.pageNumber) || 'none provided'); + + // Use current display order (from React state) if provided, otherwise use original order + const baseDocument = currentDisplayOrder || pdfDocument; + console.log('Using page order:', baseDocument.pages.map(p => p.pageNumber)); + + // Apply DOM changes to each page (rotation, split markers) + const updatedPages = baseDocument.pages.map(page => this.applyPageChanges(page)); + + // Create final document with reordered pages and applied changes + const finalDocument = { + ...pdfDocument, // Use original document metadata but updated pages + pages: updatedPages // Use reordered pages with applied changes + }; + + // Check for splits and return multiple documents if needed + if (this.hasSplitMarkers(finalDocument)) { + return this.createSplitDocuments(finalDocument); + } + + return finalDocument; + } + + /** + * Check if document has split markers + */ + private hasSplitMarkers(document: PDFDocument): boolean { + return document.pages.some(page => page.splitAfter); + } + + /** + * Create multiple documents from split markers + */ + private createSplitDocuments(document: PDFDocument): PDFDocument[] { + const documents: PDFDocument[] = []; + const splitPoints: number[] = []; + + // Find split points - pages with splitAfter create split points AFTER them + document.pages.forEach((page, index) => { + if (page.splitAfter) { + splitPoints.push(index + 1); + } + }); + + // Add end point if not already there + if (splitPoints.length === 0 || splitPoints[splitPoints.length - 1] !== document.pages.length) { + splitPoints.push(document.pages.length); + } + + let startIndex = 0; + let partNumber = 1; + + for (const endIndex of splitPoints) { + const segmentPages = document.pages.slice(startIndex, endIndex); + + if (segmentPages.length > 0) { + documents.push({ + ...document, + id: `${document.id}_part_${partNumber}`, + name: `${document.name.replace(/\.pdf$/i, '')}_part_${partNumber}.pdf`, + pages: segmentPages, + totalPages: segmentPages.length + }); + partNumber++; + } + + startIndex = endIndex; + } + + console.log(`Created ${documents.length} split documents`); + return documents; + } + + /** + * Apply DOM changes for a single page + */ + private applyPageChanges(page: PDFPage): PDFPage { + // Find the DOM element for this page + const pageElement = document.querySelector(`[data-page-id="${page.id}"]`); + if (!pageElement) { + console.log(`Page ${page.pageNumber}: No DOM element found, keeping original state`); + return page; + } + + const updatedPage = { ...page }; + + // Apply rotation changes from DOM + updatedPage.rotation = this.getRotationFromDOM(pageElement, page); + + // Apply split marker changes from document state (already handled by commands) + // Split markers are already updated by ToggleSplitCommand, so no DOM reading needed + + return updatedPage; + } + + /** + * Read rotation from DOM element + */ + private getRotationFromDOM(pageElement: Element, originalPage: PDFPage): number { + const img = pageElement.querySelector('img'); + if (img && img.style.rotate) { + // Parse rotation from DOM (e.g., "90deg" -> 90) + const rotationMatch = img.style.rotate.match(/-?\d+/); + const domRotation = rotationMatch ? parseInt(rotationMatch[0]) : 0; + + console.log(`Page ${originalPage.pageNumber}: DOM rotation = ${domRotation}°, original = ${originalPage.rotation}°`); + return domRotation; + } + + console.log(`Page ${originalPage.pageNumber}: No DOM rotation found, keeping original = ${originalPage.rotation}°`); + return originalPage.rotation; + } + + /** + * Reset all DOM changes (useful for "discard changes" functionality) + */ + resetDOMToDocumentState(pdfDocument: PDFDocument): void { + console.log('DocumentManipulationService: Resetting DOM to match document state'); + + pdfDocument.pages.forEach(page => { + const pageElement = document.querySelector(`[data-page-id="${page.id}"]`); + if (pageElement) { + const img = pageElement.querySelector('img'); + if (img) { + // Reset rotation to match document state + img.style.rotate = `${page.rotation}deg`; + } + } + }); + } + + /** + * Check if DOM state differs from document state + */ + hasUnsavedChanges(pdfDocument: PDFDocument): boolean { + return pdfDocument.pages.some(page => { + const pageElement = document.querySelector(`[data-page-id="${page.id}"]`); + if (pageElement) { + const domRotation = this.getRotationFromDOM(pageElement, page); + return domRotation !== page.rotation; + } + return false; + }); + } +} + +// Export singleton instance +export const documentManipulationService = new DocumentManipulationService(); \ No newline at end of file diff --git a/frontend/src/services/pdfExportService.ts b/frontend/src/services/pdfExportService.ts index b0662437e..cf0cdd8c2 100644 --- a/frontend/src/services/pdfExportService.ts +++ b/frontend/src/services/pdfExportService.ts @@ -4,7 +4,6 @@ import { PDFDocument, PDFPage } from '../types/pageEditor'; export interface ExportOptions { selectedOnly?: boolean; filename?: string; - splitDocuments?: boolean; } export class PDFExportService { @@ -15,8 +14,8 @@ export class PDFExportService { pdfDocument: PDFDocument, selectedPageIds: string[] = [], options: ExportOptions = {} - ): Promise<{ blob: Blob; filename: string } | { blobs: Blob[]; filenames: string[] }> { - const { selectedOnly = false, filename, splitDocuments = false } = options; + ): Promise<{ blob: Blob; filename: string }> { + const { selectedOnly = false, filename } = options; try { // Determine which pages to export @@ -28,17 +27,13 @@ export class PDFExportService { throw new Error('No pages to export'); } - // Load original PDF once + // Load original PDF and create new document const originalPDFBytes = await pdfDocument.file.arrayBuffer(); const sourceDoc = await PDFLibDocument.load(originalPDFBytes); - - if (splitDocuments) { - return await this.createSplitDocuments(sourceDoc, pagesToExport, filename || pdfDocument.name); - } else { - const blob = await this.createSingleDocument(sourceDoc, pagesToExport); - const exportFilename = this.generateFilename(filename || pdfDocument.name, selectedOnly); - return { blob, filename: exportFilename }; - } + const blob = await this.createSingleDocument(sourceDoc, pagesToExport); + const exportFilename = this.generateFilename(filename || pdfDocument.name, selectedOnly); + + return { blob, filename: exportFilename }; } catch (error) { console.error('PDF export error:', error); throw new Error(`Failed to export PDF: ${error instanceof Error ? error.message : 'Unknown error'}`); @@ -55,8 +50,8 @@ export class PDFExportService { const newDoc = await PDFLibDocument.create(); for (const page of pages) { - // Get the original page from source document - const sourcePageIndex = page.pageNumber - 1; + // Get the original page from source document using originalPageNumber + const sourcePageIndex = page.originalPageNumber - 1; if (sourcePageIndex >= 0 && sourcePageIndex < sourceDoc.getPageCount()) { // Copy the page @@ -81,70 +76,6 @@ export class PDFExportService { return new Blob([pdfBytes], { type: 'application/pdf' }); } - /** - * Create multiple PDF documents based on split markers - */ - private async createSplitDocuments( - sourceDoc: PDFLibDocument, - pages: PDFPage[], - baseFilename: string - ): Promise<{ blobs: Blob[]; filenames: string[] }> { - const splitPoints: number[] = []; - const blobs: Blob[] = []; - const filenames: string[] = []; - - // Find split points - pages.forEach((page, index) => { - if (page.splitBefore && index > 0) { - splitPoints.push(index); - } - }); - - // Add end point - splitPoints.push(pages.length); - - let startIndex = 0; - let partNumber = 1; - - for (const endIndex of splitPoints) { - const segmentPages = pages.slice(startIndex, endIndex); - - if (segmentPages.length > 0) { - const newDoc = await PDFLibDocument.create(); - - for (const page of segmentPages) { - const sourcePageIndex = page.pageNumber - 1; - - if (sourcePageIndex >= 0 && sourcePageIndex < sourceDoc.getPageCount()) { - const [copiedPage] = await newDoc.copyPages(sourceDoc, [sourcePageIndex]); - - if (page.rotation !== 0) { - copiedPage.setRotation(degrees(page.rotation)); - } - - newDoc.addPage(copiedPage); - } - } - - // Set metadata - newDoc.setCreator('Stirling PDF'); - newDoc.setProducer('Stirling PDF'); - newDoc.setTitle(`${baseFilename} - Part ${partNumber}`); - - const pdfBytes = await newDoc.save(); - const blob = new Blob([pdfBytes], { type: 'application/pdf' }); - const filename = this.generateSplitFilename(baseFilename, partNumber); - - blobs.push(blob); - filenames.push(filename); - partNumber++; - } - - startIndex = endIndex; - } - - return { blobs, filenames }; - } /** * Generate appropriate filename for export @@ -155,13 +86,6 @@ export class PDFExportService { return `${baseName}${suffix}.pdf`; } - /** - * Generate filename for split documents - */ - private generateSplitFilename(baseName: string, partNumber: number): string { - const cleanBaseName = baseName.replace(/\.pdf$/i, ''); - return `${cleanBaseName}_part_${partNumber}.pdf`; - } /** * Download a single file @@ -185,7 +109,7 @@ export class PDFExportService { * Download multiple files as a ZIP */ async downloadAsZip(blobs: Blob[], filenames: string[], zipFilename: string): Promise { - // For now, download files wherindividually + // For now, download files individually blobs.forEach((blob, index) => { setTimeout(() => { this.downloadFile(blob, filenames[index]); @@ -230,8 +154,8 @@ export class PDFExportService { ? pdfDocument.pages.filter(page => selectedPageIds.includes(page.id)) : pdfDocument.pages; - const splitCount = pagesToExport.reduce((count, page, index) => { - return count + (page.splitBefore && index > 0 ? 1 : 0); + const splitCount = pagesToExport.reduce((count, page) => { + return count + (page.splitAfter ? 1 : 0); }, 1); // At least 1 document // Rough size estimation (very approximate) diff --git a/frontend/src/types/pageEditor.ts b/frontend/src/types/pageEditor.ts index 6ad4318f7..46ee05a89 100644 --- a/frontend/src/types/pageEditor.ts +++ b/frontend/src/types/pageEditor.ts @@ -1,10 +1,11 @@ export interface PDFPage { id: string; pageNumber: number; + originalPageNumber: number; thumbnail: string | null; rotation: number; selected: boolean; - splitBefore?: boolean; + splitAfter?: boolean; } export interface PDFDocument {