Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eb2c18325b |
@@ -1,70 +0,0 @@
|
||||
# Audit: feat/jpdfium-overlay
|
||||
|
||||
Verifies whether merging `feat/jpdfium-overlay` over `feat/jpdfium-integration` actually wins anything.
|
||||
|
||||
## What the branch actually does
|
||||
|
||||
Sole code commit: `fbadcda74 impl migration to pdfium for overlay and watermark and pagenumbers`. Net diff vs `feat/jpdfium-integration` is 5 files, +212 / -2.
|
||||
|
||||
For each of the three controllers the change is identical in shape:
|
||||
|
||||
```java
|
||||
// JPDFium pre-validate catches corrupt PDFs cheaply before PDFBox does ...
|
||||
try (PdfDocument ignored = PdfDocument.open(pdfFile.getBytes())) {
|
||||
} catch (Exception e) {
|
||||
log.debug("JPDFium pre-validate failed; proceeding with PDFBox: {}", e.getMessage());
|
||||
}
|
||||
|
||||
try (PDDocument document = pdfDocumentFactory.load(pdfFile)) {
|
||||
// ... all real overlay work done by PDFBox, exactly as before
|
||||
}
|
||||
```
|
||||
|
||||
There is no JPDFium overlay being performed. PDFBox still does 100% of:
|
||||
- watermark tiled overlay (rows x cols, rotated, with non-Latin TTFs)
|
||||
- 9-position page numbers with `{n}` / `{total}` / `{filename}` templating
|
||||
- multi-line rotated stamps with rich `@var` templating
|
||||
|
||||
## Does the probe earn its keep?
|
||||
|
||||
No, on three counts:
|
||||
|
||||
1. **It does not short-circuit.** Catch swallows everything to DEBUG and falls through to PDFBox. A corrupt PDF still hits the full PDFBox parse path. The probe only adds work; it never saves any.
|
||||
2. **It costs more than it saves on the hot path.** `PdfDocument.open(byte[])` does `MultipartFile.getBytes()` (full heap copy) + `JpdfiumLib.docOpenBytes` which `Arena.allocateFrom(JAVA_BYTE, data)` copies the entire payload into native memory, parses xref, then frees. For files > 10 MB `CustomPDFDocumentFactory.load(MultipartFile)` is normally stream-to-temp; the probe forces a full byte[] read in addition. Net: one extra heap-sized copy, one native copy, one PDFium parse, all discarded.
|
||||
3. **No new corrupt-input test exercises the path.** `WatermarkControllerTest.OracleTests` and `PageNumbersControllerTest` only assert structure preservation on valid input. Probe is dead instrumentation as committed.
|
||||
|
||||
## JPDFium 1.0.x feature gaps that blocked real migration
|
||||
|
||||
Confirmed against `release/1.0.1` of JPDFium-pub.
|
||||
|
||||
| # | Gap | Where | Blocks |
|
||||
|---|-----|-------|--------|
|
||||
| 1 | `WatermarkApplier` is single-placement only; no row x col tiling. | `WatermarkApplier.applyTextWatermark` / `applyImageWatermark` | WatermarkController tiled overlay |
|
||||
| 2 | High-level `WatermarkApplier` / `HeaderFooterApplier` only accept the built-in `FontName` enum. `PdfPageEditor.loadFont(byte[], type, cid)` exists but is not plumbed through. | `WatermarkApplier` line 72, `HeaderFooterApplier` line 103 | Non-Latin scripts (Arabic, JP, KR, ZH, Thai) |
|
||||
| 3 | `HeaderFooterApplier.apply` hard-codes `size.width() / 2f` for both header and footer x. No 9-position grid. | `HeaderFooterApplier.apply` lines 49-56 | PageNumbersController 1..9 layout |
|
||||
| 4 | `expandTemplate` only knows `{page}`, `{pages}`, `{date}`. | `HeaderFooterApplier.expandTemplate` line 93 | PageNumbers `{filename}`, Stamp `@filename`/`@author`/`@title`/`@uuid`/`@date{fmt}` etc. |
|
||||
| 5 | `PdfAnnotationBuilder` takes a single `contents` string and a `Rect`; no multi-line layout, no per-line newline split, no rotated text-object matrices on the page content stream. | `PdfAnnotationBuilder.build` lines 130-137 | StampController `addTextStamp` multi-line + rotation |
|
||||
| 6 | No image stamp/watermark with explicit width/height + rotation matrix; `WatermarkApplier.applyImageWatermark` forces `targetW = pageW * 0.3f` and never rotates. | `WatermarkApplier` lines 113-136 | Stamp/Watermark image variants |
|
||||
|
||||
Each maps to a one-line upstream feature request below.
|
||||
|
||||
## Upstream feature requests for JPDFium 1.0.2
|
||||
|
||||
1. `WatermarkApplier`: add `tileRows`/`tileCols` plus `widthSpacer`/`heightSpacer` builder fields and emit a row x col grid in `applyToPage`.
|
||||
2. `Watermark`/`HeaderFooter` builders: accept `byte[] ttfFontData` (forward to `PdfPageEditor.loadFont` + use the returned `FPDF_FONT` in `createTextObject`).
|
||||
3. `HeaderFooterApplier`: add `Position` enum (9-grid) for header and footer independently; replace the hard-coded `width/2f` x with `WatermarkApplier.computePosition`.
|
||||
4. `HeaderFooter.expandTemplate`: add `{filename}`, `{author}`, `{title}`, `{subject}`, `{uuid}`, `{date:format}` and an `@var` alias set.
|
||||
5. `PdfAnnotationBuilder` (or a new `TextStampApplier`): accept `List<String> lines`, `float rotationDegrees`, line-height, and emit one transformed text object per line via `PdfPageEditor.createTextObject` + `transform`.
|
||||
6. `WatermarkApplier.applyImageWatermark`: take explicit `(width, height, rotation)` instead of fixed 30% width with no rotation.
|
||||
|
||||
## VERDICT
|
||||
|
||||
| Controller | Verdict | Reason |
|
||||
|------------|---------|--------|
|
||||
| WatermarkController | DROP | Probe is pure tax. Real migration blocked by gaps 1 + 2. |
|
||||
| PageNumbersController | DROP | Probe is pure tax. Real migration blocked by gaps 3 + 4. |
|
||||
| StampController | DROP | Probe is pure tax. Real migration blocked by gaps 4 + 5 + 6. |
|
||||
|
||||
## Bottom line
|
||||
|
||||
Drop the branch. The probe is a no-op that adds a full byte-copy + native parse to every overlay request and never short-circuits. Re-attempt overlay migration once JPDFium 1.0.2 lands feature requests 1-6, at which point a real migration (not a probe) is feasible.
|
||||
+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;
|
||||
|
||||
-11
@@ -21,7 +21,6 @@ import io.github.pixee.security.Filenames;
|
||||
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.SPDF.model.api.misc.AddPageNumbersRequest;
|
||||
@@ -33,10 +32,8 @@ 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;
|
||||
|
||||
@MiscApi
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
public class PageNumbersController {
|
||||
|
||||
@@ -83,14 +80,6 @@ public class PageNumbersController {
|
||||
}
|
||||
}
|
||||
|
||||
// JPDFium pre-validate catches corrupt PDFs cheaply before PDFBox runs.
|
||||
// Holdout: JPDFium HeaderFooter only places top-center / bottom-center; PDFBox handles 9
|
||||
// positions.
|
||||
try (PdfDocument ignored = PdfDocument.open(file.getBytes())) {
|
||||
} catch (Exception e) {
|
||||
log.debug("JPDFium pre-validate failed; proceeding with PDFBox: {}", e.getMessage());
|
||||
}
|
||||
|
||||
try (PDDocument document = pdfDocumentFactory.load(file)) {
|
||||
float marginFactor =
|
||||
switch (customMargin == null ? "" : customMargin.toLowerCase(Locale.ROOT)) {
|
||||
|
||||
+1
-10
@@ -42,7 +42,6 @@ import org.springframework.web.multipart.MultipartFile;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.SPDF.model.api.misc.AddStampRequest;
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
@@ -55,10 +54,8 @@ import stirling.software.common.util.RegexPatternUtils;
|
||||
import stirling.software.common.util.TempFile;
|
||||
import stirling.software.common.util.TempFileManager;
|
||||
import stirling.software.common.util.WebResponseUtils;
|
||||
import stirling.software.jpdfium.PdfDocument;
|
||||
|
||||
@MiscApi
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
public class StampController {
|
||||
|
||||
@@ -147,13 +144,7 @@ public class StampController {
|
||||
default -> 0.035f;
|
||||
};
|
||||
|
||||
// JPDFium pre-validate catches corrupt PDFs cheaply before PDFBox renders the stamp.
|
||||
// Holdout: PdfAnnotationBuilder lacks multi-line text + rotated text matrices used here.
|
||||
try (PdfDocument ignored = PdfDocument.open(pdfFile.getBytes())) {
|
||||
} catch (Exception e) {
|
||||
log.debug("JPDFium pre-validate failed; proceeding with PDFBox: {}", e.getMessage());
|
||||
}
|
||||
|
||||
// Load the input PDF
|
||||
try (PDDocument document = pdfDocumentFactory.load(pdfFile)) {
|
||||
|
||||
List<Integer> pageNumbers = request.getPageNumbersList(document, true);
|
||||
|
||||
+1
-10
@@ -37,7 +37,6 @@ import io.swagger.v3.oas.annotations.Operation;
|
||||
import jakarta.validation.Valid;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.SPDF.config.swagger.StandardPdfResponse;
|
||||
import stirling.software.SPDF.model.api.security.AddWatermarkRequest;
|
||||
@@ -50,10 +49,8 @@ import stirling.software.common.util.PdfUtils;
|
||||
import stirling.software.common.util.RegexPatternUtils;
|
||||
import stirling.software.common.util.TempFileManager;
|
||||
import stirling.software.common.util.WebResponseUtils;
|
||||
import stirling.software.jpdfium.PdfDocument;
|
||||
|
||||
@SecurityApi
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
public class WatermarkController {
|
||||
|
||||
@@ -110,13 +107,7 @@ public class WatermarkController {
|
||||
String customColor = request.getCustomColor();
|
||||
boolean convertPdfToImage = Boolean.TRUE.equals(request.getConvertPDFToImage());
|
||||
|
||||
// JPDFium pre-validate catches corrupt PDFs cheaply before PDFBox does the tiled overlay.
|
||||
// Holdout: JPDFium WatermarkApplier supports only single-position; PDFBox handles tiling.
|
||||
try (PdfDocument ignored = PdfDocument.open(pdfFile.getBytes())) {
|
||||
} catch (Exception e) {
|
||||
log.debug("JPDFium pre-validate failed; proceeding with PDFBox: {}", e.getMessage());
|
||||
}
|
||||
|
||||
// Load the input PDF with proper resource management
|
||||
try (PDDocument document = pdfDocumentFactory.load(pdfFile)) {
|
||||
|
||||
// Create a page in the document
|
||||
|
||||
+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);
|
||||
}
|
||||
}
|
||||
-120
@@ -1,120 +0,0 @@
|
||||
package stirling.software.SPDF.controller.api.misc;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
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.ByteArrayOutputStream;
|
||||
import java.io.File;
|
||||
import java.nio.file.Files;
|
||||
|
||||
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.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.junit.jupiter.MockitoExtension;
|
||||
import org.mockito.junit.jupiter.MockitoSettings;
|
||||
import org.mockito.quality.Strictness;
|
||||
import org.springframework.core.io.Resource;
|
||||
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.misc.AddPageNumbersRequest;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.util.TempFile;
|
||||
import stirling.software.common.util.TempFileManager;
|
||||
|
||||
@DisplayName("PageNumbersController Tests")
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
@MockitoSettings(strictness = Strictness.LENIENT)
|
||||
class PageNumbersControllerTest {
|
||||
|
||||
@Mock private CustomPDFDocumentFactory pdfDocumentFactory;
|
||||
@Mock private TempFileManager tempFileManager;
|
||||
|
||||
@InjectMocks private PageNumbersController pageNumbersController;
|
||||
|
||||
private byte[] multiPagePdf;
|
||||
|
||||
@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;
|
||||
});
|
||||
try (PDDocument doc = new PDDocument()) {
|
||||
for (int i = 0; i < 4; i++) {
|
||||
doc.addPage(new PDPage(PDRectangle.A4));
|
||||
}
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
doc.save(baos);
|
||||
multiPagePdf = baos.toByteArray();
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] drainBody(ResponseEntity<Resource> response) throws java.io.IOException {
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
try (java.io.InputStream in = response.getBody().getInputStream()) {
|
||||
in.transferTo(baos);
|
||||
}
|
||||
return baos.toByteArray();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Adds page numbers and preserves page count + MediaBox (oracle)")
|
||||
void testAddPageNumbers_Oracle() throws Exception {
|
||||
MockMultipartFile pdfFile =
|
||||
new MockMultipartFile(
|
||||
"fileInput", "doc.pdf", MediaType.APPLICATION_PDF_VALUE, multiPagePdf);
|
||||
|
||||
AddPageNumbersRequest request = new AddPageNumbersRequest();
|
||||
request.setFileInput(pdfFile);
|
||||
request.setCustomMargin("medium");
|
||||
request.setPosition(8);
|
||||
request.setStartingNumber(1);
|
||||
request.setPagesToNumber("all");
|
||||
request.setCustomText("{n}");
|
||||
request.setFontSize(12);
|
||||
request.setFontType("helvetica");
|
||||
request.setFontColor("#000000");
|
||||
|
||||
when(pdfDocumentFactory.load(any(MultipartFile.class)))
|
||||
.thenAnswer(inv -> Loader.loadPDF(multiPagePdf));
|
||||
|
||||
ResponseEntity<Resource> response = pageNumbersController.addPageNumbers(request);
|
||||
byte[] outBytes = drainBody(response);
|
||||
assertTrue(outBytes.length > 0, "output non-empty");
|
||||
|
||||
try (PDDocument out = Loader.loadPDF(outBytes)) {
|
||||
assertEquals(4, out.getNumberOfPages(), "page count unchanged");
|
||||
for (int i = 0; i < out.getNumberOfPages(); i++) {
|
||||
PDPage p = out.getPage(i);
|
||||
assertEquals(
|
||||
PDRectangle.A4.getWidth(),
|
||||
p.getMediaBox().getWidth(),
|
||||
0.01f,
|
||||
"MediaBox width preserved");
|
||||
assertTrue(p.getContentStreams().hasNext(), "page has content streams");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
-61
@@ -485,65 +485,4 @@ class WatermarkControllerTest {
|
||||
assertNotNull(response.getBody());
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@DisplayName("PDFBox Oracle Verification")
|
||||
class OracleTests {
|
||||
|
||||
@Test
|
||||
@DisplayName("Output preserves page count and MediaBox; content streams grow")
|
||||
void testOutput_PreservesStructure() throws Exception {
|
||||
byte[] multiPagePdf;
|
||||
try (PDDocument doc = new PDDocument()) {
|
||||
for (int i = 0; i < 3; i++) {
|
||||
doc.addPage(new PDPage(PDRectangle.A4));
|
||||
}
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
doc.save(baos);
|
||||
multiPagePdf = baos.toByteArray();
|
||||
}
|
||||
|
||||
MockMultipartFile pdfFile =
|
||||
new MockMultipartFile(
|
||||
"fileInput",
|
||||
"oracle.pdf",
|
||||
MediaType.APPLICATION_PDF_VALUE,
|
||||
multiPagePdf);
|
||||
|
||||
AddWatermarkRequest request = new AddWatermarkRequest();
|
||||
request.setFileInput(pdfFile);
|
||||
request.setWatermarkType("text");
|
||||
request.setWatermarkText("ORACLE");
|
||||
request.setAlphabet("roman");
|
||||
request.setFontSize(20);
|
||||
request.setRotation(0);
|
||||
request.setOpacity(0.5f);
|
||||
request.setWidthSpacer(50);
|
||||
request.setHeightSpacer(50);
|
||||
request.setCustomColor("#000000");
|
||||
request.setConvertPDFToImage(false);
|
||||
|
||||
when(pdfDocumentFactory.load(any(MultipartFile.class)))
|
||||
.thenAnswer(inv -> Loader.loadPDF(multiPagePdf));
|
||||
|
||||
byte[] outBytes = drainBody(watermarkController.addWatermark(request));
|
||||
try (PDDocument out = Loader.loadPDF(outBytes)) {
|
||||
assertEquals(3, out.getNumberOfPages(), "page count unchanged");
|
||||
for (int i = 0; i < out.getNumberOfPages(); i++) {
|
||||
PDPage p = out.getPage(i);
|
||||
assertEquals(
|
||||
PDRectangle.A4.getWidth(),
|
||||
p.getMediaBox().getWidth(),
|
||||
0.01f,
|
||||
"MediaBox width preserved");
|
||||
assertEquals(
|
||||
PDRectangle.A4.getHeight(),
|
||||
p.getMediaBox().getHeight(),
|
||||
0.01f,
|
||||
"MediaBox height preserved");
|
||||
assertTrue(p.getContentStreams().hasNext(), "page has content streams");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user