mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2025-08-26 06:09:23 +00:00
Page breaks
This commit is contained in:
parent
7cf0806749
commit
2bcb506a7b
@ -112,6 +112,8 @@ export default function Workbench() {
|
||||
onDelete={pageEditorFunctions.handleDelete}
|
||||
onSplit={pageEditorFunctions.handleSplit}
|
||||
onSplitAll={pageEditorFunctions.handleSplitAll}
|
||||
onPageBreak={pageEditorFunctions.handlePageBreak}
|
||||
onPageBreakAll={pageEditorFunctions.handlePageBreakAll}
|
||||
onExportSelected={pageEditorFunctions.onExportSelected}
|
||||
onExportAll={pageEditorFunctions.onExportAll}
|
||||
exportLoading={pageEditorFunctions.exportLoading}
|
||||
|
@ -27,6 +27,8 @@ import {
|
||||
BulkRotateCommand,
|
||||
BulkSplitCommand,
|
||||
SplitAllCommand,
|
||||
PageBreakCommand,
|
||||
BulkPageBreakCommand,
|
||||
UndoManager
|
||||
} from './commands/pageCommands';
|
||||
import { usePageDocument } from './hooks/usePageDocument';
|
||||
@ -42,6 +44,8 @@ export interface PageEditorProps {
|
||||
handleDelete: () => void;
|
||||
handleSplit: () => void;
|
||||
handleSplitAll: () => void;
|
||||
handlePageBreak: () => void;
|
||||
handlePageBreakAll: () => void;
|
||||
showExportPreview: (selectedOnly: boolean) => void;
|
||||
onExportSelected: () => void;
|
||||
onExportAll: () => void;
|
||||
@ -266,6 +270,31 @@ const PageEditor = ({
|
||||
undoManagerRef.current.executeCommand(splitAllCommand);
|
||||
}, [displayDocument, splitPositions]);
|
||||
|
||||
const handlePageBreak = useCallback(() => {
|
||||
if (!displayDocument || selectedPageNumbers.length === 0) return;
|
||||
|
||||
console.log('Insert page breaks after selected pages:', selectedPageNumbers);
|
||||
|
||||
const pageBreakCommand = new PageBreakCommand(
|
||||
selectedPageNumbers,
|
||||
() => displayDocument,
|
||||
setEditedDocument,
|
||||
setSelectedPageNumbers
|
||||
);
|
||||
undoManagerRef.current.executeCommand(pageBreakCommand);
|
||||
}, [selectedPageNumbers, displayDocument]);
|
||||
|
||||
const handlePageBreakAll = useCallback(() => {
|
||||
if (!displayDocument) return;
|
||||
|
||||
const pageBreakAllCommand = new BulkPageBreakCommand(
|
||||
() => displayDocument,
|
||||
setEditedDocument,
|
||||
setSelectedPageNumbers
|
||||
);
|
||||
undoManagerRef.current.executeCommand(pageBreakAllCommand);
|
||||
}, [displayDocument]);
|
||||
|
||||
const handleReorderPages = useCallback((sourcePageNumber: number, targetIndex: number, selectedPages?: number[]) => {
|
||||
if (!displayDocument) return;
|
||||
|
||||
@ -429,6 +458,8 @@ const PageEditor = ({
|
||||
handleDelete,
|
||||
handleSplit,
|
||||
handleSplitAll,
|
||||
handlePageBreak,
|
||||
handlePageBreakAll,
|
||||
showExportPreview: handleExportPreview,
|
||||
onExportSelected,
|
||||
onExportAll,
|
||||
@ -443,8 +474,8 @@ const PageEditor = ({
|
||||
}
|
||||
}, [
|
||||
onFunctionsReady, handleUndo, handleRedo, canUndo, canRedo, handleRotate, handleDelete, handleSplit, handleSplitAll,
|
||||
handleExportPreview, onExportSelected, onExportAll, applyChanges, exportLoading, selectionMode, selectedPageNumbers,
|
||||
splitPositions, displayDocument?.pages.length, closePdf
|
||||
handlePageBreak, handlePageBreakAll, handleExportPreview, onExportSelected, onExportAll, applyChanges, exportLoading,
|
||||
selectionMode, selectedPageNumbers, splitPositions, displayDocument?.pages.length, closePdf
|
||||
]);
|
||||
|
||||
// Display all pages - use edited or original document
|
||||
|
@ -12,6 +12,7 @@ import RotateLeftIcon from "@mui/icons-material/RotateLeft";
|
||||
import RotateRightIcon from "@mui/icons-material/RotateRight";
|
||||
import DeleteIcon from "@mui/icons-material/Delete";
|
||||
import CloseIcon from "@mui/icons-material/Close";
|
||||
import InsertPageBreakIcon from "@mui/icons-material/InsertPageBreak";
|
||||
|
||||
interface PageEditorControlsProps {
|
||||
// Close/Reset functions
|
||||
@ -28,6 +29,8 @@ interface PageEditorControlsProps {
|
||||
onDelete: () => void;
|
||||
onSplit: () => void;
|
||||
onSplitAll: () => void;
|
||||
onPageBreak: () => void;
|
||||
onPageBreakAll: () => void;
|
||||
|
||||
// Export functions
|
||||
onExportSelected: () => void;
|
||||
@ -53,6 +56,8 @@ const PageEditorControls = ({
|
||||
onDelete,
|
||||
onSplit,
|
||||
onSplitAll,
|
||||
onPageBreak,
|
||||
onPageBreakAll,
|
||||
onExportSelected,
|
||||
onExportAll,
|
||||
exportLoading,
|
||||
@ -79,6 +84,16 @@ const PageEditorControls = ({
|
||||
return hasAllSplits ? "Remove All Splits" : "Split All";
|
||||
};
|
||||
|
||||
// Calculate page break tooltip text
|
||||
const getPageBreakTooltip = () => {
|
||||
if (selectionMode) {
|
||||
return selectedPages.length > 0
|
||||
? `Insert ${selectedPages.length} Page Break${selectedPages.length > 1 ? 's' : ''}`
|
||||
: "Insert Page Breaks";
|
||||
}
|
||||
return "Insert Page Breaks After All Pages";
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
@ -183,6 +198,17 @@ const PageEditorControls = ({
|
||||
<ContentCutIcon />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<Tooltip label={getPageBreakTooltip()}>
|
||||
<ActionIcon
|
||||
onClick={selectionMode ? onPageBreak : onPageBreakAll}
|
||||
disabled={selectionMode && selectedPages.length === 0}
|
||||
variant={selectionMode && selectedPages.length > 0 ? "light" : "default"}
|
||||
color={selectionMode && selectedPages.length > 0 ? "orange" : undefined}
|
||||
size="lg"
|
||||
>
|
||||
<InsertPageBreakIcon />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
|
||||
<div style={{ width: 1, height: 28, backgroundColor: 'var(--mantine-color-gray-3)', margin: '0 8px' }} />
|
||||
|
||||
|
@ -292,7 +292,10 @@ const PageThumbnail: React.FC<PageThumbnailProps> = ({
|
||||
boxShadow: '0 2px 4px rgba(0,0,0,0.1)',
|
||||
pointerEvents: 'auto'
|
||||
}}
|
||||
onMouseDown={(e) => e.stopPropagation()}
|
||||
onMouseDown={(e) => {
|
||||
e.stopPropagation();
|
||||
onTogglePage(page.pageNumber);
|
||||
}}
|
||||
onMouseUp={(e) => e.stopPropagation()}
|
||||
onDragStart={(e) => {
|
||||
e.preventDefault();
|
||||
@ -302,9 +305,10 @@ const PageThumbnail: React.FC<PageThumbnailProps> = ({
|
||||
<Checkbox
|
||||
checked={Array.isArray(selectedPages) ? selectedPages.includes(page.pageNumber) : false}
|
||||
onChange={() => {
|
||||
// onChange is handled by the parent div click
|
||||
// Selection is handled by container mouseDown
|
||||
}}
|
||||
size="sm"
|
||||
style={{ pointerEvents: 'none' }}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
@ -323,7 +327,14 @@ const PageThumbnail: React.FC<PageThumbnailProps> = ({
|
||||
justifyContent: 'center'
|
||||
}}
|
||||
>
|
||||
{thumbnailUrl ? (
|
||||
{page.isBlankPage ? (
|
||||
<div style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
backgroundColor: 'white',
|
||||
borderRadius: 4
|
||||
}}></div>
|
||||
) : thumbnailUrl ? (
|
||||
<img
|
||||
src={thumbnailUrl}
|
||||
alt={`Page ${page.pageNumber}`}
|
||||
@ -354,7 +365,7 @@ const PageThumbnail: React.FC<PageThumbnailProps> = ({
|
||||
position: 'absolute',
|
||||
top: 5,
|
||||
left: 5,
|
||||
background: 'rgba(162, 201, 255, 0.8)',
|
||||
background: page.isBlankPage ? 'rgba(255, 165, 0, 0.8)' : 'rgba(162, 201, 255, 0.8)',
|
||||
padding: '6px 8px',
|
||||
borderRadius: 8,
|
||||
zIndex: 2,
|
||||
|
@ -396,6 +396,147 @@ export class SplitAllCommand extends DOMCommand {
|
||||
}
|
||||
}
|
||||
|
||||
export class PageBreakCommand extends DOMCommand {
|
||||
private insertedPages: PDFPage[] = [];
|
||||
private originalDocument: PDFDocument | null = null;
|
||||
|
||||
constructor(
|
||||
private selectedPageNumbers: number[],
|
||||
private getCurrentDocument: () => PDFDocument | null,
|
||||
private setDocument: (doc: PDFDocument) => void,
|
||||
private setSelectedPages: (pages: number[]) => void
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
execute(): void {
|
||||
const currentDoc = this.getCurrentDocument();
|
||||
if (!currentDoc || this.selectedPageNumbers.length === 0) return;
|
||||
|
||||
// Store original state for undo
|
||||
this.originalDocument = {
|
||||
...currentDoc,
|
||||
pages: currentDoc.pages.map(page => ({...page}))
|
||||
};
|
||||
|
||||
// Create new pages array with blank pages inserted
|
||||
const newPages: PDFPage[] = [];
|
||||
this.insertedPages = [];
|
||||
let pageNumberCounter = 1;
|
||||
|
||||
currentDoc.pages.forEach((page, index) => {
|
||||
// Add the current page
|
||||
const updatedPage = { ...page, pageNumber: pageNumberCounter++ };
|
||||
newPages.push(updatedPage);
|
||||
|
||||
// If this page is selected for page break insertion, add a blank page after it
|
||||
if (this.selectedPageNumbers.includes(page.pageNumber)) {
|
||||
const blankPage: PDFPage = {
|
||||
id: `blank-${Date.now()}-${index}`,
|
||||
pageNumber: pageNumberCounter++,
|
||||
originalPageNumber: -1, // Mark as blank page
|
||||
thumbnail: null,
|
||||
rotation: 0,
|
||||
selected: false,
|
||||
splitAfter: false,
|
||||
isBlankPage: true // Custom flag for blank pages
|
||||
};
|
||||
newPages.push(blankPage);
|
||||
this.insertedPages.push(blankPage);
|
||||
}
|
||||
});
|
||||
|
||||
// Update document
|
||||
const updatedDocument: PDFDocument = {
|
||||
...currentDoc,
|
||||
pages: newPages,
|
||||
totalPages: newPages.length,
|
||||
};
|
||||
|
||||
this.setDocument(updatedDocument);
|
||||
this.setSelectedPages([]);
|
||||
}
|
||||
|
||||
undo(): void {
|
||||
if (!this.originalDocument) return;
|
||||
this.setDocument(this.originalDocument);
|
||||
}
|
||||
|
||||
get description(): string {
|
||||
return `Insert ${this.selectedPageNumbers.length} page break(s)`;
|
||||
}
|
||||
}
|
||||
|
||||
export class BulkPageBreakCommand extends DOMCommand {
|
||||
private insertedPages: PDFPage[] = [];
|
||||
private originalDocument: PDFDocument | null = null;
|
||||
|
||||
constructor(
|
||||
private getCurrentDocument: () => PDFDocument | null,
|
||||
private setDocument: (doc: PDFDocument) => void,
|
||||
private setSelectedPages: (pages: number[]) => void
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
execute(): void {
|
||||
const currentDoc = this.getCurrentDocument();
|
||||
if (!currentDoc) return;
|
||||
|
||||
// Store original state for undo
|
||||
this.originalDocument = {
|
||||
...currentDoc,
|
||||
pages: currentDoc.pages.map(page => ({...page}))
|
||||
};
|
||||
|
||||
// Create new pages array with blank pages inserted after each page (except the last)
|
||||
const newPages: PDFPage[] = [];
|
||||
this.insertedPages = [];
|
||||
let pageNumberCounter = 1;
|
||||
|
||||
currentDoc.pages.forEach((page, index) => {
|
||||
// Add the current page
|
||||
const updatedPage = { ...page, pageNumber: pageNumberCounter++ };
|
||||
newPages.push(updatedPage);
|
||||
|
||||
// Add blank page after each page except the last one
|
||||
if (index < currentDoc.pages.length - 1) {
|
||||
const blankPage: PDFPage = {
|
||||
id: `blank-${Date.now()}-${index}`,
|
||||
pageNumber: pageNumberCounter++,
|
||||
originalPageNumber: -1,
|
||||
thumbnail: null,
|
||||
rotation: 0,
|
||||
selected: false,
|
||||
splitAfter: false,
|
||||
isBlankPage: true
|
||||
};
|
||||
newPages.push(blankPage);
|
||||
this.insertedPages.push(blankPage);
|
||||
}
|
||||
});
|
||||
|
||||
// Update document
|
||||
const updatedDocument: PDFDocument = {
|
||||
...currentDoc,
|
||||
pages: newPages,
|
||||
totalPages: newPages.length,
|
||||
};
|
||||
|
||||
this.setDocument(updatedDocument);
|
||||
this.setSelectedPages([]);
|
||||
}
|
||||
|
||||
undo(): void {
|
||||
if (!this.originalDocument) return;
|
||||
this.setDocument(this.originalDocument);
|
||||
}
|
||||
|
||||
get description(): string {
|
||||
return `Insert page breaks after all pages`;
|
||||
}
|
||||
}
|
||||
|
||||
// Simple undo manager for DOM commands
|
||||
export class UndoManager {
|
||||
private undoStack: DOMCommand[] = [];
|
||||
|
@ -121,10 +121,10 @@ export class DocumentManipulationService {
|
||||
*/
|
||||
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;
|
||||
if (img && img.style.transform) {
|
||||
// Parse rotation from transform property (e.g., "rotate(90deg)" -> 90)
|
||||
const rotationMatch = img.style.transform.match(/rotate\((-?\d+)deg\)/);
|
||||
const domRotation = rotationMatch ? parseInt(rotationMatch[1]) : 0;
|
||||
|
||||
console.log(`Page ${originalPage.pageNumber}: DOM rotation = ${domRotation}°, original = ${originalPage.rotation}°`);
|
||||
return domRotation;
|
||||
@ -146,7 +146,7 @@ export class DocumentManipulationService {
|
||||
const img = pageElement.querySelector('img');
|
||||
if (img) {
|
||||
// Reset rotation to match document state
|
||||
img.style.rotate = `${page.rotation}deg`;
|
||||
img.style.transform = `rotate(${page.rotation}deg)`;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -50,6 +50,15 @@ export class PDFExportService {
|
||||
const newDoc = await PDFLibDocument.create();
|
||||
|
||||
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 {
|
||||
// Get the original page from source document using originalPageNumber
|
||||
const sourcePageIndex = page.originalPageNumber - 1;
|
||||
|
||||
@ -65,6 +74,7 @@ export class PDFExportService {
|
||||
newDoc.addPage(copiedPage);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Set metadata
|
||||
newDoc.setCreator('Stirling PDF');
|
||||
|
@ -6,6 +6,7 @@ export interface PDFPage {
|
||||
rotation: number;
|
||||
selected: boolean;
|
||||
splitAfter?: boolean;
|
||||
isBlankPage?: boolean;
|
||||
}
|
||||
|
||||
export interface PDFDocument {
|
||||
|
Loading…
x
Reference in New Issue
Block a user