mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2025-08-26 14:19:24 +00:00
Page breaks
This commit is contained in:
parent
7cf0806749
commit
2bcb506a7b
@ -112,6 +112,8 @@ export default function Workbench() {
|
|||||||
onDelete={pageEditorFunctions.handleDelete}
|
onDelete={pageEditorFunctions.handleDelete}
|
||||||
onSplit={pageEditorFunctions.handleSplit}
|
onSplit={pageEditorFunctions.handleSplit}
|
||||||
onSplitAll={pageEditorFunctions.handleSplitAll}
|
onSplitAll={pageEditorFunctions.handleSplitAll}
|
||||||
|
onPageBreak={pageEditorFunctions.handlePageBreak}
|
||||||
|
onPageBreakAll={pageEditorFunctions.handlePageBreakAll}
|
||||||
onExportSelected={pageEditorFunctions.onExportSelected}
|
onExportSelected={pageEditorFunctions.onExportSelected}
|
||||||
onExportAll={pageEditorFunctions.onExportAll}
|
onExportAll={pageEditorFunctions.onExportAll}
|
||||||
exportLoading={pageEditorFunctions.exportLoading}
|
exportLoading={pageEditorFunctions.exportLoading}
|
||||||
|
@ -27,6 +27,8 @@ import {
|
|||||||
BulkRotateCommand,
|
BulkRotateCommand,
|
||||||
BulkSplitCommand,
|
BulkSplitCommand,
|
||||||
SplitAllCommand,
|
SplitAllCommand,
|
||||||
|
PageBreakCommand,
|
||||||
|
BulkPageBreakCommand,
|
||||||
UndoManager
|
UndoManager
|
||||||
} from './commands/pageCommands';
|
} from './commands/pageCommands';
|
||||||
import { usePageDocument } from './hooks/usePageDocument';
|
import { usePageDocument } from './hooks/usePageDocument';
|
||||||
@ -42,6 +44,8 @@ export interface PageEditorProps {
|
|||||||
handleDelete: () => void;
|
handleDelete: () => void;
|
||||||
handleSplit: () => void;
|
handleSplit: () => void;
|
||||||
handleSplitAll: () => void;
|
handleSplitAll: () => void;
|
||||||
|
handlePageBreak: () => void;
|
||||||
|
handlePageBreakAll: () => void;
|
||||||
showExportPreview: (selectedOnly: boolean) => void;
|
showExportPreview: (selectedOnly: boolean) => void;
|
||||||
onExportSelected: () => void;
|
onExportSelected: () => void;
|
||||||
onExportAll: () => void;
|
onExportAll: () => void;
|
||||||
@ -266,6 +270,31 @@ const PageEditor = ({
|
|||||||
undoManagerRef.current.executeCommand(splitAllCommand);
|
undoManagerRef.current.executeCommand(splitAllCommand);
|
||||||
}, [displayDocument, splitPositions]);
|
}, [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[]) => {
|
const handleReorderPages = useCallback((sourcePageNumber: number, targetIndex: number, selectedPages?: number[]) => {
|
||||||
if (!displayDocument) return;
|
if (!displayDocument) return;
|
||||||
|
|
||||||
@ -429,6 +458,8 @@ const PageEditor = ({
|
|||||||
handleDelete,
|
handleDelete,
|
||||||
handleSplit,
|
handleSplit,
|
||||||
handleSplitAll,
|
handleSplitAll,
|
||||||
|
handlePageBreak,
|
||||||
|
handlePageBreakAll,
|
||||||
showExportPreview: handleExportPreview,
|
showExportPreview: handleExportPreview,
|
||||||
onExportSelected,
|
onExportSelected,
|
||||||
onExportAll,
|
onExportAll,
|
||||||
@ -443,8 +474,8 @@ const PageEditor = ({
|
|||||||
}
|
}
|
||||||
}, [
|
}, [
|
||||||
onFunctionsReady, handleUndo, handleRedo, canUndo, canRedo, handleRotate, handleDelete, handleSplit, handleSplitAll,
|
onFunctionsReady, handleUndo, handleRedo, canUndo, canRedo, handleRotate, handleDelete, handleSplit, handleSplitAll,
|
||||||
handleExportPreview, onExportSelected, onExportAll, applyChanges, exportLoading, selectionMode, selectedPageNumbers,
|
handlePageBreak, handlePageBreakAll, handleExportPreview, onExportSelected, onExportAll, applyChanges, exportLoading,
|
||||||
splitPositions, displayDocument?.pages.length, closePdf
|
selectionMode, selectedPageNumbers, splitPositions, displayDocument?.pages.length, closePdf
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Display all pages - use edited or original document
|
// 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 RotateRightIcon from "@mui/icons-material/RotateRight";
|
||||||
import DeleteIcon from "@mui/icons-material/Delete";
|
import DeleteIcon from "@mui/icons-material/Delete";
|
||||||
import CloseIcon from "@mui/icons-material/Close";
|
import CloseIcon from "@mui/icons-material/Close";
|
||||||
|
import InsertPageBreakIcon from "@mui/icons-material/InsertPageBreak";
|
||||||
|
|
||||||
interface PageEditorControlsProps {
|
interface PageEditorControlsProps {
|
||||||
// Close/Reset functions
|
// Close/Reset functions
|
||||||
@ -28,6 +29,8 @@ interface PageEditorControlsProps {
|
|||||||
onDelete: () => void;
|
onDelete: () => void;
|
||||||
onSplit: () => void;
|
onSplit: () => void;
|
||||||
onSplitAll: () => void;
|
onSplitAll: () => void;
|
||||||
|
onPageBreak: () => void;
|
||||||
|
onPageBreakAll: () => void;
|
||||||
|
|
||||||
// Export functions
|
// Export functions
|
||||||
onExportSelected: () => void;
|
onExportSelected: () => void;
|
||||||
@ -53,6 +56,8 @@ const PageEditorControls = ({
|
|||||||
onDelete,
|
onDelete,
|
||||||
onSplit,
|
onSplit,
|
||||||
onSplitAll,
|
onSplitAll,
|
||||||
|
onPageBreak,
|
||||||
|
onPageBreakAll,
|
||||||
onExportSelected,
|
onExportSelected,
|
||||||
onExportAll,
|
onExportAll,
|
||||||
exportLoading,
|
exportLoading,
|
||||||
@ -79,6 +84,16 @@ const PageEditorControls = ({
|
|||||||
return hasAllSplits ? "Remove All Splits" : "Split All";
|
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 (
|
return (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
@ -183,6 +198,17 @@ const PageEditorControls = ({
|
|||||||
<ContentCutIcon />
|
<ContentCutIcon />
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
</Tooltip>
|
</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' }} />
|
<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)',
|
boxShadow: '0 2px 4px rgba(0,0,0,0.1)',
|
||||||
pointerEvents: 'auto'
|
pointerEvents: 'auto'
|
||||||
}}
|
}}
|
||||||
onMouseDown={(e) => e.stopPropagation()}
|
onMouseDown={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onTogglePage(page.pageNumber);
|
||||||
|
}}
|
||||||
onMouseUp={(e) => e.stopPropagation()}
|
onMouseUp={(e) => e.stopPropagation()}
|
||||||
onDragStart={(e) => {
|
onDragStart={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@ -302,9 +305,10 @@ const PageThumbnail: React.FC<PageThumbnailProps> = ({
|
|||||||
<Checkbox
|
<Checkbox
|
||||||
checked={Array.isArray(selectedPages) ? selectedPages.includes(page.pageNumber) : false}
|
checked={Array.isArray(selectedPages) ? selectedPages.includes(page.pageNumber) : false}
|
||||||
onChange={() => {
|
onChange={() => {
|
||||||
// onChange is handled by the parent div click
|
// Selection is handled by container mouseDown
|
||||||
}}
|
}}
|
||||||
size="sm"
|
size="sm"
|
||||||
|
style={{ pointerEvents: 'none' }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@ -323,7 +327,14 @@ const PageThumbnail: React.FC<PageThumbnailProps> = ({
|
|||||||
justifyContent: 'center'
|
justifyContent: 'center'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{thumbnailUrl ? (
|
{page.isBlankPage ? (
|
||||||
|
<div style={{
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
backgroundColor: 'white',
|
||||||
|
borderRadius: 4
|
||||||
|
}}></div>
|
||||||
|
) : thumbnailUrl ? (
|
||||||
<img
|
<img
|
||||||
src={thumbnailUrl}
|
src={thumbnailUrl}
|
||||||
alt={`Page ${page.pageNumber}`}
|
alt={`Page ${page.pageNumber}`}
|
||||||
@ -354,7 +365,7 @@ const PageThumbnail: React.FC<PageThumbnailProps> = ({
|
|||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
top: 5,
|
top: 5,
|
||||||
left: 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',
|
padding: '6px 8px',
|
||||||
borderRadius: 8,
|
borderRadius: 8,
|
||||||
zIndex: 2,
|
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
|
// Simple undo manager for DOM commands
|
||||||
export class UndoManager {
|
export class UndoManager {
|
||||||
private undoStack: DOMCommand[] = [];
|
private undoStack: DOMCommand[] = [];
|
||||||
|
@ -121,10 +121,10 @@ export class DocumentManipulationService {
|
|||||||
*/
|
*/
|
||||||
private getRotationFromDOM(pageElement: Element, originalPage: PDFPage): number {
|
private getRotationFromDOM(pageElement: Element, originalPage: PDFPage): number {
|
||||||
const img = pageElement.querySelector('img');
|
const img = pageElement.querySelector('img');
|
||||||
if (img && img.style.rotate) {
|
if (img && img.style.transform) {
|
||||||
// Parse rotation from DOM (e.g., "90deg" -> 90)
|
// Parse rotation from transform property (e.g., "rotate(90deg)" -> 90)
|
||||||
const rotationMatch = img.style.rotate.match(/-?\d+/);
|
const rotationMatch = img.style.transform.match(/rotate\((-?\d+)deg\)/);
|
||||||
const domRotation = rotationMatch ? parseInt(rotationMatch[0]) : 0;
|
const domRotation = rotationMatch ? parseInt(rotationMatch[1]) : 0;
|
||||||
|
|
||||||
console.log(`Page ${originalPage.pageNumber}: DOM rotation = ${domRotation}°, original = ${originalPage.rotation}°`);
|
console.log(`Page ${originalPage.pageNumber}: DOM rotation = ${domRotation}°, original = ${originalPage.rotation}°`);
|
||||||
return domRotation;
|
return domRotation;
|
||||||
@ -146,7 +146,7 @@ export class DocumentManipulationService {
|
|||||||
const img = pageElement.querySelector('img');
|
const img = pageElement.querySelector('img');
|
||||||
if (img) {
|
if (img) {
|
||||||
// Reset rotation to match document state
|
// 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();
|
const newDoc = await PDFLibDocument.create();
|
||||||
|
|
||||||
for (const page of pages) {
|
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
|
// Get the original page from source document using originalPageNumber
|
||||||
const sourcePageIndex = page.originalPageNumber - 1;
|
const sourcePageIndex = page.originalPageNumber - 1;
|
||||||
|
|
||||||
@ -65,6 +74,7 @@ export class PDFExportService {
|
|||||||
newDoc.addPage(copiedPage);
|
newDoc.addPage(copiedPage);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Set metadata
|
// Set metadata
|
||||||
newDoc.setCreator('Stirling PDF');
|
newDoc.setCreator('Stirling PDF');
|
||||||
|
@ -6,6 +6,7 @@ export interface PDFPage {
|
|||||||
rotation: number;
|
rotation: number;
|
||||||
selected: boolean;
|
selected: boolean;
|
||||||
splitAfter?: boolean;
|
splitAfter?: boolean;
|
||||||
|
isBlankPage?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PDFDocument {
|
export interface PDFDocument {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user