mirror of
https://github.com/arsvendg/Stirling-PDF.git
synced 2026-07-14 10:34:06 +02:00
Co-authored-by: Copilot <[email protected]>
This commit is contained in:
+207
-32
@@ -48,25 +48,144 @@ public class MultiPageLayoutController {
|
|||||||
public ResponseEntity<byte[]> mergeMultiplePagesIntoOne(
|
public ResponseEntity<byte[]> mergeMultiplePagesIntoOne(
|
||||||
@ModelAttribute MergeMultiplePagesRequest request) throws IOException {
|
@ModelAttribute MergeMultiplePagesRequest request) throws IOException {
|
||||||
|
|
||||||
int pagesPerSheet = request.getPagesPerSheet();
|
int MAX_PAGES = 100000;
|
||||||
MultipartFile file = request.getFileInput();
|
int MAX_COLS = 300;
|
||||||
boolean addBorder = Boolean.TRUE.equals(request.getAddBorder());
|
int MAX_ROWS = 300;
|
||||||
|
|
||||||
if (pagesPerSheet != 2
|
String mode = request.getMode();
|
||||||
&& pagesPerSheet != 3
|
if (mode == null || mode.trim().isEmpty()) {
|
||||||
&& pagesPerSheet != (int) Math.sqrt(pagesPerSheet) * Math.sqrt(pagesPerSheet)) {
|
mode = "DEFAULT";
|
||||||
|
}
|
||||||
|
|
||||||
|
int rows;
|
||||||
|
int cols;
|
||||||
|
int pagesPerSheet;
|
||||||
|
|
||||||
|
switch (mode) {
|
||||||
|
case "DEFAULT":
|
||||||
|
pagesPerSheet = request.getPagesPerSheet();
|
||||||
|
if (pagesPerSheet != 2
|
||||||
|
&& pagesPerSheet
|
||||||
|
!= (int) Math.sqrt(pagesPerSheet) * Math.sqrt(pagesPerSheet)) {
|
||||||
|
throw ExceptionUtils.createIllegalArgumentException(
|
||||||
|
"error.invalidFormat",
|
||||||
|
"Invalid {0} format: {1}",
|
||||||
|
"pagesPerSheet",
|
||||||
|
"must be 2 or a perfect square");
|
||||||
|
}
|
||||||
|
|
||||||
|
cols = pagesPerSheet == 2 ? pagesPerSheet : (int) Math.sqrt(pagesPerSheet);
|
||||||
|
rows = pagesPerSheet == 2 ? 1 : (int) Math.sqrt(pagesPerSheet);
|
||||||
|
break;
|
||||||
|
case "CUSTOM":
|
||||||
|
rows = request.getRows();
|
||||||
|
cols = request.getCols();
|
||||||
|
if (rows <= 0 || cols <= 0) {
|
||||||
|
throw ExceptionUtils.createIllegalArgumentException(
|
||||||
|
"error.invalidFormat",
|
||||||
|
"Invalid {0} format: {1}",
|
||||||
|
"rows and cols",
|
||||||
|
"only strictly positive values are allowed");
|
||||||
|
}
|
||||||
|
pagesPerSheet = cols * rows;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw ExceptionUtils.createIllegalArgumentException(
|
||||||
|
"error.invalidFormat",
|
||||||
|
"Invalid {0} format: {1}",
|
||||||
|
"mode",
|
||||||
|
"only 'DEFAULT' and 'CUSTOM' are supported");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pagesPerSheet > MAX_PAGES) {
|
||||||
|
throw ExceptionUtils.createIllegalArgumentException(
|
||||||
|
"error.invalidArgument",
|
||||||
|
"Invalid {0} format: {1}",
|
||||||
|
"pagesPerSheet",
|
||||||
|
"must be less than " + MAX_PAGES);
|
||||||
|
}
|
||||||
|
if (cols > MAX_COLS) {
|
||||||
|
throw ExceptionUtils.createIllegalArgumentException(
|
||||||
|
"error.invalidArgument",
|
||||||
|
"Invalid {0} format: {1}",
|
||||||
|
"cols",
|
||||||
|
"must be less than " + MAX_COLS);
|
||||||
|
}
|
||||||
|
if (rows > MAX_ROWS) {
|
||||||
|
throw ExceptionUtils.createIllegalArgumentException(
|
||||||
|
"error.invalidArgument",
|
||||||
|
"Invalid {0} format: {1}",
|
||||||
|
"rows",
|
||||||
|
"must be less than " + MAX_ROWS);
|
||||||
|
}
|
||||||
|
|
||||||
|
String orientation = request.getOrientation();
|
||||||
|
if (orientation == null || orientation.trim().isEmpty()) {
|
||||||
|
orientation = "PORTRAIT";
|
||||||
|
}
|
||||||
|
if (!"PORTRAIT".equals(orientation) && !"LANDSCAPE".equals(orientation)) {
|
||||||
throw ExceptionUtils.createIllegalArgumentException(
|
throw ExceptionUtils.createIllegalArgumentException(
|
||||||
"error.invalidFormat",
|
"error.invalidFormat",
|
||||||
"Invalid {0} format: {1}",
|
"Invalid {0} format: {1}",
|
||||||
"pagesPerSheet",
|
"orientation",
|
||||||
"must be 2, 3 or a perfect square");
|
"only 'PORTRAIT' and 'LANDSCAPE' are supported");
|
||||||
}
|
}
|
||||||
|
|
||||||
int cols =
|
String arrangement = request.getArrangement();
|
||||||
pagesPerSheet == 2 || pagesPerSheet == 3
|
if (arrangement == null || arrangement.trim().isEmpty()) {
|
||||||
? pagesPerSheet
|
arrangement = "BY_ROWS";
|
||||||
: (int) Math.sqrt(pagesPerSheet);
|
}
|
||||||
int rows = pagesPerSheet == 2 || pagesPerSheet == 3 ? 1 : (int) Math.sqrt(pagesPerSheet);
|
if (!"BY_ROWS".equals(arrangement) && !"BY_COLUMNS".equals(arrangement)) {
|
||||||
|
throw ExceptionUtils.createIllegalArgumentException(
|
||||||
|
"error.invalidFormat",
|
||||||
|
"Invalid {0} format: {1}",
|
||||||
|
"arrangement",
|
||||||
|
"only 'BY_ROWS' and 'BY_COLUMNS' are supported");
|
||||||
|
}
|
||||||
|
|
||||||
|
String readingDirection = request.getReadingDirection();
|
||||||
|
if (readingDirection == null || readingDirection.trim().isEmpty()) {
|
||||||
|
readingDirection = "LTR";
|
||||||
|
}
|
||||||
|
if (!"LTR".equals(readingDirection) && !"RTL".equals(readingDirection)) {
|
||||||
|
throw ExceptionUtils.createIllegalArgumentException(
|
||||||
|
"error.invalidFormat",
|
||||||
|
"Invalid {0} format: {1}",
|
||||||
|
"readingDirection",
|
||||||
|
"only 'LTR' and 'RTL' are supported");
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean addBorder = Boolean.TRUE.equals(request.getAddBorder());
|
||||||
|
|
||||||
|
int topMargin = request.getTopMargin();
|
||||||
|
int bottomMargin = request.getBottomMargin();
|
||||||
|
int leftMargin = request.getLeftMargin();
|
||||||
|
int rightMargin = request.getRightMargin();
|
||||||
|
int innerMargin = request.getInnerMargin();
|
||||||
|
|
||||||
|
if (topMargin < 0
|
||||||
|
|| bottomMargin < 0
|
||||||
|
|| leftMargin < 0
|
||||||
|
|| rightMargin < 0
|
||||||
|
|| innerMargin < 0) {
|
||||||
|
throw ExceptionUtils.createIllegalArgumentException(
|
||||||
|
"error.invalidFormat",
|
||||||
|
"Invalid {0} format: {1}",
|
||||||
|
"Margins",
|
||||||
|
"only positive values are allowed");
|
||||||
|
}
|
||||||
|
|
||||||
|
int borderWidth = request.getBorderWidth() == 0 ? 1 : request.getBorderWidth();
|
||||||
|
|
||||||
|
if (addBorder && borderWidth <= 0) {
|
||||||
|
throw ExceptionUtils.createIllegalArgumentException(
|
||||||
|
"error.invalidFormat",
|
||||||
|
"Invalid {0} format: {1}",
|
||||||
|
"borderWidth",
|
||||||
|
"only strictly positive values are allowed when addBorder is true");
|
||||||
|
}
|
||||||
|
|
||||||
|
MultipartFile file = request.getFileInput();
|
||||||
|
|
||||||
try (PDDocument sourceDocument = pdfDocumentFactory.load(file)) {
|
try (PDDocument sourceDocument = pdfDocumentFactory.load(file)) {
|
||||||
try (PDDocument newDocument =
|
try (PDDocument newDocument =
|
||||||
@@ -74,16 +193,53 @@ public class MultiPageLayoutController {
|
|||||||
int totalPages = sourceDocument.getNumberOfPages();
|
int totalPages = sourceDocument.getNumberOfPages();
|
||||||
LayerUtility layerUtility = new LayerUtility(newDocument);
|
LayerUtility layerUtility = new LayerUtility(newDocument);
|
||||||
|
|
||||||
|
// Margin between page and content:
|
||||||
|
float pageWidth =
|
||||||
|
"PORTRAIT".equals(orientation)
|
||||||
|
? PDRectangle.A4.getWidth()
|
||||||
|
: PDRectangle.A4.getHeight();
|
||||||
|
float pageHeight =
|
||||||
|
"PORTRAIT".equals(orientation)
|
||||||
|
? PDRectangle.A4.getHeight()
|
||||||
|
: PDRectangle.A4.getWidth();
|
||||||
|
|
||||||
// Calculate cell dimensions once (all output pages are A4) - declare outside try
|
// Calculate cell dimensions once (all output pages are A4) - declare outside try
|
||||||
// blocks
|
// blocks
|
||||||
float cellWidth = PDRectangle.A4.getWidth() / cols;
|
float cellWidth = (pageWidth - leftMargin - rightMargin) / cols;
|
||||||
float cellHeight = PDRectangle.A4.getHeight() / rows;
|
float cellHeight = (pageHeight - topMargin - bottomMargin) / rows;
|
||||||
|
// Validate that outer margins and grid configuration yield positive cell size
|
||||||
|
if (cellWidth <= 0 || cellHeight <= 0) {
|
||||||
|
throw ExceptionUtils.createIllegalArgumentException(
|
||||||
|
"error.invalidFormat",
|
||||||
|
"Invalid {0} format: {1}",
|
||||||
|
"margin/layout configuration",
|
||||||
|
"Invalid margin or layout configuration: resulting cell size is non-positive. "
|
||||||
|
+ "Please reduce outer margins or adjust rows/columns.");
|
||||||
|
}
|
||||||
|
|
||||||
|
float innerWidth = cellWidth - 2 * innerMargin;
|
||||||
|
float innerHeight = cellHeight - 2 * innerMargin;
|
||||||
|
// Validate that inner margin fits within each cell
|
||||||
|
if (innerWidth <= 0 || innerHeight <= 0) {
|
||||||
|
throw ExceptionUtils.createIllegalArgumentException(
|
||||||
|
"error.invalidFormat",
|
||||||
|
"Invalid {0} format: {1}",
|
||||||
|
"inner margin",
|
||||||
|
"Invalid inner margin: resulting inner content area is non-positive. "
|
||||||
|
+ "Please reduce inner margin or adjust outer margins/layout.");
|
||||||
|
}
|
||||||
|
|
||||||
// Process pages in groups of pagesPerSheet, creating a new page and content stream
|
// Process pages in groups of pagesPerSheet, creating a new page and content stream
|
||||||
// for each group
|
// for each group
|
||||||
for (int i = 0; i < totalPages; i += pagesPerSheet) {
|
for (int i = 0; i < totalPages; i += pagesPerSheet) {
|
||||||
// Create a new output page for each group of pagesPerSheet
|
// Create a new output page for each group of pagesPerSheet
|
||||||
PDPage newPage = new PDPage(PDRectangle.A4);
|
// Create a new A4 landscape rectangle that we use when orientation is landscape
|
||||||
|
PDRectangle a4Landscape =
|
||||||
|
new PDRectangle(PDRectangle.A4.getHeight(), PDRectangle.A4.getWidth());
|
||||||
|
PDPage newPage =
|
||||||
|
"PORTRAIT".equals(orientation)
|
||||||
|
? new PDPage(PDRectangle.A4)
|
||||||
|
: new PDPage(a4Landscape);
|
||||||
newDocument.addPage(newPage);
|
newDocument.addPage(newPage);
|
||||||
|
|
||||||
// Use try-with-resources for each content stream to ensure proper cleanup
|
// Use try-with-resources for each content stream to ensure proper cleanup
|
||||||
@@ -95,30 +251,52 @@ public class MultiPageLayoutController {
|
|||||||
PDPageContentStream.AppendMode.APPEND,
|
PDPageContentStream.AppendMode.APPEND,
|
||||||
true,
|
true,
|
||||||
true)) {
|
true)) {
|
||||||
float borderThickness = 1.5f; // Specify border thickness as required
|
|
||||||
contentStream.setLineWidth(borderThickness);
|
if (addBorder) {
|
||||||
contentStream.setStrokingColor(Color.BLACK);
|
contentStream.setLineWidth(borderWidth);
|
||||||
|
contentStream.setStrokingColor(Color.BLACK);
|
||||||
|
}
|
||||||
|
|
||||||
// Process all pages in this group
|
// Process all pages in this group
|
||||||
for (int j = 0; j < pagesPerSheet && (i + j) < totalPages; j++) {
|
for (int j = 0; j < pagesPerSheet && (i + j) < totalPages; j++) {
|
||||||
int pageIndex = i + j;
|
int pageIndex = i + j;
|
||||||
PDPage sourcePage = sourceDocument.getPage(pageIndex);
|
PDPage sourcePage = sourceDocument.getPage(pageIndex);
|
||||||
PDRectangle rect = sourcePage.getMediaBox();
|
PDRectangle rect = sourcePage.getMediaBox();
|
||||||
float scaleWidth = cellWidth / rect.getWidth();
|
float scaleWidth = innerWidth / rect.getWidth();
|
||||||
float scaleHeight = cellHeight / rect.getHeight();
|
float scaleHeight = innerHeight / rect.getHeight();
|
||||||
float scale = Math.min(scaleWidth, scaleHeight);
|
float scale = Math.min(scaleWidth, scaleHeight);
|
||||||
|
|
||||||
int adjustedPageIndex = j % pagesPerSheet;
|
int adjustedPageIndex = j % pagesPerSheet;
|
||||||
int rowIndex = adjustedPageIndex / cols;
|
int rowIndex;
|
||||||
int colIndex = adjustedPageIndex % cols;
|
int colIndex;
|
||||||
|
|
||||||
|
if ("BY_ROWS".equals(arrangement)) {
|
||||||
|
rowIndex = adjustedPageIndex / cols;
|
||||||
|
if ("LTR".equals(readingDirection)) {
|
||||||
|
colIndex = adjustedPageIndex % cols;
|
||||||
|
} else {
|
||||||
|
colIndex = cols - 1 - (adjustedPageIndex % cols);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
rowIndex = adjustedPageIndex % rows;
|
||||||
|
if ("LTR".equals(readingDirection)) {
|
||||||
|
colIndex = adjustedPageIndex / rows;
|
||||||
|
} else {
|
||||||
|
colIndex = cols - 1 - (adjustedPageIndex / rows);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
float x =
|
float x =
|
||||||
colIndex * cellWidth
|
leftMargin
|
||||||
+ (cellWidth - rect.getWidth() * scale) / 2;
|
+ colIndex * cellWidth
|
||||||
|
+ innerMargin
|
||||||
|
+ (innerWidth - rect.getWidth() * scale) / 2;
|
||||||
float y =
|
float y =
|
||||||
newPage.getMediaBox().getHeight()
|
newPage.getMediaBox().getHeight()
|
||||||
|
- topMargin
|
||||||
- ((rowIndex + 1) * cellHeight
|
- ((rowIndex + 1) * cellHeight
|
||||||
- (cellHeight - rect.getHeight() * scale) / 2);
|
- innerMargin
|
||||||
|
- (innerHeight - rect.getHeight() * scale) / 2);
|
||||||
|
|
||||||
contentStream.saveGraphicsState();
|
contentStream.saveGraphicsState();
|
||||||
contentStream.transform(Matrix.getTranslateInstance(x, y));
|
contentStream.transform(Matrix.getTranslateInstance(x, y));
|
||||||
@@ -132,11 +310,8 @@ public class MultiPageLayoutController {
|
|||||||
|
|
||||||
if (addBorder) {
|
if (addBorder) {
|
||||||
// Draw border around each page
|
// Draw border around each page
|
||||||
float borderX = colIndex * cellWidth;
|
contentStream.addRect(
|
||||||
float borderY =
|
x, y, rect.getWidth() * scale, rect.getHeight() * scale);
|
||||||
newPage.getMediaBox().getHeight()
|
|
||||||
- (rowIndex + 1) * cellHeight;
|
|
||||||
contentStream.addRect(borderX, borderY, cellWidth, cellHeight);
|
|
||||||
contentStream.stroke();
|
contentStream.stroke();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -145,7 +320,7 @@ public class MultiPageLayoutController {
|
|||||||
|
|
||||||
// If any source page is rotated, skip form copying/transformation entirely
|
// If any source page is rotated, skip form copying/transformation entirely
|
||||||
boolean hasRotation = GeneralFormCopyUtils.hasAnyRotatedPage(sourceDocument);
|
boolean hasRotation = GeneralFormCopyUtils.hasAnyRotatedPage(sourceDocument);
|
||||||
if (hasRotation) {
|
if (hasRotation || "LANDSCAPE".equals(orientation)) {
|
||||||
log.info("Source document has rotated pages; skipping form field copying.");
|
log.info("Source document has rotated pages; skipping form field copying.");
|
||||||
} else {
|
} else {
|
||||||
try {
|
try {
|
||||||
|
|||||||
+99
-2
@@ -11,13 +11,110 @@ import stirling.software.common.model.api.PDFFile;
|
|||||||
@EqualsAndHashCode(callSuper = true)
|
@EqualsAndHashCode(callSuper = true)
|
||||||
public class MergeMultiplePagesRequest extends PDFFile {
|
public class MergeMultiplePagesRequest extends PDFFile {
|
||||||
|
|
||||||
|
@Schema(
|
||||||
|
description =
|
||||||
|
"Input mode: DEFAULT uses pagesPerSheet; CUSTOM uses explicit cols x rows.",
|
||||||
|
requiredMode = Schema.RequiredMode.REQUIRED,
|
||||||
|
type = "string",
|
||||||
|
defaultValue = "DEFAULT",
|
||||||
|
allowableValues = {"DEFAULT", "CUSTOM"})
|
||||||
|
private String mode;
|
||||||
|
|
||||||
@Schema(
|
@Schema(
|
||||||
description = "The number of pages to fit onto a single sheet in the output PDF.",
|
description = "The number of pages to fit onto a single sheet in the output PDF.",
|
||||||
type = "integer",
|
type = "integer",
|
||||||
requiredMode = Schema.RequiredMode.REQUIRED,
|
allowableValues = {"2", "4", "9", "16"})
|
||||||
allowableValues = {"2", "3", "4", "9", "16"})
|
|
||||||
private int pagesPerSheet = 2;
|
private int pagesPerSheet = 2;
|
||||||
|
|
||||||
|
@Schema(
|
||||||
|
description =
|
||||||
|
"The arrangement of pages on the sheet: BY_ROWS fills pages row by row, while BY_COLUMNS fills pages column by column.",
|
||||||
|
type = "string",
|
||||||
|
defaultValue = "BY_ROWS",
|
||||||
|
allowableValues = {"BY_ROWS", "BY_COLUMNS"})
|
||||||
|
private String arrangement;
|
||||||
|
|
||||||
|
@Schema(
|
||||||
|
description =
|
||||||
|
"The direction in which pages are arranged on the sheet: LTR (left-to-right) or RTL (right-to-left).",
|
||||||
|
type = "string",
|
||||||
|
defaultValue = "LTR",
|
||||||
|
allowableValues = {"LTR", "RTL"})
|
||||||
|
private String readingDirection;
|
||||||
|
|
||||||
|
@Schema(
|
||||||
|
description = "Number of rows",
|
||||||
|
type = "number",
|
||||||
|
defaultValue = "1",
|
||||||
|
maximum = "300",
|
||||||
|
minimum = "1",
|
||||||
|
example = "3")
|
||||||
|
private int rows;
|
||||||
|
|
||||||
|
@Schema(
|
||||||
|
description = "Number of columns",
|
||||||
|
type = "number",
|
||||||
|
defaultValue = "2",
|
||||||
|
maximum = "300",
|
||||||
|
minimum = "1",
|
||||||
|
example = "2")
|
||||||
|
private int cols;
|
||||||
|
|
||||||
|
@Schema(
|
||||||
|
description = "The orientation of the output PDF pages",
|
||||||
|
type = "string",
|
||||||
|
defaultValue = "PORTRAIT",
|
||||||
|
allowableValues = {"PORTRAIT", "LANDSCAPE"})
|
||||||
|
private String orientation;
|
||||||
|
|
||||||
|
@Schema(
|
||||||
|
description = "Inner margin (in points) to apply around each page when merging",
|
||||||
|
type = "number",
|
||||||
|
defaultValue = "0",
|
||||||
|
minimum = "0",
|
||||||
|
example = "200")
|
||||||
|
private int innerMargin;
|
||||||
|
|
||||||
|
@Schema(
|
||||||
|
description = "Top margin (in points) to apply to the output pages when merging",
|
||||||
|
type = "number",
|
||||||
|
defaultValue = "0",
|
||||||
|
minimum = "0",
|
||||||
|
example = "200")
|
||||||
|
private int topMargin;
|
||||||
|
|
||||||
|
@Schema(
|
||||||
|
description = "Bottom margin (in points) to apply to the output pages when merging",
|
||||||
|
type = "number",
|
||||||
|
defaultValue = "0",
|
||||||
|
minimum = "0",
|
||||||
|
example = "200")
|
||||||
|
private int bottomMargin;
|
||||||
|
|
||||||
|
@Schema(
|
||||||
|
description = "Left margin (in points) to apply to the output pages when merging",
|
||||||
|
type = "number",
|
||||||
|
defaultValue = "0",
|
||||||
|
minimum = "0",
|
||||||
|
example = "200")
|
||||||
|
private int leftMargin;
|
||||||
|
|
||||||
|
@Schema(
|
||||||
|
description = "Right margin (in points) to apply to the output pages when merging",
|
||||||
|
type = "number",
|
||||||
|
defaultValue = "0",
|
||||||
|
minimum = "0",
|
||||||
|
example = "200")
|
||||||
|
private int rightMargin;
|
||||||
|
|
||||||
|
@Schema(
|
||||||
|
description = "Border width (in points) to apply around each page when merging",
|
||||||
|
type = "number",
|
||||||
|
defaultValue = "1",
|
||||||
|
minimum = "0",
|
||||||
|
example = "2")
|
||||||
|
private int borderWidth;
|
||||||
|
|
||||||
@Schema(description = "Boolean for if you wish to add border around the pages")
|
@Schema(description = "Boolean for if you wish to add border around the pages")
|
||||||
private Boolean addBorder;
|
private Boolean addBorder;
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-1
@@ -106,7 +106,9 @@ class MultiPageLayoutControllerTest {
|
|||||||
.thenReturn(target);
|
.thenReturn(target);
|
||||||
|
|
||||||
MergeMultiplePagesRequest req = new MergeMultiplePagesRequest();
|
MergeMultiplePagesRequest req = new MergeMultiplePagesRequest();
|
||||||
req.setPagesPerSheet(3);
|
req.setMode("CUSTOM");
|
||||||
|
req.setCols(3);
|
||||||
|
req.setRows(1);
|
||||||
req.setAddBorder(Boolean.TRUE);
|
req.setAddBorder(Boolean.TRUE);
|
||||||
req.setFileInput(fileNoExt);
|
req.setFileInput(fileNoExt);
|
||||||
|
|
||||||
|
|||||||
@@ -4511,21 +4511,132 @@ title = "Extract Pages"
|
|||||||
|
|
||||||
[pageLayout]
|
[pageLayout]
|
||||||
addBorder = "Add Borders"
|
addBorder = "Add Borders"
|
||||||
|
borderWidth = "Border Thickness"
|
||||||
|
bottom = "Bottom Margin"
|
||||||
|
cols = "Columns"
|
||||||
header = "Multi Page Layout"
|
header = "Multi Page Layout"
|
||||||
|
innerMargin = "Inner Margin"
|
||||||
|
left = "Left Margin"
|
||||||
pagesPerSheet = "Pages per sheet:"
|
pagesPerSheet = "Pages per sheet:"
|
||||||
|
right = "Right Margin"
|
||||||
|
rows = "Rows"
|
||||||
submit = "Submit"
|
submit = "Submit"
|
||||||
tags = "merge,composite,single-view,organize"
|
tags = "merge,composite,single-view,organize"
|
||||||
title = "Multi Page Layout"
|
title = "Multi Page Layout"
|
||||||
|
top = "Top Margin"
|
||||||
|
|
||||||
|
[pageLayout.mode]
|
||||||
|
default = "Default"
|
||||||
|
custom = "Custom"
|
||||||
|
label = "Mode:"
|
||||||
|
|
||||||
|
[pageLayout.arrangement]
|
||||||
|
byColumns = "By Columns"
|
||||||
|
byRows = "By Rows"
|
||||||
|
label = "Page arrangement:"
|
||||||
|
|
||||||
[pageLayout.desc]
|
[pageLayout.desc]
|
||||||
16 = "Place 16 pages on a single sheet (4 × 4 grid)."
|
16 = "Place 16 pages on a single sheet (4 × 4 grid)."
|
||||||
2 = "Place 2 pages side-by-side on a single sheet."
|
2 = "Place 2 pages side-by-side on a single sheet."
|
||||||
3 = "Place 3 pages on a single sheet in a single row."
|
|
||||||
4 = "Place 4 pages on a single sheet (2 × 2 grid)."
|
4 = "Place 4 pages on a single sheet (2 × 2 grid)."
|
||||||
9 = "Place 9 pages on a single sheet (3 × 3 grid)."
|
9 = "Place 9 pages on a single sheet (3 × 3 grid)."
|
||||||
|
|
||||||
[pageLayout.error]
|
[pageLayout.error]
|
||||||
failed = "An error occurred while creating the multi-page layout."
|
failed = "An error occurred while creating the multi-page layout."
|
||||||
|
outerVerticalMarginsTooLarge = "Top/Bottom margins are too large for this page size."
|
||||||
|
outerHorizontalMarginsTooLarge = "Left/Right margins are too large for this page size."
|
||||||
|
innerMarginTooLarge = "Inner margin is too large for the selected layout."
|
||||||
|
|
||||||
|
[pageLayout.orientation]
|
||||||
|
landscape = "Landscape"
|
||||||
|
portrait = "Portrait"
|
||||||
|
label = "Orientation:"
|
||||||
|
|
||||||
|
[pageLayout.readingDirection]
|
||||||
|
ltr = "Left to Right"
|
||||||
|
rtl = "Right to Left"
|
||||||
|
label = "Reading Direction:"
|
||||||
|
|
||||||
|
[pageLayout.tooltip.header]
|
||||||
|
title = "Page Layout Guide"
|
||||||
|
|
||||||
|
[pageLayout.tooltip.overview]
|
||||||
|
title = "What is Page Layout?"
|
||||||
|
text = "Fit multiple pages onto a single sheet for handouts or to save paper."
|
||||||
|
|
||||||
|
[pageLayout.tooltip.mode]
|
||||||
|
title = "Mode"
|
||||||
|
text = "Choose how the grid is configured:"
|
||||||
|
bullet1 = "Default: Pick a preset and the grid is calculated automatically."
|
||||||
|
bullet2 = "Custom: Set rows and columns manually."
|
||||||
|
|
||||||
|
[pageLayout.tooltip.pagesPerSheet]
|
||||||
|
title = "Pages per Sheet (Default Mode)"
|
||||||
|
text = "Choose how many pages per sheet (e.g. 4 → 2×2, 9 → 3×3)."
|
||||||
|
|
||||||
|
[pageLayout.tooltip.rowsCols]
|
||||||
|
title = "Rows & Columns (Custom Mode)"
|
||||||
|
text = "Set exact grid dimensions. Total pages per sheet = rows × columns."
|
||||||
|
|
||||||
|
[pageLayout.tooltip.orientation]
|
||||||
|
title = "Orientation"
|
||||||
|
text = "Sets the output sheet orientation:"
|
||||||
|
bullet1 = "Portrait: Taller than wide."
|
||||||
|
bullet2 = "Landscape: Wider than tall."
|
||||||
|
|
||||||
|
[pageLayout.tooltip.arrangement]
|
||||||
|
title = "Page Arrangement"
|
||||||
|
text = "Controls the order pages fill the grid:"
|
||||||
|
bullet1 = "By Rows: Fill row by row (left-to-right or right-to-left)."
|
||||||
|
bullet2 = "By Columns: Fill top-to-bottom, column by column."
|
||||||
|
|
||||||
|
[pageLayout.tooltip.readingDirection]
|
||||||
|
title = "Reading Direction"
|
||||||
|
text = "Controls the horizontal order of pages:"
|
||||||
|
bullet1 = "LTR: Left to right."
|
||||||
|
bullet2 = "RTL: Right to left."
|
||||||
|
|
||||||
|
[pageLayout.tooltip.addBorder]
|
||||||
|
title = "Add Borders"
|
||||||
|
text = "Draws border lines around each page cell for cutting guides or visual separation."
|
||||||
|
|
||||||
|
[pageLayout.marginsBorders.tooltip.header]
|
||||||
|
title = "Margins and Borders"
|
||||||
|
|
||||||
|
[pageLayout.marginsBorders.tooltip.margins]
|
||||||
|
title = "Margins"
|
||||||
|
text = "Use top, bottom, left, and right margins to control spacing around the full sheet output."
|
||||||
|
|
||||||
|
[pageLayout.marginsBorders.tooltip.innerMargin]
|
||||||
|
title = "Inner Margin"
|
||||||
|
text = "Inner margin adds spacing between cells in the page grid to improve separation and readability."
|
||||||
|
|
||||||
|
[pageLayout.marginsBorders.tooltip.borders]
|
||||||
|
title = "Add Borders"
|
||||||
|
text = "Enable borders to draw lines around each placed page. This can help visual separation or trimming."
|
||||||
|
|
||||||
|
[pageLayout.marginsBorders.tooltip.borderWidth]
|
||||||
|
title = "Border Thickness"
|
||||||
|
text = "Border thickness is only applied when borders are enabled. Higher values produce thicker lines."
|
||||||
|
|
||||||
|
[pageLayout.advanced.tooltip.header]
|
||||||
|
title = "Advanced Layout Options"
|
||||||
|
|
||||||
|
[pageLayout.advanced.tooltip.orientation]
|
||||||
|
title = "Orientation"
|
||||||
|
text = "Choose the final sheet direction. Portrait works better for tall content, while landscape fits wider layouts."
|
||||||
|
|
||||||
|
[pageLayout.advanced.tooltip.arrangement]
|
||||||
|
title = "Page Arrangement"
|
||||||
|
text = "Controls whether pages fill the grid row-by-row or column-by-column."
|
||||||
|
bullet1 = "By Rows: Fill each row first."
|
||||||
|
bullet2 = "By Columns: Fill each column first."
|
||||||
|
|
||||||
|
[pageLayout.advanced.tooltip.readingDirection]
|
||||||
|
title = "Reading Direction"
|
||||||
|
text = "Sets horizontal ordering in the grid, useful for left-to-right and right-to-left document conventions."
|
||||||
|
bullet1 = "LTR: Left to right order."
|
||||||
|
bullet2 = "RTL: Right to left order."
|
||||||
|
|
||||||
[pageRemover]
|
[pageRemover]
|
||||||
header = "PDF Page remover"
|
header = "PDF Page remover"
|
||||||
|
|||||||
@@ -0,0 +1,72 @@
|
|||||||
|
import { PageLayoutParameters } from '@app/hooks/tools/pageLayout/usePageLayoutParameters';
|
||||||
|
import { computeBoxes } from '@app/components/tools/pageLayout/utils/computeBoxes';
|
||||||
|
|
||||||
|
export default function LayoutPreview({
|
||||||
|
parameters
|
||||||
|
}: {
|
||||||
|
parameters: PageLayoutParameters
|
||||||
|
}) {
|
||||||
|
|
||||||
|
const sheet = {
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
width: parameters.orientation === "LANDSCAPE" ? 297 : 210,
|
||||||
|
height: parameters.orientation === "LANDSCAPE" ? 210 : 297
|
||||||
|
}
|
||||||
|
|
||||||
|
const boxes = computeBoxes(sheet, parameters);
|
||||||
|
const aspectRatio = sheet.width / sheet.height;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
viewBox={`${sheet.x} ${sheet.y} ${sheet.width} ${sheet.height}`}
|
||||||
|
preserveAspectRatio="xMidYMid meet"
|
||||||
|
style={{
|
||||||
|
width: parameters.orientation === "PORTRAIT" ? "50%" : `${50 * aspectRatio}%`,
|
||||||
|
aspectRatio: `${aspectRatio}`,
|
||||||
|
display: 'block',
|
||||||
|
margin: '0 auto',
|
||||||
|
padding: '10px',
|
||||||
|
}}>
|
||||||
|
<rect
|
||||||
|
x={sheet.x}
|
||||||
|
y={sheet.y}
|
||||||
|
width={sheet.width}
|
||||||
|
height={sheet.height}
|
||||||
|
rx={4}
|
||||||
|
ry={4}
|
||||||
|
fill="white"
|
||||||
|
stroke="black"
|
||||||
|
strokeWidth={2}
|
||||||
|
/>
|
||||||
|
{boxes.map(box => (
|
||||||
|
<g key={box.label}>
|
||||||
|
<rect
|
||||||
|
x={box.x}
|
||||||
|
y={box.y}
|
||||||
|
width={box.width}
|
||||||
|
height={box.height}
|
||||||
|
rx={3}
|
||||||
|
ry={3}
|
||||||
|
fill="#bfdbfe"
|
||||||
|
stroke={parameters.addBorder ? "#3b82f6" : "none"}
|
||||||
|
strokeWidth={2}
|
||||||
|
/>
|
||||||
|
<text
|
||||||
|
x={box.x + box.width / 2}
|
||||||
|
y={box.y + box.height / 2}
|
||||||
|
textAnchor="middle"
|
||||||
|
dominantBaseline="middle"
|
||||||
|
fontSize={Math.min(box.width, box.height) * 0.6}
|
||||||
|
fill="#1d4ed8"
|
||||||
|
fontFamily="Arial, sans-serif"
|
||||||
|
fontWeight="bold"
|
||||||
|
>
|
||||||
|
{box.label}
|
||||||
|
</text>
|
||||||
|
</g>))
|
||||||
|
}
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
import { Divider, Select, Stack } from '@mantine/core';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { PageLayoutParameters } from '@app/hooks/tools/pageLayout/usePageLayoutParameters';
|
||||||
|
import { Z_INDEX_AUTOMATE_DROPDOWN } from "@app/styles/zIndex";
|
||||||
|
|
||||||
|
export default function PageLayoutAdvancedSettings({
|
||||||
|
parameters,
|
||||||
|
onParameterChange,
|
||||||
|
disabled,
|
||||||
|
}: {
|
||||||
|
parameters: PageLayoutParameters;
|
||||||
|
onParameterChange: <K extends keyof PageLayoutParameters>(
|
||||||
|
key: K,
|
||||||
|
value: PageLayoutParameters[K]
|
||||||
|
) => void;
|
||||||
|
disabled?: boolean;
|
||||||
|
}) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
return (
|
||||||
|
<Stack gap="sm">
|
||||||
|
|
||||||
|
<Select
|
||||||
|
label={t('pageLayout.orientation.label', 'Orientation:')}
|
||||||
|
data={[
|
||||||
|
{ value: 'PORTRAIT', label: t('pageLayout.orientation.portrait', 'Portrait') },
|
||||||
|
{ value: 'LANDSCAPE', label: t('pageLayout.orientation.landscape', 'Landscape') },
|
||||||
|
]}
|
||||||
|
value={String(parameters.orientation)}
|
||||||
|
onChange={(v) => {
|
||||||
|
if (v === "PORTRAIT" || v === "LANDSCAPE") {
|
||||||
|
onParameterChange('orientation', v)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
disabled={disabled}
|
||||||
|
comboboxProps={{ withinPortal: true, zIndex: Z_INDEX_AUTOMATE_DROPDOWN }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Divider />
|
||||||
|
|
||||||
|
<Select
|
||||||
|
label={t('pageLayout.arrangement.label', 'Page arrangement:')}
|
||||||
|
data={[
|
||||||
|
{ value: 'BY_ROWS', label: t('pageLayout.arrangement.byRows', 'By Rows') },
|
||||||
|
{ value: 'BY_COLUMNS', label: t('pageLayout.arrangement.byColumns', 'By Columns') },
|
||||||
|
]}
|
||||||
|
value={String(parameters.arrangement)}
|
||||||
|
onChange={(v) => {
|
||||||
|
if (v === "BY_COLUMNS" || v === "BY_ROWS") {
|
||||||
|
onParameterChange('arrangement', v)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
disabled={disabled}
|
||||||
|
comboboxProps={{ withinPortal: true, zIndex: Z_INDEX_AUTOMATE_DROPDOWN }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Select
|
||||||
|
label={t('pageLayout.readingDirection.label', 'Reading Direction:')}
|
||||||
|
data={[
|
||||||
|
{ value: 'LTR', label: t('pageLayout.readingDirection.ltr', 'Left to Right') },
|
||||||
|
{ value: 'RTL', label: t('pageLayout.readingDirection.rtl', 'Right to Left') },
|
||||||
|
]}
|
||||||
|
value={String(parameters.readingDirection)}
|
||||||
|
onChange={(v) => {
|
||||||
|
if (v === "LTR" || v === "RTL") {
|
||||||
|
onParameterChange('readingDirection', v)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
disabled={disabled}
|
||||||
|
comboboxProps={{ withinPortal: true, zIndex: Z_INDEX_AUTOMATE_DROPDOWN }}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,134 @@
|
|||||||
|
import { Divider, Stack, NumberInput, Switch } from '@mantine/core';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { PageLayoutParameters } from '@app/hooks/tools/pageLayout/usePageLayoutParameters';
|
||||||
|
|
||||||
|
export default function PageLayoutMarginsBordersSettings({
|
||||||
|
parameters,
|
||||||
|
onParameterChange,
|
||||||
|
disabled,
|
||||||
|
}: {
|
||||||
|
parameters: PageLayoutParameters;
|
||||||
|
onParameterChange: <K extends keyof PageLayoutParameters>(
|
||||||
|
key: K,
|
||||||
|
value: PageLayoutParameters[K]
|
||||||
|
) => void;
|
||||||
|
disabled?: boolean;
|
||||||
|
}) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const cols = parameters.mode === "DEFAULT" ? Math.ceil(Math.sqrt(parameters.pagesPerSheet)) : parameters.cols;
|
||||||
|
const rows = parameters.mode === "DEFAULT" ? Math.ceil(parameters.pagesPerSheet / cols) : parameters.rows;
|
||||||
|
|
||||||
|
const left = parameters.leftMargin ?? 0;
|
||||||
|
const right = parameters.rightMargin ?? 0;
|
||||||
|
const top = parameters.topMargin ?? 0;
|
||||||
|
const bottom = parameters.bottomMargin ?? 0;
|
||||||
|
const inner = parameters.innerMargin ?? 0;
|
||||||
|
|
||||||
|
const pageWidth = parameters.orientation === "PORTRAIT"
|
||||||
|
? 595.28 // A4 width in points
|
||||||
|
: 841.89 ; // A4 height in points
|
||||||
|
|
||||||
|
const pageHeight = parameters.orientation === "PORTRAIT"
|
||||||
|
? 841.89 // A4 height in points
|
||||||
|
: 595.28; // A4 width in points
|
||||||
|
|
||||||
|
const cellWidth = (pageWidth - left - right) / cols;
|
||||||
|
const cellHeight = (pageHeight - top - bottom) / rows;
|
||||||
|
const innerWidth = cellWidth - 2 * inner;
|
||||||
|
const innerHeight = cellHeight - 2 * inner;
|
||||||
|
|
||||||
|
const invalidOuterWidth = left + right >= pageWidth;
|
||||||
|
const invalidOuterHeight = top + bottom >= pageHeight;
|
||||||
|
const invalidInnerSize = (innerWidth <= 0 || innerHeight <= 0) && inner > 0;
|
||||||
|
|
||||||
|
const outerHeightError = invalidOuterHeight
|
||||||
|
? t('pageLayout.error.outerVerticalMarginsTooLarge', 'Top/Bottom margins are too large for this page size.')
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
const outerWidthError = invalidOuterWidth
|
||||||
|
? t('pageLayout.error.outerHorizontalMarginsTooLarge', 'Left/Right margins are too large for this page size.')
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
const innerError = invalidInnerSize
|
||||||
|
? t('pageLayout.error.innerMarginTooLarge', 'Inner margin is too large for the selected layout.')
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack gap="sm">
|
||||||
|
<NumberInput
|
||||||
|
label={t('pageLayout.top', 'Top Margin')}
|
||||||
|
placeholder="Enter top margin"
|
||||||
|
value={parameters.topMargin}
|
||||||
|
onChange={(v) => onParameterChange('topMargin', Number(v))}
|
||||||
|
min={0}
|
||||||
|
disabled={disabled}
|
||||||
|
style={{ flex: 1 }}
|
||||||
|
error={outerHeightError}
|
||||||
|
/>
|
||||||
|
<NumberInput
|
||||||
|
label={t('pageLayout.bottom', 'Bottom Margin')}
|
||||||
|
placeholder="Enter bottom margin"
|
||||||
|
value={parameters.bottomMargin}
|
||||||
|
onChange={(v) => onParameterChange('bottomMargin', Number(v))}
|
||||||
|
min={0}
|
||||||
|
disabled={disabled}
|
||||||
|
style={{ flex: 1 }}
|
||||||
|
error={outerHeightError}
|
||||||
|
/>
|
||||||
|
<NumberInput
|
||||||
|
label={t('pageLayout.left', 'Left Margin')}
|
||||||
|
placeholder="Enter left margin"
|
||||||
|
value={parameters.leftMargin}
|
||||||
|
onChange={(v) => onParameterChange('leftMargin', Number(v))}
|
||||||
|
min={0}
|
||||||
|
disabled={disabled}
|
||||||
|
error={outerWidthError}
|
||||||
|
/>
|
||||||
|
<NumberInput
|
||||||
|
label={t('pageLayout.right', 'Right Margin')}
|
||||||
|
placeholder="Enter right margin"
|
||||||
|
value={parameters.rightMargin}
|
||||||
|
onChange={(v) => onParameterChange('rightMargin', Number(v))}
|
||||||
|
min={0}
|
||||||
|
disabled={disabled}
|
||||||
|
style={{ flex: 1 }}
|
||||||
|
error={outerWidthError}
|
||||||
|
/>
|
||||||
|
<NumberInput
|
||||||
|
label={t('pageLayout.innerMargin', 'Inner Margin')}
|
||||||
|
placeholder="Enter inner margin"
|
||||||
|
value={parameters.innerMargin}
|
||||||
|
onChange={(v) => onParameterChange('innerMargin', Number(v))}
|
||||||
|
min={0}
|
||||||
|
disabled={disabled}
|
||||||
|
style={{ flex: 1 }}
|
||||||
|
error={innerError}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Divider />
|
||||||
|
|
||||||
|
<Switch
|
||||||
|
checked={parameters.addBorder}
|
||||||
|
onChange={(e) => onParameterChange('addBorder', e.currentTarget.checked)}
|
||||||
|
label={t('pageLayout.addBorder', 'Add Borders')}
|
||||||
|
disabled={disabled}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{parameters.addBorder && (
|
||||||
|
<NumberInput
|
||||||
|
label={t('pageLayout.borderWidth', 'Border Thickness')}
|
||||||
|
placeholder="Enter border thickness"
|
||||||
|
value={parameters.borderWidth}
|
||||||
|
onChange={(v) => onParameterChange('borderWidth', Number(v))}
|
||||||
|
min={1}
|
||||||
|
disabled={disabled}
|
||||||
|
style={{ flex: 1 }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import { PageLayoutParameters } from '@app/hooks/tools/pageLayout/usePageLayoutParameters';
|
||||||
|
import LayoutPreview from '@app/components/tools/pageLayout/LayoutPreview';
|
||||||
|
import { Stack } from '@mantine/core';
|
||||||
|
|
||||||
|
export default function PageLayoutPreview({
|
||||||
|
parameters,
|
||||||
|
}: {
|
||||||
|
parameters: PageLayoutParameters;
|
||||||
|
}) {
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack gap="sm">
|
||||||
|
<LayoutPreview parameters={parameters} />
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
import { Divider, Select, Stack, Switch } from '@mantine/core';
|
import { Select, Stack, NumberInput } from '@mantine/core';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { PageLayoutParameters } from '@app/hooks/tools/pageLayout/usePageLayoutParameters';
|
import { PageLayoutParameters } from '@app/hooks/tools/pageLayout/usePageLayoutParameters';
|
||||||
import { getPagesPerSheetOptions } from '@app/components/tools/pageLayout/constants';
|
import { getPagesPerSheetOptions } from '@app/components/tools/pageLayout/constants';
|
||||||
import { Z_INDEX_AUTOMATE_DROPDOWN } from "@app/styles/zIndex";
|
import { Z_INDEX_AUTOMATE_DROPDOWN } from "@app/styles/zIndex";
|
||||||
|
import ButtonSelector from '@app/components/shared/ButtonSelector';
|
||||||
|
|
||||||
export default function PageLayoutSettings({
|
export default function PageLayoutSettings({
|
||||||
parameters,
|
parameters,
|
||||||
@@ -18,44 +19,75 @@ export default function PageLayoutSettings({
|
|||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const options = getPagesPerSheetOptions(t);
|
const pagesPerSheetOptions = getPagesPerSheetOptions(t);
|
||||||
const selected = options.find((o) => o.value === parameters.pagesPerSheet) || options[0];
|
const selectedPagesPerSheetOption = pagesPerSheetOptions.find((o) => o.value === parameters.pagesPerSheet) || pagesPerSheetOptions[0];
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack gap="sm">
|
<Stack gap="sm">
|
||||||
<Select
|
<ButtonSelector
|
||||||
label={t('pageLayout.pagesPerSheet', 'Pages per sheet:')}
|
label={t('pageLayout.mode.label', 'Mode:')}
|
||||||
data={options.map(o => ({ value: String(o.value), label: o.label }))}
|
options={[
|
||||||
value={String(parameters.pagesPerSheet)}
|
{value: "DEFAULT", label: t('pageLayout.mode.default', 'Default')},
|
||||||
onChange={(v) => onParameterChange('pagesPerSheet', Number(v))}
|
{value: "CUSTOM", label: t('pageLayout.mode.custom', 'Custom')}
|
||||||
disabled={disabled}
|
]}
|
||||||
comboboxProps={{ withinPortal: true, zIndex: Z_INDEX_AUTOMATE_DROPDOWN }}
|
value={String(parameters.mode)}
|
||||||
/>
|
onChange={(v) => {
|
||||||
|
if (v === "CUSTOM" || v === "DEFAULT") {
|
||||||
{selected && (
|
onParameterChange('mode', v)
|
||||||
<div
|
}
|
||||||
style={{
|
}}
|
||||||
backgroundColor: 'var(--information-text-bg)',
|
|
||||||
color: 'var(--information-text-color)',
|
|
||||||
padding: '8px 12px',
|
|
||||||
borderRadius: '8px',
|
|
||||||
marginTop: '4px',
|
|
||||||
fontSize: '0.75rem',
|
|
||||||
textAlign: 'center',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{selected.description}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Divider />
|
|
||||||
|
|
||||||
<Switch
|
|
||||||
checked={parameters.addBorder}
|
|
||||||
onChange={(e) => onParameterChange('addBorder', e.currentTarget.checked)}
|
|
||||||
label={t('pageLayout.addBorder', 'Add Borders')}
|
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{parameters.mode === "DEFAULT" && <>
|
||||||
|
<Select
|
||||||
|
label={t('pageLayout.pagesPerSheet', 'Pages per sheet:')}
|
||||||
|
data={pagesPerSheetOptions.map(o => ({ value: String(o.value), label: o.label }))}
|
||||||
|
value={String(parameters.pagesPerSheet)}
|
||||||
|
onChange={(v) => onParameterChange('pagesPerSheet', Number(v))}
|
||||||
|
allowDeselect={false}
|
||||||
|
disabled={disabled}
|
||||||
|
comboboxProps={{ withinPortal: true, zIndex: Z_INDEX_AUTOMATE_DROPDOWN }}
|
||||||
|
/>
|
||||||
|
{selectedPagesPerSheetOption && (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
backgroundColor: 'var(--information-text-bg)',
|
||||||
|
color: 'var(--information-text-color)',
|
||||||
|
padding: '8px 12px',
|
||||||
|
borderRadius: '8px',
|
||||||
|
marginTop: '4px',
|
||||||
|
fontSize: '0.75rem',
|
||||||
|
textAlign: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{selectedPagesPerSheetOption.description}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>}
|
||||||
|
|
||||||
|
{parameters.mode === "CUSTOM" && <>
|
||||||
|
<NumberInput
|
||||||
|
label={t('pageLayout.rows', 'Rows')}
|
||||||
|
placeholder="Enter rows"
|
||||||
|
value={parameters.rows}
|
||||||
|
onChange={(v) => onParameterChange('rows', Number(v))}
|
||||||
|
min={1}
|
||||||
|
disabled={disabled}
|
||||||
|
style={{ flex: 1 }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<NumberInput
|
||||||
|
label={t('pageLayout.cols', 'Columns')}
|
||||||
|
placeholder="Enter columns"
|
||||||
|
value={parameters.cols}
|
||||||
|
onChange={(v) => onParameterChange('cols', Number(v))}
|
||||||
|
min={1}
|
||||||
|
disabled={disabled}
|
||||||
|
style={{ flex: 1 }}
|
||||||
|
/>
|
||||||
|
</>}
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,11 +12,6 @@ export const getPagesPerSheetOptions = (t: TFunction): PagesPerSheetOption[] =>
|
|||||||
label: '2',
|
label: '2',
|
||||||
description: t('pageLayout.desc.2', 'Place 2 pages side-by-side on a single sheet.')
|
description: t('pageLayout.desc.2', 'Place 2 pages side-by-side on a single sheet.')
|
||||||
},
|
},
|
||||||
{
|
|
||||||
value: 3,
|
|
||||||
label: '3',
|
|
||||||
description: t('pageLayout.desc.3', 'Place 3 pages on a single sheet in a single row.')
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
value: 4,
|
value: 4,
|
||||||
label: '4',
|
label: '4',
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
import { PageLayoutParameters } from "@app/hooks/tools/pageLayout/usePageLayoutParameters";
|
||||||
|
|
||||||
|
export type Box = {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
label: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Sheet = {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function computeBoxes(sheet: Sheet, parameters: PageLayoutParameters): Box[] {
|
||||||
|
|
||||||
|
const { mode, arrangement, readingDirection } = parameters;
|
||||||
|
|
||||||
|
const cols = mode === "DEFAULT" ? Math.ceil(Math.sqrt(parameters.pagesPerSheet)) : parameters.cols;
|
||||||
|
const rows = mode === "DEFAULT" ? Math.ceil(parameters.pagesPerSheet / cols) : parameters.rows;
|
||||||
|
const pagesPerSheet = mode === "DEFAULT" ? parameters.pagesPerSheet : cols * rows;
|
||||||
|
|
||||||
|
const boxes: Box[] = [];
|
||||||
|
|
||||||
|
const cellWidth = sheet.width / cols;
|
||||||
|
const cellHeight = sheet.height / rows;
|
||||||
|
|
||||||
|
const margin = Math.min(cellWidth, cellHeight) * 0.1;
|
||||||
|
|
||||||
|
for (let i = 0; i < pagesPerSheet; i++) {
|
||||||
|
|
||||||
|
let rowIndex, colIndex;
|
||||||
|
|
||||||
|
if ( arrangement === "BY_ROWS" ) {
|
||||||
|
rowIndex = Math.floor(i / cols);
|
||||||
|
if ( readingDirection === "LTR" ) {
|
||||||
|
colIndex = i % cols;
|
||||||
|
} else {
|
||||||
|
colIndex = cols - 1 - (i % cols);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
rowIndex = i % rows;
|
||||||
|
if ( readingDirection === "LTR" ) {
|
||||||
|
colIndex = Math.floor(i / rows);
|
||||||
|
} else {
|
||||||
|
colIndex = cols - 1 - Math.floor(i / rows);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
boxes.push({
|
||||||
|
x: sheet.x + colIndex * cellWidth + margin,
|
||||||
|
y: sheet.y + rowIndex * cellHeight + margin,
|
||||||
|
width: cellWidth - margin * 2,
|
||||||
|
height: cellHeight - margin * 2,
|
||||||
|
label: i + 1
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return boxes;
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { TooltipContent } from '@app/types/tips';
|
||||||
|
|
||||||
|
export const usePageLayoutAdvancedTips = (): TooltipContent => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
return {
|
||||||
|
header: {
|
||||||
|
title: t('pageLayout.advanced.tooltip.header.title', 'Advanced Layout Options'),
|
||||||
|
},
|
||||||
|
tips: [
|
||||||
|
{
|
||||||
|
title: t('pageLayout.advanced.tooltip.orientation.title', 'Orientation'),
|
||||||
|
description: t(
|
||||||
|
'pageLayout.advanced.tooltip.orientation.text',
|
||||||
|
'Choose the final sheet direction. Portrait works better for tall content, while landscape fits wider layouts.'
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t('pageLayout.advanced.tooltip.arrangement.title', 'Page Arrangement'),
|
||||||
|
description: t(
|
||||||
|
'pageLayout.advanced.tooltip.arrangement.text',
|
||||||
|
'Controls whether pages fill the grid row-by-row or column-by-column.'
|
||||||
|
),
|
||||||
|
bullets: [
|
||||||
|
t('pageLayout.advanced.tooltip.arrangement.bullet1', 'By Rows: Fill each row first.'),
|
||||||
|
t('pageLayout.advanced.tooltip.arrangement.bullet2', 'By Columns: Fill each column first.'),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t('pageLayout.advanced.tooltip.readingDirection.title', 'Reading Direction'),
|
||||||
|
description: t(
|
||||||
|
'pageLayout.advanced.tooltip.readingDirection.text',
|
||||||
|
'Sets horizontal ordering in the grid, useful for left-to-right and right-to-left document conventions.'
|
||||||
|
),
|
||||||
|
bullets: [
|
||||||
|
t('pageLayout.advanced.tooltip.readingDirection.bullet1', 'LTR: Left to right order.'),
|
||||||
|
t('pageLayout.advanced.tooltip.readingDirection.bullet2', 'RTL: Right to left order.'),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { TooltipContent } from '@app/types/tips';
|
||||||
|
|
||||||
|
export const usePageLayoutMarginsBordersTips = (): TooltipContent => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
return {
|
||||||
|
header: {
|
||||||
|
title: t('pageLayout.marginsBorders.tooltip.header.title', 'Margins and Borders'),
|
||||||
|
},
|
||||||
|
tips: [
|
||||||
|
{
|
||||||
|
title: t('pageLayout.marginsBorders.tooltip.margins.title', 'Margins'),
|
||||||
|
description: t(
|
||||||
|
'pageLayout.marginsBorders.tooltip.margins.text',
|
||||||
|
'Use top, bottom, left, and right margins to control spacing around the full sheet output.'
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t('pageLayout.marginsBorders.tooltip.innerMargin.title', 'Inner Margin'),
|
||||||
|
description: t(
|
||||||
|
'pageLayout.marginsBorders.tooltip.innerMargin.text',
|
||||||
|
'Inner margin adds spacing between cells in the page grid to improve separation and readability.'
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t('pageLayout.marginsBorders.tooltip.borders.title', 'Add Borders'),
|
||||||
|
description: t(
|
||||||
|
'pageLayout.marginsBorders.tooltip.borders.text',
|
||||||
|
'Enable borders to draw lines around each placed page. This can help visual separation or trimming.'
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t('pageLayout.marginsBorders.tooltip.borderWidth.title', 'Border Thickness'),
|
||||||
|
description: t(
|
||||||
|
'pageLayout.marginsBorders.tooltip.borderWidth.text',
|
||||||
|
'Border thickness is only applied when borders are enabled. Higher values produce thicker lines.'
|
||||||
|
),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { TooltipContent } from '@app/types/tips';
|
||||||
|
|
||||||
|
export const usePageLayoutTips = (): TooltipContent => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
return {
|
||||||
|
header: {
|
||||||
|
title: t("pageLayout.tooltip.header.title", "Page Layout Guide")
|
||||||
|
},
|
||||||
|
tips: [
|
||||||
|
{
|
||||||
|
title: t("pageLayout.tooltip.overview.title", "What is Page Layout?"),
|
||||||
|
description: t("pageLayout.tooltip.overview.text", "Fit multiple pages onto a single sheet for handouts or to save paper.")
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("pageLayout.tooltip.mode.title", "Mode"),
|
||||||
|
description: t("pageLayout.tooltip.mode.text", "Choose how the grid is configured:"),
|
||||||
|
bullets: [
|
||||||
|
t("pageLayout.tooltip.mode.bullet1", "Default: Pick a preset and the grid is calculated automatically."),
|
||||||
|
t("pageLayout.tooltip.mode.bullet2", "Custom: Set rows and columns manually.")
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("pageLayout.tooltip.pagesPerSheet.title", "Pages per Sheet (Default Mode)"),
|
||||||
|
description: t("pageLayout.tooltip.pagesPerSheet.text", "Choose how many pages per sheet (e.g. 4 → 2×2, 9 → 3×3).")
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("pageLayout.tooltip.rowsCols.title", "Rows & Columns (Custom Mode)"),
|
||||||
|
description: t("pageLayout.tooltip.rowsCols.text", "Set exact grid dimensions. Total pages per sheet = rows × columns.")
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -6,8 +6,20 @@ import { PageLayoutParameters, defaultParameters } from '@app/hooks/tools/pageLa
|
|||||||
export const buildPageLayoutFormData = (parameters: PageLayoutParameters, file: File): FormData => {
|
export const buildPageLayoutFormData = (parameters: PageLayoutParameters, file: File): FormData => {
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('fileInput', file);
|
formData.append('fileInput', file);
|
||||||
|
formData.append('mode', String(parameters.mode));
|
||||||
formData.append('pagesPerSheet', String(parameters.pagesPerSheet));
|
formData.append('pagesPerSheet', String(parameters.pagesPerSheet));
|
||||||
|
formData.append('rows', String(parameters.rows));
|
||||||
|
formData.append('cols', String(parameters.cols));
|
||||||
|
formData.append('orientation', String(parameters.orientation));
|
||||||
|
formData.append('arrangement', String(parameters.arrangement));
|
||||||
|
formData.append('readingDirection', String(parameters.readingDirection));
|
||||||
|
formData.append('innerMargin', String(parameters.innerMargin ?? 0));
|
||||||
|
formData.append('topMargin', String(parameters.topMargin ?? 0));
|
||||||
|
formData.append('bottomMargin', String(parameters.bottomMargin ?? 0));
|
||||||
|
formData.append('leftMargin', String(parameters.leftMargin ?? 0));
|
||||||
|
formData.append('rightMargin', String(parameters.rightMargin ?? 0));
|
||||||
formData.append('addBorder', String(parameters.addBorder));
|
formData.append('addBorder', String(parameters.addBorder));
|
||||||
|
formData.append('borderWidth', String(parameters.borderWidth ?? 1));
|
||||||
return formData;
|
return formData;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -2,13 +2,37 @@ import { BaseParameters } from '@app/types/parameters';
|
|||||||
import { useBaseParameters, BaseParametersHook } from '@app/hooks/tools/shared/useBaseParameters';
|
import { useBaseParameters, BaseParametersHook } from '@app/hooks/tools/shared/useBaseParameters';
|
||||||
|
|
||||||
export interface PageLayoutParameters extends BaseParameters {
|
export interface PageLayoutParameters extends BaseParameters {
|
||||||
|
mode: 'DEFAULT' | 'CUSTOM';
|
||||||
pagesPerSheet: number;
|
pagesPerSheet: number;
|
||||||
|
rows: number;
|
||||||
|
cols: number;
|
||||||
|
orientation: 'PORTRAIT' | 'LANDSCAPE';
|
||||||
|
arrangement: 'BY_COLUMNS' | 'BY_ROWS';
|
||||||
|
readingDirection: 'LTR' | 'RTL';
|
||||||
|
innerMargin?: number;
|
||||||
|
topMargin?: number;
|
||||||
|
bottomMargin?: number;
|
||||||
|
leftMargin?: number;
|
||||||
|
rightMargin?: number;
|
||||||
addBorder: boolean;
|
addBorder: boolean;
|
||||||
|
borderWidth?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const defaultParameters: PageLayoutParameters = {
|
export const defaultParameters: PageLayoutParameters = {
|
||||||
|
mode: 'DEFAULT',
|
||||||
pagesPerSheet: 4,
|
pagesPerSheet: 4,
|
||||||
|
rows: 1,
|
||||||
|
cols: 1,
|
||||||
|
orientation: 'PORTRAIT',
|
||||||
|
arrangement: 'BY_COLUMNS',
|
||||||
|
readingDirection: 'LTR',
|
||||||
addBorder: false,
|
addBorder: false,
|
||||||
|
innerMargin: 0,
|
||||||
|
topMargin: 0,
|
||||||
|
bottomMargin: 0,
|
||||||
|
leftMargin: 0,
|
||||||
|
rightMargin: 0,
|
||||||
|
borderWidth: 1,
|
||||||
};
|
};
|
||||||
|
|
||||||
export type PageLayoutParametersHook = BaseParametersHook<PageLayoutParameters>;
|
export type PageLayoutParametersHook = BaseParametersHook<PageLayoutParameters>;
|
||||||
@@ -17,6 +41,36 @@ export const usePageLayoutParameters = (): PageLayoutParametersHook => {
|
|||||||
return useBaseParameters<PageLayoutParameters>({
|
return useBaseParameters<PageLayoutParameters>({
|
||||||
defaultParameters,
|
defaultParameters,
|
||||||
endpointName: 'multi-page-layout',
|
endpointName: 'multi-page-layout',
|
||||||
|
validateFn: (params) => {
|
||||||
|
const cols = params.mode === 'DEFAULT'
|
||||||
|
? Math.ceil(Math.sqrt(params.pagesPerSheet))
|
||||||
|
: params.cols;
|
||||||
|
const rows = params.mode === 'DEFAULT'
|
||||||
|
? Math.ceil(params.pagesPerSheet / cols)
|
||||||
|
: params.rows;
|
||||||
|
|
||||||
|
if (cols <= 0 || rows <= 0) return false;
|
||||||
|
|
||||||
|
const pageWidth = params.orientation === 'PORTRAIT' ? 595.28 : 841.89;
|
||||||
|
const pageHeight = params.orientation === 'PORTRAIT' ? 841.89 : 595.28;
|
||||||
|
|
||||||
|
const left = params.leftMargin ?? 0;
|
||||||
|
const right = params.rightMargin ?? 0;
|
||||||
|
const top = params.topMargin ?? 0;
|
||||||
|
const bottom = params.bottomMargin ?? 0;
|
||||||
|
const inner = params.innerMargin ?? 0;
|
||||||
|
|
||||||
|
// Reject impossible outer margins first.
|
||||||
|
if (left + right >= pageWidth) return false;
|
||||||
|
if (top + bottom >= pageHeight) return false;
|
||||||
|
|
||||||
|
const cellWidth = (pageWidth - left - right) / cols;
|
||||||
|
const cellHeight = (pageHeight - top - bottom) / rows;
|
||||||
|
const innerWidth = cellWidth - 2 * inner;
|
||||||
|
const innerHeight = cellHeight - 2 * inner;
|
||||||
|
|
||||||
|
return innerWidth > 0 && innerHeight > 0;
|
||||||
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,24 @@
|
|||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { createToolFlow } from '@app/components/tools/shared/createToolFlow';
|
import { createToolFlow } from '@app/components/tools/shared/createToolFlow';
|
||||||
import { useBaseTool } from '@app/hooks/tools/shared/useBaseTool';
|
import { useBaseTool } from '@app/hooks/tools/shared/useBaseTool';
|
||||||
|
import { useAccordionSteps } from '@app/hooks/tools/shared/useAccordionSteps';
|
||||||
import { BaseToolProps, ToolComponent } from '@app/types/tool';
|
import { BaseToolProps, ToolComponent } from '@app/types/tool';
|
||||||
import { usePageLayoutParameters } from '@app/hooks/tools/pageLayout/usePageLayoutParameters';
|
import { usePageLayoutParameters } from '@app/hooks/tools/pageLayout/usePageLayoutParameters';
|
||||||
import { usePageLayoutOperation } from '@app/hooks/tools/pageLayout/usePageLayoutOperation';
|
import { usePageLayoutOperation } from '@app/hooks/tools/pageLayout/usePageLayoutOperation';
|
||||||
import PageLayoutSettings from '@app/components/tools/pageLayout/PageLayoutSettings';
|
import PageLayoutSettings from '@app/components/tools/pageLayout/PageLayoutSettings';
|
||||||
|
import PageLayoutAdvancedSettings from '@app/components/tools/pageLayout/PageLayoutAdvancedSettings';
|
||||||
|
import PageLayoutMarginsBordersSettings from '@app/components/tools/pageLayout/PageLayoutMarginsBordersSettings';
|
||||||
|
import { usePageLayoutTips } from '@app/components/tooltips/PageLayout/usePageLayoutTips';
|
||||||
|
import { usePageLayoutAdvancedTips } from '@app/components/tooltips/PageLayout/usePageLayoutAdvancedTips';
|
||||||
|
import { usePageLayoutMarginsBordersTips } from '@app/components/tooltips/PageLayout/usePageLayoutMarginsBordersTips';
|
||||||
|
import PageLayoutPreview from '@app/components/tools/pageLayout/PageLayoutPreview';
|
||||||
|
|
||||||
|
enum PageLayoutStep {
|
||||||
|
NONE = 'none',
|
||||||
|
LAYOUT = 'layout',
|
||||||
|
ADVANCED = 'advanced',
|
||||||
|
MARGINS_BORDERS = 'marginsBorders',
|
||||||
|
}
|
||||||
|
|
||||||
const PageLayout = (props: BaseToolProps) => {
|
const PageLayout = (props: BaseToolProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@@ -16,16 +30,34 @@ const PageLayout = (props: BaseToolProps) => {
|
|||||||
props
|
props
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const pageLayoutTips = usePageLayoutTips();
|
||||||
|
const pageLayoutAdvancedTips = usePageLayoutAdvancedTips();
|
||||||
|
const pageLayoutMarginsBordersTips = usePageLayoutMarginsBordersTips();
|
||||||
|
|
||||||
|
const accordion = useAccordionSteps<PageLayoutStep>({
|
||||||
|
noneValue: PageLayoutStep.NONE,
|
||||||
|
initialStep: PageLayoutStep.LAYOUT,
|
||||||
|
stateConditions: {
|
||||||
|
hasFiles: base.hasFiles,
|
||||||
|
hasResults: base.hasResults,
|
||||||
|
},
|
||||||
|
afterResults: base.handleSettingsReset,
|
||||||
|
});
|
||||||
|
|
||||||
return createToolFlow({
|
return createToolFlow({
|
||||||
files: {
|
files: {
|
||||||
selectedFiles: base.selectedFiles,
|
selectedFiles: base.selectedFiles,
|
||||||
isCollapsed: base.hasResults,
|
isCollapsed: base.hasResults,
|
||||||
},
|
},
|
||||||
|
preview: (
|
||||||
|
<PageLayoutPreview parameters={base.params.parameters} />
|
||||||
|
),
|
||||||
steps: [
|
steps: [
|
||||||
{
|
{
|
||||||
title: 'Settings',
|
title: 'Layout settings',
|
||||||
isCollapsed: base.settingsCollapsed,
|
isCollapsed: accordion.getCollapsedState(PageLayoutStep.LAYOUT),
|
||||||
onCollapsedClick: base.settingsCollapsed ? base.handleSettingsReset : undefined,
|
onCollapsedClick: () => accordion.handleStepToggle(PageLayoutStep.LAYOUT),
|
||||||
|
tooltip: pageLayoutTips,
|
||||||
content: (
|
content: (
|
||||||
<PageLayoutSettings
|
<PageLayoutSettings
|
||||||
parameters={base.params.parameters}
|
parameters={base.params.parameters}
|
||||||
@@ -34,6 +66,32 @@ const PageLayout = (props: BaseToolProps) => {
|
|||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: 'Advanced settings',
|
||||||
|
isCollapsed: accordion.getCollapsedState(PageLayoutStep.ADVANCED),
|
||||||
|
onCollapsedClick: () => accordion.handleStepToggle(PageLayoutStep.ADVANCED),
|
||||||
|
tooltip: pageLayoutAdvancedTips,
|
||||||
|
content: (
|
||||||
|
<PageLayoutAdvancedSettings
|
||||||
|
parameters={base.params.parameters}
|
||||||
|
onParameterChange={base.params.updateParameter}
|
||||||
|
disabled={base.endpointLoading}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Margins and borders',
|
||||||
|
isCollapsed: accordion.getCollapsedState(PageLayoutStep.MARGINS_BORDERS),
|
||||||
|
onCollapsedClick: () => accordion.handleStepToggle(PageLayoutStep.MARGINS_BORDERS),
|
||||||
|
tooltip: pageLayoutMarginsBordersTips,
|
||||||
|
content: (
|
||||||
|
<PageLayoutMarginsBordersSettings
|
||||||
|
parameters={base.params.parameters}
|
||||||
|
onParameterChange={base.params.updateParameter}
|
||||||
|
disabled={base.endpointLoading}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
},
|
||||||
],
|
],
|
||||||
executeButton: {
|
executeButton: {
|
||||||
text: t('pageLayout.submit', 'Create Layout'),
|
text: t('pageLayout.submit', 'Create Layout'),
|
||||||
|
|||||||
Reference in New Issue
Block a user