diff --git a/frontend/src/components/layout/Workbench.tsx b/frontend/src/components/layout/Workbench.tsx
index 0e93313fc..35398eb14 100644
--- a/frontend/src/components/layout/Workbench.tsx
+++ b/frontend/src/components/layout/Workbench.tsx
@@ -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}
diff --git a/frontend/src/components/pageEditor/PageEditor.tsx b/frontend/src/components/pageEditor/PageEditor.tsx
index db828bdd2..21403731d 100644
--- a/frontend/src/components/pageEditor/PageEditor.tsx
+++ b/frontend/src/components/pageEditor/PageEditor.tsx
@@ -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
diff --git a/frontend/src/components/pageEditor/PageEditorControls.tsx b/frontend/src/components/pageEditor/PageEditorControls.tsx
index 7d053760a..ca351477a 100644
--- a/frontend/src/components/pageEditor/PageEditorControls.tsx
+++ b/frontend/src/components/pageEditor/PageEditorControls.tsx
@@ -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 (
+
+ 0 ? "light" : "default"}
+ color={selectionMode && selectedPages.length > 0 ? "orange" : undefined}
+ size="lg"
+ >
+
+
+
diff --git a/frontend/src/components/pageEditor/PageThumbnail.tsx b/frontend/src/components/pageEditor/PageThumbnail.tsx
index 1de6a4fa4..f734e2b85 100644
--- a/frontend/src/components/pageEditor/PageThumbnail.tsx
+++ b/frontend/src/components/pageEditor/PageThumbnail.tsx
@@ -95,7 +95,7 @@ const PageThumbnail: React.FC = ({
// Request thumbnail generation if we have the original file
if (originalFile) {
const pageNumber = page.originalPageNumber;
-
+
requestThumbnail(page.id, originalFile, pageNumber)
.then(thumbnail => {
if (!isCancelled && thumbnail) {
@@ -116,14 +116,14 @@ const PageThumbnail: React.FC = ({
if (element) {
pageRefs.current.set(page.id, element);
dragElementRef.current = element;
-
+
const dragCleanup = draggable({
element,
getInitialData: () => ({
pageNumber: page.pageNumber,
pageId: page.id,
- selectedPages: selectionMode && selectedPages.includes(page.pageNumber)
- ? selectedPages
+ selectedPages: selectionMode && selectedPages.includes(page.pageNumber)
+ ? selectedPages
: [page.pageNumber]
}),
onDragStart: () => {
@@ -131,14 +131,14 @@ const PageThumbnail: React.FC = ({
},
onDrop: ({ location }) => {
setIsDragging(false);
-
+
if (location.current.dropTargets.length === 0) {
return;
}
-
+
const dropTarget = location.current.dropTargets[0];
const targetData = dropTarget.data;
-
+
if (targetData.type === 'page') {
const targetPageNumber = targetData.pageNumber as number;
const targetIndex = pdfDocument.pages.findIndex(p => p.pageNumber === targetPageNumber);
@@ -155,7 +155,7 @@ const PageThumbnail: React.FC = ({
});
element.style.cursor = 'grab';
-
+
const dropCleanup = dropTargetForElements({
element,
getData: () => ({
@@ -164,7 +164,7 @@ const PageThumbnail: React.FC = ({
}),
onDrop: ({ source }) => {}
});
-
+
(element as any).__dragCleanup = () => {
dragCleanup();
dropCleanup();
@@ -202,11 +202,11 @@ const PageThumbnail: React.FC = ({
const handleSplit = useCallback((e: React.MouseEvent) => {
e.stopPropagation();
-
+
// Create a command to toggle split at this position
const command = createSplitCommand(index);
onExecuteCommand(command);
-
+
const hasSplit = splitPositions.has(index);
const action = hasSplit ? 'removed' : 'added';
onSetStatus(`Split marker ${action} after position ${index + 1}`);
@@ -215,7 +215,7 @@ const PageThumbnail: React.FC = ({
// Handle click vs drag differentiation
const handleMouseDown = useCallback((e: React.MouseEvent) => {
if (!selectionMode) return;
-
+
setIsMouseDown(true);
setMouseStartPos({ x: e.clientX, y: e.clientY });
}, [selectionMode]);
@@ -292,7 +292,10 @@ const PageThumbnail: React.FC = ({
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 = ({
{
- // onChange is handled by the parent div click
+ // Selection is handled by container mouseDown
}}
size="sm"
+ style={{ pointerEvents: 'none' }}
/>
)}
@@ -323,7 +327,14 @@ const PageThumbnail: React.FC = ({
justifyContent: 'center'
}}
>
- {thumbnailUrl ? (
+ {page.isBlankPage ? (
+
+ ) : thumbnailUrl ? (
= ({
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,
@@ -486,4 +497,4 @@ const PageThumbnail: React.FC = ({
);
};
-export default PageThumbnail;
\ No newline at end of file
+export default PageThumbnail;
diff --git a/frontend/src/components/pageEditor/commands/pageCommands.ts b/frontend/src/components/pageEditor/commands/pageCommands.ts
index 0cb7d0c15..a12144dcc 100644
--- a/frontend/src/components/pageEditor/commands/pageCommands.ts
+++ b/frontend/src/components/pageEditor/commands/pageCommands.ts
@@ -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[] = [];
diff --git a/frontend/src/services/documentManipulationService.ts b/frontend/src/services/documentManipulationService.ts
index 9d3a64be6..f84623c6e 100644
--- a/frontend/src/services/documentManipulationService.ts
+++ b/frontend/src/services/documentManipulationService.ts
@@ -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)`;
}
}
});
diff --git a/frontend/src/services/pdfExportService.ts b/frontend/src/services/pdfExportService.ts
index b4f02bd5d..8eaff25cd 100644
--- a/frontend/src/services/pdfExportService.ts
+++ b/frontend/src/services/pdfExportService.ts
@@ -50,19 +50,29 @@ export class PDFExportService {
const newDoc = await PDFLibDocument.create();
for (const page of pages) {
- // Get the original page from source document using originalPageNumber
- const sourcePageIndex = page.originalPageNumber - 1;
-
- if (sourcePageIndex >= 0 && sourcePageIndex < sourceDoc.getPageCount()) {
- // Copy the page
- const [copiedPage] = await newDoc.copyPages(sourceDoc, [sourcePageIndex]);
-
- // Apply rotation
+ if (page.isBlankPage || page.originalPageNumber === -1) {
+ // Create a blank page
+ const blankPage = newDoc.addPage(PageSizes.A4);
+
+ // Apply rotation if needed
if (page.rotation !== 0) {
- copiedPage.setRotation(degrees(page.rotation));
+ blankPage.setRotation(degrees(page.rotation));
}
+ } else {
+ // Get the original page from source document using originalPageNumber
+ const sourcePageIndex = page.originalPageNumber - 1;
- newDoc.addPage(copiedPage);
+ if (sourcePageIndex >= 0 && sourcePageIndex < sourceDoc.getPageCount()) {
+ // Copy the page
+ const [copiedPage] = await newDoc.copyPages(sourceDoc, [sourcePageIndex]);
+
+ // Apply rotation
+ if (page.rotation !== 0) {
+ copiedPage.setRotation(degrees(page.rotation));
+ }
+
+ newDoc.addPage(copiedPage);
+ }
}
}
diff --git a/frontend/src/types/pageEditor.ts b/frontend/src/types/pageEditor.ts
index 9622052da..dd27ef627 100644
--- a/frontend/src/types/pageEditor.ts
+++ b/frontend/src/types/pageEditor.ts
@@ -6,6 +6,7 @@ export interface PDFPage {
rotation: number;
selected: boolean;
splitAfter?: boolean;
+ isBlankPage?: boolean;
}
export interface PDFDocument {