mirror of
https://github.com/arsvendg/Stirling-PDF.git
synced 2026-07-16 19:33:11 +02:00
feat(multi-layout): implement form field copying and transformation for multi-page PDF to keep form data (#4314)
This commit is contained in:
+28
-3
@@ -1,6 +1,6 @@
|
||||
package stirling.software.SPDF.controller.api;
|
||||
|
||||
import java.awt.*;
|
||||
import java.awt.Color;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
|
||||
@@ -24,15 +24,18 @@ import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.SPDF.model.api.general.MergeMultiplePagesRequest;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.util.FormUtils;
|
||||
import stirling.software.common.util.WebResponseUtils;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/general")
|
||||
@Tag(name = "General", description = "General APIs")
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class MultiPageLayoutController {
|
||||
|
||||
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
||||
@@ -103,7 +106,8 @@ public class MultiPageLayoutController {
|
||||
float scale = Math.min(scaleWidth, scaleHeight);
|
||||
|
||||
int adjustedPageIndex =
|
||||
i % pagesPerSheet; // This will reset the index for every new page
|
||||
i % pagesPerSheet; // Close the current content stream and create a new
|
||||
// page and content stream
|
||||
int rowIndex = adjustedPageIndex / cols;
|
||||
int colIndex = adjustedPageIndex % cols;
|
||||
|
||||
@@ -131,7 +135,28 @@ public class MultiPageLayoutController {
|
||||
}
|
||||
}
|
||||
|
||||
contentStream.close(); // Close the final content stream
|
||||
contentStream.close();
|
||||
|
||||
// If any source page is rotated, skip form copying/transformation entirely
|
||||
boolean hasRotation = FormUtils.hasAnyRotatedPage(sourceDocument);
|
||||
if (hasRotation) {
|
||||
log.info("Source document has rotated pages; skipping form field copying.");
|
||||
} else {
|
||||
try {
|
||||
FormUtils.copyAndTransformFormFields(
|
||||
sourceDocument,
|
||||
newDocument,
|
||||
totalPages,
|
||||
pagesPerSheet,
|
||||
cols,
|
||||
rows,
|
||||
cellWidth,
|
||||
cellHeight);
|
||||
} catch (Exception e) {
|
||||
log.warn("Failed to copy and transform form fields: {}", e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
sourceDocument.close();
|
||||
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
|
||||
+116
@@ -0,0 +1,116 @@
|
||||
package stirling.software.SPDF.controller.api;
|
||||
|
||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||
import org.apache.pdfbox.pdmodel.PDPage;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.Mockito;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.mock.web.MockMultipartFile;
|
||||
|
||||
import stirling.software.SPDF.model.api.general.MergeMultiplePagesRequest;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class MultiPageLayoutControllerTest {
|
||||
|
||||
@Mock private CustomPDFDocumentFactory pdfDocumentFactory;
|
||||
|
||||
@InjectMocks private MultiPageLayoutController controller;
|
||||
|
||||
private MockMultipartFile fileWithExt;
|
||||
private MockMultipartFile fileNoExt;
|
||||
|
||||
@BeforeEach
|
||||
void setup() {
|
||||
fileWithExt =
|
||||
new MockMultipartFile(
|
||||
"fileInput", "test.pdf", "application/pdf", new byte[] {1, 2, 3});
|
||||
fileNoExt =
|
||||
new MockMultipartFile("fileInput", "name", "application/pdf", new byte[] {4, 5, 6});
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Rejects non-2/3 and non-perfect-square pagesPerSheet")
|
||||
void invalidPagesPerSheetThrows() {
|
||||
MergeMultiplePagesRequest req = new MergeMultiplePagesRequest();
|
||||
req.setPagesPerSheet(5);
|
||||
req.setAddBorder(Boolean.TRUE);
|
||||
req.setFileInput(fileWithExt);
|
||||
|
||||
Assertions.assertThrows(
|
||||
IllegalArgumentException.class, () -> controller.mergeMultiplePagesIntoOne(req));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Generates PDF and filename suffix for perfect-square layout with no source pages")
|
||||
void perfectSquareNoPages() throws Exception {
|
||||
PDDocument source = new PDDocument();
|
||||
PDDocument target = new PDDocument();
|
||||
Mockito.when(pdfDocumentFactory.load(fileWithExt)).thenReturn(source);
|
||||
Mockito.when(pdfDocumentFactory.createNewDocumentBasedOnOldDocument(source))
|
||||
.thenReturn(target);
|
||||
|
||||
MergeMultiplePagesRequest req = new MergeMultiplePagesRequest();
|
||||
req.setPagesPerSheet(4);
|
||||
req.setAddBorder(Boolean.FALSE);
|
||||
req.setFileInput(fileWithExt);
|
||||
|
||||
ResponseEntity<byte[]> resp = controller.mergeMultiplePagesIntoOne(req);
|
||||
Assertions.assertEquals(HttpStatus.OK, resp.getStatusCode());
|
||||
Assertions.assertEquals(MediaType.APPLICATION_PDF, resp.getHeaders().getContentType());
|
||||
Assertions.assertNotNull(resp.getBody());
|
||||
Assertions.assertTrue(resp.getBody().length > 0);
|
||||
Assertions.assertEquals(
|
||||
"test_layoutChanged.pdf", resp.getHeaders().getContentDisposition().getFilename());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Merges single source page into 2-up layout and returns PDF")
|
||||
void twoUpWithSinglePage() throws Exception {
|
||||
PDDocument source = new PDDocument();
|
||||
source.addPage(new PDPage());
|
||||
PDDocument target = new PDDocument();
|
||||
Mockito.when(pdfDocumentFactory.load(fileWithExt)).thenReturn(source);
|
||||
Mockito.when(pdfDocumentFactory.createNewDocumentBasedOnOldDocument(source))
|
||||
.thenReturn(target);
|
||||
|
||||
MergeMultiplePagesRequest req = new MergeMultiplePagesRequest();
|
||||
req.setPagesPerSheet(2);
|
||||
req.setAddBorder(Boolean.TRUE);
|
||||
req.setFileInput(fileWithExt);
|
||||
|
||||
ResponseEntity<byte[]> resp = controller.mergeMultiplePagesIntoOne(req);
|
||||
Assertions.assertEquals(HttpStatus.OK, resp.getStatusCode());
|
||||
Assertions.assertEquals(MediaType.APPLICATION_PDF, resp.getHeaders().getContentType());
|
||||
Assertions.assertNotNull(resp.getBody());
|
||||
Assertions.assertTrue(resp.getBody().length > 0);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Uses input name without extension and appends suffix for 3-up")
|
||||
void threeUpWithNameNoExtension() throws Exception {
|
||||
PDDocument source = new PDDocument();
|
||||
PDDocument target = new PDDocument();
|
||||
Mockito.when(pdfDocumentFactory.load(fileNoExt)).thenReturn(source);
|
||||
Mockito.when(pdfDocumentFactory.createNewDocumentBasedOnOldDocument(source))
|
||||
.thenReturn(target);
|
||||
|
||||
MergeMultiplePagesRequest req = new MergeMultiplePagesRequest();
|
||||
req.setPagesPerSheet(3);
|
||||
req.setAddBorder(Boolean.TRUE);
|
||||
req.setFileInput(fileNoExt);
|
||||
|
||||
ResponseEntity<byte[]> resp = controller.mergeMultiplePagesIntoOne(req);
|
||||
Assertions.assertEquals(
|
||||
"name_layoutChanged.pdf", resp.getHeaders().getContentDisposition().getFilename());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user