exception handling and exception improvements (#3858)

# Description of Changes

This pull request introduces several improvements to enhance error
handling, internationalization, and documentation in the codebase. The
key changes include the addition of `ExceptionUtils` and `I18nUtils`
utility classes for consistent exception handling and internationalized
messages, updates to documentation paths, and modifications to existing
methods to integrate the new utilities.

### Error Handling Enhancements:
* **Added `ExceptionUtils` utility class**: Provides standardized
methods for creating and handling exceptions with internationalized
error messages, including specific handling for PDF corruption,
encryption issues, and other file-related errors.
* **Integrated `ExceptionUtils` into `CustomPDFDocumentFactory`**:
Updated `loadFromFile` and `loadFromBytes` methods to log and handle
exceptions using `ExceptionUtils`, ensuring consistent error handling
across PDF operations.
[[1]](diffhunk://#diff-10208c1fc2e04631a8cf2a2a99b2a1160e532e75a7b840ad752f3b0130b89851R358-R363)
[[2]](diffhunk://#diff-10208c1fc2e04631a8cf2a2a99b2a1160e532e75a7b840ad752f3b0130b89851R375-R381)
* **Updated `FileToPdf` to use `ExceptionUtils`**: Replaced direct
exception throwing with `ExceptionUtils.createHtmlFileRequiredException`
for unsupported file formats.

### Internationalization Improvements:
* **Added `I18nUtils` utility class**: Centralized access to Spring's
`MessageSource` for retrieving localized messages, enabling consistent
internationalization across the application.

### Documentation Updates:
* **Updated documentation paths in `CONTRIBUTING.md` and `README.md`**:
Changed paths to reference the new `devGuide` folder for developer
documentation and translation guides.
[[1]](diffhunk://#diff-eca12c0a30e25b4b46522ebf89465a03ba72a03f540796c979137931d8f92055L28-R28)
[[2]](diffhunk://#diff-eca12c0a30e25b4b46522ebf89465a03ba72a03f540796c979137931d8f92055L40-R51)
[[3]](diffhunk://#diff-b335630551682c19a781afebcf4d07bf978fb1f8ac04c6bf87428ed5106870f5L171-L174)
---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/DeveloperGuide.md#6-testing)
for more details.

---------

Co-authored-by: a <a>
This commit is contained in:
Anthony Stirling
2025-07-02 16:51:45 +01:00
committed by GitHub
co-authored by a <a>
parent 5cd76b51d9
commit dc6823d7ba
45 changed files with 918 additions and 166 deletions
@@ -91,6 +91,20 @@ public class EndpointConfiguration {
return hasEnabledToolGroup;
}
// Rule 4: Single-dependency check - if no alternatives defined, check if endpoint belongs
// to any disabled tool groups
for (String group : endpointGroups.keySet()) {
if (isToolGroup(group)
&& disabledGroups.contains(group)
&& endpointGroups.get(group).contains(endpoint)) {
log.debug(
"isEndpointEnabled('{}') -> false (single tool group '{}' disabled, no alternatives)",
original,
group);
return false;
}
}
// Default: enabled if not explicitly disabled
boolean enabled = !Boolean.FALSE.equals(explicitStatus);
log.debug("isEndpointEnabled('{}') -> {} (default)", original, enabled);
@@ -140,11 +154,22 @@ public class EndpointConfiguration {
public void disableGroup(String group) {
if (disabledGroups.add(group)) {
log.debug("Disabling group: {}", group);
if (isToolGroup(group)) {
log.debug(
"Disabling tool group: {} (endpoints with alternatives remain available)",
group);
} else {
log.debug(
"Disabling functional group: {} (will disable all endpoints in group)",
group);
}
}
Set<String> endpoints = endpointGroups.get(group);
if (endpoints != null) {
endpoints.forEach(this::disableEndpoint);
// Only cascade to endpoints for *functional* groups
if (!isToolGroup(group)) {
Set<String> endpoints = endpointGroups.get(group);
if (endpoints != null) {
endpoints.forEach(this::disableEndpoint);
}
}
}
@@ -163,24 +188,44 @@ public class EndpointConfiguration {
}
public void logDisabledEndpointsSummary() {
List<String> disabledList =
endpointStatuses.entrySet().stream()
.filter(entry -> Boolean.FALSE.equals(entry.getValue()))
.map(Map.Entry::getKey)
// Get all unique endpoints across all groups
Set<String> allEndpoints =
endpointGroups.values().stream()
.flatMap(Set::stream)
.collect(java.util.stream.Collectors.toSet());
// Check which endpoints are actually disabled (functionally unavailable)
List<String> functionallyDisabledEndpoints =
allEndpoints.stream()
.filter(endpoint -> !isEndpointEnabled(endpoint))
.sorted()
.toList();
if (!disabledGroups.isEmpty()) {
// Separate tool groups from functional groups
List<String> disabledToolGroups =
disabledGroups.stream().filter(this::isToolGroup).sorted().toList();
List<String> disabledFunctionalGroups =
disabledGroups.stream().filter(group -> !isToolGroup(group)).sorted().toList();
if (!disabledToolGroups.isEmpty()) {
log.info(
"Disabled groups: {}",
String.join(", ", disabledGroups.stream().sorted().toList()));
"Disabled tool groups: {} (endpoints may have alternative implementations)",
String.join(", ", disabledToolGroups));
}
if (!disabledList.isEmpty()) {
if (!disabledFunctionalGroups.isEmpty()) {
log.info("Disabled functional groups: {}", String.join(", ", disabledFunctionalGroups));
}
if (!functionallyDisabledEndpoints.isEmpty()) {
log.info(
"Total disabled endpoints: {}. Disabled endpoints: {}",
disabledList.size(),
String.join(", ", disabledList));
functionallyDisabledEndpoints.size(),
String.join(", ", functionallyDisabledEndpoints));
} else if (!disabledToolGroups.isEmpty()) {
log.info(
"No endpoints disabled despite missing tools - fallback implementations available");
}
}
@@ -327,6 +372,7 @@ public class EndpointConfiguration {
addEndpointToGroup("Java", "remove-image-pdf");
addEndpointToGroup("Java", "pdf-to-markdown");
addEndpointToGroup("Java", "add-attachments");
addEndpointToGroup("Java", "compress-pdf");
// Javascript
addEndpointToGroup("Javascript", "pdf-organizer");
@@ -353,9 +399,24 @@ public class EndpointConfiguration {
addEndpointAlternative("repair", "Ghostscript");
addEndpointAlternative("compress-pdf", "qpdf");
addEndpointAlternative("compress-pdf", "Ghostscript");
addEndpointAlternative("compress-pdf", "Java");
addEndpointAlternative("ocr-pdf", "tesseract");
addEndpointAlternative("ocr-pdf", "OCRmyPDF");
// file-to-pdf has multiple implementations
addEndpointAlternative("file-to-pdf", "LibreOffice");
addEndpointAlternative("file-to-pdf", "Python");
addEndpointAlternative("file-to-pdf", "Unoconvert");
// pdf-to-html and pdf-to-markdown can use either LibreOffice or Pdftohtml
addEndpointAlternative("pdf-to-html", "LibreOffice");
addEndpointAlternative("pdf-to-html", "Pdftohtml");
addEndpointAlternative("pdf-to-markdown", "Pdftohtml");
// markdown-to-pdf can use either Weasyprint or Java
addEndpointAlternative("markdown-to-pdf", "Weasyprint");
addEndpointAlternative("markdown-to-pdf", "Java");
// Weasyprint dependent endpoints
addEndpointToGroup("Weasyprint", "html-to-pdf");
addEndpointToGroup("Weasyprint", "url-to-pdf");
@@ -35,7 +35,9 @@ import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.model.api.general.MergePdfsRequest;
import stirling.software.common.service.CustomPDFDocumentFactory;
import stirling.software.common.util.ExceptionUtils;
import stirling.software.common.util.GeneralUtils;
import stirling.software.common.util.PdfErrorUtils;
import stirling.software.common.util.WebResponseUtils;
@RestController
@@ -189,8 +191,17 @@ public class MergeController {
mergedTempFile = Files.createTempFile("merged-", ".pdf").toFile();
mergerUtility.setDestinationFileName(mergedTempFile.getAbsolutePath());
mergerUtility.mergeDocuments(
pdfDocumentFactory.getStreamCacheFunction(totalSize)); // Merge the documents
try {
mergerUtility.mergeDocuments(
pdfDocumentFactory.getStreamCacheFunction(
totalSize)); // Merge the documents
} catch (IOException e) {
ExceptionUtils.logException("PDF merge", e);
if (PdfErrorUtils.isCorruptedPdfError(e)) {
throw ExceptionUtils.createMultiplePdfCorruptedException(e);
}
throw e;
}
// Load the merged PDF document
mergedDocument = pdfDocumentFactory.load(mergedTempFile);
@@ -289,7 +289,7 @@ public class RearrangePagesPDFController {
+ "_rearranged.pdf");
} catch (IOException e) {
log.error("Failed rearranging documents", e);
return null;
throw e;
}
}
}
@@ -20,6 +20,7 @@ import lombok.RequiredArgsConstructor;
import stirling.software.SPDF.model.api.general.RotatePDFRequest;
import stirling.software.common.service.CustomPDFDocumentFactory;
import stirling.software.common.util.ExceptionUtils;
import stirling.software.common.util.WebResponseUtils;
@RestController
@@ -43,7 +44,8 @@ public class RotationController {
// Validate the angle is a multiple of 90
if (angle % 90 != 0) {
throw new IllegalArgumentException("Angle must be a multiple of 90");
throw ExceptionUtils.createIllegalArgumentException(
"error.angleNotMultipleOf90", "Angle must be a multiple of 90");
}
// Load the PDF document
@@ -27,6 +27,7 @@ import lombok.RequiredArgsConstructor;
import stirling.software.SPDF.model.api.general.ScalePagesRequest;
import stirling.software.common.service.CustomPDFDocumentFactory;
import stirling.software.common.util.ExceptionUtils;
import stirling.software.common.util.WebResponseUtils;
@RestController
@@ -120,9 +121,7 @@ public class ScalePagesController {
return sizeMap.get(targetPDRectangle);
}
throw new IllegalArgumentException(
"Invalid PDRectangle. It must be one of the following: A0, A1, A2, A3, A4, A5, A6,"
+ " LETTER, LEGAL, KEEP");
throw ExceptionUtils.createInvalidPageSizeException(targetPDRectangle);
}
private Map<String, PDRectangle> getSizeMap() {
@@ -84,7 +84,7 @@ public class SplitPDFController {
for (int i = previousPageNumber; i <= splitPoint; i++) {
PDPage page = document.getPage(i);
splitDocument.addPage(page);
log.info("Adding page {} to split document", i);
log.debug("Adding page {} to split document", i);
}
previousPageNumber = splitPoint + 1;
@@ -122,14 +122,14 @@ public class SplitPDFController {
zipOut.write(pdf);
zipOut.closeEntry();
log.info("Wrote split document {} to zip file", fileName);
log.debug("Wrote split document {} to zip file", fileName);
}
} catch (Exception e) {
log.error("Failed writing to zip", e);
throw e;
}
log.info("Successfully created zip file with split documents: {}", zipFile.toString());
log.debug("Successfully created zip file with split documents: {}", zipFile.toString());
byte[] data = Files.readAllBytes(zipFile);
Files.deleteIfExists(zipFile);
@@ -35,6 +35,7 @@ import stirling.software.SPDF.model.api.SplitPdfByChaptersRequest;
import stirling.software.common.model.PdfMetadata;
import stirling.software.common.service.CustomPDFDocumentFactory;
import stirling.software.common.service.PdfMetadataService;
import stirling.software.common.util.ExceptionUtils;
import stirling.software.common.util.WebResponseUtils;
@RestController
@@ -131,7 +132,8 @@ public class SplitPdfByChaptersController {
Integer bookmarkLevel =
request.getBookmarkLevel(); // levels start from 0 (top most bookmarks)
if (bookmarkLevel < 0) {
throw new IllegalArgumentException("Invalid bookmark level");
throw ExceptionUtils.createIllegalArgumentException(
"error.invalidArgument", "Invalid argument: {0}", "bookmark level");
}
sourceDocument = pdfDocumentFactory.load(file);
@@ -139,7 +141,8 @@ public class SplitPdfByChaptersController {
if (outline == null) {
log.warn("No outline found for {}", file.getOriginalFilename());
throw new IllegalArgumentException("No outline found");
throw ExceptionUtils.createIllegalArgumentException(
"error.pdfBookmarksNotFound", "No PDF bookmarks/outline found in document");
}
List<Bookmark> bookmarks = new ArrayList<>();
try {
@@ -252,7 +255,7 @@ public class SplitPdfByChaptersController {
zipOut.write(pdf);
zipOut.closeEntry();
log.info("Wrote split document {} to zip file", fileName);
log.debug("Wrote split document {} to zip file", fileName);
}
} catch (Exception e) {
log.error("Failed writing to zip", e);
@@ -280,7 +283,7 @@ public class SplitPdfByChaptersController {
i++) {
PDPage page = sourceDocument.getPage(i);
splitDocument.addPage(page);
log.info("Adding page {} to split document", i);
log.debug("Adding page {} to split document", i);
}
ByteArrayOutputStream baos = new ByteArrayOutputStream();
if (includeMetadata) {
@@ -26,6 +26,7 @@ import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.model.api.general.SplitPdfBySizeOrCountRequest;
import stirling.software.common.service.CustomPDFDocumentFactory;
import stirling.software.common.util.ExceptionUtils;
import stirling.software.common.util.GeneralUtils;
import stirling.software.common.util.WebResponseUtils;
@@ -96,8 +97,10 @@ public class SplitPdfBySizeController {
handleSplitByDocCount(sourceDocument, documentCount, zipOut, filename);
} else {
log.error("Invalid split type: {}", type);
throw new IllegalArgumentException(
"Invalid argument for split type: " + type);
throw ExceptionUtils.createIllegalArgumentException(
"error.invalidArgument",
"Invalid argument: {0}",
"split type: " + type);
}
log.debug("PDF splitting completed successfully");
@@ -276,7 +279,7 @@ public class SplitPdfBySizeController {
log.debug("Successfully created initial output document");
} catch (Exception e) {
log.error("Error creating initial output document", e);
throw new IOException("Failed to create initial output document", e);
throw ExceptionUtils.createFileProcessingException("split", e);
}
int fileIndex = 1;
@@ -295,7 +298,7 @@ public class SplitPdfBySizeController {
log.debug("Successfully added page {} to current document", pageIndex);
} catch (Exception e) {
log.error("Error adding page {} to current document", pageIndex, e);
throw new IOException("Failed to add page to document", e);
throw ExceptionUtils.createFileProcessingException("split", e);
}
currentPageCount++;
@@ -320,7 +323,7 @@ public class SplitPdfBySizeController {
log.debug("Successfully created new document");
} catch (Exception e) {
log.error("Error creating new document for next part", e);
throw new IOException("Failed to create new document", e);
throw ExceptionUtils.createFileProcessingException("split", e);
}
currentPageCount = 0;
@@ -329,7 +332,7 @@ public class SplitPdfBySizeController {
}
} catch (Exception e) {
log.error("Error iterating through pages", e);
throw new IOException("Failed to iterate through pages", e);
throw ExceptionUtils.createFileProcessingException("split", e);
}
// Add the last document if it contains any pages
@@ -351,7 +354,7 @@ public class SplitPdfBySizeController {
}
} catch (Exception e) {
log.error("Error checking or saving final document", e);
throw new IOException("Failed to process final document", e);
throw ExceptionUtils.createFileProcessingException("split", e);
} finally {
try {
log.debug("Closing final document");
@@ -390,7 +393,7 @@ public class SplitPdfBySizeController {
log.debug("Successfully created document {} of {}", i + 1, documentCount);
} catch (Exception e) {
log.error("Error creating document {} of {}", i + 1, documentCount, e);
throw new IOException("Failed to create document", e);
throw ExceptionUtils.createFileProcessingException("split", e);
}
int pagesToAdd = pagesPerDocument + (i < extraPages ? 1 : 0);
@@ -408,7 +411,7 @@ public class SplitPdfBySizeController {
currentPageIndex++;
} catch (Exception e) {
log.error("Error adding page {} to document {}", j + 1, i + 1, e);
throw new IOException("Failed to add page to document", e);
throw ExceptionUtils.createFileProcessingException("split", e);
}
}
@@ -437,7 +440,7 @@ public class SplitPdfBySizeController {
log.debug("Successfully saved document part {} ({} bytes)", index, outStream.size());
} catch (Exception e) {
log.error("Error saving document part {} to byte array", index, e);
throw new IOException("Failed to save document to byte array", e);
throw ExceptionUtils.createFileProcessingException("split", e);
}
try {
@@ -465,7 +468,7 @@ public class SplitPdfBySizeController {
log.debug("Successfully added document part {} to ZIP", index);
} catch (Exception e) {
log.error("Error adding document part {} to ZIP", index, e);
throw new IOException("Failed to add document to ZIP file", e);
throw ExceptionUtils.createFileProcessingException("split", e);
}
}
}
@@ -17,6 +17,7 @@ import stirling.software.common.configuration.RuntimePathConfig;
import stirling.software.common.model.ApplicationProperties;
import stirling.software.common.model.api.converters.HTMLToPdfRequest;
import stirling.software.common.service.CustomPDFDocumentFactory;
import stirling.software.common.util.ExceptionUtils;
import stirling.software.common.util.FileToPdf;
import stirling.software.common.util.TempFileManager;
import stirling.software.common.util.WebResponseUtils;
@@ -46,14 +47,14 @@ public class ConvertHtmlToPDF {
MultipartFile fileInput = request.getFileInput();
if (fileInput == null) {
throw new IllegalArgumentException(
"Please provide an HTML or ZIP file for conversion.");
throw ExceptionUtils.createHtmlFileRequiredException();
}
String originalFilename = Filenames.toSimpleFileName(fileInput.getOriginalFilename());
if (originalFilename == null
|| (!originalFilename.endsWith(".html") && !originalFilename.endsWith(".zip"))) {
throw new IllegalArgumentException("File must be either .html or .zip format.");
throw ExceptionUtils.createIllegalArgumentException(
"error.fileFormatRequired", "File must be in {0} format", ".html or .zip");
}
boolean disableSanitize =
@@ -34,6 +34,7 @@ import stirling.software.SPDF.model.api.converters.ConvertToImageRequest;
import stirling.software.SPDF.model.api.converters.ConvertToPdfRequest;
import stirling.software.common.service.CustomPDFDocumentFactory;
import stirling.software.common.util.CheckProgramInstall;
import stirling.software.common.util.ExceptionUtils;
import stirling.software.common.util.GeneralUtils;
import stirling.software.common.util.PdfUtils;
import stirling.software.common.util.ProcessExecutor;
@@ -104,7 +105,7 @@ public class ConvertImgPDFController {
log.error("resultant bytes for {} is null, error converting ", filename);
}
if ("webp".equalsIgnoreCase(imageFormat) && !CheckProgramInstall.isPythonAvailable()) {
throw new IOException("Python is not installed. Required for WebP conversion.");
throw ExceptionUtils.createPythonRequiredForWebpException();
} else if ("webp".equalsIgnoreCase(imageFormat)
&& CheckProgramInstall.isPythonAvailable()) {
// Write the output stream to a temp file
@@ -27,6 +27,7 @@ import stirling.software.common.configuration.RuntimePathConfig;
import stirling.software.common.model.ApplicationProperties;
import stirling.software.common.model.api.GeneralFile;
import stirling.software.common.service.CustomPDFDocumentFactory;
import stirling.software.common.util.ExceptionUtils;
import stirling.software.common.util.FileToPdf;
import stirling.software.common.util.TempFileManager;
import stirling.software.common.util.WebResponseUtils;
@@ -55,12 +56,14 @@ public class ConvertMarkdownToPdf {
MultipartFile fileInput = generalFile.getFileInput();
if (fileInput == null) {
throw new IllegalArgumentException("Please provide a Markdown file for conversion.");
throw ExceptionUtils.createIllegalArgumentException(
"error.fileFormatRequired", "File must be in {0} format", "Markdown");
}
String originalFilename = Filenames.toSimpleFileName(fileInput.getOriginalFilename());
if (originalFilename == null || !originalFilename.endsWith(".md")) {
throw new IllegalArgumentException("File must be in .md format.");
throw ExceptionUtils.createIllegalArgumentException(
"error.fileFormatRequired", "File must be in {0} format", ".md");
}
// Convert Markdown to HTML using CommonMark
@@ -67,6 +67,7 @@ import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.model.api.converters.PdfToPdfARequest;
import stirling.software.common.util.ExceptionUtils;
import stirling.software.common.util.ProcessExecutor;
import stirling.software.common.util.ProcessExecutor.ProcessExecutorResult;
import stirling.software.common.util.WebResponseUtils;
@@ -90,7 +91,7 @@ public class ConvertPDFToPDFA {
// Validate input file type
if (!"application/pdf".equals(inputFile.getContentType())) {
log.error("Invalid input file type: {}", inputFile.getContentType());
throw new IllegalArgumentException("Input file must be a PDF");
throw ExceptionUtils.createPdfFileRequiredException();
}
// Get the original filename without extension
@@ -241,15 +242,13 @@ public class ConvertPDFToPDFA {
if (returnCode.getRc() != 0) {
log.error("PDF/A conversion failed with return code: {}", returnCode.getRc());
throw new RuntimeException("PDF/A conversion failed");
throw ExceptionUtils.createPdfaConversionFailedException();
}
// Get the output file
File[] outputFiles = tempOutputDir.toFile().listFiles();
if (outputFiles == null || outputFiles.length != 1) {
throw new RuntimeException(
"Expected one output PDF, found "
+ (outputFiles == null ? "none" : outputFiles.length));
throw ExceptionUtils.createPdfaConversionFailedException();
}
return outputFiles[0].toPath();
}
@@ -23,6 +23,7 @@ import stirling.software.SPDF.model.api.converters.UrlToPdfRequest;
import stirling.software.common.configuration.RuntimePathConfig;
import stirling.software.common.model.ApplicationProperties;
import stirling.software.common.service.CustomPDFDocumentFactory;
import stirling.software.common.util.ExceptionUtils;
import stirling.software.common.util.GeneralUtils;
import stirling.software.common.util.ProcessExecutor;
import stirling.software.common.util.ProcessExecutor.ProcessExecutorResult;
@@ -50,16 +51,19 @@ public class ConvertWebsiteToPDF {
String URL = request.getUrlInput();
if (!applicationProperties.getSystem().getEnableUrlToPDF()) {
throw new IllegalArgumentException("This endpoint has been disabled by the admin.");
throw ExceptionUtils.createIllegalArgumentException(
"error.endpointDisabled", "This endpoint has been disabled by the admin");
}
// Validate the URL format
if (!URL.matches("^https?://.*") || !GeneralUtils.isValidURL(URL)) {
throw new IllegalArgumentException("Invalid URL format provided.");
throw ExceptionUtils.createInvalidArgumentException(
"URL", "provided format is invalid");
}
// validate the URL is reachable
if (!GeneralUtils.isURLReachable(URL)) {
throw new IllegalArgumentException("URL is not reachable, please provide a valid URL.");
throw ExceptionUtils.createIllegalArgumentException(
"error.urlNotReachable", "URL is not reachable, please provide a valid URL");
}
Path tempOutputFile = null;
@@ -25,6 +25,7 @@ import stirling.software.SPDF.model.api.filter.FileSizeRequest;
import stirling.software.SPDF.model.api.filter.PageRotationRequest;
import stirling.software.SPDF.model.api.filter.PageSizeRequest;
import stirling.software.common.service.CustomPDFDocumentFactory;
import stirling.software.common.util.ExceptionUtils;
import stirling.software.common.util.PdfUtils;
import stirling.software.common.util.WebResponseUtils;
@@ -96,7 +97,7 @@ public class FilterController {
valid = actualPageCount < pageCount;
break;
default:
throw new IllegalArgumentException("Invalid comparator: " + comparator);
throw ExceptionUtils.createInvalidArgumentException("comparator", comparator);
}
if (valid) return WebResponseUtils.multiPartFileToWebResponse(inputFile);
@@ -139,7 +140,7 @@ public class FilterController {
valid = actualArea < standardArea;
break;
default:
throw new IllegalArgumentException("Invalid comparator: " + comparator);
throw ExceptionUtils.createInvalidArgumentException("comparator", comparator);
}
if (valid) return WebResponseUtils.multiPartFileToWebResponse(inputFile);
@@ -172,7 +173,7 @@ public class FilterController {
valid = actualFileSize < fileSize;
break;
default:
throw new IllegalArgumentException("Invalid comparator: " + comparator);
throw ExceptionUtils.createInvalidArgumentException("comparator", comparator);
}
if (valid) return WebResponseUtils.multiPartFileToWebResponse(inputFile);
@@ -208,7 +209,7 @@ public class FilterController {
valid = actualRotation < rotation;
break;
default:
throw new IllegalArgumentException("Invalid comparator: " + comparator);
throw ExceptionUtils.createInvalidArgumentException("comparator", comparator);
}
if (valid) return WebResponseUtils.multiPartFileToWebResponse(inputFile);
@@ -53,6 +53,7 @@ import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.config.EndpointConfiguration;
import stirling.software.SPDF.model.api.misc.OptimizePdfRequest;
import stirling.software.common.service.CustomPDFDocumentFactory;
import stirling.software.common.util.ExceptionUtils;
import stirling.software.common.util.GeneralUtils;
import stirling.software.common.util.ProcessExecutor;
import stirling.software.common.util.ProcessExecutor.ProcessExecutorResult;
@@ -600,7 +601,7 @@ public class CompressController {
if (bytesRead > 0) {
byte[] dataToHash =
bytesRead == buffer.length ? buffer : Arrays.copyOf(buffer, bytesRead);
return bytesToHexString(generatMD5(dataToHash));
return bytesToHexString(generateMD5(dataToHash));
}
return "empty-stream";
}
@@ -618,12 +619,12 @@ public class CompressController {
return sb.toString();
}
private byte[] generatMD5(byte[] data) throws IOException {
private byte[] generateMD5(byte[] data) throws IOException {
try {
MessageDigest md = MessageDigest.getInstance("MD5");
return md.digest(data); // Get the MD5 hash of the image bytes
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("MD5 algorithm not available", e);
throw ExceptionUtils.createMd5AlgorithmException(e);
}
}
@@ -33,6 +33,7 @@ import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.model.api.misc.ExtractImageScansRequest;
import stirling.software.common.service.CustomPDFDocumentFactory;
import stirling.software.common.util.CheckProgramInstall;
import stirling.software.common.util.ExceptionUtils;
import stirling.software.common.util.ProcessExecutor;
import stirling.software.common.util.ProcessExecutor.ProcessExecutorResult;
import stirling.software.common.util.WebResponseUtils;
@@ -72,7 +73,8 @@ public class ExtractImageScansController {
List<Path> tempDirs = new ArrayList<>();
if (!CheckProgramInstall.isPythonAvailable()) {
throw new IOException("Python is not installed.");
throw ExceptionUtils.createIOException(
"error.toolNotInstalled", "{0} is not installed", null, "Python");
}
String pythonVersion = CheckProgramInstall.getAvailablePythonCommand();
@@ -41,6 +41,7 @@ import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.model.api.PDFExtractImagesRequest;
import stirling.software.common.service.CustomPDFDocumentFactory;
import stirling.software.common.util.ExceptionUtils;
import stirling.software.common.util.ImageProcessingUtils;
import stirling.software.common.util.WebResponseUtils;
@@ -180,34 +181,39 @@ public class ExtractImagesController {
}
int count = 1;
for (COSName name : page.getResources().getXObjectNames()) {
if (page.getResources().isImageXObject(name)) {
PDImageXObject image = (PDImageXObject) page.getResources().getXObject(name);
if (!allowDuplicates) {
byte[] data = ImageProcessingUtils.getImageData(image.getImage());
byte[] imageHash = md.digest(data);
synchronized (processedImages) {
if (processedImages.stream()
.anyMatch(hash -> Arrays.equals(hash, imageHash))) {
continue; // Skip already processed images
try {
if (page.getResources().isImageXObject(name)) {
PDImageXObject image = (PDImageXObject) page.getResources().getXObject(name);
if (!allowDuplicates) {
byte[] data = ImageProcessingUtils.getImageData(image.getImage());
byte[] imageHash = md.digest(data);
synchronized (processedImages) {
if (processedImages.stream()
.anyMatch(hash -> Arrays.equals(hash, imageHash))) {
continue; // Skip already processed images
}
processedImages.add(imageHash);
}
processedImages.add(imageHash);
}
RenderedImage renderedImage = image.getImage();
// Convert to standard RGB colorspace if needed
BufferedImage bufferedImage = convertToRGB(renderedImage, format);
// Write image to zip file
String imageName = filename + "_page_" + pageNum + "_" + count++ + "." + format;
synchronized (zos) {
zos.putNextEntry(new ZipEntry(imageName));
ByteArrayOutputStream imageBaos = new ByteArrayOutputStream();
ImageIO.write(bufferedImage, format, imageBaos);
zos.write(imageBaos.toByteArray());
zos.closeEntry();
}
}
RenderedImage renderedImage = image.getImage();
// Convert to standard RGB colorspace if needed
BufferedImage bufferedImage = convertToRGB(renderedImage, format);
// Write image to zip file
String imageName = filename + "_page_" + pageNum + "_" + count++ + "." + format;
synchronized (zos) {
zos.putNextEntry(new ZipEntry(imageName));
ByteArrayOutputStream imageBaos = new ByteArrayOutputStream();
ImageIO.write(bufferedImage, format, imageBaos);
zos.write(imageBaos.toByteArray());
zos.closeEntry();
}
} catch (IOException e) {
ExceptionUtils.logException("image extraction", e);
throw ExceptionUtils.handlePdfException(e, "during image extraction");
}
}
}
@@ -34,6 +34,7 @@ import stirling.software.SPDF.config.EndpointConfiguration;
import stirling.software.SPDF.model.api.misc.ProcessPdfWithOcrRequest;
import stirling.software.common.model.ApplicationProperties;
import stirling.software.common.service.CustomPDFDocumentFactory;
import stirling.software.common.util.ExceptionUtils;
import stirling.software.common.util.ProcessExecutor;
import stirling.software.common.util.ProcessExecutor.ProcessExecutorResult;
import stirling.software.common.util.TempDirectory;
@@ -96,7 +97,7 @@ public class OCRController {
Boolean removeImagesAfter = request.isRemoveImagesAfter();
if (selectedLanguages == null || selectedLanguages.isEmpty()) {
throw new IOException("Please select at least one language.");
throw ExceptionUtils.createOcrLanguageRequiredException();
}
if (!"hocr".equals(ocrRenderType) && !"sandwich".equals(ocrRenderType)) {
@@ -111,7 +112,7 @@ public class OCRController {
selectedLanguages.stream().filter(availableLanguages::contains).toList();
if (selectedLanguages.isEmpty()) {
throw new IOException("None of the selected languages are valid.");
throw ExceptionUtils.createOcrInvalidLanguagesException();
}
// Use try-with-resources for proper temp file management
@@ -152,7 +153,7 @@ public class OCRController {
tempOutputFile.getPath());
log.info("Tesseract processing completed successfully");
} else {
throw new IOException("No OCR tools are available");
throw ExceptionUtils.createOcrToolsUnavailableException();
}
// Read the processed PDF file
@@ -379,8 +380,12 @@ public class OCRController {
.runCommandWithOutputHandling(command);
if (result.getRc() != 0) {
throw new RuntimeException(
"Tesseract failed with exit code: " + result.getRc());
throw ExceptionUtils.createRuntimeException(
"error.commandFailed",
"{0} command failed with exit code: {1}",
null,
"Tesseract",
result.getRc());
}
// Add OCR'd PDF to merger
@@ -73,6 +73,7 @@ import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.model.api.security.SignPDFWithCertRequest;
import stirling.software.common.service.CustomPDFDocumentFactory;
import stirling.software.common.util.ExceptionUtils;
import stirling.software.common.util.WebResponseUtils;
@RestController
@@ -166,7 +167,10 @@ public class CertSignController {
Boolean showLogo = request.getShowLogo();
if (certType == null) {
throw new IllegalArgumentException("Cert type must be provided");
throw ExceptionUtils.createIllegalArgumentException(
"error.optionsNotSpecified",
"{0} options are not specified",
"certificate type");
}
KeyStore ks = null;
@@ -189,7 +193,10 @@ public class CertSignController {
ks.load(jksfile.getInputStream(), password.toCharArray());
break;
default:
throw new IllegalArgumentException("Invalid cert type: " + certType);
throw ExceptionUtils.createIllegalArgumentException(
"error.invalidArgument",
"Invalid argument: {0}",
"certificate type: " + certType);
}
CreateSignature createSignature = new CreateSignature(ks, password.toCharArray());
@@ -149,22 +149,38 @@ public class GetInfoOnPDF {
try {
PDMetadata pdMetadata = document.getDocumentCatalog().getMetadata();
if (pdMetadata != null) {
COSInputStream metaStream = pdMetadata.createInputStream();
DomXmpParser domXmpParser = new DomXmpParser();
XMPMetadata xmpMeta = domXmpParser.parse(metaStream);
try (COSInputStream metaStream = pdMetadata.createInputStream()) {
// First try to read raw metadata as string to check for standard keywords
byte[] metadataBytes = metaStream.readAllBytes();
String rawMetadata = new String(metadataBytes, StandardCharsets.UTF_8);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
new XmpSerializer().serialize(xmpMeta, baos, true);
String xmpString = new String(baos.toByteArray(), StandardCharsets.UTF_8);
if (rawMetadata.contains(standardKeyword)) {
return true;
}
}
if (xmpString.contains(standardKeyword)) {
return true;
// If raw check doesn't find it, try parsing with XMP parser
try (COSInputStream metaStream = pdMetadata.createInputStream()) {
try {
DomXmpParser domXmpParser = new DomXmpParser();
XMPMetadata xmpMeta = domXmpParser.parse(metaStream);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
new XmpSerializer().serialize(xmpMeta, baos, true);
String xmpString = new String(baos.toByteArray(), StandardCharsets.UTF_8);
if (xmpString.contains(standardKeyword)) {
return true;
}
} catch (XmpParsingException e) {
// XMP parsing failed, but we already checked raw metadata above
log.debug(
"XMP parsing failed for standard check, but raw metadata was already checked: {}",
e.getMessage());
}
}
}
} catch (
Exception
e) { // Catching general exception for brevity, ideally you'd catch specific
// exceptions.
} catch (Exception e) {
log.error("exception", e);
}
@@ -391,14 +407,22 @@ public class GetInfoOnPDF {
if (pdMetadata != null) {
try {
COSInputStream is = pdMetadata.createInputStream();
DomXmpParser domXmpParser = new DomXmpParser();
XMPMetadata xmpMeta = domXmpParser.parse(is);
try (COSInputStream is = pdMetadata.createInputStream()) {
DomXmpParser domXmpParser = new DomXmpParser();
XMPMetadata xmpMeta = domXmpParser.parse(is);
ByteArrayOutputStream os = new ByteArrayOutputStream();
new XmpSerializer().serialize(xmpMeta, os, true);
xmpString = new String(os.toByteArray(), StandardCharsets.UTF_8);
} catch (XmpParsingException | IOException e) {
ByteArrayOutputStream os = new ByteArrayOutputStream();
new XmpSerializer().serialize(xmpMeta, os, true);
xmpString = new String(os.toByteArray(), StandardCharsets.UTF_8);
}
} catch (XmpParsingException e) {
// XMP parsing failed, try to read raw metadata instead
log.debug("XMP parsing failed, reading raw metadata: {}", e.getMessage());
try (COSInputStream is = pdMetadata.createInputStream()) {
byte[] metadataBytes = is.readAllBytes();
xmpString = new String(metadataBytes, StandardCharsets.UTF_8);
}
} catch (IOException e) {
log.error("exception", e);
}
}
@@ -21,6 +21,7 @@ import lombok.RequiredArgsConstructor;
import stirling.software.SPDF.model.api.security.AddPasswordRequest;
import stirling.software.SPDF.model.api.security.PDFPasswordRequest;
import stirling.software.common.service.CustomPDFDocumentFactory;
import stirling.software.common.util.ExceptionUtils;
import stirling.software.common.util.WebResponseUtils;
@RestController
@@ -42,12 +43,19 @@ public class PasswordController {
MultipartFile fileInput = request.getFileInput();
String password = request.getPassword();
PDDocument document = pdfDocumentFactory.load(fileInput, password);
document.setAllSecurityToBeRemoved(true);
return WebResponseUtils.pdfDocToWebResponse(
document,
Filenames.toSimpleFileName(fileInput.getOriginalFilename())
.replaceFirst("[.][^.]+$", "")
+ "_password_removed.pdf");
try {
document.setAllSecurityToBeRemoved(true);
return WebResponseUtils.pdfDocToWebResponse(
document,
Filenames.toSimpleFileName(fileInput.getOriginalFilename())
.replaceFirst("[.][^.]+$", "")
+ "_password_removed.pdf");
} catch (IOException e) {
document.close();
ExceptionUtils.logException("password removal", e);
throw ExceptionUtils.handlePdfException(e);
}
}
@PostMapping(consumes = "multipart/form-data", value = "/add-password")
@@ -41,6 +41,7 @@ import stirling.software.SPDF.model.api.security.SignatureValidationRequest;
import stirling.software.SPDF.model.api.security.SignatureValidationResult;
import stirling.software.SPDF.service.CertificateValidationService;
import stirling.software.common.service.CustomPDFDocumentFactory;
import stirling.software.common.util.ExceptionUtils;
@RestController
@RequestMapping("/api/v1/security")
@@ -82,7 +83,12 @@ public class ValidateSignatureController {
CertificateFactory cf = CertificateFactory.getInstance("X.509");
customCert = (X509Certificate) cf.generateCertificate(certStream);
} catch (CertificateException e) {
throw new RuntimeException("Invalid certificate file: " + e.getMessage());
throw ExceptionUtils.createRuntimeException(
"error.invalidFormat",
"Invalid {0} format: {1}",
e,
"certificate file",
e.getMessage());
}
}
@@ -29,6 +29,7 @@ import stirling.software.SPDF.service.SignatureService;
import stirling.software.common.configuration.InstallationPathConfig;
import stirling.software.common.configuration.RuntimePathConfig;
import stirling.software.common.service.UserServiceInterface;
import stirling.software.common.util.ExceptionUtils;
import stirling.software.common.util.GeneralUtils;
@Controller
@@ -263,13 +264,17 @@ public class GeneralWebController {
}
return null;
} catch (Exception e) {
throw new RuntimeException("Error processing filename", e);
throw ExceptionUtils.createRuntimeException(
"error.fontLoadingFailed",
"Error processing font file",
e);
}
})
.filter(Objects::nonNull)
.toList();
} catch (Exception e) {
throw new RuntimeException("Failed to read font directory from " + locationPattern, e);
throw ExceptionUtils.createRuntimeException(
"error.fontDirectoryReadFailed", "Failed to read font directory", e);
}
}
@@ -170,6 +170,67 @@ sizes.medium=Medium
sizes.large=Large
sizes.x-large=X-Large
error.pdfPassword=The PDF Document is passworded and either the password was not provided or was incorrect
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.
error.pdfCorruptedMultiple=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.
error.pdfCorruptedDuring=Error {0}: 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.
# Frontend corruption error messages
error.pdfInvalid=The PDF file "{0}" appears to be corrupted or has an invalid structure. Please try using the 'Repair PDF' feature to fix the file before proceeding.
error.tryRepair=Try using the Repair PDF feature to fix corrupted files.
# Additional error messages
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.
error.fileProcessing=An error occurred while processing the file during {0} operation: {1}
# Generic error message templates
error.toolNotInstalled={0} is not installed
error.toolRequired={0} is required for {1}
error.conversionFailed={0} conversion failed
error.commandFailed={0} command failed
error.algorithmNotAvailable={0} algorithm not available
error.optionsNotSpecified={0} options are not specified
error.fileFormatRequired=File must be in {0} format
error.invalidFormat=Invalid {0} format: {1}
error.endpointDisabled=This endpoint has been disabled by the admin
error.urlNotReachable=URL is not reachable, please provide a valid URL
# DPI and image rendering messages - used by frontend for dynamic translation
# Backend sends: [TRANSLATE:messageKey:arg1,arg2] English message
# Frontend parses this and replaces with localized versions using these keys
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.
error.pageTooBigForDpi=PDF page {0} is too large to render at {1} DPI. Please try a lower DPI value (recommended: 150 or less).
error.pageTooBigExceedsArray=PDF page {0} is too large to render at {1} DPI. The resulting image would exceed Java's maximum array size. Please try a lower DPI value (recommended: 150 or less).
error.pageTooBigFor300Dpi=PDF page {0} is too large to render at 300 DPI. The resulting image would exceed Java's maximum array size. Please use a lower DPI value for PDF-to-image conversion.
# URL and website conversion messages
# System requirements messages
# Authentication and security messages
error.apiKeyInvalid=API key is not valid.
error.userNotFound=User not found.
error.passwordRequired=Password must not be null.
error.accountLocked=Your account has been locked due to too many failed login attempts.
error.invalidEmail=Invalid email addresses provided.
error.emailAttachmentRequired=An attachment is required to send the email.
error.signatureNotFound=Signature file not found.
# File processing messages
error.fileNotFound=File not found with ID: {0}
# Database and configuration messages
error.noBackupScripts=No backup scripts were found.
error.unsupportedProvider={0} is not currently supported.
error.pathTraversalDetected=Path traversal detected for security reasons.
# Validation messages
error.invalidArgument=Invalid argument: {0}
error.argumentRequired={0} must not be null
error.operationFailed=Operation failed: {0}
error.angleNotMultipleOf90=Angle must be a multiple of 90
error.pdfBookmarksNotFound=No PDF bookmarks/outline found in document
error.fontLoadingFailed=Error processing font file
error.fontDirectoryReadFailed=Failed to read font directory
delete=Delete
username=Username
password=Password
@@ -125,6 +125,16 @@ export class DecryptFile {
} else if (error.code === pdfjsLib.PasswordResponses.INCORRECT_PASSWORD) {
return {isEncrypted: true, requiresPassword: false};
}
} else if (error.name === 'InvalidPDFException' ||
(error.message && error.message.includes('Invalid PDF structure'))) {
// Handle corrupted PDF files
console.error('Corrupted PDF detected:', error);
this.showErrorBanner(
`${window.stirlingPDF.pdfCorruptedMessage.replace('{0}', file.name)}`,
error.stack || '',
`${window.stirlingPDF.tryRepairMessage}`
);
throw new Error('PDF file is corrupted.');
}
console.error('Error checking encryption:', error);
@@ -193,13 +193,13 @@
if (error.name === 'PasswordException' && error.code === 1) {
console.log(`PDF requires password: ${file.name}`, error);
console.log(`Attempting to remove password from PDF: ${file.name} with password.`);
const password = prompt(`${window.translations.decrypt.passwordPrompt}`);
const password = prompt(`${window.decrypt.passwordPrompt}`);
if (!password) {
console.error(`No password provided for encrypted PDF: ${file.name}`);
showErrorBanner(
`${window.translations.decrypt.noPassword.replace('{0}', file.name)}`,
`${window.translations.decrypt.unexpectedError}`
`${window.decrypt.noPassword.replace('{0}', file.name)}`,
`${window.decrypt.unexpectedError}`
);
throw error;
}
@@ -233,11 +233,20 @@
} catch (decryptError) {
console.error(`Failed to decrypt PDF: ${file.name}`, decryptError);
showErrorBanner(
`${window.translations.invalidPasswordHeader.replace('{0}', file.name)}`,
`${window.translations.invalidPassword}`
`${window.decrypt.invalidPasswordHeader.replace('{0}', file.name)}`,
`${window.decrypt.invalidPassword}`
);
throw decryptError;
}
} else if (error.name === 'InvalidPDFException' ||
(error.message && error.message.includes('Invalid PDF structure'))) {
// Handle corrupted PDF files
console.log(`Corrupted PDF detected: ${file.name}`, error);
showErrorBanner(
`${window.stirlingPDF.pdfCorruptedMessage.replace('{0}', file.name)}`,
`${window.stirlingPDF.tryRepairMessage}`
);
throw error;
} else {
console.log(`Error loading PDF: ${file.name}`, error);
throw error;
@@ -235,6 +235,13 @@ function setupFileInput(chooser) {
} catch (error) {
console.error(`Error decrypting file: ${file.name}`, error);
// Check if this is a PDF corruption error
if (error.message && error.message.includes('PDF file is corrupted')) {
// The error banner is already shown by DecryptFiles.js, just continue with the file
console.warn(`Continuing with corrupted PDF file: ${file.name}`);
}
if (!file.uniqueId) file.uniqueId = UUID.uuidv4();
return file;
}
@@ -250,6 +250,8 @@
window.stirlingPDF.uploadLimit = /*[[${@uploadLimitService.getUploadLimit()}]]*/ 0;
window.stirlingPDF.uploadLimitExceededSingular = /*[[#{uploadLimitExceededSingular}]]*/ 'is too large. Maximum allowed size is';
window.stirlingPDF.uploadLimitExceededPlural = /*[[#{uploadLimitExceededPlural}]]*/ 'are too large. Maximum allowed size is';
window.stirlingPDF.pdfCorruptedMessage = /*[[#{error.pdfInvalid}]]*/ 'The PDF file "{0}" appears to be corrupted or has an invalid structure. Please try using the \'Repair PDF\' feature to fix the file before proceeding.';
window.stirlingPDF.tryRepairMessage = /*[[#{error.tryRepair}]]*/ 'Try using the Repair PDF feature to fix corrupted files.';
})();
</script>
<script type="module" th:src="@{'/pdfjs-legacy/pdf.mjs'}"></script>
@@ -37,10 +37,10 @@
</div>
<div class="recent-features">
<div class="newfeature"
th:insert="~{fragments/navbarEntryCustom :: navbarEntry('redact', '/images/redact-manual.svg#icon-redact-manual', 'home.redact.title', 'home.redact.desc', 'redact.tags', 'security')}">
th:insert="~{fragments/navbarEntry :: navbarEntry('add-attachments', 'attachment', 'home.attachments.title', 'home.attachments.desc', 'attachments.tags', 'other')}">
</div>
<div class="newfeature"
th:insert="~{fragments/navbarEntry :: navbarEntry ('multi-tool', 'construction', 'home.multiTool.title', 'home.multiTool.desc', 'multiTool.tags', 'organize')}">
th:insert="~{fragments/navbarEntry :: navbarEntry('fake-scan', 'scanner', 'fakeScan.title', 'fakeScan.description', 'fakeScan.tags', 'advance')}">
</div>
<div class="newfeature"
th:insert="~{fragments/navbarEntry :: navbarEntry('compress-pdf', 'zoom_in_map', 'home.compressPdfs.title', 'home.compressPdfs.desc', 'compressPDFs.tags', 'advance')}">
@@ -160,6 +160,11 @@
success: '[[#{decrypt.success}]]',
}
window.corrupt = {
pdfInvalid: '[[#{error.pdfInvalid}]]',
tryRepair: '[[#{error.tryRepair}]]',
}
const csvInput = document.getElementById("csv-input");
csvInput.addEventListener("keydown", function (event) {
if (event.key === "Enter") {
@@ -48,7 +48,7 @@ public class ConvertWebsiteToPdfTest {
convertWebsiteToPDF.urlToPdf(request);
});
// Assert
assertEquals("Invalid URL format provided.", thrown.getMessage());
assertEquals("Invalid URL format: provided format is invalid", thrown.getMessage());
}
@Test
@@ -66,6 +66,6 @@ public class ConvertWebsiteToPdfTest {
convertWebsiteToPDF.urlToPdf(request);
});
// Assert
assertEquals("URL is not reachable, please provide a valid URL.", thrown.getMessage());
assertEquals("URL is not reachable, please provide a valid URL", thrown.getMessage());
}
}