impl migration to pdfium for crop and scale
This commit is contained in:
@@ -1,21 +1,15 @@
|
||||
package stirling.software.SPDF.controller.api;
|
||||
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
|
||||
import org.apache.pdfbox.multipdf.LayerUtility;
|
||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||
import org.apache.pdfbox.pdmodel.PDPage;
|
||||
import org.apache.pdfbox.pdmodel.PDPageContentStream;
|
||||
import org.apache.pdfbox.pdmodel.PDPageContentStream.AppendMode;
|
||||
import org.apache.pdfbox.pdmodel.common.PDRectangle;
|
||||
import org.apache.pdfbox.pdmodel.graphics.form.PDFormXObject;
|
||||
import org.apache.pdfbox.rendering.PDFRenderer;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
|
||||
@@ -27,13 +21,19 @@ import stirling.software.SPDF.model.api.general.CropPdfForm;
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
import stirling.software.common.annotations.api.GeneralApi;
|
||||
import stirling.software.common.enumeration.ResourceWeight;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.util.ExceptionUtils;
|
||||
import stirling.software.common.util.GeneralUtils;
|
||||
import stirling.software.common.util.ProcessExecutor;
|
||||
import stirling.software.common.util.TempFile;
|
||||
import stirling.software.common.util.TempFileManager;
|
||||
import stirling.software.common.util.WebResponseUtils;
|
||||
import stirling.software.jpdfium.PdfDocument;
|
||||
import stirling.software.jpdfium.PdfImageConverter;
|
||||
import stirling.software.jpdfium.PdfPage;
|
||||
import stirling.software.jpdfium.doc.PdfMarginAdjuster;
|
||||
import stirling.software.jpdfium.model.PageSize;
|
||||
import stirling.software.jpdfium.model.Rect;
|
||||
import stirling.software.jpdfium.transform.PdfPageBoxes;
|
||||
|
||||
@GeneralApi
|
||||
@RequiredArgsConstructor
|
||||
@@ -42,18 +42,15 @@ public class CropController {
|
||||
|
||||
private static final int DEFAULT_RENDER_DPI = 150;
|
||||
private static final int WHITE_THRESHOLD = 250;
|
||||
private static final String TEMP_INPUT_PREFIX = "crop_input";
|
||||
private static final String TEMP_OUTPUT_PREFIX = "crop_output";
|
||||
private static final String PDF_EXTENSION = ".pdf";
|
||||
|
||||
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
||||
private final TempFileManager tempFileManager;
|
||||
private final EndpointConfiguration endpointConfiguration;
|
||||
|
||||
private static int[] detectContentBounds(BufferedImage image) {
|
||||
int width = image.getWidth();
|
||||
int height = image.getHeight();
|
||||
|
||||
// Early exit if image is too small
|
||||
if (width < 1 || height < 1) {
|
||||
return new int[] {0, 0, width - 1, height - 1};
|
||||
}
|
||||
@@ -109,8 +106,7 @@ public class CropController {
|
||||
}
|
||||
}
|
||||
|
||||
// Return bounds in format: [left, bottom, right, top]
|
||||
// Note: Image coordinates are top-down, PDF coordinates are bottom-up
|
||||
// Image coordinates top-down, PDF coordinates bottom-up
|
||||
return new int[] {left, height - bottom - 1, right, height - top - 1};
|
||||
}
|
||||
|
||||
@@ -121,8 +117,6 @@ public class CropController {
|
||||
return r >= threshold && g >= threshold && b >= threshold;
|
||||
}
|
||||
|
||||
private final EndpointConfiguration endpointConfiguration;
|
||||
|
||||
private boolean isGhostscriptEnabled() {
|
||||
return endpointConfiguration.isGroupEnabled("Ghostscript");
|
||||
}
|
||||
@@ -152,145 +146,124 @@ public class CropController {
|
||||
|
||||
if (request.isRemoveDataOutsideCrop() && isGhostscriptEnabled()) {
|
||||
return cropWithGhostscript(request);
|
||||
} else {
|
||||
return cropWithPDFBox(request);
|
||||
}
|
||||
return cropWithJpdfium(request);
|
||||
}
|
||||
|
||||
private ResponseEntity<Resource> cropWithAutomaticDetection(@ModelAttribute CropPdfForm request)
|
||||
private ResponseEntity<Resource> cropWithAutomaticDetection(CropPdfForm request)
|
||||
throws IOException {
|
||||
try (PDDocument sourceDocument = pdfDocumentFactory.load(request)) {
|
||||
MultipartFile fileInput = request.getFileInput();
|
||||
File inputFile = tempFileManager.convertMultipartFileToFile(fileInput);
|
||||
try {
|
||||
try (PdfDocument doc = PdfDocument.open(inputFile.toPath())) {
|
||||
int pageCount = doc.pageCount();
|
||||
for (int i = 0; i < pageCount; i++) {
|
||||
BufferedImage image = PdfImageConverter.pageToImage(doc, i, DEFAULT_RENDER_DPI);
|
||||
|
||||
try (PDDocument newDocument =
|
||||
pdfDocumentFactory.createNewDocumentBasedOnOldDocument(sourceDocument)) {
|
||||
PDFRenderer renderer = new PDFRenderer(sourceDocument);
|
||||
renderer.setSubsamplingAllowed(true); // Enable subsampling to reduce memory usage
|
||||
LayerUtility layerUtility = new LayerUtility(newDocument);
|
||||
PageSize size;
|
||||
try (PdfPage page = doc.page(i)) {
|
||||
size = page.size();
|
||||
}
|
||||
|
||||
for (int i = 0; i < sourceDocument.getNumberOfPages(); i++) {
|
||||
PDPage sourcePage = sourceDocument.getPage(i);
|
||||
PDRectangle mediaBox = sourcePage.getMediaBox();
|
||||
|
||||
BufferedImage image = renderer.renderImageWithDPI(i, DEFAULT_RENDER_DPI);
|
||||
int[] bounds = detectContentBounds(image);
|
||||
|
||||
float scaleX = mediaBox.getWidth() / image.getWidth();
|
||||
float scaleY = mediaBox.getHeight() / image.getHeight();
|
||||
float scaleX = size.width() / image.getWidth();
|
||||
float scaleY = size.height() / image.getHeight();
|
||||
|
||||
CropBounds cropBounds = CropBounds.fromPixels(bounds, scaleX, scaleY);
|
||||
|
||||
PDPage newPage = new PDPage(mediaBox);
|
||||
newDocument.addPage(newPage);
|
||||
try (PDPageContentStream contentStream =
|
||||
new PDPageContentStream(
|
||||
newDocument, newPage, AppendMode.OVERWRITE, true, true)) {
|
||||
PDFormXObject formXObject =
|
||||
layerUtility.importPageAsForm(sourceDocument, i);
|
||||
contentStream.saveGraphicsState();
|
||||
contentStream.addRect(
|
||||
cropBounds.x, cropBounds.y, cropBounds.width, cropBounds.height);
|
||||
contentStream.clip();
|
||||
contentStream.drawForm(formXObject);
|
||||
contentStream.restoreGraphicsState();
|
||||
}
|
||||
|
||||
newPage.setMediaBox(
|
||||
new PDRectangle(
|
||||
cropBounds.x,
|
||||
cropBounds.y,
|
||||
cropBounds.width,
|
||||
cropBounds.height));
|
||||
applyCropMargins(doc, i, size, cropBounds);
|
||||
}
|
||||
|
||||
return WebResponseUtils.pdfDocToWebResponse(
|
||||
newDocument,
|
||||
TempFile outputTempFile = new TempFile(tempFileManager, PDF_EXTENSION);
|
||||
try {
|
||||
doc.save(outputTempFile.getPath());
|
||||
} catch (Exception e) {
|
||||
outputTempFile.close();
|
||||
throw e;
|
||||
}
|
||||
return WebResponseUtils.pdfFileToWebResponse(
|
||||
outputTempFile,
|
||||
GeneralUtils.generateFilename(
|
||||
request.getFileInput().getOriginalFilename(), "_cropped.pdf"),
|
||||
tempFileManager);
|
||||
fileInput.getOriginalFilename(), "_cropped.pdf"));
|
||||
}
|
||||
} finally {
|
||||
tempFileManager.deleteTempFile(inputFile);
|
||||
}
|
||||
}
|
||||
|
||||
private ResponseEntity<Resource> cropWithPDFBox(@ModelAttribute CropPdfForm request)
|
||||
throws IOException {
|
||||
try (PDDocument sourceDocument = pdfDocumentFactory.load(request)) {
|
||||
|
||||
try (PDDocument newDocument =
|
||||
pdfDocumentFactory.createNewDocumentBasedOnOldDocument(sourceDocument)) {
|
||||
int totalPages = sourceDocument.getNumberOfPages();
|
||||
LayerUtility layerUtility = new LayerUtility(newDocument);
|
||||
|
||||
for (int i = 0; i < totalPages; i++) {
|
||||
PDPage sourcePage = sourceDocument.getPage(i);
|
||||
|
||||
// Create a new page with the size of the source page
|
||||
PDPage newPage = new PDPage(sourcePage.getMediaBox());
|
||||
newDocument.addPage(newPage);
|
||||
try (PDPageContentStream contentStream =
|
||||
new PDPageContentStream(
|
||||
newDocument, newPage, AppendMode.OVERWRITE, true, true)) {
|
||||
// Import the source page as a form XObject
|
||||
PDFormXObject formXObject =
|
||||
layerUtility.importPageAsForm(sourceDocument, i);
|
||||
|
||||
contentStream.saveGraphicsState();
|
||||
|
||||
// Define the crop area
|
||||
contentStream.addRect(
|
||||
private ResponseEntity<Resource> cropWithJpdfium(CropPdfForm request) throws IOException {
|
||||
MultipartFile fileInput = request.getFileInput();
|
||||
File inputFile = tempFileManager.convertMultipartFileToFile(fileInput);
|
||||
try {
|
||||
try (PdfDocument doc = PdfDocument.open(inputFile.toPath())) {
|
||||
int pageCount = doc.pageCount();
|
||||
CropBounds bounds =
|
||||
new CropBounds(
|
||||
request.getX(),
|
||||
request.getY(),
|
||||
request.getWidth(),
|
||||
request.getHeight());
|
||||
contentStream.clip();
|
||||
|
||||
// Draw the entire formXObject
|
||||
contentStream.drawForm(formXObject);
|
||||
|
||||
contentStream.restoreGraphicsState();
|
||||
for (int i = 0; i < pageCount; i++) {
|
||||
PageSize size;
|
||||
try (PdfPage page = doc.page(i)) {
|
||||
size = page.size();
|
||||
}
|
||||
|
||||
// Now, set the new page's media box to the cropped size
|
||||
newPage.setMediaBox(
|
||||
new PDRectangle(
|
||||
request.getX(),
|
||||
request.getY(),
|
||||
request.getWidth(),
|
||||
request.getHeight()));
|
||||
applyCropMargins(doc, i, size, bounds);
|
||||
}
|
||||
|
||||
return WebResponseUtils.pdfDocToWebResponse(
|
||||
newDocument,
|
||||
TempFile outputTempFile = new TempFile(tempFileManager, PDF_EXTENSION);
|
||||
try {
|
||||
doc.save(outputTempFile.getPath());
|
||||
} catch (Exception e) {
|
||||
outputTempFile.close();
|
||||
throw e;
|
||||
}
|
||||
return WebResponseUtils.pdfFileToWebResponse(
|
||||
outputTempFile,
|
||||
GeneralUtils.generateFilename(
|
||||
request.getFileInput().getOriginalFilename(), "_cropped.pdf"),
|
||||
tempFileManager);
|
||||
fileInput.getOriginalFilename(), "_cropped.pdf"));
|
||||
}
|
||||
} finally {
|
||||
tempFileManager.deleteTempFile(inputFile);
|
||||
}
|
||||
}
|
||||
|
||||
private ResponseEntity<Resource> cropWithGhostscript(@ModelAttribute CropPdfForm request)
|
||||
throws IOException {
|
||||
// PdfMarginAdjuster.addMargins(doc, page, left, bottom, right, top): translates
|
||||
// content by (left, bottom), grows page by left+right and bottom+top.
|
||||
// To crop to (x, y, w, h) shift content to (-x, -y) and end at (w, h).
|
||||
private static void applyCropMargins(
|
||||
PdfDocument doc, int pageIndex, PageSize size, CropBounds bounds) {
|
||||
float left = -bounds.x();
|
||||
float bottom = -bounds.y();
|
||||
float right = bounds.width() - size.width() + bounds.x();
|
||||
float top = bounds.height() - size.height() + bounds.y();
|
||||
PdfMarginAdjuster.addMargins(doc, pageIndex, left, bottom, right, top);
|
||||
}
|
||||
|
||||
private ResponseEntity<Resource> cropWithGhostscript(CropPdfForm request) throws IOException {
|
||||
MultipartFile fileInput = request.getFileInput();
|
||||
File inputFile = tempFileManager.convertMultipartFileToFile(fileInput);
|
||||
TempFile tempInputFile = null;
|
||||
TempFile tempOutputFile = null;
|
||||
try {
|
||||
tempInputFile = new TempFile(tempFileManager, PDF_EXTENSION);
|
||||
tempOutputFile = new TempFile(tempFileManager, PDF_EXTENSION);
|
||||
|
||||
try (PDDocument sourceDocument = pdfDocumentFactory.load(request)) {
|
||||
for (int i = 0; i < sourceDocument.getNumberOfPages(); i++) {
|
||||
PDPage page = sourceDocument.getPage(i);
|
||||
PDRectangle cropBox =
|
||||
new PDRectangle(
|
||||
try (PdfDocument doc = PdfDocument.open(inputFile.toPath())) {
|
||||
Rect cropRect =
|
||||
Rect.of(
|
||||
request.getX(),
|
||||
request.getY(),
|
||||
request.getWidth(),
|
||||
request.getHeight());
|
||||
page.setCropBox(cropBox);
|
||||
int pageCount = doc.pageCount();
|
||||
for (int i = 0; i < pageCount; i++) {
|
||||
try (PdfPage page = doc.page(i)) {
|
||||
PdfPageBoxes.setCropBox(page.rawHandle(), cropRect);
|
||||
}
|
||||
}
|
||||
doc.save(tempInputFile.getPath());
|
||||
}
|
||||
|
||||
tempInputFile = tempFileManager.createManagedTempFile(PDF_EXTENSION);
|
||||
tempOutputFile = tempFileManager.createManagedTempFile(PDF_EXTENSION);
|
||||
|
||||
// Save the source document with crop boxes
|
||||
sourceDocument.save(tempInputFile.getFile());
|
||||
|
||||
// Execute Ghostscript to process the crop boxes
|
||||
ProcessExecutor processExecutor =
|
||||
ProcessExecutor.getInstance(ProcessExecutor.Processes.GHOSTSCRIPT);
|
||||
List<String> command =
|
||||
@@ -301,16 +274,13 @@ public class CropController {
|
||||
"-o",
|
||||
tempOutputFile.getAbsolutePath(),
|
||||
tempInputFile.getAbsolutePath());
|
||||
|
||||
processExecutor.runCommandWithOutputHandling(command);
|
||||
|
||||
TempFile out = tempOutputFile;
|
||||
tempOutputFile = null; // ownership transferred to response Resource
|
||||
tempOutputFile = null;
|
||||
return WebResponseUtils.pdfFileToWebResponse(
|
||||
out,
|
||||
GeneralUtils.generateFilename(
|
||||
request.getFileInput().getOriginalFilename(), "_cropped.pdf"));
|
||||
|
||||
GeneralUtils.generateFilename(fileInput.getOriginalFilename(), "_cropped.pdf"));
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
throw ExceptionUtils.createProcessingInterruptedException("Ghostscript", e);
|
||||
@@ -321,6 +291,7 @@ public class CropController {
|
||||
if (tempOutputFile != null) {
|
||||
tempOutputFile.close();
|
||||
}
|
||||
tempFileManager.deleteTempFile(inputFile);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+95
-111
@@ -1,16 +1,10 @@
|
||||
package stirling.software.SPDF.controller.api;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.apache.pdfbox.multipdf.LayerUtility;
|
||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||
import org.apache.pdfbox.pdmodel.PDPage;
|
||||
import org.apache.pdfbox.pdmodel.PDPageContentStream;
|
||||
import org.apache.pdfbox.pdmodel.common.PDRectangle;
|
||||
import org.apache.pdfbox.pdmodel.graphics.form.PDFormXObject;
|
||||
import org.apache.pdfbox.util.Matrix;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
@@ -26,91 +20,88 @@ import stirling.software.SPDF.model.api.general.ScalePagesRequest;
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
import stirling.software.common.annotations.api.GeneralApi;
|
||||
import stirling.software.common.enumeration.ResourceWeight;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.util.ExceptionUtils;
|
||||
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;
|
||||
import stirling.software.jpdfium.PdfPage;
|
||||
import stirling.software.jpdfium.doc.PdfPageScaler;
|
||||
import stirling.software.jpdfium.doc.PdfPosterizer.PaperSize;
|
||||
import stirling.software.jpdfium.model.PageSize;
|
||||
import stirling.software.jpdfium.model.Rect;
|
||||
import stirling.software.jpdfium.transform.PdfPageBoxes;
|
||||
|
||||
@GeneralApi
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
public class ScalePagesController {
|
||||
|
||||
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
||||
// PDFBox PDRectangle dimensions (width, height) in points, inlined so the
|
||||
// controller has no remaining PDFBox dependency.
|
||||
private static final float A0_W = 2383.937f;
|
||||
private static final float A0_H = 3370.3937f;
|
||||
private static final float A1_W = 1683.7795f;
|
||||
private static final float A1_H = 2383.937f;
|
||||
private static final float A2_W = 1190.5511f;
|
||||
private static final float A2_H = 1683.7795f;
|
||||
private static final float A3_W = 841.8898f;
|
||||
private static final float A3_H = 1190.5511f;
|
||||
private static final float A4_W = 595.27563f;
|
||||
private static final float A4_H = 841.8898f;
|
||||
private static final float A5_W = 419.52756f;
|
||||
private static final float A5_H = 595.27563f;
|
||||
private static final float A6_W = 297.63782f;
|
||||
private static final float A6_H = 419.52756f;
|
||||
private static final float LETTER_W = 612f;
|
||||
private static final float LETTER_H = 792f;
|
||||
private static final float LEGAL_W = 612f;
|
||||
private static final float LEGAL_H = 1008f;
|
||||
|
||||
private final TempFileManager tempFileManager;
|
||||
|
||||
private static PDRectangle getTargetSize(String targetPDRectangle, PDDocument sourceDocument) {
|
||||
if ("KEEP".equals(targetPDRectangle)) {
|
||||
if (sourceDocument.getNumberOfPages() == 0) {
|
||||
private static PageSize getTargetSize(String name, PdfDocument sourceDocument) {
|
||||
if ("KEEP".equals(name)) {
|
||||
if (sourceDocument.pageCount() == 0) {
|
||||
throw ExceptionUtils.createInvalidPageSizeException("KEEP");
|
||||
}
|
||||
|
||||
PDPage sourcePage = sourceDocument.getPage(0);
|
||||
PDRectangle sourceSize = sourcePage.getMediaBox();
|
||||
|
||||
if (sourceSize == null) {
|
||||
throw ExceptionUtils.createInvalidPageSizeException("KEEP");
|
||||
try (PdfPage first = sourceDocument.page(0)) {
|
||||
return first.size();
|
||||
}
|
||||
|
||||
return sourceSize;
|
||||
}
|
||||
|
||||
Map<String, PDRectangle> sizeMap = getSizeMap();
|
||||
|
||||
if (sizeMap.containsKey(targetPDRectangle)) {
|
||||
return sizeMap.get(targetPDRectangle);
|
||||
Map<String, PageSize> sizeMap = getSizeMap();
|
||||
PageSize size = sizeMap.get(name);
|
||||
if (size == null) {
|
||||
throw ExceptionUtils.createInvalidPageSizeException(name);
|
||||
}
|
||||
|
||||
throw ExceptionUtils.createInvalidPageSizeException(targetPDRectangle);
|
||||
return size;
|
||||
}
|
||||
|
||||
private static Map<String, PDRectangle> getSizeMap() {
|
||||
Map<String, PDRectangle> sizeMap = new HashMap<>();
|
||||
private static Map<String, PageSize> getSizeMap() {
|
||||
Map<String, PageSize> sizeMap = new HashMap<>();
|
||||
|
||||
// Portrait sizes (A0-A6)
|
||||
sizeMap.put("A0", PDRectangle.A0);
|
||||
sizeMap.put("A1", PDRectangle.A1);
|
||||
sizeMap.put("A2", PDRectangle.A2);
|
||||
sizeMap.put("A3", PDRectangle.A3);
|
||||
sizeMap.put("A4", PDRectangle.A4);
|
||||
sizeMap.put("A5", PDRectangle.A5);
|
||||
sizeMap.put("A6", PDRectangle.A6);
|
||||
sizeMap.put("A0", new PageSize(A0_W, A0_H));
|
||||
sizeMap.put("A1", new PageSize(A1_W, A1_H));
|
||||
sizeMap.put("A2", new PageSize(A2_W, A2_H));
|
||||
sizeMap.put("A3", new PageSize(A3_W, A3_H));
|
||||
sizeMap.put("A4", new PageSize(A4_W, A4_H));
|
||||
sizeMap.put("A5", new PageSize(A5_W, A5_H));
|
||||
sizeMap.put("A6", new PageSize(A6_W, A6_H));
|
||||
|
||||
// Landscape sizes (A0-A6)
|
||||
sizeMap.put(
|
||||
"A0_LANDSCAPE",
|
||||
new PDRectangle(PDRectangle.A0.getHeight(), PDRectangle.A0.getWidth()));
|
||||
sizeMap.put(
|
||||
"A1_LANDSCAPE",
|
||||
new PDRectangle(PDRectangle.A1.getHeight(), PDRectangle.A1.getWidth()));
|
||||
sizeMap.put(
|
||||
"A2_LANDSCAPE",
|
||||
new PDRectangle(PDRectangle.A2.getHeight(), PDRectangle.A2.getWidth()));
|
||||
sizeMap.put(
|
||||
"A3_LANDSCAPE",
|
||||
new PDRectangle(PDRectangle.A3.getHeight(), PDRectangle.A3.getWidth()));
|
||||
sizeMap.put(
|
||||
"A4_LANDSCAPE",
|
||||
new PDRectangle(PDRectangle.A4.getHeight(), PDRectangle.A4.getWidth()));
|
||||
sizeMap.put(
|
||||
"A5_LANDSCAPE",
|
||||
new PDRectangle(PDRectangle.A5.getHeight(), PDRectangle.A5.getWidth()));
|
||||
sizeMap.put(
|
||||
"A6_LANDSCAPE",
|
||||
new PDRectangle(PDRectangle.A6.getHeight(), PDRectangle.A6.getWidth()));
|
||||
sizeMap.put("A0_LANDSCAPE", new PageSize(A0_H, A0_W));
|
||||
sizeMap.put("A1_LANDSCAPE", new PageSize(A1_H, A1_W));
|
||||
sizeMap.put("A2_LANDSCAPE", new PageSize(A2_H, A2_W));
|
||||
sizeMap.put("A3_LANDSCAPE", new PageSize(A3_H, A3_W));
|
||||
sizeMap.put("A4_LANDSCAPE", new PageSize(A4_H, A4_W));
|
||||
sizeMap.put("A5_LANDSCAPE", new PageSize(A5_H, A5_W));
|
||||
sizeMap.put("A6_LANDSCAPE", new PageSize(A6_H, A6_W));
|
||||
|
||||
// Portrait US sizes
|
||||
sizeMap.put("LETTER", PDRectangle.LETTER);
|
||||
sizeMap.put("LEGAL", PDRectangle.LEGAL);
|
||||
sizeMap.put("LETTER", new PageSize(LETTER_W, LETTER_H));
|
||||
sizeMap.put("LEGAL", new PageSize(LEGAL_W, LEGAL_H));
|
||||
|
||||
// Landscape US sizes
|
||||
sizeMap.put(
|
||||
"LETTER_LANDSCAPE",
|
||||
new PDRectangle(PDRectangle.LETTER.getHeight(), PDRectangle.LETTER.getWidth()));
|
||||
sizeMap.put(
|
||||
"LEGAL_LANDSCAPE",
|
||||
new PDRectangle(PDRectangle.LEGAL.getHeight(), PDRectangle.LEGAL.getWidth()));
|
||||
sizeMap.put("LETTER_LANDSCAPE", new PageSize(LETTER_H, LETTER_W));
|
||||
sizeMap.put("LEGAL_LANDSCAPE", new PageSize(LEGAL_H, LEGAL_W));
|
||||
|
||||
return sizeMap;
|
||||
}
|
||||
@@ -130,53 +121,46 @@ public class ScalePagesController {
|
||||
String targetPDRectangle = request.getPageSize();
|
||||
float scaleFactor = request.getScaleFactor();
|
||||
|
||||
try (PDDocument sourceDocument = pdfDocumentFactory.load(file);
|
||||
PDDocument outputDocument =
|
||||
pdfDocumentFactory.createNewDocumentBasedOnOldDocument(sourceDocument)) {
|
||||
File inputFile = tempFileManager.convertMultipartFileToFile(file);
|
||||
try {
|
||||
try (PdfDocument doc = PdfDocument.open(inputFile.toPath())) {
|
||||
PageSize targetSize = getTargetSize(targetPDRectangle, doc);
|
||||
float targetW = targetSize.width();
|
||||
float targetH = targetSize.height();
|
||||
|
||||
PDRectangle targetSize = getTargetSize(targetPDRectangle, sourceDocument);
|
||||
// Build a "virtual" paper so PdfPageScaler.scale produces base*scaleFactor
|
||||
// scaling, then restore the real MediaBox/CropBox to the requested target
|
||||
// size with the content centered.
|
||||
float virtualW = targetW * scaleFactor;
|
||||
float virtualH = targetH * scaleFactor;
|
||||
PaperSize virtualPaper = new PaperSize(virtualW, virtualH, "virtual");
|
||||
|
||||
// Create LayerUtility once outside the loop for better performance
|
||||
LayerUtility layerUtility = new LayerUtility(outputDocument);
|
||||
int pageCount = doc.pageCount();
|
||||
for (int i = 0; i < pageCount; i++) {
|
||||
PdfPageScaler.scale(doc, i, virtualPaper, PdfPageScaler.FitMode.FIT_PAGE);
|
||||
|
||||
int totalPages = sourceDocument.getNumberOfPages();
|
||||
for (int i = 0; i < totalPages; i++) {
|
||||
PDPage sourcePage = sourceDocument.getPage(i);
|
||||
PDRectangle sourceSize = sourcePage.getMediaBox();
|
||||
|
||||
float scaleWidth = targetSize.getWidth() / sourceSize.getWidth();
|
||||
float scaleHeight = targetSize.getHeight() / sourceSize.getHeight();
|
||||
float scale = Math.min(scaleWidth, scaleHeight) * scaleFactor;
|
||||
|
||||
PDPage newPage = new PDPage(targetSize);
|
||||
outputDocument.addPage(newPage);
|
||||
|
||||
try (PDPageContentStream contentStream =
|
||||
new PDPageContentStream(
|
||||
outputDocument,
|
||||
newPage,
|
||||
PDPageContentStream.AppendMode.APPEND,
|
||||
true,
|
||||
true)) {
|
||||
|
||||
float x = (targetSize.getWidth() - sourceSize.getWidth() * scale) / 2;
|
||||
float y = (targetSize.getHeight() - sourceSize.getHeight() * scale) / 2;
|
||||
|
||||
contentStream.saveGraphicsState();
|
||||
contentStream.transform(Matrix.getTranslateInstance(x, y));
|
||||
contentStream.transform(Matrix.getScaleInstance(scale, scale));
|
||||
|
||||
PDFormXObject form = layerUtility.importPageAsForm(sourceDocument, i);
|
||||
contentStream.drawForm(form);
|
||||
|
||||
contentStream.restoreGraphicsState();
|
||||
float offsetX = (virtualW - targetW) / 2f;
|
||||
float offsetY = (virtualH - targetH) / 2f;
|
||||
Rect mediaBox = Rect.of(offsetX, offsetY, targetW, targetH);
|
||||
try (PdfPage page = doc.page(i)) {
|
||||
PdfPageBoxes.setMediaBox(page.rawHandle(), mediaBox);
|
||||
PdfPageBoxes.setCropBox(page.rawHandle(), mediaBox);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return WebResponseUtils.pdfDocToWebResponse(
|
||||
outputDocument,
|
||||
GeneralUtils.generateFilename(file.getOriginalFilename(), "_scaled.pdf"),
|
||||
tempFileManager);
|
||||
TempFile outputTempFile = new TempFile(tempFileManager, ".pdf");
|
||||
try {
|
||||
doc.save(outputTempFile.getPath());
|
||||
} catch (Exception e) {
|
||||
outputTempFile.close();
|
||||
throw e;
|
||||
}
|
||||
return WebResponseUtils.pdfFileToWebResponse(
|
||||
outputTempFile,
|
||||
GeneralUtils.generateFilename(file.getOriginalFilename(), "_scaled.pdf"));
|
||||
}
|
||||
} finally {
|
||||
tempFileManager.deleteTempFile(inputFile);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+83
-223
@@ -1,6 +1,7 @@
|
||||
package stirling.software.SPDF.controller.api;
|
||||
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
@@ -27,53 +28,50 @@ import org.junit.jupiter.params.provider.ValueSource;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.core.io.ByteArrayResource;
|
||||
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.config.EndpointConfiguration;
|
||||
import stirling.software.SPDF.model.api.general.CropPdfForm;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.util.TempFile;
|
||||
import stirling.software.common.util.TempFileManager;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
@DisplayName("CropController Tests")
|
||||
class CropControllerTest {
|
||||
private static ResponseEntity<Resource> streamingOk(byte[] bytes) {
|
||||
return ResponseEntity.ok(new ByteArrayResource(bytes));
|
||||
}
|
||||
|
||||
private static byte[] drainBody(ResponseEntity<Resource> response) throws java.io.IOException {
|
||||
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);
|
||||
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;
|
||||
@Mock private EndpointConfiguration endpointConfiguration;
|
||||
@InjectMocks private CropController cropController;
|
||||
private TestPdfFactory pdfFactory;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() throws Exception {
|
||||
lenient()
|
||||
.when(tempFileManager.createManagedTempFile(anyString()))
|
||||
.when(tempFileManager.convertMultipartFileToFile(any(MultipartFile.class)))
|
||||
.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;
|
||||
MultipartFile mf = inv.getArgument(0);
|
||||
File f = Files.createTempFile("crop-in", ".pdf").toFile();
|
||||
Files.write(f.toPath(), mf.getBytes());
|
||||
return f;
|
||||
});
|
||||
lenient()
|
||||
.when(tempFileManager.createTempFile(anyString()))
|
||||
.thenAnswer(inv -> Files.createTempFile("crop-out", inv.getArgument(0)).toFile());
|
||||
lenient().when(endpointConfiguration.isGroupEnabled("Ghostscript")).thenReturn(false);
|
||||
pdfFactory = new TestPdfFactory();
|
||||
}
|
||||
|
||||
@@ -112,8 +110,10 @@ class CropControllerTest {
|
||||
private static final PDType1Font HELVETICA =
|
||||
new PDType1Font(Standard14Fonts.FontName.HELVETICA);
|
||||
|
||||
// PDFium TransFormWithClip rejects pages without content streams; always
|
||||
// add at least a sentinel character so the page has a /Contents entry.
|
||||
MockMultipartFile createStandardPdf(String filename) throws IOException {
|
||||
return createPdf(filename, PDRectangle.LETTER, null);
|
||||
return createPdf(filename, PDRectangle.LETTER, "X");
|
||||
}
|
||||
|
||||
MockMultipartFile createPdfWithContent(String filename, String content) throws IOException {
|
||||
@@ -121,30 +121,26 @@ class CropControllerTest {
|
||||
}
|
||||
|
||||
MockMultipartFile createPdfWithSize(String filename, PDRectangle size) throws IOException {
|
||||
return createPdf(filename, size, null);
|
||||
return createPdf(filename, size, "X");
|
||||
}
|
||||
|
||||
MockMultipartFile createPdf(String filename, PDRectangle pageSize, String content)
|
||||
throws IOException {
|
||||
Path testPdfPath = tempDir.resolve(filename);
|
||||
|
||||
try (PDDocument doc = new PDDocument()) {
|
||||
PDPage page = new PDPage(pageSize);
|
||||
doc.addPage(page);
|
||||
|
||||
if (content != null && !content.isEmpty()) {
|
||||
try (PDPageContentStream contentStream = new PDPageContentStream(doc, page)) {
|
||||
contentStream.beginText();
|
||||
contentStream.setFont(HELVETICA, 12);
|
||||
contentStream.newLineAtOffset(50, pageSize.getHeight() - 50);
|
||||
contentStream.showText(content);
|
||||
contentStream.endText();
|
||||
try (PDPageContentStream cs = new PDPageContentStream(doc, page)) {
|
||||
cs.beginText();
|
||||
cs.setFont(HELVETICA, 12);
|
||||
cs.newLineAtOffset(50, pageSize.getHeight() - 50);
|
||||
cs.showText(content);
|
||||
cs.endText();
|
||||
}
|
||||
}
|
||||
|
||||
doc.save(testPdfPath.toFile());
|
||||
}
|
||||
|
||||
return new MockMultipartFile(
|
||||
"fileInput",
|
||||
filename,
|
||||
@@ -156,26 +152,22 @@ class CropControllerTest {
|
||||
throws IOException {
|
||||
Path testPdfPath = tempDir.resolve(filename);
|
||||
PDRectangle pageSize = PDRectangle.LETTER;
|
||||
|
||||
try (PDDocument doc = new PDDocument()) {
|
||||
PDPage page = new PDPage(pageSize);
|
||||
doc.addPage(page);
|
||||
|
||||
if (content != null && !content.isEmpty()) {
|
||||
try (PDPageContentStream contentStream = new PDPageContentStream(doc, page)) {
|
||||
contentStream.beginText();
|
||||
contentStream.setFont(HELVETICA, 12);
|
||||
try (PDPageContentStream cs = new PDPageContentStream(doc, page)) {
|
||||
cs.beginText();
|
||||
cs.setFont(HELVETICA, 12);
|
||||
float x = pageSize.getWidth() / 2 - 50;
|
||||
float y = pageSize.getHeight() / 2;
|
||||
contentStream.newLineAtOffset(x, y);
|
||||
contentStream.showText(content);
|
||||
contentStream.endText();
|
||||
cs.newLineAtOffset(x, y);
|
||||
cs.showText(content);
|
||||
cs.endText();
|
||||
}
|
||||
}
|
||||
|
||||
doc.save(testPdfPath.toFile());
|
||||
}
|
||||
|
||||
return new MockMultipartFile(
|
||||
"fileInput",
|
||||
filename,
|
||||
@@ -185,43 +177,34 @@ class CropControllerTest {
|
||||
}
|
||||
|
||||
@Nested
|
||||
@DisplayName("Manual Crop with PDFBox")
|
||||
class ManualCropPDFBoxTests {
|
||||
@DisplayName("Manual Crop with JPDFium")
|
||||
@Tag("integration")
|
||||
class ManualCropTests {
|
||||
|
||||
@Test
|
||||
@DisplayName(
|
||||
"Should successfully crop PDF using PDFBox when removeDataOutsideCrop is false")
|
||||
void shouldCropPdfSuccessfullyWithPDFBox() throws IOException {
|
||||
@DisplayName("Should produce PDF with cropped MediaBox dimensions")
|
||||
void shouldCropPdfWithExpectedDimensions() throws IOException {
|
||||
MockMultipartFile testFile = pdfFactory.createStandardPdf("test.pdf");
|
||||
float w = 512f;
|
||||
float h = 692f;
|
||||
CropPdfForm request =
|
||||
new CropRequestBuilder()
|
||||
.withFile(testFile)
|
||||
.withCoordinates(50f, 50f, 512f, 692f)
|
||||
.withCoordinates(50f, 50f, w, h)
|
||||
.withRemoveDataOutsideCrop(false)
|
||||
.withAutoCrop(false)
|
||||
.build();
|
||||
|
||||
PDDocument mockDocument = mock(PDDocument.class);
|
||||
PDDocument newDocument = mock(PDDocument.class);
|
||||
when(pdfDocumentFactory.load(request)).thenReturn(mockDocument);
|
||||
when(pdfDocumentFactory.createNewDocumentBasedOnOldDocument(mockDocument))
|
||||
.thenReturn(newDocument);
|
||||
|
||||
ResponseEntity<Resource> response = cropController.cropPdf(request);
|
||||
|
||||
assertThat(response)
|
||||
.isNotNull()
|
||||
.extracting(ResponseEntity::getStatusCode, ResponseEntity::getBody)
|
||||
.satisfies(
|
||||
tuple -> {
|
||||
assertThat(tuple.get(0)).isEqualTo(HttpStatus.OK);
|
||||
assertThat(tuple.get(1)).isNotNull();
|
||||
});
|
||||
|
||||
verify(pdfDocumentFactory).load(request);
|
||||
verify(pdfDocumentFactory).createNewDocumentBasedOnOldDocument(mockDocument);
|
||||
verify(mockDocument, times(1)).close();
|
||||
verify(newDocument, times(1)).close();
|
||||
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
|
||||
byte[] out = drainBody(response);
|
||||
try (PDDocument result = Loader.loadPDF(out)) {
|
||||
assertThat(result.getNumberOfPages()).isEqualTo(1);
|
||||
PDRectangle mediaBox = result.getPage(0).getMediaBox();
|
||||
assertThat(mediaBox.getWidth()).isCloseTo(w, within(0.1f));
|
||||
assertThat(mediaBox.getHeight()).isCloseTo(h, within(0.1f));
|
||||
}
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@@ -238,21 +221,15 @@ class CropControllerTest {
|
||||
.withAutoCrop(false)
|
||||
.build();
|
||||
|
||||
PDDocument mockDocument = mock(PDDocument.class);
|
||||
PDDocument newDocument = mock(PDDocument.class);
|
||||
when(pdfDocumentFactory.load(request)).thenReturn(mockDocument);
|
||||
when(pdfDocumentFactory.createNewDocumentBasedOnOldDocument(mockDocument))
|
||||
.thenReturn(newDocument);
|
||||
|
||||
ResponseEntity<Resource> response = cropController.cropPdf(request);
|
||||
|
||||
assertThat(response).isNotNull();
|
||||
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
|
||||
assertThat(response.getBody()).isNotNull();
|
||||
|
||||
verify(pdfDocumentFactory).load(request);
|
||||
verify(mockDocument, times(1)).close();
|
||||
verify(newDocument, times(1)).close();
|
||||
try (PDDocument result = Loader.loadPDF(drainBody(response))) {
|
||||
assertThat(result.getNumberOfPages()).isEqualTo(1);
|
||||
PDRectangle mediaBox = result.getPage(0).getMediaBox();
|
||||
assertThat(mediaBox.getWidth()).isCloseTo(width, within(0.5f));
|
||||
assertThat(mediaBox.getHeight()).isCloseTo(height, within(0.5f));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -261,69 +238,44 @@ class CropControllerTest {
|
||||
@Tag("integration")
|
||||
class AutoCropTests {
|
||||
|
||||
private TestPdfFactory autoCropPdfFactory;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() throws Exception {
|
||||
autoCropPdfFactory = new TestPdfFactory();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should auto-crop PDF with content successfully")
|
||||
void shouldAutoCropPdfSuccessfully() throws IOException {
|
||||
MockMultipartFile testFile =
|
||||
autoCropPdfFactory.createPdfWithCenteredContent(
|
||||
pdfFactory.createPdfWithCenteredContent(
|
||||
"test_autocrop.pdf", "Test Content for Auto Crop");
|
||||
CropPdfForm request =
|
||||
new CropRequestBuilder().withFile(testFile).withAutoCrop(true).build();
|
||||
|
||||
// Mock the pdfDocumentFactory to load real PDFs
|
||||
try (PDDocument sourceDoc = Loader.loadPDF(testFile.getBytes());
|
||||
PDDocument newDoc = new PDDocument()) {
|
||||
when(pdfDocumentFactory.load(request)).thenReturn(sourceDoc);
|
||||
when(pdfDocumentFactory.createNewDocumentBasedOnOldDocument(sourceDoc))
|
||||
.thenReturn(newDoc);
|
||||
ResponseEntity<Resource> response = cropController.cropPdf(request);
|
||||
|
||||
ResponseEntity<Resource> response = cropController.cropPdf(request);
|
||||
|
||||
assertThat(response).isNotNull();
|
||||
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
|
||||
assertThat(drainBody(response)).isNotEmpty();
|
||||
|
||||
try (PDDocument result = Loader.loadPDF(drainBody(response))) {
|
||||
assertThat(result.getNumberOfPages()).isEqualTo(1);
|
||||
|
||||
PDPage page = result.getPage(0);
|
||||
assertThat(page).isNotNull();
|
||||
assertThat(page.getMediaBox()).isNotNull();
|
||||
}
|
||||
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
|
||||
byte[] out = drainBody(response);
|
||||
assertThat(out).isNotEmpty();
|
||||
try (PDDocument result = Loader.loadPDF(out)) {
|
||||
assertThat(result.getNumberOfPages()).isEqualTo(1);
|
||||
PDPage page = result.getPage(0);
|
||||
assertThat(page.getMediaBox()).isNotNull();
|
||||
// Cropped result should be smaller than original LETTER.
|
||||
assertThat(page.getMediaBox().getWidth()).isLessThan(PDRectangle.LETTER.getWidth());
|
||||
assertThat(page.getMediaBox().getHeight())
|
||||
.isLessThan(PDRectangle.LETTER.getHeight());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should handle PDF with minimal content")
|
||||
void shouldHandleMinimalContentPdf() throws IOException {
|
||||
MockMultipartFile testFile =
|
||||
autoCropPdfFactory.createPdfWithContent("minimal.pdf", "X");
|
||||
MockMultipartFile testFile = pdfFactory.createPdfWithContent("minimal.pdf", "X");
|
||||
CropPdfForm request =
|
||||
new CropRequestBuilder().withFile(testFile).withAutoCrop(true).build();
|
||||
|
||||
// Mock the pdfDocumentFactory to load real PDFs
|
||||
try (PDDocument sourceDoc = Loader.loadPDF(testFile.getBytes());
|
||||
PDDocument newDoc = new PDDocument()) {
|
||||
when(pdfDocumentFactory.load(request)).thenReturn(sourceDoc);
|
||||
when(pdfDocumentFactory.createNewDocumentBasedOnOldDocument(sourceDoc))
|
||||
.thenReturn(newDoc);
|
||||
ResponseEntity<Resource> response = cropController.cropPdf(request);
|
||||
|
||||
ResponseEntity<Resource> response = cropController.cropPdf(request);
|
||||
|
||||
assertThat(response).isNotNull();
|
||||
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
|
||||
|
||||
Assertions.assertNotNull(response.getBody());
|
||||
try (PDDocument result = Loader.loadPDF(drainBody(response))) {
|
||||
assertThat(result.getNumberOfPages()).isEqualTo(1);
|
||||
}
|
||||
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
|
||||
assertThat(response.getBody()).isNotNull();
|
||||
try (PDDocument result = Loader.loadPDF(drainBody(response))) {
|
||||
assertThat(result.getNumberOfPages()).isEqualTo(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -384,9 +336,7 @@ class CropControllerTest {
|
||||
@DisplayName("Should detect full image bounds for all white image")
|
||||
void shouldDetectFullBoundsForWhiteImage() throws Exception {
|
||||
BufferedImage whiteImage = createWhiteImage(100, 100);
|
||||
|
||||
int[] bounds = (int[]) detectContentBoundsMethod.invoke(null, whiteImage);
|
||||
|
||||
assertThat(bounds).containsExactly(0, 0, 99, 99);
|
||||
}
|
||||
|
||||
@@ -395,9 +345,7 @@ class CropControllerTest {
|
||||
void shouldDetectBlackRectangleBounds() throws Exception {
|
||||
BufferedImage image = createWhiteImage(100, 100);
|
||||
drawBlackRectangle(image, 25, 25, 75, 75);
|
||||
|
||||
int[] bounds = (int[]) detectContentBoundsMethod.invoke(null, image);
|
||||
|
||||
assertThat(bounds).containsExactly(25, 25, 74, 74);
|
||||
}
|
||||
|
||||
@@ -409,9 +357,7 @@ class CropControllerTest {
|
||||
image.setRGB(99, 0, 0x000000);
|
||||
image.setRGB(0, 99, 0x000000);
|
||||
image.setRGB(99, 99, 0x000000);
|
||||
|
||||
int[] bounds = (int[]) detectContentBoundsMethod.invoke(null, image);
|
||||
|
||||
assertThat(bounds).containsExactly(0, 0, 99, 99);
|
||||
}
|
||||
|
||||
@@ -422,9 +368,7 @@ class CropControllerTest {
|
||||
image.setRGB(10, 10, 0xF0F0F0);
|
||||
image.setRGB(90, 90, 0xF0F0F0);
|
||||
drawBlackRectangle(image, 30, 30, 70, 70);
|
||||
|
||||
int[] bounds = (int[]) detectContentBoundsMethod.invoke(null, image);
|
||||
|
||||
assertThat(bounds).containsExactly(10, 9, 90, 89);
|
||||
}
|
||||
|
||||
@@ -433,9 +377,7 @@ class CropControllerTest {
|
||||
void shouldTreatGrayPixelsAsContent() throws Exception {
|
||||
BufferedImage image = createImageFilledWith(50, 50, 0xF0F0F0);
|
||||
drawDarkerRectangle(image, 20, 20, 30, 30, 0xC0C0C0);
|
||||
|
||||
int[] bounds = (int[]) detectContentBoundsMethod.invoke(null, image);
|
||||
|
||||
assertThat(bounds).containsExactly(0, 0, 49, 49);
|
||||
}
|
||||
}
|
||||
@@ -554,33 +496,6 @@ class CropControllerTest {
|
||||
@DisplayName("Error Handling")
|
||||
class ErrorHandlingTests {
|
||||
|
||||
@Test
|
||||
@DisplayName("Should throw exception for corrupt PDF file")
|
||||
void shouldThrowExceptionForCorruptPdf() throws IOException {
|
||||
MockMultipartFile corruptFile =
|
||||
new MockMultipartFile(
|
||||
"fileInput",
|
||||
"corrupt.pdf",
|
||||
MediaType.APPLICATION_PDF_VALUE,
|
||||
"not a valid pdf content".getBytes());
|
||||
|
||||
CropPdfForm request =
|
||||
new CropRequestBuilder()
|
||||
.withFile(corruptFile)
|
||||
.withCoordinates(50f, 50f, 512f, 692f)
|
||||
.withRemoveDataOutsideCrop(false)
|
||||
.withAutoCrop(false)
|
||||
.build();
|
||||
|
||||
when(pdfDocumentFactory.load(request)).thenThrow(new IOException("Invalid PDF format"));
|
||||
|
||||
assertThatThrownBy(() -> cropController.cropPdf(request))
|
||||
.isInstanceOf(IOException.class)
|
||||
.hasMessageContaining("Invalid PDF format");
|
||||
|
||||
verify(pdfDocumentFactory).load(request);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should throw exception when coordinates are missing for manual crop")
|
||||
void shouldThrowExceptionForMissingCoordinates() throws IOException {
|
||||
@@ -593,54 +508,6 @@ class CropControllerTest {
|
||||
.hasMessage(
|
||||
"Crop coordinates (x, y, width, height) are required when auto-crop is not enabled");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should handle negative coordinates gracefully")
|
||||
void shouldHandleNegativeCoordinates() throws IOException {
|
||||
MockMultipartFile testFile = pdfFactory.createStandardPdf("test.pdf");
|
||||
CropPdfForm request =
|
||||
new CropRequestBuilder()
|
||||
.withFile(testFile)
|
||||
.withCoordinates(-10f, 50f, 512f, 692f)
|
||||
.withRemoveDataOutsideCrop(false)
|
||||
.withAutoCrop(false)
|
||||
.build();
|
||||
|
||||
PDDocument mockDocument = mock(PDDocument.class);
|
||||
PDDocument newDocument = mock(PDDocument.class);
|
||||
when(pdfDocumentFactory.load(request)).thenReturn(mockDocument);
|
||||
when(pdfDocumentFactory.createNewDocumentBasedOnOldDocument(mockDocument))
|
||||
.thenReturn(newDocument);
|
||||
|
||||
assertThatCode(() -> cropController.cropPdf(request)).doesNotThrowAnyException();
|
||||
|
||||
verify(mockDocument, times(1)).close();
|
||||
verify(newDocument, times(1)).close();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should handle zero width or height")
|
||||
void shouldHandleZeroDimensions() throws IOException {
|
||||
MockMultipartFile testFile = pdfFactory.createStandardPdf("test.pdf");
|
||||
CropPdfForm request =
|
||||
new CropRequestBuilder()
|
||||
.withFile(testFile)
|
||||
.withCoordinates(50f, 50f, 0f, 692f)
|
||||
.withRemoveDataOutsideCrop(false)
|
||||
.withAutoCrop(false)
|
||||
.build();
|
||||
|
||||
PDDocument mockDocument = mock(PDDocument.class);
|
||||
PDDocument newDocument = mock(PDDocument.class);
|
||||
when(pdfDocumentFactory.load(request)).thenReturn(mockDocument);
|
||||
when(pdfDocumentFactory.createNewDocumentBasedOnOldDocument(mockDocument))
|
||||
.thenReturn(newDocument);
|
||||
|
||||
assertThatCode(() -> cropController.cropPdf(request)).doesNotThrowAnyException();
|
||||
|
||||
verify(mockDocument, times(1)).close();
|
||||
verify(newDocument, times(1)).close();
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@@ -663,7 +530,6 @@ class CropControllerTest {
|
||||
MockMultipartFile testFile = pdfFactory.createStandardPdf("test.pdf");
|
||||
float expectedWidth = 400f;
|
||||
float expectedHeight = 500f;
|
||||
|
||||
CropPdfForm request =
|
||||
new CropRequestBuilder()
|
||||
.withFile(testFile)
|
||||
@@ -672,16 +538,14 @@ class CropControllerTest {
|
||||
.withAutoCrop(false)
|
||||
.build();
|
||||
|
||||
PDDocument mockDocument = mock(PDDocument.class);
|
||||
PDDocument newDocument = mock(PDDocument.class);
|
||||
when(pdfDocumentFactory.load(request)).thenReturn(mockDocument);
|
||||
when(pdfDocumentFactory.createNewDocumentBasedOnOldDocument(mockDocument))
|
||||
.thenReturn(newDocument);
|
||||
|
||||
ResponseEntity<Resource> response = cropController.cropPdf(request);
|
||||
|
||||
assertThat(response).isNotNull();
|
||||
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
|
||||
try (PDDocument result = Loader.loadPDF(drainBody(response))) {
|
||||
PDRectangle mediaBox = result.getPage(0).getMediaBox();
|
||||
assertThat(mediaBox.getWidth()).isCloseTo(expectedWidth, within(0.5f));
|
||||
assertThat(mediaBox.getHeight()).isCloseTo(expectedHeight, within(0.5f));
|
||||
}
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@@ -691,7 +555,6 @@ class CropControllerTest {
|
||||
throws IOException {
|
||||
PDRectangle pageSize = getPageSize(pageSizeName);
|
||||
MockMultipartFile testFile = pdfFactory.createPdfWithSize(filename, pageSize);
|
||||
|
||||
CropPdfForm request =
|
||||
new CropRequestBuilder()
|
||||
.withFile(testFile)
|
||||
@@ -700,17 +563,14 @@ class CropControllerTest {
|
||||
.withAutoCrop(false)
|
||||
.build();
|
||||
|
||||
PDDocument mockDocument = mock(PDDocument.class);
|
||||
PDDocument newDocument = mock(PDDocument.class);
|
||||
when(pdfDocumentFactory.load(request)).thenReturn(mockDocument);
|
||||
when(pdfDocumentFactory.createNewDocumentBasedOnOldDocument(mockDocument))
|
||||
.thenReturn(newDocument);
|
||||
|
||||
ResponseEntity<Resource> response = cropController.cropPdf(request);
|
||||
|
||||
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
|
||||
verify(mockDocument, times(1)).close();
|
||||
verify(newDocument, times(1)).close();
|
||||
try (PDDocument result = Loader.loadPDF(drainBody(response))) {
|
||||
PDRectangle mediaBox = result.getPage(0).getMediaBox();
|
||||
assertThat(mediaBox.getWidth()).isCloseTo(300f, within(0.5f));
|
||||
assertThat(mediaBox.getHeight()).isCloseTo(400f, within(0.5f));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+131
-180
@@ -1,6 +1,6 @@
|
||||
package stirling.software.SPDF.controller.api;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.Mockito.*;
|
||||
@@ -10,17 +10,21 @@ 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.PDPageContentStream;
|
||||
import org.apache.pdfbox.pdmodel.common.PDRectangle;
|
||||
import org.apache.pdfbox.pdmodel.font.PDType1Font;
|
||||
import org.apache.pdfbox.pdmodel.font.Standard14Fonts;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Tag;
|
||||
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.ByteArrayResource;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
@@ -28,264 +32,211 @@ import org.springframework.mock.web.MockMultipartFile;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import stirling.software.SPDF.model.api.general.ScalePagesRequest;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.util.TempFile;
|
||||
import stirling.software.common.util.TempFileManager;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
@Tag("integration")
|
||||
class ScalePagesControllerTest {
|
||||
private static ResponseEntity<Resource> streamingOk(byte[] bytes) {
|
||||
return ResponseEntity.ok(new ByteArrayResource(bytes));
|
||||
}
|
||||
|
||||
private static byte[] drainBody(ResponseEntity<Resource> response) throws java.io.IOException {
|
||||
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);
|
||||
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 ScalePagesController controller;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() throws Exception {
|
||||
lenient()
|
||||
.when(tempFileManager.createManagedTempFile(anyString()))
|
||||
.when(tempFileManager.convertMultipartFileToFile(any(MultipartFile.class)))
|
||||
.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;
|
||||
MultipartFile mf = inv.getArgument(0);
|
||||
File f = Files.createTempFile("scale-in", ".pdf").toFile();
|
||||
Files.write(f.toPath(), mf.getBytes());
|
||||
return f;
|
||||
});
|
||||
lenient()
|
||||
.when(tempFileManager.createTempFile(anyString()))
|
||||
.thenAnswer(inv -> Files.createTempFile("scale-out", inv.getArgument(0)).toFile());
|
||||
}
|
||||
|
||||
private byte[] createRealPdf(PDRectangle pageSize, int numPages) throws IOException {
|
||||
// PDFium TransFormWithClip requires each page to have a content stream; add a
|
||||
// sentinel character on every page so the test fixtures are not empty.
|
||||
private MockMultipartFile createRealPdf(PDRectangle pageSize, int numPages) throws IOException {
|
||||
PDType1Font helv = new PDType1Font(Standard14Fonts.FontName.HELVETICA);
|
||||
try (PDDocument doc = new PDDocument()) {
|
||||
for (int i = 0; i < numPages; i++) {
|
||||
doc.addPage(new PDPage(pageSize));
|
||||
PDPage page = new PDPage(pageSize);
|
||||
doc.addPage(page);
|
||||
try (PDPageContentStream cs = new PDPageContentStream(doc, page)) {
|
||||
cs.beginText();
|
||||
cs.setFont(helv, 12);
|
||||
cs.newLineAtOffset(50, pageSize.getHeight() - 50);
|
||||
cs.showText("X");
|
||||
cs.endText();
|
||||
}
|
||||
}
|
||||
Path pdfPath = tempDir.resolve("input.pdf");
|
||||
Path pdfPath = tempDir.resolve("input-" + System.nanoTime() + ".pdf");
|
||||
doc.save(pdfPath.toFile());
|
||||
return Files.readAllBytes(pdfPath);
|
||||
return new MockMultipartFile(
|
||||
"fileInput",
|
||||
"test.pdf",
|
||||
MediaType.APPLICATION_PDF_VALUE,
|
||||
Files.readAllBytes(pdfPath));
|
||||
}
|
||||
}
|
||||
|
||||
private void setupFactory() throws IOException {
|
||||
when(pdfDocumentFactory.load(any(MultipartFile.class)))
|
||||
.thenAnswer(
|
||||
inv -> {
|
||||
byte[] bytes = ((MultipartFile) inv.getArgument(0)).getBytes();
|
||||
return org.apache.pdfbox.Loader.loadPDF(bytes);
|
||||
});
|
||||
when(pdfDocumentFactory.createNewDocumentBasedOnOldDocument(any(PDDocument.class)))
|
||||
.thenAnswer(inv -> new PDDocument());
|
||||
private ScalePagesRequest buildRequest(MockMultipartFile file, String size, float factor) {
|
||||
ScalePagesRequest request = new ScalePagesRequest();
|
||||
request.setFileInput(file);
|
||||
request.setPageSize(size);
|
||||
request.setScaleFactor(factor);
|
||||
return request;
|
||||
}
|
||||
|
||||
@Test
|
||||
void testScalePages_A4ToA3() throws Exception {
|
||||
byte[] pdfBytes = createRealPdf(PDRectangle.A4, 1);
|
||||
MockMultipartFile file =
|
||||
new MockMultipartFile(
|
||||
"fileInput", "test.pdf", MediaType.APPLICATION_PDF_VALUE, pdfBytes);
|
||||
MockMultipartFile file = createRealPdf(PDRectangle.A4, 1);
|
||||
ResponseEntity<Resource> response = controller.scalePages(buildRequest(file, "A3", 1.0f));
|
||||
|
||||
ScalePagesRequest request = new ScalePagesRequest();
|
||||
request.setFileInput(file);
|
||||
request.setPageSize("A3");
|
||||
request.setScaleFactor(1.0f);
|
||||
|
||||
setupFactory();
|
||||
|
||||
ResponseEntity<Resource> response = controller.scalePages(request);
|
||||
|
||||
assertNotNull(response);
|
||||
assertEquals(200, response.getStatusCode().value());
|
||||
assertNotNull(response.getBody());
|
||||
assertTrue(drainBody(response).length > 0);
|
||||
assertThat(response.getStatusCode().value()).isEqualTo(200);
|
||||
try (PDDocument out = Loader.loadPDF(drainBody(response))) {
|
||||
assertThat(out.getNumberOfPages()).isEqualTo(1);
|
||||
PDRectangle mb = out.getPage(0).getMediaBox();
|
||||
assertThat(mb.getWidth()).isCloseTo(PDRectangle.A3.getWidth(), within(0.5f));
|
||||
assertThat(mb.getHeight()).isCloseTo(PDRectangle.A3.getHeight(), within(0.5f));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testScalePages_KeepSize() throws Exception {
|
||||
byte[] pdfBytes = createRealPdf(PDRectangle.A4, 2);
|
||||
MockMultipartFile file =
|
||||
new MockMultipartFile(
|
||||
"fileInput", "test.pdf", MediaType.APPLICATION_PDF_VALUE, pdfBytes);
|
||||
MockMultipartFile file = createRealPdf(PDRectangle.A4, 2);
|
||||
ResponseEntity<Resource> response = controller.scalePages(buildRequest(file, "KEEP", 1.0f));
|
||||
|
||||
ScalePagesRequest request = new ScalePagesRequest();
|
||||
request.setFileInput(file);
|
||||
request.setPageSize("KEEP");
|
||||
request.setScaleFactor(1.0f);
|
||||
|
||||
setupFactory();
|
||||
|
||||
ResponseEntity<Resource> response = controller.scalePages(request);
|
||||
|
||||
assertNotNull(response);
|
||||
assertEquals(200, response.getStatusCode().value());
|
||||
assertThat(response.getStatusCode().value()).isEqualTo(200);
|
||||
try (PDDocument out = Loader.loadPDF(drainBody(response))) {
|
||||
assertThat(out.getNumberOfPages()).isEqualTo(2);
|
||||
PDRectangle mb = out.getPage(0).getMediaBox();
|
||||
assertThat(mb.getWidth()).isCloseTo(PDRectangle.A4.getWidth(), within(0.5f));
|
||||
assertThat(mb.getHeight()).isCloseTo(PDRectangle.A4.getHeight(), within(0.5f));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testScalePages_WithScaleFactor() throws Exception {
|
||||
byte[] pdfBytes = createRealPdf(PDRectangle.A4, 1);
|
||||
MockMultipartFile file =
|
||||
new MockMultipartFile(
|
||||
"fileInput", "test.pdf", MediaType.APPLICATION_PDF_VALUE, pdfBytes);
|
||||
MockMultipartFile file = createRealPdf(PDRectangle.A4, 1);
|
||||
ResponseEntity<Resource> response = controller.scalePages(buildRequest(file, "A4", 0.5f));
|
||||
|
||||
ScalePagesRequest request = new ScalePagesRequest();
|
||||
request.setFileInput(file);
|
||||
request.setPageSize("A4");
|
||||
request.setScaleFactor(0.5f);
|
||||
|
||||
setupFactory();
|
||||
|
||||
ResponseEntity<Resource> response = controller.scalePages(request);
|
||||
|
||||
assertNotNull(response);
|
||||
assertEquals(200, response.getStatusCode().value());
|
||||
assertThat(response.getStatusCode().value()).isEqualTo(200);
|
||||
try (PDDocument out = Loader.loadPDF(drainBody(response))) {
|
||||
// Page size stays A4; content is scaled by 0.5x and centered.
|
||||
PDRectangle mb = out.getPage(0).getMediaBox();
|
||||
assertThat(mb.getWidth()).isCloseTo(PDRectangle.A4.getWidth(), within(0.5f));
|
||||
assertThat(mb.getHeight()).isCloseTo(PDRectangle.A4.getHeight(), within(0.5f));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testScalePages_Letter() throws Exception {
|
||||
byte[] pdfBytes = createRealPdf(PDRectangle.A4, 1);
|
||||
MockMultipartFile file =
|
||||
new MockMultipartFile(
|
||||
"fileInput", "test.pdf", MediaType.APPLICATION_PDF_VALUE, pdfBytes);
|
||||
MockMultipartFile file = createRealPdf(PDRectangle.A4, 1);
|
||||
ResponseEntity<Resource> response =
|
||||
controller.scalePages(buildRequest(file, "LETTER", 1.0f));
|
||||
|
||||
ScalePagesRequest request = new ScalePagesRequest();
|
||||
request.setFileInput(file);
|
||||
request.setPageSize("LETTER");
|
||||
request.setScaleFactor(1.0f);
|
||||
|
||||
setupFactory();
|
||||
|
||||
ResponseEntity<Resource> response = controller.scalePages(request);
|
||||
|
||||
assertNotNull(response);
|
||||
assertEquals(200, response.getStatusCode().value());
|
||||
assertThat(response.getStatusCode().value()).isEqualTo(200);
|
||||
try (PDDocument out = Loader.loadPDF(drainBody(response))) {
|
||||
PDRectangle mb = out.getPage(0).getMediaBox();
|
||||
assertThat(mb.getWidth()).isCloseTo(PDRectangle.LETTER.getWidth(), within(0.5f));
|
||||
assertThat(mb.getHeight()).isCloseTo(PDRectangle.LETTER.getHeight(), within(0.5f));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testScalePages_Legal() throws Exception {
|
||||
byte[] pdfBytes = createRealPdf(PDRectangle.A4, 1);
|
||||
MockMultipartFile file =
|
||||
new MockMultipartFile(
|
||||
"fileInput", "test.pdf", MediaType.APPLICATION_PDF_VALUE, pdfBytes);
|
||||
MockMultipartFile file = createRealPdf(PDRectangle.A4, 1);
|
||||
ResponseEntity<Resource> response =
|
||||
controller.scalePages(buildRequest(file, "LEGAL", 1.0f));
|
||||
|
||||
ScalePagesRequest request = new ScalePagesRequest();
|
||||
request.setFileInput(file);
|
||||
request.setPageSize("LEGAL");
|
||||
request.setScaleFactor(1.0f);
|
||||
|
||||
setupFactory();
|
||||
|
||||
ResponseEntity<Resource> response = controller.scalePages(request);
|
||||
|
||||
assertNotNull(response);
|
||||
assertEquals(200, response.getStatusCode().value());
|
||||
assertThat(response.getStatusCode().value()).isEqualTo(200);
|
||||
try (PDDocument out = Loader.loadPDF(drainBody(response))) {
|
||||
PDRectangle mb = out.getPage(0).getMediaBox();
|
||||
assertThat(mb.getWidth()).isCloseTo(PDRectangle.LEGAL.getWidth(), within(0.5f));
|
||||
assertThat(mb.getHeight()).isCloseTo(PDRectangle.LEGAL.getHeight(), within(0.5f));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testScalePages_InvalidPageSize() throws Exception {
|
||||
byte[] pdfBytes = createRealPdf(PDRectangle.A4, 1);
|
||||
MockMultipartFile file =
|
||||
new MockMultipartFile(
|
||||
"fileInput", "test.pdf", MediaType.APPLICATION_PDF_VALUE, pdfBytes);
|
||||
MockMultipartFile file = createRealPdf(PDRectangle.A4, 1);
|
||||
ScalePagesRequest request = buildRequest(file, "INVALID_SIZE", 1.0f);
|
||||
|
||||
ScalePagesRequest request = new ScalePagesRequest();
|
||||
request.setFileInput(file);
|
||||
request.setPageSize("INVALID_SIZE");
|
||||
request.setScaleFactor(1.0f);
|
||||
|
||||
setupFactory();
|
||||
|
||||
assertThrows(IllegalArgumentException.class, () -> controller.scalePages(request));
|
||||
assertThatThrownBy(() -> controller.scalePages(request))
|
||||
.isInstanceOf(IllegalArgumentException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testScalePages_MultiplePages() throws Exception {
|
||||
byte[] pdfBytes = createRealPdf(PDRectangle.A4, 5);
|
||||
MockMultipartFile file =
|
||||
new MockMultipartFile(
|
||||
"fileInput", "test.pdf", MediaType.APPLICATION_PDF_VALUE, pdfBytes);
|
||||
MockMultipartFile file = createRealPdf(PDRectangle.A4, 5);
|
||||
ResponseEntity<Resource> response = controller.scalePages(buildRequest(file, "A5", 1.0f));
|
||||
|
||||
ScalePagesRequest request = new ScalePagesRequest();
|
||||
request.setFileInput(file);
|
||||
request.setPageSize("A5");
|
||||
request.setScaleFactor(1.0f);
|
||||
|
||||
setupFactory();
|
||||
|
||||
ResponseEntity<Resource> response = controller.scalePages(request);
|
||||
|
||||
assertNotNull(response);
|
||||
assertEquals(200, response.getStatusCode().value());
|
||||
assertThat(response.getStatusCode().value()).isEqualTo(200);
|
||||
try (PDDocument out = Loader.loadPDF(drainBody(response))) {
|
||||
assertThat(out.getNumberOfPages()).isEqualTo(5);
|
||||
for (int i = 0; i < 5; i++) {
|
||||
PDRectangle mb = out.getPage(i).getMediaBox();
|
||||
assertThat(mb.getWidth()).isCloseTo(PDRectangle.A5.getWidth(), within(0.5f));
|
||||
assertThat(mb.getHeight()).isCloseTo(PDRectangle.A5.getHeight(), within(0.5f));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testScalePages_LandscapeSize() throws Exception {
|
||||
byte[] pdfBytes = createRealPdf(PDRectangle.A4, 1);
|
||||
MockMultipartFile file =
|
||||
new MockMultipartFile(
|
||||
"fileInput", "test.pdf", MediaType.APPLICATION_PDF_VALUE, pdfBytes);
|
||||
MockMultipartFile file = createRealPdf(PDRectangle.A4, 1);
|
||||
ResponseEntity<Resource> response =
|
||||
controller.scalePages(buildRequest(file, "A4_LANDSCAPE", 1.0f));
|
||||
|
||||
ScalePagesRequest request = new ScalePagesRequest();
|
||||
request.setFileInput(file);
|
||||
request.setPageSize("A4_LANDSCAPE");
|
||||
request.setScaleFactor(1.0f);
|
||||
|
||||
setupFactory();
|
||||
|
||||
ResponseEntity<Resource> response = controller.scalePages(request);
|
||||
|
||||
assertNotNull(response);
|
||||
assertEquals(200, response.getStatusCode().value());
|
||||
assertThat(response.getStatusCode().value()).isEqualTo(200);
|
||||
try (PDDocument out = Loader.loadPDF(drainBody(response))) {
|
||||
PDRectangle mb = out.getPage(0).getMediaBox();
|
||||
assertThat(mb.getWidth()).isCloseTo(PDRectangle.A4.getHeight(), within(0.5f));
|
||||
assertThat(mb.getHeight()).isCloseTo(PDRectangle.A4.getWidth(), within(0.5f));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testScalePages_KeepWithEmptyDoc() throws Exception {
|
||||
// Create a PDF then load it, but mock factory to return empty doc for KEEP check
|
||||
byte[] pdfBytes = createRealPdf(PDRectangle.A4, 1);
|
||||
Path pdfPath = tempDir.resolve("empty.pdf");
|
||||
try (PDDocument doc = new PDDocument()) {
|
||||
doc.save(pdfPath.toFile());
|
||||
}
|
||||
MockMultipartFile file =
|
||||
new MockMultipartFile(
|
||||
"fileInput", "test.pdf", MediaType.APPLICATION_PDF_VALUE, pdfBytes);
|
||||
"fileInput",
|
||||
"empty.pdf",
|
||||
MediaType.APPLICATION_PDF_VALUE,
|
||||
Files.readAllBytes(pdfPath));
|
||||
ScalePagesRequest request = buildRequest(file, "KEEP", 1.0f);
|
||||
|
||||
ScalePagesRequest request = new ScalePagesRequest();
|
||||
request.setFileInput(file);
|
||||
request.setPageSize("KEEP");
|
||||
request.setScaleFactor(1.0f);
|
||||
|
||||
// Return an empty document to trigger the KEEP exception
|
||||
when(pdfDocumentFactory.load(any(MultipartFile.class))).thenReturn(new PDDocument());
|
||||
when(pdfDocumentFactory.createNewDocumentBasedOnOldDocument(any(PDDocument.class)))
|
||||
.thenAnswer(inv -> new PDDocument());
|
||||
|
||||
assertThrows(IllegalArgumentException.class, () -> controller.scalePages(request));
|
||||
assertThatThrownBy(() -> controller.scalePages(request))
|
||||
.isInstanceOf(IllegalArgumentException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testScalePages_A0Size() throws Exception {
|
||||
byte[] pdfBytes = createRealPdf(PDRectangle.A4, 1);
|
||||
MockMultipartFile file =
|
||||
new MockMultipartFile(
|
||||
"fileInput", "test.pdf", MediaType.APPLICATION_PDF_VALUE, pdfBytes);
|
||||
MockMultipartFile file = createRealPdf(PDRectangle.A4, 1);
|
||||
ResponseEntity<Resource> response = controller.scalePages(buildRequest(file, "A0", 1.0f));
|
||||
|
||||
ScalePagesRequest request = new ScalePagesRequest();
|
||||
request.setFileInput(file);
|
||||
request.setPageSize("A0");
|
||||
request.setScaleFactor(1.0f);
|
||||
|
||||
setupFactory();
|
||||
|
||||
ResponseEntity<Resource> response = controller.scalePages(request);
|
||||
|
||||
assertNotNull(response);
|
||||
assertEquals(200, response.getStatusCode().value());
|
||||
assertThat(response.getStatusCode().value()).isEqualTo(200);
|
||||
try (PDDocument out = Loader.loadPDF(drainBody(response))) {
|
||||
PDRectangle mb = out.getPage(0).getMediaBox();
|
||||
assertThat(mb.getWidth()).isCloseTo(PDRectangle.A0.getWidth(), within(0.5f));
|
||||
assertThat(mb.getHeight()).isCloseTo(PDRectangle.A0.getHeight(), within(0.5f));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user