mirror of
https://github.com/Frooodle/Stirling-PDF.git
synced 2025-09-08 17:51:20 +02:00
perf(core): Stream responses and unify temp file lifecycle across controllers (#4330)
This commit is contained in:
parent
9a39aff19f
commit
9b3e2c29a5
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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,11 +65,10 @@ 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(
|
||||||
@ -82,8 +83,8 @@ public class MergeController {
|
|||||||
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" ->
|
||||||
return (file1, file2) -> {
|
(file1, file2) -> {
|
||||||
try {
|
try {
|
||||||
BasicFileAttributes attr1 =
|
BasicFileAttributes attr1 =
|
||||||
Files.readAttributes(
|
Files.readAttributes(
|
||||||
@ -98,8 +99,8 @@ public class MergeController {
|
|||||||
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" ->
|
||||||
return (file1, file2) -> {
|
(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();
|
||||||
@ -109,10 +110,9 @@ public class MergeController {
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
case "orderProvided":
|
case "orderProvided" -> (file1, file2) -> 0; // Default is the order provided
|
||||||
default:
|
default -> (file1, file2) -> 0; // Default is the order provided
|
||||||
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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
|
@ -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(
|
||||||
|
@ -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,15 +57,14 @@ 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")) {
|
||||||
|
Path zipFile = zipTempFile.getPath();
|
||||||
|
log.debug("Created temporary zip file: {}", zipFile);
|
||||||
try {
|
try {
|
||||||
log.debug("Reading input file bytes");
|
log.debug("Reading input file bytes");
|
||||||
byte[] pdfBytes = file.getBytes();
|
byte[] pdfBytes = file.getBytes();
|
||||||
@ -102,41 +104,21 @@ public class SplitPdfBySizeController {
|
|||||||
"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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
byte[] data = Files.readAllBytes(zipFile);
|
||||||
|
log.debug("Successfully read {} bytes from ZIP file", data.length);
|
||||||
|
|
||||||
|
log.debug("Returning response with {} bytes of data", data.length);
|
||||||
|
return WebResponseUtils.bytesToWebResponse(
|
||||||
|
data, filename + ".zip", MediaType.APPLICATION_OCTET_STREAM);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
ExceptionUtils.logException("PDF splitting process", e);
|
ExceptionUtils.logException("PDF splitting process", e);
|
||||||
throw e; // Re-throw to ensure proper error response
|
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);
|
|
||||||
} catch (IOException e) {
|
|
||||||
log.error("Error reading ZIP file data", e);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
log.debug("Deleting temporary ZIP file");
|
|
||||||
boolean deleted = Files.deleteIfExists(zipFile);
|
|
||||||
log.debug("Temporary ZIP file deleted: {}", deleted);
|
|
||||||
} catch (IOException e) {
|
|
||||||
log.error("Error deleting temporary ZIP file", e);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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(
|
||||||
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user