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 74f65e713..9f795bb46 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 @@ -324,4 +324,63 @@ public class ExceptionUtils { return createIllegalArgumentException( "error.argumentRequired", "{0} must not be null", argumentName); } + + /** + * Create a RuntimeException for memory/image size errors when rendering PDF images with DPI. + * Handles OutOfMemoryError and related conditions (e.g., NegativeArraySizeException) that + * result from images exceeding Java's array/memory limits. + * + * @param pageNumber the page number that caused the error + * @param dpi the DPI value used + * @param cause the original error/exception (e.g., OutOfMemoryError, + * NegativeArraySizeException) + * @return RuntimeException with user-friendly message + */ + public static RuntimeException createOutOfMemoryDpiException( + int pageNumber, int dpi, Throwable cause) { + String message = + MessageFormat.format( + "Out of memory or image-too-large error while rendering PDF page {0} at {1} DPI. " + + "This can occur when the resulting image exceeds Java's array/memory limits (e.g., NegativeArraySizeException). " + + "Please use a lower DPI value (recommended: 150 or less) or process the document in smaller chunks.", + pageNumber, dpi); + return new RuntimeException(message, cause); + } + + /** + * Create a RuntimeException for OutOfMemoryError when rendering PDF images with DPI. + * + * @param pageNumber the page number that caused the error + * @param dpi the DPI value used + * @param cause the original OutOfMemoryError + * @return RuntimeException with user-friendly message + */ + public static RuntimeException createOutOfMemoryDpiException( + int pageNumber, int dpi, OutOfMemoryError cause) { + return createOutOfMemoryDpiException(pageNumber, dpi, (Throwable) cause); + } + + /** + * Create a RuntimeException for memory/image size errors when rendering PDF images with DPI. + * Handles OutOfMemoryError and related conditions (e.g., NegativeArraySizeException) that + * result from images exceeding Java's array/memory limits. + * + * @param dpi the DPI value used + * @param cause the original error/exception (e.g., OutOfMemoryError, + * NegativeArraySizeException) + * @return RuntimeException with user-friendly message + */ + public static RuntimeException createOutOfMemoryDpiException(int dpi, Throwable cause) { + String message = + MessageFormat.format( + "Out of memory or image-too-large error while rendering PDF at {0} DPI. " + + "This can occur when the resulting image exceeds Java's array/memory limits (e.g., NegativeArraySizeException). " + + "Please use a lower DPI value (recommended: 150 or less) or process the document in smaller chunks.", + dpi); + return new RuntimeException(message, cause); + } + + public static RuntimeException createOutOfMemoryDpiException(int dpi, OutOfMemoryError cause) { + return createOutOfMemoryDpiException(dpi, (Throwable) cause); + } } 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 7d8753cce..967314808 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 @@ -205,6 +205,10 @@ public class PdfUtils { DPI); } throw e; + } catch (OutOfMemoryError e) { + throw ExceptionUtils.createOutOfMemoryDpiException(i + 1, DPI, e); + } catch (NegativeArraySizeException e) { + throw ExceptionUtils.createOutOfMemoryDpiException(i + 1, DPI, e); } writer.writeToSequence(new IIOImage(image, null, null), param); } @@ -253,6 +257,10 @@ public class PdfUtils { DPI); } throw e; + } catch (OutOfMemoryError e) { + throw ExceptionUtils.createOutOfMemoryDpiException(i + 1, DPI, e); + } catch (NegativeArraySizeException e) { + throw ExceptionUtils.createOutOfMemoryDpiException(i + 1, DPI, e); } pdfSizeImageIndex = i; dimension = @@ -296,6 +304,10 @@ public class PdfUtils { DPI); } throw e; + } catch (OutOfMemoryError e) { + throw ExceptionUtils.createOutOfMemoryDpiException(i + 1, DPI, e); + } catch (NegativeArraySizeException e) { + throw ExceptionUtils.createOutOfMemoryDpiException(i + 1, DPI, e); } } @@ -330,6 +342,10 @@ public class PdfUtils { DPI); } throw e; + } catch (OutOfMemoryError e) { + throw ExceptionUtils.createOutOfMemoryDpiException(i + 1, DPI, e); + } catch (NegativeArraySizeException e) { + throw ExceptionUtils.createOutOfMemoryDpiException(i + 1, DPI, e); } try (ByteArrayOutputStream baosImage = new ByteArrayOutputStream()) { ImageIO.write(image, imageType, baosImage); @@ -369,8 +385,17 @@ public class PdfUtils { pdfRenderer.setSubsamplingAllowed(true); for (int page = 0; page < document.getNumberOfPages(); ++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(); + } + try { - bim = pdfRenderer.renderImageWithDPI(page, 300, ImageType.RGB); + bim = pdfRenderer.renderImageWithDPI(page, renderDpi, ImageType.RGB); } catch (IllegalArgumentException e) { if (e.getMessage() != null && e.getMessage().contains("Maximum size of image exceeded")) { @@ -382,6 +407,10 @@ public class PdfUtils { page + 1); } throw e; + } catch (OutOfMemoryError e) { + throw ExceptionUtils.createOutOfMemoryDpiException(page + 1, 300, e); + } catch (NegativeArraySizeException e) { + throw ExceptionUtils.createOutOfMemoryDpiException(page + 1, 300, e); } PDPage originalPage = document.getPage(page); 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 df40737d3..d220cfcfa 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 @@ -19,7 +19,10 @@ import org.apache.pdfbox.rendering.PDFRenderer; import org.springframework.core.io.InputStreamResource; import org.springframework.web.multipart.MultipartFile; +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; public class InvertFullColorStrategy extends ReplaceAndInvertColorStrategy { @@ -44,8 +47,25 @@ public class InvertFullColorStrategy extends ReplaceAndInvertColorStrategy { // Render each page and invert colors PDFRenderer pdfRenderer = new PDFRenderer(document); for (int page = 0; page < document.getNumberOfPages(); page++) { - BufferedImage image = - pdfRenderer.renderImageWithDPI(page, 300); // Render page at 300 DPI + BufferedImage image; + + // 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(); + } + + try { + image = + pdfRenderer.renderImageWithDPI( + page, renderDpi); // Render page with global DPI setting + } catch (OutOfMemoryError e) { + throw ExceptionUtils.createOutOfMemoryDpiException(page + 1, renderDpi, e); + } catch (NegativeArraySizeException e) { + throw ExceptionUtils.createOutOfMemoryDpiException(page + 1, renderDpi, e); + } // Invert the colors invertImageColors(image); diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/AutoSplitPdfController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/AutoSplitPdfController.java index bcc20e219..785de3a9b 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/AutoSplitPdfController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/AutoSplitPdfController.java @@ -34,7 +34,10 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import stirling.software.SPDF.model.api.misc.AutoSplitPdfRequest; +import stirling.software.common.model.ApplicationProperties; import stirling.software.common.service.CustomPDFDocumentFactory; +import stirling.software.common.util.ApplicationContextProvider; +import stirling.software.common.util.ExceptionUtils; import stirling.software.common.util.TempFile; import stirling.software.common.util.TempFileManager; import stirling.software.common.util.WebResponseUtils; @@ -128,7 +131,23 @@ public class AutoSplitPdfController { pdfRenderer.setSubsamplingAllowed(true); for (int page = 0; page < document.getNumberOfPages(); ++page) { - BufferedImage bim = pdfRenderer.renderImageWithDPI(page, 150); + BufferedImage bim; + + // Use global maximum DPI setting, fallback to 300 if not set + int renderDpi = 150; // Default fallback + ApplicationProperties properties = + ApplicationContextProvider.getBean(ApplicationProperties.class); + if (properties != null && properties.getSystem() != null) { + renderDpi = properties.getSystem().getMaxDPI(); + } + + try { + bim = pdfRenderer.renderImageWithDPI(page, renderDpi); + } catch (OutOfMemoryError e) { + throw ExceptionUtils.createOutOfMemoryDpiException(page + 1, renderDpi, e); + } catch (NegativeArraySizeException e) { + throw ExceptionUtils.createOutOfMemoryDpiException(page + 1, renderDpi, e); + } String result = decodeQRCode(bim); boolean isValidQrCode = VALID_QR_CONTENTS.contains(result); diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/BlankPageController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/BlankPageController.java index fed566c35..090cf1ebd 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/BlankPageController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/BlankPageController.java @@ -30,7 +30,10 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import stirling.software.SPDF.model.api.misc.RemoveBlankPagesRequest; +import stirling.software.common.model.ApplicationProperties; import stirling.software.common.service.CustomPDFDocumentFactory; +import stirling.software.common.util.ApplicationContextProvider; +import stirling.software.common.util.ExceptionUtils; import stirling.software.common.util.PdfUtils; import stirling.software.common.util.WebResponseUtils; @@ -108,7 +111,25 @@ public class BlankPageController { if (hasImages) { log.info("page {} has image, running blank detection", pageIndex); // Render image and save as temp file - BufferedImage image = pdfRenderer.renderImageWithDPI(pageIndex, 30); + BufferedImage image; + + // Use global maximum DPI setting + int renderDpi = 30; // Default fallback + ApplicationProperties properties = + ApplicationContextProvider.getBean(ApplicationProperties.class); + if (properties != null && properties.getSystem() != null) { + renderDpi = properties.getSystem().getMaxDPI(); + } + + try { + image = pdfRenderer.renderImageWithDPI(pageIndex, renderDpi); + } catch (OutOfMemoryError e) { + throw ExceptionUtils.createOutOfMemoryDpiException( + pageIndex + 1, renderDpi, e); + } catch (NegativeArraySizeException e) { + throw ExceptionUtils.createOutOfMemoryDpiException( + pageIndex + 1, renderDpi, e); + } blank = isBlankImage(image, threshold, whitePercent, threshold); } } diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/ExtractImageScansController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/ExtractImageScansController.java index bb4490163..fb063a830 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/ExtractImageScansController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/misc/ExtractImageScansController.java @@ -32,7 +32,9 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import stirling.software.SPDF.model.api.misc.ExtractImageScansRequest; +import stirling.software.common.model.ApplicationProperties; import stirling.software.common.service.CustomPDFDocumentFactory; +import stirling.software.common.util.ApplicationContextProvider; import stirling.software.common.util.CheckProgramInstall; import stirling.software.common.util.ExceptionUtils; import stirling.software.common.util.GeneralUtils; @@ -97,7 +99,23 @@ public class ExtractImageScansController { Path tempFile = Files.createTempFile("image_", ".png"); // Render image and save as temp file - BufferedImage image = pdfRenderer.renderImageWithDPI(i, 300); + BufferedImage image; + + // 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(); + } + + try { + image = pdfRenderer.renderImageWithDPI(i, renderDpi); + } catch (OutOfMemoryError e) { + throw ExceptionUtils.createOutOfMemoryDpiException(i + 1, renderDpi, e); + } catch (NegativeArraySizeException e) { + throw ExceptionUtils.createOutOfMemoryDpiException(i + 1, renderDpi, e); + } ImageIO.write(image, "png", tempFile.toFile()); // Add temp file path to images list 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 ff710ac20..a76a737b5 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 @@ -27,7 +27,10 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import stirling.software.SPDF.model.api.misc.FlattenRequest; +import stirling.software.common.model.ApplicationProperties; import stirling.software.common.service.CustomPDFDocumentFactory; +import stirling.software.common.util.ApplicationContextProvider; +import stirling.software.common.util.ExceptionUtils; import stirling.software.common.util.WebResponseUtils; @RestController @@ -67,7 +70,23 @@ public class FlattenController { int numPages = document.getNumberOfPages(); for (int i = 0; i < numPages; i++) { try { - BufferedImage image = pdfRenderer.renderImageWithDPI(i, 300, ImageType.RGB); + BufferedImage image; + + // 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(); + } + + try { + image = pdfRenderer.renderImageWithDPI(i, renderDpi, ImageType.RGB); + } catch (OutOfMemoryError e) { + throw ExceptionUtils.createOutOfMemoryDpiException(i + 1, renderDpi, e); + } catch (NegativeArraySizeException e) { + throw ExceptionUtils.createOutOfMemoryDpiException(i + 1, renderDpi, e); + } PDPage page = new PDPage(); page.setMediaBox(document.getPage(i).getMediaBox()); newDocument.addPage(page); 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 0f8f68723..3b5dcc2ee 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 @@ -359,7 +359,24 @@ public class OCRController { if (shouldOcr) { // Convert page to image - BufferedImage image = pdfRenderer.renderImageWithDPI(pageNum, 300); + BufferedImage image; + + // Use global maximum DPI setting, fallback to 300 if not set + int renderDpi = 300; // Default fallback + if (applicationProperties != null + && applicationProperties.getSystem() != null) { + renderDpi = applicationProperties.getSystem().getMaxDPI(); + } + + try { + image = pdfRenderer.renderImageWithDPI(pageNum, renderDpi); + } catch (OutOfMemoryError e) { + throw ExceptionUtils.createOutOfMemoryDpiException( + pageNum + 1, renderDpi, e); + } catch (NegativeArraySizeException e) { + throw ExceptionUtils.createOutOfMemoryDpiException( + pageNum + 1, renderDpi, e); + } File imagePath = new File(tempImagesDir, String.format("page_%d.png", pageNum)); ImageIO.write(image, "png", imagePath); 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 5741d3854..33ff3db47 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 @@ -34,7 +34,10 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import stirling.software.SPDF.model.api.misc.ScannerEffectRequest; +import stirling.software.common.model.ApplicationProperties; import stirling.software.common.service.CustomPDFDocumentFactory; +import stirling.software.common.util.ApplicationContextProvider; +import stirling.software.common.util.ExceptionUtils; import stirling.software.common.util.WebResponseUtils; @RestController @@ -82,6 +85,22 @@ public class ScannerEffectController { int resolution = request.getResolution(); ScannerEffectRequest.Colorspace colorspace = request.getColorspace(); + // Validate and limit DPI to prevent excessive memory usage (respecting global limits) + int maxSafeDpi = 500; // Default maximum safe DPI + ApplicationProperties properties = + ApplicationContextProvider.getBean(ApplicationProperties.class); + if (properties != null && properties.getSystem() != null) { + maxSafeDpi = properties.getSystem().getMaxDPI(); + } + if (resolution > maxSafeDpi) { + throw ExceptionUtils.createIllegalArgumentException( + "error.dpiExceedsLimit", + "DPI value {0} exceeds maximum safe limit of {1}. High DPI values can cause" + + " memory issues and crashes. Please use a lower DPI value.", + resolution, + maxSafeDpi); + } + try (PDDocument document = pdfDocumentFactory.load(file)) { PDDocument outputDocument = new PDDocument(); PDFRenderer pdfRenderer = new PDFRenderer(document); @@ -118,7 +137,14 @@ public class ScannerEffectController { } // Render page to image with safe resolution - BufferedImage image = pdfRenderer.renderImageWithDPI(i, safeResolution); + BufferedImage image; + try { + image = pdfRenderer.renderImageWithDPI(i, safeResolution); + } catch (OutOfMemoryError e) { + throw ExceptionUtils.createOutOfMemoryDpiException(i + 1, safeResolution, e); + } catch (NegativeArraySizeException e) { + throw ExceptionUtils.createOutOfMemoryDpiException(i + 1, safeResolution, e); + } log.debug( "Processing page {} with dimensions {}x{} ({} pixels) at {}dpi",