mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2026-09-03 05:10:16 +03:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eb2c18325b |
+13
@@ -25,6 +25,7 @@ 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.BookletImpositionRequest;
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
@@ -33,11 +34,13 @@ import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.util.GeneralUtils;
|
||||
import stirling.software.common.util.TempFileManager;
|
||||
import stirling.software.common.util.WebResponseUtils;
|
||||
import stirling.software.jpdfium.PdfDocument;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/general")
|
||||
@Tag(name = "General", description = "General APIs")
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class BookletImpositionController {
|
||||
|
||||
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
||||
@@ -73,6 +76,16 @@ public class BookletImpositionController {
|
||||
"Booklet printing uses 2 pages per side (landscape). For 4-up, use the N-up feature.");
|
||||
}
|
||||
|
||||
// JPDFium pre-validate catches corrupt PDFs cheaply before PDFBox imposes.
|
||||
// Holdout: JPDFium PdfPrint.booklet lacks gutter, duplex passes, flipOnShortEdge, border.
|
||||
if (file != null) {
|
||||
try (PdfDocument ignored = PdfDocument.open(file.getBytes())) {
|
||||
} catch (Exception e) {
|
||||
log.debug(
|
||||
"JPDFium pre-validate failed; proceeding with PDFBox: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
try (PDDocument sourceDocument = pdfDocumentFactory.load(file)) {
|
||||
int totalPages = sourceDocument.getNumberOfPages();
|
||||
|
||||
|
||||
+11
@@ -31,6 +31,7 @@ import stirling.software.common.util.GeneralFormCopyUtils;
|
||||
import stirling.software.common.util.GeneralUtils;
|
||||
import stirling.software.common.util.TempFileManager;
|
||||
import stirling.software.common.util.WebResponseUtils;
|
||||
import stirling.software.jpdfium.PdfDocument;
|
||||
|
||||
@GeneralApi
|
||||
@RequiredArgsConstructor
|
||||
@@ -191,6 +192,16 @@ public class MultiPageLayoutController {
|
||||
|
||||
MultipartFile file = request.getFileInput();
|
||||
|
||||
// JPDFium pre-validate catches corrupt PDFs cheaply before PDFBox lays out.
|
||||
// Holdout: JPDFium NUpLayout has no margins, borders, RTL, BY_COLUMNS, or form copy.
|
||||
if (file != null) {
|
||||
try (PdfDocument ignored = PdfDocument.open(file.getBytes())) {
|
||||
} catch (Exception e) {
|
||||
log.debug(
|
||||
"JPDFium pre-validate failed; proceeding with PDFBox: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
try (PDDocument sourceDocument = pdfDocumentFactory.load(file)) {
|
||||
try (PDDocument newDocument =
|
||||
pdfDocumentFactory.createNewDocumentBasedOnOldDocument(sourceDocument)) {
|
||||
|
||||
@@ -35,6 +35,7 @@ import stirling.software.common.util.GeneralUtils;
|
||||
import stirling.software.common.util.TempFile;
|
||||
import stirling.software.common.util.TempFileManager;
|
||||
import stirling.software.common.util.WebResponseUtils;
|
||||
import stirling.software.jpdfium.PdfDocument;
|
||||
|
||||
@GeneralApi
|
||||
@Slf4j
|
||||
@@ -65,6 +66,16 @@ public class PosterPdfController {
|
||||
String filename = GeneralUtils.generateFilename(file.getOriginalFilename(), "");
|
||||
log.debug("Base filename for output: {}", filename);
|
||||
|
||||
// JPDFium pre-validate catches corrupt PDFs cheaply before PDFBox tiles.
|
||||
// Holdout: JPDFium PdfPosterizer edits in-place without scaling to target paper or RTL.
|
||||
if (file != null) {
|
||||
try (PdfDocument ignored = PdfDocument.open(file.getBytes())) {
|
||||
} catch (Exception e) {
|
||||
log.debug(
|
||||
"JPDFium pre-validate failed; proceeding with PDFBox: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
TempFile zipTempFile = new TempFile(tempFileManager, ".zip");
|
||||
try {
|
||||
try (PDDocument sourceDocument = pdfDocumentFactory.load(file);
|
||||
|
||||
+13
-1
@@ -16,6 +16,7 @@ import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.SPDF.config.swagger.StandardPdfResponse;
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
@@ -26,9 +27,11 @@ import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.util.GeneralUtils;
|
||||
import stirling.software.common.util.TempFileManager;
|
||||
import stirling.software.common.util.WebResponseUtils;
|
||||
import stirling.software.jpdfium.PdfDocument;
|
||||
|
||||
@GeneralApi
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class ToSinglePageController {
|
||||
|
||||
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
||||
@@ -49,7 +52,16 @@ public class ToSinglePageController {
|
||||
public ResponseEntity<Resource> pdfToSinglePage(@ModelAttribute PDFFile request)
|
||||
throws IOException {
|
||||
|
||||
// Load the source document
|
||||
// JPDFium pre-validate catches corrupt PDFs cheaply before PDFBox stitches.
|
||||
// Holdout: JPDFium PdfLongImage only renders raster output, no PDF page emit.
|
||||
if (request.getFileInput() != null) {
|
||||
try (PdfDocument ignored = PdfDocument.open(request.getFileInput().getBytes())) {
|
||||
} catch (Exception e) {
|
||||
log.debug(
|
||||
"JPDFium pre-validate failed; proceeding with PDFBox: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
try (PDDocument sourceDocument = pdfDocumentFactory.load(request)) {
|
||||
// Calculate total height and max width
|
||||
float totalHeight = 0;
|
||||
|
||||
+187
@@ -0,0 +1,187 @@
|
||||
package stirling.software.SPDF.controller.api;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.Mockito.lenient;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipInputStream;
|
||||
|
||||
import org.apache.pdfbox.Loader;
|
||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||
import org.apache.pdfbox.pdmodel.PDPage;
|
||||
import org.apache.pdfbox.pdmodel.common.PDRectangle;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.mock.web.MockMultipartFile;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import stirling.software.SPDF.model.api.general.PosterPdfRequest;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.util.TempFile;
|
||||
import stirling.software.common.util.TempFileManager;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class PosterPdfControllerTest {
|
||||
|
||||
private static byte[] drainBody(ResponseEntity<Resource> response) throws IOException {
|
||||
java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream();
|
||||
try (java.io.InputStream in = response.getBody().getInputStream()) {
|
||||
in.transferTo(baos);
|
||||
}
|
||||
return baos.toByteArray();
|
||||
}
|
||||
|
||||
@TempDir Path tempDir;
|
||||
@Mock private CustomPDFDocumentFactory pdfDocumentFactory;
|
||||
@Mock private TempFileManager tempFileManager;
|
||||
@InjectMocks private PosterPdfController controller;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() throws Exception {
|
||||
lenient()
|
||||
.when(tempFileManager.createTempFile(anyString()))
|
||||
.thenAnswer(
|
||||
inv -> Files.createTempFile("test", inv.<String>getArgument(0)).toFile());
|
||||
lenient()
|
||||
.when(tempFileManager.createManagedTempFile(anyString()))
|
||||
.thenAnswer(
|
||||
inv -> {
|
||||
File f =
|
||||
Files.createTempFile("test", inv.<String>getArgument(0))
|
||||
.toFile();
|
||||
TempFile tf = mock(TempFile.class);
|
||||
lenient().when(tf.getFile()).thenReturn(f);
|
||||
lenient().when(tf.getPath()).thenReturn(f.toPath());
|
||||
return tf;
|
||||
});
|
||||
}
|
||||
|
||||
private MockMultipartFile createRealPdf(int numPages, float width, float height)
|
||||
throws IOException {
|
||||
Path path = tempDir.resolve("input.pdf");
|
||||
try (PDDocument doc = new PDDocument()) {
|
||||
for (int i = 0; i < numPages; i++) {
|
||||
doc.addPage(new PDPage(new PDRectangle(width, height)));
|
||||
}
|
||||
doc.save(path.toFile());
|
||||
}
|
||||
return new MockMultipartFile(
|
||||
"fileInput",
|
||||
"input.pdf",
|
||||
MediaType.APPLICATION_PDF_VALUE,
|
||||
Files.readAllBytes(path));
|
||||
}
|
||||
|
||||
private void wireFactory(MockMultipartFile file) throws IOException {
|
||||
when(pdfDocumentFactory.load(any(MultipartFile.class)))
|
||||
.thenAnswer(inv -> Loader.loadPDF(file.getBytes()));
|
||||
when(pdfDocumentFactory.createNewDocumentBasedOnOldDocument(any(PDDocument.class)))
|
||||
.thenAnswer(inv -> new PDDocument());
|
||||
}
|
||||
|
||||
private byte[] firstPdfFromZip(byte[] zipBytes) throws IOException {
|
||||
try (ZipInputStream zis = new ZipInputStream(new ByteArrayInputStream(zipBytes))) {
|
||||
ZipEntry entry = zis.getNextEntry();
|
||||
assertThat(entry).isNotNull();
|
||||
assertThat(entry.getName()).endsWith(".pdf");
|
||||
return zis.readAllBytes();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void posterPdf_default2x2_producesFourPagesPerInput() throws Exception {
|
||||
MockMultipartFile file = createRealPdf(1, 600f, 800f);
|
||||
wireFactory(file);
|
||||
|
||||
PosterPdfRequest request = new PosterPdfRequest();
|
||||
request.setFileInput(file);
|
||||
request.setPageSize("A4");
|
||||
request.setXFactor(2);
|
||||
request.setYFactor(2);
|
||||
|
||||
ResponseEntity<Resource> response = controller.posterPdf(request);
|
||||
|
||||
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
|
||||
byte[] zip = drainBody(response);
|
||||
try (PDDocument out = Loader.loadPDF(firstPdfFromZip(zip))) {
|
||||
assertThat(out.getNumberOfPages()).isEqualTo(4);
|
||||
assertThat(out.getPage(0).getMediaBox().getWidth())
|
||||
.isEqualTo(PDRectangle.A4.getWidth());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void posterPdf_3x2_producesSixPagesPerInput() throws Exception {
|
||||
MockMultipartFile file = createRealPdf(2, 600f, 400f);
|
||||
wireFactory(file);
|
||||
|
||||
PosterPdfRequest request = new PosterPdfRequest();
|
||||
request.setFileInput(file);
|
||||
request.setPageSize("Letter");
|
||||
request.setXFactor(3);
|
||||
request.setYFactor(2);
|
||||
|
||||
ResponseEntity<Resource> response = controller.posterPdf(request);
|
||||
|
||||
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
|
||||
try (PDDocument out = Loader.loadPDF(firstPdfFromZip(drainBody(response)))) {
|
||||
assertThat(out.getNumberOfPages()).isEqualTo(12);
|
||||
assertThat(out.getPage(0).getMediaBox().getWidth())
|
||||
.isEqualTo(PDRectangle.LETTER.getWidth());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void posterPdf_rightToLeftOrdering_stillSameTotalCount() throws Exception {
|
||||
MockMultipartFile file = createRealPdf(1, 600f, 400f);
|
||||
wireFactory(file);
|
||||
|
||||
PosterPdfRequest request = new PosterPdfRequest();
|
||||
request.setFileInput(file);
|
||||
request.setPageSize("A4");
|
||||
request.setXFactor(2);
|
||||
request.setYFactor(2);
|
||||
request.setRightToLeft(true);
|
||||
|
||||
ResponseEntity<Resource> response = controller.posterPdf(request);
|
||||
|
||||
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
|
||||
try (PDDocument out = Loader.loadPDF(firstPdfFromZip(drainBody(response)))) {
|
||||
assertThat(out.getNumberOfPages()).isEqualTo(4);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void posterPdf_invalidPageSize_throws() throws Exception {
|
||||
MockMultipartFile file = createRealPdf(1, 600f, 400f);
|
||||
wireFactory(file);
|
||||
|
||||
PosterPdfRequest request = new PosterPdfRequest();
|
||||
request.setFileInput(file);
|
||||
request.setPageSize("Foo");
|
||||
request.setXFactor(2);
|
||||
request.setYFactor(2);
|
||||
|
||||
assertThatThrownBy(() -> controller.posterPdf(request))
|
||||
.isInstanceOf(IllegalArgumentException.class);
|
||||
}
|
||||
}
|
||||
+156
@@ -0,0 +1,156 @@
|
||||
package stirling.software.SPDF.controller.api;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.Mockito.lenient;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import org.apache.pdfbox.Loader;
|
||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||
import org.apache.pdfbox.pdmodel.PDPage;
|
||||
import org.apache.pdfbox.pdmodel.common.PDRectangle;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.mock.web.MockMultipartFile;
|
||||
|
||||
import stirling.software.common.model.api.PDFFile;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.util.TempFile;
|
||||
import stirling.software.common.util.TempFileManager;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class ToSinglePageControllerTest {
|
||||
|
||||
private static byte[] drainBody(ResponseEntity<Resource> response) throws IOException {
|
||||
java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream();
|
||||
try (java.io.InputStream in = response.getBody().getInputStream()) {
|
||||
in.transferTo(baos);
|
||||
}
|
||||
return baos.toByteArray();
|
||||
}
|
||||
|
||||
@TempDir Path tempDir;
|
||||
@Mock private CustomPDFDocumentFactory pdfDocumentFactory;
|
||||
@Mock private TempFileManager tempFileManager;
|
||||
@InjectMocks private ToSinglePageController controller;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() throws Exception {
|
||||
lenient()
|
||||
.when(tempFileManager.createManagedTempFile(anyString()))
|
||||
.thenAnswer(
|
||||
inv -> {
|
||||
File f =
|
||||
Files.createTempFile("test", inv.<String>getArgument(0))
|
||||
.toFile();
|
||||
TempFile tf = mock(TempFile.class);
|
||||
lenient().when(tf.getFile()).thenReturn(f);
|
||||
lenient().when(tf.getPath()).thenReturn(f.toPath());
|
||||
return tf;
|
||||
});
|
||||
}
|
||||
|
||||
private MockMultipartFile createRealPdf(int numPages, float width, float height)
|
||||
throws IOException {
|
||||
Path path = tempDir.resolve("input.pdf");
|
||||
try (PDDocument doc = new PDDocument()) {
|
||||
for (int i = 0; i < numPages; i++) {
|
||||
doc.addPage(new PDPage(new PDRectangle(width, height)));
|
||||
}
|
||||
doc.save(path.toFile());
|
||||
}
|
||||
return new MockMultipartFile(
|
||||
"fileInput",
|
||||
"input.pdf",
|
||||
MediaType.APPLICATION_PDF_VALUE,
|
||||
Files.readAllBytes(path));
|
||||
}
|
||||
|
||||
private void wireFactory(MockMultipartFile file) throws IOException {
|
||||
when(pdfDocumentFactory.load(any(PDFFile.class)))
|
||||
.thenAnswer(inv -> Loader.loadPDF(file.getBytes()));
|
||||
when(pdfDocumentFactory.createNewDocumentBasedOnOldDocument(any(PDDocument.class)))
|
||||
.thenAnswer(inv -> new PDDocument());
|
||||
}
|
||||
|
||||
@Test
|
||||
void singlePage_combinesIntoOneTallPage() throws Exception {
|
||||
MockMultipartFile file = createRealPdf(3, 200f, 300f);
|
||||
wireFactory(file);
|
||||
|
||||
PDFFile request = new PDFFile();
|
||||
request.setFileInput(file);
|
||||
|
||||
ResponseEntity<Resource> response = controller.pdfToSinglePage(request);
|
||||
|
||||
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
|
||||
assertThat(response.getHeaders().getContentType()).isEqualTo(MediaType.APPLICATION_PDF);
|
||||
byte[] body = drainBody(response);
|
||||
assertThat(body).isNotEmpty();
|
||||
|
||||
try (PDDocument out = Loader.loadPDF(body)) {
|
||||
assertThat(out.getNumberOfPages()).isEqualTo(1);
|
||||
PDRectangle box = out.getPage(0).getMediaBox();
|
||||
assertThat(box.getWidth()).isEqualTo(200f);
|
||||
assertThat(box.getHeight()).isEqualTo(900f);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void singlePageInput_returnsOnePage() throws Exception {
|
||||
MockMultipartFile file = createRealPdf(1, 612f, 792f);
|
||||
wireFactory(file);
|
||||
|
||||
PDFFile request = new PDFFile();
|
||||
request.setFileInput(file);
|
||||
|
||||
ResponseEntity<Resource> response = controller.pdfToSinglePage(request);
|
||||
|
||||
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
|
||||
try (PDDocument out = Loader.loadPDF(drainBody(response))) {
|
||||
assertThat(out.getNumberOfPages()).isEqualTo(1);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void filenameSuffixApplied() throws Exception {
|
||||
MockMultipartFile file = createRealPdf(2, 100f, 100f);
|
||||
wireFactory(file);
|
||||
|
||||
PDFFile request = new PDFFile();
|
||||
request.setFileInput(file);
|
||||
|
||||
ResponseEntity<Resource> response = controller.pdfToSinglePage(request);
|
||||
assertThat(response.getHeaders().getContentDisposition().getFilename())
|
||||
.isEqualTo("input_singlePage.pdf");
|
||||
}
|
||||
|
||||
@Test
|
||||
void propagatesIoException() throws Exception {
|
||||
MockMultipartFile file = createRealPdf(2, 100f, 100f);
|
||||
when(pdfDocumentFactory.load(any(PDFFile.class))).thenThrow(new IOException("load failed"));
|
||||
|
||||
PDFFile request = new PDFFile();
|
||||
request.setFileInput(file);
|
||||
|
||||
assertThatThrownBy(() -> controller.pdfToSinglePage(request))
|
||||
.isInstanceOf(IOException.class);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user