refactor(resource): improve resource management and exception safety across controllers and utilities (#5350)

TLDR:
- Use try-with-resources for InputStreams, PDDocument, TempFile, and
ByteArrayOutputStream to ensure proper cleanup
- Refactor PDF manipulation methods to handle exceptions and resource
closure more robustly
- Simplify session sorting logic in SessionPersistentRegistry
- Add missing resource cleanup in MergeController and
SplitPdfBySectionsController
- Update PrintFileController to close streams and documents safely
- Add unit test for SplitPdfBySizeController

# Description of Changes

This pull request introduces several improvements to resource management
and error handling throughout the codebase, especially for temporary
files and PDF document objects. The changes help prevent resource leaks
by ensuring files and documents are properly closed or deleted in the
event of errors or after use. Notable updates include the use of
try-with-resources, custom AutoCloseable wrappers, and enhanced error
handling logic.

**Resource Management and Error Handling Improvements**

* Added try-catch blocks to delete temporary files if an exception
occurs during file conversion in `GeneralUtils.java`, ensuring no
orphaned temp files are left behind
* Introduced the `TempFile` AutoCloseable helper class in
`InvertFullColorStrategy.java`, and refactored the PDF processing logic
to use try-with-resources for both temporary files and `PDDocument`
objects, ensuring proper cleanup
* Improved error handling in `MergeController.java` by ensuring that
partially created merged documents are closed if an error occurs during
the merge process
* Enhanced the splitting logic in `SplitPdfBySectionsController.java` to
close any partially created `PDDocument` objects if an exception is
thrown, preventing resource leaks

**Refactoring for Safer Document Handling**

* Refactored `handleSplitBySize` in `SplitPdfBySizeController.java` to
use a custom `DocHolder` AutoCloseable wrapper for managing the
lifecycle of `PDDocument` objects, and updated all related logic to use
this wrapper, improving code safety and clarity
* Updated `handleSplitByPageCount` in `SplitPdfBySizeController.java` to
ensure `PDDocument` objects are set to null after being saved and
closed, reducing the risk of operating on closed resources

<!--
Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)
-->

---

## 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/devGuide/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/devGuide/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/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### 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/devGuide/DeveloperGuide.md#6-testing)
for more details.

---------

