mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2025-08-27 06:39:24 +00:00
Multi document full support
This commit is contained in:
parent
2bcb506a7b
commit
a7690b3754
@ -308,6 +308,26 @@ const PageEditor = ({
|
|||||||
undoManagerRef.current.executeCommand(reorderCommand);
|
undoManagerRef.current.executeCommand(reorderCommand);
|
||||||
}, [displayDocument]);
|
}, [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 () => {
|
const onExportSelected = useCallback(async () => {
|
||||||
@ -334,7 +354,16 @@ const PageEditor = ({
|
|||||||
|
|
||||||
// Step 3: Export with pdfExportService
|
// Step 3: Export with pdfExportService
|
||||||
console.log('Exporting selected pages:', selectedPageNumbers, 'with DOM rotations applied');
|
console.log('Exporting selected pages:', selectedPageNumbers, 'with DOM rotations applied');
|
||||||
const result = await pdfExportService.exportPDF(
|
|
||||||
|
const sourceFiles = getSourceFiles();
|
||||||
|
const result = sourceFiles
|
||||||
|
? await pdfExportService.exportPDFMultiFile(
|
||||||
|
documentWithDOMState,
|
||||||
|
sourceFiles,
|
||||||
|
selectedPageIds,
|
||||||
|
{ selectedOnly: true, filename: documentWithDOMState.name }
|
||||||
|
)
|
||||||
|
: await pdfExportService.exportPDF(
|
||||||
documentWithDOMState,
|
documentWithDOMState,
|
||||||
selectedPageIds,
|
selectedPageIds,
|
||||||
{ selectedOnly: true, filename: documentWithDOMState.name }
|
{ selectedOnly: true, filename: documentWithDOMState.name }
|
||||||
@ -348,7 +377,7 @@ const PageEditor = ({
|
|||||||
console.error('Export failed:', error);
|
console.error('Export failed:', error);
|
||||||
setExportLoading(false);
|
setExportLoading(false);
|
||||||
}
|
}
|
||||||
}, [displayDocument, selectedPageNumbers, mergedPdfDocument, splitPositions]);
|
}, [displayDocument, selectedPageNumbers, mergedPdfDocument, splitPositions, getSourceFiles]);
|
||||||
|
|
||||||
const onExportAll = useCallback(async () => {
|
const onExportAll = useCallback(async () => {
|
||||||
if (!displayDocument) return;
|
if (!displayDocument) return;
|
||||||
@ -370,8 +399,11 @@ const PageEditor = ({
|
|||||||
const blobs: Blob[] = [];
|
const blobs: Blob[] = [];
|
||||||
const filenames: string[] = [];
|
const filenames: string[] = [];
|
||||||
|
|
||||||
|
const sourceFiles = getSourceFiles();
|
||||||
for (const doc of processedDocuments) {
|
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);
|
blobs.push(result.blob);
|
||||||
filenames.push(result.filename);
|
filenames.push(result.filename);
|
||||||
}
|
}
|
||||||
@ -391,7 +423,15 @@ const PageEditor = ({
|
|||||||
} else {
|
} else {
|
||||||
// Single document - regular export
|
// Single document - regular export
|
||||||
console.log('Exporting as single PDF');
|
console.log('Exporting as single PDF');
|
||||||
const result = await pdfExportService.exportPDF(
|
const sourceFiles = getSourceFiles();
|
||||||
|
const result = sourceFiles
|
||||||
|
? await pdfExportService.exportPDFMultiFile(
|
||||||
|
processedDocuments,
|
||||||
|
sourceFiles,
|
||||||
|
[],
|
||||||
|
{ selectedOnly: false, filename: processedDocuments.name }
|
||||||
|
)
|
||||||
|
: await pdfExportService.exportPDF(
|
||||||
processedDocuments,
|
processedDocuments,
|
||||||
[],
|
[],
|
||||||
{ selectedOnly: false, filename: processedDocuments.name }
|
{ selectedOnly: false, filename: processedDocuments.name }
|
||||||
@ -405,7 +445,7 @@ const PageEditor = ({
|
|||||||
console.error('Export failed:', error);
|
console.error('Export failed:', error);
|
||||||
setExportLoading(false);
|
setExportLoading(false);
|
||||||
}
|
}
|
||||||
}, [displayDocument, mergedPdfDocument, splitPositions]);
|
}, [displayDocument, mergedPdfDocument, splitPositions, getSourceFiles]);
|
||||||
|
|
||||||
// Apply DOM changes to document state using dedicated service
|
// Apply DOM changes to document state using dedicated service
|
||||||
const applyChanges = useCallback(() => {
|
const applyChanges = useCallback(() => {
|
||||||
@ -587,7 +627,7 @@ const PageEditor = ({
|
|||||||
page={page}
|
page={page}
|
||||||
index={index}
|
index={index}
|
||||||
totalPages={displayDocument.pages.length}
|
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}
|
selectedPages={selectedPageNumbers}
|
||||||
selectionMode={selectionMode}
|
selectionMode={selectionMode}
|
||||||
movingPage={movingPage}
|
movingPage={movingPage}
|
||||||
|
@ -68,6 +68,18 @@ const PageThumbnail: React.FC<PageThumbnailProps> = ({
|
|||||||
const [thumbnailUrl, setThumbnailUrl] = useState<string | null>(page.thumbnail);
|
const [thumbnailUrl, setThumbnailUrl] = useState<string | null>(page.thumbnail);
|
||||||
const { getThumbnailFromCache, requestThumbnail } = useThumbnailGeneration();
|
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
|
// Update thumbnail URL when page prop changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (page.thumbnail && page.thumbnail !== thumbnailUrl) {
|
if (page.thumbnail && page.thumbnail !== thumbnailUrl) {
|
||||||
@ -331,9 +343,18 @@ const PageThumbnail: React.FC<PageThumbnailProps> = ({
|
|||||||
<div style={{
|
<div style={{
|
||||||
width: '100%',
|
width: '100%',
|
||||||
height: '100%',
|
height: '100%',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center'
|
||||||
|
}}>
|
||||||
|
<div style={{
|
||||||
|
width: '70%',
|
||||||
|
aspectRatio: getDocumentAspectRatio(),
|
||||||
backgroundColor: 'white',
|
backgroundColor: 'white',
|
||||||
borderRadius: 4
|
border: '1px solid #e9ecef',
|
||||||
|
borderRadius: 2
|
||||||
}}></div>
|
}}></div>
|
||||||
|
</div>
|
||||||
) : thumbnailUrl ? (
|
) : thumbnailUrl ? (
|
||||||
<img
|
<img
|
||||||
src={thumbnailUrl}
|
src={thumbnailUrl}
|
||||||
|
@ -8,7 +8,7 @@ export interface ExportOptions {
|
|||||||
|
|
||||||
export class PDFExportService {
|
export class PDFExportService {
|
||||||
/**
|
/**
|
||||||
* Export PDF document with applied operations
|
* Export PDF document with applied operations (single file source)
|
||||||
*/
|
*/
|
||||||
async exportPDF(
|
async exportPDF(
|
||||||
pdfDocument: PDFDocument,
|
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(
|
private async createSingleDocument(
|
||||||
sourceDoc: PDFLibDocument,
|
sourceDoc: PDFLibDocument,
|
||||||
|
@ -7,6 +7,7 @@ export interface PDFPage {
|
|||||||
selected: boolean;
|
selected: boolean;
|
||||||
splitAfter?: boolean;
|
splitAfter?: boolean;
|
||||||
isBlankPage?: boolean;
|
isBlankPage?: boolean;
|
||||||
|
originalFileId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PDFDocument {
|
export interface PDFDocument {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user