diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/BookletImpositionController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/BookletImpositionController.java index 72010243b..d2e278429 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/BookletImpositionController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/BookletImpositionController.java @@ -49,28 +49,37 @@ public class BookletImpositionController { @ModelAttribute BookletImpositionRequest request) throws IOException { MultipartFile file = request.getFileInput(); - String bookletType = request.getBookletType(); int pagesPerSheet = request.getPagesPerSheet(); boolean addBorder = Boolean.TRUE.equals(request.getAddBorder()); - String pageOrientation = request.getPageOrientation(); + String spineLocation = + request.getSpineLocation() != null ? request.getSpineLocation() : "LEFT"; + boolean addGutter = Boolean.TRUE.equals(request.getAddGutter()); + float gutterSize = request.getGutterSize(); + boolean doubleSided = Boolean.TRUE.equals(request.getDoubleSided()); + String duplexPass = request.getDuplexPass() != null ? request.getDuplexPass() : "BOTH"; + boolean flipOnShortEdge = Boolean.TRUE.equals(request.getFlipOnShortEdge()); - // Validate pages per sheet for booklet - if (pagesPerSheet != 2 && pagesPerSheet != 4) { + // Validate pages per sheet for booklet - only 2-up landscape is proper booklet + if (pagesPerSheet != 2) { throw new IllegalArgumentException( - "pagesPerSheet must be 2 or 4 for booklet imposition"); + "Booklet printing uses 2 pages per side (landscape). For 4-up, use the N-up feature."); } PDDocument sourceDocument = pdfDocumentFactory.load(file); int totalPages = sourceDocument.getNumberOfPages(); - // Step 1: Reorder pages for booklet (reusing logic from RearrangePagesPDFController) - List bookletOrder = getBookletPageOrder(bookletType, totalPages); - - // Step 2: Create new document with multi-page layout (reusing logic from - // MultiPageLayoutController) + // Create proper booklet with signature-based page ordering PDDocument newDocument = - createBookletWithLayout( - sourceDocument, bookletOrder, pagesPerSheet, addBorder, pageOrientation); + createSaddleBooklet( + sourceDocument, + totalPages, + addBorder, + spineLocation, + addGutter, + gutterSize, + doubleSided, + duplexPass, + flipOnShortEdge); sourceDocument.close(); @@ -85,124 +94,231 @@ public class BookletImpositionController { + "_booklet.pdf"); } - // Reused logic from RearrangePagesPDFController - private List getBookletPageOrder(String bookletType, int totalPages) { - if ("SIDE_STITCH_BOOKLET".equals(bookletType)) { - return sideStitchBookletSort(totalPages); - } else { - return bookletSort(totalPages); + private static int padToMultipleOf4(int n) { + return (n + 3) / 4 * 4; + } + + private static class Side { + final int left, right; + final boolean isBack; + + Side(int left, int right, boolean isBack) { + this.left = left; + this.right = right; + this.isBack = isBack; } } - private List bookletSort(int totalPages) { - List newPageOrder = new ArrayList<>(); - for (int i = 0; i < totalPages / 2; i++) { - newPageOrder.add(i); - newPageOrder.add(totalPages - i - 1); + private static List saddleStitchSides( + int totalPagesOriginal, + boolean doubleSided, + String duplexPass, + boolean flipOnShortEdge) { + int N = padToMultipleOf4(totalPagesOriginal); + List out = new ArrayList<>(); + int sheets = N / 4; + + for (int s = 0; s < sheets; s++) { + int a = N - 1 - (s * 2); // left, front + int b = (s * 2); // right, front + int c = (s * 2) + 1; // left, back + int d = N - 2 - (s * 2); // right, back + + // clamp to -1 (blank) if >= totalPagesOriginal + a = (a < totalPagesOriginal) ? a : -1; + b = (b < totalPagesOriginal) ? b : -1; + c = (c < totalPagesOriginal) ? c : -1; + d = (d < totalPagesOriginal) ? d : -1; + + // Handle duplex pass selection + boolean includeFront = "BOTH".equals(duplexPass) || "FIRST".equals(duplexPass); + boolean includeBack = "BOTH".equals(duplexPass) || "SECOND".equals(duplexPass); + + if (includeFront) { + out.add(new Side(a, b, false)); // front side + } + + if (includeBack) { + // For short-edge duplex, swap back-side left/right + // Note: flipOnShortEdge is ignored in manual duplex mode since users physically + // flip the stack + if (doubleSided && flipOnShortEdge) { + out.add(new Side(d, c, true)); // swapped back side (automatic duplex only) + } else { + out.add(new Side(c, d, true)); // normal back side + } + } } - return newPageOrder; + return out; } - private List sideStitchBookletSort(int totalPages) { - List newPageOrder = new ArrayList<>(); - for (int i = 0; i < (totalPages + 3) / 4; i++) { - int begin = i * 4; - newPageOrder.add(Math.min(begin + 3, totalPages - 1)); - newPageOrder.add(Math.min(begin, totalPages - 1)); - newPageOrder.add(Math.min(begin + 1, totalPages - 1)); - newPageOrder.add(Math.min(begin + 2, totalPages - 1)); - } - return newPageOrder; - } - - // Reused and adapted logic from MultiPageLayoutController - private PDDocument createBookletWithLayout( - PDDocument sourceDocument, - List pageOrder, - int pagesPerSheet, + private PDDocument createSaddleBooklet( + PDDocument src, + int totalPages, boolean addBorder, - String pageOrientation) + String spineLocation, + boolean addGutter, + float gutterSize, + boolean doubleSided, + String duplexPass, + boolean flipOnShortEdge) throws IOException { - PDDocument newDocument = - pdfDocumentFactory.createNewDocumentBasedOnOldDocument(sourceDocument); + PDDocument dst = pdfDocumentFactory.createNewDocumentBasedOnOldDocument(src); - int cols = pagesPerSheet == 2 ? 2 : 2; // 2x1 for 2 pages, 2x2 for 4 pages - int rows = pagesPerSheet == 2 ? 1 : 2; + // Derive paper size from source document's first page CropBox + PDRectangle srcBox = src.getPage(0).getCropBox(); + PDRectangle portraitPaper = new PDRectangle(srcBox.getWidth(), srcBox.getHeight()); + // Force landscape for booklet (Acrobat booklet uses landscape paper to fold to portrait) + PDRectangle pageSize = new PDRectangle(portraitPaper.getHeight(), portraitPaper.getWidth()); - int currentPageIndex = 0; - int totalOrderedPages = pageOrder.size(); + // Validate and clamp gutter size + if (gutterSize < 0) gutterSize = 0; + if (gutterSize >= pageSize.getWidth() / 2f) gutterSize = pageSize.getWidth() / 2f - 1f; - while (currentPageIndex < totalOrderedPages) { - // Use landscape orientation for booklets (A4 landscape -> A5 portrait when folded) - PDRectangle pageSize = - "LANDSCAPE".equals(pageOrientation) - ? new PDRectangle(PDRectangle.A4.getHeight(), PDRectangle.A4.getWidth()) - : PDRectangle.A4; - PDPage newPage = new PDPage(pageSize); - newDocument.addPage(newPage); + List sides = saddleStitchSides(totalPages, doubleSided, duplexPass, flipOnShortEdge); - float cellWidth = newPage.getMediaBox().getWidth() / cols; - float cellHeight = newPage.getMediaBox().getHeight() / rows; + for (Side side : sides) { + PDPage out = new PDPage(pageSize); + dst.addPage(out); - PDPageContentStream contentStream = + float cellW = pageSize.getWidth() / 2f; + float cellH = pageSize.getHeight(); + + // For RIGHT spine (RTL), swap left/right placements + boolean rtl = "RIGHT".equalsIgnoreCase(spineLocation); + int leftCol = rtl ? 1 : 0; + int rightCol = rtl ? 0 : 1; + + // Apply gutter margins with centered gap option + float g = addGutter ? gutterSize : 0f; + float leftCellX = leftCol * cellW + (g / 2f); + float rightCellX = rightCol * cellW - (g / 2f); + float leftCellW = cellW - (g / 2f); + float rightCellW = cellW - (g / 2f); + + // Create LayerUtility once per page for efficiency + LayerUtility layerUtility = new LayerUtility(dst); + + try (PDPageContentStream cs = new PDPageContentStream( - newDocument, - newPage, - PDPageContentStream.AppendMode.APPEND, - true, - true); - LayerUtility layerUtility = new LayerUtility(newDocument); - - if (addBorder) { - contentStream.setLineWidth(1.5f); - contentStream.setStrokingColor(Color.BLACK); - } - - // Place pages on the current sheet - for (int sheetPosition = 0; - sheetPosition < pagesPerSheet && currentPageIndex < totalOrderedPages; - sheetPosition++) { - int sourcePageIndex = pageOrder.get(currentPageIndex); - PDPage sourcePage = sourceDocument.getPage(sourcePageIndex); - PDRectangle rect = sourcePage.getMediaBox(); - - float scaleWidth = cellWidth / rect.getWidth(); - float scaleHeight = cellHeight / rect.getHeight(); - float scale = Math.min(scaleWidth, scaleHeight); - - int rowIndex = sheetPosition / cols; - int colIndex = sheetPosition % cols; - - float x = colIndex * cellWidth + (cellWidth - rect.getWidth() * scale) / 2; - float y = - newPage.getMediaBox().getHeight() - - ((rowIndex + 1) * cellHeight - - (cellHeight - rect.getHeight() * scale) / 2); - - contentStream.saveGraphicsState(); - contentStream.transform(Matrix.getTranslateInstance(x, y)); - contentStream.transform(Matrix.getScaleInstance(scale, scale)); - - PDFormXObject formXObject = - layerUtility.importPageAsForm(sourceDocument, sourcePageIndex); - contentStream.drawForm(formXObject); - - contentStream.restoreGraphicsState(); + dst, out, PDPageContentStream.AppendMode.APPEND, true, true)) { if (addBorder) { - float borderX = colIndex * cellWidth; - float borderY = newPage.getMediaBox().getHeight() - (rowIndex + 1) * cellHeight; - contentStream.addRect(borderX, borderY, cellWidth, cellHeight); - contentStream.stroke(); + cs.setLineWidth(1.5f); + cs.setStrokingColor(Color.BLACK); } - currentPageIndex++; + // draw left cell + drawCell( + src, + dst, + cs, + layerUtility, + side.left, + leftCellX, + 0f, + leftCellW, + cellH, + addBorder); + // draw right cell + drawCell( + src, + dst, + cs, + layerUtility, + side.right, + rightCellX, + 0f, + rightCellW, + cellH, + addBorder); } + } + return dst; + } - contentStream.close(); + private void drawCell( + PDDocument src, + PDDocument dst, + PDPageContentStream cs, + LayerUtility layerUtility, + int pageIndex, + float cellX, + float cellY, + float cellW, + float cellH, + boolean addBorder) + throws IOException { + + if (pageIndex < 0) { + // Draw border for blank cell if needed + if (addBorder) { + cs.addRect(cellX, cellY, cellW, cellH); + cs.stroke(); + } + return; } - return newDocument; + PDPage srcPage = src.getPage(pageIndex); + PDRectangle r = srcPage.getCropBox(); // Use CropBox instead of MediaBox + int rot = (srcPage.getRotation() + 360) % 360; + + // Calculate scale factors, accounting for rotation + float sx = cellW / r.getWidth(); + float sy = cellH / r.getHeight(); + float s = Math.min(sx, sy); + + // If rotated 90/270 degrees, swap dimensions for fitting + if (rot == 90 || rot == 270) { + sx = cellW / r.getHeight(); + sy = cellH / r.getWidth(); + s = Math.min(sx, sy); + } + + float drawnW = (rot == 90 || rot == 270) ? r.getHeight() * s : r.getWidth() * s; + float drawnH = (rot == 90 || rot == 270) ? r.getWidth() * s : r.getHeight() * s; + + // Center in cell, accounting for CropBox offset + float tx = cellX + (cellW - drawnW) / 2f - r.getLowerLeftX() * s; + float ty = cellY + (cellH - drawnH) / 2f - r.getLowerLeftY() * s; + + cs.saveGraphicsState(); + cs.transform(Matrix.getTranslateInstance(tx, ty)); + cs.transform(Matrix.getScaleInstance(s, s)); + + // Apply rotation if needed (rotate about origin), then translate to keep in cell + switch (rot) { + case 90: + cs.transform(Matrix.getRotateInstance(Math.PI / 2, 0, 0)); + // After 90° CCW, the content spans x in [-r.getHeight(), 0] and y in [0, + // r.getWidth()] + cs.transform(Matrix.getTranslateInstance(0, -r.getWidth())); + break; + case 180: + cs.transform(Matrix.getRotateInstance(Math.PI, 0, 0)); + cs.transform(Matrix.getTranslateInstance(-r.getWidth(), -r.getHeight())); + break; + case 270: + cs.transform(Matrix.getRotateInstance(3 * Math.PI / 2, 0, 0)); + // After 270° CCW, the content spans x in [0, r.getHeight()] and y in + // [-r.getWidth(), 0] + cs.transform(Matrix.getTranslateInstance(-r.getHeight(), 0)); + break; + default: + // 0°: no-op + } + + // Reuse LayerUtility passed from caller + PDFormXObject form = layerUtility.importPageAsForm(src, pageIndex); + cs.drawForm(form); + + cs.restoreGraphicsState(); + + // Draw border on top of form to ensure visibility + if (addBorder) { + cs.addRect(cellX, cellY, cellW, cellH); + cs.stroke(); + } } } diff --git a/app/core/src/main/java/stirling/software/SPDF/model/api/general/BookletImpositionRequest.java b/app/core/src/main/java/stirling/software/SPDF/model/api/general/BookletImpositionRequest.java index b7a7a5a1b..456302e55 100644 --- a/app/core/src/main/java/stirling/software/SPDF/model/api/general/BookletImpositionRequest.java +++ b/app/core/src/main/java/stirling/software/SPDF/model/api/general/BookletImpositionRequest.java @@ -12,29 +12,43 @@ import stirling.software.common.model.api.PDFFile; public class BookletImpositionRequest extends PDFFile { @Schema( - description = "The booklet type to create.", - type = "string", - defaultValue = "BOOKLET", - requiredMode = Schema.RequiredMode.REQUIRED, - allowableValues = {"BOOKLET", "SIDE_STITCH_BOOKLET"}) - private String bookletType = "BOOKLET"; - - @Schema( - description = "The number of pages to fit onto a single sheet in the output PDF.", + description = + "The number of pages per side for booklet printing (always 2 for proper booklet).", type = "number", defaultValue = "2", requiredMode = Schema.RequiredMode.REQUIRED, - allowableValues = {"2", "4"}) + allowableValues = {"2"}) private int pagesPerSheet = 2; @Schema(description = "Boolean for if you wish to add border around the pages") private Boolean addBorder = false; @Schema( - description = "The page orientation for the output booklet sheets.", + description = "The spine location for the booklet.", type = "string", - defaultValue = "LANDSCAPE", - requiredMode = Schema.RequiredMode.REQUIRED, - allowableValues = {"LANDSCAPE", "PORTRAIT"}) - private String pageOrientation = "LANDSCAPE"; + defaultValue = "LEFT", + allowableValues = {"LEFT", "RIGHT"}) + private String spineLocation = "LEFT"; + + @Schema(description = "Add gutter margin (inner margin for binding)") + private Boolean addGutter = false; + + @Schema( + description = "Gutter margin size in points (used when addGutter is true)", + type = "number", + defaultValue = "12") + private float gutterSize = 12f; + + @Schema(description = "Generate both front and back sides (double-sided printing)") + private Boolean doubleSided = true; + + @Schema( + description = "For manual duplex: which pass to generate", + type = "string", + defaultValue = "BOTH", + allowableValues = {"BOTH", "FIRST", "SECOND"}) + private String duplexPass = "BOTH"; + + @Schema(description = "Flip back sides for short-edge duplex printing (default is long-edge)") + private Boolean flipOnShortEdge = false; } diff --git a/frontend/public/locales/en-GB/translation.json b/frontend/public/locales/en-GB/translation.json index 41510422e..629ce31e2 100644 --- a/frontend/public/locales/en-GB/translation.json +++ b/frontend/public/locales/en-GB/translation.json @@ -1936,59 +1936,90 @@ "title": "Booklet Imposition", "header": "Booklet Imposition", "submit": "Create Booklet", - "bookletType": { - "label": "Style", - "standard": "Standard", - "sideStitch": "Side Stitch", - "standardDesc": "Standard for saddle-stitched binding (fold in half)", - "sideStitchDesc": "Side stitch for binding along the edge" + "spineLocation": { + "label": "Spine Location", + "left": "Left (Standard)", + "right": "Right (RTL)" }, - "pagesPerSheet": { - "label": "Pages Per Sheet", - "two": "2 Pages", - "four": "4 Pages", - "twoDesc": "Two pages side by side on each sheet (most common)", - "fourDesc": "Four pages arranged in a 2x2 grid on each sheet" + "doubleSided": { + "label": "Double-sided printing", + "tooltip": "Creates both front and back sides for proper booklet printing" }, - "pageOrientation": { - "label": "Page Orientation", - "landscape": "Landscape", - "portrait": "Portrait", - "landscapeDesc": "A4 landscape → A5 portrait when folded (recommended, standard booklet size)", - "portraitDesc": "A4 portrait → A6 when folded (smaller booklet)" + "manualDuplex": { + "title": "Manual Duplex Mode", + "instructions": "For printers without automatic duplex. You'll need to run this twice:" + }, + "duplexPass": { + "label": "Print Pass", + "first": "1st Pass", + "second": "2nd Pass", + "firstInstructions": "Prints front sides → stack face-down → run again with 2nd Pass", + "secondInstructions": "Load printed stack face-down → prints back sides" + }, + "rtlBinding": { + "label": "Right-to-left binding", + "tooltip": "For Arabic, Hebrew, or other right-to-left languages" }, "addBorder": { "label": "Add borders around pages", - "tooltip": "Adds borders around each page section to help with cutting and alignment", - "description": "Helpful for cutting and alignment when printing" + "tooltip": "Adds borders around each page section to help with cutting and alignment" }, + "addGutter": { + "label": "Add gutter margin", + "tooltip": "Adds inner margin space for binding" + }, + "gutterSize": { + "label": "Gutter size (points)" + }, + "flipOnShortEdge": { + "label": "Flip on short edge (automatic duplex only)", + "tooltip": "Enable for short-edge duplex printing (automatic duplex only - ignored in manual mode)", + "manualNote": "Not needed in manual mode - you flip the stack yourself" + }, + "advanced": { + "toggle": "Advanced Options" + }, + "paperSizeNote": "Paper size is automatically derived from your first page.", "tooltip": { - "title": "Booklet Imposition Guide", - "overview": { + "header": { + "title": "Booklet Creation Guide" + }, + "description": { "title": "What is Booklet Imposition?", - "description": "Arranges PDF pages in the correct order for booklet printing. Pages are reordered so that when printed and folded, they appear in sequence.", - "bullet1": "Creates printable booklets from regular PDFs", - "bullet2": "Handles page ordering for folding", - "bullet3": "Supports saddle-stitch and side-stitch binding" + "text": "Creates professional booklets by arranging pages in the correct printing order. Your PDF pages are placed 2-up on landscape sheets so when folded and bound, they read in proper sequence like a real book." }, - "bookletTypes": { - "title": "Booklet Types", - "standard": "Standard: Saddle-stitched binding (staples along fold)", - "sideStitch": "Side-Stitch: Binding along edge (spiral, ring, perfect)" + "example": { + "title": "Example: 8-Page Booklet", + "text": "Your 8-page document becomes 2 sheets:", + "bullet1": "Sheet 1 Front: Pages 8, 1 | Back: Pages 2, 7", + "bullet2": "Sheet 2 Front: Pages 6, 3 | Back: Pages 4, 5", + "bullet3": "When folded & stacked: Reads 1→2→3→4→5→6→7→8" }, - "pagesPerSheet": { - "title": "Pages Per Sheet", - "two": "2 Pages: Standard layout (most common)", - "four": "4 Pages: Compact layout" + "printing": { + "title": "How to Print & Assemble", + "text": "Follow these steps for perfect booklets:", + "bullet1": "Print double-sided with 'Flip on long edge'", + "bullet2": "Stack sheets in order, fold in half", + "bullet3": "Staple or bind along the folded spine", + "bullet4": "For short-edge printers: Enable 'Flip on short edge' option" }, - "orientation": { - "title": "Page Orientation", - "landscape": "Landscape: A4 → A5 booklet (recommended)", - "portrait": "Portrait: A4 → A6 booklet (compact)" + "manualDuplex": { + "title": "Manual Duplex (Single-sided Printers)", + "text": "For printers without automatic duplex:", + "bullet1": "Turn OFF 'Double-sided printing'", + "bullet2": "Select '1st Pass' → Print → Stack face-down", + "bullet3": "Select '2nd Pass' → Load stack → Print backs", + "bullet4": "Fold and assemble as normal" + }, + "advanced": { + "title": "Advanced Options", + "text": "Fine-tune your booklet:", + "bullet1": "Right-to-Left Binding: For Arabic, Hebrew, or RTL languages", + "bullet2": "Borders: Shows cut lines for trimming", + "bullet3": "Gutter Margin: Adds space for binding/stapling", + "bullet4": "Short-edge Flip: Only for automatic duplex printers" } }, - "files": { - }, "error": { "failed": "An error occurred while creating the booklet imposition." } diff --git a/frontend/src/components/tools/bookletImposition/BookletImpositionSettings.tsx b/frontend/src/components/tools/bookletImposition/BookletImpositionSettings.tsx index 698240c5d..e2e7a502b 100644 --- a/frontend/src/components/tools/bookletImposition/BookletImpositionSettings.tsx +++ b/frontend/src/components/tools/bookletImposition/BookletImpositionSettings.tsx @@ -1,6 +1,6 @@ -import React from "react"; +import React, { useState } from "react"; import { useTranslation } from "react-i18next"; -import { Stack, Text, Divider } from "@mantine/core"; +import { Stack, Text, Divider, Collapse, Button, NumberInput } from "@mantine/core"; import { BookletImpositionParameters } from "../../../hooks/tools/bookletImposition/useBookletImpositionParameters"; import ButtonSelector from "../../shared/ButtonSelector"; @@ -12,73 +12,165 @@ interface BookletImpositionSettingsProps { const BookletImpositionSettings = ({ parameters, onParameterChange, disabled = false }: BookletImpositionSettingsProps) => { const { t } = useTranslation(); + const [advancedOpen, setAdvancedOpen] = useState(false); return ( - - {/* Booklet Type */} - - onParameterChange('bookletType', value)} - options={[ - { value: 'BOOKLET', label: t('bookletImposition.bookletType.standard', 'Standard Booklet') }, - { value: 'SIDE_STITCH_BOOKLET', label: t('bookletImposition.bookletType.sideStitch', 'Side-Stitch Booklet') } - ]} - disabled={disabled} - /> - - - {/* Pages Per Sheet */} - - onParameterChange('pagesPerSheet', value)} - options={[ - { value: 2, label: t('bookletImposition.pagesPerSheet.two', '2 Pages') }, - { value: 4, label: t('bookletImposition.pagesPerSheet.four', '4 Pages') } - ]} - disabled={disabled} - /> - - - - - {/* Page Orientation */} - - onParameterChange('pageOrientation', value)} - options={[ - { value: 'LANDSCAPE', label: t('bookletImposition.pageOrientation.landscape', 'Landscape') }, - { value: 'PORTRAIT', label: t('bookletImposition.pageOrientation.portrait', 'Portrait') } - ]} - disabled={disabled} - /> - - - - - {/* Add Border Option */} + {/* Double Sided */} + + {/* Manual Duplex Pass Selection - only show when double-sided is OFF */} + {!parameters.doubleSided && ( + + + {t('bookletImposition.manualDuplex.title', 'Manual Duplex Mode')} + + + {t('bookletImposition.manualDuplex.instructions', 'For printers without automatic duplex. You\'ll need to run this twice:')} + + + onParameterChange('duplexPass', value)} + options={[ + { value: 'FIRST', label: t('bookletImposition.duplexPass.first', '1st Pass') }, + { value: 'SECOND', label: t('bookletImposition.duplexPass.second', '2nd Pass') } + ]} + disabled={disabled} + /> + + + {parameters.duplexPass === 'FIRST' + ? t('bookletImposition.duplexPass.firstInstructions', 'Prints front sides → stack face-down → run again with 2nd Pass') + : t('bookletImposition.duplexPass.secondInstructions', 'Load printed stack face-down → prints back sides') + } + + + )} + + + + + {/* Advanced Options */} + + + + + + {/* Right-to-Left Binding */} + + + {/* Add Border Option */} + + + {/* Gutter Margin */} + + + + {parameters.addGutter && ( + onParameterChange('gutterSize', value || 12)} + min={6} + max={72} + step={6} + disabled={disabled} + size="sm" + /> + )} + + + {/* Flip on Short Edge */} + + + {/* Paper Size Note */} + + {t('bookletImposition.paperSizeNote', 'Paper size is automatically derived from your first page.')} + + + ); diff --git a/frontend/src/components/tooltips/useBookletImpositionTips.ts b/frontend/src/components/tooltips/useBookletImpositionTips.ts index 70deb6c5b..e690bf284 100644 --- a/frontend/src/components/tooltips/useBookletImpositionTips.ts +++ b/frontend/src/components/tooltips/useBookletImpositionTips.ts @@ -6,37 +6,50 @@ export const useBookletImpositionTips = (): TooltipContent => { return { header: { - title: t("bookletImposition.tooltip.title", "Booklet Imposition Guide") + title: t("bookletImposition.tooltip.header.title", "Booklet Creation Guide") }, tips: [ { - title: t("bookletImposition.tooltip.overview.title", "What is Booklet Imposition?"), - description: t("bookletImposition.tooltip.overview.description", "Arranges PDF pages in the correct order for booklet printing. Pages are reordered so that when printed and folded, they appear in sequence."), + title: t("bookletImposition.tooltip.description.title", "What is Booklet Imposition?"), + description: t("bookletImposition.tooltip.description.text", "Creates professional booklets by arranging pages in the correct printing order. Your PDF pages are placed 2-up on landscape sheets so when folded and bound, they read in proper sequence like a real book.") + }, + { + title: t("bookletImposition.tooltip.example.title", "Example: 8-Page Booklet"), + description: t("bookletImposition.tooltip.example.text", "Your 8-page document becomes 2 sheets:"), bullets: [ - t("bookletImposition.tooltip.overview.bullet1", "Creates printable booklets from regular PDFs"), - t("bookletImposition.tooltip.overview.bullet2", "Handles page ordering for folding"), - t("bookletImposition.tooltip.overview.bullet3", "Supports saddle-stitch and side-stitch binding") + t("bookletImposition.tooltip.example.bullet1", "Sheet 1 Front: Pages 8, 1 | Back: Pages 2, 7"), + t("bookletImposition.tooltip.example.bullet2", "Sheet 2 Front: Pages 6, 3 | Back: Pages 4, 5"), + t("bookletImposition.tooltip.example.bullet3", "When folded & stacked: Reads 1→2→3→4→5→6→7→8") ] }, { - title: t("bookletImposition.tooltip.bookletTypes.title", "Booklet Types"), + title: t("bookletImposition.tooltip.printing.title", "How to Print & Assemble"), + description: t("bookletImposition.tooltip.printing.text", "Follow these steps for perfect booklets:"), bullets: [ - t("bookletImposition.tooltip.bookletTypes.standard", "Standard: Saddle-stitched binding (staples along fold)"), - t("bookletImposition.tooltip.bookletTypes.sideStitch", "Side-Stitch: Binding along edge (spiral, ring, perfect)") + t("bookletImposition.tooltip.printing.bullet1", "Print double-sided with 'Flip on long edge'"), + t("bookletImposition.tooltip.printing.bullet2", "Stack sheets in order, fold in half"), + t("bookletImposition.tooltip.printing.bullet3", "Staple or bind along the folded spine"), + t("bookletImposition.tooltip.printing.bullet4", "For short-edge printers: Enable 'Flip on short edge' option") ] }, { - title: t("bookletImposition.tooltip.pagesPerSheet.title", "Pages Per Sheet"), + title: t("bookletImposition.tooltip.manualDuplex.title", "Manual Duplex (Single-sided Printers)"), + description: t("bookletImposition.tooltip.manualDuplex.text", "For printers without automatic duplex:"), bullets: [ - t("bookletImposition.tooltip.pagesPerSheet.two", "2 Pages: Standard layout (most common)"), - t("bookletImposition.tooltip.pagesPerSheet.four", "4 Pages: Compact layout") + t("bookletImposition.tooltip.manualDuplex.bullet1", "Turn OFF 'Double-sided printing'"), + t("bookletImposition.tooltip.manualDuplex.bullet2", "Select '1st Pass' → Print → Stack face-down"), + t("bookletImposition.tooltip.manualDuplex.bullet3", "Select '2nd Pass' → Load stack → Print backs"), + t("bookletImposition.tooltip.manualDuplex.bullet4", "Fold and assemble as normal") ] }, { - title: t("bookletImposition.tooltip.orientation.title", "Page Orientation"), + title: t("bookletImposition.tooltip.advanced.title", "Advanced Options"), + description: t("bookletImposition.tooltip.advanced.text", "Fine-tune your booklet:"), bullets: [ - t("bookletImposition.tooltip.orientation.landscape", "Landscape: A4 → A5 booklet (recommended)"), - t("bookletImposition.tooltip.orientation.portrait", "Portrait: A4 → A6 booklet (compact)") + t("bookletImposition.tooltip.advanced.bullet1", "Right-to-Left Binding: For Arabic, Hebrew, or RTL languages"), + t("bookletImposition.tooltip.advanced.bullet2", "Borders: Shows cut lines for trimming"), + t("bookletImposition.tooltip.advanced.bullet3", "Gutter Margin: Adds space for binding/stapling"), + t("bookletImposition.tooltip.advanced.bullet4", "Short-edge Flip: Only for automatic duplex printers") ] } ] diff --git a/frontend/src/hooks/tools/bookletImposition/useBookletImpositionOperation.ts b/frontend/src/hooks/tools/bookletImposition/useBookletImpositionOperation.ts index 81f9f4691..7a3f904c8 100644 --- a/frontend/src/hooks/tools/bookletImposition/useBookletImpositionOperation.ts +++ b/frontend/src/hooks/tools/bookletImposition/useBookletImpositionOperation.ts @@ -7,10 +7,14 @@ import { BookletImpositionParameters, defaultParameters } from './useBookletImpo export const buildBookletImpositionFormData = (parameters: BookletImpositionParameters, file: File): FormData => { const formData = new FormData(); formData.append("fileInput", file); - formData.append("bookletType", parameters.bookletType); formData.append("pagesPerSheet", parameters.pagesPerSheet.toString()); formData.append("addBorder", parameters.addBorder.toString()); - formData.append("pageOrientation", parameters.pageOrientation); + formData.append("spineLocation", parameters.spineLocation); + formData.append("addGutter", parameters.addGutter.toString()); + formData.append("gutterSize", parameters.gutterSize.toString()); + formData.append("doubleSided", parameters.doubleSided.toString()); + formData.append("duplexPass", parameters.duplexPass); + formData.append("flipOnShortEdge", parameters.flipOnShortEdge.toString()); return formData; }; diff --git a/frontend/src/hooks/tools/bookletImposition/useBookletImpositionParameters.ts b/frontend/src/hooks/tools/bookletImposition/useBookletImpositionParameters.ts index 2e951a1fa..fd794379b 100644 --- a/frontend/src/hooks/tools/bookletImposition/useBookletImpositionParameters.ts +++ b/frontend/src/hooks/tools/bookletImposition/useBookletImpositionParameters.ts @@ -2,17 +2,25 @@ import { BaseParameters } from '../../../types/parameters'; import { useBaseParameters, BaseParametersHook } from '../shared/useBaseParameters'; export interface BookletImpositionParameters extends BaseParameters { - bookletType: 'BOOKLET' | 'SIDE_STITCH_BOOKLET'; - pagesPerSheet: 2 | 4; + pagesPerSheet: 2; addBorder: boolean; - pageOrientation: 'LANDSCAPE' | 'PORTRAIT'; + spineLocation: 'LEFT' | 'RIGHT'; + addGutter: boolean; + gutterSize: number; + doubleSided: boolean; + duplexPass: 'BOTH' | 'FIRST' | 'SECOND'; + flipOnShortEdge: boolean; } export const defaultParameters: BookletImpositionParameters = { - bookletType: 'BOOKLET', pagesPerSheet: 2, addBorder: false, - pageOrientation: 'LANDSCAPE', + spineLocation: 'LEFT', + addGutter: false, + gutterSize: 12, + doubleSided: true, + duplexPass: 'BOTH', + flipOnShortEdge: false, }; export type BookletImpositionParametersHook = BaseParametersHook; @@ -22,7 +30,7 @@ export const useBookletImpositionParameters = (): BookletImpositionParametersHoo defaultParameters, endpointName: 'booklet-imposition', validateFn: (params) => { - return params.pagesPerSheet === 2 || params.pagesPerSheet === 4; + return params.pagesPerSheet === 2; }, }); }; \ No newline at end of file