diff --git a/app/common/build.gradle b/app/common/build.gradle index 313eecdf8..e5bc8b274 100644 --- a/app/common/build.gradle +++ b/app/common/build.gradle @@ -37,6 +37,7 @@ dependencies { api 'com.drewnoakes:metadata-extractor:2.19.0' // Image metadata extractor api 'com.vladsch.flexmark:flexmark-html2md-converter:0.64.8' api "org.apache.pdfbox:pdfbox:$pdfboxVersion" + api "org.apache.pdfbox:pdfbox-io:$pdfboxVersion" api "org.apache.pdfbox:xmpbox:$pdfboxVersion" api "org.apache.pdfbox:preflight:$pdfboxVersion" api 'com.github.junrar:junrar:7.5.7' // RAR archive support for CBR files diff --git a/app/common/src/main/java/stirling/software/common/util/CbrUtils.java b/app/common/src/main/java/stirling/software/common/util/CbrUtils.java index c6f9ea78f..6c1589232 100644 --- a/app/common/src/main/java/stirling/software/common/util/CbrUtils.java +++ b/app/common/src/main/java/stirling/software/common/util/CbrUtils.java @@ -129,7 +129,12 @@ public class CbrUtils { new PDRectangle(pdImage.getWidth(), pdImage.getHeight())); document.addPage(page); try (PDPageContentStream contentStream = - new PDPageContentStream(document, page)) { + new PDPageContentStream( + document, + page, + PDPageContentStream.AppendMode.OVERWRITE, + true, + true)) { contentStream.drawImage(pdImage, 0, 0); } } catch (IOException e) { diff --git a/app/common/src/main/java/stirling/software/common/util/CbzUtils.java b/app/common/src/main/java/stirling/software/common/util/CbzUtils.java index d9fb40189..f8af4207b 100644 --- a/app/common/src/main/java/stirling/software/common/util/CbzUtils.java +++ b/app/common/src/main/java/stirling/software/common/util/CbzUtils.java @@ -97,7 +97,12 @@ public class CbzUtils { new PDRectangle(pdImage.getWidth(), pdImage.getHeight())); document.addPage(page); try (PDPageContentStream contentStream = - new PDPageContentStream(document, page)) { + new PDPageContentStream( + document, + page, + PDPageContentStream.AppendMode.OVERWRITE, + true, + true)) { contentStream.drawImage(pdImage, 0, 0); } } catch (IOException e) { diff --git a/app/common/src/main/java/stirling/software/common/util/ExceptionUtils.java b/app/common/src/main/java/stirling/software/common/util/ExceptionUtils.java index 02b8da9ee..0ec94fd51 100644 --- a/app/common/src/main/java/stirling/software/common/util/ExceptionUtils.java +++ b/app/common/src/main/java/stirling/software/common/util/ExceptionUtils.java @@ -41,7 +41,7 @@ import lombok.extern.slf4j.Slf4j; *
{@code
* // In service layer - create exception with ExceptionUtils
* try {
- * PDDocument doc = PDDocument.load(file);
+ * PDDocument doc = Loader.loadPDF(file);
* } catch (IOException e) {
* throw ExceptionUtils.createPdfCorruptedException("during load", e);
* }
diff --git a/app/common/src/main/java/stirling/software/common/util/PdfToCbrUtils.java b/app/common/src/main/java/stirling/software/common/util/PdfToCbrUtils.java
index 3d17cfc8e..cfc95a290 100644
--- a/app/common/src/main/java/stirling/software/common/util/PdfToCbrUtils.java
+++ b/app/common/src/main/java/stirling/software/common/util/PdfToCbrUtils.java
@@ -60,6 +60,7 @@ public class PdfToCbrUtils {
private static byte[] createCbrFromPdf(PDDocument document, int dpi) throws IOException {
PDFRenderer pdfRenderer = new PDFRenderer(document);
+ pdfRenderer.setSubsamplingAllowed(true); // Enable subsampling to reduce memory usage
Path tempDir = Files.createTempDirectory("stirling-pdf-cbr-");
List generatedImages = new ArrayList<>();
diff --git a/app/common/src/main/java/stirling/software/common/util/PdfToCbzUtils.java b/app/common/src/main/java/stirling/software/common/util/PdfToCbzUtils.java
index 4fee38a7f..e0a0d39fe 100644
--- a/app/common/src/main/java/stirling/software/common/util/PdfToCbzUtils.java
+++ b/app/common/src/main/java/stirling/software/common/util/PdfToCbzUtils.java
@@ -55,6 +55,7 @@ public class PdfToCbzUtils {
private static byte[] createCbzFromPdf(PDDocument document, int dpi) throws IOException {
PDFRenderer pdfRenderer = new PDFRenderer(document);
+ pdfRenderer.setSubsamplingAllowed(true); // Enable subsampling to reduce memory usage
try (ByteArrayOutputStream cbzOutputStream = new ByteArrayOutputStream();
ZipOutputStream zipOut = new ZipOutputStream(cbzOutputStream)) {
diff --git a/app/common/src/main/java/stirling/software/common/util/PdfUtils.java b/app/common/src/main/java/stirling/software/common/util/PdfUtils.java
index d294da2ff..ee0fa3f97 100644
--- a/app/common/src/main/java/stirling/software/common/util/PdfUtils.java
+++ b/app/common/src/main/java/stirling/software/common/util/PdfUtils.java
@@ -119,11 +119,11 @@ public class PdfUtils {
public boolean hasTextOnPage(PDPage page, String phrase) throws IOException {
PDFTextStripper textStripper = new PDFTextStripper();
- PDDocument tempDoc = new PDDocument();
- tempDoc.addPage(page);
- String pageText = textStripper.getText(tempDoc);
- tempDoc.close();
- return pageText.contains(phrase);
+ try (PDDocument tempDoc = new PDDocument()) {
+ tempDoc.addPage(page);
+ String pageText = textStripper.getText(tempDoc);
+ return pageText.contains(phrase);
+ }
}
public byte[] convertFromPdf(
@@ -153,7 +153,8 @@ public class PdfUtils {
maxSafeDpi);
}
- try (PDDocument document = pdfDocumentFactory.load(inputStream)) {
+ try (PDDocument document = pdfDocumentFactory.load(inputStream);
+ ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
PDFRenderer pdfRenderer = new PDFRenderer(document);
pdfRenderer.setSubsamplingAllowed(true);
if (!includeAnnotations) {
@@ -161,9 +162,6 @@ public class PdfUtils {
}
int pageCount = document.getNumberOfPages();
- // Create a ByteArrayOutputStream to save the image(s) to
- ByteArrayOutputStream baos = new ByteArrayOutputStream();
-
if (singleImage) {
if ("tiff".equals(imageType.toLowerCase(Locale.ROOT))
|| "tif".equals(imageType.toLowerCase(Locale.ROOT))) {
@@ -400,55 +398,61 @@ public class PdfUtils {
*/
public PDDocument convertPdfToPdfImage(PDDocument document) throws IOException {
PDDocument imageDocument = new PDDocument();
- PDFRenderer pdfRenderer = new PDFRenderer(document);
- pdfRenderer.setSubsamplingAllowed(true);
- for (int page = 0; page < document.getNumberOfPages(); ++page) {
- final int pageIndex = page;
- BufferedImage bim;
+ try {
+ PDFRenderer pdfRenderer = new PDFRenderer(document);
+ pdfRenderer.setSubsamplingAllowed(true);
+ for (int page = 0; page < document.getNumberOfPages(); ++page) {
+ final int pageIndex = page;
+ BufferedImage bim;
- // Use global maximum DPI setting, fallback to 300 if not set
- int renderDpi = 300; // Default fallback
- ApplicationProperties properties =
- ApplicationContextProvider.getBean(ApplicationProperties.class);
- if (properties != null && properties.getSystem() != null) {
- renderDpi = properties.getSystem().getMaxDPI();
- }
- final int dpi = renderDpi;
-
- try {
- bim =
- ExceptionUtils.handleOomRendering(
- pageIndex + 1,
- dpi,
- () ->
- pdfRenderer.renderImageWithDPI(
- pageIndex, dpi, ImageType.RGB));
- } catch (IllegalArgumentException e) {
- if (e.getMessage() != null
- && e.getMessage().contains("Maximum size of image exceeded")) {
- throw ExceptionUtils.createIllegalArgumentException(
- "error.pageTooBigFor300Dpi",
- "PDF page {0} is too large to render at 300 DPI. The resulting image"
- + " would exceed Java's maximum array size. Please use a lower DPI"
- + " value for PDF-to-image conversion.",
- pageIndex + 1);
+ // Use global maximum DPI setting, fallback to 300 if not set
+ int renderDpi = 300; // Default fallback
+ ApplicationProperties properties =
+ ApplicationContextProvider.getBean(ApplicationProperties.class);
+ if (properties != null && properties.getSystem() != null) {
+ renderDpi = properties.getSystem().getMaxDPI();
}
- throw e;
+ final int dpi = renderDpi;
+
+ try {
+ bim =
+ ExceptionUtils.handleOomRendering(
+ pageIndex + 1,
+ dpi,
+ () ->
+ pdfRenderer.renderImageWithDPI(
+ pageIndex, dpi, ImageType.RGB));
+ } catch (IllegalArgumentException e) {
+ if (e.getMessage() != null
+ && e.getMessage().contains("Maximum size of image exceeded")) {
+ throw ExceptionUtils.createIllegalArgumentException(
+ "error.pageTooBigFor300Dpi",
+ "PDF page {0} is too large to render at 300 DPI. The resulting image"
+ + " would exceed Java's maximum array size. Please use a lower DPI"
+ + " value for PDF-to-image conversion.",
+ pageIndex + 1);
+ }
+ throw e;
+ }
+ PDPage originalPage = document.getPage(page);
+
+ float width = originalPage.getMediaBox().getWidth();
+ float height = originalPage.getMediaBox().getHeight();
+
+ PDPage newPage = new PDPage(new PDRectangle(width, height));
+ imageDocument.addPage(newPage);
+ PDImageXObject pdImage = LosslessFactory.createFromImage(imageDocument, bim);
+ try (PDPageContentStream contentStream =
+ new PDPageContentStream(
+ imageDocument, newPage, AppendMode.APPEND, true, true)) {
+ contentStream.drawImage(pdImage, 0, 0, width, height);
+ }
+ bim.flush();
}
- PDPage originalPage = document.getPage(page);
-
- float width = originalPage.getMediaBox().getWidth();
- float height = originalPage.getMediaBox().getHeight();
-
- PDPage newPage = new PDPage(new PDRectangle(width, height));
- imageDocument.addPage(newPage);
- PDImageXObject pdImage = LosslessFactory.createFromImage(imageDocument, bim);
- PDPageContentStream contentStream =
- new PDPageContentStream(imageDocument, newPage, AppendMode.APPEND, true, true);
- contentStream.drawImage(pdImage, 0, 0, width, height);
- contentStream.close();
+ return imageDocument;
+ } catch (Exception e) {
+ throw e;
}
- return imageDocument;
}
private BufferedImage prepareImageForPdfToImage(int maxWidth, int height, String imageType) {
diff --git a/app/common/src/main/java/stirling/software/common/util/WebResponseUtils.java b/app/common/src/main/java/stirling/software/common/util/WebResponseUtils.java
index 3ab0bddb3..a5132e2a1 100644
--- a/app/common/src/main/java/stirling/software/common/util/WebResponseUtils.java
+++ b/app/common/src/main/java/stirling/software/common/util/WebResponseUtils.java
@@ -69,7 +69,6 @@ public class WebResponseUtils {
// Open Byte Array and save document to it
ByteArrayOutputStream baos = new ByteArrayOutputStream();
document.save(baos);
- document.close();
return baosToWebResponse(baos, docName);
}
diff --git a/app/common/src/main/java/stirling/software/common/util/misc/CustomColorReplaceStrategy.java b/app/common/src/main/java/stirling/software/common/util/misc/CustomColorReplaceStrategy.java
index 6703a159a..1f9f52719 100644
--- a/app/common/src/main/java/stirling/software/common/util/misc/CustomColorReplaceStrategy.java
+++ b/app/common/src/main/java/stirling/software/common/util/misc/CustomColorReplaceStrategy.java
@@ -146,13 +146,18 @@ public class CustomColorReplaceStrategy extends ReplaceAndInvertColorStrategy {
// Save the modified PDF to a ByteArrayOutputStream
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
document.save(byteArrayOutputStream);
- document.close();
// Prepare the modified PDF for download
ByteArrayInputStream inputStream =
new ByteArrayInputStream(byteArrayOutputStream.toByteArray());
InputStreamResource resource = new InputStreamResource(inputStream);
return resource;
+ } finally {
+ try {
+ Files.deleteIfExists(file.toPath());
+ } catch (IOException e) {
+ log.warn("Failed to delete temporary file: {}", file.getAbsolutePath(), e);
+ }
}
}
diff --git a/app/common/src/main/java/stirling/software/common/util/misc/InvertFullColorStrategy.java b/app/common/src/main/java/stirling/software/common/util/misc/InvertFullColorStrategy.java
index 6a48364f9..75782f898 100644
--- a/app/common/src/main/java/stirling/software/common/util/misc/InvertFullColorStrategy.java
+++ b/app/common/src/main/java/stirling/software/common/util/misc/InvertFullColorStrategy.java
@@ -7,6 +7,7 @@ import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
+import java.nio.file.Path;
import javax.imageio.ImageIO;
@@ -19,11 +20,14 @@ import org.apache.pdfbox.rendering.PDFRenderer;
import org.springframework.core.io.InputStreamResource;
import org.springframework.web.multipart.MultipartFile;
+import lombok.extern.slf4j.Slf4j;
+
import stirling.software.common.model.ApplicationProperties;
import stirling.software.common.model.api.misc.ReplaceAndInvert;
import stirling.software.common.util.ApplicationContextProvider;
import stirling.software.common.util.ExceptionUtils;
+@Slf4j
public class InvertFullColorStrategy extends ReplaceAndInvertColorStrategy {
public InvertFullColorStrategy(MultipartFile file, ReplaceAndInvert replaceAndInvert) {
@@ -43,6 +47,8 @@ public class InvertFullColorStrategy extends ReplaceAndInvertColorStrategy {
try (PDDocument document = Loader.loadPDF(tempFile.getFile())) {
// Render each page and invert colors
PDFRenderer pdfRenderer = new PDFRenderer(document);
+ pdfRenderer.setSubsamplingAllowed(
+ true); // Enable subsampling to reduce memory usage
for (int page = 0; page < document.getNumberOfPages(); page++) {
BufferedImage image;
@@ -73,12 +79,27 @@ public class InvertFullColorStrategy extends ReplaceAndInvertColorStrategy {
PDImageXObject pdImage =
PDImageXObject.createFromFileByContent(tempImageFile, document);
+ // Delete temp file immediately after loading into memory to prevent disk
+ // exhaustion
+ // The file content is now in the PDImageXObject, so the file is no longer
+ // needed
+ try {
+ Files.deleteIfExists(tempImageFile.toPath());
+ tempImageFile = null; // Mark as deleted to avoid double deletion
+ } catch (IOException e) {
+ log.warn(
+ "Failed to delete temporary image file: {}",
+ tempImageFile.getAbsolutePath(),
+ e);
+ }
+
try (PDPageContentStream contentStream =
new PDPageContentStream(
document,
pdPage,
PDPageContentStream.AppendMode.OVERWRITE,
- true)) {
+ true,
+ true)) { // resetContext=true ensures clean graphics state
contentStream.drawImage(
pdImage,
0,
@@ -87,8 +108,16 @@ public class InvertFullColorStrategy extends ReplaceAndInvertColorStrategy {
pdPage.getMediaBox().getHeight());
}
} finally {
+ // Safety net: ensure temp file is deleted even if an exception occurred
if (tempImageFile != null && tempImageFile.exists()) {
- Files.delete(tempImageFile.toPath());
+ try {
+ Files.deleteIfExists(tempImageFile.toPath());
+ } catch (IOException e) {
+ log.warn(
+ "Failed to delete temporary image file: {}",
+ tempImageFile.getAbsolutePath(),
+ e);
+ }
}
}
}
@@ -128,7 +157,10 @@ public class InvertFullColorStrategy extends ReplaceAndInvertColorStrategy {
// Helper method to convert BufferedImage to InputStream
private File convertToBufferedImageTpFile(BufferedImage image) throws IOException {
- File file = File.createTempFile("image", ".png");
+ // Use Files.createTempFile instead of File.createTempFile for better security and modern
+ // Java practices
+ Path tempPath = Files.createTempFile("image", ".png");
+ File file = tempPath.toFile();
ImageIO.write(image, "png", file);
return file;
}
diff --git a/app/common/src/test/java/stirling/software/common/util/WebResponseUtilsTest.java b/app/common/src/test/java/stirling/software/common/util/WebResponseUtilsTest.java
index 1aac5ff85..86be0c812 100644
--- a/app/common/src/test/java/stirling/software/common/util/WebResponseUtilsTest.java
+++ b/app/common/src/test/java/stirling/software/common/util/WebResponseUtilsTest.java
@@ -92,8 +92,7 @@ public class WebResponseUtilsTest {
@Test
public void testPdfDocToWebResponse() {
- try {
- PDDocument document = new PDDocument();
+ try (PDDocument document = new PDDocument()) {
document.addPage(new org.apache.pdfbox.pdmodel.PDPage());
String docName = "sample.pdf";
diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/BookletImpositionController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/BookletImpositionController.java
index 12bcdf411..fdf3c716f 100644
--- a/app/core/src/main/java/stirling/software/SPDF/controller/api/BookletImpositionController.java
+++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/BookletImpositionController.java
@@ -29,6 +29,7 @@ import lombok.RequiredArgsConstructor;
import stirling.software.SPDF.model.api.general.BookletImpositionRequest;
import stirling.software.common.annotations.AutoJobPostMapping;
import stirling.software.common.service.CustomPDFDocumentFactory;
+import stirling.software.common.util.GeneralUtils;
import stirling.software.common.util.WebResponseUtils;
@RestController
@@ -68,33 +69,33 @@ public class BookletImpositionController {
"Booklet printing uses 2 pages per side (landscape). For 4-up, use the N-up feature.");
}
- PDDocument sourceDocument = pdfDocumentFactory.load(file);
- int totalPages = sourceDocument.getNumberOfPages();
+ try (PDDocument sourceDocument = pdfDocumentFactory.load(file)) {
+ int totalPages = sourceDocument.getNumberOfPages();
- // Create proper booklet with signature-based page ordering
- PDDocument newDocument =
- createSaddleBooklet(
- sourceDocument,
- totalPages,
- addBorder,
- spineLocation,
- addGutter,
- gutterSize,
- doubleSided,
- duplexPass,
- flipOnShortEdge);
+ // Create proper booklet with signature-based page ordering
+ try (PDDocument newDocument =
+ createSaddleBooklet(
+ sourceDocument,
+ totalPages,
+ addBorder,
+ spineLocation,
+ addGutter,
+ gutterSize,
+ doubleSided,
+ duplexPass,
+ flipOnShortEdge)) {
- sourceDocument.close();
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ newDocument.save(baos);
- ByteArrayOutputStream baos = new ByteArrayOutputStream();
- newDocument.save(baos);
- newDocument.close();
-
- byte[] result = baos.toByteArray();
- return WebResponseUtils.bytesToWebResponse(
- result,
- Filenames.toSimpleFileName(file.getOriginalFilename()).replaceFirst("[.][^.]+$", "")
- + "_booklet.pdf");
+ byte[] result = baos.toByteArray();
+ return WebResponseUtils.bytesToWebResponse(
+ result,
+ GeneralUtils.generateFilename(
+ Filenames.toSimpleFileName(file.getOriginalFilename()),
+ "_booklet.pdf"));
+ }
+ }
}
private static int padToMultipleOf4(int n) {
diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/CropController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/CropController.java
index 7db4265bd..39f4687f0 100644
--- a/app/core/src/main/java/stirling/software/SPDF/controller/api/CropController.java
+++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/CropController.java
@@ -155,6 +155,7 @@ public class CropController {
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);
for (int i = 0; i < sourceDocument.getNumberOfPages(); i++) {
diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/MultiPageLayoutController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/MultiPageLayoutController.java
index 87a3d61b8..61965b79e 100644
--- a/app/core/src/main/java/stirling/software/SPDF/controller/api/MultiPageLayoutController.java
+++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/MultiPageLayoutController.java
@@ -70,108 +70,109 @@ public class MultiPageLayoutController {
: (int) Math.sqrt(pagesPerSheet);
int rows = pagesPerSheet == 2 || pagesPerSheet == 3 ? 1 : (int) Math.sqrt(pagesPerSheet);
- PDDocument sourceDocument = pdfDocumentFactory.load(file);
- PDDocument newDocument =
- pdfDocumentFactory.createNewDocumentBasedOnOldDocument(sourceDocument);
- PDPage newPage = new PDPage(PDRectangle.A4);
- newDocument.addPage(newPage);
+ try (PDDocument sourceDocument = pdfDocumentFactory.load(file)) {
+ try (PDDocument newDocument =
+ pdfDocumentFactory.createNewDocumentBasedOnOldDocument(sourceDocument)) {
+ int totalPages = sourceDocument.getNumberOfPages();
+ LayerUtility layerUtility = new LayerUtility(newDocument);
- int totalPages = sourceDocument.getNumberOfPages();
- float cellWidth = newPage.getMediaBox().getWidth() / cols;
- float cellHeight = newPage.getMediaBox().getHeight() / rows;
+ // Calculate cell dimensions once (all output pages are A4) - declare outside try
+ // blocks
+ float cellWidth = PDRectangle.A4.getWidth() / cols;
+ float cellHeight = PDRectangle.A4.getHeight() / rows;
- PDPageContentStream contentStream =
- new PDPageContentStream(
- newDocument, newPage, PDPageContentStream.AppendMode.APPEND, true, true);
- LayerUtility layerUtility = new LayerUtility(newDocument);
+ // Process pages in groups of pagesPerSheet, creating a new page and content stream
+ // for each group
+ for (int i = 0; i < totalPages; i += pagesPerSheet) {
+ // Create a new output page for each group of pagesPerSheet
+ PDPage newPage = new PDPage(PDRectangle.A4);
+ newDocument.addPage(newPage);
- float borderThickness = 1.5f; // Specify border thickness as required
- contentStream.setLineWidth(borderThickness);
- contentStream.setStrokingColor(Color.BLACK);
+ // Use try-with-resources for each content stream to ensure proper cleanup
+ // resetContext=true: Start with a clean graphics state for new content
+ try (PDPageContentStream contentStream =
+ new PDPageContentStream(
+ newDocument,
+ newPage,
+ PDPageContentStream.AppendMode.APPEND,
+ true,
+ true)) {
+ float borderThickness = 1.5f; // Specify border thickness as required
+ contentStream.setLineWidth(borderThickness);
+ contentStream.setStrokingColor(Color.BLACK);
- for (int i = 0; i < totalPages; i++) {
- if (i != 0 && i % pagesPerSheet == 0) {
- // Close the current content stream and create a new page and content stream
- contentStream.close();
- newPage = new PDPage(PDRectangle.A4);
- newDocument.addPage(newPage);
- contentStream =
- new PDPageContentStream(
+ // Process all pages in this group
+ for (int j = 0; j < pagesPerSheet && (i + j) < totalPages; j++) {
+ int pageIndex = i + j;
+ PDPage sourcePage = sourceDocument.getPage(pageIndex);
+ PDRectangle rect = sourcePage.getMediaBox();
+ float scaleWidth = cellWidth / rect.getWidth();
+ float scaleHeight = cellHeight / rect.getHeight();
+ float scale = Math.min(scaleWidth, scaleHeight);
+
+ int adjustedPageIndex = j % pagesPerSheet;
+ int rowIndex = adjustedPageIndex / cols;
+ int colIndex = adjustedPageIndex % cols;
+
+ float x =
+ colIndex * cellWidth
+ + (cellWidth - rect.getWidth() * scale) / 2;
+ float y =
+ newPage.getMediaBox().getHeight()
+ - ((rowIndex + 1) * cellHeight
+ - (cellHeight - rect.getHeight() * scale) / 2);
+
+ contentStream.saveGraphicsState();
+ contentStream.transform(Matrix.getTranslateInstance(x, y));
+ contentStream.transform(Matrix.getScaleInstance(scale, scale));
+
+ PDFormXObject formXObject =
+ layerUtility.importPageAsForm(sourceDocument, pageIndex);
+ contentStream.drawForm(formXObject);
+
+ contentStream.restoreGraphicsState();
+
+ if (addBorder) {
+ // Draw border around each page
+ float borderX = colIndex * cellWidth;
+ float borderY =
+ newPage.getMediaBox().getHeight()
+ - (rowIndex + 1) * cellHeight;
+ contentStream.addRect(borderX, borderY, cellWidth, cellHeight);
+ contentStream.stroke();
+ }
+ }
+ } // contentStream is automatically closed here
+ }
+
+ // If any source page is rotated, skip form copying/transformation entirely
+ boolean hasRotation = GeneralFormCopyUtils.hasAnyRotatedPage(sourceDocument);
+ if (hasRotation) {
+ log.info("Source document has rotated pages; skipping form field copying.");
+ } else {
+ try {
+ GeneralFormCopyUtils.copyAndTransformFormFields(
+ sourceDocument,
newDocument,
- newPage,
- PDPageContentStream.AppendMode.APPEND,
- true,
- true);
- }
+ totalPages,
+ pagesPerSheet,
+ cols,
+ rows,
+ cellWidth,
+ cellHeight);
+ } catch (Exception e) {
+ log.warn("Failed to copy and transform form fields: {}", e.getMessage(), e);
+ }
+ }
- PDPage sourcePage = sourceDocument.getPage(i);
- PDRectangle rect = sourcePage.getMediaBox();
- float scaleWidth = cellWidth / rect.getWidth();
- float scaleHeight = cellHeight / rect.getHeight();
- float scale = Math.min(scaleWidth, scaleHeight);
-
- int adjustedPageIndex =
- i % pagesPerSheet; // Close the current content stream and create a new
- // page and content stream
- int rowIndex = adjustedPageIndex / cols;
- int colIndex = adjustedPageIndex % cols;
-
- float x = colIndex * cellWidth + (cellWidth - rect.getWidth() * scale) / 2;
- float y =
- newPage.getMediaBox().getHeight()
- - ((rowIndex + 1) * cellHeight
- - (cellHeight - rect.getHeight() * scale) / 2);
-
- contentStream.saveGraphicsState();
- contentStream.transform(Matrix.getTranslateInstance(x, y));
- contentStream.transform(Matrix.getScaleInstance(scale, scale));
-
- PDFormXObject formXObject = layerUtility.importPageAsForm(sourceDocument, i);
- contentStream.drawForm(formXObject);
-
- contentStream.restoreGraphicsState();
-
- if (addBorder) {
- // Draw border around each page
- float borderX = colIndex * cellWidth;
- float borderY = newPage.getMediaBox().getHeight() - (rowIndex + 1) * cellHeight;
- contentStream.addRect(borderX, borderY, cellWidth, cellHeight);
- contentStream.stroke();
- }
- }
-
- contentStream.close();
-
- // If any source page is rotated, skip form copying/transformation entirely
- boolean hasRotation = GeneralFormCopyUtils.hasAnyRotatedPage(sourceDocument);
- if (hasRotation) {
- log.info("Source document has rotated pages; skipping form field copying.");
- } else {
- try {
- GeneralFormCopyUtils.copyAndTransformFormFields(
- sourceDocument,
- newDocument,
- totalPages,
- pagesPerSheet,
- cols,
- rows,
- cellWidth,
- cellHeight);
- } catch (Exception e) {
- log.warn("Failed to copy and transform form fields: {}", e.getMessage(), e);
- }
- }
-
- sourceDocument.close();
-
- ByteArrayOutputStream baos = new ByteArrayOutputStream();
- newDocument.save(baos);
- newDocument.close();
-
- byte[] result = baos.toByteArray();
- return WebResponseUtils.bytesToWebResponse(
- result,
- GeneralUtils.generateFilename(
- file.getOriginalFilename(), "_multi_page_layout.pdf"));
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ newDocument.save(baos);
+ byte[] result = baos.toByteArray();
+ return WebResponseUtils.bytesToWebResponse(
+ result,
+ GeneralUtils.generateFilename(
+ file.getOriginalFilename(), "_multi_page_layout.pdf"));
+ } // newDocument is closed here
+ } // sourceDocument is closed here
}
}
diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/PdfImageRemovalController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/PdfImageRemovalController.java
index 9da42d057..edb66a87b 100644
--- a/app/core/src/main/java/stirling/software/SPDF/controller/api/PdfImageRemovalController.java
+++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/PdfImageRemovalController.java
@@ -53,25 +53,28 @@ public class PdfImageRemovalController {
"This endpoint remove images from file to reduce the file size.Input:PDF"
+ " Output:PDF Type:SISO")
public ResponseEntity removeImages(@ModelAttribute PDFFile file) throws IOException {
- // Load the PDF document
- PDDocument document = pdfDocumentFactory.load(file);
+ // Load the PDF document with proper resource management
+ try (PDDocument document = pdfDocumentFactory.load(file)) {
- // Remove images from the PDF document using the service
- PDDocument modifiedDocument = pdfImageRemovalService.removeImagesFromPdf(document);
+ // Remove images from the PDF document using the service
+ try (PDDocument modifiedDocument =
+ pdfImageRemovalService.removeImagesFromPdf(document)) {
- // Create a ByteArrayOutputStream to hold the modified PDF data
- ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+ // Create a ByteArrayOutputStream to hold the modified PDF data
+ ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
- // Save the modified PDF document to the output stream
- modifiedDocument.save(outputStream);
- modifiedDocument.close();
+ // Save the modified PDF document to the output stream
+ modifiedDocument.save(outputStream);
- // Generate a new filename for the modified PDF
- String mergedFileName =
- GeneralUtils.generateFilename(
- file.getFileInput().getOriginalFilename(), "_images_removed.pdf");
+ // Generate a new filename for the modified PDF
+ String mergedFileName =
+ GeneralUtils.generateFilename(
+ file.getFileInput().getOriginalFilename(), "_images_removed.pdf");
- // Convert the byte array to a web response and return it
- return WebResponseUtils.bytesToWebResponse(outputStream.toByteArray(), mergedFileName);
+ // Convert the byte array to a web response and return it
+ return WebResponseUtils.bytesToWebResponse(
+ outputStream.toByteArray(), mergedFileName);
+ }
+ }
}
}
diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/RearrangePagesPDFController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/RearrangePagesPDFController.java
index 8f2b46bb8..3507da95f 100644
--- a/app/core/src/main/java/stirling/software/SPDF/controller/api/RearrangePagesPDFController.java
+++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/RearrangePagesPDFController.java
@@ -50,23 +50,25 @@ public class RearrangePagesPDFController {
MultipartFile pdfFile = request.getFileInput();
String pagesToDelete = request.getPageNumbers();
- PDDocument document = pdfDocumentFactory.load(pdfFile);
+ try (PDDocument document = pdfDocumentFactory.load(pdfFile)) {
- // Split the page order string into an array of page numbers or range of numbers
- String[] pageOrderArr = pagesToDelete.split(",");
+ // Split the page order string into an array of page numbers or range of numbers
+ String[] pageOrderArr = pagesToDelete.split(",");
- List pagesToRemove =
- GeneralUtils.parsePageList(pageOrderArr, document.getNumberOfPages(), false);
+ List pagesToRemove =
+ GeneralUtils.parsePageList(pageOrderArr, document.getNumberOfPages(), false);
- Collections.sort(pagesToRemove);
+ Collections.sort(pagesToRemove);
- for (int i = pagesToRemove.size() - 1; i >= 0; i--) {
- int pageIndex = pagesToRemove.get(i);
- document.removePage(pageIndex);
+ for (int i = pagesToRemove.size() - 1; i >= 0; i--) {
+ int pageIndex = pagesToRemove.get(i);
+ document.removePage(pageIndex);
+ }
+ return WebResponseUtils.pdfDocToWebResponse(
+ document,
+ GeneralUtils.generateFilename(
+ pdfFile.getOriginalFilename(), "_removed_pages.pdf"));
}
- return WebResponseUtils.pdfDocToWebResponse(
- document,
- GeneralUtils.generateFilename(pdfFile.getOriginalFilename(), "_removed_pages.pdf"));
}
private List removeFirst(int totalPages) {
@@ -243,41 +245,43 @@ public class RearrangePagesPDFController {
String pageOrder = request.getPageNumbers();
String sortType = request.getCustomMode();
try {
- // Load the input PDF
- PDDocument document = pdfDocumentFactory.load(pdfFile);
+ // Load the input PDF with proper resource management
+ try (PDDocument document = pdfDocumentFactory.load(pdfFile)) {
- // Split the page order string into an array of page numbers or range of numbers
- String[] pageOrderArr = pageOrder != null ? pageOrder.split(",") : new String[0];
- int totalPages = document.getNumberOfPages();
- List newPageOrder;
- if (sortType != null
- && !sortType.isEmpty()
- && !"custom".equals(sortType.toLowerCase(Locale.ROOT))) {
- newPageOrder = processSortTypes(sortType, totalPages, pageOrder);
- } else {
- newPageOrder = GeneralUtils.parsePageList(pageOrderArr, totalPages, false);
+ // Split the page order string into an array of page numbers or range of numbers
+ String[] pageOrderArr = pageOrder != null ? pageOrder.split(",") : new String[0];
+ int totalPages = document.getNumberOfPages();
+ List newPageOrder;
+ if (sortType != null
+ && !sortType.isEmpty()
+ && !"custom".equals(sortType.toLowerCase(Locale.ROOT))) {
+ newPageOrder = processSortTypes(sortType, totalPages, pageOrder);
+ } else {
+ newPageOrder = GeneralUtils.parsePageList(pageOrderArr, totalPages, false);
+ }
+ log.info("newPageOrder = {}", newPageOrder);
+ log.info("totalPages = {}", totalPages);
+ // Create a new list to hold the pages in the new order
+ List newPages = new ArrayList<>();
+ for (int i = 0; i < newPageOrder.size(); i++) {
+ newPages.add(document.getPage(newPageOrder.get(i)));
+ }
+
+ // Create a new document based on the original one
+ try (PDDocument rearrangedDocument =
+ pdfDocumentFactory.createNewDocumentBasedOnOldDocument(document)) {
+
+ // Add the pages in the new order
+ for (PDPage page : newPages) {
+ rearrangedDocument.addPage(page);
+ }
+
+ return WebResponseUtils.pdfDocToWebResponse(
+ rearrangedDocument,
+ GeneralUtils.generateFilename(
+ pdfFile.getOriginalFilename(), "_rearranged.pdf"));
+ }
}
- log.info("newPageOrder = {}", newPageOrder);
- log.info("totalPages = {}", totalPages);
- // Create a new list to hold the pages in the new order
- List newPages = new ArrayList<>();
- for (int i = 0; i < newPageOrder.size(); i++) {
- newPages.add(document.getPage(newPageOrder.get(i)));
- }
-
- // Create a new document based on the original one
- PDDocument rearrangedDocument =
- pdfDocumentFactory.createNewDocumentBasedOnOldDocument(document);
-
- // Add the pages in the new order
- for (PDPage page : newPages) {
- rearrangedDocument.addPage(page);
- }
-
- return WebResponseUtils.pdfDocToWebResponse(
- rearrangedDocument,
- GeneralUtils.generateFilename(
- pdfFile.getOriginalFilename(), "_rearranged.pdf"));
} catch (IOException e) {
ExceptionUtils.logException("document rearrangement", e);
throw e;
diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/RotationController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/RotationController.java
index 25437b432..9b305fdc5 100644
--- a/app/core/src/main/java/stirling/software/SPDF/controller/api/RotationController.java
+++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/RotationController.java
@@ -47,19 +47,20 @@ public class RotationController {
"error.angleNotMultipleOf90", "Angle must be a multiple of 90");
}
- // Load the PDF document
- PDDocument document = pdfDocumentFactory.load(request);
+ // Load the PDF document with proper resource management
+ try (PDDocument document = pdfDocumentFactory.load(request)) {
- // Get the list of pages in the document
- PDPageTree pages = document.getPages();
+ // Get the list of pages in the document
+ PDPageTree pages = document.getPages();
- for (PDPage page : pages) {
- page.setRotation(page.getRotation() + angle);
+ for (PDPage page : pages) {
+ page.setRotation(page.getRotation() + angle);
+ }
+
+ // Return the rotated PDF as a response
+ return WebResponseUtils.pdfDocToWebResponse(
+ document,
+ GeneralUtils.generateFilename(pdfFile.getOriginalFilename(), "_rotated.pdf"));
}
-
- // Return the rotated PDF as a response
- return WebResponseUtils.pdfDocToWebResponse(
- document,
- GeneralUtils.generateFilename(pdfFile.getOriginalFilename(), "_rotated.pdf"));
}
}
diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/SplitPdfBySectionsController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/SplitPdfBySectionsController.java
index 9e54da906..91f892761 100644
--- a/app/core/src/main/java/stirling/software/SPDF/controller/api/SplitPdfBySectionsController.java
+++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/SplitPdfBySectionsController.java
@@ -4,6 +4,7 @@ import java.io.IOException;
import java.io.OutputStream;
import java.nio.file.Files;
import java.util.*;
+import java.util.stream.IntStream;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
@@ -264,27 +265,19 @@ public class SplitPdfBySectionsController {
break;
case SPLIT_ALL:
- for (int i = 0; i < totalPages; i++) {
- pagesToSplit.add(i);
- }
+ pagesToSplit.addAll(IntStream.range(0, totalPages).boxed().toList());
break;
case SPLIT_ALL_EXCEPT_FIRST:
- for (int i = 1; i < totalPages; i++) {
- pagesToSplit.add(i);
- }
+ pagesToSplit.addAll(IntStream.range(1, totalPages).boxed().toList());
break;
case SPLIT_ALL_EXCEPT_LAST:
- for (int i = 0; i < totalPages - 1; i++) {
- pagesToSplit.add(i);
- }
+ pagesToSplit.addAll(IntStream.range(0, totalPages - 1).boxed().toList());
break;
case SPLIT_ALL_EXCEPT_FIRST_AND_LAST:
- for (int i = 1; i < totalPages - 1; i++) {
- pagesToSplit.add(i);
- }
+ pagesToSplit.addAll(IntStream.range(1, totalPages - 1).boxed().toList());
break;
default:
diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/SplitPdfBySizeController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/SplitPdfBySizeController.java
index 879f1c857..f4fc0b8ff 100644
--- a/app/core/src/main/java/stirling/software/SPDF/controller/api/SplitPdfBySizeController.java
+++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/SplitPdfBySizeController.java
@@ -473,34 +473,36 @@ public class SplitPdfBySizeController {
PDDocument document, ZipOutputStream zipOut, String baseFilename, int index)
throws IOException {
log.debug("Starting saveDocumentToZip for document part {}", index);
- ByteArrayOutputStream outStream = new ByteArrayOutputStream();
+ try (ByteArrayOutputStream outStream = new ByteArrayOutputStream()) {
- try (PDDocument doc = document) {
- log.debug("Saving document part {} to byte array", index);
- doc.save(outStream);
- log.debug("Successfully saved document part {} ({} bytes)", index, outStream.size());
- } catch (Exception e) {
- log.error("Error saving document part {} to byte array", index, e);
- throw ExceptionUtils.createFileProcessingException("split", e);
- }
+ try (PDDocument doc = document) {
+ log.debug("Saving document part {} to byte array", index);
+ doc.save(outStream);
+ log.debug(
+ "Successfully saved document part {} ({} bytes)", index, outStream.size());
+ } catch (Exception e) {
+ log.error("Error saving document part {} to byte array", index, e);
+ throw ExceptionUtils.createFileProcessingException("split", e);
+ }
- try {
- // Create a new zip entry
- String entryName = baseFilename + "_" + index + ".pdf";
- log.debug("Creating ZIP entry: {}", entryName);
- ZipEntry zipEntry = new ZipEntry(entryName);
- zipOut.putNextEntry(zipEntry);
+ try {
+ // Create a new zip entry
+ String entryName = baseFilename + "_" + index + ".pdf";
+ log.debug("Creating ZIP entry: {}", entryName);
+ ZipEntry zipEntry = new ZipEntry(entryName);
+ zipOut.putNextEntry(zipEntry);
- byte[] bytes = outStream.toByteArray();
- log.debug("Writing {} bytes to ZIP entry", bytes.length);
- zipOut.write(bytes);
+ byte[] bytes = outStream.toByteArray();
+ log.debug("Writing {} bytes to ZIP entry", bytes.length);
+ zipOut.write(bytes);
- log.debug("Closing ZIP entry");
- zipOut.closeEntry();
- log.debug("Successfully added document part {} to ZIP", index);
- } catch (Exception e) {
- log.error("Error adding document part {} to ZIP", index, e);
- throw ExceptionUtils.createFileProcessingException("split", e);
+ log.debug("Closing ZIP entry");
+ zipOut.closeEntry();
+ log.debug("Successfully added document part {} to ZIP", index);
+ } catch (Exception e) {
+ log.error("Error adding document part {} to ZIP", index, e);
+ throw ExceptionUtils.createFileProcessingException("split", e);
+ }
}
}
}
diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertImgPDFController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertImgPDFController.java
index 4e9d7715c..2546479f0 100644
--- a/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertImgPDFController.java
+++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertImgPDFController.java
@@ -188,15 +188,15 @@ public class ConvertImgPDFController {
bodyBytes = Files.readAllBytes(webpFilePath);
} else {
// Create a ZIP file containing all WebP images
- ByteArrayOutputStream zipOutputStream = new ByteArrayOutputStream();
- try (ZipOutputStream zos = new ZipOutputStream(zipOutputStream)) {
+ try (ByteArrayOutputStream zipOutputStream = new ByteArrayOutputStream();
+ ZipOutputStream zos = new ZipOutputStream(zipOutputStream)) {
for (Path webpFile : webpFiles) {
zos.putNextEntry(new ZipEntry(webpFile.getFileName().toString()));
Files.copy(webpFile, zos);
zos.closeEntry();
}
+ bodyBytes = zipOutputStream.toByteArray();
}
- bodyBytes = zipOutputStream.toByteArray();
}
// Clean up the temporary files
Files.deleteIfExists(tempFile);
diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertOfficeController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertOfficeController.java
index fda633416..f62efeb75 100644
--- a/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertOfficeController.java
+++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertOfficeController.java
@@ -196,11 +196,12 @@ public class ConvertOfficeController {
try {
file = convertToPdf(inputFile);
- PDDocument doc = pdfDocumentFactory.load(file);
- return WebResponseUtils.pdfDocToWebResponse(
- doc,
- GeneralUtils.generateFilename(
- inputFile.getOriginalFilename(), "_convertedToPDF.pdf"));
+ try (PDDocument doc = pdfDocumentFactory.load(file)) {
+ return WebResponseUtils.pdfDocToWebResponse(
+ doc,
+ GeneralUtils.generateFilename(
+ inputFile.getOriginalFilename(), "_convertedToPDF.pdf"));
+ }
} finally {
if (file != null && file.getParent() != null) {
FileUtils.deleteDirectory(file.getParentFile());
diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertPDFToPDFA.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertPDFToPDFA.java
index f08675f7f..8f79e9e77 100644
--- a/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertPDFToPDFA.java
+++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertPDFToPDFA.java
@@ -51,7 +51,7 @@ import org.apache.pdfbox.pdmodel.graphics.image.LosslessFactory;
import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject;
import org.apache.pdfbox.pdmodel.graphics.optionalcontent.PDOptionalContentProperties;
import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotation;
-import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotationTextMarkup;
+import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotationHighlight;
import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotationWidget;
import org.apache.pdfbox.pdmodel.interactive.annotation.PDAppearanceDictionary;
import org.apache.pdfbox.pdmodel.interactive.viewerpreferences.PDViewerPreferences;
@@ -1211,7 +1211,7 @@ public class ConvertPDFToPDFA {
List annotations = page.getAnnotations();
for (PDAnnotation annot : annotations) {
if (ANNOTATION_HIGHLIGHT.equals(annot.getSubtype())
- && annot instanceof PDAnnotationTextMarkup highlight) {
+ && annot instanceof PDAnnotationHighlight highlight) {
float[] colorComponents =
highlight.getColor() != null
? highlight.getColor().getComponents()
@@ -1851,7 +1851,7 @@ public class ConvertPDFToPDFA {
doc, page, PDPageContentStream.AppendMode.PREPEND, true, true)) {
for (PDAnnotation annot : annotations) {
- if (annot instanceof PDAnnotationTextMarkup highlight
+ if (annot instanceof PDAnnotationHighlight highlight
&& ANNOTATION_HIGHLIGHT.equals(annot.getSubtype())) {
PDColor color = highlight.getColor();
@@ -1973,7 +1973,7 @@ public class ConvertPDFToPDFA {
return annot.getAppearance() != null;
}
- if (annot instanceof PDAnnotationTextMarkup) {
+ if (annot instanceof PDAnnotationHighlight) {
return false; // Will be handled by flattening
}
diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/AutoRenameController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/AutoRenameController.java
index 3d8a122ba..32eef1a1e 100644
--- a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/AutoRenameController.java
+++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/AutoRenameController.java
@@ -47,102 +47,105 @@ public class AutoRenameController {
MultipartFile file = request.getFileInput();
boolean useFirstTextAsFallback = Boolean.TRUE.equals(request.getUseFirstTextAsFallback());
- PDDocument document = pdfDocumentFactory.load(file);
- PDFTextStripper reader =
- new PDFTextStripper() {
- List lineInfos = new ArrayList<>();
- StringBuilder lineBuilder = new StringBuilder();
- float lastY = -1;
- float maxFontSizeInLine = 0.0f;
- int lineCount = 0;
+ try (PDDocument document = pdfDocumentFactory.load(file)) {
+ PDFTextStripper reader =
+ new PDFTextStripper() {
+ List lineInfos = new ArrayList<>();
+ StringBuilder lineBuilder = new StringBuilder();
+ float lastY = -1;
+ float maxFontSizeInLine = 0.0f;
+ int lineCount = 0;
- @Override
- protected void processTextPosition(TextPosition text) {
- if (lastY != text.getY() && lineCount < LINE_LIMIT) {
- processLine();
- lineBuilder = new StringBuilder(text.getUnicode());
- maxFontSizeInLine = text.getFontSizeInPt();
- lastY = text.getY();
- lineCount++;
- } else if (lineCount < LINE_LIMIT) {
- lineBuilder.append(text.getUnicode());
- if (text.getFontSizeInPt() > maxFontSizeInLine) {
+ @Override
+ protected void processTextPosition(TextPosition text) {
+ if (lastY != text.getY() && lineCount < LINE_LIMIT) {
+ processLine();
+ lineBuilder = new StringBuilder(text.getUnicode());
maxFontSizeInLine = text.getFontSizeInPt();
+ lastY = text.getY();
+ lineCount++;
+ } else if (lineCount < LINE_LIMIT) {
+ lineBuilder.append(text.getUnicode());
+ if (text.getFontSizeInPt() > maxFontSizeInLine) {
+ maxFontSizeInLine = text.getFontSizeInPt();
+ }
}
}
- }
- private void processLine() {
- if (!lineBuilder.isEmpty() && lineCount < LINE_LIMIT) {
- lineInfos.add(new LineInfo(lineBuilder.toString(), maxFontSizeInLine));
- }
- }
-
- @Override
- public String getText(PDDocument doc) throws IOException {
- this.lineInfos.clear();
- this.lineBuilder = new StringBuilder();
- this.lastY = -1;
- this.maxFontSizeInLine = 0.0f;
- this.lineCount = 0;
- super.getText(doc);
- processLine(); // Process the last line
-
- // Merge lines with same font size
- List mergedLineInfos = new ArrayList<>();
- for (int i = 0; i < lineInfos.size(); i++) {
- StringBuilder mergedText = new StringBuilder(lineInfos.get(i).text);
- float fontSize = lineInfos.get(i).fontSize;
- while (i + 1 < lineInfos.size()
- && lineInfos.get(i + 1).fontSize == fontSize) {
- mergedText.append(" ").append(lineInfos.get(i + 1).text);
- i++;
+ private void processLine() {
+ if (!lineBuilder.isEmpty() && lineCount < LINE_LIMIT) {
+ lineInfos.add(
+ new LineInfo(lineBuilder.toString(), maxFontSizeInLine));
}
- mergedLineInfos.add(new LineInfo(mergedText.toString(), fontSize));
}
- // Sort lines by font size in descending order and get the first one
- mergedLineInfos.sort(
- Comparator.comparing((LineInfo li) -> li.fontSize).reversed());
- String title =
- mergedLineInfos.isEmpty() ? null : mergedLineInfos.get(0).text;
+ @Override
+ public String getText(PDDocument doc) throws IOException {
+ this.lineInfos.clear();
+ this.lineBuilder = new StringBuilder();
+ this.lastY = -1;
+ this.maxFontSizeInLine = 0.0f;
+ this.lineCount = 0;
+ super.getText(doc);
+ processLine(); // Process the last line
- return title != null
- ? title
- : (useFirstTextAsFallback
- ? (mergedLineInfos.isEmpty()
- ? null
- : mergedLineInfos.get(mergedLineInfos.size() - 1)
- .text)
- : null);
- }
+ // Merge lines with same font size
+ List mergedLineInfos = new ArrayList<>();
+ for (int i = 0; i < lineInfos.size(); i++) {
+ StringBuilder mergedText = new StringBuilder(lineInfos.get(i).text);
+ float fontSize = lineInfos.get(i).fontSize;
+ while (i + 1 < lineInfos.size()
+ && lineInfos.get(i + 1).fontSize == fontSize) {
+ mergedText.append(" ").append(lineInfos.get(i + 1).text);
+ i++;
+ }
+ mergedLineInfos.add(new LineInfo(mergedText.toString(), fontSize));
+ }
- class LineInfo {
- String text;
- float fontSize;
+ // Sort lines by font size in descending order and get the first one
+ mergedLineInfos.sort(
+ Comparator.comparing((LineInfo li) -> li.fontSize).reversed());
+ String title =
+ mergedLineInfos.isEmpty() ? null : mergedLineInfos.get(0).text;
- LineInfo(String text, float fontSize) {
- this.text = text;
- this.fontSize = fontSize;
+ return title != null
+ ? title
+ : (useFirstTextAsFallback
+ ? (mergedLineInfos.isEmpty()
+ ? null
+ : mergedLineInfos.get(
+ mergedLineInfos.size() - 1)
+ .text)
+ : null);
}
- }
- };
- String header = reader.getText(document);
+ class LineInfo {
+ String text;
+ float fontSize;
- // Sanitize the header string by removing characters not allowed in a filename.
- if (header != null && header.length() < 255) {
- header =
- RegexPatternUtils.getInstance()
- .getSafeFilenamePattern()
- .matcher(header)
- .replaceAll("")
- .trim();
- return WebResponseUtils.pdfDocToWebResponse(document, header + ".pdf");
- } else {
- log.info("File has no good title to be found");
- return WebResponseUtils.pdfDocToWebResponse(
- document, Filenames.toSimpleFileName(file.getOriginalFilename()));
+ LineInfo(String text, float fontSize) {
+ this.text = text;
+ this.fontSize = fontSize;
+ }
+ }
+ };
+
+ String header = reader.getText(document);
+
+ // Sanitize the header string by removing characters not allowed in a filename.
+ if (header != null && header.length() < 255) {
+ header =
+ RegexPatternUtils.getInstance()
+ .getSafeFilenamePattern()
+ .matcher(header)
+ .replaceAll("")
+ .trim();
+ return WebResponseUtils.pdfDocToWebResponse(document, header + ".pdf");
+ } else {
+ log.info("File has no good title to be found");
+ return WebResponseUtils.pdfDocToWebResponse(
+ document, Filenames.toSimpleFileName(file.getOriginalFilename()));
+ }
}
}
}
diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/CompressController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/CompressController.java
index 83ae2b1a7..9b87512ae 100644
--- a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/CompressController.java
+++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/CompressController.java
@@ -1100,8 +1100,9 @@ public class CompressController {
inputFile.getOriginalFilename(), "_Optimized.pdf");
try {
- return WebResponseUtils.pdfDocToWebResponse(
- pdfDocumentFactory.load(currentFile.toFile()), outputFilename);
+ try (PDDocument document = pdfDocumentFactory.load(currentFile.toFile())) {
+ return WebResponseUtils.pdfDocToWebResponse(document, outputFilename);
+ }
} catch (IOException e) {
throw ExceptionUtils.handlePdfException(e, "PDF optimization");
}
diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/FlattenController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/FlattenController.java
index 28ec9f67e..eecf1269f 100644
--- a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/FlattenController.java
+++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/FlattenController.java
@@ -49,93 +49,103 @@ public class FlattenController {
public ResponseEntity flatten(@ModelAttribute FlattenRequest request) throws Exception {
MultipartFile file = request.getFileInput();
- PDDocument document = pdfDocumentFactory.load(file);
- Boolean flattenOnlyForms = request.getFlattenOnlyForms();
+ try (PDDocument document = pdfDocumentFactory.load(file)) {
+ Boolean flattenOnlyForms = request.getFlattenOnlyForms();
- if (Boolean.TRUE.equals(flattenOnlyForms)) {
- PDAcroForm acroForm = document.getDocumentCatalog().getAcroForm();
- if (acroForm != null) {
- acroForm.flatten();
- }
- return WebResponseUtils.pdfDocToWebResponse(
- document, Filenames.toSimpleFileName(file.getOriginalFilename()));
- } else {
- // flatten whole page aka convert each page to image and re-add it (making text
- // unselectable)
- PDFRenderer pdfRenderer = new PDFRenderer(document);
- PDDocument newDocument =
- pdfDocumentFactory.createNewDocumentBasedOnOldDocument(document);
+ if (Boolean.TRUE.equals(flattenOnlyForms)) {
+ PDAcroForm acroForm = document.getDocumentCatalog().getAcroForm();
+ if (acroForm != null) {
+ acroForm.flatten();
+ }
+ return WebResponseUtils.pdfDocToWebResponse(
+ document, Filenames.toSimpleFileName(file.getOriginalFilename()));
+ } else {
+ // flatten whole page aka convert each page to image and re-add it (making text
+ // unselectable)
+ PDFRenderer pdfRenderer = new PDFRenderer(document);
+ pdfRenderer.setSubsamplingAllowed(
+ true); // Enable subsampling to reduce memory usage
- int defaultRenderDpi = 100; // Default fallback
- ApplicationProperties properties =
- ApplicationContextProvider.getBean(ApplicationProperties.class);
- Integer configuredMaxDpi = null;
- if (properties != null && properties.getSystem() != null) {
- configuredMaxDpi = properties.getSystem().getMaxDPI();
- }
+ try (PDDocument newDocument =
+ pdfDocumentFactory.createNewDocumentBasedOnOldDocument(document)) {
- int maxDpi =
- (configuredMaxDpi != null && configuredMaxDpi > 0)
- ? configuredMaxDpi
- : defaultRenderDpi;
-
- Integer requestedDpi = request.getRenderDpi();
- int renderDpiTemp = maxDpi;
- if (requestedDpi != null) {
- renderDpiTemp = Math.min(requestedDpi, maxDpi);
- renderDpiTemp = Math.max(renderDpiTemp, 72);
- }
- final int renderDpi = renderDpiTemp;
-
- int numPages = document.getNumberOfPages();
- for (int i = 0; i < numPages; i++) {
- final int pageIndex = i;
- BufferedImage image = null;
- try {
- // Validate dimensions BEFORE rendering to prevent OOM
- ExceptionUtils.validateRenderingDimensions(
- document.getPage(pageIndex), pageIndex + 1, renderDpi);
-
- // Wrap entire rendering operation to catch OutOfMemoryError from any depth
- image =
- ExceptionUtils.handleOomRendering(
- pageIndex + 1,
- renderDpi,
- () ->
- pdfRenderer.renderImageWithDPI(
- pageIndex, renderDpi, ImageType.RGB));
-
- PDPage page = new PDPage();
- page.setMediaBox(document.getPage(i).getMediaBox());
- newDocument.addPage(page);
- try (PDPageContentStream contentStream =
- new PDPageContentStream(newDocument, page)) {
- PDImageXObject pdImage = JPEGFactory.createFromImage(newDocument, image);
- float pageWidth = page.getMediaBox().getWidth();
- float pageHeight = page.getMediaBox().getHeight();
-
- contentStream.drawImage(pdImage, 0, 0, pageWidth, pageHeight);
+ int defaultRenderDpi = 100; // Default fallback
+ ApplicationProperties properties =
+ ApplicationContextProvider.getBean(ApplicationProperties.class);
+ Integer configuredMaxDpi = null;
+ if (properties != null && properties.getSystem() != null) {
+ configuredMaxDpi = properties.getSystem().getMaxDPI();
}
- } catch (ExceptionUtils.OutOfMemoryDpiException e) {
- // Re-throw OutOfMemoryDpiException to be handled by GlobalExceptionHandler
- newDocument.close();
- document.close();
- throw e;
- } catch (IOException e) {
- log.error("IOException during page processing: ", e);
- // Continue processing other pages
- } catch (OutOfMemoryError e) {
- // Catch any OutOfMemoryError that escaped the inner try block
- newDocument.close();
- document.close();
- throw ExceptionUtils.createOutOfMemoryDpiException(i + 1, renderDpi, e);
- } finally {
- // Help GC by clearing the image reference
- image = null;
+
+ int maxDpi =
+ (configuredMaxDpi != null && configuredMaxDpi > 0)
+ ? configuredMaxDpi
+ : defaultRenderDpi;
+
+ Integer requestedDpi = request.getRenderDpi();
+ int renderDpiTemp = maxDpi;
+ if (requestedDpi != null) {
+ renderDpiTemp = Math.min(requestedDpi, maxDpi);
+ renderDpiTemp = Math.max(renderDpiTemp, 72);
+ }
+ final int renderDpi = renderDpiTemp;
+
+ int numPages = document.getNumberOfPages();
+ for (int i = 0; i < numPages; i++) {
+ final int pageIndex = i;
+ BufferedImage image = null;
+ try {
+ // Validate dimensions BEFORE rendering to prevent OOM
+ ExceptionUtils.validateRenderingDimensions(
+ document.getPage(pageIndex), pageIndex + 1, renderDpi);
+
+ // Wrap entire rendering operation to catch OutOfMemoryError from any
+ // depth
+ image =
+ ExceptionUtils.handleOomRendering(
+ pageIndex + 1,
+ renderDpi,
+ () ->
+ pdfRenderer.renderImageWithDPI(
+ pageIndex, renderDpi, ImageType.RGB));
+
+ PDPage page = new PDPage();
+ page.setMediaBox(document.getPage(i).getMediaBox());
+ newDocument.addPage(page);
+ // resetContext=true: Ensure clean graphics state when overwriting.
+ try (PDPageContentStream contentStream =
+ new PDPageContentStream(
+ newDocument,
+ page,
+ PDPageContentStream.AppendMode.OVERWRITE,
+ true,
+ true)) {
+ PDImageXObject pdImage =
+ JPEGFactory.createFromImage(newDocument, image);
+ float pageWidth = page.getMediaBox().getWidth();
+ float pageHeight = page.getMediaBox().getHeight();
+
+ contentStream.drawImage(pdImage, 0, 0, pageWidth, pageHeight);
+ }
+ } catch (ExceptionUtils.OutOfMemoryDpiException e) {
+ // Re-throw OutOfMemoryDpiException to be handled by
+ // GlobalExceptionHandler
+ throw e;
+ } catch (IOException e) {
+ log.error("IOException during page processing: ", e);
+ // Continue processing other pages
+ } catch (OutOfMemoryError e) {
+ // Catch any OutOfMemoryError that escaped the inner try block
+ throw ExceptionUtils.createOutOfMemoryDpiException(i + 1, renderDpi, e);
+ } finally {
+ // Help GC by clearing the image reference
+ image = null;
+ }
+ }
+ return WebResponseUtils.pdfDocToWebResponse(
+ newDocument, Filenames.toSimpleFileName(file.getOriginalFilename()));
}
}
- return WebResponseUtils.pdfDocToWebResponse(
- newDocument, Filenames.toSimpleFileName(file.getOriginalFilename()));
}
}
}
diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/MetadataController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/MetadataController.java
index 449313d0b..1eb8f9268 100644
--- a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/MetadataController.java
+++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/MetadataController.java
@@ -84,93 +84,102 @@ public class MetadataController {
if (allRequestParams == null) {
allRequestParams = new java.util.HashMap();
}
- // Load the PDF file into a PDDocument
- PDDocument document = pdfDocumentFactory.load(pdfFile, true);
+ // Load the PDF file into a PDDocument with proper resource management
+ try (PDDocument document = pdfDocumentFactory.load(pdfFile, true)) {
- // Get the document information from the PDF
- PDDocumentInformation info = document.getDocumentInformation();
+ // Get the document information from the PDF
+ PDDocumentInformation info = document.getDocumentInformation();
- // Check if each metadata value is "undefined" and set it to null if it is
- author = checkUndefined(author);
- creationDate = checkUndefined(creationDate);
- creator = checkUndefined(creator);
- keywords = checkUndefined(keywords);
- modificationDate = checkUndefined(modificationDate);
- producer = checkUndefined(producer);
- subject = checkUndefined(subject);
- title = checkUndefined(title);
- trapped = checkUndefined(trapped);
+ // Check if each metadata value is "undefined" and set it to null if it is
+ author = checkUndefined(author);
+ creationDate = checkUndefined(creationDate);
+ creator = checkUndefined(creator);
+ keywords = checkUndefined(keywords);
+ modificationDate = checkUndefined(modificationDate);
+ producer = checkUndefined(producer);
+ subject = checkUndefined(subject);
+ title = checkUndefined(title);
+ trapped = checkUndefined(trapped);
- // If the "deleteAll" flag is set, remove all metadata from the document
- // information
- if (deleteAll) {
- for (String key : info.getMetadataKeys()) {
- info.setCustomMetadataValue(key, null);
- }
- // Remove metadata from the PDF history
- document.getDocumentCatalog().getCOSObject().removeItem(COSName.getPDFName("Metadata"));
- document.getDocumentCatalog()
- .getCOSObject()
- .removeItem(COSName.getPDFName("PieceInfo"));
- author = null;
- creationDate = null;
- creator = null;
- keywords = null;
- modificationDate = null;
- producer = null;
- subject = null;
- title = null;
- trapped = null;
- } else {
- // Iterate through the request parameters and set the metadata values
- for (Entry entry : allRequestParams.entrySet()) {
- String key = entry.getKey();
- // Check if the key is a standard metadata key
- if (!"Author".equalsIgnoreCase(key)
- && !"CreationDate".equalsIgnoreCase(key)
- && !"Creator".equalsIgnoreCase(key)
- && !"Keywords".equalsIgnoreCase(key)
- && !"modificationDate".equalsIgnoreCase(key)
- && !"Producer".equalsIgnoreCase(key)
- && !"Subject".equalsIgnoreCase(key)
- && !"Title".equalsIgnoreCase(key)
- && !"Trapped".equalsIgnoreCase(key)
- && !key.contains("customKey")
- && !key.contains("customValue")) {
- info.setCustomMetadataValue(key, entry.getValue());
- } else if (key.contains("customKey")) {
- int number =
- Integer.parseInt(
- RegexPatternUtils.getInstance()
- .getNumericExtractionPattern()
- .matcher(key)
- .replaceAll(""));
- String customKey = entry.getValue();
- String customValue = allRequestParams.get("customValue" + number);
- info.setCustomMetadataValue(customKey, customValue);
+ // If the "deleteAll" flag is set, remove all metadata from the document
+ // information
+ if (deleteAll) {
+ for (String key : info.getMetadataKeys()) {
+ info.setCustomMetadataValue(key, null);
+ }
+ // Remove metadata from the PDF history
+ document.getDocumentCatalog()
+ .getCOSObject()
+ .removeItem(COSName.getPDFName("Metadata"));
+ document.getDocumentCatalog()
+ .getCOSObject()
+ .removeItem(COSName.getPDFName("PieceInfo"));
+ author = null;
+ creationDate = null;
+ creator = null;
+ keywords = null;
+ modificationDate = null;
+ producer = null;
+ subject = null;
+ title = null;
+ trapped = null;
+ } else {
+ // Iterate through the request parameters and set the metadata values
+ for (Entry entry : allRequestParams.entrySet()) {
+ String key = entry.getKey();
+ // Check if the key is a standard metadata key
+ if (!"Author".equalsIgnoreCase(key)
+ && !"CreationDate".equalsIgnoreCase(key)
+ && !"Creator".equalsIgnoreCase(key)
+ && !"Keywords".equalsIgnoreCase(key)
+ && !"modificationDate".equalsIgnoreCase(key)
+ && !"Producer".equalsIgnoreCase(key)
+ && !"Subject".equalsIgnoreCase(key)
+ && !"Title".equalsIgnoreCase(key)
+ && !"Trapped".equalsIgnoreCase(key)
+ && !key.contains("customKey")
+ && !key.contains("customValue")) {
+ info.setCustomMetadataValue(key, entry.getValue());
+ } else if (key.contains("customKey")) {
+ try {
+ int number =
+ Integer.parseInt(
+ RegexPatternUtils.getInstance()
+ .getNumericExtractionPattern()
+ .matcher(key)
+ .replaceAll(""));
+ String customKey = entry.getValue();
+ String customValue = allRequestParams.get("customValue" + number);
+ info.setCustomMetadataValue(customKey, customValue);
+ } catch (NumberFormatException e) {
+ // Skip invalid custom key entries that don't have valid numeric
+ // suffixes
+ log.warn("Skipping invalid custom key '{}': {}", key, e.getMessage());
+ }
+ }
}
}
+ // Set creation date using utility method
+ Calendar creationDateCal = PdfMetadataService.parseToCalendar(creationDate);
+ info.setCreationDate(creationDateCal);
+
+ // Set modification date using utility method
+ Calendar modificationDateCal = PdfMetadataService.parseToCalendar(modificationDate);
+ info.setModificationDate(modificationDateCal);
+ info.setCreator(creator);
+ info.setKeywords(keywords);
+ info.setAuthor(author);
+ info.setProducer(producer);
+ info.setSubject(subject);
+ info.setTitle(title);
+ info.setTrapped(trapped);
+
+ document.setDocumentInformation(info);
+ return WebResponseUtils.pdfDocToWebResponse(
+ document,
+ GeneralUtils.removeExtension(
+ Filenames.toSimpleFileName(pdfFile.getOriginalFilename()))
+ + "_metadata.pdf");
}
- // Set creation date using utility method
- Calendar creationDateCal = PdfMetadataService.parseToCalendar(creationDate);
- info.setCreationDate(creationDateCal);
-
- // Set modification date using utility method
- Calendar modificationDateCal = PdfMetadataService.parseToCalendar(modificationDate);
- info.setModificationDate(modificationDateCal);
- info.setCreator(creator);
- info.setKeywords(keywords);
- info.setAuthor(author);
- info.setProducer(producer);
- info.setSubject(subject);
- info.setTitle(title);
- info.setTrapped(trapped);
-
- document.setDocumentInformation(info);
- return WebResponseUtils.pdfDocToWebResponse(
- document,
- GeneralUtils.removeExtension(
- Filenames.toSimpleFileName(pdfFile.getOriginalFilename()))
- + "_metadata.pdf");
}
}
diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/OCRController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/OCRController.java
index 72b8c95d9..56edcd162 100644
--- a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/OCRController.java
+++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/OCRController.java
@@ -15,6 +15,7 @@ import java.util.zip.ZipOutputStream;
import javax.imageio.ImageIO;
+import org.apache.pdfbox.io.IOUtils;
import org.apache.pdfbox.multipdf.PDFMergerUtility;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage;
@@ -326,6 +327,8 @@ public class OCRController {
try (PDDocument document = pdfDocumentFactory.load(tempInputFile.toFile())) {
PDFRenderer pdfRenderer = new PDFRenderer(document);
+ pdfRenderer.setSubsamplingAllowed(
+ true); // Enable subsampling to reduce memory usage
int pageCount = document.getNumberOfPages();
for (int pageNum = 0; pageNum < pageCount; pageNum++) {
@@ -415,7 +418,7 @@ public class OCRController {
}
// Merge all pages into final PDF
- merger.mergeDocuments(null);
+ merger.mergeDocuments(IOUtils.createTempFileOnlyStreamCache());
// Copy final output to the expected location
Files.copy(
diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/PageNumbersController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/PageNumbersController.java
index 3915ada1e..41cbe7328 100644
--- a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/PageNumbersController.java
+++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/PageNumbersController.java
@@ -65,113 +65,114 @@ public class PageNumbersController {
}
}
- PDDocument document = pdfDocumentFactory.load(file);
-
- float marginFactor =
- switch (customMargin == null ? "" : customMargin.toLowerCase(Locale.ROOT)) {
- case "small" -> 0.02f;
- case "large" -> 0.05f;
- case "x-large" -> 0.075f;
- case "medium" -> 0.035f;
- default -> 0.035f;
- };
-
- if (pagesToNumber == null || pagesToNumber.isEmpty()) {
- pagesToNumber = "all";
- }
- if (customText == null || customText.isEmpty()) {
- customText = "{n}";
- }
-
- final String baseFilename =
- Filenames.toSimpleFileName(file.getOriginalFilename())
- .replaceFirst("[.][^.]+$", "");
-
- List pagesToNumberList =
- GeneralUtils.parsePageList(pagesToNumber.split(","), document.getNumberOfPages());
-
- // Clamp position to 1..9 (1 = top-left, 9 = bottom-right)
- int pos = Math.max(1, Math.min(9, position));
-
- for (int i : pagesToNumberList) {
- PDPage page = document.getPage(i);
- PDRectangle pageSize = page.getMediaBox();
-
- String text =
- customText
- .replace("{n}", String.valueOf(pageNumber))
- .replace("{total}", String.valueOf(document.getNumberOfPages()))
- .replace(
- "{filename}",
- GeneralUtils.removeExtension(
- Filenames.toSimpleFileName(
- file.getOriginalFilename())));
-
- PDType1Font currentFont =
- switch (fontType == null ? "" : fontType.toLowerCase(Locale.ROOT)) {
- case "courier" -> new PDType1Font(Standard14Fonts.FontName.COURIER);
- case "times" -> new PDType1Font(Standard14Fonts.FontName.TIMES_ROMAN);
- default -> new PDType1Font(Standard14Fonts.FontName.HELVETICA);
+ try (PDDocument document = pdfDocumentFactory.load(file)) {
+ float marginFactor =
+ switch (customMargin == null ? "" : customMargin.toLowerCase(Locale.ROOT)) {
+ case "small" -> 0.02f;
+ case "large" -> 0.05f;
+ case "x-large" -> 0.075f;
+ case "medium" -> 0.035f;
+ default -> 0.035f;
};
- // Text dimensions and font metrics
- float textWidth = currentFont.getStringWidth(text) / 1000f * fontSize;
- float ascent = currentFont.getFontDescriptor().getAscent() / 1000f * fontSize;
- float descent = currentFont.getFontDescriptor().getDescent() / 1000f * fontSize;
-
- // Derive column/row in range 1..3 (1 = left/top, 2 = center/middle, 3 = right/bottom)
- int col = ((pos - 1) % 3) + 1; // 1 = left, 2 = center, 3 = right
- int row = ((pos - 1) / 3) + 1; // 1 = top, 2 = middle, 3 = bottom
-
- // Anchor coordinates with margin
- float leftX = pageSize.getLowerLeftX() + marginFactor * pageSize.getWidth();
- float midX = pageSize.getLowerLeftX() + pageSize.getWidth() / 2f;
- float rightX = pageSize.getUpperRightX() - marginFactor * pageSize.getWidth();
-
- float botY = pageSize.getLowerLeftY() + marginFactor * pageSize.getHeight();
- float midY = pageSize.getLowerLeftY() + pageSize.getHeight() / 2f;
- float topY = pageSize.getUpperRightY() - marginFactor * pageSize.getHeight();
-
- // Horizontal alignment: left = anchor, center = centered, right = right-aligned
- float x =
- switch (col) {
- case 1 -> leftX;
- case 2 -> midX - textWidth / 2f;
- default -> rightX - textWidth;
- };
-
- // Vertical alignment (baseline!):
- // top = align text top at topY,
- // middle = optical middle using ascent/descent,
- // bottom = baseline at botY
- float y =
- switch (row) {
- case 1 -> topY - ascent;
- case 2 -> midY - (ascent + descent) / 2f;
- default -> botY;
- };
-
- try (PDPageContentStream contentStream =
- new PDPageContentStream(
- document, page, PDPageContentStream.AppendMode.APPEND, true, true)) {
- contentStream.beginText();
- contentStream.setFont(currentFont, fontSize);
- contentStream.setNonStrokingColor(color);
- contentStream.newLineAtOffset(x, y);
- contentStream.showText(text);
- contentStream.endText();
+ if (pagesToNumber == null || pagesToNumber.isEmpty()) {
+ pagesToNumber = "all";
+ }
+ if (customText == null || customText.isEmpty()) {
+ customText = "{n}";
}
- pageNumber++;
+ List pagesToNumberList =
+ GeneralUtils.parsePageList(
+ pagesToNumber.split(","), document.getNumberOfPages());
+
+ // Clamp position to 1..9 (1 = top-left, 9 = bottom-right)
+ int pos = Math.max(1, Math.min(9, position));
+
+ for (int i : pagesToNumberList) {
+ PDPage page = document.getPage(i);
+ PDRectangle pageSize = page.getMediaBox();
+
+ String text =
+ customText
+ .replace("{n}", String.valueOf(pageNumber))
+ .replace("{total}", String.valueOf(document.getNumberOfPages()))
+ .replace(
+ "{filename}",
+ GeneralUtils.removeExtension(
+ Filenames.toSimpleFileName(
+ file.getOriginalFilename())));
+
+ PDType1Font currentFont =
+ switch (fontType == null ? "" : fontType.toLowerCase(Locale.ROOT)) {
+ case "courier" -> new PDType1Font(Standard14Fonts.FontName.COURIER);
+ case "times" -> new PDType1Font(Standard14Fonts.FontName.TIMES_ROMAN);
+ default -> new PDType1Font(Standard14Fonts.FontName.HELVETICA);
+ };
+
+ // Text dimensions and font metrics
+ float textWidth = currentFont.getStringWidth(text) / 1000f * fontSize;
+ float ascent = currentFont.getFontDescriptor().getAscent() / 1000f * fontSize;
+ float descent = currentFont.getFontDescriptor().getDescent() / 1000f * fontSize;
+
+ // Derive column/row in range 1..3 (1 = left/top, 2 = center/middle, 3 =
+ // right/bottom)
+ int col = ((pos - 1) % 3) + 1; // 1 = left, 2 = center, 3 = right
+ int row = ((pos - 1) / 3) + 1; // 1 = top, 2 = middle, 3 = bottom
+
+ // Anchor coordinates with margin
+ float leftX = pageSize.getLowerLeftX() + marginFactor * pageSize.getWidth();
+ float midX = pageSize.getLowerLeftX() + pageSize.getWidth() / 2f;
+ float rightX = pageSize.getUpperRightX() - marginFactor * pageSize.getWidth();
+
+ float botY = pageSize.getLowerLeftY() + marginFactor * pageSize.getHeight();
+ float midY = pageSize.getLowerLeftY() + pageSize.getHeight() / 2f;
+ float topY = pageSize.getUpperRightY() - marginFactor * pageSize.getHeight();
+
+ // Horizontal alignment: left = anchor, center = centered, right = right-aligned
+ float x =
+ switch (col) {
+ case 1 -> leftX;
+ case 2 -> midX - textWidth / 2f;
+ default -> rightX - textWidth;
+ };
+
+ // Vertical alignment (baseline!):
+ // top = align text top at topY,
+ // middle = optical middle using ascent/descent,
+ // bottom = baseline at botY
+ float y =
+ switch (row) {
+ case 1 -> topY - ascent;
+ case 2 -> midY - (ascent + descent) / 2f;
+ default -> botY;
+ };
+
+ try (PDPageContentStream contentStream =
+ new PDPageContentStream(
+ document,
+ page,
+ PDPageContentStream.AppendMode.APPEND,
+ true,
+ true)) {
+ contentStream.beginText();
+ contentStream.setFont(currentFont, fontSize);
+ contentStream.setNonStrokingColor(color);
+ contentStream.newLineAtOffset(x, y);
+ contentStream.showText(text);
+ contentStream.endText();
+ }
+
+ pageNumber++;
+ }
+
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ document.save(baos);
+
+ return WebResponseUtils.bytesToWebResponse(
+ baos.toByteArray(),
+ GeneralUtils.generateFilename(
+ file.getOriginalFilename(), "_page_numbers_added.pdf"));
}
-
- ByteArrayOutputStream baos = new ByteArrayOutputStream();
- document.save(baos);
- document.close();
-
- return WebResponseUtils.bytesToWebResponse(
- baos.toByteArray(),
- GeneralUtils.generateFilename(
- file.getOriginalFilename(), "_page_numbers_added.pdf"));
}
}
diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/PrintFileController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/PrintFileController.java
index 484aa0909..48a9b5586 100644
--- a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/PrintFileController.java
+++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/PrintFileController.java
@@ -7,7 +7,10 @@ import java.awt.print.Printable;
import java.awt.print.PrinterException;
import java.awt.print.PrinterJob;
import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
import java.nio.file.Paths;
+import java.nio.file.StandardCopyOption;
import java.util.Arrays;
import java.util.Locale;
@@ -77,11 +80,19 @@ public class PrintFileController {
log.info("Selected Printer: {}", selectedService.getName());
if (MediaType.APPLICATION_PDF_VALUE.equals(contentType)) {
- try (PDDocument document = Loader.loadPDF(file.getBytes())) {
- PrinterJob job = PrinterJob.getPrinterJob();
- job.setPrintService(selectedService);
- job.setPageable(new PDFPageable(document));
- job.print();
+ // Use Stream-to-File pattern: write to temp file first, then load from file
+ Path tempFile = Files.createTempFile("print-", ".pdf");
+ try {
+ Files.copy(
+ file.getInputStream(), tempFile, StandardCopyOption.REPLACE_EXISTING);
+ try (PDDocument document = Loader.loadPDF(tempFile.toFile())) {
+ PrinterJob job = PrinterJob.getPrinterJob();
+ job.setPrintService(selectedService);
+ job.setPageable(new PDFPageable(document));
+ job.print();
+ }
+ } finally {
+ Files.deleteIfExists(tempFile);
}
} else if (contentType.startsWith("image/")) {
try (var inputStream = file.getInputStream()) {
diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/ScannerEffectController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/ScannerEffectController.java
index ad7ce7ce1..e581eb895 100644
--- a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/ScannerEffectController.java
+++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/ScannerEffectController.java
@@ -463,7 +463,13 @@ public class ScannerEffectController {
PDPage newPage = new PDPage(new PDRectangle(page.origW, page.origH));
document.addPage(newPage);
- try (PDPageContentStream contentStream = new PDPageContentStream(document, newPage)) {
+ try (PDPageContentStream contentStream =
+ new PDPageContentStream(
+ document,
+ newPage,
+ PDPageContentStream.AppendMode.OVERWRITE,
+ true,
+ true)) {
PDImageXObject pdImage = LosslessFactory.createFromImage(document, page.image);
contentStream.drawImage(
pdImage, page.offsetX, page.offsetY, page.drawW, page.drawH);
diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/StampController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/StampController.java
index dd570df68..dd1366f75 100644
--- a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/StampController.java
+++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/StampController.java
@@ -134,60 +134,65 @@ public class StampController {
};
// Load the input PDF
- PDDocument document = pdfDocumentFactory.load(pdfFile);
+ try (PDDocument document = pdfDocumentFactory.load(pdfFile)) {
- List pageNumbers = request.getPageNumbersList(document, true);
+ List pageNumbers = request.getPageNumbersList(document, true);
- for (int pageIndex : pageNumbers) {
- int zeroBasedIndex = pageIndex - 1;
- if (zeroBasedIndex >= 0 && zeroBasedIndex < document.getNumberOfPages()) {
- PDPage page = document.getPage(zeroBasedIndex);
- PDRectangle pageSize = page.getMediaBox();
- float margin = marginFactor * (pageSize.getWidth() + pageSize.getHeight()) / 2;
+ for (int pageIndex : pageNumbers) {
+ int zeroBasedIndex = pageIndex - 1;
+ if (zeroBasedIndex >= 0 && zeroBasedIndex < document.getNumberOfPages()) {
+ PDPage page = document.getPage(zeroBasedIndex);
+ PDRectangle pageSize = page.getMediaBox();
+ float margin = marginFactor * (pageSize.getWidth() + pageSize.getHeight()) / 2;
- PDPageContentStream contentStream =
- new PDPageContentStream(
- document, page, PDPageContentStream.AppendMode.APPEND, true, true);
+ PDPageContentStream contentStream =
+ new PDPageContentStream(
+ document,
+ page,
+ PDPageContentStream.AppendMode.APPEND,
+ true,
+ true);
- PDExtendedGraphicsState graphicsState = new PDExtendedGraphicsState();
- graphicsState.setNonStrokingAlphaConstant(opacity);
- contentStream.setGraphicsStateParameters(graphicsState);
+ PDExtendedGraphicsState graphicsState = new PDExtendedGraphicsState();
+ graphicsState.setNonStrokingAlphaConstant(opacity);
+ contentStream.setGraphicsStateParameters(graphicsState);
- if ("text".equalsIgnoreCase(stampType)) {
- addTextStamp(
- contentStream,
- stampText,
- document,
- page,
- rotation,
- position,
- fontSize,
- alphabet,
- overrideX,
- overrideY,
- margin,
- customColor);
- } else if ("image".equalsIgnoreCase(stampType)) {
- addImageStamp(
- contentStream,
- stampImage,
- document,
- page,
- rotation,
- position,
- fontSize,
- overrideX,
- overrideY,
- margin);
+ if ("text".equalsIgnoreCase(stampType)) {
+ addTextStamp(
+ contentStream,
+ stampText,
+ document,
+ page,
+ rotation,
+ position,
+ fontSize,
+ alphabet,
+ overrideX,
+ overrideY,
+ margin,
+ customColor);
+ } else if ("image".equalsIgnoreCase(stampType)) {
+ addImageStamp(
+ contentStream,
+ stampImage,
+ document,
+ page,
+ rotation,
+ position,
+ fontSize,
+ overrideX,
+ overrideY,
+ margin);
+ }
+
+ contentStream.close();
}
-
- contentStream.close();
}
+ // Return the stamped PDF as a response
+ return WebResponseUtils.pdfDocToWebResponse(
+ document,
+ GeneralUtils.generateFilename(pdfFile.getOriginalFilename(), "_stamped.pdf"));
}
- // Return the stamped PDF as a response
- return WebResponseUtils.pdfDocToWebResponse(
- document,
- GeneralUtils.generateFilename(pdfFile.getOriginalFilename(), "_stamped.pdf"));
}
private void addTextStamp(
diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/security/GetInfoOnPDF.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/security/GetInfoOnPDF.java
index 2c2c401f9..e5e6c162e 100644
--- a/app/core/src/main/java/stirling/software/SPDF/controller/api/security/GetInfoOnPDF.java
+++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/security/GetInfoOnPDF.java
@@ -3,6 +3,8 @@ package stirling.software.SPDF.controller.api.security;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
import java.time.Instant;
import java.time.ZoneId;
import java.time.ZonedDateTime;
@@ -14,7 +16,8 @@ import java.util.regex.Pattern;
import org.apache.pdfbox.cos.COSInputStream;
import org.apache.pdfbox.cos.COSName;
import org.apache.pdfbox.cos.COSString;
-import org.apache.pdfbox.io.RandomAccessReadBuffer;
+import org.apache.pdfbox.io.RandomAccessRead;
+import org.apache.pdfbox.io.RandomAccessReadBufferedFile;
import org.apache.pdfbox.pdmodel.*;
import org.apache.pdfbox.pdmodel.common.PDMetadata;
import org.apache.pdfbox.pdmodel.common.PDRectangle;
@@ -198,10 +201,20 @@ public class GetInfoOnPDF {
return false;
}
- try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
- document.save(baos);
+ // Use Stream-to-File pattern: save to temp file instead of loading into memory
+ // This prevents OutOfMemoryError on large PDFs
+ Path tempFile = null;
+ try {
+ tempFile = Files.createTempFile("preflight-", ".pdf");
- try (RandomAccessReadBuffer source = new RandomAccessReadBuffer(baos.toByteArray())) {
+ // Save document to temp file (avoids loading entire document into memory)
+ try (var outputStream = Files.newOutputStream(tempFile)) {
+ document.save(outputStream);
+ }
+
+ // Use RandomAccessReadBufferedFile for efficient file-based reading
+ // This avoids Windows file locking issues that occur with memory-mapped files
+ try (RandomAccessRead source = new RandomAccessReadBufferedFile(tempFile.toFile())) {
PreflightParser parser = new PreflightParser(source);
try (PDDocument parsedDocument = parser.parse()) {
@@ -243,6 +256,19 @@ public class GetInfoOnPDF {
log.debug("IOException during PDF/A validation: {}", e.getMessage());
} catch (Exception e) {
log.debug("Unexpected error during PDF/A validation: {}", e.getMessage());
+ } finally {
+ // Explicitly clean up temp file to prevent disk exhaustion
+ // This must be in finally block to ensure cleanup even on exceptions
+ if (tempFile != null) {
+ try {
+ Files.deleteIfExists(tempFile);
+ } catch (IOException e) {
+ log.warn(
+ "Failed to delete temp file during PDF/A validation cleanup: {}",
+ tempFile,
+ e);
+ }
+ }
}
return false;
diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/security/PasswordController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/security/PasswordController.java
index dc2c919e8..567f1dd2a 100644
--- a/app/core/src/main/java/stirling/software/SPDF/controller/api/security/PasswordController.java
+++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/security/PasswordController.java
@@ -42,25 +42,17 @@ public class PasswordController {
MultipartFile fileInput = request.getFileInput();
String password = request.getPassword();
- PDDocument document;
- try {
- document = pdfDocumentFactory.load(fileInput, password);
- } catch (IOException e) {
- // Handle password errors specifically
- if (ExceptionUtils.isPasswordError(e)) {
- throw ExceptionUtils.createPdfPasswordException(e);
- }
- throw ExceptionUtils.handlePdfException(e);
- }
-
- try {
+ try (PDDocument document = pdfDocumentFactory.load(fileInput, password)) {
document.setAllSecurityToBeRemoved(true);
return WebResponseUtils.pdfDocToWebResponse(
document,
GeneralUtils.generateFilename(
fileInput.getOriginalFilename(), "_password_removed.pdf"));
} catch (IOException e) {
- document.close();
+ // Handle password errors specifically
+ if (ExceptionUtils.isPasswordError(e)) {
+ throw ExceptionUtils.createPdfPasswordException(e);
+ }
ExceptionUtils.logException("password removal", e);
throw ExceptionUtils.handlePdfException(e);
}
@@ -91,31 +83,36 @@ public class PasswordController {
boolean preventPrinting = Boolean.TRUE.equals(request.getPreventPrinting());
boolean preventPrintingFaithful = Boolean.TRUE.equals(request.getPreventPrintingFaithful());
- PDDocument document = pdfDocumentFactory.load(fileInput);
- AccessPermission ap = new AccessPermission();
- ap.setCanAssembleDocument(!preventAssembly);
- ap.setCanExtractContent(!preventExtractContent);
- ap.setCanExtractForAccessibility(!preventExtractForAccessibility);
- ap.setCanFillInForm(!preventFillInForm);
- ap.setCanModify(!preventModify);
- ap.setCanModifyAnnotations(!preventModifyAnnotations);
- ap.setCanPrint(!preventPrinting);
- ap.setCanPrintFaithful(!preventPrintingFaithful);
- StandardProtectionPolicy spp = new StandardProtectionPolicy(ownerPassword, password, ap);
+ try (PDDocument document = pdfDocumentFactory.load(fileInput)) {
+ AccessPermission ap = new AccessPermission();
+ ap.setCanAssembleDocument(!preventAssembly);
+ ap.setCanExtractContent(!preventExtractContent);
+ ap.setCanExtractForAccessibility(!preventExtractForAccessibility);
+ ap.setCanFillInForm(!preventFillInForm);
+ ap.setCanModify(!preventModify);
+ ap.setCanModifyAnnotations(!preventModifyAnnotations);
+ ap.setCanPrint(!preventPrinting);
+ ap.setCanPrintFaithful(!preventPrintingFaithful);
+ StandardProtectionPolicy spp =
+ new StandardProtectionPolicy(ownerPassword, password, ap);
- if (!"".equals(ownerPassword) || !"".equals(password)) {
- spp.setEncryptionKeyLength(keyLength);
- }
- spp.setPermissions(ap);
- document.protect(spp);
+ if ((ownerPassword != null && ownerPassword.length() > 0)
+ || (password != null && password.length() > 0)) {
+ spp.setEncryptionKeyLength(keyLength);
+ }
+ spp.setPermissions(ap);
+ document.protect(spp);
- if ("".equals(ownerPassword) && "".equals(password))
+ if ((ownerPassword == null || ownerPassword.length() == 0)
+ && (password == null || password.length() == 0))
+ return WebResponseUtils.pdfDocToWebResponse(
+ document,
+ GeneralUtils.generateFilename(
+ fileInput.getOriginalFilename(), "_permissions.pdf"));
return WebResponseUtils.pdfDocToWebResponse(
document,
GeneralUtils.generateFilename(
- fileInput.getOriginalFilename(), "_permissions.pdf"));
- return WebResponseUtils.pdfDocToWebResponse(
- document,
- GeneralUtils.generateFilename(fileInput.getOriginalFilename(), "_passworded.pdf"));
+ fileInput.getOriginalFilename(), "_passworded.pdf"));
+ }
}
}
diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/security/RemoveCertSignController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/security/RemoveCertSignController.java
index 79a2ec955..91644747e 100644
--- a/app/core/src/main/java/stirling/software/SPDF/controller/api/security/RemoveCertSignController.java
+++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/security/RemoveCertSignController.java
@@ -41,28 +41,29 @@ public class RemoveCertSignController {
throws Exception {
MultipartFile pdf = request.getFileInput();
- // Load the PDF document
- PDDocument document = pdfDocumentFactory.load(pdf);
+ // Load the PDF document with proper resource management
+ try (PDDocument document = pdfDocumentFactory.load(pdf)) {
- // Get the document catalog
- PDDocumentCatalog catalog = document.getDocumentCatalog();
+ // Get the document catalog
+ PDDocumentCatalog catalog = document.getDocumentCatalog();
- // Get the AcroForm
- PDAcroForm acroForm = catalog.getAcroForm();
- if (acroForm != null) {
- // Remove signature fields safely
- List fieldsToRemove =
- acroForm.getFields().stream()
- .filter(field -> field instanceof PDSignatureField)
- .toList();
+ // Get the AcroForm
+ PDAcroForm acroForm = catalog.getAcroForm();
+ if (acroForm != null) {
+ // Remove signature fields safely
+ List fieldsToRemove =
+ acroForm.getFields().stream()
+ .filter(field -> field instanceof PDSignatureField)
+ .toList();
- if (!fieldsToRemove.isEmpty()) {
- acroForm.flatten(fieldsToRemove, false);
+ if (!fieldsToRemove.isEmpty()) {
+ acroForm.flatten(fieldsToRemove, false);
+ }
}
+ // Return the modified PDF as a response
+ return WebResponseUtils.pdfDocToWebResponse(
+ document,
+ GeneralUtils.generateFilename(pdf.getOriginalFilename(), "_unsigned.pdf"));
}
- // Return the modified PDF as a response
- return WebResponseUtils.pdfDocToWebResponse(
- document,
- GeneralUtils.generateFilename(pdf.getOriginalFilename(), "_unsigned.pdf"));
}
}
diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/security/SanitizeController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/security/SanitizeController.java
index a2cacce53..9a6f44692 100644
--- a/app/core/src/main/java/stirling/software/SPDF/controller/api/security/SanitizeController.java
+++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/security/SanitizeController.java
@@ -67,39 +67,40 @@ public class SanitizeController {
boolean removeLinks = Boolean.TRUE.equals(request.getRemoveLinks());
boolean removeFonts = Boolean.TRUE.equals(request.getRemoveFonts());
- PDDocument document = pdfDocumentFactory.load(inputFile, true);
- if (removeJavaScript) {
- sanitizeJavaScript(document);
+ try (PDDocument document = pdfDocumentFactory.load(inputFile, true)) {
+ if (removeJavaScript) {
+ sanitizeJavaScript(document);
+ }
+
+ if (removeEmbeddedFiles) {
+ sanitizeEmbeddedFiles(document);
+ }
+
+ if (removeXMPMetadata) {
+ sanitizeXMPMetadata(document);
+ }
+
+ if (removeMetadata) {
+ sanitizeDocumentInfoMetadata(document);
+ }
+
+ if (removeLinks) {
+ sanitizeLinks(document);
+ }
+
+ if (removeFonts) {
+ sanitizeFonts(document);
+ }
+
+ // Save the sanitized document to output stream
+ ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+ document.save(outputStream);
+
+ return WebResponseUtils.bytesToWebResponse(
+ outputStream.toByteArray(),
+ GeneralUtils.generateFilename(
+ inputFile.getOriginalFilename(), "_sanitized.pdf"));
}
-
- if (removeEmbeddedFiles) {
- sanitizeEmbeddedFiles(document);
- }
-
- if (removeXMPMetadata) {
- sanitizeXMPMetadata(document);
- }
-
- if (removeMetadata) {
- sanitizeDocumentInfoMetadata(document);
- }
-
- if (removeLinks) {
- sanitizeLinks(document);
- }
-
- if (removeFonts) {
- sanitizeFonts(document);
- }
-
- // Save the sanitized document to output stream
- ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
- document.save(outputStream);
- document.close();
-
- return WebResponseUtils.bytesToWebResponse(
- outputStream.toByteArray(),
- GeneralUtils.generateFilename(inputFile.getOriginalFilename(), "_sanitized.pdf"));
}
private static void sanitizeJavaScript(PDDocument document) throws IOException {
diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/security/WatermarkController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/security/WatermarkController.java
index 6afcf25ed..2e272eeaa 100644
--- a/app/core/src/main/java/stirling/software/SPDF/controller/api/security/WatermarkController.java
+++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/security/WatermarkController.java
@@ -98,60 +98,67 @@ public class WatermarkController {
String customColor = request.getCustomColor();
boolean convertPdfToImage = Boolean.TRUE.equals(request.getConvertPDFToImage());
- // Load the input PDF
- PDDocument document = pdfDocumentFactory.load(pdfFile);
+ // Load the input PDF with proper resource management
+ try (PDDocument document = pdfDocumentFactory.load(pdfFile)) {
- // Create a page in the document
- for (PDPage page : document.getPages()) {
+ // Create a page in the document
+ for (PDPage page : document.getPages()) {
+ // Get the page's content stream
+ try (PDPageContentStream contentStream =
+ new PDPageContentStream(
+ document,
+ page,
+ PDPageContentStream.AppendMode.APPEND,
+ true,
+ true)) {
- // Get the page's content stream
- PDPageContentStream contentStream =
- new PDPageContentStream(
- document, page, PDPageContentStream.AppendMode.APPEND, true, true);
+ // Set transparency
+ PDExtendedGraphicsState graphicsState = new PDExtendedGraphicsState();
+ graphicsState.setNonStrokingAlphaConstant(opacity);
+ contentStream.setGraphicsStateParameters(graphicsState);
- // Set transparency
- PDExtendedGraphicsState graphicsState = new PDExtendedGraphicsState();
- graphicsState.setNonStrokingAlphaConstant(opacity);
- contentStream.setGraphicsStateParameters(graphicsState);
-
- if ("text".equalsIgnoreCase(watermarkType)) {
- addTextWatermark(
- contentStream,
- watermarkText,
- document,
- page,
- rotation,
- widthSpacer,
- heightSpacer,
- fontSize,
- alphabet,
- customColor);
- } else if ("image".equalsIgnoreCase(watermarkType)) {
- addImageWatermark(
- contentStream,
- watermarkImage,
- document,
- page,
- rotation,
- widthSpacer,
- heightSpacer,
- fontSize);
+ if ("text".equalsIgnoreCase(watermarkType)) {
+ addTextWatermark(
+ contentStream,
+ watermarkText,
+ document,
+ page,
+ rotation,
+ widthSpacer,
+ heightSpacer,
+ fontSize,
+ alphabet,
+ customColor);
+ } else if ("image".equalsIgnoreCase(watermarkType)) {
+ addImageWatermark(
+ contentStream,
+ watermarkImage,
+ document,
+ page,
+ rotation,
+ widthSpacer,
+ heightSpacer,
+ fontSize);
+ }
+ }
}
- // Close the content stream
- contentStream.close();
+ if (convertPdfToImage) {
+ try (PDDocument convertedPdf = PdfUtils.convertPdfToPdfImage(document)) {
+ // Return the watermarked PDF as a response
+ return WebResponseUtils.pdfDocToWebResponse(
+ convertedPdf,
+ GeneralUtils.generateFilename(
+ pdfFile.getOriginalFilename(), "_watermarked.pdf"));
+ }
+ } else {
+ // Return the watermarked PDF as a response
+ return WebResponseUtils.pdfDocToWebResponse(
+ document,
+ GeneralUtils.generateFilename(
+ pdfFile.getOriginalFilename(), "_watermarked.pdf"));
+ }
}
-
- if (convertPdfToImage) {
- PDDocument convertedPdf = PdfUtils.convertPdfToPdfImage(document);
- document.close();
- document = convertedPdf;
- }
-
- // Return the watermarked PDF as a response
- return WebResponseUtils.pdfDocToWebResponse(
- document,
- GeneralUtils.generateFilename(pdfFile.getOriginalFilename(), "_watermarked.pdf"));
}
private void addTextWatermark(
diff --git a/app/core/src/main/java/stirling/software/SPDF/exception/GlobalExceptionHandler.java b/app/core/src/main/java/stirling/software/SPDF/exception/GlobalExceptionHandler.java
index 82d1b2bb9..3244144aa 100644
--- a/app/core/src/main/java/stirling/software/SPDF/exception/GlobalExceptionHandler.java
+++ b/app/core/src/main/java/stirling/software/SPDF/exception/GlobalExceptionHandler.java
@@ -80,7 +80,7 @@ import stirling.software.common.util.RegexPatternUtils;
* {@code
* // In controllers/services - use ExceptionUtils to create typed exceptions:
* try {
- * PDDocument doc = PDDocument.load(file);
+ * PDDocument doc = Loader.loadPDF(file);
* } catch (IOException e) {
* throw ExceptionUtils.createPdfCorruptedException("during load", e);
* }
@@ -1117,6 +1117,34 @@ public class GlobalExceptionHandler {
return handleBaseApp((BaseAppException) processedException, request);
}
+ // Check if this is a NoSuchFileException (temp file was deleted prematurely)
+ if (ex instanceof java.nio.file.NoSuchFileException) {
+ log.error(
+ "Temporary file not found at {}: {}",
+ request.getRequestURI(),
+ ex.getMessage(),
+ ex);
+
+ String message =
+ getLocalizedMessage(
+ "error.tempFileNotFound.detail",
+ "The temporary file was not found. This may indicate a processing error or cleanup issue. Please try again.");
+ String title =
+ getLocalizedMessage("error.tempFileNotFound.title", "Temporary File Not Found");
+
+ ProblemDetail problemDetail =
+ createBaseProblemDetail(HttpStatus.INTERNAL_SERVER_ERROR, message, request);
+ problemDetail.setType(URI.create("https://stirlingpdf.com/errors/temp-file-not-found"));
+ problemDetail.setTitle(title);
+ problemDetail.setProperty("title", title);
+ problemDetail.setProperty("errorCode", "E999");
+ problemDetail.setProperty(
+ "hint.1",
+ "This error usually occurs when temporary files are cleaned up before processing completes.");
+ problemDetail.setProperty("hint.2", "Try submitting your request again.");
+ return new ResponseEntity<>(problemDetail, HttpStatus.INTERNAL_SERVER_ERROR);
+ }
+
log.error("IO error at {}: {}", request.getRequestURI(), ex.getMessage(), ex);
String message =
@@ -1161,9 +1189,19 @@ public class GlobalExceptionHandler {
*/
@ExceptionHandler(Exception.class)
public ResponseEntity handleGenericException(
- Exception ex, HttpServletRequest request) {
+ Exception ex, HttpServletRequest request, HttpServletResponse response) {
log.error("Unexpected error at {}: {}", request.getRequestURI(), ex.getMessage(), ex);
+ // If response is already committed (e.g., during streaming), we can't send an error
+ // response
+ // Log the error and return null to let Spring handle it gracefully
+ if (response.isCommitted()) {
+ log.warn(
+ "Cannot send error response because response is already committed for URI: {}",
+ request.getRequestURI());
+ return null; // Spring will handle gracefully
+ }
+
String userMessage =
getLocalizedMessage(
"error.unexpected",
diff --git a/app/core/src/main/java/stirling/software/SPDF/service/pdfjson/type3/library/Type3FontLibrary.java b/app/core/src/main/java/stirling/software/SPDF/service/pdfjson/type3/library/Type3FontLibrary.java
index f00c729a2..a061cff5e 100644
--- a/app/core/src/main/java/stirling/software/SPDF/service/pdfjson/type3/library/Type3FontLibrary.java
+++ b/app/core/src/main/java/stirling/software/SPDF/service/pdfjson/type3/library/Type3FontLibrary.java
@@ -220,6 +220,9 @@ public class Type3FontLibrary {
}
private byte[] loadResourceBytes(String location) throws IOException {
+ if (location == null || location.isBlank()) {
+ throw new IOException("Resource location is null or blank");
+ }
String resolved = resolveLocation(location);
Resource resource = resourceLoader.getResource(resolved);
if (!resource.exists()) {
diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/util/FormUtils.java b/app/proprietary/src/main/java/stirling/software/proprietary/util/FormUtils.java
index a3cf6ea55..3633a2544 100644
--- a/app/proprietary/src/main/java/stirling/software/proprietary/util/FormUtils.java
+++ b/app/proprietary/src/main/java/stirling/software/proprietary/util/FormUtils.java
@@ -292,6 +292,7 @@ public class FormUtils {
}
PDFRenderer renderer = new PDFRenderer(document);
+ renderer.setSubsamplingAllowed(true); // Enable subsampling to reduce memory usage
ApplicationProperties properties =
ApplicationContextProvider.getBean(ApplicationProperties.class);