Multi page layout init for #212
This commit is contained in:
parent
8bbbdbd359
commit
2d88987cb3
7 changed files with 165 additions and 1 deletions
|
@ -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<byte[]> 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -108,4 +108,13 @@ public class OtherWebController {
|
||||||
return "other/remove-blanks";
|
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";
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -128,6 +128,10 @@ home.compare.desc=Compares and shows the differences between 2 PDF Documents
|
||||||
home.certSign.title=Sign with Certificate
|
home.certSign.title=Sign with Certificate
|
||||||
home.certSign.desc=Signs a PDF with a Certificate/Key (PEM/P12)
|
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
|
downloadPdf=Download PDF
|
||||||
text=Text
|
text=Text
|
||||||
|
@ -135,6 +139,11 @@ font=Font
|
||||||
selectFillter=-- Select --
|
selectFillter=-- Select --
|
||||||
pageNum=Page Number
|
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.title=Certificate Signing
|
||||||
certSign.header=Sign a PDF with your certificate (Work in progress)
|
certSign.header=Sign a PDF with your certificate (Work in progress)
|
||||||
certSign.selectPDF=Select a PDF File for Signing:
|
certSign.selectPDF=Select a PDF File for Signing:
|
||||||
|
|
3
src/main/resources/static/images/page-layout.svg
Normal file
3
src/main/resources/static/images/page-layout.svg
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-grid" viewBox="0 0 16 16">
|
||||||
|
<path d="M1 2.5A1.5 1.5 0 0 1 2.5 1h3A1.5 1.5 0 0 1 7 2.5v3A1.5 1.5 0 0 1 5.5 7h-3A1.5 1.5 0 0 1 1 5.5v-3zM2.5 2a.5.5 0 0 0-.5.5v3a.5.5 0 0 0 .5.5h3a.5.5 0 0 0 .5-.5v-3a.5.5 0 0 0-.5-.5h-3zm6.5.5A1.5 1.5 0 0 1 10.5 1h3A1.5 1.5 0 0 1 15 2.5v3A1.5 1.5 0 0 1 13.5 7h-3A1.5 1.5 0 0 1 9 5.5v-3zm1.5-.5a.5.5 0 0 0-.5.5v3a.5.5 0 0 0 .5.5h3a.5.5 0 0 0 .5-.5v-3a.5.5 0 0 0-.5-.5h-3zM1 10.5A1.5 1.5 0 0 1 2.5 9h3A1.5 1.5 0 0 1 7 10.5v3A1.5 1.5 0 0 1 5.5 15h-3A1.5 1.5 0 0 1 1 13.5v-3zm1.5-.5a.5.5 0 0 0-.5.5v3a.5.5 0 0 0 .5.5h3a.5.5 0 0 0 .5-.5v-3a.5.5 0 0 0-.5-.5h-3zm6.5.5A1.5 1.5 0 0 1 10.5 9h3a1.5 1.5 0 0 1 1.5 1.5v3a1.5 1.5 0 0 1-1.5 1.5h-3A1.5 1.5 0 0 1 9 13.5v-3zm1.5-.5a.5.5 0 0 0-.5.5v3a.5.5 0 0 0 .5.5h3a.5.5 0 0 0 .5-.5v-3a.5.5 0 0 0-.5-.5h-3z"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 880 B |
|
@ -156,7 +156,8 @@ function compareVersions(version1, version2) {
|
||||||
<div th:replace="~{fragments/navbarEntry :: navbarEntry ( 'pdf-organizer', 'images/sort-numeric-down.svg', 'home.pdfOrganiser.title', 'home.pdfOrganiser.desc')}"></div>
|
<div th:replace="~{fragments/navbarEntry :: navbarEntry ( 'pdf-organizer', 'images/sort-numeric-down.svg', 'home.pdfOrganiser.title', 'home.pdfOrganiser.desc')}"></div>
|
||||||
<div th:replace="~{fragments/navbarEntry :: navbarEntry ( 'rotate-pdf', 'images/arrow-clockwise.svg', 'home.rotate.title', 'home.rotate.desc')}"></div>
|
<div th:replace="~{fragments/navbarEntry :: navbarEntry ( 'rotate-pdf', 'images/arrow-clockwise.svg', 'home.rotate.title', 'home.rotate.desc')}"></div>
|
||||||
<div th:replace="~{fragments/navbarEntry :: navbarEntry ( 'remove-pages', 'images/file-earmark-x.svg', 'home.removePages.title', 'home.removePages.desc')}"></div>
|
<div th:replace="~{fragments/navbarEntry :: navbarEntry ( 'remove-pages', 'images/file-earmark-x.svg', 'home.removePages.title', 'home.removePages.desc')}"></div>
|
||||||
|
<div th:replace="~{fragments/navbarEntry :: navbarEntry ( 'multi-page-layout', 'images/page-layout.svg', 'home.pageLayout.title', 'home.pageLayout.desc')}"></div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item nav-item-separator"></li>
|
<li class="nav-item nav-item-separator"></li>
|
||||||
|
|
|
@ -136,7 +136,10 @@ filter: invert(0.2) sepia(2) saturate(50) hue-rotate(190deg);
|
||||||
<div th:replace="~{fragments/card :: card(id='compare', cardTitle=#{home.compare.title}, cardText=#{home.compare.desc}, cardLink='compare', svgPath='images/scales.svg')}"></div>
|
<div th:replace="~{fragments/card :: card(id='compare', cardTitle=#{home.compare.title}, cardText=#{home.compare.desc}, cardLink='compare', svgPath='images/scales.svg')}"></div>
|
||||||
|
|
||||||
<div th:replace="~{fragments/card :: card(id='cert-sign', cardTitle=#{home.certSign.title}, cardText=#{home.certSign.desc}, cardLink='cert-sign', svgPath='images/award.svg')}"></div>
|
<div th:replace="~{fragments/card :: card(id='cert-sign', cardTitle=#{home.certSign.title}, cardText=#{home.certSign.desc}, cardLink='cert-sign', svgPath='images/award.svg')}"></div>
|
||||||
|
<div th:replace="~{fragments/card :: card(id='multi-page-layout', cardTitle=#{home.pageLayout.title}, cardText=#{home.pageLayout.desc}, cardLink='multi-page-layout', svgPath='images/page-layout.svg')}"></div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
function toggleFavorite(element) {
|
function toggleFavorite(element) {
|
||||||
var img = element.querySelector('img');
|
var img = element.querySelector('img');
|
||||||
|
|
39
src/main/resources/templates/other/multi-page-layout.html
Normal file
39
src/main/resources/templates/other/multi-page-layout.html
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html th:lang="${#locale.toString()}" th:lang-direction="#{language.direction}" xmlns:th="http://www.thymeleaf.org">
|
||||||
|
|
||||||
|
|
||||||
|
<th:block th:insert="~{fragments/common :: head(title=#{pageLayout.title})}"></th:block>
|
||||||
|
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div id="page-container">
|
||||||
|
<div id="content-wrap">
|
||||||
|
<div th:insert="~{fragments/navbar.html :: navbar}"></div>
|
||||||
|
<br> <br>
|
||||||
|
<div class="container">
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<h2 th:text="#{pageLayout.header}"></h2>
|
||||||
|
<form id="multiPdfForm" th:action="@{multi-page-layout}" method="post" enctype="multipart/form-data">
|
||||||
|
<div th:replace="~{fragments/common :: fileSelector(name='fileInput', multiple=false, accept='application/pdf')}"></div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="pagesPerSheet" th:text="#{pageLayout.pagesPerSheet}"></label>
|
||||||
|
<select id="pagesPerSheet" name="pagesPerSheet" required>
|
||||||
|
<option value="2">2</option>
|
||||||
|
<option value="3">3</option>
|
||||||
|
<option value="4">4</option>
|
||||||
|
<option value="9">9</option>
|
||||||
|
<option value="16">16</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{pageLayout.submit}"></button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div th:insert="~{fragments/footer.html :: footer}"></div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
Loading…
Reference in a new issue