Multi document full support

This commit is contained in:
Reece Browne 2025-08-25 17:11:37 +01:00
parent 2bcb506a7b
commit a7690b3754
4 changed files with 176 additions and 21 deletions

View File

@ -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<string, File> | null => {
const sourceFiles = new Map<string, File>();
// 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}

View File

@ -68,6 +68,18 @@ const PageThumbnail: React.FC<PageThumbnailProps> = ({
const [thumbnailUrl, setThumbnailUrl] = useState<string | null>(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<PageThumbnailProps> = ({
>
{page.isBlankPage ? (
<div style={{
width: '100%',
height: '100%',
backgroundColor: 'white',
borderRadius: 4
}}></div>
width: '100%',
height: '100%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
}}>
<div style={{
width: '70%',
aspectRatio: getDocumentAspectRatio(),
backgroundColor: 'white',
border: '1px solid #e9ecef',
borderRadius: 2
}}></div>
</div>
) : thumbnailUrl ? (
<img
src={thumbnailUrl}

View File

@ -8,7 +8,7 @@ export interface ExportOptions {
export class PDFExportService {
/**
* Export PDF document with applied operations
* Export PDF document with applied operations (single file source)
*/
async exportPDF(
pdfDocument: PDFDocument,
@ -41,7 +41,100 @@ export class PDFExportService {
}
/**
* Create a single PDF document with all operations applied
* Export PDF document with applied operations (multi-file source)
*/
async exportPDFMultiFile(
pdfDocument: PDFDocument,
sourceFiles: Map<string, File>,
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<string, File>,
pages: PDFPage[]
): Promise<Blob> {
const newDoc = await PDFLibDocument.create();
// Load all source documents once and cache them
const loadedDocs = new Map<string, PDFLibDocument>();
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,

View File

@ -7,6 +7,7 @@ export interface PDFPage {
selected: boolean;
splitAfter?: boolean;
isBlankPage?: boolean;
originalFileId?: string;
}
export interface PDFDocument {