Signed-off-by: Balázs Szücs <[email protected]>
This commit is contained in:
Balázs Szücs
2025-12-31 18:09:29 +00:00
committed by GitHub
parent 3fd0398648
commit 02f9785212
19 changed files with 856 additions and 605 deletions
@@ -91,6 +91,14 @@ public class GeneralUtils {
while ((bytesRead = inputStream.read(buffer)) != -1) { while ((bytesRead = inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, bytesRead); outputStream.write(buffer, 0, bytesRead);
} }
} catch (IOException e) {
if (tempFile.exists()) {
try {
Files.delete(tempFile.toPath());
} catch (IOException ignored) {
}
}
throw e;
} }
return tempFile; return tempFile;
} }
@@ -499,6 +507,12 @@ public class GeneralUtils {
while ((bytesRead = in.read(buffer)) != -1) { while ((bytesRead = in.read(buffer)) != -1) {
out.write(buffer, 0, bytesRead); out.write(buffer, 0, bytesRead);
} }
} catch (IOException e) {
try {
Files.deleteIfExists(tempFile);
} catch (IOException ignored) {
}
throw e;
} }
return tempFile.toFile(); return tempFile.toFile();
} }
@@ -63,16 +63,17 @@ public class ImageProcessingUtils {
} else { } else {
int width = image.getWidth(); int width = image.getWidth();
int height = image.getHeight(); int height = image.getHeight();
int[] pixels = new int[width * height];
image.getRGB(0, 0, width, height, pixels, 0, width);
byte[] data = new byte[width * height * 3]; byte[] data = new byte[width * height * 3];
int index = 0; int index = 0;
for (int y = 0; y < height; y++) { for (int rgb : pixels) {
for (int x = 0; x < width; x++) {
int rgb = image.getRGB(x, y);
data[index++] = (byte) ((rgb >> 16) & 0xFF); // Red data[index++] = (byte) ((rgb >> 16) & 0xFF); // Red
data[index++] = (byte) ((rgb >> 8) & 0xFF); // Green data[index++] = (byte) ((rgb >> 8) & 0xFF); // Green
data[index++] = (byte) (rgb & 0xFF); // Blue data[index++] = (byte) (rgb & 0xFF); // Blue
} }
}
return data; return data;
} }
} }
@@ -32,18 +32,15 @@ public class InvertFullColorStrategy extends ReplaceAndInvertColorStrategy {
@Override @Override
public InputStreamResource replace() throws IOException { public InputStreamResource replace() throws IOException {
try (TempFile tempFile =
File file = null; new TempFile(
try { Files.createTempFile("temp", getFileInput().getOriginalFilename())
// Create a temporary file, with the original filename from the multipart file .toFile())) {
file = Files.createTempFile("temp", getFileInput().getOriginalFilename()).toFile();
// Transfer the content of the multipart file to the file // Transfer the content of the multipart file to the file
getFileInput().transferTo(file); getFileInput().transferTo(tempFile.getFile());
// Load the uploaded PDF // Load the uploaded PDF
PDDocument document = Loader.loadPDF(file); try (PDDocument document = Loader.loadPDF(tempFile.getFile())) {
// Render each page and invert colors // Render each page and invert colors
PDFRenderer pdfRenderer = new PDFRenderer(document); PDFRenderer pdfRenderer = new PDFRenderer(document);
for (int page = 0; page < document.getNumberOfPages(); page++) { for (int page = 0; page < document.getNumberOfPages(); page++) {
@@ -76,19 +73,19 @@ public class InvertFullColorStrategy extends ReplaceAndInvertColorStrategy {
PDImageXObject pdImage = PDImageXObject pdImage =
PDImageXObject.createFromFileByContent(tempImageFile, document); PDImageXObject.createFromFileByContent(tempImageFile, document);
PDPageContentStream contentStream = try (PDPageContentStream contentStream =
new PDPageContentStream( new PDPageContentStream(
document, document,
pdPage, pdPage,
PDPageContentStream.AppendMode.OVERWRITE, PDPageContentStream.AppendMode.OVERWRITE,
true); true)) {
contentStream.drawImage( contentStream.drawImage(
pdImage, pdImage,
0, 0,
0, 0,
pdPage.getMediaBox().getWidth(), pdPage.getMediaBox().getWidth(),
pdPage.getMediaBox().getHeight()); pdPage.getMediaBox().getHeight());
contentStream.close(); }
} finally { } finally {
if (tempImageFile != null && tempImageFile.exists()) { if (tempImageFile != null && tempImageFile.exists()) {
Files.delete(tempImageFile.toPath()); Files.delete(tempImageFile.toPath());
@@ -99,16 +96,12 @@ public class InvertFullColorStrategy extends ReplaceAndInvertColorStrategy {
// Save the modified PDF to a ByteArrayOutputStream // Save the modified PDF to a ByteArrayOutputStream
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
document.save(byteArrayOutputStream); document.save(byteArrayOutputStream);
document.close();
// Prepare the modified PDF for download // Prepare the modified PDF for download
ByteArrayInputStream inputStream = ByteArrayInputStream inputStream =
new ByteArrayInputStream(byteArrayOutputStream.toByteArray()); new ByteArrayInputStream(byteArrayOutputStream.toByteArray());
InputStreamResource resource = new InputStreamResource(inputStream); InputStreamResource resource = new InputStreamResource(inputStream);
return resource; return resource;
} finally {
if (file != null && file.exists()) {
Files.delete(file.toPath());
} }
} }
} }
@@ -117,18 +110,20 @@ public class InvertFullColorStrategy extends ReplaceAndInvertColorStrategy {
private void invertImageColors(BufferedImage image) { private void invertImageColors(BufferedImage image) {
int width = image.getWidth(); int width = image.getWidth();
int height = image.getHeight(); int height = image.getHeight();
for (int x = 0; x < width; x++) { int[] pixels = new int[width * height];
for (int y = 0; y < height; y++) { image.getRGB(0, 0, width, height, pixels, 0, width);
int rgba = image.getRGB(x, y); for (int i = 0; i < pixels.length; i++) {
Color color = new Color(rgba, true); int pixel = pixels[i];
Color invertedColor =
new Color( int a = 0xff;
255 - color.getRed(),
255 - color.getGreen(), int r = (pixel >> 16) & 0xff;
255 - color.getBlue()); int g = (pixel >> 8) & 0xff;
image.setRGB(x, y, invertedColor.getRGB()); int b = pixel & 0xff;
}
pixels[i] = (a << 24) | ((255 - r) << 16) | ((255 - g) << 8) | (255 - b);
} }
image.setRGB(0, 0, width, height, pixels, 0, width);
} }
// Helper method to convert BufferedImage to InputStream // Helper method to convert BufferedImage to InputStream
@@ -137,4 +132,23 @@ public class InvertFullColorStrategy extends ReplaceAndInvertColorStrategy {
ImageIO.write(image, "png", file); ImageIO.write(image, "png", file);
return file; return file;
} }
private static class TempFile implements AutoCloseable {
private final File file;
public TempFile(File file) {
this.file = file;
}
public File getFile() {
return file;
}
@Override
public void close() throws IOException {
if (file != null && file.exists()) {
Files.delete(file.toPath());
}
}
}
} }
@@ -57,12 +57,20 @@ public class MergeController {
// Merges a list of PDDocument objects into a single PDDocument // Merges a list of PDDocument objects into a single PDDocument
public PDDocument mergeDocuments(List<PDDocument> documents) throws IOException { public PDDocument mergeDocuments(List<PDDocument> documents) throws IOException {
PDDocument mergedDoc = pdfDocumentFactory.createNewDocument(); PDDocument mergedDoc = pdfDocumentFactory.createNewDocument();
boolean success = false;
try {
for (PDDocument doc : documents) { for (PDDocument doc : documents) {
for (PDPage page : doc.getPages()) { for (PDPage page : doc.getPages()) {
mergedDoc.addPage(page); mergedDoc.addPage(page);
} }
} }
success = true;
return mergedDoc; return mergedDoc;
} finally {
if (!success) {
mergedDoc.close();
}
}
} }
// Re-order files to match the explicit order provided by the front-end. // Re-order files to match the explicit order provided by the front-end.
@@ -363,9 +371,18 @@ public class MergeController {
// Save the modified document to a temporary file // Save the modified document to a temporary file
outputTempFile = new TempFile(tempFileManager, ".pdf"); outputTempFile = new TempFile(tempFileManager, ".pdf");
try {
mergedDocument.save(outputTempFile.getFile()); mergedDocument.save(outputTempFile.getFile());
} catch (Exception e) {
outputTempFile.close();
outputTempFile = null;
throw e;
}
} }
} catch (Exception ex) { } catch (Exception ex) {
if (outputTempFile != null) {
outputTempFile.close();
}
if (ex instanceof IOException && PdfErrorUtils.isCorruptedPdfError((IOException) ex)) { if (ex instanceof IOException && PdfErrorUtils.isCorruptedPdfError((IOException) ex)) {
log.warn("Corrupted PDF detected in merge pdf process: {}", ex.getMessage()); log.warn("Corrupted PDF detected in merge pdf process: {}", ex.getMessage());
} else { } else {
@@ -1,6 +1,5 @@
package stirling.software.SPDF.controller.api; package stirling.software.SPDF.controller.api;
import java.io.ByteArrayOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.OutputStream; import java.io.OutputStream;
import java.nio.file.Files; import java.nio.file.Files;
@@ -60,8 +59,6 @@ public class SplitPdfBySectionsController {
+ " Input:PDF Output:ZIP-PDF Type:SISO") + " Input:PDF Output:ZIP-PDF Type:SISO")
public ResponseEntity<StreamingResponseBody> splitPdf( public ResponseEntity<StreamingResponseBody> splitPdf(
@ModelAttribute SplitPdfBySectionsRequest request) throws Exception { @ModelAttribute SplitPdfBySectionsRequest request) throws Exception {
List<ByteArrayOutputStream> splitDocumentsBoas = new ArrayList<>();
MultipartFile file = request.getFileInput(); MultipartFile file = request.getFileInput();
String pageNumbers = request.getPageNumbers(); String pageNumbers = request.getPageNumbers();
SplitTypes splitMode = SplitTypes splitMode =
@@ -69,8 +66,7 @@ public class SplitPdfBySectionsController {
.map(SplitTypes::valueOf) .map(SplitTypes::valueOf)
.orElse(SplitTypes.SPLIT_ALL); .orElse(SplitTypes.SPLIT_ALL);
PDDocument sourceDocument = pdfDocumentFactory.load(file); try (PDDocument sourceDocument = pdfDocumentFactory.load(file)) {
Set<Integer> pagesToSplit = Set<Integer> pagesToSplit =
getPagesToSplit(pageNumbers, splitMode, sourceDocument.getNumberOfPages()); getPagesToSplit(pageNumbers, splitMode, sourceDocument.getNumberOfPages());
@@ -78,46 +74,174 @@ public class SplitPdfBySectionsController {
int horiz = request.getHorizontalDivisions() + 1; int horiz = request.getHorizontalDivisions() + 1;
int verti = request.getVerticalDivisions() + 1; int verti = request.getVerticalDivisions() + 1;
boolean merge = Boolean.TRUE.equals(request.getMerge()); boolean merge = Boolean.TRUE.equals(request.getMerge());
List<PDDocument> splitDocuments = splitPdfPages(sourceDocument, verti, horiz, pagesToSplit); String filename = GeneralUtils.generateFilename(file.getOriginalFilename(), "_split");
String filename = GeneralUtils.generateFilename(file.getOriginalFilename(), "_split.pdf");
if (merge) { if (merge) {
TempFile tempFile = new TempFile(tempFileManager, ".pdf"); TempFile tempFile = new TempFile(tempFileManager, ".pdf");
try (PDDocument merged = pdfService.mergeDocuments(splitDocuments); try (PDDocument mergedDoc = pdfDocumentFactory.createNewDocument();
OutputStream out = Files.newOutputStream(tempFile.getPath())) { OutputStream out = Files.newOutputStream(tempFile.getPath())) {
merged.save(out); LayerUtility layerUtility = new LayerUtility(mergedDoc);
for (PDDocument d : splitDocuments) d.close(); for (int pageIndex = 0;
sourceDocument.close(); pageIndex < sourceDocument.getNumberOfPages();
pageIndex++) {
if (pagesToSplit.contains(pageIndex)) {
addSplitPageToTarget(
sourceDocument,
pageIndex,
mergedDoc,
layerUtility,
horiz,
verti);
} else {
addPageToTarget(sourceDocument, pageIndex, mergedDoc, layerUtility);
} }
return WebResponseUtils.pdfFileToWebResponse(tempFile, filename + "_split.pdf");
} }
for (PDDocument doc : splitDocuments) { mergedDoc.save(out);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
doc.save(baos);
doc.close();
splitDocumentsBoas.add(baos);
} }
return WebResponseUtils.pdfFileToWebResponse(tempFile, filename + ".pdf");
sourceDocument.close(); } else {
TempFile zipTempFile = new TempFile(tempFileManager, ".zip"); TempFile zipTempFile = new TempFile(tempFileManager, ".zip");
try (ZipOutputStream zipOut = try (ZipOutputStream zipOut =
new ZipOutputStream(Files.newOutputStream(zipTempFile.getPath()))) { new ZipOutputStream(Files.newOutputStream(zipTempFile.getPath()))) {
int pageNum = 1; for (int pageIndex = 0;
for (int i = 0; i < splitDocumentsBoas.size(); i++) { pageIndex < sourceDocument.getNumberOfPages();
ByteArrayOutputStream baos = splitDocumentsBoas.get(i); pageIndex++) {
int sectionNum = (i % (horiz * verti)) + 1; int pageNum = pageIndex + 1;
String fileName = filename + "_" + pageNum + "_" + sectionNum + ".pdf"; if (pagesToSplit.contains(pageIndex)) {
byte[] pdf = baos.toByteArray(); for (int i = 0; i < horiz; i++) {
ZipEntry pdfEntry = new ZipEntry(fileName); for (int j = 0; j < verti; j++) {
zipOut.putNextEntry(pdfEntry); try (PDDocument subDoc =
zipOut.write(pdf); pdfDocumentFactory.createNewDocument()) {
zipOut.closeEntry(); LayerUtility subLayerUtility = new LayerUtility(subDoc);
addSingleSectionToTarget(
sourceDocument,
pageIndex,
subDoc,
subLayerUtility,
i,
j,
horiz,
verti);
int sectionNum = i * verti + j + 1;
String entryName =
filename
+ "_"
+ pageNum
+ "_"
+ sectionNum
+ ".pdf";
saveDocToZip(subDoc, zipOut, entryName);
}
}
}
} else {
try (PDDocument subDoc = pdfDocumentFactory.createNewDocument()) {
LayerUtility subLayerUtility = new LayerUtility(subDoc);
addPageToTarget(sourceDocument, pageIndex, subDoc, subLayerUtility);
String entryName = filename + "_" + pageNum + "_1.pdf";
saveDocToZip(subDoc, zipOut, entryName);
}
}
}
}
return WebResponseUtils.zipFileToWebResponse(zipTempFile, filename + ".zip");
}
}
}
if (sectionNum == horiz * verti) pageNum++; private void addPageToTarget(
PDDocument sourceDoc, int pageIndex, PDDocument targetDoc, LayerUtility layerUtility)
throws IOException {
PDPage sourcePage = sourceDoc.getPage(pageIndex);
PDPage newPage = new PDPage(sourcePage.getMediaBox());
targetDoc.addPage(newPage);
PDFormXObject form = layerUtility.importPageAsForm(sourceDoc, pageIndex);
try (PDPageContentStream contentStream =
new PDPageContentStream(targetDoc, newPage, AppendMode.APPEND, true, true)) {
contentStream.drawForm(form);
} }
} }
return WebResponseUtils.zipFileToWebResponse(zipTempFile, filename + "_split.zip");
private void addSplitPageToTarget(
PDDocument sourceDoc,
int pageIndex,
PDDocument targetDoc,
LayerUtility layerUtility,
int totalHoriz,
int totalVert)
throws IOException {
PDPage sourcePage = sourceDoc.getPage(pageIndex);
PDRectangle mediaBox = sourcePage.getMediaBox();
float width = mediaBox.getWidth();
float height = mediaBox.getHeight();
float subPageWidth = width / totalHoriz;
float subPageHeight = height / totalVert;
PDFormXObject form = layerUtility.importPageAsForm(sourceDoc, pageIndex);
for (int i = 0; i < totalHoriz; i++) {
for (int j = 0; j < totalVert; j++) {
PDPage subPage = new PDPage(new PDRectangle(subPageWidth, subPageHeight));
targetDoc.addPage(subPage);
try (PDPageContentStream contentStream =
new PDPageContentStream(
targetDoc, subPage, AppendMode.APPEND, true, true)) {
float translateX = -subPageWidth * i;
float translateY = -subPageHeight * (totalVert - 1 - j);
contentStream.saveGraphicsState();
contentStream.addRect(0, 0, subPageWidth, subPageHeight);
contentStream.clip();
contentStream.transform(new Matrix(1, 0, 0, 1, translateX, translateY));
contentStream.drawForm(form);
contentStream.restoreGraphicsState();
}
}
}
}
private void addSingleSectionToTarget(
PDDocument sourceDoc,
int pageIndex,
PDDocument targetDoc,
LayerUtility layerUtility,
int horizIndex,
int vertIndex,
int totalHoriz,
int totalVert)
throws IOException {
PDPage sourcePage = sourceDoc.getPage(pageIndex);
PDRectangle mediaBox = sourcePage.getMediaBox();
float subPageWidth = mediaBox.getWidth() / totalHoriz;
float subPageHeight = mediaBox.getHeight() / totalVert;
PDPage subPage = new PDPage(new PDRectangle(subPageWidth, subPageHeight));
targetDoc.addPage(subPage);
PDFormXObject form = layerUtility.importPageAsForm(sourceDoc, pageIndex);
try (PDPageContentStream contentStream =
new PDPageContentStream(targetDoc, subPage, AppendMode.APPEND, true, true)) {
float translateX = -subPageWidth * horizIndex;
float translateY = -subPageHeight * (totalVert - 1 - vertIndex);
contentStream.saveGraphicsState();
contentStream.addRect(0, 0, subPageWidth, subPageHeight);
contentStream.clip();
contentStream.transform(new Matrix(1, 0, 0, 1, translateX, translateY));
contentStream.drawForm(form);
contentStream.restoreGraphicsState();
}
}
private void saveDocToZip(PDDocument doc, ZipOutputStream zipOut, String entryName)
throws IOException {
ZipEntry entry = new ZipEntry(entryName);
zipOut.putNextEntry(entry);
doc.save(zipOut);
zipOut.closeEntry();
} }
// Based on the mode, get the pages that need to be split and return the pages set // Based on the mode, get the pages that need to be split and return the pages set
@@ -170,68 +294,4 @@ public class SplitPdfBySectionsController {
return pagesToSplit; return pagesToSplit;
} }
public List<PDDocument> splitPdfPages(
PDDocument document,
int horizontalDivisions,
int verticalDivisions,
Set<Integer> pagesToSplit)
throws IOException {
List<PDDocument> splitDocuments = new ArrayList<>();
int pageIndex = 0;
for (PDPage originalPage : document.getPages()) {
// If current page is not to split, add it to the splitDocuments directly.
if (!pagesToSplit.contains(pageIndex)) {
PDDocument newDoc = pdfDocumentFactory.createNewDocument();
newDoc.addPage(originalPage);
splitDocuments.add(newDoc);
} else {
// Otherwise, split current page.
PDRectangle originalMediaBox = originalPage.getMediaBox();
float width = originalMediaBox.getWidth();
float height = originalMediaBox.getHeight();
float subPageWidth = width / horizontalDivisions;
float subPageHeight = height / verticalDivisions;
LayerUtility layerUtility = new LayerUtility(document);
for (int i = 0; i < horizontalDivisions; i++) {
for (int j = 0; j < verticalDivisions; j++) {
PDDocument subDoc = new PDDocument();
PDPage subPage = new PDPage(new PDRectangle(subPageWidth, subPageHeight));
subDoc.addPage(subPage);
PDFormXObject form =
layerUtility.importPageAsForm(
document, document.getPages().indexOf(originalPage));
try (PDPageContentStream contentStream =
new PDPageContentStream(
subDoc, subPage, AppendMode.APPEND, true, true)) {
// Set clipping area and position
float translateX = -subPageWidth * i;
// float translateY = height - subPageHeight * (verticalDivisions - j);
float translateY = -subPageHeight * (verticalDivisions - 1 - j);
contentStream.saveGraphicsState();
contentStream.addRect(0, 0, subPageWidth, subPageHeight);
contentStream.clip();
contentStream.transform(new Matrix(1, 0, 0, 1, translateX, translateY));
// Draw the form
contentStream.drawForm(form);
contentStream.restoreGraphicsState();
}
splitDocuments.add(subDoc);
}
}
}
pageIndex++;
}
return splitDocuments;
}
} }
@@ -124,9 +124,40 @@ public class SplitPdfBySizeController {
throws IOException { throws IOException {
log.debug("Starting handleSplitBySize with maxBytes={}", maxBytes); log.debug("Starting handleSplitBySize with maxBytes={}", maxBytes);
PDDocument currentDoc = class DocHolder implements AutoCloseable {
pdfDocumentFactory.createNewDocumentBasedOnOldDocument(sourceDocument); private PDDocument doc;
public DocHolder(PDDocument doc) {
this.doc = doc;
}
public PDDocument getDoc() {
return doc;
}
public void setDoc(PDDocument doc) {
if (this.doc != null) {
try {
this.doc.close();
} catch (IOException e) {
log.error("Error closing document", e);
}
}
this.doc = doc;
}
@Override
public void close() throws IOException {
if (doc != null) {
doc.close();
}
}
}
int fileIndex = 1; int fileIndex = 1;
try (DocHolder holder =
new DocHolder(
pdfDocumentFactory.createNewDocumentBasedOnOldDocument(sourceDocument))) {
int totalPages = sourceDocument.getNumberOfPages(); int totalPages = sourceDocument.getNumberOfPages();
int pageAdded = 0; int pageAdded = 0;
@@ -139,7 +170,7 @@ public class SplitPdfBySizeController {
// Add the page to current document // Add the page to current document
PDPage newPage = new PDPage(page.getCOSObject()); PDPage newPage = new PDPage(page.getCOSObject());
currentDoc.addPage(newPage); holder.getDoc().addPage(newPage);
pageAdded++; pageAdded++;
// Dynamic size checking based on document size and page count // Dynamic size checking based on document size and page count
@@ -150,25 +181,30 @@ public class SplitPdfBySizeController {
if (shouldCheckSize) { if (shouldCheckSize) {
log.debug("Performing size check after {} pages", pageAdded); log.debug("Performing size check after {} pages", pageAdded);
ByteArrayOutputStream checkSizeStream = new ByteArrayOutputStream(); long actualSize;
currentDoc.save(checkSizeStream); try (ByteArrayOutputStream checkSizeStream = new ByteArrayOutputStream()) {
long actualSize = checkSizeStream.size(); holder.getDoc().save(checkSizeStream);
log.debug("Current document size: {} bytes (max: {} bytes)", actualSize, maxBytes); actualSize = checkSizeStream.size();
}
log.debug(
"Current document size: {} bytes (max: {} bytes)",
actualSize,
maxBytes);
if (actualSize > maxBytes) { if (actualSize > maxBytes) {
// We exceeded the limit - remove the last page and save // We exceeded the limit - remove the last page and save
if (currentDoc.getNumberOfPages() > 1) { if (holder.getDoc().getNumberOfPages() > 1) {
currentDoc.removePage(currentDoc.getNumberOfPages() - 1); holder.getDoc().removePage(holder.getDoc().getNumberOfPages() - 1);
pageIndex--; // Process this page again in the next document pageIndex--; // Process this page again in the next document
log.debug("Size limit exceeded - removed last page"); log.debug("Size limit exceeded - removed last page");
} }
log.debug( log.debug(
"Saving document with {} pages as part {}", "Saving document with {} pages as part {}",
currentDoc.getNumberOfPages(), holder.getDoc().getNumberOfPages(),
fileIndex); fileIndex);
saveDocumentToZip(currentDoc, zipOut, baseFilename, fileIndex++); saveDocumentToZip(holder.getDoc(), zipOut, baseFilename, fileIndex++);
currentDoc = new PDDocument(); holder.setDoc(new PDDocument());
pageAdded = 0; pageAdded = 0;
} else if (pageIndex < totalPages - 1) { } else if (pageIndex < totalPages - 1) {
// We're under the limit, calculate if we might fit more pages // We're under the limit, calculate if we might fit more pages
@@ -183,10 +219,12 @@ public class SplitPdfBySizeController {
pagesToLookAhead); pagesToLookAhead);
// Create a temp document with current pages + look-ahead pages // Create a temp document with current pages + look-ahead pages
PDDocument testDoc = new PDDocument(); try (PDDocument testDoc = new PDDocument()) {
// First copy existing pages // First copy existing pages
for (int i = 0; i < currentDoc.getNumberOfPages(); i++) { for (int i = 0; i < holder.getDoc().getNumberOfPages(); i++) {
testDoc.addPage(new PDPage(currentDoc.getPage(i).getCOSObject())); testDoc.addPage(
new PDPage(
holder.getDoc().getPage(i).getCOSObject()));
} }
// Try adding look-ahead pages one by one // Try adding look-ahead pages one by one
@@ -197,9 +235,12 @@ public class SplitPdfBySizeController {
testDoc.addPage(new PDPage(testPage.getCOSObject())); testDoc.addPage(new PDPage(testPage.getCOSObject()));
// Check if we're still under size // Check if we're still under size
ByteArrayOutputStream testStream = new ByteArrayOutputStream(); long testSize;
try (ByteArrayOutputStream testStream =
new ByteArrayOutputStream()) {
testDoc.save(testStream); testDoc.save(testStream);
long testSize = testStream.size(); testSize = testStream.size();
}
if (testSize <= maxBytes) { if (testSize <= maxBytes) {
extraPagesAdded++; extraPagesAdded++;
@@ -215,16 +256,16 @@ public class SplitPdfBySizeController {
break; break;
} }
} }
testDoc.close();
// Add the pages we verified would fit // Add the pages we verified would fit
if (extraPagesAdded > 0) { if (extraPagesAdded > 0) {
log.debug("Adding {} verified pages ahead", extraPagesAdded); log.debug(
"Adding {} verified pages ahead", extraPagesAdded);
for (int i = 0; i < extraPagesAdded; i++) { for (int i = 0; i < extraPagesAdded; i++) {
int extraPageIndex = pageIndex + 1 + i; int extraPageIndex = pageIndex + 1 + i;
PDPage extraPage = sourceDocument.getPage(extraPageIndex); PDPage extraPage =
currentDoc.addPage(new PDPage(extraPage.getCOSObject())); sourceDocument.getPage(extraPageIndex);
holder.getDoc()
.addPage(new PDPage(extraPage.getCOSObject()));
} }
pageIndex += extraPagesAdded; pageIndex += extraPagesAdded;
pageAdded += extraPagesAdded; pageAdded += extraPagesAdded;
@@ -234,14 +275,17 @@ public class SplitPdfBySizeController {
} }
} }
} }
}
// Save final document if it has any pages // Save final document if it has any pages
if (currentDoc.getNumberOfPages() > 0) { if (holder.getDoc() != null && holder.getDoc().getNumberOfPages() > 0) {
log.debug( log.debug(
"Saving final document with {} pages as part {}", "Saving final document with {} pages as part {}",
currentDoc.getNumberOfPages(), holder.getDoc().getNumberOfPages(),
fileIndex); fileIndex);
saveDocumentToZip(currentDoc, zipOut, baseFilename, fileIndex++); saveDocumentToZip(holder.getDoc(), zipOut, baseFilename, fileIndex++);
holder.setDoc(null);
}
} }
log.debug("Completed handleSplitBySize with {} document parts created", fileIndex - 1); log.debug("Completed handleSplitBySize with {} document parts created", fileIndex - 1);
@@ -252,8 +296,11 @@ public class SplitPdfBySizeController {
throws IOException { throws IOException {
log.debug("Starting handleSplitByPageCount with pageCount={}", pageCount); log.debug("Starting handleSplitByPageCount with pageCount={}", pageCount);
int currentPageCount = 0; int currentPageCount = 0;
PDDocument currentDoc = null;
int fileIndex = 1;
try {
log.debug("Creating initial output document"); log.debug("Creating initial output document");
PDDocument currentDoc;
try { try {
currentDoc = pdfDocumentFactory.createNewDocumentBasedOnOldDocument(sourceDocument); currentDoc = pdfDocumentFactory.createNewDocumentBasedOnOldDocument(sourceDocument);
log.debug("Successfully created initial output document"); log.debug("Successfully created initial output document");
@@ -262,7 +309,6 @@ public class SplitPdfBySizeController {
throw ExceptionUtils.createFileProcessingException("split", e); throw ExceptionUtils.createFileProcessingException("split", e);
} }
int fileIndex = 1;
int pageIndex = 0; int pageIndex = 0;
int totalPages = sourceDocument.getNumberOfPages(); int totalPages = sourceDocument.getNumberOfPages();
log.debug("Processing {} pages", totalPages); log.debug("Processing {} pages", totalPages);
@@ -291,6 +337,7 @@ public class SplitPdfBySizeController {
fileIndex); fileIndex);
try { try {
saveDocumentToZip(currentDoc, zipOut, baseFilename, fileIndex++); saveDocumentToZip(currentDoc, zipOut, baseFilename, fileIndex++);
currentDoc = null; // Document is closed by saveDocumentToZip
log.debug("Successfully saved document part {}", fileIndex - 1); log.debug("Successfully saved document part {}", fileIndex - 1);
} catch (Exception e) { } catch (Exception e) {
log.error("Error saving document part {}", fileIndex - 1, e); log.error("Error saving document part {}", fileIndex - 1, e);
@@ -317,13 +364,14 @@ public class SplitPdfBySizeController {
// Add the last document if it contains any pages // Add the last document if it contains any pages
try { try {
if (currentDoc.getPages().getCount() != 0) { if (currentDoc != null && currentDoc.getPages().getCount() != 0) {
log.debug( log.debug(
"Saving final document with {} pages as part {}", "Saving final document with {} pages as part {}",
currentDoc.getPages().getCount(), currentDoc.getPages().getCount(),
fileIndex); fileIndex);
try { try {
saveDocumentToZip(currentDoc, zipOut, baseFilename, fileIndex++); saveDocumentToZip(currentDoc, zipOut, baseFilename, fileIndex++);
currentDoc = null; // Document is closed by saveDocumentToZip
log.debug("Successfully saved final document part {}", fileIndex - 1); log.debug("Successfully saved final document part {}", fileIndex - 1);
} catch (Exception e) { } catch (Exception e) {
log.error("Error saving final document part {}", fileIndex - 1, e); log.error("Error saving final document part {}", fileIndex - 1, e);
@@ -335,13 +383,16 @@ public class SplitPdfBySizeController {
} catch (Exception e) { } catch (Exception e) {
log.error("Error checking or saving final document", e); log.error("Error checking or saving final document", e);
throw ExceptionUtils.createFileProcessingException("split", e); throw ExceptionUtils.createFileProcessingException("split", e);
}
} finally { } finally {
if (currentDoc != null) {
try { try {
log.debug("Closing final document"); log.debug("Closing remaining document");
currentDoc.close(); currentDoc.close();
log.debug("Successfully closed final document"); log.debug("Successfully closed remaining document");
} catch (Exception e) { } catch (Exception e) {
log.error("Error closing final document", e); log.error("Error closing remaining document", e);
}
} }
} }
@@ -367,14 +418,10 @@ public class SplitPdfBySizeController {
for (int i = 0; i < documentCount; i++) { for (int i = 0; i < documentCount; i++) {
log.debug("Creating document {} of {}", i + 1, documentCount); log.debug("Creating document {} of {}", i + 1, documentCount);
PDDocument currentDoc; PDDocument currentDoc = null;
try { try {
currentDoc = pdfDocumentFactory.createNewDocumentBasedOnOldDocument(sourceDocument); currentDoc = pdfDocumentFactory.createNewDocumentBasedOnOldDocument(sourceDocument);
log.debug("Successfully created document {} of {}", i + 1, documentCount); log.debug("Successfully created document {} of {}", i + 1, documentCount);
} catch (Exception e) {
log.error("Error creating document {} of {}", i + 1, documentCount, e);
throw ExceptionUtils.createFileProcessingException("split", e);
}
int pagesToAdd = pagesPerDocument + (i < extraPages ? 1 : 0); int pagesToAdd = pagesPerDocument + (i < extraPages ? 1 : 0);
log.debug("Adding {} pages to document {}", pagesToAdd, i + 1); log.debug("Adding {} pages to document {}", pagesToAdd, i + 1);
@@ -398,11 +445,25 @@ public class SplitPdfBySizeController {
try { try {
log.debug("Saving document {} with {} pages", i + 1, pagesToAdd); log.debug("Saving document {} with {} pages", i + 1, pagesToAdd);
saveDocumentToZip(currentDoc, zipOut, baseFilename, fileIndex++); saveDocumentToZip(currentDoc, zipOut, baseFilename, fileIndex++);
// saveDocumentToZip closes the document
currentDoc = null;
log.debug("Successfully saved document {}", i + 1); log.debug("Successfully saved document {}", i + 1);
} catch (Exception e) { } catch (Exception e) {
log.error("Error saving document {}", i + 1, e); log.error("Error saving document {}", i + 1, e);
throw e; throw e;
} }
} catch (Exception e) {
log.error("Error creating document {} of {}", i + 1, documentCount, e);
throw ExceptionUtils.createFileProcessingException("split", e);
} finally {
if (currentDoc != null) {
try {
currentDoc.close();
} catch (IOException e) {
log.error("Error closing document {} of {}", i + 1, documentCount, e);
}
}
}
} }
log.debug("Completed handleSplitByDocCount with {} documents created", documentCount); log.debug("Completed handleSplitByDocCount with {} documents created", documentCount);
@@ -414,24 +475,15 @@ public class SplitPdfBySizeController {
log.debug("Starting saveDocumentToZip for document part {}", index); log.debug("Starting saveDocumentToZip for document part {}", index);
ByteArrayOutputStream outStream = new ByteArrayOutputStream(); ByteArrayOutputStream outStream = new ByteArrayOutputStream();
try { try (PDDocument doc = document) {
log.debug("Saving document part {} to byte array", index); log.debug("Saving document part {} to byte array", index);
document.save(outStream); doc.save(outStream);
log.debug("Successfully saved document part {} ({} bytes)", index, outStream.size()); log.debug("Successfully saved document part {} ({} bytes)", index, outStream.size());
} catch (Exception e) { } catch (Exception e) {
log.error("Error saving document part {} to byte array", index, e); log.error("Error saving document part {} to byte array", index, e);
throw ExceptionUtils.createFileProcessingException("split", e); throw ExceptionUtils.createFileProcessingException("split", e);
} }
try {
log.debug("Closing document part {}", index);
document.close();
log.debug("Successfully closed document part {}", index);
} catch (Exception e) {
log.error("Error closing document part {}", index, e);
// Continue despite close error
}
try { try {
// Create a new zip entry // Create a new zip entry
String entryName = baseFilename + "_" + index + ".pdf"; String entryName = baseFilename + "_" + index + ".pdf";
@@ -7,7 +7,6 @@ import java.io.IOException;
import org.apache.pdfbox.multipdf.LayerUtility; import org.apache.pdfbox.multipdf.LayerUtility;
import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage; import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.PDPageContentStream;
import org.apache.pdfbox.pdmodel.common.PDRectangle; import org.apache.pdfbox.pdmodel.common.PDRectangle;
import org.apache.pdfbox.pdmodel.graphics.form.PDFormXObject; import org.apache.pdfbox.pdmodel.graphics.form.PDFormXObject;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
@@ -47,8 +46,7 @@ public class ToSinglePageController {
throws IOException { throws IOException {
// Load the source document // Load the source document
PDDocument sourceDocument = pdfDocumentFactory.load(request); try (PDDocument sourceDocument = pdfDocumentFactory.load(request)) {
// Calculate total height and max width // Calculate total height and max width
float totalHeight = 0; float totalHeight = 0;
float maxWidth = 0; float maxWidth = 0;
@@ -59,36 +57,36 @@ public class ToSinglePageController {
} }
// Create new document and page with calculated dimensions // Create new document and page with calculated dimensions
PDDocument newDocument = try (PDDocument newDocument =
pdfDocumentFactory.createNewDocumentBasedOnOldDocument(sourceDocument); pdfDocumentFactory.createNewDocumentBasedOnOldDocument(sourceDocument)) {
PDPage newPage = new PDPage(new PDRectangle(maxWidth, totalHeight)); PDPage newPage = new PDPage(new PDRectangle(maxWidth, totalHeight));
newDocument.addPage(newPage); newDocument.addPage(newPage);
// Initialize the content stream of the new page
PDPageContentStream contentStream = new PDPageContentStream(newDocument, newPage);
contentStream.close();
LayerUtility layerUtility = new LayerUtility(newDocument); LayerUtility layerUtility = new LayerUtility(newDocument);
float yOffset = totalHeight; float yOffset = totalHeight;
// For each page, copy its content to the new page at the correct offset // For each page, copy its content to the new page at the correct offset
try {
layerUtility.wrapInSaveRestore(newPage);
} catch (NullPointerException e) {
}
int pageIndex = 0; int pageIndex = 0;
for (PDPage page : sourceDocument.getPages()) { for (PDPage page : sourceDocument.getPages()) {
PDFormXObject form = layerUtility.importPageAsForm(sourceDocument, pageIndex); PDFormXObject form = layerUtility.importPageAsForm(sourceDocument, pageIndex);
if (form != null) {
AffineTransform af = AffineTransform af =
AffineTransform.getTranslateInstance( AffineTransform.getTranslateInstance(
0, yOffset - page.getMediaBox().getHeight()); 0, yOffset - page.getMediaBox().getHeight());
layerUtility.wrapInSaveRestore(newPage);
String defaultLayerName = "Layer" + pageIndex; String defaultLayerName = "Layer" + pageIndex;
layerUtility.appendFormAsLayer(newPage, form, af, defaultLayerName); layerUtility.appendFormAsLayer(newPage, form, af, defaultLayerName);
}
yOffset -= page.getMediaBox().getHeight(); yOffset -= page.getMediaBox().getHeight();
pageIndex++; pageIndex++;
} }
ByteArrayOutputStream baos = new ByteArrayOutputStream(); ByteArrayOutputStream baos = new ByteArrayOutputStream();
newDocument.save(baos); newDocument.save(baos);
newDocument.close();
sourceDocument.close();
byte[] result = baos.toByteArray(); byte[] result = baos.toByteArray();
return WebResponseUtils.bytesToWebResponse( return WebResponseUtils.bytesToWebResponse(
@@ -97,3 +95,5 @@ public class ToSinglePageController {
request.getFileInput().getOriginalFilename(), "_singlePage.pdf")); request.getFileInput().getOriginalFilename(), "_singlePage.pdf"));
} }
} }
}
}
@@ -91,8 +91,7 @@ public class UIDataController {
LicensesData data = new LicensesData(); LicensesData data = new LicensesData();
Resource resource = new ClassPathResource("static/3rdPartyLicenses.json"); Resource resource = new ClassPathResource("static/3rdPartyLicenses.json");
try { try (InputStream is = resource.getInputStream()) {
InputStream is = resource.getInputStream();
String json = new String(is.readAllBytes(), StandardCharsets.UTF_8); String json = new String(is.readAllBytes(), StandardCharsets.UTF_8);
ObjectMapper mapper = new ObjectMapper(); ObjectMapper mapper = new ObjectMapper();
Map<String, List<Dependency>> licenseData = Map<String, List<Dependency>> licenseData =
@@ -380,7 +380,7 @@ public class ConvertImgPDFController {
/** /**
* Rearranges the pages of the given PDF document based on the specified page order. * Rearranges the pages of the given PDF document based on the specified page order.
* *
* @param pdfBytes The byte array of the original PDF file. * @param pdfFile The MultipartFile of the original PDF file.
* @param pageOrderArr An array of page numbers indicating the new order. * @param pageOrderArr An array of page numbers indicating the new order.
* @return A byte array of the rearranged PDF. * @return A byte array of the rearranged PDF.
* @throws IOException If an error occurs while processing the PDF. * @throws IOException If an error occurs while processing the PDF.
@@ -388,9 +388,11 @@ public class ConvertImgPDFController {
private byte[] rearrangePdfPages(MultipartFile pdfFile, String[] pageOrderArr) private byte[] rearrangePdfPages(MultipartFile pdfFile, String[] pageOrderArr)
throws IOException { throws IOException {
// Load the input PDF // Load the input PDF
PDDocument document = pdfDocumentFactory.load(pdfFile); try (PDDocument document = pdfDocumentFactory.load(pdfFile);
ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
int totalPages = document.getNumberOfPages(); int totalPages = document.getNumberOfPages();
List<Integer> newPageOrder = GeneralUtils.parsePageList(pageOrderArr, totalPages, false); List<Integer> newPageOrder =
GeneralUtils.parsePageList(pageOrderArr, totalPages, false);
// Create a new list to hold the pages in the new order // Create a new list to hold the pages in the new order
List<PDPage> newPages = new ArrayList<>(); List<PDPage> newPages = new ArrayList<>();
@@ -409,14 +411,8 @@ public class ConvertImgPDFController {
} }
// Convert PDDocument to byte array // Convert PDDocument to byte array
byte[] newPdfBytes;
try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
document.save(baos); document.save(baos);
newPdfBytes = baos.toByteArray(); return baos.toByteArray();
} finally {
document.close();
} }
return newPdfBytes;
} }
} }
@@ -1,5 +1,6 @@
package stirling.software.SPDF.controller.api.converters; package stirling.software.SPDF.controller.api.converters;
import java.io.ByteArrayOutputStream;
import java.io.IOException; import java.io.IOException;
import java.net.URI; import java.net.URI;
import java.net.http.HttpClient; import java.net.http.HttpClient;
@@ -106,7 +107,6 @@ public class ConvertWebsiteToPDF {
Path tempOutputFile = null; Path tempOutputFile = null;
Path tempHtmlInput = null; Path tempHtmlInput = null;
PDDocument doc = null;
try { try {
// Download the remote content first to ensure we don't allow dangerous schemes // Download the remote content first to ensure we don't allow dangerous schemes
String htmlContent = fetchRemoteHtml(URL); String htmlContent = fetchRemoteHtml(URL);
@@ -140,18 +140,14 @@ public class ConvertWebsiteToPDF {
.runCommandWithOutputHandling(command); .runCommandWithOutputHandling(command);
// Load the PDF using pdfDocumentFactory // Load the PDF using pdfDocumentFactory
doc = pdfDocumentFactory.load(tempOutputFile.toFile()); try (PDDocument doc = pdfDocumentFactory.load(tempOutputFile.toFile());
ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
// Convert URL to a safe filename // Convert URL to a safe filename
String outputFilename = convertURLToFileName(URL); String outputFilename = convertURLToFileName(URL);
ResponseEntity<byte[]> response = doc.save(baos);
WebResponseUtils.pdfDocToWebResponse(doc, outputFilename); return WebResponseUtils.baosToWebResponse(baos, outputFilename);
if (response == null) {
// Defensive fallback - should not happen but avoids null returns breaking tests
return ResponseEntity.ok(new byte[0]);
} }
return response;
} finally { } finally {
if (tempHtmlInput != null) { if (tempHtmlInput != null) {
try { try {
@@ -119,13 +119,9 @@ public class AutoSplitPdfController {
MultipartFile file = request.getFileInput(); MultipartFile file = request.getFileInput();
boolean duplexMode = Boolean.TRUE.equals(request.getDuplexMode()); boolean duplexMode = Boolean.TRUE.equals(request.getDuplexMode());
PDDocument document = null;
List<PDDocument> splitDocuments = new ArrayList<>(); List<PDDocument> splitDocuments = new ArrayList<>();
TempFile outputTempFile = null; try (TempFile outputTempFile = new TempFile(tempFileManager, ".zip");
PDDocument document = pdfDocumentFactory.load(file.getInputStream())) {
try {
outputTempFile = new TempFile(tempFileManager, ".zip");
document = pdfDocumentFactory.load(file.getInputStream());
PDFRenderer pdfRenderer = new PDFRenderer(document); PDFRenderer pdfRenderer = new PDFRenderer(document);
pdfRenderer.setSubsamplingAllowed(true); pdfRenderer.setSubsamplingAllowed(true);
@@ -201,15 +197,7 @@ public class AutoSplitPdfController {
log.error("Error in auto split", e); log.error("Error in auto split", e);
throw e; throw e;
} finally { } finally {
// Clean up resources // Clean up split documents
if (document != null) {
try {
document.close();
} catch (IOException e) {
log.error("Error closing main PDDocument", e);
}
}
for (PDDocument splitDoc : splitDocuments) { for (PDDocument splitDoc : splitDocuments) {
try { try {
splitDoc.close(); splitDoc.close();
@@ -217,10 +205,6 @@ public class AutoSplitPdfController {
log.error("Error closing split PDDocument", e); log.error("Error closing split PDDocument", e);
} }
} }
if (outputTempFile != null) {
outputTempFile.close();
}
} }
} }
} }
@@ -53,18 +53,20 @@ public class BlankPageController {
// Convert to binary image based on the threshold // Convert to binary image based on the threshold
int whitePixels = 0; int whitePixels = 0;
int totalPixels = image.getWidth() * image.getHeight(); int width = image.getWidth();
int height = image.getHeight();
int[] pixels = new int[width * height];
for (int i = 0; i < image.getHeight(); i++) { image.getRGB(0, 0, width, height, pixels, 0, width);
for (int j = 0; j < image.getWidth(); j++) {
int color = image.getRGB(j, i) & 0xFF; for (int pixel : pixels) {
if (color >= 255 - threshold) { int blue = pixel & 0xFF;
if (blue >= 255 - threshold) {
whitePixels++; whitePixels++;
} }
} }
}
double whitePixelPercentage = (whitePixels / (double) totalPixels) * 100; double whitePixelPercentage = (whitePixels / (double) (width * height)) * 100;
log.info( log.info(
String.format( String.format(
Locale.ROOT, Locale.ROOT,
@@ -77,14 +77,15 @@ public class PrintFileController {
log.info("Selected Printer: {}", selectedService.getName()); log.info("Selected Printer: {}", selectedService.getName());
if (MediaType.APPLICATION_PDF_VALUE.equals(contentType)) { if (MediaType.APPLICATION_PDF_VALUE.equals(contentType)) {
PDDocument document = Loader.loadPDF(file.getBytes()); try (PDDocument document = Loader.loadPDF(file.getBytes())) {
PrinterJob job = PrinterJob.getPrinterJob(); PrinterJob job = PrinterJob.getPrinterJob();
job.setPrintService(selectedService); job.setPrintService(selectedService);
job.setPageable(new PDFPageable(document)); job.setPageable(new PDFPageable(document));
job.print(); job.print();
document.close(); }
} else if (contentType.startsWith("image/")) { } else if (contentType.startsWith("image/")) {
BufferedImage image = ImageIO.read(file.getInputStream()); try (var inputStream = file.getInputStream()) {
BufferedImage image = ImageIO.read(inputStream);
PrinterJob job = PrinterJob.getPrinterJob(); PrinterJob job = PrinterJob.getPrinterJob();
job.setPrintService(selectedService); job.setPrintService(selectedService);
job.setPrintable( job.setPrintable(
@@ -110,6 +111,7 @@ public class PrintFileController {
}); });
job.print(); job.print();
} }
}
return new ResponseEntity<>( return new ResponseEntity<>(
"File printed successfully to " + selectedService.getName(), HttpStatus.OK); "File printed successfully to " + selectedService.getName(), HttpStatus.OK);
} catch (Exception e) { } catch (Exception e) {
@@ -133,13 +133,13 @@ public class CertSignController {
signature.setReason(reason); signature.setReason(reason);
signature.setSignDate(Calendar.getInstance()); // PDFBox requires Calendar signature.setSignDate(Calendar.getInstance()); // PDFBox requires Calendar
if (Boolean.TRUE.equals(showSignature)) { if (Boolean.TRUE.equals(showSignature)) {
SignatureOptions signatureOptions = new SignatureOptions(); try (SignatureOptions signatureOptions = new SignatureOptions()) {
signatureOptions.setVisualSignature( signatureOptions.setVisualSignature(
instance.createVisibleSignature(doc, signature, pageNumber, showLogo)); instance.createVisibleSignature(doc, signature, pageNumber, showLogo));
signatureOptions.setPage(pageNumber); signatureOptions.setPage(pageNumber);
doc.addSignature(signature, instance, signatureOptions); doc.addSignature(signature, instance, signatureOptions);
}
} else { } else {
doc.addSignature(signature, instance); doc.addSignature(signature, instance);
} }
@@ -45,8 +45,7 @@ public class HomeWebController {
public String licensesForm(Model model) { public String licensesForm(Model model) {
model.addAttribute("currentPage", "licenses"); model.addAttribute("currentPage", "licenses");
Resource resource = new ClassPathResource("static/3rdPartyLicenses.json"); Resource resource = new ClassPathResource("static/3rdPartyLicenses.json");
try { try (InputStream is = resource.getInputStream()) {
InputStream is = resource.getInputStream();
String json = new String(is.readAllBytes(), StandardCharsets.UTF_8); String json = new String(is.readAllBytes(), StandardCharsets.UTF_8);
ObjectMapper mapper = new ObjectMapper(); ObjectMapper mapper = new ObjectMapper();
Map<String, List<Dependency>> data = mapper.readValue(json, new TypeReference<>() {}); Map<String, List<Dependency>> data = mapper.readValue(json, new TypeReference<>() {});
@@ -423,8 +423,8 @@ public class CertificateValidationService {
private void loadBundledMozillaCACerts() { private void loadBundledMozillaCACerts() {
try { try {
log.info("Loading bundled Mozilla CA certificates from resources"); log.info("Loading bundled Mozilla CA certificates from resources");
InputStream certStream = try (InputStream certStream =
getClass().getClassLoader().getResourceAsStream("certs/cacert.pem"); getClass().getClassLoader().getResourceAsStream("certs/cacert.pem")) {
if (certStream == null) { if (certStream == null) {
log.warn("Bundled Mozilla CA certificate file not found in resources"); log.warn("Bundled Mozilla CA certificate file not found in resources");
return; return;
@@ -432,7 +432,6 @@ public class CertificateValidationService {
CertificateFactory cf = CertificateFactory.getInstance("X.509"); CertificateFactory cf = CertificateFactory.getInstance("X.509");
Collection<? extends Certificate> certs = cf.generateCertificates(certStream); Collection<? extends Certificate> certs = cf.generateCertificates(certStream);
certStream.close();
int loadedCount = 0; int loadedCount = 0;
int skippedCount = 0; int skippedCount = 0;
@@ -455,6 +454,7 @@ public class CertificateValidationService {
"Loaded {} Mozilla CA certificates as trust anchors (skipped {} non-CA certs)", "Loaded {} Mozilla CA certificates as trust anchors (skipped {} non-CA certs)",
loadedCount, loadedCount,
skippedCount); skippedCount);
}
} catch (Exception e) { } catch (Exception e) {
log.error("Failed to load bundled Mozilla CA certificates: {}", e.getMessage(), e); log.error("Failed to load bundled Mozilla CA certificates: {}", e.getMessage(), e);
} }
@@ -0,0 +1,118 @@
package stirling.software.SPDF.controller.api;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.when;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import org.apache.pdfbox.Loader;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.common.PDRectangle;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.api.io.TempDir;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.mock.web.MockMultipartFile;
import stirling.software.SPDF.model.api.general.SplitPdfBySizeOrCountRequest;
import stirling.software.common.service.CustomPDFDocumentFactory;
import stirling.software.common.util.TempFileManager;
@ExtendWith(MockitoExtension.class)
class SplitPdfBySizeControllerTest {
@TempDir Path tempDir;
@Mock private CustomPDFDocumentFactory pdfDocumentFactory;
@Mock private TempFileManager tempFileManager;
@InjectMocks private SplitPdfBySizeController controller;
@BeforeEach
void setUp() throws IOException {
when(tempFileManager.createTempFile(anyString()))
.thenAnswer(
invocation -> {
String suffix = invocation.getArgument(0);
return Files.createTempFile(tempDir, "test", suffix).toFile();
});
}
@Test
@DisplayName("Should split by page count successfully")
void shouldSplitByPageCount() throws Exception {
byte[] pdfBytes;
try (PDDocument doc = new PDDocument()) {
for (int i = 0; i < 5; i++) {
doc.addPage(new PDPage(PDRectangle.A4));
}
Path pdfPath = tempDir.resolve("input.pdf");
doc.save(pdfPath.toFile());
pdfBytes = Files.readAllBytes(pdfPath);
}
MockMultipartFile file =
new MockMultipartFile(
"fileInput", "input.pdf", MediaType.APPLICATION_PDF_VALUE, pdfBytes);
SplitPdfBySizeOrCountRequest request = new SplitPdfBySizeOrCountRequest();
request.setFileInput(file);
request.setSplitType(1); // Page count
request.setSplitValue("2");
when(pdfDocumentFactory.load(any(byte[].class)))
.thenAnswer(inv -> Loader.loadPDF((byte[]) inv.getArgument(0)));
when(pdfDocumentFactory.createNewDocumentBasedOnOldDocument(any(PDDocument.class)))
.thenAnswer(inv -> new PDDocument());
ResponseEntity<byte[]> response = controller.autoSplitPdf(request);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(response.getBody()).isNotEmpty();
assertThat(response.getHeaders().getContentType())
.isEqualTo(MediaType.APPLICATION_OCTET_STREAM);
}
@Test
@DisplayName("Should split by document count successfully")
void shouldSplitByDocCount() throws Exception {
byte[] pdfBytes;
try (PDDocument doc = new PDDocument()) {
for (int i = 0; i < 6; i++) {
doc.addPage(new PDPage(PDRectangle.A4));
}
Path pdfPath = tempDir.resolve("input.pdf");
doc.save(pdfPath.toFile());
pdfBytes = Files.readAllBytes(pdfPath);
}
MockMultipartFile file =
new MockMultipartFile(
"fileInput", "input.pdf", MediaType.APPLICATION_PDF_VALUE, pdfBytes);
SplitPdfBySizeOrCountRequest request = new SplitPdfBySizeOrCountRequest();
request.setFileInput(file);
request.setSplitType(2); // Document count
request.setSplitValue("3"); // Split into 3 docs (2 pages each)
when(pdfDocumentFactory.load(any(byte[].class)))
.thenAnswer(inv -> Loader.loadPDF((byte[]) inv.getArgument(0)));
when(pdfDocumentFactory.createNewDocumentBasedOnOldDocument(any(PDDocument.class)))
.thenAnswer(inv -> new PDDocument());
ResponseEntity<byte[]> response = controller.autoSplitPdf(request);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(response.getBody()).isNotEmpty();
}
}
@@ -6,6 +6,7 @@ import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
import java.io.ByteArrayOutputStream;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.lang.reflect.Method; import java.lang.reflect.Method;
@@ -189,9 +190,12 @@ public class ConvertWebsiteToPdfTest {
when(mockExec.runCommandWithOutputHandling(cmdCaptor.capture())) when(mockExec.runCommandWithOutputHandling(cmdCaptor.capture()))
.thenReturn(dummyResult); .thenReturn(dummyResult);
// Mock WebResponseUtils
ResponseEntity<byte[]> fakeResponse = ResponseEntity.ok(new byte[0]); ResponseEntity<byte[]> fakeResponse = ResponseEntity.ok(new byte[0]);
wr.when(() -> WebResponseUtils.pdfDocToWebResponse(any(PDDocument.class), anyString()))
wr.when(
() ->
WebResponseUtils.baosToWebResponse(
any(ByteArrayOutputStream.class), any()))
.thenReturn(fakeResponse); .thenReturn(fakeResponse);
// Act // Act
@@ -261,7 +265,10 @@ public class ConvertWebsiteToPdfTest {
// WebResponseUtils // WebResponseUtils
ResponseEntity<byte[]> fakeResponse = ResponseEntity.ok(new byte[0]); ResponseEntity<byte[]> fakeResponse = ResponseEntity.ok(new byte[0]);
wr.when(() -> WebResponseUtils.pdfDocToWebResponse(any(PDDocument.class), anyString())) wr.when(
() ->
WebResponseUtils.baosToWebResponse(
any(ByteArrayOutputStream.class), any()))
.thenReturn(fakeResponse); .thenReturn(fakeResponse);
// Act: should not throw and should return a Response // Act: should not throw and should return a Response
@@ -3,8 +3,6 @@ package stirling.software.proprietary.security.session;
import java.time.Duration; import java.time.Duration;
import java.time.Instant; import java.time.Instant;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date; import java.util.Date;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
@@ -182,15 +180,7 @@ public class SessionPersistentRegistry implements SessionRegistry {
} }
// Sort sessions by lastRequest in descending order // Sort sessions by lastRequest in descending order
Collections.sort( allSessions.sort((s1, s2) -> s2.getLastRequest().compareTo(s1.getLastRequest()));
allSessions,
new Comparator<SessionEntity>() {
@Override
public int compare(SessionEntity s1, SessionEntity s2) {
// Sort by lastRequest in descending order
return s2.getLastRequest().compareTo(s1.getLastRequest());
}
});
// The first session in the list is the latest session for the given principal name // The first session in the list is the latest session for the given principal name
return Optional.of(allSessions.get(0)); return Optional.of(allSessions.get(0));