feat: custom error handling when calling renderImageWithDPI, controllers to respect global DPI (#4407)

This commit is contained in:
Balázs Szücs 2025-09-18 14:43:21 +02:00 committed by GitHub
parent a438a15105
commit c684a51cf9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 237 additions and 9 deletions

View File

@ -324,4 +324,63 @@ public class ExceptionUtils {
return createIllegalArgumentException( return createIllegalArgumentException(
"error.argumentRequired", "{0} must not be null", argumentName); "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);
}
} }

View File

@ -205,6 +205,10 @@ public class PdfUtils {
DPI); DPI);
} }
throw e; 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); writer.writeToSequence(new IIOImage(image, null, null), param);
} }
@ -253,6 +257,10 @@ public class PdfUtils {
DPI); DPI);
} }
throw e; throw e;
} catch (OutOfMemoryError e) {
throw ExceptionUtils.createOutOfMemoryDpiException(i + 1, DPI, e);
} catch (NegativeArraySizeException e) {
throw ExceptionUtils.createOutOfMemoryDpiException(i + 1, DPI, e);
} }
pdfSizeImageIndex = i; pdfSizeImageIndex = i;
dimension = dimension =
@ -296,6 +304,10 @@ public class PdfUtils {
DPI); DPI);
} }
throw e; 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); DPI);
} }
throw e; 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()) { try (ByteArrayOutputStream baosImage = new ByteArrayOutputStream()) {
ImageIO.write(image, imageType, baosImage); ImageIO.write(image, imageType, baosImage);
@ -369,8 +385,17 @@ public class PdfUtils {
pdfRenderer.setSubsamplingAllowed(true); pdfRenderer.setSubsamplingAllowed(true);
for (int page = 0; page < document.getNumberOfPages(); ++page) { for (int page = 0; page < document.getNumberOfPages(); ++page) {
BufferedImage bim; 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 { try {
bim = pdfRenderer.renderImageWithDPI(page, 300, ImageType.RGB); bim = pdfRenderer.renderImageWithDPI(page, renderDpi, ImageType.RGB);
} catch (IllegalArgumentException e) { } catch (IllegalArgumentException e) {
if (e.getMessage() != null if (e.getMessage() != null
&& e.getMessage().contains("Maximum size of image exceeded")) { && e.getMessage().contains("Maximum size of image exceeded")) {
@ -382,6 +407,10 @@ public class PdfUtils {
page + 1); page + 1);
} }
throw e; 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); PDPage originalPage = document.getPage(page);

View File

@ -19,7 +19,10 @@ import org.apache.pdfbox.rendering.PDFRenderer;
import org.springframework.core.io.InputStreamResource; import org.springframework.core.io.InputStreamResource;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
import stirling.software.common.model.ApplicationProperties;
import stirling.software.common.model.api.misc.ReplaceAndInvert; 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 { public class InvertFullColorStrategy extends ReplaceAndInvertColorStrategy {
@ -44,8 +47,25 @@ public class InvertFullColorStrategy extends ReplaceAndInvertColorStrategy {
// Render each page and invert colors // Render each page and invert colors
PDFRenderer pdfRenderer = new PDFRenderer(document); PDFRenderer pdfRenderer = new PDFRenderer(document);
for (int page = 0; page < document.getNumberOfPages(); page++) { for (int page = 0; page < document.getNumberOfPages(); page++) {
BufferedImage image = BufferedImage image;
pdfRenderer.renderImageWithDPI(page, 300); // Render page at 300 DPI
// 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 // Invert the colors
invertImageColors(image); invertImageColors(image);

View File

@ -34,7 +34,10 @@ import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.model.api.misc.AutoSplitPdfRequest; import stirling.software.SPDF.model.api.misc.AutoSplitPdfRequest;
import stirling.software.common.model.ApplicationProperties;
import stirling.software.common.service.CustomPDFDocumentFactory; 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.TempFile;
import stirling.software.common.util.TempFileManager; import stirling.software.common.util.TempFileManager;
import stirling.software.common.util.WebResponseUtils; import stirling.software.common.util.WebResponseUtils;
@ -128,7 +131,23 @@ public class AutoSplitPdfController {
pdfRenderer.setSubsamplingAllowed(true); pdfRenderer.setSubsamplingAllowed(true);
for (int page = 0; page < document.getNumberOfPages(); ++page) { 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); String result = decodeQRCode(bim);
boolean isValidQrCode = VALID_QR_CONTENTS.contains(result); boolean isValidQrCode = VALID_QR_CONTENTS.contains(result);

View File

@ -30,7 +30,10 @@ import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.model.api.misc.RemoveBlankPagesRequest; import stirling.software.SPDF.model.api.misc.RemoveBlankPagesRequest;
import stirling.software.common.model.ApplicationProperties;
import stirling.software.common.service.CustomPDFDocumentFactory; 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.PdfUtils;
import stirling.software.common.util.WebResponseUtils; import stirling.software.common.util.WebResponseUtils;
@ -108,7 +111,25 @@ public class BlankPageController {
if (hasImages) { if (hasImages) {
log.info("page {} has image, running blank detection", pageIndex); log.info("page {} has image, running blank detection", pageIndex);
// Render image and save as temp file // 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); blank = isBlankImage(image, threshold, whitePercent, threshold);
} }
} }

View File

@ -32,7 +32,9 @@ import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.model.api.misc.ExtractImageScansRequest; import stirling.software.SPDF.model.api.misc.ExtractImageScansRequest;
import stirling.software.common.model.ApplicationProperties;
import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.service.CustomPDFDocumentFactory;
import stirling.software.common.util.ApplicationContextProvider;
import stirling.software.common.util.CheckProgramInstall; import stirling.software.common.util.CheckProgramInstall;
import stirling.software.common.util.ExceptionUtils; import stirling.software.common.util.ExceptionUtils;
import stirling.software.common.util.GeneralUtils; import stirling.software.common.util.GeneralUtils;
@ -97,7 +99,23 @@ public class ExtractImageScansController {
Path tempFile = Files.createTempFile("image_", ".png"); Path tempFile = Files.createTempFile("image_", ".png");
// Render image and save as temp file // 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()); ImageIO.write(image, "png", tempFile.toFile());
// Add temp file path to images list // Add temp file path to images list

View File

@ -27,7 +27,10 @@ import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.model.api.misc.FlattenRequest; import stirling.software.SPDF.model.api.misc.FlattenRequest;
import stirling.software.common.model.ApplicationProperties;
import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.service.CustomPDFDocumentFactory;
import stirling.software.common.util.ApplicationContextProvider;
import stirling.software.common.util.ExceptionUtils;
import stirling.software.common.util.WebResponseUtils; import stirling.software.common.util.WebResponseUtils;
@RestController @RestController
@ -67,7 +70,23 @@ public class FlattenController {
int numPages = document.getNumberOfPages(); int numPages = document.getNumberOfPages();
for (int i = 0; i < numPages; i++) { for (int i = 0; i < numPages; i++) {
try { 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(); PDPage page = new PDPage();
page.setMediaBox(document.getPage(i).getMediaBox()); page.setMediaBox(document.getPage(i).getMediaBox());
newDocument.addPage(page); newDocument.addPage(page);

View File

@ -359,7 +359,24 @@ public class OCRController {
if (shouldOcr) { if (shouldOcr) {
// Convert page to image // 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 = File imagePath =
new File(tempImagesDir, String.format("page_%d.png", pageNum)); new File(tempImagesDir, String.format("page_%d.png", pageNum));
ImageIO.write(image, "png", imagePath); ImageIO.write(image, "png", imagePath);

View File

@ -34,7 +34,10 @@ import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.model.api.misc.ScannerEffectRequest; import stirling.software.SPDF.model.api.misc.ScannerEffectRequest;
import stirling.software.common.model.ApplicationProperties;
import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.service.CustomPDFDocumentFactory;
import stirling.software.common.util.ApplicationContextProvider;
import stirling.software.common.util.ExceptionUtils;
import stirling.software.common.util.WebResponseUtils; import stirling.software.common.util.WebResponseUtils;
@RestController @RestController
@ -82,6 +85,22 @@ public class ScannerEffectController {
int resolution = request.getResolution(); int resolution = request.getResolution();
ScannerEffectRequest.Colorspace colorspace = request.getColorspace(); 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)) { try (PDDocument document = pdfDocumentFactory.load(file)) {
PDDocument outputDocument = new PDDocument(); PDDocument outputDocument = new PDDocument();
PDFRenderer pdfRenderer = new PDFRenderer(document); PDFRenderer pdfRenderer = new PDFRenderer(document);
@ -118,7 +137,14 @@ public class ScannerEffectController {
} }
// Render page to image with safe resolution // 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( log.debug(
"Processing page {} with dimensions {}x{} ({} pixels) at {}dpi", "Processing page {} with dimensions {}x{} ({} pixels) at {}dpi",