mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2025-08-26 06:09:23 +00:00
Page editor continued improvements
This commit is contained in:
parent
383d227dea
commit
abed82cc6b
@ -11,6 +11,7 @@ import { PDFDocument, PDFPage } from "../../types/pageEditor";
|
|||||||
import { ProcessedFile as EnhancedProcessedFile } from "../../types/processing";
|
import { ProcessedFile as EnhancedProcessedFile } from "../../types/processing";
|
||||||
import { useUndoRedo } from "../../hooks/useUndoRedo";
|
import { useUndoRedo } from "../../hooks/useUndoRedo";
|
||||||
import { pdfExportService } from "../../services/pdfExportService";
|
import { pdfExportService } from "../../services/pdfExportService";
|
||||||
|
import { documentManipulationService } from "../../services/documentManipulationService";
|
||||||
import { enhancedPDFProcessingService } from "../../services/enhancedPDFProcessingService";
|
import { enhancedPDFProcessingService } from "../../services/enhancedPDFProcessingService";
|
||||||
import { fileProcessingService } from "../../services/fileProcessingService";
|
import { fileProcessingService } from "../../services/fileProcessingService";
|
||||||
import { pdfProcessingService } from "../../services/pdfProcessingService";
|
import { pdfProcessingService } from "../../services/pdfProcessingService";
|
||||||
@ -42,7 +43,7 @@ class RotatePageCommand extends DOMCommand {
|
|||||||
}
|
}
|
||||||
|
|
||||||
execute(): void {
|
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}"]`);
|
const pageElement = document.querySelector(`[data-page-id="${this.pageId}"]`);
|
||||||
if (pageElement) {
|
if (pageElement) {
|
||||||
const img = pageElement.querySelector('img');
|
const img = pageElement.querySelector('img');
|
||||||
@ -55,6 +56,7 @@ class RotatePageCommand extends DOMCommand {
|
|||||||
}
|
}
|
||||||
|
|
||||||
undo(): void {
|
undo(): void {
|
||||||
|
// Only update DOM
|
||||||
const pageElement = document.querySelector(`[data-page-id="${this.pageId}"]`);
|
const pageElement = document.querySelector(`[data-page-id="${this.pageId}"]`);
|
||||||
if (pageElement) {
|
if (pageElement) {
|
||||||
const img = pageElement.querySelector('img');
|
const img = pageElement.querySelector('img');
|
||||||
@ -128,6 +130,7 @@ export interface PageEditorProps {
|
|||||||
showExportPreview: (selectedOnly: boolean) => void;
|
showExportPreview: (selectedOnly: boolean) => void;
|
||||||
onExportSelected: () => void;
|
onExportSelected: () => void;
|
||||||
onExportAll: () => void;
|
onExportAll: () => void;
|
||||||
|
applyChanges: () => void;
|
||||||
exportLoading: boolean;
|
exportLoading: boolean;
|
||||||
selectionMode: boolean;
|
selectionMode: boolean;
|
||||||
selectedPages: number[];
|
selectedPages: number[];
|
||||||
@ -267,8 +270,8 @@ const PageEditor = ({
|
|||||||
thumbnail: page.thumbnail || null,
|
thumbnail: page.thumbnail || null,
|
||||||
rotation: page.rotation || 0,
|
rotation: page.rotation || 0,
|
||||||
selected: false,
|
selected: false,
|
||||||
splitBefore: page.splitBefore || false,
|
splitAfter: page.splitAfter || false,
|
||||||
originalPageNumber: page.pageNumber,
|
originalPageNumber: page.originalPageNumber || page.pageNumber || pageIndex + 1,
|
||||||
originalFileId: fileId,
|
originalFileId: fileId,
|
||||||
}));
|
}));
|
||||||
} else if (processedFile?.totalPages) {
|
} else if (processedFile?.totalPages) {
|
||||||
@ -282,7 +285,7 @@ const PageEditor = ({
|
|||||||
rotation: 0,
|
rotation: 0,
|
||||||
thumbnail: null, // Will be generated later
|
thumbnail: null, // Will be generated later
|
||||||
selected: false,
|
selected: false,
|
||||||
splitBefore: false,
|
splitAfter: false,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -442,9 +445,30 @@ const PageEditor = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
class ToggleSplitCommand {
|
class ToggleSplitCommand {
|
||||||
constructor(public pageId: string) {}
|
constructor(public pageIds: string[]) {}
|
||||||
execute() {
|
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]);
|
}, [selectedPageNumbers]);
|
||||||
|
|
||||||
const handleSplit = useCallback(() => {
|
const handleSplit = useCallback(() => {
|
||||||
console.log('Split at selected pages:', selectedPageNumbers);
|
if (!displayDocument || selectedPageNumbers.length === 0) return;
|
||||||
}, [selectedPageNumbers]);
|
|
||||||
|
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[]) => {
|
const handleReorderPages = useCallback((sourcePageNumber: number, targetIndex: number, selectedPages?: number[]) => {
|
||||||
if (!displayDocument) return;
|
if (!displayDocument) return;
|
||||||
@ -535,6 +572,9 @@ const PageEditor = ({
|
|||||||
totalPages: newPages.length,
|
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
|
// Update the edited document state
|
||||||
setEditedDocument(reorderedDocument);
|
setEditedDocument(reorderedDocument);
|
||||||
|
|
||||||
@ -542,42 +582,23 @@ const PageEditor = ({
|
|||||||
}, [displayDocument]);
|
}, [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 () => {
|
const onExportSelected = useCallback(async () => {
|
||||||
if (!displayDocument || selectedPageNumbers.length === 0) return;
|
if (!displayDocument || selectedPageNumbers.length === 0) return;
|
||||||
|
|
||||||
setExportLoading(true);
|
setExportLoading(true);
|
||||||
try {
|
try {
|
||||||
// Step 1: Update document with current DOM state (rotations)
|
// Step 1: Apply DOM changes to document state first
|
||||||
const documentWithDOMState = updateDocumentWithDOMState(displayDocument);
|
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 selectedPageIds = selectedPageNumbers.map(pageNum => {
|
||||||
const page = documentWithDOMState.pages.find(p => p.pageNumber === pageNum);
|
const page = documentWithDOMState.pages.find(p => p.pageNumber === pageNum);
|
||||||
return page?.id || '';
|
return page?.id || '';
|
||||||
@ -592,35 +613,61 @@ const PageEditor = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Step 4: Download the result
|
// Step 4: Download the result
|
||||||
if ('blob' in result) {
|
pdfExportService.downloadFile(result.blob, result.filename);
|
||||||
pdfExportService.downloadFile(result.blob, result.filename);
|
|
||||||
}
|
|
||||||
|
|
||||||
setExportLoading(false);
|
setExportLoading(false);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Export failed:', error);
|
console.error('Export failed:', error);
|
||||||
setExportLoading(false);
|
setExportLoading(false);
|
||||||
}
|
}
|
||||||
}, [displayDocument, selectedPageNumbers, updateDocumentWithDOMState]);
|
}, [displayDocument, selectedPageNumbers, mergedPdfDocument]);
|
||||||
|
|
||||||
const onExportAll = useCallback(async () => {
|
const onExportAll = useCallback(async () => {
|
||||||
if (!displayDocument) return;
|
if (!displayDocument) return;
|
||||||
|
|
||||||
setExportLoading(true);
|
setExportLoading(true);
|
||||||
try {
|
try {
|
||||||
// Step 1: Update document with current DOM state (rotations)
|
// Step 1: Apply DOM changes to document state first
|
||||||
const documentWithDOMState = updateDocumentWithDOMState(displayDocument);
|
console.log('Applying DOM changes before export...');
|
||||||
|
const processedDocuments = documentManipulationService.applyDOMChangesToDocument(
|
||||||
// Step 2: Export all pages with pdfExportService
|
mergedPdfDocument || displayDocument, // Original order
|
||||||
console.log('Exporting all pages with DOM rotations applied');
|
displayDocument // Current display order (includes reordering)
|
||||||
const result = await pdfExportService.exportPDF(
|
|
||||||
documentWithDOMState,
|
|
||||||
[],
|
|
||||||
{ selectedOnly: false, filename: documentWithDOMState.name }
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// 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);
|
pdfExportService.downloadFile(result.blob, result.filename);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -629,7 +676,25 @@ const PageEditor = ({
|
|||||||
console.error('Export failed:', error);
|
console.error('Export failed:', error);
|
||||||
setExportLoading(false);
|
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(() => {
|
const closePdf = useCallback(() => {
|
||||||
actions.clearAllFiles();
|
actions.clearAllFiles();
|
||||||
@ -665,6 +730,7 @@ const PageEditor = ({
|
|||||||
showExportPreview: handleExportPreview,
|
showExportPreview: handleExportPreview,
|
||||||
onExportSelected,
|
onExportSelected,
|
||||||
onExportAll,
|
onExportAll,
|
||||||
|
applyChanges,
|
||||||
exportLoading,
|
exportLoading,
|
||||||
selectionMode,
|
selectionMode,
|
||||||
selectedPages: selectedPageNumbers,
|
selectedPages: selectedPageNumbers,
|
||||||
@ -673,7 +739,7 @@ const PageEditor = ({
|
|||||||
}
|
}
|
||||||
}, [
|
}, [
|
||||||
onFunctionsReady, handleUndo, handleRedo, handleRotate, handleDelete, handleSplit,
|
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
|
// Display all pages - use edited or original document
|
||||||
|
@ -194,9 +194,14 @@ const PageThumbnail: React.FC<PageThumbnailProps> = ({
|
|||||||
|
|
||||||
const handleSplit = useCallback((e: React.MouseEvent) => {
|
const handleSplit = useCallback((e: React.MouseEvent) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
console.log('Split at page:', page.pageNumber);
|
|
||||||
onSetStatus(`Split marker toggled for page ${page.pageNumber}`);
|
// Create a command to toggle split marker
|
||||||
}, [page.pageNumber, onSetStatus]);
|
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 (
|
return (
|
||||||
<div
|
<div
|
||||||
@ -411,8 +416,8 @@ const PageThumbnail: React.FC<PageThumbnailProps> = ({
|
|||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
{index > 0 && (
|
{index < totalPages - 1 && (
|
||||||
<Tooltip label="Split Here">
|
<Tooltip label="Split After">
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
size="md"
|
size="md"
|
||||||
variant="subtle"
|
variant="subtle"
|
||||||
@ -427,16 +432,16 @@ const PageThumbnail: React.FC<PageThumbnailProps> = ({
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Split indicator */}
|
{/* Split indicator - shows where document will be split */}
|
||||||
{page.splitBefore && (
|
{page.splitAfter && (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
top: '-1px',
|
right: '-8px',
|
||||||
left: '50%',
|
top: '50%',
|
||||||
transform: 'translateX(-50%)',
|
transform: 'translateY(-50%)',
|
||||||
width: '100px',
|
width: '2px',
|
||||||
height: '2px',
|
height: '60px',
|
||||||
backgroundColor: '#3b82f6',
|
backgroundColor: '#3b82f6',
|
||||||
zIndex: 5,
|
zIndex: 5,
|
||||||
}}
|
}}
|
||||||
|
@ -77,6 +77,7 @@ export function usePDFProcessor() {
|
|||||||
pages.push({
|
pages.push({
|
||||||
id: `${file.name}-page-${i}`,
|
id: `${file.name}-page-${i}`,
|
||||||
pageNumber: i,
|
pageNumber: i,
|
||||||
|
originalPageNumber: i,
|
||||||
thumbnail: null, // Will be loaded lazily
|
thumbnail: null, // Will be loaded lazily
|
||||||
rotation: 0,
|
rotation: 0,
|
||||||
selected: false
|
selected: false
|
||||||
|
162
frontend/src/services/documentManipulationService.ts
Normal file
162
frontend/src/services/documentManipulationService.ts
Normal file
@ -0,0 +1,162 @@
|
|||||||
|
import { PDFDocument, PDFPage } from '../types/pageEditor';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service for applying DOM changes to PDF document state
|
||||||
|
* Reads current DOM state and updates the document accordingly
|
||||||
|
*/
|
||||||
|
export class DocumentManipulationService {
|
||||||
|
/**
|
||||||
|
* Apply all DOM changes (rotations, splits, reordering) to document state
|
||||||
|
* Returns single document or multiple documents if splits are present
|
||||||
|
*/
|
||||||
|
applyDOMChangesToDocument(pdfDocument: PDFDocument, currentDisplayOrder?: PDFDocument): PDFDocument | PDFDocument[] {
|
||||||
|
console.log('DocumentManipulationService: Applying DOM changes to document');
|
||||||
|
console.log('Original document page order:', pdfDocument.pages.map(p => 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();
|
@ -4,7 +4,6 @@ import { PDFDocument, PDFPage } from '../types/pageEditor';
|
|||||||
export interface ExportOptions {
|
export interface ExportOptions {
|
||||||
selectedOnly?: boolean;
|
selectedOnly?: boolean;
|
||||||
filename?: string;
|
filename?: string;
|
||||||
splitDocuments?: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class PDFExportService {
|
export class PDFExportService {
|
||||||
@ -15,8 +14,8 @@ export class PDFExportService {
|
|||||||
pdfDocument: PDFDocument,
|
pdfDocument: PDFDocument,
|
||||||
selectedPageIds: string[] = [],
|
selectedPageIds: string[] = [],
|
||||||
options: ExportOptions = {}
|
options: ExportOptions = {}
|
||||||
): Promise<{ blob: Blob; filename: string } | { blobs: Blob[]; filenames: string[] }> {
|
): Promise<{ blob: Blob; filename: string }> {
|
||||||
const { selectedOnly = false, filename, splitDocuments = false } = options;
|
const { selectedOnly = false, filename } = options;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Determine which pages to export
|
// Determine which pages to export
|
||||||
@ -28,17 +27,13 @@ export class PDFExportService {
|
|||||||
throw new Error('No pages to export');
|
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 originalPDFBytes = await pdfDocument.file.arrayBuffer();
|
||||||
const sourceDoc = await PDFLibDocument.load(originalPDFBytes);
|
const sourceDoc = await PDFLibDocument.load(originalPDFBytes);
|
||||||
|
const blob = await this.createSingleDocument(sourceDoc, pagesToExport);
|
||||||
if (splitDocuments) {
|
const exportFilename = this.generateFilename(filename || pdfDocument.name, selectedOnly);
|
||||||
return await this.createSplitDocuments(sourceDoc, pagesToExport, filename || pdfDocument.name);
|
|
||||||
} else {
|
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) {
|
} catch (error) {
|
||||||
console.error('PDF export error:', error);
|
console.error('PDF export error:', error);
|
||||||
throw new Error(`Failed to export PDF: ${error instanceof Error ? error.message : 'Unknown 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();
|
const newDoc = await PDFLibDocument.create();
|
||||||
|
|
||||||
for (const page of pages) {
|
for (const page of pages) {
|
||||||
// Get the original page from source document
|
// Get the original page from source document using originalPageNumber
|
||||||
const sourcePageIndex = page.pageNumber - 1;
|
const sourcePageIndex = page.originalPageNumber - 1;
|
||||||
|
|
||||||
if (sourcePageIndex >= 0 && sourcePageIndex < sourceDoc.getPageCount()) {
|
if (sourcePageIndex >= 0 && sourcePageIndex < sourceDoc.getPageCount()) {
|
||||||
// Copy the page
|
// Copy the page
|
||||||
@ -81,70 +76,6 @@ export class PDFExportService {
|
|||||||
return new Blob([pdfBytes], { type: 'application/pdf' });
|
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
|
* Generate appropriate filename for export
|
||||||
@ -155,13 +86,6 @@ export class PDFExportService {
|
|||||||
return `${baseName}${suffix}.pdf`;
|
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
|
* Download a single file
|
||||||
@ -185,7 +109,7 @@ export class PDFExportService {
|
|||||||
* Download multiple files as a ZIP
|
* Download multiple files as a ZIP
|
||||||
*/
|
*/
|
||||||
async downloadAsZip(blobs: Blob[], filenames: string[], zipFilename: string): Promise<void> {
|
async downloadAsZip(blobs: Blob[], filenames: string[], zipFilename: string): Promise<void> {
|
||||||
// For now, download files wherindividually
|
// For now, download files individually
|
||||||
blobs.forEach((blob, index) => {
|
blobs.forEach((blob, index) => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.downloadFile(blob, filenames[index]);
|
this.downloadFile(blob, filenames[index]);
|
||||||
@ -230,8 +154,8 @@ export class PDFExportService {
|
|||||||
? pdfDocument.pages.filter(page => selectedPageIds.includes(page.id))
|
? pdfDocument.pages.filter(page => selectedPageIds.includes(page.id))
|
||||||
: pdfDocument.pages;
|
: pdfDocument.pages;
|
||||||
|
|
||||||
const splitCount = pagesToExport.reduce((count, page, index) => {
|
const splitCount = pagesToExport.reduce((count, page) => {
|
||||||
return count + (page.splitBefore && index > 0 ? 1 : 0);
|
return count + (page.splitAfter ? 1 : 0);
|
||||||
}, 1); // At least 1 document
|
}, 1); // At least 1 document
|
||||||
|
|
||||||
// Rough size estimation (very approximate)
|
// Rough size estimation (very approximate)
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
export interface PDFPage {
|
export interface PDFPage {
|
||||||
id: string;
|
id: string;
|
||||||
pageNumber: number;
|
pageNumber: number;
|
||||||
|
originalPageNumber: number;
|
||||||
thumbnail: string | null;
|
thumbnail: string | null;
|
||||||
rotation: number;
|
rotation: number;
|
||||||
selected: boolean;
|
selected: boolean;
|
||||||
splitBefore?: boolean;
|
splitAfter?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PDFDocument {
|
export interface PDFDocument {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user