diff --git a/src/main/java/stirling/software/SPDF/controller/api/other/MultiPageLayoutController.java b/src/main/java/stirling/software/SPDF/controller/api/other/MultiPageLayoutController.java new file mode 100644 index 00000000..458e06e9 --- /dev/null +++ b/src/main/java/stirling/software/SPDF/controller/api/other/MultiPageLayoutController.java @@ -0,0 +1,100 @@ +package stirling.software.SPDF.controller.api.other; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpHeaders; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; + +import com.itextpdf.kernel.geom.PageSize; +import com.itextpdf.kernel.geom.Rectangle; +import com.itextpdf.kernel.pdf.PdfDocument; +import com.itextpdf.kernel.pdf.PdfPage; +import com.itextpdf.kernel.pdf.PdfReader; +import com.itextpdf.kernel.pdf.PdfWriter; +import com.itextpdf.kernel.pdf.canvas.PdfCanvas; +import com.itextpdf.kernel.pdf.xobject.PdfFormXObject; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Schema; + +@RestController +public class MultiPageLayoutController { + + private static final Logger logger = LoggerFactory.getLogger(MultiPageLayoutController.class); + + @PostMapping(value = "/multi-page-layout", consumes = "multipart/form-data") + @Operation(summary = "Merge multiple pages of a PDF document into a single page", description = "This operation takes an input PDF file and the number of pages to merge into a single sheet in the output PDF file.") + public ResponseEntity mergeMultiplePagesIntoOne( + @Parameter(description = "The input PDF file", required = true) @RequestParam("fileInput") MultipartFile file, + @Parameter(description = "The number of pages to fit onto a single sheet in the output PDF. Acceptable values are 2, 3, 4, 9, 16.", required = true, schema = @Schema(type = "integer", allowableValues = { + "2", "3", "4", "9", "16" })) @RequestParam("pagesPerSheet") int pagesPerSheet) + throws IOException { + + if (pagesPerSheet != 2 && pagesPerSheet != 3 + && pagesPerSheet != (int) Math.sqrt(pagesPerSheet) * Math.sqrt(pagesPerSheet)) { + throw new IllegalArgumentException("pagesPerSheet must be 2, 3 or a perfect square"); + } + + int cols = pagesPerSheet == 2 || pagesPerSheet == 3 ? pagesPerSheet : (int) Math.sqrt(pagesPerSheet); + int rows = pagesPerSheet == 2 || pagesPerSheet == 3 ? 1 : (int) Math.sqrt(pagesPerSheet); + + byte[] bytes = file.getBytes(); + PdfReader reader = new PdfReader(new ByteArrayInputStream(bytes)); + PdfDocument pdfDoc = new PdfDocument(reader); + + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + PdfWriter writer = new PdfWriter(baos); + PdfDocument outputPdf = new PdfDocument(writer); + PageSize pageSize = new PageSize(PageSize.A4.rotate()); + + int totalPages = pdfDoc.getNumberOfPages(); + float cellWidth = pageSize.getWidth() / cols; + float cellHeight = pageSize.getHeight() / rows; + + for (int i = 1; i <= totalPages; i += pagesPerSheet) { + PdfPage page = outputPdf.addNewPage(pageSize); + PdfCanvas pdfCanvas = new PdfCanvas(page); + + for (int row = 0; row < rows; row++) { + for (int col = 0; col < cols; col++) { + int index = i + row * cols + col; + if (index <= totalPages) { + // Get the page and calculate scaling factors + Rectangle rect = pdfDoc.getPage(index).getPageSize(); + float scaleWidth = cellWidth / rect.getWidth(); + float scaleHeight = cellHeight / rect.getHeight(); + float scale = Math.min(scaleWidth, scaleHeight); + + PdfFormXObject formXObject = pdfDoc.getPage(index).copyAsFormXObject(outputPdf); + float x = col * cellWidth + (cellWidth - rect.getWidth() * scale) / 2; + float y = (rows - 1 - row) * cellHeight + (cellHeight - rect.getHeight() * scale) / 2; + + // Save the graphics state, apply the transformations, add the object, and then + // restore the graphics state + pdfCanvas.saveState(); + pdfCanvas.concatMatrix(scale, 0, 0, scale, x, y); + pdfCanvas.addXObject(formXObject, 0, 0); + pdfCanvas.restoreState(); + } + } + } + } + + outputPdf.close(); + byte[] pdfContent = baos.toByteArray(); + pdfDoc.close(); + return ResponseEntity.ok() + .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"modifiedDocument.pdf\"") + .body(pdfContent); + } + +} diff --git a/src/main/java/stirling/software/SPDF/controller/web/OtherWebController.java b/src/main/java/stirling/software/SPDF/controller/web/OtherWebController.java index aa4fbbf2..e86fb764 100644 --- a/src/main/java/stirling/software/SPDF/controller/web/OtherWebController.java +++ b/src/main/java/stirling/software/SPDF/controller/web/OtherWebController.java @@ -108,4 +108,13 @@ public class OtherWebController { return "other/remove-blanks"; } + @GetMapping("/multi-page-layout") + @Hidden + public String multiPageLayoutForm(Model model) { + model.addAttribute("currentPage", "multi-page-layout"); + return "other/multi-page-layout"; + } + + + } diff --git a/src/main/resources/messages_en_GB.properties b/src/main/resources/messages_en_GB.properties index 3134b477..02e5286d 100644 --- a/src/main/resources/messages_en_GB.properties +++ b/src/main/resources/messages_en_GB.properties @@ -128,6 +128,10 @@ home.compare.desc=Compares and shows the differences between 2 PDF Documents home.certSign.title=Sign with Certificate home.certSign.desc=Signs a PDF with a Certificate/Key (PEM/P12) +home.pageLayout.title=Multi-Page Layout +home.pageLayout.desc=Merge multiple pages of a PDF document into a single page + + downloadPdf=Download PDF text=Text @@ -135,6 +139,11 @@ font=Font selectFillter=-- Select -- pageNum=Page Number +pageLayout.title=Multi Page Layout +pageLayout.header=Multi Page Layout +pageLayout.pagesPerSheet=Pages per sheet: +pageLayout.submit=Submit + certSign.title=Certificate Signing certSign.header=Sign a PDF with your certificate (Work in progress) certSign.selectPDF=Select a PDF File for Signing: diff --git a/src/main/resources/static/images/page-layout.svg b/src/main/resources/static/images/page-layout.svg new file mode 100644 index 00000000..bc505957 --- /dev/null +++ b/src/main/resources/static/images/page-layout.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/main/resources/templates/fragments/navbar.html b/src/main/resources/templates/fragments/navbar.html index c14a9314..78f8508d 100644 --- a/src/main/resources/templates/fragments/navbar.html +++ b/src/main/resources/templates/fragments/navbar.html @@ -156,7 +156,8 @@ function compareVersions(version1, version2) {
- +
+ diff --git a/src/main/resources/templates/home.html b/src/main/resources/templates/home.html index 24b7b66d..5d40ee9d 100644 --- a/src/main/resources/templates/home.html +++ b/src/main/resources/templates/home.html @@ -136,7 +136,10 @@ filter: invert(0.2) sepia(2) saturate(50) hue-rotate(190deg);
+
+ +