perf(core): Stream responses and unify temp file lifecycle across controllers (#4330)

This commit is contained in:
Ludy 2025-09-05 12:27:28 +02:00 committed by GitHub
parent 9a39aff19f
commit 9b3e2c29a5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 261 additions and 187 deletions

View File

@ -0,0 +1,35 @@
package stirling.software.common.util;
import java.io.IOException;
import java.util.List;
import org.apache.pdfbox.multipdf.PDFMergerUtility;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.springframework.stereotype.Service;
import lombok.RequiredArgsConstructor;
import stirling.software.common.service.CustomPDFDocumentFactory;
@Service
@RequiredArgsConstructor
public class PDFService {
private final CustomPDFDocumentFactory pdfDocumentFactory;
/*
* Merge multiple PDF documents into a single PDF document
*
* @param documents List of PDDocument to be merged
* @return Merged PDDocument
* @throws IOException If an error occurs during merging
*/
public PDDocument mergeDocuments(List<PDDocument> documents) throws IOException {
PDDocument merged = pdfDocumentFactory.createNewDocument();
PDFMergerUtility merger = new PDFMergerUtility();
for (PDDocument doc : documents) {
merger.appendDocument(merged, doc);
}
return merged;
}
}

View File

@ -4,6 +4,8 @@ import java.io.ByteArrayOutputStream;
import java.io.IOException; import java.io.IOException;
import java.net.URLEncoder; import java.net.URLEncoder;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.PDDocument;
import org.springframework.http.HttpHeaders; import org.springframework.http.HttpHeaders;
@ -11,9 +13,13 @@ import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;
import io.github.pixee.security.Filenames; import io.github.pixee.security.Filenames;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class WebResponseUtils { public class WebResponseUtils {
public static ResponseEntity<byte[]> baosToWebResponse( public static ResponseEntity<byte[]> baosToWebResponse(
@ -64,4 +70,59 @@ public class WebResponseUtils {
return baosToWebResponse(baos, docName); return baosToWebResponse(baos, docName);
} }
/**
* Convert a File to a web response (PDF default).
*
* @param outputTempFile The temporary file to be sent as a response.
* @param docName The name of the document.
* @return A ResponseEntity containing the file as a resource.
*/
public static ResponseEntity<StreamingResponseBody> pdfFileToWebResponse(
TempFile outputTempFile, String docName) throws IOException {
return fileToWebResponse(outputTempFile, docName, MediaType.APPLICATION_PDF);
}
/**
* Convert a File to a web response (ZIP default).
*
* @param outputTempFile The temporary file to be sent as a response.
* @param docName The name of the document.
* @return A ResponseEntity containing the file as a resource.
*/
public static ResponseEntity<StreamingResponseBody> zipFileToWebResponse(
TempFile outputTempFile, String docName) throws IOException {
return fileToWebResponse(outputTempFile, docName, MediaType.APPLICATION_OCTET_STREAM);
}
/**
* Convert a File to a web response with explicit media type (e.g., ZIP).
*
* @param outputTempFile The temporary file to be sent as a response.
* @param docName The name of the document.
* @param mediaType The content type to set on the response.
* @return A ResponseEntity containing the file as a resource.
*/
public static ResponseEntity<StreamingResponseBody> fileToWebResponse(
TempFile outputTempFile, String docName, MediaType mediaType) throws IOException {
Path path = outputTempFile.getFile().toPath().normalize();
long len = Files.size(path);
HttpHeaders headers = new HttpHeaders();
headers.setContentType(mediaType);
headers.setContentLength(len);
headers.add(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + docName + "\"");
StreamingResponseBody body =
os -> {
try (os) {
Files.copy(path, os);
os.flush();
} finally {
outputTempFile.close();
}
};
return new ResponseEntity<>(body, headers, HttpStatus.OK);
}
} }

View File

@ -1,6 +1,5 @@
package stirling.software.SPDF.controller.api; package stirling.software.SPDF.controller.api;
import java.io.ByteArrayOutputStream;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.nio.file.Files; import java.nio.file.Files;
@ -27,6 +26,7 @@ import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
@ -37,8 +37,9 @@ import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.model.api.general.MergePdfsRequest; import stirling.software.SPDF.model.api.general.MergePdfsRequest;
import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.service.CustomPDFDocumentFactory;
import stirling.software.common.util.ExceptionUtils; import stirling.software.common.util.ExceptionUtils;
import stirling.software.common.util.GeneralUtils;
import stirling.software.common.util.PdfErrorUtils; import stirling.software.common.util.PdfErrorUtils;
import stirling.software.common.util.TempFile;
import stirling.software.common.util.TempFileManager;
import stirling.software.common.util.WebResponseUtils; import stirling.software.common.util.WebResponseUtils;
@RestController @RestController
@ -49,6 +50,7 @@ import stirling.software.common.util.WebResponseUtils;
public class MergeController { public class MergeController {
private final CustomPDFDocumentFactory pdfDocumentFactory; private final CustomPDFDocumentFactory pdfDocumentFactory;
private final TempFileManager tempFileManager;
// 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 {
@ -63,56 +65,54 @@ public class MergeController {
// Returns a comparator for sorting MultipartFile arrays based on the given sort type // Returns a comparator for sorting MultipartFile arrays based on the given sort type
private Comparator<MultipartFile> getSortComparator(String sortType) { private Comparator<MultipartFile> getSortComparator(String sortType) {
switch (sortType) { return switch (sortType) {
case "byFileName": case "byFileName" -> Comparator.comparing(MultipartFile::getOriginalFilename);
return Comparator.comparing(MultipartFile::getOriginalFilename); case "byDateModified" ->
case "byDateModified": (file1, file2) -> {
return (file1, file2) -> { try {
try { BasicFileAttributes attr1 =
BasicFileAttributes attr1 = Files.readAttributes(
Files.readAttributes( Paths.get(file1.getOriginalFilename()),
Paths.get(file1.getOriginalFilename()), BasicFileAttributes.class);
BasicFileAttributes.class); BasicFileAttributes attr2 =
BasicFileAttributes attr2 = Files.readAttributes(
Files.readAttributes( Paths.get(file2.getOriginalFilename()),
Paths.get(file2.getOriginalFilename()), BasicFileAttributes.class);
BasicFileAttributes.class); return attr1.lastModifiedTime().compareTo(attr2.lastModifiedTime());
return attr1.lastModifiedTime().compareTo(attr2.lastModifiedTime()); } catch (IOException e) {
} catch (IOException e) { return 0; // If there's an error, treat them as equal
return 0; // If there's an error, treat them as equal }
} };
}; case "byDateCreated" ->
case "byDateCreated": (file1, file2) -> {
return (file1, file2) -> { try {
try { BasicFileAttributes attr1 =
BasicFileAttributes attr1 = Files.readAttributes(
Files.readAttributes( Paths.get(file1.getOriginalFilename()),
Paths.get(file1.getOriginalFilename()), BasicFileAttributes.class);
BasicFileAttributes.class); BasicFileAttributes attr2 =
BasicFileAttributes attr2 = Files.readAttributes(
Files.readAttributes( Paths.get(file2.getOriginalFilename()),
Paths.get(file2.getOriginalFilename()), BasicFileAttributes.class);
BasicFileAttributes.class); return attr1.creationTime().compareTo(attr2.creationTime());
return attr1.creationTime().compareTo(attr2.creationTime()); } catch (IOException e) {
} catch (IOException e) { return 0; // If there's an error, treat them as equal
return 0; // If there's an error, treat them as equal }
} };
}; case "byPDFTitle" ->
case "byPDFTitle": (file1, file2) -> {
return (file1, file2) -> { try (PDDocument doc1 = pdfDocumentFactory.load(file1);
try (PDDocument doc1 = pdfDocumentFactory.load(file1); PDDocument doc2 = pdfDocumentFactory.load(file2)) {
PDDocument doc2 = pdfDocumentFactory.load(file2)) { String title1 = doc1.getDocumentInformation().getTitle();
String title1 = doc1.getDocumentInformation().getTitle(); String title2 = doc2.getDocumentInformation().getTitle();
String title2 = doc2.getDocumentInformation().getTitle(); return title1.compareTo(title2);
return title1.compareTo(title2); } catch (IOException e) {
} catch (IOException e) { return 0;
return 0; }
} };
}; case "orderProvided" -> (file1, file2) -> 0; // Default is the order provided
case "orderProvided": default -> (file1, file2) -> 0; // Default is the order provided
default: };
return (file1, file2) -> 0; // Default is the order provided
}
} }
// Adds a table of contents to the merged document using filenames as chapter titles // Adds a table of contents to the merged document using filenames as chapter titles
@ -162,10 +162,11 @@ public class MergeController {
"This endpoint merges multiple PDF files into a single PDF file. The merged" "This endpoint merges multiple PDF files into a single PDF file. The merged"
+ " file will contain all pages from the input files in the order they were" + " file will contain all pages from the input files in the order they were"
+ " provided. Input:PDF Output:PDF Type:MISO") + " provided. Input:PDF Output:PDF Type:MISO")
public ResponseEntity<byte[]> mergePdfs(@ModelAttribute MergePdfsRequest request) public ResponseEntity<StreamingResponseBody> mergePdfs(@ModelAttribute MergePdfsRequest request)
throws IOException { throws IOException {
List<File> filesToDelete = new ArrayList<>(); // List of temporary files to delete List<File> filesToDelete = new ArrayList<>(); // List of temporary files to delete
File mergedTempFile = null; TempFile mergedTempFile = null;
TempFile outputTempFile = null;
PDDocument mergedDocument = null; PDDocument mergedDocument = null;
boolean removeCertSign = Boolean.TRUE.equals(request.getRemoveCertSign()); boolean removeCertSign = Boolean.TRUE.equals(request.getRemoveCertSign());
@ -183,14 +184,14 @@ public class MergeController {
for (MultipartFile multipartFile : files) { for (MultipartFile multipartFile : files) {
totalSize += multipartFile.getSize(); totalSize += multipartFile.getSize();
File tempFile = File tempFile =
GeneralUtils.convertMultipartFileToFile( tempFileManager.convertMultipartFileToFile(
multipartFile); // Convert MultipartFile to File multipartFile); // Convert MultipartFile to File
filesToDelete.add(tempFile); // Add temp file to the list for later deletion filesToDelete.add(tempFile); // Add temp file to the list for later deletion
mergerUtility.addSource(tempFile); // Add source file to the merger utility mergerUtility.addSource(tempFile); // Add source file to the merger utility
} }
mergedTempFile = Files.createTempFile("merged-", ".pdf").toFile(); mergedTempFile = new TempFile(tempFileManager, ".pdf");
mergerUtility.setDestinationFileName(mergedTempFile.getAbsolutePath()); mergerUtility.setDestinationFileName(mergedTempFile.getFile().getAbsolutePath());
try { try {
mergerUtility.mergeDocuments( mergerUtility.mergeDocuments(
@ -205,7 +206,7 @@ public class MergeController {
} }
// Load the merged PDF document // Load the merged PDF document
mergedDocument = pdfDocumentFactory.load(mergedTempFile); mergedDocument = pdfDocumentFactory.load(mergedTempFile.getFile());
// Remove signatures if removeCertSign is true // Remove signatures if removeCertSign is true
if (removeCertSign) { if (removeCertSign) {
@ -214,7 +215,7 @@ public class MergeController {
if (acroForm != null) { if (acroForm != null) {
List<PDField> fieldsToRemove = List<PDField> fieldsToRemove =
acroForm.getFields().stream() acroForm.getFields().stream()
.filter(field -> field instanceof PDSignatureField) .filter(PDSignatureField.class::isInstance)
.toList(); .toList();
if (!fieldsToRemove.isEmpty()) { if (!fieldsToRemove.isEmpty()) {
@ -230,16 +231,15 @@ public class MergeController {
addTableOfContents(mergedDocument, files); addTableOfContents(mergedDocument, files);
} }
// Save the modified document to a new ByteArrayOutputStream // Save the modified document to a temporary file
ByteArrayOutputStream baos = new ByteArrayOutputStream(); outputTempFile = new TempFile(tempFileManager, ".pdf");
mergedDocument.save(baos); mergedDocument.save(outputTempFile.getFile());
String mergedFileName = String mergedFileName =
files[0].getOriginalFilename().replaceFirst("[.][^.]+$", "") files[0].getOriginalFilename().replaceFirst("[.][^.]+$", "")
+ "_merged_unsigned.pdf"; + "_merged_unsigned.pdf";
return WebResponseUtils.baosToWebResponse( return WebResponseUtils.pdfFileToWebResponse(
baos, mergedFileName); // Return the modified PDF outputTempFile, mergedFileName); // Return the modified PDF as stream
} catch (Exception ex) { } catch (Exception ex) {
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());
@ -252,12 +252,10 @@ public class MergeController {
mergedDocument.close(); // Close the merged document mergedDocument.close(); // Close the merged document
} }
for (File file : filesToDelete) { for (File file : filesToDelete) {
if (file != null) { tempFileManager.deleteTempFile(file); // Delete temporary files
Files.deleteIfExists(file.toPath()); // Delete temporary files
}
} }
if (mergedTempFile != null) { if (mergedTempFile != null) {
Files.deleteIfExists(mergedTempFile.toPath()); mergedTempFile.close();
} }
} }
} }

View File

@ -30,6 +30,8 @@ import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.model.api.PDFWithPageNums; import stirling.software.SPDF.model.api.PDFWithPageNums;
import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.service.CustomPDFDocumentFactory;
import stirling.software.common.util.ExceptionUtils; import stirling.software.common.util.ExceptionUtils;
import stirling.software.common.util.TempFile;
import stirling.software.common.util.TempFileManager;
import stirling.software.common.util.WebResponseUtils; import stirling.software.common.util.WebResponseUtils;
@RestController @RestController
@ -40,6 +42,7 @@ import stirling.software.common.util.WebResponseUtils;
public class SplitPDFController { public class SplitPDFController {
private final CustomPDFDocumentFactory pdfDocumentFactory; private final CustomPDFDocumentFactory pdfDocumentFactory;
private final TempFileManager tempFileManager;
@PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/split-pages") @PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/split-pages")
@Operation( @Operation(
@ -55,8 +58,11 @@ public class SplitPDFController {
PDDocument document = null; PDDocument document = null;
Path zipFile = null; Path zipFile = null;
List<ByteArrayOutputStream> splitDocumentsBoas = new ArrayList<>(); List<ByteArrayOutputStream> splitDocumentsBoas = new ArrayList<>();
String filename;
TempFile outputTempFile = null;
try { try {
outputTempFile = new TempFile(tempFileManager, ".zip");
MultipartFile file = request.getFileInput(); MultipartFile file = request.getFileInput();
String pages = request.getPageNumbers(); String pages = request.getPageNumbers();
@ -105,12 +111,11 @@ public class SplitPDFController {
// closing the original document // closing the original document
document.close(); document.close();
zipFile = Files.createTempFile("split_documents", ".zip"); filename =
String filename =
Filenames.toSimpleFileName(file.getOriginalFilename()) Filenames.toSimpleFileName(file.getOriginalFilename())
.replaceFirst("[.][^.]+$", ""); .replaceFirst("[.][^.]+$", "");
try (ZipOutputStream zipOut = new ZipOutputStream(Files.newOutputStream(zipFile))) { try (ZipOutputStream zipOut =
new ZipOutputStream(Files.newOutputStream(outputTempFile.getPath()))) {
// loop through the split documents and write them to the zip file // loop through the split documents and write them to the zip file
for (int i = 0; i < splitDocumentsBoas.size(); i++) { for (int i = 0; i < splitDocumentsBoas.size(); i++) {
String fileName = filename + "_" + (i + 1) + ".pdf"; String fileName = filename + "_" + (i + 1) + ".pdf";
@ -125,19 +130,13 @@ public class SplitPDFController {
log.debug("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.debug(
log.debug("Successfully created zip file with split documents: {}", zipFile.toString()); "Successfully created zip file with split documents: {}",
byte[] data = Files.readAllBytes(zipFile); outputTempFile.getPath());
Files.deleteIfExists(zipFile); byte[] data = Files.readAllBytes(outputTempFile.getPath());
// return the Resource in the response
return WebResponseUtils.bytesToWebResponse( return WebResponseUtils.bytesToWebResponse(
data, filename + ".zip", MediaType.APPLICATION_OCTET_STREAM); data, filename + ".zip", MediaType.APPLICATION_OCTET_STREAM);
} finally { } finally {
try { try {
// Close the main document // Close the main document
@ -152,9 +151,9 @@ public class SplitPDFController {
} }
} }
// Delete temporary zip file // Close the output temporary file
if (zipFile != null) { if (outputTempFile != null) {
Files.deleteIfExists(zipFile); outputTempFile.close();
} }
} catch (Exception e) { } catch (Exception e) {
log.error("Error while cleaning up resources", e); log.error("Error while cleaning up resources", e);

View File

@ -2,8 +2,8 @@ package stirling.software.SPDF.controller.api;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.OutputStream;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.zip.ZipEntry; import java.util.zip.ZipEntry;
@ -17,13 +17,13 @@ import org.apache.pdfbox.pdmodel.PDPageContentStream.AppendMode;
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.apache.pdfbox.util.Matrix; import org.apache.pdfbox.util.Matrix;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;
import io.github.pixee.security.Filenames; import io.github.pixee.security.Filenames;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
@ -33,6 +33,9 @@ import lombok.RequiredArgsConstructor;
import stirling.software.SPDF.model.api.SplitPdfBySectionsRequest; import stirling.software.SPDF.model.api.SplitPdfBySectionsRequest;
import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.service.CustomPDFDocumentFactory;
import stirling.software.common.util.PDFService;
import stirling.software.common.util.TempFile;
import stirling.software.common.util.TempFileManager;
import stirling.software.common.util.WebResponseUtils; import stirling.software.common.util.WebResponseUtils;
@RestController @RestController
@ -42,6 +45,8 @@ import stirling.software.common.util.WebResponseUtils;
public class SplitPdfBySectionsController { public class SplitPdfBySectionsController {
private final CustomPDFDocumentFactory pdfDocumentFactory; private final CustomPDFDocumentFactory pdfDocumentFactory;
private final TempFileManager tempFileManager;
private final PDFService pdfService;
@PostMapping(value = "/split-pdf-by-sections", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) @PostMapping(value = "/split-pdf-by-sections", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
@Operation( @Operation(
@ -50,8 +55,8 @@ public class SplitPdfBySectionsController {
"Split each page of a PDF into smaller sections based on the user's choice" "Split each page of a PDF into smaller sections based on the user's choice"
+ " (halves, thirds, quarters, etc.), both vertically and horizontally." + " (halves, thirds, quarters, etc.), both vertically and horizontally."
+ " Input:PDF Output:ZIP-PDF Type:SISO") + " Input:PDF Output:ZIP-PDF Type:SISO")
public ResponseEntity<byte[]> splitPdf(@ModelAttribute SplitPdfBySectionsRequest request) public ResponseEntity<StreamingResponseBody> splitPdf(
throws Exception { @ModelAttribute SplitPdfBySectionsRequest request) throws Exception {
List<ByteArrayOutputStream> splitDocumentsBoas = new ArrayList<>(); List<ByteArrayOutputStream> splitDocumentsBoas = new ArrayList<>();
MultipartFile file = request.getFileInput(); MultipartFile file = request.getFileInput();
@ -67,10 +72,14 @@ public class SplitPdfBySectionsController {
Filenames.toSimpleFileName(file.getOriginalFilename()) Filenames.toSimpleFileName(file.getOriginalFilename())
.replaceFirst("[.][^.]+$", ""); .replaceFirst("[.][^.]+$", "");
if (merge) { if (merge) {
MergeController mergeController = new MergeController(pdfDocumentFactory); TempFile tempFile = new TempFile(tempFileManager, ".pdf");
ByteArrayOutputStream baos = new ByteArrayOutputStream(); try (PDDocument merged = pdfService.mergeDocuments(splitDocuments);
mergeController.mergeDocuments(splitDocuments).save(baos); OutputStream out = Files.newOutputStream(tempFile.getPath())) {
return WebResponseUtils.bytesToWebResponse(baos.toByteArray(), filename + "_split.pdf"); merged.save(out);
for (PDDocument d : splitDocuments) d.close();
sourceDocument.close();
}
return WebResponseUtils.pdfFileToWebResponse(tempFile, filename + "_split.pdf");
} }
for (PDDocument doc : splitDocuments) { for (PDDocument doc : splitDocuments) {
ByteArrayOutputStream baos = new ByteArrayOutputStream(); ByteArrayOutputStream baos = new ByteArrayOutputStream();
@ -81,10 +90,9 @@ public class SplitPdfBySectionsController {
sourceDocument.close(); sourceDocument.close();
Path zipFile = Files.createTempFile("split_documents", ".zip"); TempFile zipTempFile = new TempFile(tempFileManager, ".zip");
byte[] data; try (ZipOutputStream zipOut =
new ZipOutputStream(Files.newOutputStream(zipTempFile.getPath()))) {
try (ZipOutputStream zipOut = new ZipOutputStream(Files.newOutputStream(zipFile))) {
int pageNum = 1; int pageNum = 1;
for (int i = 0; i < splitDocumentsBoas.size(); i++) { for (int i = 0; i < splitDocumentsBoas.size(); i++) {
ByteArrayOutputStream baos = splitDocumentsBoas.get(i); ByteArrayOutputStream baos = splitDocumentsBoas.get(i);
@ -98,15 +106,8 @@ public class SplitPdfBySectionsController {
if (sectionNum == horiz * verti) pageNum++; if (sectionNum == horiz * verti) pageNum++;
} }
zipOut.finish();
data = Files.readAllBytes(zipFile);
return WebResponseUtils.bytesToWebResponse(
data, filename + "_split.zip", MediaType.APPLICATION_OCTET_STREAM);
} finally {
Files.deleteIfExists(zipFile);
} }
return WebResponseUtils.zipFileToWebResponse(zipTempFile, filename + "_split.zip");
} }
public List<PDDocument> splitPdfPages( public List<PDDocument> splitPdfPages(

View File

@ -28,6 +28,8 @@ import stirling.software.SPDF.model.api.general.SplitPdfBySizeOrCountRequest;
import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.service.CustomPDFDocumentFactory;
import stirling.software.common.util.ExceptionUtils; import stirling.software.common.util.ExceptionUtils;
import stirling.software.common.util.GeneralUtils; import stirling.software.common.util.GeneralUtils;
import stirling.software.common.util.TempFile;
import stirling.software.common.util.TempFileManager;
import stirling.software.common.util.WebResponseUtils; import stirling.software.common.util.WebResponseUtils;
@RestController @RestController
@ -38,6 +40,7 @@ import stirling.software.common.util.WebResponseUtils;
public class SplitPdfBySizeController { public class SplitPdfBySizeController {
private final CustomPDFDocumentFactory pdfDocumentFactory; private final CustomPDFDocumentFactory pdfDocumentFactory;
private final TempFileManager tempFileManager;
@PostMapping(value = "/split-by-size-or-count", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) @PostMapping(value = "/split-by-size-or-count", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
@Operation( @Operation(
@ -54,89 +57,68 @@ public class SplitPdfBySizeController {
log.debug("Starting PDF split process with request: {}", request); log.debug("Starting PDF split process with request: {}", request);
MultipartFile file = request.getFileInput(); MultipartFile file = request.getFileInput();
Path zipFile = Files.createTempFile("split_documents", ".zip");
log.debug("Created temporary zip file: {}", zipFile);
String filename = String filename =
Filenames.toSimpleFileName(file.getOriginalFilename()) Filenames.toSimpleFileName(file.getOriginalFilename())
.replaceFirst("[.][^.]+$", ""); .replaceFirst("[.][^.]+$", "");
log.debug("Base filename for output: {}", filename); log.debug("Base filename for output: {}", filename);
byte[] data = null; try (TempFile zipTempFile = new TempFile(tempFileManager, ".zip")) {
try { Path zipFile = zipTempFile.getPath();
log.debug("Reading input file bytes"); log.debug("Created temporary zip file: {}", zipFile);
byte[] pdfBytes = file.getBytes(); try {
log.debug("Successfully read {} bytes from input file", pdfBytes.length); log.debug("Reading input file bytes");
byte[] pdfBytes = file.getBytes();
log.debug("Successfully read {} bytes from input file", pdfBytes.length);
log.debug("Creating ZIP output stream"); log.debug("Creating ZIP output stream");
try (ZipOutputStream zipOut = new ZipOutputStream(Files.newOutputStream(zipFile))) { try (ZipOutputStream zipOut = new ZipOutputStream(Files.newOutputStream(zipFile))) {
log.debug("Loading PDF document"); log.debug("Loading PDF document");
try (PDDocument sourceDocument = pdfDocumentFactory.load(pdfBytes)) { try (PDDocument sourceDocument = pdfDocumentFactory.load(pdfBytes)) {
log.debug( log.debug(
"Successfully loaded PDF with {} pages", "Successfully loaded PDF with {} pages",
sourceDocument.getNumberOfPages()); sourceDocument.getNumberOfPages());
int type = request.getSplitType(); int type = request.getSplitType();
String value = request.getSplitValue(); String value = request.getSplitValue();
log.debug("Split type: {}, Split value: {}", type, value); log.debug("Split type: {}, Split value: {}", type, value);
if (type == 0) { if (type == 0) {
log.debug("Processing split by size"); log.debug("Processing split by size");
long maxBytes = GeneralUtils.convertSizeToBytes(value); long maxBytes = GeneralUtils.convertSizeToBytes(value);
log.debug("Max bytes per document: {}", maxBytes); log.debug("Max bytes per document: {}", maxBytes);
handleSplitBySize(sourceDocument, maxBytes, zipOut, filename); handleSplitBySize(sourceDocument, maxBytes, zipOut, filename);
} else if (type == 1) { } else if (type == 1) {
log.debug("Processing split by page count"); log.debug("Processing split by page count");
int pageCount = Integer.parseInt(value); int pageCount = Integer.parseInt(value);
log.debug("Pages per document: {}", pageCount); log.debug("Pages per document: {}", pageCount);
handleSplitByPageCount(sourceDocument, pageCount, zipOut, filename); handleSplitByPageCount(sourceDocument, pageCount, zipOut, filename);
} else if (type == 2) { } else if (type == 2) {
log.debug("Processing split by document count"); log.debug("Processing split by document count");
int documentCount = Integer.parseInt(value); int documentCount = Integer.parseInt(value);
log.debug("Total number of documents: {}", documentCount); log.debug("Total number of documents: {}", documentCount);
handleSplitByDocCount(sourceDocument, documentCount, zipOut, filename); handleSplitByDocCount(sourceDocument, documentCount, zipOut, filename);
} else { } else {
log.error("Invalid split type: {}", type); log.error("Invalid split type: {}", type);
throw ExceptionUtils.createIllegalArgumentException( throw ExceptionUtils.createIllegalArgumentException(
"error.invalidArgument", "error.invalidArgument",
"Invalid argument: {0}", "Invalid argument: {0}",
"split type: " + type); "split type: " + type);
}
log.debug("PDF splitting completed successfully");
} }
log.debug("PDF splitting completed successfully");
} catch (Exception e) {
ExceptionUtils.logException("PDF document loading or processing", e);
throw e;
} }
} catch (IOException e) {
log.error("Error creating or writing to ZIP file", e);
throw e;
}
} catch (Exception e) { byte[] data = Files.readAllBytes(zipFile);
ExceptionUtils.logException("PDF splitting process", e);
throw e; // Re-throw to ensure proper error response
} finally {
try {
log.debug("Reading ZIP file data");
data = Files.readAllBytes(zipFile);
log.debug("Successfully read {} bytes from ZIP file", data.length); log.debug("Successfully read {} bytes from ZIP file", data.length);
} catch (IOException e) {
log.error("Error reading ZIP file data", e);
}
try { log.debug("Returning response with {} bytes of data", data.length);
log.debug("Deleting temporary ZIP file"); return WebResponseUtils.bytesToWebResponse(
boolean deleted = Files.deleteIfExists(zipFile); data, filename + ".zip", MediaType.APPLICATION_OCTET_STREAM);
log.debug("Temporary ZIP file deleted: {}", deleted); } catch (Exception e) {
} catch (IOException e) { ExceptionUtils.logException("PDF splitting process", e);
log.error("Error deleting temporary ZIP file", e); throw e; // Re-throw to ensure proper error response
} }
} }
log.debug("Returning response with {} bytes of data", data != null ? data.length : 0);
return WebResponseUtils.bytesToWebResponse(
data, filename + ".zip", MediaType.APPLICATION_OCTET_STREAM);
} }
private void handleSplitBySize( private void handleSplitBySize(

View File

@ -6,7 +6,6 @@ import java.awt.image.DataBufferInt;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.IOException; import java.io.IOException;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
@ -36,6 +35,8 @@ import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.model.api.misc.AutoSplitPdfRequest; import stirling.software.SPDF.model.api.misc.AutoSplitPdfRequest;
import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.service.CustomPDFDocumentFactory;
import stirling.software.common.util.TempFile;
import stirling.software.common.util.TempFileManager;
import stirling.software.common.util.WebResponseUtils; import stirling.software.common.util.WebResponseUtils;
@RestController @RestController
@ -53,6 +54,7 @@ public class AutoSplitPdfController {
"https://stirlingpdf.com")); "https://stirlingpdf.com"));
private final CustomPDFDocumentFactory pdfDocumentFactory; private final CustomPDFDocumentFactory pdfDocumentFactory;
private final TempFileManager tempFileManager;
private static String decodeQRCode(BufferedImage bufferedImage) { private static String decodeQRCode(BufferedImage bufferedImage) {
LuminanceSource source; LuminanceSource source;
@ -117,10 +119,10 @@ public class AutoSplitPdfController {
PDDocument document = null; PDDocument document = null;
List<PDDocument> splitDocuments = new ArrayList<>(); List<PDDocument> splitDocuments = new ArrayList<>();
Path zipFile = null; TempFile outputTempFile = null;
byte[] data = null;
try { try {
outputTempFile = new TempFile(tempFileManager, ".zip");
document = pdfDocumentFactory.load(file.getInputStream()); document = pdfDocumentFactory.load(file.getInputStream());
PDFRenderer pdfRenderer = new PDFRenderer(document); PDFRenderer pdfRenderer = new PDFRenderer(document);
pdfRenderer.setSubsamplingAllowed(true); pdfRenderer.setSubsamplingAllowed(true);
@ -152,12 +154,12 @@ public class AutoSplitPdfController {
// Remove split documents that have no pages // Remove split documents that have no pages
splitDocuments.removeIf(pdDocument -> pdDocument.getNumberOfPages() == 0); splitDocuments.removeIf(pdDocument -> pdDocument.getNumberOfPages() == 0);
zipFile = Files.createTempFile("split_documents", ".zip");
String filename = String filename =
Filenames.toSimpleFileName(file.getOriginalFilename()) Filenames.toSimpleFileName(file.getOriginalFilename())
.replaceFirst("[.][^.]+$", ""); .replaceFirst("[.][^.]+$", "");
try (ZipOutputStream zipOut = new ZipOutputStream(Files.newOutputStream(zipFile))) { try (ZipOutputStream zipOut =
new ZipOutputStream(Files.newOutputStream(outputTempFile.getPath()))) {
for (int i = 0; i < splitDocuments.size(); i++) { for (int i = 0; i < splitDocuments.size(); i++) {
String fileName = filename + "_" + (i + 1) + ".pdf"; String fileName = filename + "_" + (i + 1) + ".pdf";
PDDocument splitDocument = splitDocuments.get(i); PDDocument splitDocument = splitDocuments.get(i);
@ -173,10 +175,10 @@ public class AutoSplitPdfController {
} }
} }
data = Files.readAllBytes(zipFile); byte[] data = Files.readAllBytes(outputTempFile.getPath());
return WebResponseUtils.bytesToWebResponse( return WebResponseUtils.bytesToWebResponse(
data, filename + ".zip", MediaType.APPLICATION_OCTET_STREAM); data, filename + ".zip", MediaType.APPLICATION_OCTET_STREAM);
} catch (Exception e) { } catch (Exception e) {
log.error("Error in auto split", e); log.error("Error in auto split", e);
throw e; throw e;
@ -198,12 +200,8 @@ public class AutoSplitPdfController {
} }
} }
if (zipFile != null) { if (outputTempFile != null) {
try { outputTempFile.close();
Files.deleteIfExists(zipFile);
} catch (IOException e) {
log.error("Error deleting temporary zip file", e);
}
} }
} }
} }