operation)
+ throws IOException {
+ try {
+ return operation.render();
+ } catch (OutOfMemoryError | NegativeArraySizeException e) {
+ throw createOutOfMemoryDpiException(pageNumber, dpi, e);
+ }
+ }
+
+ /**
+ * Execute a PDF rendering operation with automatic OutOfMemory exception handling (no page
+ * number).
+ *
+ * Use this variant when you don't have a specific page number context.
+ *
+ * @param the return type of the rendering operation
+ * @param dpi the DPI value used for rendering
+ * @param operation the rendering operation to execute
+ * @return the result of the rendering operation
+ * @throws OutOfMemoryDpiException if OutOfMemoryError or NegativeArraySizeException occurs
+ * @throws IOException if any other I/O error occurs during rendering
+ */
+ public static T handleOomRendering(int dpi, RenderOperation operation)
+ throws IOException {
+ try {
+ return operation.render();
+ } catch (OutOfMemoryError | NegativeArraySizeException e) {
+ throw createOutOfMemoryDpiException(dpi, e);
+ }
+ }
+
+ /**
+ * Create IllegalArgumentException from ErrorCode with formatted arguments.
+ *
+ * @param errorCode the error code
+ * @param args optional arguments for message formatting
+ * @return IllegalArgumentException with formatted message
+ */
+ public static IllegalArgumentException createIllegalArgumentException(
+ ErrorCode errorCode, Object... args) {
+ requireNonNull(errorCode, "errorCode");
+ String message = getMessage(errorCode, args);
+ return new IllegalArgumentException(message);
+ }
+
+ /**
+ * Create a PdfCorruptedException with internationalized message and context.
*
* @param context additional context (e.g., "during merge", "during image extraction")
* @param cause the original exception
- * @return IOException with user-friendly message
+ * @return PdfCorruptedException with user-friendly message
*/
- public static IOException createPdfCorruptedException(String context, Exception cause) {
+ public static PdfCorruptedException createPdfCorruptedException(
+ String context, Exception cause) {
+ requireNonNull(cause, "cause");
+
String message;
if (context != null && !context.isEmpty()) {
- message =
- String.format(
- Locale.ROOT,
- "Error %s: PDF file appears to be corrupted or damaged. Please try using the 'Repair PDF' feature first to fix the file before proceeding with this operation.",
- context);
+ String contextKey = "error.pdfCorruptedDuring";
+ String defaultMsg =
+ MessageFormat.format(
+ "Error {0}: {1}", context, getMessage(ErrorCode.PDF_CORRUPTED));
+ message = getMessage(contextKey, defaultMsg, context);
} else {
message =
- "PDF file appears to be corrupted or damaged. Please try using the 'Repair PDF'"
- + " feature first to fix the file before proceeding with this operation.";
+ "PDF file appears to be corrupted or damaged. Please try using the 'Repair PDF' feature first to fix the file before proceeding with this operation.";
}
- return new IOException(message, cause);
+
+ return new PdfCorruptedException(message, cause, ErrorCode.PDF_CORRUPTED.getCode());
}
/**
- * Create an IOException with internationalized message for multiple corrupted PDFs.
+ * Create a PdfCorruptedException for multiple corrupted PDFs.
*
* @param cause the original exception
- * @return IOException with user-friendly message
+ * @return PdfCorruptedException with user-friendly message
*/
- public static IOException createMultiplePdfCorruptedException(Exception cause) {
- String message =
- "One or more PDF files appear to be corrupted or damaged. Please try using the"
- + " 'Repair PDF' feature on each file first before attempting to merge them.";
- return new IOException(message, cause);
+ public static PdfCorruptedException createMultiplePdfCorruptedException(Exception cause) {
+ requireNonNull(cause, "cause");
+ String message = getMessage(ErrorCode.PDF_MULTIPLE_CORRUPTED);
+ return new PdfCorruptedException(
+ message, cause, ErrorCode.PDF_MULTIPLE_CORRUPTED.getCode());
}
/**
- * Create an IOException with internationalized message for PDF encryption issues.
+ * Create a PdfEncryptionException with internationalized message.
*
* @param cause the original exception
- * @return IOException with user-friendly message
+ * @return PdfEncryptionException with user-friendly message
*/
- public static IOException createPdfEncryptionException(Exception cause) {
- String message =
- "The PDF appears to have corrupted encryption data. This can happen when the PDF"
- + " was created with incompatible encryption methods. Please try using the"
- + " 'Repair PDF' feature first, or contact the document creator for a new"
- + " copy.";
- return new IOException(message, cause);
+ public static PdfEncryptionException createPdfEncryptionException(Exception cause) {
+ requireNonNull(cause, "cause");
+ String message = getMessage(ErrorCode.PDF_ENCRYPTION);
+ return new PdfEncryptionException(message, cause, ErrorCode.PDF_ENCRYPTION.getCode());
}
/**
- * Create an IOException with internationalized message for PDF password issues.
+ * Create a PdfPasswordException with internationalized message.
*
* @param cause the original exception
- * @return IOException with user-friendly message
+ * @return PdfPasswordException with user-friendly message
*/
- public static IOException createPdfPasswordException(Exception cause) {
- String message =
- "The PDF Document is passworded and either the password was not provided or was"
- + " incorrect";
- return new IOException(message, cause);
+ public static PdfPasswordException createPdfPasswordException(Exception cause) {
+ requireNonNull(cause, "cause");
+ String message = getMessage(ErrorCode.PDF_PASSWORD);
+ return new PdfPasswordException(message, cause, ErrorCode.PDF_PASSWORD.getCode());
}
/**
- * Create an IOException with internationalized message for file processing errors.
+ * Create a CbrFormatException for corrupted or unsupported CBR/RAR archives.
+ *
+ * @param message the error message
+ * @return CbrFormatException with user-friendly message
+ */
+ public static CbrFormatException createCbrInvalidFormatException(String message) {
+ String fullMessage = message != null ? message : getMessage(ErrorCode.CBR_INVALID_FORMAT);
+ return new CbrFormatException(fullMessage, ErrorCode.CBR_INVALID_FORMAT.getCode());
+ }
+
+ /**
+ * Create a CbrFormatException for encrypted CBR/RAR archives. Note: This now uses
+ * CBR_INVALID_FORMAT as encryption is covered by that error.
+ *
+ * @return CbrFormatException with user-friendly message
+ */
+ public static CbrFormatException createCbrEncryptedException() {
+ String message = getMessage(ErrorCode.CBR_INVALID_FORMAT);
+ return new CbrFormatException(message, ErrorCode.CBR_INVALID_FORMAT.getCode());
+ }
+
+ /**
+ * Create a CbrFormatException for CBR files with no valid images.
+ *
+ * @return CbrFormatException with user-friendly message
+ */
+ public static CbrFormatException createCbrNoImagesException() {
+ String message = getMessage(ErrorCode.CBR_NO_IMAGES);
+ return new CbrFormatException(message, ErrorCode.CBR_NO_IMAGES.getCode());
+ }
+
+ /**
+ * Create a CbrFormatException for CBR files where images are corrupted beyond recovery.
+ *
+ * @return CbrFormatException with user-friendly message
+ */
+ public static CbrFormatException createCbrCorruptedImagesException() {
+ String message = getMessage(ErrorCode.CBR_NO_IMAGES);
+ return new CbrFormatException(message, ErrorCode.CBR_NO_IMAGES.getCode());
+ }
+
+ /**
+ * Create a CbrFormatException for non-CBR files.
+ *
+ * @return CbrFormatException with user-friendly message
+ */
+ public static CbrFormatException createNotCbrFileException() {
+ String message = getMessage(ErrorCode.CBR_NOT_CBR);
+ return new CbrFormatException(message, ErrorCode.CBR_NOT_CBR.getCode());
+ }
+
+ /**
+ * Create a CbzFormatException for invalid CBZ/ZIP archives.
+ *
+ * @param cause the original exception
+ * @return CbzFormatException with user-friendly message
+ */
+ public static CbzFormatException createCbzInvalidFormatException(Exception cause) {
+ String message = getMessage(ErrorCode.CBZ_INVALID_FORMAT);
+ return new CbzFormatException(message, cause, ErrorCode.CBZ_INVALID_FORMAT.getCode());
+ }
+
+ /**
+ * Create a CbzFormatException for empty CBZ archives. Note: This now uses CBZ_INVALID_FORMAT as
+ * empty archives are covered by that error.
+ *
+ * @return CbzFormatException with user-friendly message
+ */
+ public static CbzFormatException createCbzEmptyException() {
+ String message = getMessage(ErrorCode.CBZ_INVALID_FORMAT);
+ return new CbzFormatException(message, ErrorCode.CBZ_INVALID_FORMAT.getCode());
+ }
+
+ /**
+ * Create a CbzFormatException for CBZ files with no valid images.
+ *
+ * @return CbzFormatException with user-friendly message
+ */
+ public static CbzFormatException createCbzNoImagesException() {
+ String message = getMessage(ErrorCode.CBZ_NO_IMAGES);
+ return new CbzFormatException(message, ErrorCode.CBZ_NO_IMAGES.getCode());
+ }
+
+ /**
+ * Create a CbzFormatException for CBZ files where all images are corrupted. Note: This now uses
+ * CBZ_NO_IMAGES as corrupted images are covered by that error.
+ *
+ * @return CbzFormatException with user-friendly message
+ */
+ public static CbzFormatException createCbzCorruptedImagesException() {
+ String message = getMessage(ErrorCode.CBZ_NO_IMAGES);
+ return new CbzFormatException(message, ErrorCode.CBZ_NO_IMAGES.getCode());
+ }
+
+ /**
+ * Create a CbzFormatException for non-CBZ files.
+ *
+ * @return CbzFormatException with user-friendly message
+ */
+ public static CbzFormatException createNotCbzFileException() {
+ String message = getMessage(ErrorCode.CBZ_NOT_CBZ);
+ return new CbzFormatException(message, ErrorCode.CBZ_NOT_CBZ.getCode());
+ }
+
+ /**
+ * Create an EmlFormatException for empty or null EML files.
+ *
+ * @return EmlFormatException with user-friendly message
+ */
+ public static EmlFormatException createEmlEmptyException() {
+ String message = getMessage(ErrorCode.EML_EMPTY);
+ return new EmlFormatException(message, ErrorCode.EML_EMPTY.getCode());
+ }
+
+ /**
+ * Create an EmlFormatException for invalid EML structure.
+ *
+ * @return EmlFormatException with user-friendly message
+ */
+ public static EmlFormatException createEmlInvalidFormatException() {
+ String message = getMessage(ErrorCode.EML_INVALID_FORMAT);
+ return new EmlFormatException(message, ErrorCode.EML_INVALID_FORMAT.getCode());
+ }
+
+ /**
+ * Create an IOException for file processing errors.
*
* @param operation the operation being performed (e.g., "merge", "split", "convert")
* @param cause the original exception
* @return IOException with user-friendly message
*/
public static IOException createFileProcessingException(String operation, Exception cause) {
+ requireNonNull(operation, "operation");
+ requireNonNull(cause, "cause");
String message =
- String.format(
- Locale.ROOT,
- "An error occurred while processing the file during %s operation: %s",
+ getMessage(
+ ErrorCode.FILE_PROCESSING.getMessageKey(),
+ ErrorCode.FILE_PROCESSING.getDefaultMessage(),
operation,
cause.getMessage());
return new IOException(message, cause);
}
+ public static IOException createImageReadException(String filename) {
+ requireNonNull(filename, "filename");
+ String message =
+ getMessage(
+ ErrorCode.IMAGE_READ_ERROR.getMessageKey(),
+ ErrorCode.IMAGE_READ_ERROR.getDefaultMessage(),
+ filename);
+ return new IOException(message);
+ }
+
/**
* Create a generic IOException with internationalized message.
*
@@ -115,8 +602,8 @@ public class ExceptionUtils {
*/
public static IOException createIOException(
String messageKey, String defaultMessage, Exception cause, Object... args) {
- String message = MessageFormat.format(defaultMessage, args);
- return new IOException(message, cause);
+ String message = getMessage(messageKey, defaultMessage, args);
+ return cause != null ? new IOException(message, cause) : new IOException(message);
}
/**
@@ -130,8 +617,8 @@ public class ExceptionUtils {
*/
public static RuntimeException createRuntimeException(
String messageKey, String defaultMessage, Exception cause, Object... args) {
- String message = MessageFormat.format(defaultMessage, args);
- return new RuntimeException(message, cause);
+ String message = getMessage(messageKey, defaultMessage, args);
+ return cause != null ? new RuntimeException(message, cause) : new RuntimeException(message);
}
/**
@@ -144,117 +631,394 @@ public class ExceptionUtils {
*/
public static IllegalArgumentException createIllegalArgumentException(
String messageKey, String defaultMessage, Object... args) {
- String message = MessageFormat.format(defaultMessage, args);
+ String message = getMessage(messageKey, defaultMessage, args);
return new IllegalArgumentException(message);
}
/** Create file validation exceptions. */
public static IllegalArgumentException createHtmlFileRequiredException() {
- return createIllegalArgumentException(
- "error.fileFormatRequired", "File must be in {0} format", "HTML or ZIP");
+ String message = getMessage(ErrorCode.HTML_FILE_REQUIRED);
+ return new IllegalArgumentException(message);
}
public static IllegalArgumentException createPdfFileRequiredException() {
- return createIllegalArgumentException(
- "error.fileFormatRequired", "File must be in {0} format", "PDF");
+ String message = getMessage(ErrorCode.PDF_NOT_PDF);
+ return new IllegalArgumentException(message);
}
public static IllegalArgumentException createInvalidPageSizeException(String size) {
- return createIllegalArgumentException(
- "error.invalidFormat", "Invalid {0} format: {1}", "page size", size);
+ requireNonNull(size, "size");
+ String message =
+ getMessage(
+ ErrorCode.INVALID_PAGE_SIZE.getMessageKey(),
+ ErrorCode.INVALID_PAGE_SIZE.getDefaultMessage(),
+ size);
+ return new IllegalArgumentException(message);
+ }
+
+ public static IllegalArgumentException createFileNullOrEmptyException() {
+ String message = getMessage(ErrorCode.FILE_NULL_OR_EMPTY);
+ return new IllegalArgumentException(message);
+ }
+
+ public static IllegalArgumentException createFileNoNameException() {
+ String message = getMessage(ErrorCode.FILE_NO_NAME);
+ return new IllegalArgumentException(message);
+ }
+
+ public static IllegalArgumentException createPdfNoPages() {
+ String message = getMessage(ErrorCode.PDF_NO_PAGES);
+ return new IllegalArgumentException(message);
}
/** Create OCR-related exceptions. */
public static IOException createOcrLanguageRequiredException() {
- return createIOException(
- "error.optionsNotSpecified", "{0} options are not specified", null, "OCR language");
+ String message = getMessage(ErrorCode.OCR_LANGUAGE_REQUIRED);
+ return new IOException(message);
}
public static IOException createOcrInvalidLanguagesException() {
- return createIOException(
- "error.invalidFormat",
- "Invalid {0} format: {1}",
- null,
- "OCR languages",
- "none of the selected languages are valid");
+ String message = getMessage(ErrorCode.OCR_INVALID_LANGUAGES);
+ return new IOException(message);
}
public static IOException createOcrToolsUnavailableException() {
- return createIOException(
- "error.toolNotInstalled", "{0} is not installed", null, "OCR tools");
+ String message = getMessage(ErrorCode.OCR_TOOLS_UNAVAILABLE);
+ return new IOException(message);
+ }
+
+ public static IOException createOcrInvalidRenderTypeException() {
+ String message = getMessage(ErrorCode.OCR_INVALID_RENDER_TYPE);
+ return new IOException(message);
+ }
+
+ public static IOException createOcrProcessingFailedException(int returnCode) {
+ String message =
+ getMessage(
+ ErrorCode.OCR_PROCESSING_FAILED.getMessageKey(),
+ ErrorCode.OCR_PROCESSING_FAILED.getDefaultMessage(),
+ returnCode);
+ return new IOException(message);
}
/** Create system requirement exceptions. */
+ public static FfmpegRequiredException createFfmpegRequiredException() {
+ String message = getMessage(ErrorCode.FFMPEG_REQUIRED);
+ return new FfmpegRequiredException(message, ErrorCode.FFMPEG_REQUIRED.getCode());
+ }
+
public static IOException createPythonRequiredForWebpException() {
return createIOException(
"error.toolRequired", "{0} is required for {1}", null, "Python", "WebP conversion");
}
- public static IOException createFfmpegRequiredException() {
- return createIOException(
- "error.toolRequired",
- "{0} is required for {1}",
- null,
- "FFmpeg",
- "PDF to Video Slideshow conversion");
- }
-
- /** Create file operation exceptions. */
- public static IOException createFileNotFoundException(String fileId) {
- return createIOException("error.fileNotFound", "File not found with ID: {0}", null, fileId);
- }
-
- public static RuntimeException createPdfaConversionFailedException() {
- return createRuntimeException(
- "error.conversionFailed", "{0} conversion failed", null, "PDF/A");
- }
-
- public static IllegalArgumentException createInvalidComparatorException() {
- return createIllegalArgumentException(
- "error.invalidFormat",
- "Invalid {0} format: {1}",
- "comparator",
- "only 'greater', 'equal', and 'less' are supported");
- }
-
/** Create compression-related exceptions. */
public static RuntimeException createMd5AlgorithmException(Exception cause) {
- return createRuntimeException(
- "error.algorithmNotAvailable", "{0} algorithm not available", cause, "MD5");
+ requireNonNull(cause, "cause");
+ String message = getMessage(ErrorCode.MD5_ALGORITHM);
+ return new RuntimeException(message, cause);
}
- public static IllegalArgumentException createCompressionOptionsException() {
- return createIllegalArgumentException(
- "error.optionsNotSpecified",
- "{0} options are not specified",
- "compression (expected output size and optimize level)");
+ public static GhostscriptException createGhostscriptCompressionException() {
+ return createGhostscriptCompressionException(null, null);
}
- public static IOException createGhostscriptCompressionException() {
- return createIOException(
- "error.commandFailed", "{0} command failed", null, "Ghostscript compression");
+ public static GhostscriptException createGhostscriptCompressionException(String processOutput) {
+ return createGhostscriptCompressionException(processOutput, null);
}
- public static IOException createGhostscriptCompressionException(Exception cause) {
- return createIOException(
- "error.commandFailed", "{0} command failed", cause, "Ghostscript compression");
+ public static GhostscriptException createGhostscriptCompressionException(Exception cause) {
+ requireNonNull(cause, "cause");
+ return createGhostscriptCompressionException(cause.getMessage(), cause);
}
- public static IOException createQpdfCompressionException(Exception cause) {
- return createIOException("error.commandFailed", "{0} command failed", cause, "QPDF");
+ public static GhostscriptException createGhostscriptCompressionException(
+ String processOutput, Exception cause) {
+ GhostscriptErrorInfo errorInfo = analyzeGhostscriptOutput(processOutput, cause);
+ return buildGhostscriptException(errorInfo, processOutput, cause);
+ }
+
+ public static GhostscriptException detectGhostscriptCriticalError(String processOutput) {
+ GhostscriptErrorInfo errorInfo = analyzeGhostscriptOutput(processOutput, null);
+ if (errorInfo.critical()) {
+ return buildGhostscriptException(errorInfo, processOutput, null);
+ }
+ return null;
+ }
+
+ private static GhostscriptException buildGhostscriptException(
+ GhostscriptErrorInfo errorInfo, String processOutput, Exception cause) {
+ String targetDescription;
+ if (errorInfo.affectedPages() != null && !errorInfo.affectedPages().isEmpty()) {
+ if (errorInfo.affectedPages().size() == 1) {
+ targetDescription = "page " + errorInfo.affectedPages().get(0);
+ } else {
+ targetDescription =
+ "pages "
+ + String.join(
+ ", ",
+ errorInfo.affectedPages().stream()
+ .map(String::valueOf)
+ .toArray(String[]::new));
+ }
+ } else {
+ targetDescription = "the input file";
+ }
+
+ String diagnostic =
+ errorInfo.diagnostic() != null
+ ? errorInfo.diagnostic()
+ : deriveDefaultGhostscriptDiagnostic(processOutput);
+
+ String message;
+ if (errorInfo.errorCode() == ErrorCode.GHOSTSCRIPT_PAGE_DRAWING) {
+ message =
+ getMessage(
+ errorInfo.errorCode().getMessageKey(),
+ errorInfo.errorCode().getDefaultMessage(),
+ targetDescription,
+ diagnostic);
+ } else {
+ message = getMessage(errorInfo.errorCode());
+ if (errorInfo.diagnostic() != null && !errorInfo.diagnostic().isBlank()) {
+ message = message + " " + errorInfo.diagnostic();
+ }
+ }
+
+ return new GhostscriptException(message, cause, errorInfo.errorCode().getCode());
+ }
+
+ private static GhostscriptErrorInfo analyzeGhostscriptOutput(
+ String processOutput, Exception cause) {
+ String combinedOutput = processOutput;
+ if ((combinedOutput == null || combinedOutput.isBlank()) && cause != null) {
+ combinedOutput = cause.getMessage();
+ }
+
+ if (combinedOutput == null || combinedOutput.isBlank()) {
+ return GhostscriptErrorInfo.unknown();
+ }
+
+ String[] lines =
+ RegexPatternUtils.getInstance().getLineSeparatorPattern().split(combinedOutput);
+ List affectedPages = new ArrayList<>();
+ Set uniqueDiagnostics = new java.util.LinkedHashSet<>();
+ boolean recognized = false;
+ Integer currentPage = null;
+
+ for (String rawLine : lines) {
+ String line = rawLine == null ? "" : rawLine.trim();
+ if (line.isEmpty()) {
+ continue;
+ }
+
+ // Check for page number markers
+ Matcher pageMatcher = GS_PAGE_PATTERN.matcher(line);
+ if (pageMatcher.find()) {
+ try {
+ currentPage = Integer.parseInt(pageMatcher.group(1));
+ } catch (NumberFormatException ignore) {
+ // Ignore invalid page numbers and continue parsing
+ }
+ }
+
+ String lowerLine = line.toLowerCase(Locale.ROOT);
+ if (lowerLine.contains("page drawing error")
+ || lowerLine.contains("could not draw this page")
+ || lowerLine.contains("eps files may not contain multiple pages")) {
+ recognized = true;
+
+ // Record the page number if we found an error
+ if (currentPage != null && !affectedPages.contains(currentPage)) {
+ affectedPages.add(currentPage);
+ }
+
+ String normalized = normalizeGhostscriptLine(line);
+ if (!normalized.isEmpty() && !normalized.startsWith("GPL Ghostscript")) {
+ uniqueDiagnostics.add(normalized);
+ }
+ }
+ }
+
+ if (recognized) {
+ // Build a clean diagnostic message without duplicates
+ String diagnostic = String.join(". ", uniqueDiagnostics);
+ if (!diagnostic.isEmpty() && diagnostic.charAt(diagnostic.length() - 1) != '.') {
+ diagnostic += ".";
+ }
+
+ // Use the first page number, or null if none found
+ Integer pageNumber = affectedPages.isEmpty() ? null : affectedPages.get(0);
+
+ return new GhostscriptErrorInfo(
+ ErrorCode.GHOSTSCRIPT_PAGE_DRAWING,
+ pageNumber,
+ diagnostic,
+ true,
+ affectedPages);
+ }
+
+ // Fallback: capture the first non-empty informative line for context
+ for (String rawLine : lines) {
+ String line = rawLine == null ? "" : rawLine.trim();
+ if (line.isEmpty() || line.startsWith("GPL Ghostscript")) {
+ continue;
+ }
+ String normalized = normalizeGhostscriptLine(line);
+ if (!normalized.isEmpty()) {
+ return new GhostscriptErrorInfo(
+ ErrorCode.GHOSTSCRIPT_COMPRESSION, null, normalized, false, List.of());
+ }
+ }
+
+ return new GhostscriptErrorInfo(
+ ErrorCode.GHOSTSCRIPT_COMPRESSION, null, null, false, List.of());
+ }
+
+ private static String normalizeGhostscriptLine(String line) {
+ if (line == null) {
+ return "";
+ }
+ String trimmed = line.trim();
+ if (trimmed.isEmpty()) {
+ return "";
+ }
+ return RegexPatternUtils.getInstance()
+ .getLeadingAsterisksWhitespacePattern()
+ .matcher(trimmed)
+ .replaceFirst("");
+ }
+
+ private static String deriveDefaultGhostscriptDiagnostic(String processOutput) {
+ return getMessage(
+ "error.ghostscriptDefaultDiagnostic",
+ "The source file contains content Ghostscript cannot render.");
}
public static IOException createGhostscriptConversionException(String outputType) {
- return createIOException(
- "error.commandFailed", "{0} command failed", null, "Ghostscript " + outputType);
+ requireNonNull(outputType, "outputType");
+ String message =
+ getMessage(
+ ErrorCode.GHOSTSCRIPT_COMPRESSION.getMessageKey(),
+ ErrorCode.GHOSTSCRIPT_COMPRESSION.getDefaultMessage(),
+ outputType);
+ return new IOException(message);
+ }
+
+ public static IOException createProcessingInterruptedException(
+ String processType, InterruptedException cause) {
+ requireNonNull(processType, "processType");
+ requireNonNull(cause, "cause");
+ String message =
+ getMessage(
+ ErrorCode.PROCESSING_INTERRUPTED.getMessageKey(),
+ ErrorCode.PROCESSING_INTERRUPTED.getDefaultMessage(),
+ processType);
+ return new IOException(message, cause);
+ }
+
+ public static RuntimeException createPdfaConversionFailedException() {
+ String message = getMessage(ErrorCode.PDFA_CONVERSION_FAILED);
+ return new RuntimeException(message);
+ }
+
+ public static IllegalArgumentException createInvalidArgumentException(
+ String argumentName, String value) {
+ requireNonNull(argumentName, "argumentName");
+ requireNonNull(value, "value");
+ String message =
+ getMessage(
+ ErrorCode.INVALID_ARGUMENT.getMessageKey(),
+ ErrorCode.INVALID_ARGUMENT.getDefaultMessage(),
+ argumentName,
+ value);
+ return new IllegalArgumentException(message);
+ }
+
+ public static IllegalArgumentException createNullArgumentException(String argumentName) {
+ requireNonNull(argumentName, "argumentName");
+ String message =
+ getMessage(
+ ErrorCode.NULL_ARGUMENT.getMessageKey(),
+ ErrorCode.NULL_ARGUMENT.getDefaultMessage(),
+ argumentName);
+ return new IllegalArgumentException(message);
+ }
+
+ /**
+ * Create an OutOfMemoryDpiException 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 OutOfMemoryDpiException with user-friendly message
+ */
+ public static OutOfMemoryDpiException createOutOfMemoryDpiException(
+ int pageNumber, int dpi, Throwable cause) {
+ requireNonNull(cause, "cause");
+ String message =
+ getMessage(
+ ErrorCode.OUT_OF_MEMORY_DPI.getMessageKey(),
+ ErrorCode.OUT_OF_MEMORY_DPI.getDefaultMessage(),
+ pageNumber,
+ dpi);
+ return new OutOfMemoryDpiException(message, cause, ErrorCode.OUT_OF_MEMORY_DPI.getCode());
+ }
+
+ /**
+ * Create an OutOfMemoryDpiException 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 OutOfMemoryDpiException with user-friendly message
+ */
+ public static OutOfMemoryDpiException createOutOfMemoryDpiException(
+ int pageNumber, int dpi, OutOfMemoryError cause) {
+ return createOutOfMemoryDpiException(pageNumber, dpi, (Throwable) cause);
+ }
+
+ /**
+ * Create an OutOfMemoryDpiException 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 OutOfMemoryDpiException with user-friendly message
+ */
+ public static OutOfMemoryDpiException createOutOfMemoryDpiException(int dpi, Throwable cause) {
+ requireNonNull(cause, "cause");
+ String message =
+ getMessage(
+ ErrorCode.OUT_OF_MEMORY_DPI.getMessageKey(),
+ ErrorCode.OUT_OF_MEMORY_DPI.getDefaultMessage(),
+ dpi);
+ return new OutOfMemoryDpiException(message, cause, ErrorCode.OUT_OF_MEMORY_DPI.getCode());
+ }
+
+ /**
+ * Create an OutOfMemoryDpiException for OutOfMemoryError when rendering PDF images with DPI.
+ *
+ * @param dpi the DPI value used
+ * @param cause the original OutOfMemoryError
+ * @return OutOfMemoryDpiException with user-friendly message
+ */
+ public static OutOfMemoryDpiException createOutOfMemoryDpiException(
+ int dpi, OutOfMemoryError cause) {
+ return createOutOfMemoryDpiException(dpi, (Throwable) cause);
}
/**
* Check if an exception indicates a corrupted PDF and wrap it with appropriate message.
*
* @param e the exception to check
- * @return the original exception if not PDF corruption, or a new IOException with user-friendly
- * message
+ * @return the original exception if not PDF corruption, or a new PdfCorruptedException with
+ * user-friendly message
*/
public static IOException handlePdfException(IOException e) {
return handlePdfException(e, null);
@@ -265,10 +1029,12 @@ public class ExceptionUtils {
*
* @param e the exception to check
* @param context additional context for the error
- * @return the original exception if not PDF corruption, or a new IOException with user-friendly
+ * @return the original exception if not PDF corruption, or a new exception with user-friendly
* message
*/
public static IOException handlePdfException(IOException e, String context) {
+ requireNonNull(e, "exception");
+
if (PdfErrorUtils.isCorruptedPdfError(e)) {
return createPdfCorruptedException(context, e);
}
@@ -284,6 +1050,340 @@ public class ExceptionUtils {
return e; // Return original exception if no specific handling needed
}
+ /**
+ * Log an exception with appropriate level based on its type. Returns the exception for fluent
+ * log-and-throw pattern.
+ *
+ * @param the exception type
+ * @param operation the operation being performed
+ * @param exception the exception that occurred
+ * @return the exception (for fluent log-and-throw)
+ */
+ public static T logException(String operation, T exception) {
+ requireNonNull(operation, "operation");
+ requireNonNull(exception, "exception");
+
+ if (PdfErrorUtils.isCorruptedPdfError(exception)) {
+ log.warn("PDF corruption detected during {}: {}", operation, exception.getMessage());
+ } else if (exception instanceof IOException
+ && (isEncryptionError((IOException) exception)
+ || isPasswordError((IOException) exception))) {
+ log.info("PDF security issue during {}: {}", operation, exception.getMessage());
+ } else {
+ log.error("Unexpected error during {}", operation, exception);
+ }
+
+ return exception;
+ }
+
+ /**
+ * Wrap a generic exception with appropriate context for better error reporting. This method is
+ * useful when catching exceptions in controllers and wanting to provide better context to
+ * GlobalExceptionHandler.
+ *
+ * @param e the exception to wrap
+ * @param operation the operation being performed (e.g., "PDF merge", "image extraction")
+ * @return a RuntimeException wrapping the original exception with operation context
+ */
+ public static RuntimeException wrapException(Exception e, String operation) {
+ requireNonNull(e, "exception");
+ requireNonNull(operation, "operation");
+
+ if (e instanceof RuntimeException) {
+ return (RuntimeException) e;
+ }
+
+ if (e instanceof IOException) {
+ IOException ioException = handlePdfException((IOException) e, operation);
+ // BaseAppException extends IOException, wrap it in RuntimeException for rethrowing
+ if (ioException instanceof BaseAppException) {
+ return new RuntimeException(ioException);
+ }
+ return new RuntimeException(createFileProcessingException(operation, e));
+ }
+
+ return new RuntimeException(
+ MessageFormat.format("Error during {0}: {1}", operation, e.getMessage()), e);
+ }
+
+ /**
+ * Error codes for consistent error tracking and documentation. Each error code includes a
+ * unique identifier, i18n message key, and default message.
+ *
+ * These codes are used by {@link stirling.software.SPDF.exception.GlobalExceptionHandler} to
+ * provide consistent RFC 7807 Problem Details responses.
+ */
+ @Getter
+ public enum ErrorCode {
+ // PDF-related errors
+ PDF_CORRUPTED(
+ "E001",
+ "error.pdfCorrupted",
+ "PDF file appears to be corrupted or damaged. Please try using the 'Repair PDF' feature first to fix the file before proceeding with this operation."),
+ PDF_MULTIPLE_CORRUPTED(
+ "E002",
+ "error.multiplePdfCorrupted",
+ "One or more PDF files appear to be corrupted or damaged. Please try using the 'Repair PDF' feature on each file first before attempting to merge them."),
+ PDF_ENCRYPTION(
+ "E003",
+ "error.pdfEncryption",
+ "The PDF appears to have corrupted encryption data. This can happen when the PDF was created with incompatible encryption methods. Please try using the 'Repair PDF' feature first, or contact the document creator for a new copy."),
+ PDF_PASSWORD(
+ "E004",
+ "error.pdfPassword",
+ "The PDF Document is passworded and either the password was not provided or was incorrect"),
+ PDF_NO_PAGES("E005", "error.pdfNoPages", "PDF file contains no pages"),
+ PDF_NOT_PDF("E006", "error.notPdfFile", "File must be in PDF format"),
+
+ // CBR/CBZ errors
+ CBR_INVALID_FORMAT(
+ "E010",
+ "error.cbrInvalidFormat",
+ "Invalid or corrupted CBR/RAR archive. The file may be corrupted, use an unsupported RAR format (RAR5+), encrypted, or may not be a valid RAR archive."),
+ CBR_NO_IMAGES(
+ "E012",
+ "error.cbrNoImages",
+ "No valid images found in the CBR file. The archive may be empty, or all images may be corrupted or in unsupported formats."),
+ CBR_NOT_CBR("E014", "error.notCbrFile", "File must be a CBR or RAR archive"),
+ CBZ_INVALID_FORMAT(
+ "E015",
+ "error.cbzInvalidFormat",
+ "Invalid or corrupted CBZ/ZIP archive. The file may be empty, corrupted, or may not be a valid ZIP archive."),
+ CBZ_NO_IMAGES(
+ "E016",
+ "error.cbzNoImages",
+ "No valid images found in the CBZ file. The archive may be empty, or all images may be corrupted or in unsupported formats."),
+ CBZ_NOT_CBZ("E018", "error.notCbzFile", "File must be a CBZ or ZIP archive"),
+
+ // EML errors
+ EML_EMPTY("E020", "error.emlEmpty", "EML file is empty or null"),
+ EML_INVALID_FORMAT("E021", "error.emlInvalidFormat", "Invalid EML file format"),
+
+ // File processing errors
+ FILE_NOT_FOUND("E030", "error.fileNotFound", "File not found with ID: {0}"),
+ FILE_PROCESSING(
+ "E031",
+ "error.fileProcessing",
+ "An error occurred while processing the file during {0} operation: {1}"),
+ FILE_NULL_OR_EMPTY("E032", "error.fileNullOrEmpty", "File cannot be null or empty"),
+ FILE_NO_NAME("E033", "error.fileNoName", "File must have a name"),
+ IMAGE_READ_ERROR("E034", "error.imageReadError", "Unable to read image from file: {0}"),
+
+ // OCR errors
+ OCR_LANGUAGE_REQUIRED(
+ "E040", "error.ocrLanguageRequired", "OCR language options are not specified"),
+ OCR_INVALID_LANGUAGES(
+ "E041",
+ "error.ocrInvalidLanguages",
+ "Invalid OCR languages format: none of the selected languages are valid"),
+ OCR_TOOLS_UNAVAILABLE("E042", "error.ocrToolsUnavailable", "OCR tools are not installed"),
+ OCR_INVALID_RENDER_TYPE(
+ "E043",
+ "error.ocrInvalidRenderType",
+ "Invalid OCR render type. Must be 'hocr' or 'sandwich'"),
+ OCR_PROCESSING_FAILED(
+ "E044", "error.ocrProcessingFailed", "OCRmyPDF failed with return code: {0}"),
+
+ // Compression errors
+ COMPRESSION_OPTIONS(
+ "E050",
+ "error.compressionOptions",
+ "Compression options are not specified (expected output size and optimize level)"),
+ GHOSTSCRIPT_COMPRESSION(
+ "E051", "error.ghostscriptCompression", "Ghostscript compression command failed"),
+ QPDF_COMPRESSION("E052", "error.qpdfCompression", "QPDF command failed"),
+ PROCESSING_INTERRUPTED(
+ "E053", "error.processingInterrupted", "{0} processing was interrupted"),
+ GHOSTSCRIPT_PAGE_DRAWING(
+ "E054", "error.ghostscriptPageDrawing", "Ghostscript could not render {0}. {1}"),
+
+ // Conversion errors
+ PDFA_CONVERSION_FAILED("E060", "error.pdfaConversionFailed", "PDF/A conversion failed"),
+ HTML_FILE_REQUIRED("E061", "error.htmlFileRequired", "File must be in HTML or ZIP format"),
+ PYTHON_REQUIRED_WEBP(
+ "E062", "error.pythonRequiredWebp", "Python is required for WebP conversion"),
+ FFMPEG_REQUIRED(
+ "E063",
+ "error.ffmpegRequired",
+ "FFmpeg must be installed to convert PDFs to video. Install FFmpeg and ensure it is available on the system PATH."),
+
+ // Validation errors
+ INVALID_ARGUMENT("E070", "error.invalidArgument", "Invalid argument ''{0}'': {1}"),
+ NULL_ARGUMENT("E071", "error.nullArgument", "{0} must not be null"),
+ INVALID_PAGE_SIZE("E072", "error.invalidPageSize", "Invalid page size format: {0}"),
+ INVALID_COMPARATOR(
+ "E073",
+ "error.invalidComparator",
+ "Invalid comparator format: only 'greater', 'equal', and 'less' are supported"),
+
+ // System errors
+ MD5_ALGORITHM("E080", "error.md5Algorithm", "MD5 algorithm not available"),
+ OUT_OF_MEMORY_DPI(
+ "E081",
+ "error.outOfMemoryDpi",
+ "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.");
+
+ private final String code;
+ private final String messageKey;
+ private final String defaultMessage;
+
+ ErrorCode(String code, String messageKey, String defaultMessage) {
+ this.code = code;
+ this.messageKey = messageKey;
+ this.defaultMessage = defaultMessage;
+ }
+ }
+
+ /**
+ * Functional interface for PDF rendering operations that may throw OutOfMemoryError or
+ * NegativeArraySizeException.
+ *
+ * @param the return type of the operation
+ */
+ @FunctionalInterface
+ public interface RenderOperation {
+ T render() throws IOException;
+ }
+
+ /**
+ * Common interface for exceptions that provide error codes.
+ *
+ * This interface enables polymorphic handling of different exception types (BaseAppException
+ * and BaseValidationException) without using instanceof checks.
+ *
+ * @see BaseAppException
+ * @see BaseValidationException
+ */
+ public interface ErrorCodeProvider {
+ /**
+ * Get the error message.
+ *
+ * @return the error message
+ */
+ String getMessage();
+
+ /**
+ * Get the error code.
+ *
+ * @return the error code (e.g., "E001")
+ */
+ String getErrorCode();
+ }
+
+ /** Exception thrown when Ghostscript fails to render or compress a file. */
+ public static class GhostscriptException extends BaseAppException {
+ public GhostscriptException(String message, Throwable cause, String errorCode) {
+ super(message, cause, errorCode);
+ }
+ }
+
+ /** Exception thrown when FFmpeg is not available on the host system. */
+ public static class FfmpegRequiredException extends BaseAppException {
+ public FfmpegRequiredException(String message, String errorCode) {
+ super(message, null, errorCode);
+ }
+
+ public FfmpegRequiredException(String message, Throwable cause, String errorCode) {
+ super(message, cause, errorCode);
+ }
+ }
+
+ private record GhostscriptErrorInfo(
+ ErrorCode errorCode,
+ Integer pageNumber,
+ String diagnostic,
+ boolean critical,
+ List affectedPages) {
+ private GhostscriptErrorInfo(
+ ErrorCode errorCode,
+ Integer pageNumber,
+ String diagnostic,
+ boolean critical,
+ List affectedPages) {
+ this.errorCode = errorCode;
+ this.pageNumber = pageNumber;
+ this.diagnostic = diagnostic;
+ this.critical = critical;
+ this.affectedPages = affectedPages != null ? affectedPages : List.of();
+ }
+
+ private static GhostscriptErrorInfo unknown() {
+ return new GhostscriptErrorInfo(
+ ErrorCode.GHOSTSCRIPT_COMPRESSION, null, null, false, List.of());
+ }
+ }
+
+ /** Base exception with error code support for IO-related errors. */
+ public abstract static class BaseAppException extends IOException implements ErrorCodeProvider {
+ @Getter(onMethod_ = {@Override})
+ private final String errorCode;
+
+ protected BaseAppException(String message, Throwable cause, String errorCode) {
+ super(message, cause);
+ this.errorCode = errorCode;
+ }
+ }
+
+ /** Base exception with error code support for illegal argument errors. */
+ public abstract static class BaseValidationException extends IllegalArgumentException
+ implements ErrorCodeProvider {
+ @Getter(onMethod_ = {@Override})
+ private final String errorCode;
+
+ protected BaseValidationException(String message, String errorCode) {
+ super(message);
+ this.errorCode = errorCode;
+ }
+
+ protected BaseValidationException(String message, Throwable cause, String errorCode) {
+ super(message, cause);
+ this.errorCode = errorCode;
+ }
+ }
+
+ /** Exception thrown when a PDF file is corrupted or damaged. */
+ public static class PdfCorruptedException extends BaseAppException {
+ public PdfCorruptedException(String message, Throwable cause, String errorCode) {
+ super(message, cause, errorCode);
+ }
+ }
+
+ /** Exception thrown when a PDF has encryption/decryption issues. */
+ public static class PdfEncryptionException extends BaseAppException {
+ public PdfEncryptionException(String message, Throwable cause, String errorCode) {
+ super(message, cause, errorCode);
+ }
+ }
+
+ /** Exception thrown when PDF password is incorrect or missing. */
+ public static class PdfPasswordException extends BaseAppException {
+ public PdfPasswordException(String message, Throwable cause, String errorCode) {
+ super(message, cause, errorCode);
+ }
+ }
+
+ /** Exception thrown when CBR/RAR archive is invalid or unsupported. */
+ public static class CbrFormatException extends BaseValidationException {
+ public CbrFormatException(String message, String errorCode) {
+ super(message, errorCode);
+ }
+
+ public CbrFormatException(String message, Throwable cause, String errorCode) {
+ super(message, cause, errorCode);
+ }
+ }
+
+ /** Exception thrown when CBZ/ZIP archive is invalid. */
+ public static class CbzFormatException extends BaseValidationException {
+ public CbzFormatException(String message, String errorCode) {
+ super(message, errorCode);
+ }
+
+ public CbzFormatException(String message, Throwable cause, String errorCode) {
+ super(message, cause, errorCode);
+ }
+ }
+
/**
* Check if an exception indicates a PDF encryption/decryption error.
*
@@ -315,100 +1415,17 @@ public class ExceptionUtils {
|| message.contains("PDF contains an encryption dictionary");
}
- /**
- * Log an exception with appropriate level based on its type.
- *
- * @param operation the operation being performed
- * @param e the exception that occurred
- */
- public static void logException(String operation, Exception e) {
- if (PdfErrorUtils.isCorruptedPdfError(e)) {
- log.warn("PDF corruption detected during {}: {}", operation, e.getMessage());
- } else if (e instanceof IOException
- && (isEncryptionError((IOException) e) || isPasswordError((IOException) e))) {
- log.info("PDF security issue during {}: {}", operation, e.getMessage());
- } else {
- log.error("Unexpected error during {}", operation, e);
+ /** Exception thrown when EML file format is invalid. */
+ public static class EmlFormatException extends BaseValidationException {
+ public EmlFormatException(String message, String errorCode) {
+ super(message, errorCode);
}
}
- /** Create common validation exceptions. */
- public static IllegalArgumentException createInvalidArgumentException(String argumentName) {
- return createIllegalArgumentException(
- "error.invalidArgument", "Invalid argument: {0}", argumentName);
- }
-
- public static IllegalArgumentException createInvalidArgumentException(
- String argumentName, String value) {
- return createIllegalArgumentException(
- "error.invalidFormat", "Invalid {0} format: {1}", argumentName, value);
- }
-
- public static IllegalArgumentException createNullArgumentException(String argumentName) {
- 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);
+ /** Exception thrown when rendering PDF pages causes out-of-memory or array size errors. */
+ public static class OutOfMemoryDpiException extends BaseAppException {
+ public OutOfMemoryDpiException(String message, Throwable cause, String errorCode) {
+ super(message, cause, errorCode);
+ }
}
}
diff --git a/app/common/src/main/java/stirling/software/common/util/GeneralUtils.java b/app/common/src/main/java/stirling/software/common/util/GeneralUtils.java
index a6724b628..874417990 100644
--- a/app/common/src/main/java/stirling/software/common/util/GeneralUtils.java
+++ b/app/common/src/main/java/stirling/software/common/util/GeneralUtils.java
@@ -1100,17 +1100,29 @@ public class GeneralUtils {
ProcessExecutor.getInstance(ProcessExecutor.Processes.GHOSTSCRIPT)
.runCommandWithOutputHandling(command);
+ ExceptionUtils.GhostscriptException detectedError =
+ ExceptionUtils.detectGhostscriptCriticalError(result.getMessages());
+ if (detectedError != null) {
+ log.warn(
+ "Ghostscript ebook optimization reported a critical error: {}",
+ detectedError.getMessage());
+ throw detectedError;
+ }
+
if (result.getRc() != 0) {
log.warn(
"Ghostscript ebook optimization failed with return code: {}",
result.getRc());
- throw ExceptionUtils.createGhostscriptCompressionException();
+ throw ExceptionUtils.createGhostscriptCompressionException(result.getMessages());
}
return Files.readAllBytes(tempOutput);
} catch (Exception e) {
log.warn("Ghostscript ebook optimization failed", e);
+ if (e instanceof ExceptionUtils.GhostscriptException ghostscriptException) {
+ throw ghostscriptException;
+ }
throw ExceptionUtils.createGhostscriptCompressionException(e);
} finally {
if (tempInput != null) {
diff --git a/app/common/src/main/java/stirling/software/common/util/ImageProcessingUtils.java b/app/common/src/main/java/stirling/software/common/util/ImageProcessingUtils.java
index 77bc8c7b3..2a23080e0 100644
--- a/app/common/src/main/java/stirling/software/common/util/ImageProcessingUtils.java
+++ b/app/common/src/main/java/stirling/software/common/util/ImageProcessingUtils.java
@@ -144,7 +144,7 @@ public class ImageProcessingUtils {
}
if (image == null) {
- throw new IOException("Unable to read image from file: " + filename);
+ throw ExceptionUtils.createImageReadException(filename);
}
double orientation = extractImageOrientation(file.getInputStream());
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 306939069..3d17cfc8e 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
@@ -35,7 +35,7 @@ public class PdfToCbrUtils {
try (PDDocument document = pdfDocumentFactory.load(pdfFile)) {
if (document.getNumberOfPages() == 0) {
- throw new IllegalArgumentException("PDF file contains no pages");
+ throw ExceptionUtils.createPdfNoPages();
}
return createCbrFromPdf(document, dpi);
@@ -44,17 +44,17 @@ public class PdfToCbrUtils {
private static void validatePdfFile(MultipartFile file) {
if (file == null || file.isEmpty()) {
- throw new IllegalArgumentException("File cannot be null or empty");
+ throw ExceptionUtils.createFileNullOrEmptyException();
}
String filename = file.getOriginalFilename();
if (filename == null) {
- throw new IllegalArgumentException("File must have a name");
+ throw ExceptionUtils.createFileNoNameException();
}
String extension = FilenameUtils.getExtension(filename).toLowerCase(Locale.ROOT);
if (!"pdf".equals(extension)) {
- throw new IllegalArgumentException("File must be a PDF");
+ throw ExceptionUtils.createPdfFileRequiredException();
}
}
@@ -67,28 +67,36 @@ public class PdfToCbrUtils {
int totalPages = document.getNumberOfPages();
for (int pageIndex = 0; pageIndex < totalPages; pageIndex++) {
+ final int currentPage = pageIndex;
try {
BufferedImage image =
- pdfRenderer.renderImageWithDPI(pageIndex, dpi, ImageType.RGB);
+ ExceptionUtils.handleOomRendering(
+ currentPage + 1,
+ dpi,
+ () ->
+ pdfRenderer.renderImageWithDPI(
+ currentPage, dpi, ImageType.RGB));
String imageFilename =
- String.format(Locale.ROOT, "page_%03d.png", pageIndex + 1);
+ String.format(Locale.ROOT, "page_%03d.png", currentPage + 1);
Path imagePath = tempDir.resolve(imageFilename);
ImageIO.write(image, "PNG", imagePath.toFile());
generatedImages.add(imagePath);
+ } catch (ExceptionUtils.OutOfMemoryDpiException e) {
+ // Re-throw OOM exceptions without wrapping
+ throw e;
} catch (IOException e) {
- log.warn("Error processing page {}: {}", pageIndex + 1, e.getMessage());
- } catch (OutOfMemoryError e) {
- throw ExceptionUtils.createOutOfMemoryDpiException(pageIndex + 1, dpi, e);
- } catch (NegativeArraySizeException e) {
- throw ExceptionUtils.createOutOfMemoryDpiException(pageIndex + 1, dpi, e);
+ // Wrap other IOExceptions with context
+ throw ExceptionUtils.createFileProcessingException(
+ "CBR creation for page " + (currentPage + 1), e);
}
}
if (generatedImages.isEmpty()) {
- throw new IOException("Failed to render any pages to images for CBR conversion");
+ throw ExceptionUtils.createFileProcessingException(
+ "CBR conversion", new IOException("No pages were successfully rendered"));
}
return createRarArchive(tempDir, generatedImages);
@@ -117,15 +125,18 @@ public class PdfToCbrUtils {
ProcessExecutorResult result =
executor.runCommandWithOutputHandling(command, tempDir.toFile());
if (result.getRc() != 0) {
- throw new IOException("RAR command failed: " + result.getMessages());
+ throw ExceptionUtils.createFileProcessingException(
+ "RAR archive creation",
+ new IOException("RAR command failed with code " + result.getRc()));
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
- throw new IOException("RAR command interrupted", e);
+ throw ExceptionUtils.createProcessingInterruptedException("RAR creation", e);
}
if (!Files.exists(rarFile)) {
- throw new IOException("RAR file was not created");
+ throw ExceptionUtils.createFileProcessingException(
+ "RAR archive creation", new IOException("RAR file was not created"));
}
try (FileInputStream fis = new FileInputStream(rarFile.toFile());
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 d0557406b..4fee38a7f 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
@@ -30,7 +30,7 @@ public class PdfToCbzUtils {
try (PDDocument document = pdfDocumentFactory.load(pdfFile)) {
if (document.getNumberOfPages() == 0) {
- throw new IllegalArgumentException("PDF file contains no pages");
+ throw ExceptionUtils.createPdfNoPages();
}
return createCbzFromPdf(document, dpi);
@@ -39,17 +39,17 @@ public class PdfToCbzUtils {
private static void validatePdfFile(MultipartFile file) {
if (file == null || file.isEmpty()) {
- throw new IllegalArgumentException("File cannot be null or empty");
+ throw ExceptionUtils.createFileNullOrEmptyException();
}
String filename = file.getOriginalFilename();
if (filename == null) {
- throw new IllegalArgumentException("File must have a name");
+ throw ExceptionUtils.createFileNoNameException();
}
String extension = FilenameUtils.getExtension(filename).toLowerCase(Locale.ROOT);
if (!"pdf".equals(extension)) {
- throw new IllegalArgumentException("File must be a PDF");
+ throw ExceptionUtils.createPdfFileRequiredException();
}
}
@@ -62,25 +62,31 @@ public class PdfToCbzUtils {
int totalPages = document.getNumberOfPages();
for (int pageIndex = 0; pageIndex < totalPages; pageIndex++) {
+ final int currentPage = pageIndex;
try {
BufferedImage image =
- pdfRenderer.renderImageWithDPI(pageIndex, dpi, ImageType.RGB);
+ ExceptionUtils.handleOomRendering(
+ currentPage + 1,
+ dpi,
+ () ->
+ pdfRenderer.renderImageWithDPI(
+ currentPage, dpi, ImageType.RGB));
String imageFilename =
- String.format(Locale.ROOT, "page_%03d.png", pageIndex + 1);
-
+ String.format(Locale.ROOT, "page_%03d.png", currentPage + 1);
ZipEntry zipEntry = new ZipEntry(imageFilename);
zipOut.putNextEntry(zipEntry);
ImageIO.write(image, "PNG", zipOut);
zipOut.closeEntry();
+ } catch (ExceptionUtils.OutOfMemoryDpiException e) {
+ // Re-throw OOM exceptions without wrapping
+ throw e;
} catch (IOException e) {
- log.warn("Error processing page {}: {}", pageIndex + 1, e.getMessage());
- } catch (OutOfMemoryError e) {
- throw ExceptionUtils.createOutOfMemoryDpiException(pageIndex + 1, dpi, e);
- } catch (NegativeArraySizeException e) {
- throw ExceptionUtils.createOutOfMemoryDpiException(pageIndex + 1, dpi, e);
+ // Wrap other IOExceptions with context
+ throw ExceptionUtils.createFileProcessingException(
+ "CBZ creation for page " + (currentPage + 1), e);
}
}
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 39c776a61..d294da2ff 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
@@ -179,9 +179,20 @@ public class PdfUtils {
writer.prepareWriteSequence(null);
for (int i = 0; i < pageCount; ++i) {
+ final int pageIndex = i;
BufferedImage image;
try {
- image = pdfRenderer.renderImageWithDPI(i, DPI, colorType);
+ // Validate dimensions before rendering
+ ExceptionUtils.validateRenderingDimensions(
+ document.getPage(pageIndex), pageIndex + 1, DPI);
+
+ image =
+ ExceptionUtils.handleOomRendering(
+ pageIndex + 1,
+ DPI,
+ () ->
+ pdfRenderer.renderImageWithDPI(
+ pageIndex, DPI, colorType));
} catch (IllegalArgumentException e) {
if (e.getMessage() != null
&& e.getMessage()
@@ -195,10 +206,6 @@ 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);
}
@@ -222,6 +229,7 @@ public class PdfUtils {
HashMap pageSizes =
new HashMap<>();
for (int i = 0; i < pageCount; ++i) {
+ final int pageIndex = i;
PDPage page = document.getPage(i);
PDRectangle mediaBox = page.getMediaBox();
int rotation = page.getRotation();
@@ -232,7 +240,17 @@ public class PdfUtils {
if (dimension == null) {
// Render the image to get the dimensions
try {
- pdfSizeImage = pdfRenderer.renderImageWithDPI(i, DPI, colorType);
+ // Validate dimensions before rendering
+ ExceptionUtils.validateRenderingDimensions(
+ page, pageIndex + 1, DPI);
+
+ pdfSizeImage =
+ ExceptionUtils.handleOomRendering(
+ pageIndex + 1,
+ DPI,
+ () ->
+ pdfRenderer.renderImageWithDPI(
+ pageIndex, DPI, colorType));
} catch (IllegalArgumentException e) {
if (e.getMessage() != null
&& e.getMessage()
@@ -247,10 +265,6 @@ 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 =
@@ -276,11 +290,22 @@ public class PdfUtils {
boolean firstImageAlreadyRendered = pdfSizeImageIndex == 0;
for (int i = 0; i < pageCount; ++i) {
+ final int pageIndex = i;
if (firstImageAlreadyRendered && i == 0) {
pageImage = pdfSizeImage;
} else {
try {
- pageImage = pdfRenderer.renderImageWithDPI(i, DPI, colorType);
+ // Validate dimensions before rendering
+ ExceptionUtils.validateRenderingDimensions(
+ document.getPage(pageIndex), pageIndex + 1, DPI);
+
+ pageImage =
+ ExceptionUtils.handleOomRendering(
+ pageIndex + 1,
+ DPI,
+ () ->
+ pdfRenderer.renderImageWithDPI(
+ pageIndex, DPI, colorType));
} catch (IllegalArgumentException e) {
if (e.getMessage() != null
&& e.getMessage()
@@ -294,10 +319,6 @@ 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);
}
}
@@ -318,9 +339,20 @@ public class PdfUtils {
// Zip the images and return as byte array
try (ZipOutputStream zos = new ZipOutputStream(baos)) {
for (int i = 0; i < pageCount; ++i) {
+ final int pageIndex = i;
BufferedImage image;
try {
- image = pdfRenderer.renderImageWithDPI(i, DPI, colorType);
+ // Validate dimensions before rendering
+ ExceptionUtils.validateRenderingDimensions(
+ document.getPage(pageIndex), pageIndex + 1, DPI);
+
+ image =
+ ExceptionUtils.handleOomRendering(
+ pageIndex + 1,
+ DPI,
+ () ->
+ pdfRenderer.renderImageWithDPI(
+ pageIndex, DPI, colorType));
} catch (IllegalArgumentException e) {
if (e.getMessage() != null
&& e.getMessage().contains("Maximum size of image exceeded")) {
@@ -332,10 +364,6 @@ 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);
@@ -375,6 +403,7 @@ public class PdfUtils {
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
@@ -384,9 +413,16 @@ public class PdfUtils {
if (properties != null && properties.getSystem() != null) {
renderDpi = properties.getSystem().getMaxDPI();
}
+ final int dpi = renderDpi;
try {
- bim = pdfRenderer.renderImageWithDPI(page, renderDpi, ImageType.RGB);
+ 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")) {
@@ -395,13 +431,9 @@ public class PdfUtils {
"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.",
- page + 1);
+ pageIndex + 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/RegexPatternUtils.java b/app/common/src/main/java/stirling/software/common/util/RegexPatternUtils.java
index 8858c99bf..0aa4cfac0 100644
--- a/app/common/src/main/java/stirling/software/common/util/RegexPatternUtils.java
+++ b/app/common/src/main/java/stirling/software/common/util/RegexPatternUtils.java
@@ -301,6 +301,11 @@ public final class RegexPatternUtils {
return getPattern("\\+");
}
+ /** Pattern for splitting on pipe delimiter (used for hint lists in i18n messages) */
+ public Pattern getPipeDelimiterPattern() {
+ return getPattern("\\|");
+ }
+
/** Pattern for username validation */
public Pattern getUsernameValidationPattern() {
return getPattern("^[a-zA-Z0-9](?!.*[-@._+]{2,})[a-zA-Z0-9@._+-]{1,48}[a-zA-Z0-9]$");
@@ -518,6 +523,16 @@ public final class RegexPatternUtils {
return getPattern("^[a-zA-Z0-9]{2,4}$", Pattern.CASE_INSENSITIVE);
}
+ /** Pattern for splitting on line breaks (Unicode line separator) */
+ public Pattern getLineSeparatorPattern() {
+ return getPattern("\\R");
+ }
+
+ /** Pattern for removing leading asterisks and whitespace */
+ public Pattern getLeadingAsterisksWhitespacePattern() {
+ return getPattern("^[*\\s]+");
+ }
+
private record PatternKey(String regex, int flags) {
// Record automatically provides equals, hashCode, and toString
}
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 d220cfcfa..806515c63 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
@@ -56,16 +56,14 @@ public class InvertFullColorStrategy extends ReplaceAndInvertColorStrategy {
if (properties != null && properties.getSystem() != null) {
renderDpi = properties.getSystem().getMaxDPI();
}
+ final int dpi = renderDpi;
+ final int pageNum = page;
- 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);
- }
+ image =
+ ExceptionUtils.handleOomRendering(
+ pageNum + 1,
+ dpi,
+ () -> pdfRenderer.renderImageWithDPI(pageNum, dpi));
// Invert the colors
invertImageColors(image);
diff --git a/app/common/src/test/java/stirling/software/common/service/JobExecutorServiceTest.java b/app/common/src/test/java/stirling/software/common/service/JobExecutorServiceTest.java
index 3257c4573..7982b0991 100644
--- a/app/common/src/test/java/stirling/software/common/service/JobExecutorServiceTest.java
+++ b/app/common/src/test/java/stirling/software/common/service/JobExecutorServiceTest.java
@@ -1,6 +1,10 @@
package stirling.software.common.service;
-import static org.junit.jupiter.api.Assertions.*;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.anyString;
@@ -8,7 +12,6 @@ import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
-import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeoutException;
import java.util.function.Supplier;
@@ -61,7 +64,7 @@ class JobExecutorServiceTest {
}
@Test
- void shouldRunSyncJobSuccessfully() {
+ void shouldRunSyncJobSuccessfully() throws Exception {
// Given
Supplier