Cleanup work + stream endpoints to reduce memory usage (#6106)

This commit is contained in:
Anthony Stirling
2026-04-15 15:34:17 +01:00
committed by GitHub
parent 702f4e5c2c
commit cc5a0b8def
116 changed files with 3005 additions and 1512 deletions
@@ -1,8 +1,10 @@
package stirling.software.common.service;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.UUID;
@@ -10,6 +12,7 @@ import java.util.UUID;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@@ -143,6 +146,24 @@ public class FileStorage {
return new StoredFile(fileId, size);
}
public String storeFromStreamingBody(StreamingResponseBody body, String originalName)
throws IOException {
String fileId = generateFileId();
Path filePath = getFilePath(fileId);
Files.createDirectories(filePath.getParent());
boolean success = false;
try (OutputStream os = new BufferedOutputStream(Files.newOutputStream(filePath))) {
body.writeTo(os);
success = true;
} finally {
if (!success) {
Files.deleteIfExists(filePath);
}
}
log.debug("Stored StreamingResponseBody with ID: {}", fileId);
return fileId;
}
/**
* Delete a file by its ID
*
@@ -16,6 +16,7 @@ import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;
import jakarta.servlet.http.HttpServletRequest;
@@ -305,33 +306,21 @@ public class JobExecutorService {
Object body = response.getBody();
if (body instanceof byte[]) {
// Extract filename from content-disposition header if available
String filename = "result.pdf";
String contentType = MediaType.APPLICATION_PDF_VALUE;
String filename = extractResponseFilename(response);
String contentType = extractResponseContentType(response);
if (response.getHeaders().getContentDisposition() != null) {
String disposition =
response.getHeaders().getContentDisposition().toString();
if (disposition.contains("filename=")) {
filename =
disposition.substring(
disposition.indexOf("filename=") + 9,
disposition.lastIndexOf('"'));
}
}
MediaType mediaType = response.getHeaders().getContentType();
if (mediaType != null) {
contentType = mediaType.toString();
}
// Store byte array directly to disk
String fileId = fileStorage.storeBytes((byte[]) body, filename);
taskManager.setFileResult(jobId, fileId, filename, contentType);
log.debug("Stored ResponseEntity<byte[]> result with fileId: {}", fileId);
} else if (body instanceof StreamingResponseBody streamingBody) {
String filename = extractResponseFilename(response);
String contentType = extractResponseContentType(response);
// Let the GC handle the memory naturally
String fileId = fileStorage.storeFromStreamingBody(streamingBody, filename);
taskManager.setFileResult(jobId, fileId, filename, contentType);
log.debug(
"Stored ResponseEntity<StreamingResponseBody> result with fileId: {}",
fileId);
} else {
// Check if the response body contains a fileId
if (body != null && body.toString().contains("fileId")) {
@@ -481,6 +470,21 @@ public class JobExecutorService {
}
}
private static String extractResponseFilename(ResponseEntity<?> response) {
if (response.getHeaders().getContentDisposition() != null) {
String filename = response.getHeaders().getContentDisposition().getFilename();
if (filename != null && !filename.isEmpty()) {
return filename;
}
}
return "result.pdf";
}
private static String extractResponseContentType(ResponseEntity<?> response) {
MediaType mediaType = response.getHeaders().getContentType();
return mediaType != null ? mediaType.toString() : MediaType.APPLICATION_PDF_VALUE;
}
/**
* Parse session timeout string (e.g., "30m", "1h") to milliseconds
*
@@ -401,7 +401,7 @@ public class JobQueue implements SmartLifecycle {
* @throws Exception If there is an execution error
*/
private <T> T executeWithTimeout(Supplier<T> supplier, long timeoutMs) throws Exception {
CompletableFuture<T> future = CompletableFuture.supplyAsync(supplier);
CompletableFuture<T> future = CompletableFuture.supplyAsync(supplier, jobExecutor);
try {
if (timeoutMs <= 0) {
@@ -7,11 +7,8 @@ import java.lang.management.MemoryMXBean;
import java.lang.management.OperatingSystemMXBean;
import java.lang.management.RuntimeMXBean;
import java.lang.management.ThreadMXBean;
import java.net.InetAddress;
import java.net.NetworkInterface;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
@@ -94,21 +91,12 @@ public class PostHogService {
metrics.put("os_name", System.getProperty("os.name"));
metrics.put("os_version", System.getProperty("os.version"));
metrics.put("java_version", System.getProperty("java.version"));
metrics.put("user_name", System.getProperty("user.name"));
metrics.put("user_home", System.getProperty("user.home"));
metrics.put("user_dir", System.getProperty("user.dir"));
// CPU and Memory
metrics.put("cpu_cores", Runtime.getRuntime().availableProcessors());
metrics.put("total_memory", Runtime.getRuntime().totalMemory());
metrics.put("free_memory", Runtime.getRuntime().freeMemory());
// Network and Server Identity
InetAddress localHost = InetAddress.getLocalHost();
metrics.put("ip_address", localHost.getHostAddress());
metrics.put("hostname", localHost.getHostName());
metrics.put("mac_address", getMacAddress());
// JVM info
metrics.put("jvm_vendor", System.getProperty("java.vendor"));
metrics.put("jvm_version", System.getProperty("java.vm.version"));
@@ -153,9 +141,6 @@ public class PostHogService {
metrics.put("gc_" + gcBean.getName() + "_time", gcBean.getCollectionTime());
}
// Network interfaces
metrics.put("network_interfaces", getNetworkInterfacesInfo());
// Docker detection and stats
boolean isDocker = isRunningInDocker();
if (isDocker) {
@@ -353,30 +338,6 @@ public class PostHogService {
.getProFeatures()
.getCustomMetadata()
.isAutoUpdateMetadata());
addIfNotEmpty(
properties,
"enterpriseEdition_customMetadata_author",
applicationProperties
.getPremium()
.getProFeatures()
.getCustomMetadata()
.getAuthor());
addIfNotEmpty(
properties,
"enterpriseEdition_customMetadata_creator",
applicationProperties
.getPremium()
.getProFeatures()
.getCustomMetadata()
.getCreator());
addIfNotEmpty(
properties,
"enterpriseEdition_customMetadata_producer",
applicationProperties
.getPremium()
.getProFeatures()
.getCustomMetadata()
.getProducer());
}
// Capture AutoPipeline properties
addIfNotEmpty(
@@ -386,39 +347,4 @@ public class PostHogService {
return properties;
}
private String getMacAddress() {
try {
Enumeration<NetworkInterface> networkInterfaces =
NetworkInterface.getNetworkInterfaces();
while (networkInterfaces.hasMoreElements()) {
NetworkInterface ni = networkInterfaces.nextElement();
byte[] hardwareAddress = ni.getHardwareAddress();
if (hardwareAddress != null) {
String[] hexadecimal = new String[hardwareAddress.length];
for (int i = 0; i < hardwareAddress.length; i++) {
hexadecimal[i] = String.format("%02X", hardwareAddress[i]);
}
return String.join("-", hexadecimal);
}
}
} catch (Exception e) {
// Handle exception
}
return "Unknown";
}
private Map<String, String> getNetworkInterfacesInfo() {
Map<String, String> interfacesInfo = new HashMap<>();
try {
Enumeration<NetworkInterface> nets = NetworkInterface.getNetworkInterfaces();
while (nets.hasMoreElements()) {
NetworkInterface netint = nets.nextElement();
interfacesInfo.put(netint.getName(), netint.getDisplayName());
}
} catch (Exception e) {
interfacesInfo.put("error", e.getMessage());
}
return interfacesInfo;
}
}
@@ -1,6 +1,5 @@
package stirling.software.common.util;
import java.io.ByteArrayInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.UncheckedIOException;
@@ -66,16 +65,7 @@ public class FileToPdf {
ProcessExecutor.getInstance(ProcessExecutor.Processes.WEASYPRINT)
.runCommandWithOutputHandling(command);
byte[] pdfBytes = Files.readAllBytes(tempOutputFile.getPath());
try {
return pdfBytes;
} catch (Exception e) {
pdfBytes = Files.readAllBytes(tempOutputFile.getPath());
if (pdfBytes.length < 1) {
throw e;
}
return pdfBytes;
}
return Files.readAllBytes(tempOutputFile.getPath());
} // tempInputFile auto-closed
} // tempOutputFile auto-closed
}
@@ -92,8 +82,7 @@ public class FileToPdf {
throws IOException {
try (TempDirectory tempUnzippedDir = new TempDirectory(tempFileManager)) {
try (ZipInputStream zipIn =
ZipSecurity.createHardenedInputStream(
new ByteArrayInputStream(Files.readAllBytes(zipFilePath)))) {
ZipSecurity.createHardenedInputStream(Files.newInputStream(zipFilePath))) {
ZipEntry entry = zipIn.getNextEntry();
while (entry != null) {
Path filePath =
@@ -1,9 +1,9 @@
package stirling.software.common.util;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
@@ -20,6 +20,7 @@ import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;
import com.vladsch.flexmark.html2md.converter.FlexmarkHtmlConverter;
import com.vladsch.flexmark.util.data.MutableDataSet;
@@ -48,7 +49,7 @@ public class PDFToFile {
this.runtimePathConfig = runtimePathConfig;
}
public ResponseEntity<byte[]> processPdfToMarkdown(MultipartFile inputFile)
public ResponseEntity<StreamingResponseBody> processPdfToMarkdown(MultipartFile inputFile)
throws IOException, InterruptedException {
if (!MediaType.APPLICATION_PDF_VALUE.equals(inputFile.getContentType())) {
return new ResponseEntity<>(HttpStatus.BAD_REQUEST);
@@ -85,78 +86,77 @@ public class PDFToFile {
pdfBaseName = originalPdfFileName.substring(0, originalPdfFileName.lastIndexOf('.'));
}
byte[] fileBytes;
String fileName;
String fileName = pdfBaseName + "ToMarkdown.zip";
TempFile finalOut = tempFileManager.createManagedTempFile(".zip");
try {
try (TempFile tempInputFile = new TempFile(tempFileManager, ".pdf");
TempDirectory tempOutputDir = new TempDirectory(tempFileManager)) {
inputFile.transferTo(tempInputFile.getFile());
try (TempFile tempInputFile = new TempFile(tempFileManager, ".pdf");
TempDirectory tempOutputDir = new TempDirectory(tempFileManager)) {
inputFile.transferTo(tempInputFile.getFile());
List<String> command =
new ArrayList<>(
Arrays.asList(
"pdftohtml",
"-s",
"-noframes",
"-c",
tempInputFile.getAbsolutePath(),
pdfBaseName));
List<String> command =
new ArrayList<>(
Arrays.asList(
"pdftohtml",
"-s",
"-noframes",
"-c",
tempInputFile.getAbsolutePath(),
pdfBaseName));
ProcessExecutorResult returnCode =
ProcessExecutor.getInstance(ProcessExecutor.Processes.PDFTOHTML)
.runCommandWithOutputHandling(
command, tempOutputDir.getPath().toFile());
// Process HTML files to Markdown
File[] outputFiles =
Objects.requireNonNull(tempOutputDir.getPath().toFile().listFiles());
List<File> markdownFiles = new ArrayList<>();
List<File> imageFiles = new ArrayList<>();
ProcessExecutorResult returnCode =
ProcessExecutor.getInstance(ProcessExecutor.Processes.PDFTOHTML)
.runCommandWithOutputHandling(
command, tempOutputDir.getPath().toFile());
// Process HTML files to Markdown
File[] outputFiles =
Objects.requireNonNull(tempOutputDir.getPath().toFile().listFiles());
List<File> markdownFiles = new ArrayList<>();
List<File> imageFiles = new ArrayList<>();
// Convert HTML files to Markdown and collect image files
for (File outputFile : outputFiles) {
if (outputFile.getName().endsWith(".html")) {
String html = Files.readString(outputFile.toPath());
String markdown = htmlToMarkdownConverter.convert(html);
// Convert HTML files to Markdown and collect image files
for (File outputFile : outputFiles) {
if (outputFile.getName().endsWith(".html")) {
String html = Files.readString(outputFile.toPath());
String markdown = htmlToMarkdownConverter.convert(html);
// Update image references to point to images/ folder
markdown = updateImageReferences(markdown);
// Update image references to point to images/ folder
markdown = updateImageReferences(markdown);
String mdFileName = outputFile.getName().replace(".html", ".md");
File mdFile = new File(tempOutputDir.getPath().toFile(), mdFileName);
Files.writeString(mdFile.toPath(), markdown);
markdownFiles.add(mdFile);
} else if (!outputFile.getName().endsWith(".md")) {
// Collect non-HTML, non-MD files as images/assets
imageFiles.add(outputFile);
}
}
String mdFileName = outputFile.getName().replace(".html", ".md");
File mdFile = new File(tempOutputDir.getPath().toFile(), mdFileName);
Files.writeString(mdFile.toPath(), markdown);
markdownFiles.add(mdFile);
} else if (!outputFile.getName().endsWith(".md")) {
// Collect non-HTML, non-MD files as images/assets
imageFiles.add(outputFile);
try (OutputStream fos = Files.newOutputStream(finalOut.getPath());
ZipOutputStream zipOutputStream = new ZipOutputStream(fos)) {
// Add markdown files to root of ZIP
for (File mdFile : markdownFiles) {
ZipEntry mdEntry = new ZipEntry(mdFile.getName());
zipOutputStream.putNextEntry(mdEntry);
Files.copy(mdFile.toPath(), zipOutputStream);
zipOutputStream.closeEntry();
}
// Add images and other assets to images/ folder
for (File imageFile : imageFiles) {
ZipEntry assetEntry = new ZipEntry("images/" + imageFile.getName());
zipOutputStream.putNextEntry(assetEntry);
Files.copy(imageFile.toPath(), zipOutputStream);
zipOutputStream.closeEntry();
}
}
}
// Always create a ZIP file
fileName = pdfBaseName + "ToMarkdown.zip";
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
try (ZipOutputStream zipOutputStream = new ZipOutputStream(byteArrayOutputStream)) {
// Add markdown files to root of ZIP
for (File mdFile : markdownFiles) {
ZipEntry mdEntry = new ZipEntry(mdFile.getName());
zipOutputStream.putNextEntry(mdEntry);
Files.copy(mdFile.toPath(), zipOutputStream);
zipOutputStream.closeEntry();
}
// Add images and other assets to images/ folder
for (File imageFile : imageFiles) {
ZipEntry assetEntry = new ZipEntry("images/" + imageFile.getName());
zipOutputStream.putNextEntry(assetEntry);
Files.copy(imageFile.toPath(), zipOutputStream);
zipOutputStream.closeEntry();
}
}
fileBytes = byteArrayOutputStream.toByteArray();
} catch (Exception e) {
finalOut.close();
throw e;
}
return WebResponseUtils.bytesToWebResponse(
fileBytes, fileName, MediaType.APPLICATION_OCTET_STREAM);
return WebResponseUtils.fileToWebResponse(
finalOut, fileName, MediaType.APPLICATION_OCTET_STREAM);
}
/**
@@ -169,7 +169,7 @@ public class PDFToFile {
return PATTERN.matcher(markdown).replaceAll("$1(images/$2)");
}
public ResponseEntity<byte[]> processPdfToHtml(MultipartFile inputFile)
public ResponseEntity<StreamingResponseBody> processPdfToHtml(MultipartFile inputFile)
throws IOException, InterruptedException {
if (!MediaType.APPLICATION_PDF_VALUE.equals(inputFile.getContentType())) {
return new ResponseEntity<>(HttpStatus.BAD_REQUEST);
@@ -182,56 +182,57 @@ public class PDFToFile {
pdfBaseName = originalPdfFileName.substring(0, originalPdfFileName.lastIndexOf('.'));
}
byte[] fileBytes;
String fileName;
String fileName = pdfBaseName + "ToHtml.zip";
TempFile finalOut = tempFileManager.createManagedTempFile(".zip");
try {
try (TempFile inputFileTemp = new TempFile(tempFileManager, ".pdf");
TempDirectory outputDirTemp = new TempDirectory(tempFileManager)) {
try (TempFile inputFileTemp = new TempFile(tempFileManager, ".pdf");
TempDirectory outputDirTemp = new TempDirectory(tempFileManager)) {
Path tempInputFile = inputFileTemp.getPath();
Path tempOutputDir = outputDirTemp.getPath();
Path tempInputFile = inputFileTemp.getPath();
Path tempOutputDir = outputDirTemp.getPath();
// Save the uploaded file to a temporary location
inputFile.transferTo(tempInputFile);
// Save the uploaded file to a temporary location
inputFile.transferTo(tempInputFile);
// Run the pdftohtml command with complex output
List<String> command =
new ArrayList<>(
Arrays.asList(
"pdftohtml", "-c", tempInputFile.toString(), pdfBaseName));
// Run the pdftohtml command with complex output
List<String> command =
new ArrayList<>(
Arrays.asList(
"pdftohtml", "-c", tempInputFile.toString(), pdfBaseName));
ProcessExecutorResult returnCode =
ProcessExecutor.getInstance(ProcessExecutor.Processes.PDFTOHTML)
.runCommandWithOutputHandling(command, tempOutputDir.toFile());
ProcessExecutorResult returnCode =
ProcessExecutor.getInstance(ProcessExecutor.Processes.PDFTOHTML)
.runCommandWithOutputHandling(command, tempOutputDir.toFile());
// Get output files
File[] outputFiles = Objects.requireNonNull(tempOutputDir.toFile().listFiles());
// Get output files
File[] outputFiles = Objects.requireNonNull(tempOutputDir.toFile().listFiles());
// Return output files in a ZIP archive
fileName = pdfBaseName + "ToHtml.zip";
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
try (ZipOutputStream zipOutputStream = new ZipOutputStream(byteArrayOutputStream)) {
for (File outputFile : outputFiles) {
ZipEntry entry = new ZipEntry(outputFile.getName());
zipOutputStream.putNextEntry(entry);
try (FileInputStream fis = new FileInputStream(outputFile)) {
IOUtils.copy(fis, zipOutputStream);
} catch (IOException e) {
log.error("Exception writing zip entry", e);
try (OutputStream fos = Files.newOutputStream(finalOut.getPath());
ZipOutputStream zipOutputStream = new ZipOutputStream(fos)) {
for (File outputFile : outputFiles) {
ZipEntry entry = new ZipEntry(outputFile.getName());
zipOutputStream.putNextEntry(entry);
try (FileInputStream fis = new FileInputStream(outputFile)) {
IOUtils.copy(fis, zipOutputStream);
} catch (IOException e) {
log.error("Exception writing zip entry", e);
}
zipOutputStream.closeEntry();
}
zipOutputStream.closeEntry();
} catch (IOException e) {
log.error("Exception writing zip", e);
}
} catch (IOException e) {
log.error("Exception writing zip", e);
}
fileBytes = byteArrayOutputStream.toByteArray();
} catch (Exception e) {
finalOut.close();
throw e;
}
return WebResponseUtils.bytesToWebResponse(
fileBytes, fileName, MediaType.APPLICATION_OCTET_STREAM);
return WebResponseUtils.fileToWebResponse(
finalOut, fileName, MediaType.APPLICATION_OCTET_STREAM);
}
public ResponseEntity<byte[]> processPdfToOfficeFormat(
public ResponseEntity<StreamingResponseBody> processPdfToOfficeFormat(
MultipartFile inputFile, String outputFormat, String libreOfficeFilter)
throws IOException, InterruptedException {
@@ -257,109 +258,115 @@ public class PDFToFile {
return new ResponseEntity<>(HttpStatus.BAD_REQUEST);
}
byte[] fileBytes;
String fileName;
TempFile finalOut =
tempFileManager.createManagedTempFile("." + resolvePrimaryExtension(outputFormat));
Path libreOfficeProfile = null;
try (TempFile inputFileTemp = new TempFile(tempFileManager, ".pdf");
TempDirectory outputDirTemp = new TempDirectory(tempFileManager)) {
try {
try (TempFile inputFileTemp = new TempFile(tempFileManager, ".pdf");
TempDirectory outputDirTemp = new TempDirectory(tempFileManager)) {
Path tempInputFile = inputFileTemp.getPath();
Path tempOutputDir = outputDirTemp.getPath();
Path unoOutputFile =
tempOutputDir.resolve(
pdfBaseName + "." + resolvePrimaryExtension(outputFormat));
Path tempInputFile = inputFileTemp.getPath();
Path tempOutputDir = outputDirTemp.getPath();
Path unoOutputFile =
tempOutputDir.resolve(
pdfBaseName + "." + resolvePrimaryExtension(outputFormat));
// Save the uploaded file to a temporary location
inputFile.transferTo(tempInputFile);
// Save the uploaded file to a temporary location
inputFile.transferTo(tempInputFile);
// Run the LibreOffice command
ProcessExecutorResult returnCode = null;
IOException unoconvertException = null;
// Run the LibreOffice command
ProcessExecutorResult returnCode = null;
IOException unoconvertException = null;
if (isUnoConvertEnabled()) {
try {
List<String> unoCommand =
buildUnoConvertCommand(
tempInputFile, unoOutputFile, outputFormat, libreOfficeFilter);
returnCode =
ProcessExecutor.getInstance(ProcessExecutor.Processes.LIBRE_OFFICE)
.runCommandWithOutputHandling(unoCommand);
} catch (IOException e) {
unoconvertException = e;
log.warn(
"Unoconvert command failed ({}). Falling back to soffice command.",
e.getMessage());
}
}
if (returnCode == null) {
// Run the LibreOffice command as a fallback
libreOfficeProfile = Files.createTempDirectory("libreoffice_profile_");
List<String> command = new ArrayList<>();
command.add(runtimePathConfig.getSOfficePath());
command.add("-env:UserInstallation=" + libreOfficeProfile.toUri().toString());
command.add("--headless");
command.add("--nologo");
command.add("--infilter=" + libreOfficeFilter);
command.add("--convert-to");
command.add(outputFormat);
command.add("--outdir");
command.add(tempOutputDir.toString());
command.add(tempInputFile.toString());
try {
returnCode =
ProcessExecutor.getInstance(ProcessExecutor.Processes.LIBRE_OFFICE)
.runCommandWithOutputHandling(command);
} catch (IOException e) {
if (unoconvertException != null) {
e.addSuppressed(unoconvertException);
if (isUnoConvertEnabled()) {
try {
List<String> unoCommand =
buildUnoConvertCommand(
tempInputFile,
unoOutputFile,
outputFormat,
libreOfficeFilter);
returnCode =
ProcessExecutor.getInstance(ProcessExecutor.Processes.LIBRE_OFFICE)
.runCommandWithOutputHandling(unoCommand);
} catch (IOException e) {
unoconvertException = e;
log.warn(
"Unoconvert command failed ({}). Falling back to soffice command.",
e.getMessage());
}
throw e;
}
}
// Get output files
List<File> outputFiles = Arrays.asList(tempOutputDir.toFile().listFiles());
if (returnCode == null) {
// Run the LibreOffice command as a fallback
libreOfficeProfile = Files.createTempDirectory("libreoffice_profile_");
List<String> command = new ArrayList<>();
command.add(runtimePathConfig.getSOfficePath());
command.add("-env:UserInstallation=" + libreOfficeProfile.toUri().toString());
command.add("--headless");
command.add("--nologo");
command.add("--infilter=" + libreOfficeFilter);
command.add("--convert-to");
command.add(outputFormat);
command.add("--outdir");
command.add(tempOutputDir.toString());
command.add(tempInputFile.toString());
if (outputFiles.size() == 1) {
// Return single output file
File outputFile = outputFiles.get(0);
if ("txt:Text".equals(outputFormat)) {
outputFormat = "txt";
}
fileName = pdfBaseName + "." + outputFormat;
fileBytes = FileUtils.readFileToByteArray(outputFile);
} else {
// Return output files in a ZIP archive
fileName = pdfBaseName + "To" + outputFormat + ".zip";
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
try (ZipOutputStream zipOutputStream = new ZipOutputStream(byteArrayOutputStream)) {
for (File outputFile : outputFiles) {
ZipEntry entry = new ZipEntry(outputFile.getName());
zipOutputStream.putNextEntry(entry);
try (FileInputStream fis = new FileInputStream(outputFile)) {
IOUtils.copy(fis, zipOutputStream);
} catch (IOException e) {
log.error("Exception writing zip entry", e);
try {
returnCode =
ProcessExecutor.getInstance(ProcessExecutor.Processes.LIBRE_OFFICE)
.runCommandWithOutputHandling(command);
} catch (IOException e) {
if (unoconvertException != null) {
e.addSuppressed(unoconvertException);
}
zipOutputStream.closeEntry();
throw e;
}
} catch (IOException e) {
log.error("Exception writing zip", e);
}
fileBytes = byteArrayOutputStream.toByteArray();
// Get output files
List<File> outputFiles = Arrays.asList(tempOutputDir.toFile().listFiles());
if (outputFiles.size() == 1) {
// Return single output file
File outputFile = outputFiles.get(0);
if ("txt:Text".equals(outputFormat)) {
outputFormat = "txt";
}
fileName = pdfBaseName + "." + outputFormat;
FileUtils.copyFile(outputFile, finalOut.getFile());
} else {
// Return output files in a ZIP archive
fileName = pdfBaseName + "To" + outputFormat + ".zip";
try (OutputStream fos = Files.newOutputStream(finalOut.getPath());
ZipOutputStream zipOutputStream = new ZipOutputStream(fos)) {
for (File outputFile : outputFiles) {
ZipEntry entry = new ZipEntry(outputFile.getName());
zipOutputStream.putNextEntry(entry);
try (FileInputStream fis = new FileInputStream(outputFile)) {
IOUtils.copy(fis, zipOutputStream);
} catch (IOException e) {
log.error("Exception writing zip entry", e);
}
zipOutputStream.closeEntry();
}
} catch (IOException e) {
log.error("Exception writing zip", e);
}
}
}
} catch (Exception e) {
finalOut.close();
throw e;
} finally {
if (libreOfficeProfile != null) {
FileUtils.deleteQuietly(libreOfficeProfile.toFile());
}
}
return WebResponseUtils.bytesToWebResponse(
fileBytes, fileName, MediaType.APPLICATION_OCTET_STREAM);
return WebResponseUtils.fileToWebResponse(
finalOut, fileName, MediaType.APPLICATION_OCTET_STREAM);
}
private boolean isUnoConvertEnabled() {
@@ -73,6 +73,19 @@ public class WebResponseUtils {
return baosToWebResponse(baos, docName);
}
public static ResponseEntity<StreamingResponseBody> pdfDocToWebResponse(
PDDocument document, String docName, TempFileManager tempFileManager)
throws IOException {
TempFile tempFile = tempFileManager.createManagedTempFile(".pdf");
try {
document.save(tempFile.getFile());
} catch (IOException e) {
tempFile.close();
throw e;
}
return pdfFileToWebResponse(tempFile, docName);
}
/**
* Convert a File to a web response (PDF default).
*
@@ -108,23 +121,37 @@ public class WebResponseUtils {
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 + "\"");
try {
Path path = outputTempFile.getFile().toPath().normalize();
long len = Files.size(path);
HttpHeaders headers = new HttpHeaders();
headers.setContentType(mediaType);
headers.setContentLength(len);
String encodedDocName =
RegexPatternUtils.getInstance()
.getPlusSignPattern()
.matcher(URLEncoder.encode(docName, StandardCharsets.UTF_8))
.replaceAll("%20");
headers.setContentDispositionFormData("attachment", encodedDocName);
StreamingResponseBody body =
os -> {
try (os) {
Files.copy(path, os);
os.flush();
} finally {
outputTempFile.close();
}
};
StreamingResponseBody body =
os -> {
try (os) {
Files.copy(path, os);
os.flush();
} finally {
outputTempFile.close();
}
};
return new ResponseEntity<>(body, headers, HttpStatus.OK);
return new ResponseEntity<>(body, headers, HttpStatus.OK);
} catch (IOException | RuntimeException e) {
try {
outputTempFile.close();
} catch (Exception closeEx) {
e.addSuppressed(closeEx);
}
throw e;
}
}
}
@@ -9,6 +9,7 @@ import static org.mockito.Mockito.lenient;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.when;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
@@ -29,6 +30,7 @@ import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.mock.web.MockMultipartFile;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;
import io.github.pixee.security.ZipSecurity;
@@ -59,6 +61,19 @@ class PDFToFileTest {
.thenAnswer(
invocation ->
Files.createTempFile("test", invocation.getArgument(0)).toFile());
lenient()
.when(mockTempFileManager.createManagedTempFile(anyString()))
.thenAnswer(
invocation -> {
File f =
Files.createTempFile("test", invocation.<String>getArgument(0))
.toFile();
TempFile tf = org.mockito.Mockito.mock(TempFile.class);
lenient().when(tf.getFile()).thenReturn(f);
lenient().when(tf.getPath()).thenReturn(f.toPath());
lenient().when(tf.getAbsolutePath()).thenReturn(f.getAbsolutePath());
return tf;
});
lenient()
.when(mockTempFileManager.createTempDirectory())
.thenAnswer(invocation -> Files.createTempDirectory("test"));
@@ -68,6 +83,12 @@ class PDFToFileTest {
pdfToFile = new PDFToFile(mockTempFileManager, mockRuntimePathConfig);
}
private static byte[] drain(ResponseEntity<StreamingResponseBody> response) throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
response.getBody().writeTo(baos);
return baos.toByteArray();
}
@Test
void testProcessPdfToMarkdown_InvalidContentType() throws IOException, InterruptedException {
// Prepare
@@ -79,7 +100,7 @@ class PDFToFileTest {
"This is not a PDF".getBytes());
// Execute
ResponseEntity<byte[]> response = pdfToFile.processPdfToMarkdown(nonPdfFile);
ResponseEntity<StreamingResponseBody> response = pdfToFile.processPdfToMarkdown(nonPdfFile);
// Verify
assertEquals(HttpStatus.BAD_REQUEST, response.getStatusCode());
@@ -96,7 +117,7 @@ class PDFToFileTest {
"This is not a PDF".getBytes());
// Execute
ResponseEntity<byte[]> response = pdfToFile.processPdfToHtml(nonPdfFile);
ResponseEntity<StreamingResponseBody> response = pdfToFile.processPdfToHtml(nonPdfFile);
// Verify
assertEquals(HttpStatus.BAD_REQUEST, response.getStatusCode());
@@ -114,7 +135,7 @@ class PDFToFileTest {
"This is not a PDF".getBytes());
// Execute
ResponseEntity<byte[]> response =
ResponseEntity<StreamingResponseBody> response =
pdfToFile.processPdfToOfficeFormat(nonPdfFile, "docx", "draw_pdf_import");
// Verify
@@ -133,7 +154,7 @@ class PDFToFileTest {
"Fake PDF content".getBytes());
// Execute with invalid format
ResponseEntity<byte[]> response =
ResponseEntity<StreamingResponseBody> response =
pdfToFile.processPdfToOfficeFormat(pdfFile, "invalid_format", "draw_pdf_import");
// Verify
@@ -184,12 +205,14 @@ class PDFToFileTest {
});
// Execute the method
ResponseEntity<byte[]> response = pdfToFile.processPdfToMarkdown(pdfFile);
ResponseEntity<StreamingResponseBody> response =
pdfToFile.processPdfToMarkdown(pdfFile);
// Verify - should now return a ZIP file instead of plain markdown
assertEquals(HttpStatus.OK, response.getStatusCode());
assertNotNull(response.getBody());
assertTrue(response.getBody().length > 0);
byte[] bodyBytes = drain(response);
assertNotNull(bodyBytes);
assertTrue(bodyBytes.length > 0);
// Verify content disposition indicates a ZIP file
assertTrue(
@@ -201,7 +224,7 @@ class PDFToFileTest {
// Verify the content by unzipping it
try (ZipInputStream zipStream =
ZipSecurity.createHardenedInputStream(
new java.io.ByteArrayInputStream(response.getBody()))) {
new java.io.ByteArrayInputStream(bodyBytes))) {
ZipEntry entry;
boolean foundMdFile = false;
boolean foundImageInFolder = false;
@@ -275,12 +298,14 @@ class PDFToFileTest {
});
// Execute the method
ResponseEntity<byte[]> response = pdfToFile.processPdfToMarkdown(pdfFile);
ResponseEntity<StreamingResponseBody> response =
pdfToFile.processPdfToMarkdown(pdfFile);
// Verify
assertEquals(HttpStatus.OK, response.getStatusCode());
assertNotNull(response.getBody());
assertTrue(response.getBody().length > 0);
byte[] bodyBytes = drain(response);
assertNotNull(bodyBytes);
assertTrue(bodyBytes.length > 0);
// Verify content disposition indicates a zip file
assertTrue(
@@ -292,7 +317,7 @@ class PDFToFileTest {
// Verify the content by unzipping it
try (ZipInputStream zipStream =
ZipSecurity.createHardenedInputStream(
new java.io.ByteArrayInputStream(response.getBody()))) {
new java.io.ByteArrayInputStream(bodyBytes))) {
ZipEntry entry;
boolean foundMdFiles = false;
boolean foundImage = false;
@@ -352,12 +377,13 @@ class PDFToFileTest {
});
// Execute the method
ResponseEntity<byte[]> response = pdfToFile.processPdfToHtml(pdfFile);
ResponseEntity<StreamingResponseBody> response = pdfToFile.processPdfToHtml(pdfFile);
// Verify
assertEquals(HttpStatus.OK, response.getStatusCode());
assertNotNull(response.getBody());
assertTrue(response.getBody().length > 0);
byte[] bodyBytes = drain(response);
assertNotNull(bodyBytes);
assertTrue(bodyBytes.length > 0);
// Verify content disposition indicates a zip file
assertTrue(
@@ -369,7 +395,7 @@ class PDFToFileTest {
// Verify the content by unzipping it
try (ZipInputStream zipStream =
ZipSecurity.createHardenedInputStream(
new java.io.ByteArrayInputStream(response.getBody()))) {
new java.io.ByteArrayInputStream(bodyBytes))) {
ZipEntry entry;
boolean foundMainHtml = false;
boolean foundIndexHtml = false;
@@ -437,13 +463,14 @@ class PDFToFileTest {
});
// Execute the method with docx format
ResponseEntity<byte[]> response =
ResponseEntity<StreamingResponseBody> response =
pdfToFile.processPdfToOfficeFormat(pdfFile, "docx", "draw_pdf_import");
// Verify
assertEquals(HttpStatus.OK, response.getStatusCode());
assertNotNull(response.getBody());
assertTrue(response.getBody().length > 0);
byte[] bodyBytes = drain(response);
assertNotNull(bodyBytes);
assertTrue(bodyBytes.length > 0);
// Verify content disposition has correct filename
assertTrue(
@@ -508,13 +535,14 @@ class PDFToFileTest {
});
// Execute the method with ODP format
ResponseEntity<byte[]> response =
ResponseEntity<StreamingResponseBody> response =
pdfToFile.processPdfToOfficeFormat(pdfFile, "odp", "draw_pdf_import");
// Verify
assertEquals(HttpStatus.OK, response.getStatusCode());
assertNotNull(response.getBody());
assertTrue(response.getBody().length > 0);
byte[] bodyBytes = drain(response);
assertNotNull(bodyBytes);
assertTrue(bodyBytes.length > 0);
// Verify content disposition for zip file
assertTrue(
@@ -526,7 +554,7 @@ class PDFToFileTest {
// Verify the content by unzipping it
try (ZipInputStream zipStream =
ZipSecurity.createHardenedInputStream(
new java.io.ByteArrayInputStream(response.getBody()))) {
new java.io.ByteArrayInputStream(bodyBytes))) {
ZipEntry entry;
boolean foundMainFile = false;
boolean foundMediaFiles = false;
@@ -592,13 +620,14 @@ class PDFToFileTest {
});
// Execute the method with text format
ResponseEntity<byte[]> response =
ResponseEntity<StreamingResponseBody> response =
pdfToFile.processPdfToOfficeFormat(pdfFile, "txt:Text", "draw_pdf_import");
// Verify
assertEquals(HttpStatus.OK, response.getStatusCode());
assertNotNull(response.getBody());
assertTrue(response.getBody().length > 0);
byte[] bodyBytes = drain(response);
assertNotNull(bodyBytes);
assertTrue(bodyBytes.length > 0);
// Verify content disposition has txt extension
assertTrue(
@@ -650,13 +679,14 @@ class PDFToFileTest {
});
// Execute the method
ResponseEntity<byte[]> response =
ResponseEntity<StreamingResponseBody> response =
pdfToFile.processPdfToOfficeFormat(pdfFile, "docx", "draw_pdf_import");
// Verify
assertEquals(HttpStatus.OK, response.getStatusCode());
assertNotNull(response.getBody());
assertTrue(response.getBody().length > 0);
byte[] bodyBytes = drain(response);
assertNotNull(bodyBytes);
assertTrue(bodyBytes.length > 0);
// Verify content disposition contains output.docx
assertTrue(
@@ -696,12 +726,13 @@ class PDFToFileTest {
return mockExecutorResult;
});
ResponseEntity<byte[]> response =
ResponseEntity<StreamingResponseBody> response =
pdfToFileWithUno.processPdfToOfficeFormat(pdfFile, "docx", "writer_pdf_import");
assertEquals(HttpStatus.OK, response.getStatusCode());
assertNotNull(response.getBody());
assertTrue(response.getBody().length > 0);
byte[] bodyBytes = drain(response);
assertNotNull(bodyBytes);
assertTrue(bodyBytes.length > 0);
assertTrue(
response.getHeaders()
.getContentDisposition()
@@ -759,12 +790,13 @@ class PDFToFileTest {
return mockExecutorResult;
});
ResponseEntity<byte[]> response =
ResponseEntity<StreamingResponseBody> response =
pdfToFileWithUno.processPdfToOfficeFormat(pdfFile, "docx", "writer_pdf_import");
assertEquals(HttpStatus.OK, response.getStatusCode());
assertNotNull(response.getBody());
assertTrue(response.getBody().length > 0);
byte[] bodyBytes = drain(response);
assertNotNull(bodyBytes);
assertTrue(bodyBytes.length > 0);
assertTrue(
response.getHeaders()
.getContentDisposition()
@@ -47,7 +47,7 @@ public class OpenApiConfig {
.version(version)
.license(
new License()
.name("MIT")
.name("Open-Core - MIT Licensed")
.url(
"https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/refs/heads/main/LICENSE"))
.termsOfService("https://www.stirlingpdf.com/terms")
@@ -28,7 +28,17 @@ public class SpringDocConfig {
"/api/v1/proprietary/ui-data/**",
"/api/v1/info/**",
"/api/v1/general/job/**",
"/api/v1/general/files/**")
"/api/v1/general/files/**",
"/api/v1/general/signatures/**",
"/api/v1/database/**",
"/api/v1/storage/**",
"/api/v1/proprietary/signatures/**",
"/api/v1/workflow/participant/**",
"/api/v1/security/cert-sign/sessions",
"/api/v1/security/cert-sign/sessions/**",
"/api/v1/security/cert-sign/sign-requests",
"/api/v1/security/cert-sign/sign-requests/**",
"/api/v1/security/cert-sign/validate-certificate")
.addOpenApiCustomizer(pdfFileOneOfCustomizer)
.addOpenApiCustomizer(
openApi -> {
@@ -53,7 +63,16 @@ public class SpringDocConfig {
"/api/v1/team/**",
"/api/v1/auth/**",
"/api/v1/invite/**",
"/api/v1/audit/**")
"/api/v1/audit/**",
"/api/v1/database/**",
"/api/v1/storage/**",
"/api/v1/proprietary/signatures/**",
"/api/v1/workflow/participant/**",
"/api/v1/security/cert-sign/sessions",
"/api/v1/security/cert-sign/sessions/**",
"/api/v1/security/cert-sign/sign-requests",
"/api/v1/security/cert-sign/sign-requests/**",
"/api/v1/security/cert-sign/validate-certificate")
.addOpenApiCustomizer(
openApi -> {
openApi.info(
@@ -75,7 +94,8 @@ public class SpringDocConfig {
"/api/v1/proprietary/ui-data/**",
"/api/v1/info/**",
"/api/v1/general/job/**",
"/api/v1/general/files/**")
"/api/v1/general/files/**",
"/api/v1/general/signatures/**")
.addOpenApiCustomizer(
openApi -> {
openApi.info(
@@ -1,7 +1,6 @@
package stirling.software.SPDF.config;
import java.lang.management.ManagementFactory;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
@@ -106,25 +105,27 @@ public class TauriProcessMonitor {
logger.info("Orphaned Java backend detected. Shutting down gracefully...");
// Shutdown asynchronously to avoid blocking the monitor thread
CompletableFuture.runAsync(
() -> {
try {
// Give a small delay to ensure logging completes
Thread.sleep(1000);
Thread.ofVirtual()
.name("tauri-graceful-shutdown")
.start(
() -> {
try {
// Give a small delay to ensure logging completes
Thread.sleep(1000);
if (applicationContext instanceof ConfigurableApplicationContext) {
((ConfigurableApplicationContext) applicationContext).close();
} else {
// Fallback to system exit
logger.warn(
"Unable to shutdown Spring context gracefully, using System.exit");
System.exit(0);
}
} catch (Exception e) {
logger.error("Error during graceful shutdown", e);
System.exit(1);
}
});
if (applicationContext instanceof ConfigurableApplicationContext) {
((ConfigurableApplicationContext) applicationContext).close();
} else {
// Fallback to system exit
logger.warn(
"Unable to shutdown Spring context gracefully, using System.exit");
System.exit(0);
}
} catch (Exception e) {
logger.error("Error during graceful shutdown", e);
System.exit(1);
}
});
}
@PreDestroy
@@ -1,7 +1,6 @@
package stirling.software.SPDF.controller.api;
import java.awt.*;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
@@ -19,6 +18,7 @@ import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;
import io.github.pixee.security.Filenames;
import io.swagger.v3.oas.annotations.Operation;
@@ -30,6 +30,7 @@ import stirling.software.SPDF.model.api.general.BookletImpositionRequest;
import stirling.software.common.annotations.AutoJobPostMapping;
import stirling.software.common.service.CustomPDFDocumentFactory;
import stirling.software.common.util.GeneralUtils;
import stirling.software.common.util.TempFileManager;
import stirling.software.common.util.WebResponseUtils;
@RestController
@@ -39,6 +40,7 @@ import stirling.software.common.util.WebResponseUtils;
public class BookletImpositionController {
private final CustomPDFDocumentFactory pdfDocumentFactory;
private final TempFileManager tempFileManager;
@AutoJobPostMapping(
value = "/booklet-imposition",
@@ -49,7 +51,7 @@ public class BookletImpositionController {
"This operation combines page reordering for booklet printing with multi-page layout. "
+ "It rearranges pages in the correct order for booklet printing and places multiple pages "
+ "on each sheet for proper folding and binding. Input:PDF Output:PDF Type:SISO")
public ResponseEntity<byte[]> createBookletImposition(
public ResponseEntity<StreamingResponseBody> createBookletImposition(
@ModelAttribute BookletImpositionRequest request) throws IOException {
MultipartFile file = request.getFileInput();
@@ -85,15 +87,12 @@ public class BookletImpositionController {
duplexPass,
flipOnShortEdge)) {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
newDocument.save(baos);
byte[] result = baos.toByteArray();
return WebResponseUtils.bytesToWebResponse(
result,
return WebResponseUtils.pdfDocToWebResponse(
newDocument,
GeneralUtils.generateFilename(
Filenames.toSimpleFileName(file.getOriginalFilename()),
"_booklet.pdf"));
"_booklet.pdf"),
tempFileManager);
}
}
}
@@ -1,10 +1,7 @@
package stirling.software.SPDF.controller.api;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import org.apache.pdfbox.multipdf.LayerUtility;
@@ -18,6 +15,7 @@ import org.apache.pdfbox.rendering.PDFRenderer;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;
import io.swagger.v3.oas.annotations.Operation;
@@ -32,6 +30,8 @@ import stirling.software.common.service.CustomPDFDocumentFactory;
import stirling.software.common.util.ExceptionUtils;
import stirling.software.common.util.GeneralUtils;
import stirling.software.common.util.ProcessExecutor;
import stirling.software.common.util.TempFile;
import stirling.software.common.util.TempFileManager;
import stirling.software.common.util.WebResponseUtils;
@GeneralApi
@@ -46,6 +46,7 @@ public class CropController {
private static final String PDF_EXTENSION = ".pdf";
private final CustomPDFDocumentFactory pdfDocumentFactory;
private final TempFileManager tempFileManager;
private static int[] detectContentBounds(BufferedImage image) {
int width = image.getWidth();
@@ -131,7 +132,8 @@ public class CropController {
description =
"This operation takes an input PDF file and crops it according to the given"
+ " coordinates. Input:PDF Output:PDF Type:SISO")
public ResponseEntity<byte[]> cropPdf(@ModelAttribute CropPdfForm request) throws IOException {
public ResponseEntity<StreamingResponseBody> cropPdf(@ModelAttribute CropPdfForm request)
throws IOException {
if (request.isAutoCrop()) {
return cropWithAutomaticDetection(request);
}
@@ -151,8 +153,8 @@ public class CropController {
}
}
private ResponseEntity<byte[]> cropWithAutomaticDetection(@ModelAttribute CropPdfForm request)
throws IOException {
private ResponseEntity<StreamingResponseBody> cropWithAutomaticDetection(
@ModelAttribute CropPdfForm request) throws IOException {
try (PDDocument sourceDocument = pdfDocumentFactory.load(request)) {
try (PDDocument newDocument =
@@ -196,20 +198,17 @@ public class CropController {
cropBounds.height));
}
ByteArrayOutputStream baos = new ByteArrayOutputStream();
newDocument.save(baos);
byte[] pdfContent = baos.toByteArray();
return WebResponseUtils.bytesToWebResponse(
pdfContent,
return WebResponseUtils.pdfDocToWebResponse(
newDocument,
GeneralUtils.generateFilename(
request.getFileInput().getOriginalFilename(), "_cropped.pdf"));
request.getFileInput().getOriginalFilename(), "_cropped.pdf"),
tempFileManager);
}
}
}
private ResponseEntity<byte[]> cropWithPDFBox(@ModelAttribute CropPdfForm request)
throws IOException {
private ResponseEntity<StreamingResponseBody> cropWithPDFBox(
@ModelAttribute CropPdfForm request) throws IOException {
try (PDDocument sourceDocument = pdfDocumentFactory.load(request)) {
try (PDDocument newDocument =
@@ -255,22 +254,19 @@ public class CropController {
request.getHeight()));
}
ByteArrayOutputStream baos = new ByteArrayOutputStream();
newDocument.save(baos);
byte[] pdfContent = baos.toByteArray();
return WebResponseUtils.bytesToWebResponse(
pdfContent,
return WebResponseUtils.pdfDocToWebResponse(
newDocument,
GeneralUtils.generateFilename(
request.getFileInput().getOriginalFilename(), "_cropped.pdf"));
request.getFileInput().getOriginalFilename(), "_cropped.pdf"),
tempFileManager);
}
}
}
private ResponseEntity<byte[]> cropWithGhostscript(@ModelAttribute CropPdfForm request)
throws IOException {
Path tempInputFile = null;
Path tempOutputFile = null;
private ResponseEntity<StreamingResponseBody> cropWithGhostscript(
@ModelAttribute CropPdfForm request) throws IOException {
TempFile tempInputFile = null;
TempFile tempOutputFile = null;
try (PDDocument sourceDocument = pdfDocumentFactory.load(request)) {
for (int i = 0; i < sourceDocument.getNumberOfPages(); i++) {
@@ -284,11 +280,11 @@ public class CropController {
page.setCropBox(cropBox);
}
tempInputFile = Files.createTempFile(TEMP_INPUT_PREFIX, PDF_EXTENSION);
tempOutputFile = Files.createTempFile(TEMP_OUTPUT_PREFIX, PDF_EXTENSION);
tempInputFile = tempFileManager.createManagedTempFile(PDF_EXTENSION);
tempOutputFile = tempFileManager.createManagedTempFile(PDF_EXTENSION);
// Save the source document with crop boxes
sourceDocument.save(tempInputFile.toFile());
sourceDocument.save(tempInputFile.getFile());
// Execute Ghostscript to process the crop boxes
ProcessExecutor processExecutor =
@@ -299,15 +295,15 @@ public class CropController {
"-sDEVICE=pdfwrite",
"-dUseCropBox",
"-o",
tempOutputFile.toString(),
tempInputFile.toString());
tempOutputFile.getAbsolutePath(),
tempInputFile.getAbsolutePath());
processExecutor.runCommandWithOutputHandling(command);
byte[] pdfContent = Files.readAllBytes(tempOutputFile);
return WebResponseUtils.bytesToWebResponse(
pdfContent,
TempFile out = tempOutputFile;
tempOutputFile = null; // ownership transferred to StreamingResponseBody
return WebResponseUtils.pdfFileToWebResponse(
out,
GeneralUtils.generateFilename(
request.getFileInput().getOriginalFilename(), "_cropped.pdf"));
@@ -316,10 +312,10 @@ public class CropController {
throw ExceptionUtils.createProcessingInterruptedException("Ghostscript", e);
} finally {
if (tempInputFile != null) {
Files.deleteIfExists(tempInputFile);
tempInputFile.close();
}
if (tempOutputFile != null) {
Files.deleteIfExists(tempOutputFile);
tempOutputFile.close();
}
}
}
@@ -1,6 +1,5 @@
package stirling.software.SPDF.controller.api;
import java.io.ByteArrayOutputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
@@ -14,6 +13,7 @@ import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;
import io.swagger.v3.oas.annotations.Operation;
@@ -27,6 +27,7 @@ import stirling.software.common.annotations.AutoJobPostMapping;
import stirling.software.common.annotations.api.GeneralApi;
import stirling.software.common.service.CustomPDFDocumentFactory;
import stirling.software.common.util.GeneralUtils;
import stirling.software.common.util.TempFileManager;
import stirling.software.common.util.WebResponseUtils;
import tools.jackson.core.type.TypeReference;
@@ -39,6 +40,7 @@ public class EditTableOfContentsController {
private final CustomPDFDocumentFactory pdfDocumentFactory;
private final ObjectMapper objectMapper;
private final TempFileManager tempFileManager;
@AutoJobPostMapping(
value = "/extract-bookmarks",
@@ -149,12 +151,11 @@ public class EditTableOfContentsController {
@Operation(
summary = "Edit Table of Contents",
description = "Add or edit bookmarks/table of contents in a PDF document.")
public ResponseEntity<byte[]> editTableOfContents(
public ResponseEntity<StreamingResponseBody> editTableOfContents(
@ModelAttribute EditTableOfContentsRequest request) throws Exception {
MultipartFile file = request.getFileInput();
try (PDDocument document = pdfDocumentFactory.load(file);
ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
try (PDDocument document = pdfDocumentFactory.load(file)) {
// Parse the bookmark data from JSON
List<BookmarkItem> bookmarks =
@@ -168,13 +169,10 @@ public class EditTableOfContentsController {
// Add bookmarks to the outline
addBookmarksToOutline(document, outline, bookmarks);
// Save the document to a byte array
document.save(baos);
return WebResponseUtils.bytesToWebResponse(
baos.toByteArray(),
return WebResponseUtils.pdfDocToWebResponse(
document,
GeneralUtils.generateFilename(file.getOriginalFilename(), "_with_toc.pdf"),
MediaType.APPLICATION_PDF);
tempFileManager);
}
}
@@ -3,7 +3,6 @@ package stirling.software.SPDF.controller.api;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
@@ -29,6 +28,7 @@ import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;
import io.swagger.v3.oas.annotations.Operation;
@@ -279,7 +279,7 @@ public class MergeController {
"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"
+ " provided. Input:PDF Output:PDF Type:MISO")
public ResponseEntity<byte[]> mergePdfs(
public ResponseEntity<StreamingResponseBody> mergePdfs(
@ModelAttribute MergePdfsRequest request,
@RequestParam(value = "fileOrder", required = false) String fileOrder)
throws IOException {
@@ -399,12 +399,6 @@ public class MergeController {
String mergedFileName =
GeneralUtils.generateFilename(firstFilename, "_merged_unsigned.pdf");
byte[] pdfBytes;
try {
pdfBytes = Files.readAllBytes(outputTempFile.getPath());
} finally {
outputTempFile.close();
}
return WebResponseUtils.bytesToWebResponse(pdfBytes, mergedFileName);
return WebResponseUtils.pdfFileToWebResponse(outputTempFile, mergedFileName);
}
}
@@ -1,7 +1,6 @@
package stirling.software.SPDF.controller.api;
import java.awt.Color;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import org.apache.pdfbox.multipdf.LayerUtility;
@@ -15,6 +14,7 @@ import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;
import io.swagger.v3.oas.annotations.Operation;
@@ -28,6 +28,7 @@ import stirling.software.common.service.CustomPDFDocumentFactory;
import stirling.software.common.util.ExceptionUtils;
import stirling.software.common.util.GeneralFormCopyUtils;
import stirling.software.common.util.GeneralUtils;
import stirling.software.common.util.TempFileManager;
import stirling.software.common.util.WebResponseUtils;
@GeneralApi
@@ -36,6 +37,7 @@ import stirling.software.common.util.WebResponseUtils;
public class MultiPageLayoutController {
private final CustomPDFDocumentFactory pdfDocumentFactory;
private final TempFileManager tempFileManager;
@AutoJobPostMapping(
value = "/multi-page-layout",
@@ -45,7 +47,7 @@ public class MultiPageLayoutController {
description =
"This operation takes an input PDF file and the number of pages to merge into a"
+ " single sheet in the output PDF file. Input:PDF Output:PDF Type:SISO")
public ResponseEntity<byte[]> mergeMultiplePagesIntoOne(
public ResponseEntity<StreamingResponseBody> mergeMultiplePagesIntoOne(
@ModelAttribute MergeMultiplePagesRequest request) throws IOException {
int MAX_PAGES = 100000;
@@ -338,13 +340,11 @@ public class MultiPageLayoutController {
}
}
ByteArrayOutputStream baos = new ByteArrayOutputStream();
newDocument.save(baos);
byte[] result = baos.toByteArray();
return WebResponseUtils.bytesToWebResponse(
result,
return WebResponseUtils.pdfDocToWebResponse(
newDocument,
GeneralUtils.generateFilename(
file.getOriginalFilename(), "_multi_page_layout.pdf"));
file.getOriginalFilename(), "_multi_page_layout.pdf"),
tempFileManager);
} // newDocument is closed here
} // sourceDocument is closed here
}
@@ -1,6 +1,5 @@
package stirling.software.SPDF.controller.api;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
@@ -16,6 +15,7 @@ import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;
import io.swagger.v3.oas.annotations.Operation;
@@ -28,6 +28,8 @@ import stirling.software.common.annotations.api.GeneralApi;
import stirling.software.common.service.CustomPDFDocumentFactory;
import stirling.software.common.util.ExceptionUtils;
import stirling.software.common.util.GeneralUtils;
import stirling.software.common.util.TempFile;
import stirling.software.common.util.TempFileManager;
import stirling.software.common.util.WebResponseUtils;
@GeneralApi
@@ -35,6 +37,7 @@ import stirling.software.common.util.WebResponseUtils;
public class PdfOverlayController {
private final CustomPDFDocumentFactory pdfDocumentFactory;
private final TempFileManager tempFileManager;
@AutoJobPostMapping(value = "/overlay-pdfs", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
@StandardPdfResponse
@@ -43,8 +46,8 @@ public class PdfOverlayController {
description =
"Overlay PDF files onto a base PDF with different modes: Sequential,"
+ " Interleaved, or Fixed Repeat. Input:PDF Output:PDF Type:MIMO")
public ResponseEntity<byte[]> overlayPdfs(@ModelAttribute OverlayPdfsRequest request)
throws IOException {
public ResponseEntity<StreamingResponseBody> overlayPdfs(
@ModelAttribute OverlayPdfsRequest request) throws IOException {
MultipartFile baseFile = request.getFileInput();
int overlayPos = request.getOverlayPosition();
@@ -52,6 +55,7 @@ public class PdfOverlayController {
File[] overlayPdfFiles = new File[overlayFiles.length];
List<File> tempFiles = new ArrayList<>(); // List to keep track of temporary files
TempFile tempOut = null;
try {
for (int i = 0; i < overlayFiles.length; i++) {
overlayPdfFiles[i] = GeneralUtils.multipartToFile(overlayFiles[i]);
@@ -62,8 +66,7 @@ public class PdfOverlayController {
int[] counts = request.getCounts(); // Used for FixedRepeatOverlay mode
try (PDDocument basePdf = pdfDocumentFactory.load(baseFile);
Overlay overlay = new Overlay();
ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
Overlay overlay = new Overlay()) {
Map<Integer, String> overlayGuide =
prepareOverlayGuide(
basePdf.getNumberOfPages(),
@@ -79,15 +82,21 @@ public class PdfOverlayController {
overlay.setOverlayPosition(Overlay.Position.BACKGROUND);
}
overlay.overlay(overlayGuide).save(outputStream);
byte[] data = outputStream.toByteArray();
tempOut = tempFileManager.createManagedTempFile(".pdf");
overlay.overlay(overlayGuide).save(tempOut.getFile());
String outputFilename =
GeneralUtils.generateFilename(
baseFile.getOriginalFilename(), "_overlayed.pdf");
return WebResponseUtils.bytesToWebResponse(
data, outputFilename, MediaType.APPLICATION_PDF);
TempFile out = tempOut;
tempOut = null; // ownership transferred to StreamingResponseBody
return WebResponseUtils.pdfFileToWebResponse(out, outputFilename);
}
} catch (Exception e) {
if (tempOut != null) {
tempOut.close();
}
throw e;
} finally {
for (File overlayPdfFile : overlayPdfFiles) {
if (overlayPdfFile != null) {
@@ -12,6 +12,7 @@ import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;
import io.swagger.v3.oas.annotations.Operation;
@@ -27,6 +28,7 @@ import stirling.software.common.annotations.api.GeneralApi;
import stirling.software.common.service.CustomPDFDocumentFactory;
import stirling.software.common.util.ExceptionUtils;
import stirling.software.common.util.GeneralUtils;
import stirling.software.common.util.TempFileManager;
import stirling.software.common.util.WebResponseUtils;
@GeneralApi
@@ -35,6 +37,7 @@ import stirling.software.common.util.WebResponseUtils;
public class RearrangePagesPDFController {
private final CustomPDFDocumentFactory pdfDocumentFactory;
private final TempFileManager tempFileManager;
@AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/remove-pages")
@StandardPdfResponse
@@ -44,8 +47,8 @@ public class RearrangePagesPDFController {
"This endpoint removes specified pages from a given PDF file. Users can provide"
+ " a comma-separated list of page numbers or ranges to delete. Input:PDF"
+ " Output:PDF Type:SISO")
public ResponseEntity<byte[]> deletePages(@ModelAttribute PDFWithPageNums request)
throws IOException {
public ResponseEntity<StreamingResponseBody> deletePages(
@ModelAttribute PDFWithPageNums request) throws IOException {
MultipartFile pdfFile = request.getFileInput();
String pagesToDelete = request.getPageNumbers();
@@ -67,7 +70,8 @@ public class RearrangePagesPDFController {
return WebResponseUtils.pdfDocToWebResponse(
document,
GeneralUtils.generateFilename(
pdfFile.getOriginalFilename(), "_removed_pages.pdf"));
pdfFile.getOriginalFilename(), "_removed_pages.pdf"),
tempFileManager);
}
}
@@ -224,8 +228,8 @@ public class RearrangePagesPDFController {
+ " order or custom mode. Users can provide a page order as a"
+ " comma-separated list of page numbers or page ranges, or a custom mode."
+ " Input:PDF Output:PDF")
public ResponseEntity<byte[]> rearrangePages(@ModelAttribute RearrangePagesRequest request)
throws IOException {
public ResponseEntity<StreamingResponseBody> rearrangePages(
@ModelAttribute RearrangePagesRequest request) throws IOException {
MultipartFile pdfFile = request.getFileInput();
String pageOrder = request.getPageNumbers();
String sortType = request.getCustomMode();
@@ -264,7 +268,8 @@ public class RearrangePagesPDFController {
return WebResponseUtils.pdfDocToWebResponse(
rearrangedDocument,
GeneralUtils.generateFilename(
pdfFile.getOriginalFilename(), "_rearranged.pdf"));
pdfFile.getOriginalFilename(), "_rearranged.pdf"),
tempFileManager);
}
}
} catch (IOException e) {
@@ -9,6 +9,7 @@ import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;
import io.swagger.v3.oas.annotations.Operation;
@@ -21,6 +22,7 @@ import stirling.software.common.annotations.api.GeneralApi;
import stirling.software.common.service.CustomPDFDocumentFactory;
import stirling.software.common.util.ExceptionUtils;
import stirling.software.common.util.GeneralUtils;
import stirling.software.common.util.TempFileManager;
import stirling.software.common.util.WebResponseUtils;
@GeneralApi
@@ -28,6 +30,7 @@ import stirling.software.common.util.WebResponseUtils;
public class RotationController {
private final CustomPDFDocumentFactory pdfDocumentFactory;
private final TempFileManager tempFileManager;
@AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/rotate-pdf")
@StandardPdfResponse
@@ -36,7 +39,7 @@ public class RotationController {
description =
"This endpoint rotates a given PDF file by a specified angle. The angle must be"
+ " a multiple of 90. Input:PDF Output:PDF Type:SISO")
public ResponseEntity<byte[]> rotatePDF(@ModelAttribute RotatePDFRequest request)
public ResponseEntity<StreamingResponseBody> rotatePDF(@ModelAttribute RotatePDFRequest request)
throws IOException {
MultipartFile pdfFile = request.getFileInput();
Integer angle = request.getAngle();
@@ -60,7 +63,8 @@ public class RotationController {
// Return the rotated PDF as a response
return WebResponseUtils.pdfDocToWebResponse(
document,
GeneralUtils.generateFilename(pdfFile.getOriginalFilename(), "_rotated.pdf"));
GeneralUtils.generateFilename(pdfFile.getOriginalFilename(), "_rotated.pdf"),
tempFileManager);
}
}
}
@@ -1,6 +1,5 @@
package stirling.software.SPDF.controller.api;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
@@ -16,6 +15,7 @@ import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;
import io.swagger.v3.oas.annotations.Operation;
@@ -28,6 +28,7 @@ import stirling.software.common.annotations.api.GeneralApi;
import stirling.software.common.service.CustomPDFDocumentFactory;
import stirling.software.common.util.ExceptionUtils;
import stirling.software.common.util.GeneralUtils;
import stirling.software.common.util.TempFileManager;
import stirling.software.common.util.WebResponseUtils;
@GeneralApi
@@ -36,6 +37,7 @@ import stirling.software.common.util.WebResponseUtils;
public class ScalePagesController {
private final CustomPDFDocumentFactory pdfDocumentFactory;
private final TempFileManager tempFileManager;
private static PDRectangle getTargetSize(String targetPDRectangle, PDDocument sourceDocument) {
if ("KEEP".equals(targetPDRectangle)) {
@@ -118,16 +120,15 @@ public class ScalePagesController {
description =
"This operation takes an input PDF file and the size to scale the pages to in"
+ " the output PDF file. Input:PDF Output:PDF Type:SISO")
public ResponseEntity<byte[]> scalePages(@ModelAttribute ScalePagesRequest request)
throws IOException {
public ResponseEntity<StreamingResponseBody> scalePages(
@ModelAttribute ScalePagesRequest request) throws IOException {
MultipartFile file = request.getFileInput();
String targetPDRectangle = request.getPageSize();
float scaleFactor = request.getScaleFactor();
try (PDDocument sourceDocument = pdfDocumentFactory.load(file);
PDDocument outputDocument =
pdfDocumentFactory.createNewDocumentBasedOnOldDocument(sourceDocument);
ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
pdfDocumentFactory.createNewDocumentBasedOnOldDocument(sourceDocument)) {
PDRectangle targetSize = getTargetSize(targetPDRectangle, sourceDocument);
@@ -168,11 +169,10 @@ public class ScalePagesController {
}
}
outputDocument.save(baos);
return WebResponseUtils.bytesToWebResponse(
baos.toByteArray(),
GeneralUtils.generateFilename(file.getOriginalFilename(), "_scaled.pdf"));
return WebResponseUtils.pdfDocToWebResponse(
outputDocument,
GeneralUtils.generateFilename(file.getOriginalFilename(), "_scaled.pdf"),
tempFileManager);
}
}
}
@@ -1,6 +1,5 @@
package stirling.software.SPDF.controller.api;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.file.Files;
import java.util.*;
@@ -20,6 +19,7 @@ import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;
import io.swagger.v3.oas.annotations.Operation;
@@ -59,8 +59,8 @@ public class SplitPdfBySectionsController {
+ " which page to split, and how to split"
+ " ( halves, thirds, quarters, etc.), both vertically and horizontally."
+ " Input:PDF Output:ZIP-PDF Type:SISO")
public ResponseEntity<byte[]> splitPdf(@Valid @ModelAttribute SplitPdfBySectionsRequest request)
throws Exception {
public ResponseEntity<StreamingResponseBody> splitPdf(
@Valid @ModelAttribute SplitPdfBySectionsRequest request) throws Exception {
MultipartFile file = request.getFileInput();
String pageNumbers = request.getPageNumbers();
SplitTypes splitMode =
@@ -80,9 +80,7 @@ public class SplitPdfBySectionsController {
if (merge) {
try (PDDocument mergedDoc =
pdfDocumentFactory.createNewDocumentBasedOnOldDocument(
sourceDocument);
ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
pdfDocumentFactory.createNewDocumentBasedOnOldDocument(sourceDocument)) {
LayerUtility layerUtility = new LayerUtility(mergedDoc);
for (int pageIndex = 0;
pageIndex < sourceDocument.getNumberOfPages();
@@ -99,11 +97,12 @@ public class SplitPdfBySectionsController {
addPageToTarget(sourceDocument, pageIndex, mergedDoc, layerUtility);
}
}
mergedDoc.save(baos);
return WebResponseUtils.baosToWebResponse(baos, filename + ".pdf");
return WebResponseUtils.pdfDocToWebResponse(
mergedDoc, filename + ".pdf", tempFileManager);
}
} else {
try (TempFile zipTempFile = new TempFile(tempFileManager, ".zip")) {
TempFile zipTempFile = tempFileManager.createManagedTempFile(".zip");
try {
try (ZipOutputStream zipOut =
new ZipOutputStream(Files.newOutputStream(zipTempFile.getPath()))) {
for (int pageIndex = 0;
@@ -161,9 +160,10 @@ public class SplitPdfBySectionsController {
log.error("Error creating ZIP file with split PDF sections", e);
throw e;
}
byte[] zipBytes = Files.readAllBytes(zipTempFile.getPath());
return WebResponseUtils.bytesToWebResponse(
zipBytes, filename + ".zip", MediaType.APPLICATION_OCTET_STREAM);
return WebResponseUtils.zipFileToWebResponse(zipTempFile, filename + ".zip");
} catch (Exception ex) {
zipTempFile.close();
throw ex;
}
}
} catch (Exception e) {
@@ -1,7 +1,6 @@
package stirling.software.SPDF.controller.api;
import java.awt.geom.AffineTransform;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import org.apache.pdfbox.multipdf.LayerUtility;
@@ -12,6 +11,7 @@ import org.apache.pdfbox.pdmodel.graphics.form.PDFormXObject;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;
import io.swagger.v3.oas.annotations.Operation;
@@ -23,6 +23,7 @@ import stirling.software.common.annotations.api.GeneralApi;
import stirling.software.common.model.api.PDFFile;
import stirling.software.common.service.CustomPDFDocumentFactory;
import stirling.software.common.util.GeneralUtils;
import stirling.software.common.util.TempFileManager;
import stirling.software.common.util.WebResponseUtils;
@GeneralApi
@@ -30,6 +31,7 @@ import stirling.software.common.util.WebResponseUtils;
public class ToSinglePageController {
private final CustomPDFDocumentFactory pdfDocumentFactory;
private final TempFileManager tempFileManager;
@AutoJobPostMapping(
consumes = MediaType.MULTIPART_FORM_DATA_VALUE,
@@ -42,7 +44,7 @@ public class ToSinglePageController {
+ " document. The width of the single page will be same as the input's"
+ " width, but the height will be the sum of all the pages' heights."
+ " Input:PDF Output:PDF Type:SISO")
public ResponseEntity<byte[]> pdfToSinglePage(@ModelAttribute PDFFile request)
public ResponseEntity<StreamingResponseBody> pdfToSinglePage(@ModelAttribute PDFFile request)
throws IOException {
// Load the source document
@@ -85,14 +87,11 @@ public class ToSinglePageController {
pageIndex++;
}
ByteArrayOutputStream baos = new ByteArrayOutputStream();
newDocument.save(baos);
byte[] result = baos.toByteArray();
return WebResponseUtils.bytesToWebResponse(
result,
return WebResponseUtils.pdfDocToWebResponse(
newDocument,
GeneralUtils.generateFilename(
request.getFileInput().getOriginalFilename(), "_singlePage.pdf"));
request.getFileInput().getOriginalFilename(), "_singlePage.pdf"),
tempFileManager);
}
}
}
@@ -95,9 +95,8 @@ public class UIDataController {
Resource resource = new ClassPathResource("static/3rdPartyLicenses.json");
try (InputStream is = resource.getInputStream()) {
String json = new String(is.readAllBytes(), StandardCharsets.UTF_8);
Map<String, List<Dependency>> licenseData =
objectMapper.readValue(json, new TypeReference<>() {});
objectMapper.readValue(is, new TypeReference<>() {});
data.setDependencies(licenseData.get("dependencies"));
} catch (IOException e) {
log.error("Failed to load licenses data", e);
@@ -16,6 +16,7 @@ import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;
import io.github.pixee.security.Filenames;
import io.swagger.v3.oas.annotations.Operation;
@@ -31,6 +32,7 @@ import stirling.software.common.service.CustomPDFDocumentFactory;
import stirling.software.common.util.GeneralUtils;
import stirling.software.common.util.ProcessExecutor;
import stirling.software.common.util.ProcessExecutor.ProcessExecutorResult;
import stirling.software.common.util.TempFile;
import stirling.software.common.util.TempFileManager;
import stirling.software.common.util.WebResponseUtils;
@@ -60,7 +62,7 @@ public class ConvertEbookToPDFController {
description =
"This endpoint converts common eBook formats (EPUB, MOBI, AZW3, FB2, TXT, DOCX)"
+ " to PDF using Calibre. Input:BOOK Output:PDF Type:SISO")
public ResponseEntity<byte[]> convertEbookToPdf(
public ResponseEntity<StreamingResponseBody> convertEbookToPdf(
@ModelAttribute ConvertEbookToPdfRequest request) throws Exception {
if (!isCalibreEnabled()) {
throw new IllegalStateException("Calibre support is disabled");
@@ -140,24 +142,35 @@ public class ConvertEbookToPDFController {
String outputFilename =
GeneralUtils.generateFilename(originalFilename, "_convertedToPDF.pdf");
TempFile tempOut = null;
try {
tempOut = tempFileManager.createManagedTempFile(".pdf");
if (optimizeForEbook) {
byte[] pdfBytes = Files.readAllBytes(outputPath);
try {
byte[] optimizedPdf = GeneralUtils.optimizePdfWithGhostscript(pdfBytes);
return WebResponseUtils.bytesToWebResponse(optimizedPdf, outputFilename);
Files.write(tempOut.getPath(), optimizedPdf);
} catch (IOException e) {
log.warn(
"Ghostscript optimization failed for ebook conversion, returning"
+ " original PDF",
e);
return WebResponseUtils.bytesToWebResponse(pdfBytes, outputFilename);
Files.write(tempOut.getPath(), pdfBytes);
}
} else {
try (PDDocument document = pdfDocumentFactory.load(outputPath.toFile())) {
document.save(tempOut.getFile());
}
}
try (PDDocument document = pdfDocumentFactory.load(outputPath.toFile())) {
return WebResponseUtils.pdfDocToWebResponse(document, outputFilename);
ResponseEntity<StreamingResponseBody> response =
WebResponseUtils.pdfFileToWebResponse(tempOut, outputFilename);
tempOut = null;
return response;
} catch (Exception e) {
if (tempOut != null) {
tempOut.close();
}
throw e;
} finally {
cleanupTempFiles(workingDirectory, inputPath, outputPath);
}
@@ -2,6 +2,7 @@ package stirling.software.SPDF.controller.api.converters;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.Locale;
import org.jetbrains.annotations.NotNull;
@@ -10,6 +11,7 @@ import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;
import org.springframework.web.util.HtmlUtils;
import io.github.pixee.security.Filenames;
@@ -26,6 +28,7 @@ import stirling.software.common.model.api.converters.EmlToPdfRequest;
import stirling.software.common.service.CustomPDFDocumentFactory;
import stirling.software.common.util.CustomHtmlSanitizer;
import stirling.software.common.util.EmlToPdf;
import stirling.software.common.util.TempFile;
import stirling.software.common.util.TempFileManager;
import stirling.software.common.util.WebResponseUtils;
@@ -48,7 +51,8 @@ public class ConvertEmlToPDF {
+ " with extensive customization options. Features include font settings,"
+ " image constraints, display modes, attachment handling, and HTML debug"
+ " output. Input: EML or MSG file, Output: PDF or HTML file. Type: SISO")
public ResponseEntity<byte[]> convertEmlToPdf(@ModelAttribute EmlToPdfRequest request) {
public ResponseEntity<StreamingResponseBody> convertEmlToPdf(
@ModelAttribute EmlToPdfRequest request) {
MultipartFile inputFile = request.getFileInput();
String originalFilename = inputFile.getOriginalFilename();
@@ -56,22 +60,19 @@ public class ConvertEmlToPDF {
// Validate input
if (inputFile.isEmpty()) {
log.error("No file provided for EML/MSG to PDF conversion.");
return ResponseEntity.badRequest()
.body("No file provided".getBytes(StandardCharsets.UTF_8));
return errorResponse(HttpStatus.BAD_REQUEST, "No file provided");
}
if (originalFilename == null || originalFilename.trim().isEmpty()) {
log.error("Filename is null or empty.");
return ResponseEntity.badRequest()
.body("Please provide a valid filename".getBytes(StandardCharsets.UTF_8));
return errorResponse(HttpStatus.BAD_REQUEST, "Please provide a valid filename");
}
// Validate file type - support EML and MSG (Outlook) files
String lowerFilename = originalFilename.toLowerCase(Locale.ROOT);
if (!lowerFilename.endsWith(".eml") && !lowerFilename.endsWith(".msg")) {
log.error("Invalid file type for EML/MSG to PDF: {}", originalFilename);
return ResponseEntity.badRequest()
.body("Please upload a valid EML or MSG file".getBytes(StandardCharsets.UTF_8));
return errorResponse(HttpStatus.BAD_REQUEST, "Please upload a valid EML or MSG file");
}
String baseFilename = Filenames.toSimpleFileName(originalFilename); // Use Filenames utility
@@ -84,16 +85,20 @@ public class ConvertEmlToPDF {
String htmlContent =
EmlToPdf.convertEmlToHtml(fileBytes, request, customHtmlSanitizer);
log.info("Successfully converted email to HTML: {}", originalFilename);
return WebResponseUtils.bytesToWebResponse(
htmlContent.getBytes(StandardCharsets.UTF_8),
baseFilename + ".html",
MediaType.TEXT_HTML);
TempFile tempOut = tempFileManager.createManagedTempFile(".html");
try {
Files.writeString(tempOut.getPath(), htmlContent, StandardCharsets.UTF_8);
} catch (Exception ex) {
tempOut.close();
throw ex;
}
return WebResponseUtils.fileToWebResponse(
tempOut, baseFilename + ".html", MediaType.TEXT_HTML);
} catch (IOException | IllegalArgumentException e) {
log.error("HTML conversion failed for {}", originalFilename, e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(
("HTML conversion failed: " + e.getMessage())
.getBytes(StandardCharsets.UTF_8));
return errorResponse(
HttpStatus.INTERNAL_SERVER_ERROR,
"HTML conversion failed: " + e.getMessage());
}
}
@@ -111,20 +116,25 @@ public class ConvertEmlToPDF {
if (pdfBytes == null || pdfBytes.length == 0) {
log.error("PDF conversion failed - empty output for {}", originalFilename);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(
"PDF conversion failed - empty output"
.getBytes(StandardCharsets.UTF_8));
return errorResponse(
HttpStatus.INTERNAL_SERVER_ERROR,
"PDF conversion failed - empty output");
}
log.info("Successfully converted email to PDF: {}", originalFilename);
return WebResponseUtils.bytesToWebResponse(
pdfBytes, baseFilename + ".pdf", MediaType.APPLICATION_PDF);
TempFile tempOut = tempFileManager.createManagedTempFile(".pdf");
try {
Files.write(tempOut.getPath(), pdfBytes);
} catch (Exception ex) {
tempOut.close();
throw ex;
}
return WebResponseUtils.pdfFileToWebResponse(tempOut, baseFilename + ".pdf");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
log.error("Email to PDF conversion was interrupted for {}", originalFilename, e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body("Conversion was interrupted".getBytes(StandardCharsets.UTF_8));
return errorResponse(
HttpStatus.INTERNAL_SERVER_ERROR, "Conversion was interrupted");
} catch (IllegalArgumentException e) {
String errorMessage = buildErrorMessage(e, originalFilename);
log.error(
@@ -132,8 +142,7 @@ public class ConvertEmlToPDF {
originalFilename,
errorMessage,
e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(errorMessage.getBytes(StandardCharsets.UTF_8));
return errorResponse(HttpStatus.INTERNAL_SERVER_ERROR, errorMessage);
} catch (RuntimeException e) {
String errorMessage = buildErrorMessage(e, originalFilename);
log.error(
@@ -141,17 +150,25 @@ public class ConvertEmlToPDF {
originalFilename,
errorMessage,
e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(errorMessage.getBytes(StandardCharsets.UTF_8));
return errorResponse(HttpStatus.INTERNAL_SERVER_ERROR, errorMessage);
}
} catch (IOException e) {
log.error("File processing error for email to PDF: {}", originalFilename, e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body("File processing error".getBytes(StandardCharsets.UTF_8));
return errorResponse(HttpStatus.INTERNAL_SERVER_ERROR, "File processing error");
}
}
private ResponseEntity<StreamingResponseBody> errorResponse(HttpStatus status, String message) {
byte[] body = message.getBytes(StandardCharsets.UTF_8);
StreamingResponseBody streaming =
os -> {
os.write(body);
os.flush();
};
return ResponseEntity.status(status).body(streaming);
}
private static @NotNull String buildErrorMessage(Exception e, String originalFilename) {
String safeFilename = HtmlUtils.htmlEscape(originalFilename);
String exceptionMessage = e.getMessage();
@@ -1,9 +1,12 @@
package stirling.software.SPDF.controller.api.converters;
import java.nio.file.Files;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;
import io.github.pixee.security.Filenames;
import io.swagger.v3.oas.annotations.Operation;
@@ -37,7 +40,7 @@ public class ConvertHtmlToPDF {
description =
"This endpoint takes an HTML or ZIP file input and converts it to a PDF format."
+ " Input:HTML Output:PDF Type:SISO")
public ResponseEntity<byte[]> HtmlToPdf(@ModelAttribute HTMLToPdfRequest request)
public ResponseEntity<StreamingResponseBody> HtmlToPdf(@ModelAttribute HTMLToPdfRequest request)
throws Exception {
MultipartFile fileInput = request.getFileInput();
@@ -65,6 +68,13 @@ public class ConvertHtmlToPDF {
String outputFilename = GeneralUtils.generateFilename(originalFilename, ".pdf");
return WebResponseUtils.bytesToWebResponse(pdfBytes, outputFilename);
TempFile tempOut = tempFileManager.createManagedTempFile(".pdf");
try {
Files.write(tempOut.getPath(), pdfBytes);
} catch (Exception e) {
tempOut.close();
throw e;
}
return WebResponseUtils.pdfFileToWebResponse(tempOut, outputFilename);
}
}
@@ -1,5 +1,6 @@
package stirling.software.SPDF.controller.api.converters;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Map;
@@ -14,6 +15,7 @@ import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;
import io.github.pixee.security.Filenames;
import io.swagger.v3.oas.annotations.Operation;
@@ -46,8 +48,8 @@ public class ConvertMarkdownToPdf {
description =
"This endpoint takes a Markdown file or ZIP (containing Markdown + images) input, converts it to HTML, and then to"
+ " PDF format. Input:MARKDOWN Output:PDF Type:SISO")
public ResponseEntity<byte[]> markdownToPdf(@ModelAttribute GeneralFile generalFile)
throws Exception {
public ResponseEntity<StreamingResponseBody> markdownToPdf(
@ModelAttribute GeneralFile generalFile) throws Exception {
MultipartFile fileInput = generalFile.getFileInput();
if (fileInput == null) {
@@ -79,7 +81,7 @@ public class ConvertMarkdownToPdf {
java.nio.file.Path tempDirPath = tempDir.getPath();
try (java.util.zip.ZipInputStream zipIn =
io.github.pixee.security.ZipSecurity.createHardenedInputStream(
new java.io.ByteArrayInputStream(fileInput.getBytes()))) {
fileInput.getInputStream())) {
java.util.zip.ZipEntry entry;
while ((entry = zipIn.getNextEntry()) != null) {
if (!entry.isDirectory()) {
@@ -141,7 +143,7 @@ public class ConvertMarkdownToPdf {
List<Extension> extensions = List.of(TablesExtension.create());
Parser parser = Parser.builder().extensions(extensions).build();
Node document = parser.parse(new String(fileInput.getBytes()));
Node document = parser.parse(new String(fileInput.getBytes(), StandardCharsets.UTF_8));
HtmlRenderer renderer =
HtmlRenderer.builder()
.attributeProviderFactory(context -> new TableAttributeProvider())
@@ -154,7 +156,7 @@ public class ConvertMarkdownToPdf {
FileToPdf.convertHtmlToPdf(
runtimePathConfig.getWeasyPrintPath(),
null,
htmlContent.getBytes(),
htmlContent.getBytes(StandardCharsets.UTF_8),
"converted.html",
tempFileManager,
customHtmlSanitizer);
@@ -163,7 +165,15 @@ public class ConvertMarkdownToPdf {
}
pdfBytes = pdfDocumentFactory.createNewBytesBasedOnOldDocument(pdfBytes);
return WebResponseUtils.bytesToWebResponse(pdfBytes, outputFilename);
TempFile tempOut = tempFileManager.createManagedTempFile(".pdf");
try {
java.nio.file.Files.write(tempOut.getPath(), pdfBytes);
} catch (Exception e) {
tempOut.close();
throw e;
}
return WebResponseUtils.pdfFileToWebResponse(tempOut, outputFilename);
}
/**
@@ -17,6 +17,7 @@ import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;
import io.github.pixee.security.Filenames;
import io.swagger.v3.oas.annotations.Operation;
@@ -36,6 +37,8 @@ import stirling.software.common.util.GeneralUtils;
import stirling.software.common.util.ProcessExecutor;
import stirling.software.common.util.ProcessExecutor.ProcessExecutorResult;
import stirling.software.common.util.RegexPatternUtils;
import stirling.software.common.util.TempFile;
import stirling.software.common.util.TempFileManager;
import stirling.software.common.util.WebResponseUtils;
@ConvertApi
@@ -47,6 +50,7 @@ public class ConvertOfficeController {
private final RuntimePathConfig runtimePathConfig;
private final CustomHtmlSanitizer customHtmlSanitizer;
private final EndpointConfiguration endpointConfiguration;
private final TempFileManager tempFileManager;
private boolean isUnoconvertAvailable() {
return endpointConfiguration.isGroupEnabled("Unoconvert")
@@ -202,21 +206,32 @@ public class ConvertOfficeController {
description =
"This endpoint converts a given file to a PDF using LibreOffice API Input:ANY"
+ " Output:PDF Type:SISO")
public ResponseEntity<byte[]> processFileToPDF(@ModelAttribute GeneralFile generalFile)
throws Exception {
public ResponseEntity<StreamingResponseBody> processFileToPDF(
@ModelAttribute GeneralFile generalFile) throws Exception {
MultipartFile inputFile = generalFile.getFileInput();
// unused but can start server instance if startup time is to long
// LibreOfficeListener.getInstance().start();
File file = null;
TempFile tempOut = null;
try {
file = convertToPdf(inputFile);
tempOut = tempFileManager.createManagedTempFile(".pdf");
try (PDDocument doc = pdfDocumentFactory.load(file)) {
return WebResponseUtils.pdfDocToWebResponse(
doc,
GeneralUtils.generateFilename(
inputFile.getOriginalFilename(), "_convertedToPDF.pdf"));
doc.save(tempOut.getFile());
}
String filename =
GeneralUtils.generateFilename(
inputFile.getOriginalFilename(), "_convertedToPDF.pdf");
ResponseEntity<StreamingResponseBody> response =
WebResponseUtils.pdfFileToWebResponse(tempOut, filename);
tempOut = null;
return response;
} catch (Exception e) {
if (tempOut != null) {
tempOut.close();
}
throw e;
} finally {
if (file != null && file.getParent() != null) {
FileUtils.deleteDirectory(file.getParentFile());
@@ -13,6 +13,7 @@ import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;
import io.github.pixee.security.Filenames;
import io.swagger.v3.oas.annotations.Operation;
@@ -29,6 +30,7 @@ import stirling.software.common.annotations.api.ConvertApi;
import stirling.software.common.util.GeneralUtils;
import stirling.software.common.util.ProcessExecutor;
import stirling.software.common.util.ProcessExecutor.ProcessExecutorResult;
import stirling.software.common.util.TempFile;
import stirling.software.common.util.TempFileManager;
import stirling.software.common.util.WebResponseUtils;
@@ -85,8 +87,8 @@ public class ConvertPDFToEpubController {
description =
"Convert a PDF file to a high-quality EPUB or AZW3 ebook using Calibre. Input:PDF"
+ " Output:EPUB/AZW3 Type:SISO")
public ResponseEntity<byte[]> convertPdfToEpub(@ModelAttribute ConvertPdfToEpubRequest request)
throws Exception {
public ResponseEntity<StreamingResponseBody> convertPdfToEpub(
@ModelAttribute ConvertPdfToEpubRequest request) throws Exception {
if (!endpointConfiguration.isGroupEnabled(CALIBRE_GROUP)) {
throw new IllegalStateException(
@@ -170,9 +172,16 @@ public class ConvertPDFToEpubController {
+ "."
+ outputFormat.getExtension());
byte[] outputBytes = Files.readAllBytes(outputPath);
MediaType mediaType = MediaType.valueOf(outputFormat.getMediaType());
return WebResponseUtils.bytesToWebResponse(outputBytes, outputFilename, mediaType);
TempFile tempOut =
tempFileManager.createManagedTempFile("." + outputFormat.getExtension());
try {
Files.copy(outputPath, tempOut.getPath(), StandardCopyOption.REPLACE_EXISTING);
} catch (Exception e) {
tempOut.close();
throw e;
}
return WebResponseUtils.fileToWebResponse(tempOut, outputFilename, mediaType);
} finally {
cleanupTempFiles(workingDirectory, inputPath, outputPath);
}
@@ -1,6 +1,7 @@
package stirling.software.SPDF.controller.api.converters;
import java.io.ByteArrayOutputStream;
import java.io.OutputStream;
import java.nio.file.Files;
import java.util.List;
import java.util.Locale;
@@ -11,11 +12,10 @@ import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.usermodel.Workbook;
import org.apache.poi.ss.util.WorkbookUtil;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.springframework.http.ContentDisposition;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;
import io.swagger.v3.oas.annotations.Operation;
@@ -27,6 +27,9 @@ import stirling.software.common.annotations.AutoJobPostMapping;
import stirling.software.common.annotations.api.ConvertApi;
import stirling.software.common.service.CustomPDFDocumentFactory;
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 technology.tabula.ObjectExtractor;
import technology.tabula.Page;
@@ -40,6 +43,7 @@ import technology.tabula.extractors.SpreadsheetExtractionAlgorithm;
public class ConvertPDFToExcelController {
private final CustomPDFDocumentFactory pdfDocumentFactory;
private final TempFileManager tempFileManager;
@AutoJobPostMapping(value = "/pdf/xlsx", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
@Operation(
@@ -47,11 +51,12 @@ public class ConvertPDFToExcelController {
description =
"Extracts tabular data from each page of a PDF and writes it into an Excel"
+ " workbook, one sheet per table. Input:PDF Output:XLSX Type:SISO")
public ResponseEntity<byte[]> pdfToExcel(@ModelAttribute PDFWithPageNums request)
public ResponseEntity<StreamingResponseBody> pdfToExcel(@ModelAttribute PDFWithPageNums request)
throws Exception {
String baseName =
GeneralUtils.removeExtension(request.getFileInput().getOriginalFilename());
TempFile tempOut = tempFileManager.createManagedTempFile(".xlsx");
try (PDDocument document = pdfDocumentFactory.load(request);
XSSFWorkbook workbook = new XSSFWorkbook();
ObjectExtractor extractor = new ObjectExtractor(document)) {
@@ -89,21 +94,22 @@ public class ConvertPDFToExcelController {
}
if (sheetCount == 0) {
tempOut.close();
return ResponseEntity.noContent().build();
}
ByteArrayOutputStream baos = new ByteArrayOutputStream();
workbook.write(baos);
HttpHeaders headers = new HttpHeaders();
headers.setContentDisposition(
ContentDisposition.builder("attachment").filename(baseName + ".xlsx").build());
headers.setContentType(
MediaType.parseMediaType(
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"));
return ResponseEntity.ok().headers(headers).body(baos.toByteArray());
try (OutputStream os = Files.newOutputStream(tempOut.getPath())) {
workbook.write(os);
}
} catch (Exception e) {
tempOut.close();
throw e;
}
MediaType mediaType =
MediaType.parseMediaType(
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
return WebResponseUtils.fileToWebResponse(tempOut, baseName + ".xlsx", mediaType);
}
private String getUniqueSheetName(Workbook workbook, String baseName) {
@@ -4,6 +4,7 @@ import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;
import io.swagger.v3.oas.annotations.Operation;
@@ -28,7 +29,8 @@ public class ConvertPDFToHtml {
summary = "Convert PDF to HTML",
description =
"This endpoint converts a PDF file to HTML format. Input:PDF Output:HTML Type:SISO")
public ResponseEntity<byte[]> processPdfToHTML(@ModelAttribute PDFFile file) throws Exception {
public ResponseEntity<StreamingResponseBody> processPdfToHTML(@ModelAttribute PDFFile file)
throws Exception {
MultipartFile inputFile = file.getFileInput();
PDFToFile pdfToFile = new PDFToFile(tempFileManager, runtimePathConfig);
return pdfToFile.processPdfToHtml(inputFile);
@@ -1,6 +1,8 @@
package stirling.software.SPDF.controller.api.converters;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.text.PDFTextStripper;
@@ -8,6 +10,7 @@ import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;
import io.swagger.v3.oas.annotations.Operation;
@@ -23,6 +26,7 @@ import stirling.software.common.model.api.PDFFile;
import stirling.software.common.service.CustomPDFDocumentFactory;
import stirling.software.common.util.GeneralUtils;
import stirling.software.common.util.PDFToFile;
import stirling.software.common.util.TempFile;
import stirling.software.common.util.TempFileManager;
import stirling.software.common.util.WebResponseUtils;
@@ -40,7 +44,7 @@ public class ConvertPDFToOffice {
description =
"This endpoint converts a given PDF file to a Presentation format. Input:PDF"
+ " Output:PPT Type:SISO")
public ResponseEntity<byte[]> processPdfToPresentation(
public ResponseEntity<StreamingResponseBody> processPdfToPresentation(
@ModelAttribute PdfToPresentationRequest request)
throws IOException, InterruptedException {
MultipartFile inputFile = request.getFileInput();
@@ -55,20 +59,24 @@ public class ConvertPDFToOffice {
description =
"This endpoint converts a given PDF file to Text or RTF format. Input:PDF"
+ " Output:TXT Type:SISO")
public ResponseEntity<byte[]> processPdfToRTForTXT(
public ResponseEntity<StreamingResponseBody> processPdfToRTForTXT(
@ModelAttribute PdfToTextOrRTFRequest request)
throws IOException, InterruptedException {
MultipartFile inputFile = request.getFileInput();
String outputFormat = request.getOutputFormat();
if ("txt".equals(request.getOutputFormat())) {
String fileName =
GeneralUtils.generateFilename(inputFile.getOriginalFilename(), ".txt");
TempFile finalOut = tempFileManager.createManagedTempFile(".txt");
try (PDDocument document = pdfDocumentFactory.load(inputFile)) {
PDFTextStripper stripper = new PDFTextStripper();
String text = stripper.getText(document);
return WebResponseUtils.bytesToWebResponse(
text.getBytes(),
GeneralUtils.generateFilename(inputFile.getOriginalFilename(), ".txt"),
MediaType.TEXT_PLAIN);
Files.writeString(finalOut.getPath(), text, StandardCharsets.UTF_8);
} catch (Exception e) {
finalOut.close();
throw e;
}
return WebResponseUtils.fileToWebResponse(finalOut, fileName, MediaType.TEXT_PLAIN);
} else {
PDFToFile pdfToFile = new PDFToFile(tempFileManager, runtimePathConfig);
return pdfToFile.processPdfToOfficeFormat(inputFile, outputFormat, "writer_pdf_import");
@@ -81,8 +89,8 @@ public class ConvertPDFToOffice {
description =
"This endpoint converts a given PDF file to a Word document format. Input:PDF"
+ " Output:WORD Type:SISO")
public ResponseEntity<byte[]> processPdfToWord(@ModelAttribute PdfToWordRequest request)
throws IOException, InterruptedException {
public ResponseEntity<StreamingResponseBody> processPdfToWord(
@ModelAttribute PdfToWordRequest request) throws IOException, InterruptedException {
MultipartFile inputFile = request.getFileInput();
String outputFormat = request.getOutputFormat();
PDFToFile pdfToFile = new PDFToFile(tempFileManager, runtimePathConfig);
@@ -95,7 +103,8 @@ public class ConvertPDFToOffice {
description =
"This endpoint converts a PDF file to an XML file. Input:PDF Output:XML"
+ " Type:SISO")
public ResponseEntity<byte[]> processPdfToXML(@ModelAttribute PDFFile file) throws Exception {
public ResponseEntity<StreamingResponseBody> processPdfToXML(@ModelAttribute PDFFile file)
throws Exception {
MultipartFile inputFile = file.getFileInput();
PDFToFile pdfToFile = new PDFToFile(tempFileManager, runtimePathConfig);
@@ -77,6 +77,7 @@ import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.server.ResponseStatusException;
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;
import io.github.pixee.security.Filenames;
import io.swagger.v3.oas.annotations.Operation;
@@ -92,6 +93,8 @@ import stirling.software.common.configuration.RuntimePathConfig;
import stirling.software.common.util.ExceptionUtils;
import stirling.software.common.util.ProcessExecutor;
import stirling.software.common.util.ProcessExecutor.ProcessExecutorResult;
import stirling.software.common.util.TempFile;
import stirling.software.common.util.TempFileManager;
import stirling.software.common.util.WebResponseUtils;
@ConvertApi
@@ -102,6 +105,7 @@ public class ConvertPDFToPDFA {
private static final Pattern NON_PRINTABLE_ASCII = Pattern.compile("[^\\x20-\\x7E]");
private final RuntimePathConfig runtimePathConfig;
private final stirling.software.SPDF.service.VeraPDFService veraPDFService;
private final TempFileManager tempFileManager;
private static final String ICC_RESOURCE_PATH = "/icc/sRGB2014.icc";
private static final int PDFA_COMPATIBILITY_POLICY = 1;
@@ -573,7 +577,7 @@ public class ConvertPDFToPDFA {
summary = "Convert a PDF to a PDF/A or PDF/X",
description =
"This endpoint converts a PDF file to a PDF/A or PDF/X file using Ghostscript (preferred) or PDFBox/LibreOffice (fallback). PDF/A is a format designed for long-term archiving, while PDF/X is optimized for print production. Input:PDF Output:PDF Type:SISO")
public ResponseEntity<byte[]> pdfToPdfA(@ModelAttribute PdfToPdfARequest request)
public ResponseEntity<StreamingResponseBody> pdfToPdfA(@ModelAttribute PdfToPdfARequest request)
throws Exception {
MultipartFile inputFile = request.getFileInput();
String outputFormat = request.getOutputFormat();
@@ -609,7 +613,7 @@ public class ConvertPDFToPDFA {
return missing;
}
private ResponseEntity<byte[]> handlePdfXConversion(
private ResponseEntity<StreamingResponseBody> handlePdfXConversion(
MultipartFile inputFile, String outputFormat) throws Exception {
PdfXProfile profile = PdfXProfile.fromRequest(outputFormat);
@@ -640,8 +644,14 @@ public class ConvertPDFToPDFA {
log.info("PDF/X conversion completed successfully to {}", profile.getDisplayName());
return WebResponseUtils.bytesToWebResponse(
converted, outputFilename, MediaType.APPLICATION_PDF);
TempFile tempOut = tempFileManager.createManagedTempFile(".pdf");
try {
Files.write(tempOut.getPath(), converted);
} catch (Exception ex) {
tempOut.close();
throw ex;
}
return WebResponseUtils.pdfFileToWebResponse(tempOut, outputFilename);
} catch (IOException | InterruptedException e) {
log.error("PDF/X conversion failed", e);
@@ -1796,7 +1806,7 @@ public class ConvertPDFToPDFA {
return Files.readAllBytes(outputPdf);
}
private ResponseEntity<byte[]> handlePdfAConversion(
private ResponseEntity<StreamingResponseBody> handlePdfAConversion(
MultipartFile inputFile, String outputFormat, boolean strict) throws Exception {
PdfaProfile profile = PdfaProfile.fromRequest(outputFormat);
@@ -1830,8 +1840,14 @@ public class ConvertPDFToPDFA {
verifyStrictCompliance(converted);
}
return WebResponseUtils.bytesToWebResponse(
converted, outputFilename, MediaType.APPLICATION_PDF);
TempFile tempOut = tempFileManager.createManagedTempFile(".pdf");
try {
Files.write(tempOut.getPath(), converted);
} catch (Exception ex) {
tempOut.close();
throw ex;
}
return WebResponseUtils.pdfFileToWebResponse(tempOut, outputFilename);
} catch (IOException | InterruptedException e) {
log.warn(
"Ghostscript conversion failed, falling back to PDFBox/LibreOffice method",
@@ -1851,8 +1867,14 @@ public class ConvertPDFToPDFA {
verifyStrictCompliance(converted);
}
return WebResponseUtils.bytesToWebResponse(
converted, outputFilename, MediaType.APPLICATION_PDF);
TempFile tempOut = tempFileManager.createManagedTempFile(".pdf");
try {
Files.write(tempOut.getPath(), converted);
} catch (Exception ex) {
tempOut.close();
throw ex;
}
return WebResponseUtils.pdfFileToWebResponse(tempOut, outputFilename);
} finally {
deleteQuietly(workingDir);
}
@@ -1,6 +1,7 @@
package stirling.software.SPDF.controller.api.converters;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.Optional;
import java.util.UUID;
import java.util.regex.Pattern;
@@ -14,6 +15,7 @@ import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;
import io.github.pixee.security.Filenames;
import io.swagger.v3.oas.annotations.Operation;
@@ -31,6 +33,8 @@ import stirling.software.common.model.api.GeneralFile;
import stirling.software.common.model.api.PDFFile;
import stirling.software.common.service.JobOwnershipService;
import stirling.software.common.util.ExceptionUtils;
import stirling.software.common.util.TempFile;
import stirling.software.common.util.TempFileManager;
import stirling.software.common.util.WebResponseUtils;
@Slf4j
@@ -42,6 +46,7 @@ public class ConvertPdfJsonController {
private static final Pattern WHITESPACE_PATTERN = Pattern.compile("[\\r\\n\\t]+");
private static final Pattern NON_PRINTABLE_PATTERN = Pattern.compile("[^\\x20-\\x7E]");
private final PdfJsonConversionService pdfJsonConversionService;
private final TempFileManager tempFileManager;
@Autowired(required = false)
private JobOwnershipService jobOwnershipService;
@@ -51,7 +56,7 @@ public class ConvertPdfJsonController {
summary = "Convert PDF to Text Editor Format",
description =
"Extracts PDF text, fonts, and metadata into an editable JSON structure for the text editor tool. Input:PDF Output:JSON Type:SISO")
public ResponseEntity<byte[]> convertPdfToJson(
public ResponseEntity<StreamingResponseBody> convertPdfToJson(
@ModelAttribute PDFFile request,
@RequestParam(value = "lightweight", defaultValue = "false") boolean lightweight)
throws Exception {
@@ -60,6 +65,8 @@ public class ConvertPdfJsonController {
throw ExceptionUtils.createNullArgumentException("fileInput");
}
// TODO: Refactor PdfJsonConversionService to write directly to an OutputStream
// instead of returning byte[], avoiding the intermediate heap allocation + temp file write
byte[] jsonBytes = pdfJsonConversionService.convertPdfToJson(inputFile, lightweight);
logJsonResponse("pdf/text-editor", jsonBytes);
String originalName = inputFile.getOriginalFilename();
@@ -70,7 +77,14 @@ public class ConvertPdfJsonController {
.replaceFirst("")
: "document";
String docName = baseName + ".json";
return WebResponseUtils.bytesToWebResponse(jsonBytes, docName, MediaType.APPLICATION_JSON);
TempFile tempOut = tempFileManager.createManagedTempFile(".json");
try {
Files.write(tempOut.getPath(), jsonBytes);
} catch (Exception e) {
tempOut.close();
throw e;
}
return WebResponseUtils.fileToWebResponse(tempOut, docName, MediaType.APPLICATION_JSON);
}
@AutoJobPostMapping(consumes = "multipart/form-data", value = "/text-editor/pdf")
@@ -79,8 +93,8 @@ public class ConvertPdfJsonController {
summary = "Convert Text Editor Format to PDF",
description =
"Rebuilds a PDF from the editable JSON structure generated by the text editor tool. Input:JSON Output:PDF Type:SISO")
public ResponseEntity<byte[]> convertJsonToPdf(@ModelAttribute GeneralFile request)
throws Exception {
public ResponseEntity<StreamingResponseBody> convertJsonToPdf(
@ModelAttribute GeneralFile request) throws Exception {
MultipartFile jsonFile = request.getFileInput();
if (jsonFile == null) {
throw ExceptionUtils.createNullArgumentException("fileInput");
@@ -95,7 +109,14 @@ public class ConvertPdfJsonController {
.replaceFirst("")
: "document";
String docName = baseName.endsWith(".pdf") ? baseName : baseName + ".pdf";
return WebResponseUtils.bytesToWebResponse(pdfBytes, docName);
TempFile tempOut = tempFileManager.createManagedTempFile(".pdf");
try {
Files.write(tempOut.getPath(), pdfBytes);
} catch (Exception e) {
tempOut.close();
throw e;
}
return WebResponseUtils.pdfFileToWebResponse(tempOut, docName);
}
@AutoJobPostMapping(consumes = "multipart/form-data", value = "/pdf/text-editor/metadata")
@@ -105,17 +126,15 @@ public class ConvertPdfJsonController {
"Extracts document metadata, fonts, and page dimensions for the text editor tool. Caches the document for"
+ " subsequent page requests. Returns a server-generated jobId scoped to the"
+ " authenticated user. Input:PDF Output:JSON Type:SISO")
public ResponseEntity<byte[]> extractPdfMetadata(@ModelAttribute PDFFile request)
public ResponseEntity<StreamingResponseBody> extractPdfMetadata(@ModelAttribute PDFFile request)
throws Exception {
MultipartFile inputFile = request.getFileInput();
if (inputFile == null) {
throw ExceptionUtils.createNullArgumentException("fileInput");
}
// Generate server-side UUID for job
String baseJobId = UUID.randomUUID().toString();
// Scope job to authenticated user if security is enabled
String scopedJobKey = getScopedJobKey(baseJobId);
log.debug("Extracting metadata for PDF, assigned jobId: {}", scopedJobKey);
@@ -123,20 +142,27 @@ public class ConvertPdfJsonController {
byte[] jsonBytes =
pdfJsonConversionService.extractDocumentMetadata(inputFile, scopedJobKey);
logJsonResponse("pdf/text-editor/metadata", jsonBytes);
String originalName = inputFile.getOriginalFilename();
String baseName =
(originalName != null && !originalName.isBlank())
? FILE_EXTENSION_PATTERN
.matcher(Filenames.toSimpleFileName(originalName))
.replaceFirst("")
: "document";
String docName = baseName + "_metadata.json";
// Return jobId in response header for client
TempFile tempOut = tempFileManager.createManagedTempFile(".json");
try {
Files.write(tempOut.getPath(), jsonBytes);
} catch (Exception e) {
tempOut.close();
throw e;
}
return ResponseEntity.ok()
.header("X-Job-Id", scopedJobKey)
.contentType(MediaType.APPLICATION_JSON)
.body(jsonBytes);
.contentLength(java.nio.file.Files.size(tempOut.getPath()))
.body(
os -> {
try (os) {
Files.copy(tempOut.getPath(), os);
os.flush();
} finally {
tempOut.close();
}
});
}
@AutoJobPostMapping(
@@ -149,7 +175,7 @@ public class ConvertPdfJsonController {
"Applies edits for the specified pages of a cached PDF and returns an updated PDF."
+ " Requires the PDF to have been previously cached via the text editor metadata endpoint."
+ " The jobId must be obtained from the metadata extraction endpoint.")
public ResponseEntity<byte[]> exportPartialPdf(
public ResponseEntity<StreamingResponseBody> exportPartialPdf(
@PathVariable String jobId,
@RequestBody PdfJsonDocument document,
@RequestParam(value = "filename", required = false) String filename)
@@ -158,7 +184,6 @@ public class ConvertPdfJsonController {
throw ExceptionUtils.createNullArgumentException("document");
}
// Validate job ownership
validateJobAccess(jobId);
byte[] pdfBytes = pdfJsonConversionService.exportUpdatedPages(jobId, document);
@@ -173,7 +198,14 @@ public class ConvertPdfJsonController {
.filter(title -> title != null && !title.isBlank())
.orElse("document");
String docName = baseName.endsWith(".pdf") ? baseName : baseName + ".pdf";
return WebResponseUtils.bytesToWebResponse(pdfBytes, docName);
TempFile tempOut = tempFileManager.createManagedTempFile(".pdf");
try {
Files.write(tempOut.getPath(), pdfBytes);
} catch (Exception e) {
tempOut.close();
throw e;
}
return WebResponseUtils.pdfFileToWebResponse(tempOut, docName);
}
@GetMapping(value = "/pdf/text-editor/page/{jobId}/{pageNumber}")
@@ -183,16 +215,22 @@ public class ConvertPdfJsonController {
"Retrieves a single page's content from a previously cached PDF document for the text editor tool."
+ " Requires prior call to /pdf/text-editor/metadata. The jobId must belong to the"
+ " authenticated user. Output:JSON")
public ResponseEntity<byte[]> extractSinglePage(
public ResponseEntity<StreamingResponseBody> extractSinglePage(
@PathVariable String jobId, @PathVariable int pageNumber) throws Exception {
// Validate job ownership
validateJobAccess(jobId);
byte[] jsonBytes = pdfJsonConversionService.extractSinglePage(jobId, pageNumber);
logJsonResponse("pdf/text-editor/page", jsonBytes);
String docName = "page_" + pageNumber + ".json";
return WebResponseUtils.bytesToWebResponse(jsonBytes, docName, MediaType.APPLICATION_JSON);
TempFile tempOut = tempFileManager.createManagedTempFile(".json");
try {
Files.write(tempOut.getPath(), jsonBytes);
} catch (Exception e) {
tempOut.close();
throw e;
}
return WebResponseUtils.fileToWebResponse(tempOut, docName, MediaType.APPLICATION_JSON);
}
@GetMapping(value = "/pdf/text-editor/fonts/{jobId}/{pageNumber}")
@@ -202,16 +240,22 @@ public class ConvertPdfJsonController {
"Retrieves the font payloads used by a single page from a previously cached PDF document."
+ " Requires prior call to /pdf/text-editor/metadata. The jobId must belong to the"
+ " authenticated user. Output:JSON")
public ResponseEntity<byte[]> extractPageFonts(
public ResponseEntity<StreamingResponseBody> extractPageFonts(
@PathVariable String jobId, @PathVariable int pageNumber) throws Exception {
// Validate job ownership
validateJobAccess(jobId);
byte[] jsonBytes = pdfJsonConversionService.extractPageFonts(jobId, pageNumber);
logJsonResponse("pdf/text-editor/fonts/page", jsonBytes);
String docName = "page_fonts_" + pageNumber + ".json";
return WebResponseUtils.bytesToWebResponse(jsonBytes, docName, MediaType.APPLICATION_JSON);
TempFile tempOut = tempFileManager.createManagedTempFile(".json");
try {
Files.write(tempOut.getPath(), jsonBytes);
} catch (Exception e) {
tempOut.close();
throw e;
}
return WebResponseUtils.fileToWebResponse(tempOut, docName, MediaType.APPLICATION_JSON);
}
@AutoJobPostMapping(
@@ -225,24 +269,16 @@ public class ConvertPdfJsonController {
+ " authenticated user.")
public ResponseEntity<Void> clearCache(@PathVariable String jobId) {
// Validate job ownership
validateJobAccess(jobId);
pdfJsonConversionService.clearCachedDocument(jobId);
return ResponseEntity.ok().build();
}
/**
* Get a scoped job key that includes user ownership when security is enabled.
*
* @param baseJobId the base job identifier
* @return scoped job key, or just baseJobId if no ownership service available
*/
private String getScopedJobKey(String baseJobId) {
if (jobOwnershipService != null) {
return jobOwnershipService.createScopedJobKey(baseJobId);
}
// Security disabled, return unsecured job key
return baseJobId;
}
@@ -252,7 +288,6 @@ public class ConvertPdfJsonController {
return;
}
// Only perform expensive tail extraction if debug logging is enabled
if (log.isDebugEnabled()) {
int length = jsonBytes.length;
boolean endsWithJson =
@@ -431,16 +466,9 @@ public class ConvertPdfJsonController {
return WHITESPACE_PATTERN.matcher(value.substring(0, max)).replaceAll(" ") + "...";
}
/**
* Validate that the current user has access to the given job.
*
* @param jobId the job identifier to validate
* @throws SecurityException if current user does not own the job
*/
private void validateJobAccess(String jobId) {
if (jobOwnershipService != null) {
jobOwnershipService.validateJobAccess(jobId);
}
// If jobOwnershipService is null (security disabled), allow all access
}
}
@@ -14,6 +14,7 @@ import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;
import io.github.pixee.security.Filenames;
import io.swagger.v3.oas.annotations.Operation;
@@ -53,7 +54,8 @@ public class ConvertSvgToPDF {
+ "SVG dimensions (width/height) determine the PDF page size; defaults to A4 if not specified. "
+ "SVG content is sanitized to prevent XSS attacks. "
+ "Input: SVG file(s), Output: PDF file(s) or ZIP. Type: MIMO")
public ResponseEntity<byte[]> convertSvgToPdf(@ModelAttribute SvgToPdfRequest request) {
public ResponseEntity<StreamingResponseBody> convertSvgToPdf(
@ModelAttribute SvgToPdfRequest request) {
MultipartFile[] inputFiles = request.getFileInput();
boolean combineIntoSinglePdf = Boolean.TRUE.equals(request.getCombineIntoSinglePdf());
@@ -61,8 +63,7 @@ public class ConvertSvgToPDF {
// Validate input
if (inputFiles == null || inputFiles.length == 0) {
log.error("No files provided for SVG to PDF conversion.");
return ResponseEntity.badRequest()
.body("No files provided".getBytes(StandardCharsets.UTF_8));
return errorResponse(HttpStatus.BAD_REQUEST, "No files provided");
}
try {
@@ -103,8 +104,7 @@ public class ConvertSvgToPDF {
if (sanitizedSvgs.isEmpty()) {
log.error("No valid SVG files were found");
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body("No valid SVG files were found".getBytes(StandardCharsets.UTF_8));
return errorResponse(HttpStatus.BAD_REQUEST, "No valid SVG files were found");
}
if (combineIntoSinglePdf) {
@@ -115,14 +115,23 @@ public class ConvertSvgToPDF {
} catch (Exception e) {
log.error("Unexpected error during SVG to PDF conversion", e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(
"An unexpected error occurred during conversion"
.getBytes(StandardCharsets.UTF_8));
return errorResponse(
HttpStatus.INTERNAL_SERVER_ERROR,
"An unexpected error occurred during conversion");
}
}
private ResponseEntity<byte[]> handleCombinedConversion(
private ResponseEntity<StreamingResponseBody> errorResponse(HttpStatus status, String message) {
byte[] body = message.getBytes(StandardCharsets.UTF_8);
StreamingResponseBody streaming =
os -> {
os.write(body);
os.flush();
};
return ResponseEntity.status(status).body(streaming);
}
private ResponseEntity<StreamingResponseBody> handleCombinedConversion(
List<byte[]> sanitizedSvgs, List<String> filenames) {
try {
log.info("Combining {} SVG files into single PDF", sanitizedSvgs.size());
@@ -131,10 +140,8 @@ public class ConvertSvgToPDF {
if (pdfBytes == null || pdfBytes.length == 0) {
log.error("PDF conversion failed - empty output");
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(
"PDF conversion failed - empty output"
.getBytes(StandardCharsets.UTF_8));
return errorResponse(
HttpStatus.INTERNAL_SERVER_ERROR, "PDF conversion failed - empty output");
}
pdfBytes = pdfDocumentFactory.createNewBytesBasedOnOldDocument(pdfBytes);
@@ -146,19 +153,23 @@ public class ConvertSvgToPDF {
log.info("Successfully combined {} SVGs into single PDF", sanitizedSvgs.size());
return WebResponseUtils.bytesToWebResponse(
pdfBytes, outputFilename, MediaType.APPLICATION_PDF);
TempFile tempOut = tempFileManager.createManagedTempFile(".pdf");
try {
Files.write(tempOut.getPath(), pdfBytes);
} catch (Exception e) {
tempOut.close();
throw e;
}
return WebResponseUtils.pdfFileToWebResponse(tempOut, outputFilename);
} catch (IOException e) {
log.error("Error combining SVGs into PDF", e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(
("Conversion failed: " + e.getMessage())
.getBytes(StandardCharsets.UTF_8));
return errorResponse(
HttpStatus.INTERNAL_SERVER_ERROR, "Conversion failed: " + e.getMessage());
}
}
private ResponseEntity<byte[]> handleSeparateConversion(
private ResponseEntity<StreamingResponseBody> handleSeparateConversion(
List<byte[]> sanitizedSvgs, List<String> filenames) {
List<ConvertedPdf> convertedPdfs = new ArrayList<>();
@@ -188,15 +199,21 @@ public class ConvertSvgToPDF {
if (convertedPdfs.isEmpty()) {
log.error("No files were successfully converted");
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body("No files were successfully converted".getBytes(StandardCharsets.UTF_8));
return errorResponse(
HttpStatus.INTERNAL_SERVER_ERROR, "No files were successfully converted");
}
try {
if (convertedPdfs.size() == 1) {
ConvertedPdf pdf = convertedPdfs.get(0);
return WebResponseUtils.bytesToWebResponse(
pdf.content, pdf.filename, MediaType.APPLICATION_PDF);
TempFile tempOut = tempFileManager.createManagedTempFile(".pdf");
try {
Files.write(tempOut.getPath(), pdf.content);
} catch (Exception e) {
tempOut.close();
throw e;
}
return WebResponseUtils.pdfFileToWebResponse(tempOut, pdf.filename);
}
String zipFilename =
@@ -204,22 +221,18 @@ public class ConvertSvgToPDF {
? "converted_svgs.zip"
: GeneralUtils.generateFilename(
filenames.get(0), "_converted_svgs.zip");
byte[] zipBytes = createZipFromPdfs(convertedPdfs);
return WebResponseUtils.bytesToWebResponse(
zipBytes, zipFilename, MediaType.APPLICATION_OCTET_STREAM);
TempFile zipFile = createZipFromPdfs(convertedPdfs);
return WebResponseUtils.zipFileToWebResponse(zipFile, zipFilename);
} catch (IOException e) {
log.error("Failed to create response", e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body("Failed to create response".getBytes(StandardCharsets.UTF_8));
return errorResponse(HttpStatus.INTERNAL_SERVER_ERROR, "Failed to create response");
}
}
private byte[] createZipFromPdfs(List<ConvertedPdf> pdfs) throws IOException {
try (TempFile tempZipFile = new TempFile(tempFileManager, ".zip");
ZipOutputStream zipOut =
new ZipOutputStream(Files.newOutputStream(tempZipFile.getPath()))) {
private TempFile createZipFromPdfs(List<ConvertedPdf> pdfs) throws IOException {
TempFile tempZipFile = tempFileManager.createManagedTempFile(".zip");
try (ZipOutputStream zipOut =
new ZipOutputStream(Files.newOutputStream(tempZipFile.getPath()))) {
for (ConvertedPdf pdf : pdfs) {
ZipEntry pdfEntry = new ZipEntry(pdf.filename);
zipOut.putNextEntry(pdfEntry);
@@ -227,9 +240,11 @@ public class ConvertSvgToPDF {
zipOut.closeEntry();
log.debug("Added {} to ZIP", pdf.filename);
}
return Files.readAllBytes(tempZipFile.getPath());
} catch (IOException e) {
tempZipFile.close();
throw e;
}
return tempZipFile;
}
private static class ConvertedPdf {
@@ -1,6 +1,5 @@
package stirling.software.SPDF.controller.api.converters;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
@@ -39,6 +38,8 @@ import stirling.software.common.util.ExceptionUtils;
import stirling.software.common.util.GeneralUtils;
import stirling.software.common.util.ProcessExecutor;
import stirling.software.common.util.RegexPatternUtils;
import stirling.software.common.util.TempFile;
import stirling.software.common.util.TempFileManager;
import stirling.software.common.util.WebResponseUtils;
@ConvertApi
@@ -49,6 +50,7 @@ public class ConvertWebsiteToPDF {
private final CustomPDFDocumentFactory pdfDocumentFactory;
private final RuntimePathConfig runtimePathConfig;
private final ApplicationProperties applicationProperties;
private final TempFileManager tempFileManager;
private static final Pattern FILE_SCHEME_PATTERN =
Pattern.compile("(?<![a-z0-9_])file\\s*:(?:/{1,3}|%2f|%5c|%3a|&#x2f;|&#47;)");
@@ -136,14 +138,15 @@ public class ConvertWebsiteToPDF {
.runCommandWithOutputHandling(command);
// Load the PDF using pdfDocumentFactory
try (PDDocument doc = pdfDocumentFactory.load(tempOutputFile.toFile());
ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
// Convert URL to a safe filename
String outputFilename = convertURLToFileName(URL);
doc.save(baos);
return WebResponseUtils.baosToWebResponse(baos, outputFilename);
String outputFilename = convertURLToFileName(URL);
TempFile tempOut = tempFileManager.createManagedTempFile(".pdf");
try (PDDocument doc = pdfDocumentFactory.load(tempOutputFile.toFile())) {
doc.save(tempOut.getFile());
} catch (Exception e) {
tempOut.close();
throw e;
}
return WebResponseUtils.pdfFileToWebResponse(tempOut, outputFilename);
} finally {
if (tempHtmlInput != null) {
try {
@@ -1,7 +1,6 @@
package stirling.software.SPDF.controller.api.converters;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.StringWriter;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
@@ -32,6 +31,7 @@ import stirling.software.common.annotations.AutoJobPostMapping;
import stirling.software.common.annotations.api.ConvertApi;
import stirling.software.common.service.CustomPDFDocumentFactory;
import stirling.software.common.util.GeneralUtils;
import stirling.software.common.util.WebResponseUtils;
import technology.tabula.ObjectExtractor;
import technology.tabula.Page;
@@ -90,7 +90,7 @@ public class ExtractCSVController {
}
private ResponseEntity<byte[]> createZipResponse(List<CsvEntry> entries, String baseName)
throws IOException {
throws Exception {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
try (ZipOutputStream zipOut = new ZipOutputStream(baos)) {
for (CsvEntry entry : entries) {
@@ -101,14 +101,10 @@ public class ExtractCSVController {
}
}
HttpHeaders headers = new HttpHeaders();
headers.setContentDisposition(
ContentDisposition.builder("attachment")
.filename(baseName + "_extracted.zip")
.build());
headers.setContentType(MediaType.parseMediaType("application/zip"));
return ResponseEntity.ok().headers(headers).body(baos.toByteArray());
return WebResponseUtils.bytesToWebResponse(
baos.toByteArray(),
baseName + "_extracted.zip",
MediaType.APPLICATION_OCTET_STREAM);
}
private ResponseEntity<String> createCsvResponse(CsvEntry entry, String baseName) {
@@ -13,6 +13,7 @@ import org.apache.commons.io.FilenameUtils;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;
import io.swagger.v3.oas.annotations.Operation;
@@ -38,7 +39,6 @@ import stirling.software.common.util.WebResponseUtils;
@RequiredArgsConstructor
public class PdfVectorExportController {
private static final MediaType PDF_MEDIA_TYPE = MediaType.APPLICATION_PDF;
private static final Set<String> GHOSTSCRIPT_INPUTS =
Set.of("ps", "eps", "epsf"); // PCL/PXL/XPS require GhostPDL (gpcl6/gxps)
@@ -51,7 +51,7 @@ public class PdfVectorExportController {
description =
"Converts PostScript vector inputs (PS, EPS, EPSF) to PDF using Ghostscript."
+ " Input:PS/EPS Output:PDF Type:SISO")
public ResponseEntity<byte[]> convertGhostscriptInputsToPdf(
public ResponseEntity<StreamingResponseBody> convertGhostscriptInputsToPdf(
@Valid @ModelAttribute PdfVectorExportRequest request) throws Exception {
String originalName =
@@ -63,9 +63,9 @@ public class PdfVectorExportController {
? FilenameUtils.getExtension(originalName).toLowerCase(Locale.ROOT)
: "";
TempFile outputTemp = tempFileManager.createManagedTempFile(".pdf");
try (TempFile inputTemp =
new TempFile(tempFileManager, extension.isEmpty() ? "" : "." + extension);
TempFile outputTemp = new TempFile(tempFileManager, ".pdf")) {
new TempFile(tempFileManager, extension.isEmpty() ? "" : "." + extension)) {
request.getFileInput().transferTo(inputTemp.getFile());
@@ -83,11 +83,13 @@ public class PdfVectorExportController {
"Unsupported Ghostscript input format {0}",
extension);
}
byte[] pdfBytes = Files.readAllBytes(outputTemp.getPath());
String outputName = GeneralUtils.generateFilename(originalName, "_converted.pdf");
return WebResponseUtils.bytesToWebResponse(pdfBytes, outputName, PDF_MEDIA_TYPE);
} catch (Exception e) {
outputTemp.close();
throw e;
}
String outputName = GeneralUtils.generateFilename(originalName, "_converted.pdf");
return WebResponseUtils.pdfFileToWebResponse(outputTemp, outputName);
}
@AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/pdf/vector")
@@ -96,7 +98,7 @@ public class PdfVectorExportController {
description =
"Converts PDF to Ghostscript vector formats (EPS, PS, PCL, or XPS)."
+ " Input:PDF Output:VECTOR Type:SISO")
public ResponseEntity<byte[]> convertPdfToVector(
public ResponseEntity<StreamingResponseBody> convertPdfToVector(
@Valid @ModelAttribute PdfVectorExportRequest request) throws Exception {
String originalName =
@@ -110,35 +112,37 @@ public class PdfVectorExportController {
}
outputFormat = outputFormat.toLowerCase(Locale.ROOT);
try (TempFile inputTemp = new TempFile(tempFileManager, ".pdf");
TempFile outputTemp = new TempFile(tempFileManager, "." + outputFormat)) {
TempFile outputTemp = tempFileManager.createManagedTempFile("." + outputFormat);
try (TempFile inputTemp = new TempFile(tempFileManager, ".pdf")) {
request.getFileInput().transferTo(inputTemp.getFile());
runGhostscriptPdfToVector(inputTemp.getPath(), outputTemp.getPath(), outputFormat);
byte[] vectorBytes = Files.readAllBytes(outputTemp.getPath());
String outputName =
GeneralUtils.generateFilename(originalName, "_converted." + outputFormat);
MediaType mediaType;
switch (outputFormat.toLowerCase(Locale.ROOT)) {
case "eps":
case "ps":
mediaType = MediaType.parseMediaType("application/postscript");
break;
case "pcl":
mediaType = MediaType.parseMediaType("application/vnd.hp-PCL");
break;
case "xps":
mediaType = MediaType.parseMediaType("application/vnd.ms-xpsdocument");
break;
default:
mediaType = MediaType.APPLICATION_OCTET_STREAM;
}
return WebResponseUtils.bytesToWebResponse(vectorBytes, outputName, mediaType);
} catch (Exception e) {
outputTemp.close();
throw e;
}
String outputName =
GeneralUtils.generateFilename(originalName, "_converted." + outputFormat);
MediaType mediaType;
switch (outputFormat.toLowerCase(Locale.ROOT)) {
case "eps":
case "ps":
mediaType = MediaType.parseMediaType("application/postscript");
break;
case "pcl":
mediaType = MediaType.parseMediaType("application/vnd.hp-PCL");
break;
case "xps":
mediaType = MediaType.parseMediaType("application/vnd.ms-xpsdocument");
break;
default:
mediaType = MediaType.APPLICATION_OCTET_STREAM;
}
return WebResponseUtils.fileToWebResponse(outputTemp, outputName, mediaType);
}
private void runGhostscriptPdfToVector(Path inputPath, Path outputPath, String outputFormat)
@@ -9,6 +9,7 @@ import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;
import io.github.pixee.security.Filenames;
import io.swagger.v3.oas.annotations.Operation;
@@ -29,6 +30,7 @@ import stirling.software.common.annotations.api.FilterApi;
import stirling.software.common.service.CustomPDFDocumentFactory;
import stirling.software.common.util.ExceptionUtils;
import stirling.software.common.util.PdfUtils;
import stirling.software.common.util.TempFileManager;
import stirling.software.common.util.WebResponseUtils;
@FilterApi
@@ -36,6 +38,7 @@ import stirling.software.common.util.WebResponseUtils;
public class FilterController {
private final CustomPDFDocumentFactory pdfDocumentFactory;
private final TempFileManager tempFileManager;
@AutoJobPostMapping(
consumes = MediaType.MULTIPART_FORM_DATA_VALUE,
@@ -53,8 +56,8 @@ public class FilterController {
description = "PDF did not pass filter",
content = @Content())
})
public ResponseEntity<byte[]> containsText(@ModelAttribute ContainsTextRequest request)
throws IOException, InterruptedException {
public ResponseEntity<StreamingResponseBody> containsText(
@ModelAttribute ContainsTextRequest request) throws IOException, InterruptedException {
MultipartFile inputFile = request.getFileInput();
String text = request.getText();
String pageNumber = request.getPageNumbers();
@@ -62,7 +65,9 @@ public class FilterController {
try (PDDocument pdfDocument = pdfDocumentFactory.load(inputFile)) {
if (PdfUtils.hasText(pdfDocument, pageNumber, text)) {
return WebResponseUtils.pdfDocToWebResponse(
pdfDocument, Filenames.toSimpleFileName(inputFile.getOriginalFilename()));
pdfDocument,
Filenames.toSimpleFileName(inputFile.getOriginalFilename()),
tempFileManager);
}
}
return ResponseEntity.noContent().build();
@@ -84,15 +89,17 @@ public class FilterController {
description = "PDF did not pass filter",
content = @Content())
})
public ResponseEntity<byte[]> containsImage(@ModelAttribute PDFWithPageNums request)
throws IOException, InterruptedException {
public ResponseEntity<StreamingResponseBody> containsImage(
@ModelAttribute PDFWithPageNums request) throws IOException, InterruptedException {
MultipartFile inputFile = request.getFileInput();
String pageNumber = request.getPageNumbers();
try (PDDocument pdfDocument = pdfDocumentFactory.load(inputFile)) {
if (PdfUtils.hasImages(pdfDocument, pageNumber)) {
return WebResponseUtils.pdfDocToWebResponse(
pdfDocument, Filenames.toSimpleFileName(inputFile.getOriginalFilename()));
pdfDocument,
Filenames.toSimpleFileName(inputFile.getOriginalFilename()),
tempFileManager);
}
}
return ResponseEntity.noContent().build();
@@ -18,6 +18,7 @@ import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;
import com.opencsv.CSVWriter;
@@ -34,6 +35,7 @@ import stirling.software.common.model.FormFieldWithCoordinates;
import stirling.software.common.service.CustomPDFDocumentFactory;
import stirling.software.common.util.ExceptionUtils;
import stirling.software.common.util.FormUtils;
import stirling.software.common.util.TempFileManager;
import stirling.software.common.util.WebResponseUtils;
import tools.jackson.core.type.TypeReference;
@@ -59,12 +61,11 @@ public class FormFillController {
private final CustomPDFDocumentFactory pdfDocumentFactory;
private final ObjectMapper objectMapper;
private final TempFileManager tempFileManager;
private static ResponseEntity<byte[]> saveDocument(PDDocument document, String baseName)
private ResponseEntity<StreamingResponseBody> saveDocument(PDDocument document, String baseName)
throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
document.save(baos);
return WebResponseUtils.bytesToWebResponse(baos.toByteArray(), baseName + ".pdf");
return WebResponseUtils.pdfDocToWebResponse(document, baseName + ".pdf", tempFileManager);
}
private static String buildBaseName(MultipartFile file, String suffix) {
@@ -261,7 +262,7 @@ public class FormFillController {
summary = "Modify existing form fields",
description =
"Updates existing fields in the provided PDF and returns the updated file")
public ResponseEntity<byte[]> modifyFields(
public ResponseEntity<StreamingResponseBody> modifyFields(
@Parameter(
description = "The input PDF file",
required = true,
@@ -292,7 +293,7 @@ public class FormFillController {
@Operation(
summary = "Delete form fields",
description = "Removes the specified fields from the PDF and returns the updated file")
public ResponseEntity<byte[]> deleteFields(
public ResponseEntity<StreamingResponseBody> deleteFields(
@Parameter(
description = "The input PDF file",
required = true,
@@ -328,7 +329,7 @@ public class FormFillController {
description =
"Populates the supplied PDF form using values from the provided JSON payload"
+ " and returns the filled PDF")
public ResponseEntity<byte[]> fillForm(
public ResponseEntity<StreamingResponseBody> fillForm(
@Parameter(
description = "The input PDF file",
required = true,
@@ -355,7 +356,7 @@ public class FormFillController {
document -> FormUtils.applyFieldValues(document, values, flatten, true));
}
private ResponseEntity<byte[]> processSingleFile(
private ResponseEntity<StreamingResponseBody> processSingleFile(
MultipartFile file, String suffix, DocumentProcessor processor) throws IOException {
requirePdf(file);
@@ -1,7 +1,7 @@
package stirling.software.SPDF.controller.api.misc;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.file.Files;
import java.util.List;
import java.util.Optional;
@@ -10,6 +10,7 @@ import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;
import io.github.pixee.security.Filenames;
import io.swagger.v3.oas.annotations.Operation;
@@ -30,6 +31,8 @@ import stirling.software.common.annotations.api.MiscApi;
import stirling.software.common.service.CustomPDFDocumentFactory;
import stirling.software.common.util.ExceptionUtils;
import stirling.software.common.util.GeneralUtils;
import stirling.software.common.util.TempFile;
import stirling.software.common.util.TempFileManager;
import stirling.software.common.util.WebResponseUtils;
@MiscApi
@@ -43,14 +46,16 @@ public class AttachmentController {
private final ConvertPDFToPDFA convertPDFToPDFA;
private final TempFileManager tempFileManager;
@AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/add-attachments")
@StandardPdfResponse
@Operation(
summary = "Add attachments to PDF",
description =
"This endpoint adds attachments to a PDF. Input:PDF, Output:PDF Type:MISO")
public ResponseEntity<byte[]> addAttachments(@ModelAttribute AddAttachmentRequest request)
throws Exception {
public ResponseEntity<StreamingResponseBody> addAttachments(
@ModelAttribute AddAttachmentRequest request) throws Exception {
MultipartFile fileInput = request.getFileInput();
List<MultipartFile> attachments = request.getAttachments();
boolean convertToPdfA3b = request.isConvertToPdfA3b();
@@ -79,13 +84,9 @@ public class AttachmentController {
ConvertPDFToPDFA.fixType1FontCharSet(pdfaDocument);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
pdfaDocument.save(baos);
byte[] resultBytes = baos.toByteArray();
String outputFilename = baseFileName + "_with_attachments_PDFA-3b.pdf";
return WebResponseUtils.bytesToWebResponse(
resultBytes, outputFilename, MediaType.APPLICATION_PDF);
return WebResponseUtils.pdfDocToWebResponse(
pdfaDocument, outputFilename, tempFileManager);
}
} else {
try (PDDocument document = pdfDocumentFactory.load(request, false)) {
@@ -94,7 +95,8 @@ public class AttachmentController {
document,
GeneralUtils.generateFilename(
Filenames.toSimpleFileName(fileInput.getOriginalFilename()),
"_with_attachments.pdf"));
"_with_attachments.pdf"),
tempFileManager);
}
}
}
@@ -141,7 +143,7 @@ public class AttachmentController {
description =
"This endpoint extracts all embedded attachments from a PDF into a ZIP archive."
+ " Input:PDF Output:ZIP Type:SISO")
public ResponseEntity<byte[]> extractAttachments(
public ResponseEntity<StreamingResponseBody> extractAttachments(
@ModelAttribute ExtractAttachmentsRequest request) throws IOException {
try (PDDocument document = pdfDocumentFactory.load(request, true)) {
Optional<byte[]> extracted = pdfAttachmentService.extractAttachments(document);
@@ -159,8 +161,14 @@ public class AttachmentController {
Filenames.toSimpleFileName(
GeneralUtils.generateFilename(sourceName, "_attachments.zip"));
return WebResponseUtils.bytesToWebResponse(
extracted.get(), outputName, MediaType.APPLICATION_OCTET_STREAM);
TempFile tempOut = tempFileManager.createManagedTempFile(".zip");
try {
Files.write(tempOut.getFile().toPath(), extracted.get());
} catch (IOException e) {
tempOut.close();
throw e;
}
return WebResponseUtils.zipFileToWebResponse(tempOut, outputName);
}
}
@@ -187,8 +195,8 @@ public class AttachmentController {
summary = "Rename attachment in PDF",
description =
"This endpoint renames an embedded attachment in a PDF. Input:PDF Output:PDF Type:MISO")
public ResponseEntity<byte[]> renameAttachment(@ModelAttribute RenameAttachmentRequest request)
throws Exception {
public ResponseEntity<StreamingResponseBody> renameAttachment(
@ModelAttribute RenameAttachmentRequest request) throws Exception {
MultipartFile fileInput = request.getFileInput();
String attachmentName = request.getAttachmentName();
String newName = request.getNewName();
@@ -209,7 +217,8 @@ public class AttachmentController {
document,
GeneralUtils.generateFilename(
Filenames.toSimpleFileName(fileInput.getOriginalFilename()),
"_attachment_renamed.pdf"));
"_attachment_renamed.pdf"),
tempFileManager);
}
}
@@ -221,8 +230,8 @@ public class AttachmentController {
summary = "Delete attachment from PDF",
description =
"This endpoint deletes an embedded attachment from a PDF. Input:PDF Output:PDF Type:MISO")
public ResponseEntity<byte[]> deleteAttachment(@ModelAttribute DeleteAttachmentRequest request)
throws Exception {
public ResponseEntity<StreamingResponseBody> deleteAttachment(
@ModelAttribute DeleteAttachmentRequest request) throws Exception {
MultipartFile fileInput = request.getFileInput();
String attachmentName = request.getAttachmentName();
@@ -238,7 +247,8 @@ public class AttachmentController {
document,
GeneralUtils.generateFilename(
Filenames.toSimpleFileName(fileInput.getOriginalFilename()),
"_attachment_deleted.pdf"));
"_attachment_deleted.pdf"),
tempFileManager);
}
}
}
@@ -12,6 +12,7 @@ import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;
import io.github.pixee.security.Filenames;
import io.swagger.v3.oas.annotations.Operation;
@@ -24,6 +25,7 @@ import stirling.software.common.annotations.AutoJobPostMapping;
import stirling.software.common.annotations.api.MiscApi;
import stirling.software.common.service.CustomPDFDocumentFactory;
import stirling.software.common.util.RegexPatternUtils;
import stirling.software.common.util.TempFileManager;
import stirling.software.common.util.WebResponseUtils;
@MiscApi
@@ -35,6 +37,7 @@ public class AutoRenameController {
private static final int LINE_LIMIT = 200;
private final CustomPDFDocumentFactory pdfDocumentFactory;
private final TempFileManager tempFileManager;
@AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/auto-rename")
@Operation(
@@ -42,8 +45,8 @@ public class AutoRenameController {
description =
"This endpoint accepts a PDF file and attempts to extract its title or header"
+ " based on heuristics. Input:PDF Output:PDF Type:SISO")
public ResponseEntity<byte[]> extractHeader(@ModelAttribute ExtractHeaderRequest request)
throws Exception {
public ResponseEntity<StreamingResponseBody> extractHeader(
@ModelAttribute ExtractHeaderRequest request) throws Exception {
MultipartFile file = request.getFileInput();
boolean useFirstTextAsFallback = Boolean.TRUE.equals(request.getUseFirstTextAsFallback());
@@ -140,11 +143,14 @@ public class AutoRenameController {
.matcher(header)
.replaceAll("")
.trim();
return WebResponseUtils.pdfDocToWebResponse(document, header + ".pdf");
return WebResponseUtils.pdfDocToWebResponse(
document, header + ".pdf", tempFileManager);
} else {
log.info("File has no good title to be found");
return WebResponseUtils.pdfDocToWebResponse(
document, Filenames.toSimpleFileName(file.getOriginalFilename()));
document,
Filenames.toSimpleFileName(file.getOriginalFilename()),
tempFileManager);
}
}
}
@@ -22,6 +22,7 @@ import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;
import com.google.zxing.*;
import com.google.zxing.common.GlobalHistogramBinarizer;
@@ -275,8 +276,8 @@ public class AutoSplitPdfController {
+ " splits the document at the QR code boundaries. The output is a zip"
+ " file containing each separate PDF document. Input:PDF Output:ZIP-PDF"
+ " Type:SISO")
public ResponseEntity<byte[]> autoSplitPdf(@ModelAttribute AutoSplitPdfRequest request)
throws IOException {
public ResponseEntity<StreamingResponseBody> autoSplitPdf(
@ModelAttribute AutoSplitPdfRequest request) throws IOException {
MultipartFile file = request.getFileInput();
boolean duplexMode = Boolean.TRUE.equals(request.getDuplexMode());
@@ -287,8 +288,8 @@ public class AutoSplitPdfController {
duplexMode);
List<PDDocument> splitDocuments = new ArrayList<>();
try (TempFile outputTempFile = new TempFile(tempFileManager, ".zip");
PDDocument document = pdfDocumentFactory.load(file.getInputStream())) {
TempFile outputTempFile = new TempFile(tempFileManager, ".zip");
try (PDDocument document = pdfDocumentFactory.load(file.getInputStream())) {
int totalPages = document.getNumberOfPages();
log.info("PDF loaded, totalPages={}", totalPages);
@@ -357,11 +358,10 @@ public class AutoSplitPdfController {
}
}
byte[] data = Files.readAllBytes(outputTempFile.getPath());
return WebResponseUtils.bytesToWebResponse(
data, filename + ".zip", MediaType.APPLICATION_OCTET_STREAM);
return WebResponseUtils.zipFileToWebResponse(outputTempFile, filename + ".zip");
} catch (Exception e) {
outputTempFile.close();
log.error("Error in auto split", e);
throw e;
} finally {
@@ -1,8 +1,9 @@
package stirling.software.SPDF.controller.api.misc;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
@@ -19,6 +20,7 @@ import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;
import io.github.pixee.security.Filenames;
import io.swagger.v3.oas.annotations.Operation;
@@ -35,6 +37,8 @@ import stirling.software.common.util.ApplicationContextProvider;
import stirling.software.common.util.ExceptionUtils;
import stirling.software.common.util.GeneralUtils;
import stirling.software.common.util.PdfUtils;
import stirling.software.common.util.TempFile;
import stirling.software.common.util.TempFileManager;
import stirling.software.common.util.WebResponseUtils;
@MiscApi
@@ -43,6 +47,7 @@ import stirling.software.common.util.WebResponseUtils;
public class BlankPageController {
private final CustomPDFDocumentFactory pdfDocumentFactory;
private final TempFileManager tempFileManager;
public static boolean isBlankImage(
BufferedImage image, int threshold, double whitePercent, int blurSize) {
@@ -83,7 +88,8 @@ public class BlankPageController {
"This endpoint removes blank pages from a given PDF file. Users can specify the"
+ " threshold and white percentage to tune the detection of blank pages."
+ " Input:PDF Output:PDF Type:SISO")
public ResponseEntity<byte[]> removeBlankPages(@ModelAttribute RemoveBlankPagesRequest request)
public ResponseEntity<StreamingResponseBody> removeBlankPages(
@ModelAttribute RemoveBlankPagesRequest request)
throws IOException, InterruptedException {
MultipartFile inputFile = request.getFileInput();
int threshold = request.getThreshold();
@@ -149,28 +155,29 @@ public class BlankPageController {
pageIndex++;
}
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ZipOutputStream zos = new ZipOutputStream(baos);
String filename =
GeneralUtils.removeExtension(
Filenames.toSimpleFileName(inputFile.getOriginalFilename()));
if (!nonBlankPages.isEmpty()) {
createZipEntry(zos, nonBlankPages, filename + "_nonBlankPages.pdf");
} else {
createZipEntry(zos, blankPages, filename + "_allBlankPages.pdf");
}
TempFile tempOut = tempFileManager.createManagedTempFile(".zip");
try (OutputStream fos = Files.newOutputStream(tempOut.getFile().toPath());
ZipOutputStream zos = new ZipOutputStream(fos)) {
if (!nonBlankPages.isEmpty()) {
createZipEntry(zos, nonBlankPages, filename + "_nonBlankPages.pdf");
} else {
createZipEntry(zos, blankPages, filename + "_allBlankPages.pdf");
}
if (!nonBlankPages.isEmpty() && !blankPages.isEmpty()) {
createZipEntry(zos, blankPages, filename + "_blankPages.pdf");
if (!nonBlankPages.isEmpty() && !blankPages.isEmpty()) {
createZipEntry(zos, blankPages, filename + "_blankPages.pdf");
}
} catch (IOException e) {
tempOut.close();
throw e;
}
zos.close();
log.info("Returning ZIP file: {}", filename + "_processed.zip");
return WebResponseUtils.baosToWebResponse(
baos, filename + "_processed.zip", MediaType.APPLICATION_OCTET_STREAM);
return WebResponseUtils.zipFileToWebResponse(tempOut, filename + "_processed.zip");
} catch (ExceptionUtils.OutOfMemoryDpiException e) {
throw e;
@@ -38,6 +38,7 @@ import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.server.ResponseStatusException;
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;
import io.swagger.v3.oas.annotations.Operation;
@@ -328,7 +329,8 @@ public class CompressController {
+ "_"
+ image.getBitsPerComponent();
return bytesToHexString(generateMD5(enhancedData.getBytes()));
return bytesToHexString(
generateMD5(enhancedData.getBytes(StandardCharsets.UTF_8)));
}
return "empty-stream";
}
@@ -727,7 +729,8 @@ public class CompressController {
params.append("_").append(image.getDecode().toString());
}
return bytesToHexString(generateMD5(params.toString().getBytes()));
return bytesToHexString(
generateMD5(params.toString().getBytes(StandardCharsets.UTF_8)));
} catch (Exception e) {
return "fallback-decode-" + System.identityHashCode(image);
}
@@ -798,7 +801,8 @@ public class CompressController {
metadata.append("_softmask");
}
return bytesToHexString(generateMD5(metadata.toString().getBytes()));
return bytesToHexString(
generateMD5(metadata.toString().getBytes(StandardCharsets.UTF_8)));
} catch (Exception e) {
return "fallback-meta-" + System.identityHashCode(image);
}
@@ -924,8 +928,8 @@ public class CompressController {
description =
"This endpoint accepts a PDF file and optimizes it based on the provided"
+ " parameters. Input:PDF Output:PDF Type:SISO")
public ResponseEntity<byte[]> optimizePdf(@ModelAttribute OptimizePdfRequest request)
throws Exception {
public ResponseEntity<StreamingResponseBody> optimizePdf(
@ModelAttribute OptimizePdfRequest request) throws Exception {
MultipartFile inputFile = request.getFileInput();
// Validate input file
@@ -1097,7 +1101,8 @@ public class CompressController {
try {
try (PDDocument document = pdfDocumentFactory.load(currentFile.toFile())) {
return WebResponseUtils.pdfDocToWebResponse(document, outputFilename);
return WebResponseUtils.pdfDocToWebResponse(
document, outputFilename, tempFileManager);
}
} catch (IOException e) {
throw ExceptionUtils.handlePdfException(e, "PDF optimization");
@@ -1,6 +1,5 @@
package stirling.software.SPDF.controller.api.misc;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.HashSet;
@@ -14,6 +13,7 @@ import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;
import io.swagger.v3.oas.annotations.Operation;
@@ -26,6 +26,8 @@ import stirling.software.common.model.api.PDFFile;
import stirling.software.common.service.CustomPDFDocumentFactory;
import stirling.software.common.util.ExceptionUtils;
import stirling.software.common.util.GeneralUtils;
import stirling.software.common.util.TempFile;
import stirling.software.common.util.TempFileManager;
import stirling.software.common.util.WebResponseUtils;
@MiscApi
@@ -34,12 +36,13 @@ import stirling.software.common.util.WebResponseUtils;
public class DecompressPdfController {
private final CustomPDFDocumentFactory pdfDocumentFactory;
private final TempFileManager tempFileManager;
@AutoJobPostMapping(value = "/decompress-pdf", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
@Operation(
summary = "Decompress PDF streams",
description = "Fully decompresses all PDF streams including text content")
public ResponseEntity<byte[]> decompressPdf(@ModelAttribute PDFFile request)
public ResponseEntity<StreamingResponseBody> decompressPdf(@ModelAttribute PDFFile request)
throws IOException {
MultipartFile file = request.getFileInput();
@@ -48,13 +51,18 @@ public class DecompressPdfController {
// Process all objects in document
processAllObjects(document);
// Save with explicit no compression
ByteArrayOutputStream baos = new ByteArrayOutputStream();
document.save(baos, CompressParameters.NO_COMPRESSION);
// Save with explicit no compression to a temp file
TempFile tempOut = tempFileManager.createManagedTempFile(".pdf");
try {
document.save(tempOut.getFile(), CompressParameters.NO_COMPRESSION);
} catch (IOException e) {
tempOut.close();
throw e;
}
// Return the PDF as a response
return WebResponseUtils.bytesToWebResponse(
baos.toByteArray(),
// Return the PDF as a streaming response
return WebResponseUtils.pdfFileToWebResponse(
tempOut,
GeneralUtils.generateFilename(file.getOriginalFilename(), "_decompressed.pdf"));
}
}
@@ -1,8 +1,8 @@
package stirling.software.SPDF.controller.api.misc;
import java.awt.image.BufferedImage;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
@@ -21,6 +21,7 @@ import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;
import io.swagger.v3.oas.annotations.Operation;
@@ -39,6 +40,8 @@ import stirling.software.common.util.ExceptionUtils;
import stirling.software.common.util.GeneralUtils;
import stirling.software.common.util.ProcessExecutor;
import stirling.software.common.util.ProcessExecutor.ProcessExecutorResult;
import stirling.software.common.util.TempFile;
import stirling.software.common.util.TempFileManager;
import stirling.software.common.util.WebResponseUtils;
@MiscApi
@@ -49,6 +52,7 @@ public class ExtractImageScansController {
private static final String REPLACEFIRST = "[.][^.]+$";
private final CustomPDFDocumentFactory pdfDocumentFactory;
private final TempFileManager tempFileManager;
@AutoJobPostMapping(
consumes = MediaType.MULTIPART_FORM_DATA_VALUE,
@@ -61,7 +65,7 @@ public class ExtractImageScansController {
+ " parameters. Users can specify angle threshold, tolerance, minimum area,"
+ " minimum contour area, and border size. Input:PDF Output:IMAGE/ZIP"
+ " Type:SIMO")
public ResponseEntity<byte[]> extractImageScans(
public ResponseEntity<StreamingResponseBody> extractImageScans(
@ModelAttribute ExtractImageScansRequest request)
throws IOException, InterruptedException {
MultipartFile inputFile = request.getFileInput();
@@ -71,9 +75,8 @@ public class ExtractImageScansController {
List<String> images = new ArrayList<>();
List<Path> tempImageFiles = new ArrayList<>();
Path tempInputFile;
Path tempZipFile = null;
List<TempFile> tempImageFiles = new ArrayList<>();
TempFile tempInputFile = null;
List<Path> tempDirs = new ArrayList<>();
if (!CheckProgramInstall.isPythonAvailable()) {
@@ -83,6 +86,8 @@ public class ExtractImageScansController {
String pythonVersion = CheckProgramInstall.getAvailablePythonCommand();
Path splitPhotosScript = GeneralUtils.extractScript("split_photos.py");
TempFile finalOutput = null;
boolean finalOutputOwnershipTransferred = false;
try {
// Check if input file is a PDF
if ("pdf".equalsIgnoreCase(extension)) {
@@ -96,7 +101,8 @@ public class ExtractImageScansController {
// Create images of all pages
for (int i = 0; i < pageCount; i++) {
// Create temp file to save the image
Path tempFile = Files.createTempFile("image_", ".png");
TempFile tempImage = tempFileManager.createManagedTempFile(".png");
tempImageFiles.add(tempImage);
// Render image and save as temp file
BufferedImage image;
@@ -116,18 +122,17 @@ public class ExtractImageScansController {
pageIndex + 1,
dpi,
() -> pdfRenderer.renderImageWithDPI(pageIndex, dpi));
ImageIO.write(image, "png", tempFile.toFile());
ImageIO.write(image, "png", tempImage.getFile());
// Add temp file path to images list
images.add(tempFile.toString());
tempImageFiles.add(tempFile);
images.add(tempImage.getAbsolutePath());
}
}
} else {
tempInputFile = Files.createTempFile("input_", "." + extension);
inputFile.transferTo(tempInputFile);
tempInputFile = tempFileManager.createManagedTempFile("." + extension);
inputFile.transferTo(tempInputFile.getFile());
// Add input file path to images list
images.add(tempInputFile.toString());
images.add(tempInputFile.getAbsolutePath());
}
List<byte[]> processedImageBytes = new ArrayList<>();
@@ -177,10 +182,10 @@ public class ExtractImageScansController {
if (processedImageBytes.size() > 1) {
String outputZipFilename =
GeneralUtils.generateFilename(fileName, "_processed.zip");
tempZipFile = Files.createTempFile("output_", ".zip");
finalOutput = tempFileManager.createManagedTempFile(".zip");
try (ZipOutputStream zipOut =
new ZipOutputStream(new FileOutputStream(tempZipFile.toFile()))) {
new ZipOutputStream(Files.newOutputStream(finalOutput.getPath()))) {
// Add processed images to the zip
for (int i = 0; i < processedImageBytes.size(); i++) {
ZipEntry entry =
@@ -193,13 +198,10 @@ public class ExtractImageScansController {
}
}
byte[] zipBytes = Files.readAllBytes(tempZipFile);
// Clean up the temporary zip file
Files.deleteIfExists(tempZipFile);
return WebResponseUtils.bytesToWebResponse(
zipBytes, outputZipFilename, MediaType.APPLICATION_OCTET_STREAM);
ResponseEntity<StreamingResponseBody> response =
WebResponseUtils.zipFileToWebResponse(finalOutput, outputZipFilename);
finalOutputOwnershipTransferred = true;
return response;
}
if (processedImageBytes.isEmpty()) {
throw ExceptionUtils.createIllegalArgumentException(
@@ -208,28 +210,28 @@ public class ExtractImageScansController {
// Return the processed image as a response
byte[] imageBytes = processedImageBytes.get(0);
return WebResponseUtils.bytesToWebResponse(
imageBytes,
GeneralUtils.generateFilename(fileName, ".png"),
MediaType.IMAGE_PNG);
finalOutput = tempFileManager.createManagedTempFile(".png");
try (OutputStream out = Files.newOutputStream(finalOutput.getPath())) {
out.write(imageBytes);
}
ResponseEntity<StreamingResponseBody> response =
WebResponseUtils.fileToWebResponse(
finalOutput,
GeneralUtils.generateFilename(fileName, ".png"),
MediaType.IMAGE_PNG);
finalOutputOwnershipTransferred = true;
return response;
}
} finally {
if (finalOutput != null && !finalOutputOwnershipTransferred) {
finalOutput.close();
}
// Cleanup logic for all temporary files and directories
tempImageFiles.forEach(
path -> {
try {
Files.deleteIfExists(path);
} catch (IOException e) {
log.error("Failed to delete temporary image file: {}", path, e);
}
});
tempImageFiles.forEach(TempFile::close);
if (tempZipFile != null && Files.exists(tempZipFile)) {
try {
Files.deleteIfExists(tempZipFile);
} catch (IOException e) {
log.error("Failed to delete temporary zip file: {}", tempZipFile, e);
}
if (tempInputFile != null) {
tempInputFile.close();
}
tempDirs.forEach(
@@ -15,6 +15,7 @@ import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;
import io.github.pixee.security.Filenames;
import io.swagger.v3.oas.annotations.Operation;
@@ -30,6 +31,7 @@ import stirling.software.common.model.ApplicationProperties;
import stirling.software.common.service.CustomPDFDocumentFactory;
import stirling.software.common.util.ApplicationContextProvider;
import stirling.software.common.util.ExceptionUtils;
import stirling.software.common.util.TempFileManager;
import stirling.software.common.util.WebResponseUtils;
@MiscApi
@@ -38,6 +40,7 @@ import stirling.software.common.util.WebResponseUtils;
public class FlattenController {
private final CustomPDFDocumentFactory pdfDocumentFactory;
private final TempFileManager tempFileManager;
@AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/flatten")
@StandardPdfResponse
@@ -46,7 +49,8 @@ public class FlattenController {
description =
"Flattening just PDF form fields or converting each page to images to make text"
+ " unselectable. Input:PDF, Output:PDF. Type:SISO")
public ResponseEntity<byte[]> flatten(@ModelAttribute FlattenRequest request) throws Exception {
public ResponseEntity<StreamingResponseBody> flatten(@ModelAttribute FlattenRequest request)
throws Exception {
MultipartFile file = request.getFileInput();
try (PDDocument document = pdfDocumentFactory.load(file)) {
@@ -58,7 +62,9 @@ public class FlattenController {
acroForm.flatten();
}
return WebResponseUtils.pdfDocToWebResponse(
document, Filenames.toSimpleFileName(file.getOriginalFilename()));
document,
Filenames.toSimpleFileName(file.getOriginalFilename()),
tempFileManager);
} else {
// flatten whole page aka convert each page to image and re-add it (making text
// unselectable)
@@ -143,7 +149,9 @@ public class FlattenController {
}
}
return WebResponseUtils.pdfDocToWebResponse(
newDocument, Filenames.toSimpleFileName(file.getOriginalFilename()));
newDocument,
Filenames.toSimpleFileName(file.getOriginalFilename()),
tempFileManager);
}
}
}
@@ -13,6 +13,7 @@ import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;
import io.github.pixee.security.Filenames;
import io.swagger.v3.oas.annotations.Operation;
@@ -28,6 +29,7 @@ import stirling.software.common.service.CustomPDFDocumentFactory;
import stirling.software.common.service.PdfMetadataService;
import stirling.software.common.util.GeneralUtils;
import stirling.software.common.util.RegexPatternUtils;
import stirling.software.common.util.TempFileManager;
import stirling.software.common.util.WebResponseUtils;
import stirling.software.common.util.propertyeditor.StringToMapPropertyEditor;
@@ -37,6 +39,7 @@ import stirling.software.common.util.propertyeditor.StringToMapPropertyEditor;
public class MetadataController {
private final CustomPDFDocumentFactory pdfDocumentFactory;
private final TempFileManager tempFileManager;
private String checkUndefined(String entry) {
// Check if the string is "undefined"
@@ -61,7 +64,7 @@ public class MetadataController {
"This endpoint allows you to update the metadata of a given PDF file. You can"
+ " add, modify, or delete standard and custom metadata fields. Input:PDF"
+ " Output:PDF Type:SISO")
public ResponseEntity<byte[]> metadata(@ModelAttribute MetadataRequest request)
public ResponseEntity<StreamingResponseBody> metadata(@ModelAttribute MetadataRequest request)
throws IOException {
// Extract PDF file from the request object
@@ -179,7 +182,8 @@ public class MetadataController {
document,
GeneralUtils.removeExtension(
Filenames.toSimpleFileName(pdfFile.getOriginalFilename()))
+ "_metadata.pdf");
+ "_metadata.pdf",
tempFileManager);
}
}
}
@@ -25,6 +25,7 @@ import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;
import io.github.pixee.security.Filenames;
import io.swagger.v3.oas.annotations.Operation;
@@ -89,7 +90,7 @@ public class OCRController {
+ " specify languages, sidecar, deskew, clean, cleanFinal, ocrType, ocrRenderType,"
+ " and removeImagesAfter options. Uses OCRmyPDF if available, falls back to"
+ " Tesseract. Input:PDF Output:PDF Type:SI-Conditional")
public ResponseEntity<byte[]> processPdfWithOCR(
public ResponseEntity<StreamingResponseBody> processPdfWithOCR(
@ModelAttribute ProcessPdfWithOcrRequest request)
throws IOException, InterruptedException {
MultipartFile inputFile = request.getFileInput();
@@ -121,9 +122,11 @@ public class OCRController {
throw ExceptionUtils.createOcrInvalidLanguagesException();
}
// Use try-with-resources for proper temp file management
TempFile tempOutputFile = new TempFile(tempFileManager, ".pdf");
TempFile tempZipFile = null;
boolean pdfOwnershipTransferred = false;
boolean zipOwnershipTransferred = false;
try (TempFile tempInputFile = new TempFile(tempFileManager, ".pdf");
TempFile tempOutputFile = new TempFile(tempFileManager, ".pdf");
TempFile sidecarTextFile = sidecar ? new TempFile(tempFileManager, ".txt") : null) {
inputFile.transferTo(tempInputFile.getFile());
@@ -156,9 +159,6 @@ public class OCRController {
throw ExceptionUtils.createOcrToolsUnavailableException();
}
// Read the processed PDF file
byte[] pdfBytes = Files.readAllBytes(tempOutputFile.getPath());
// Return the OCR processed PDF as a response
String outputFilename =
GeneralUtils.removeExtension(
@@ -172,14 +172,14 @@ public class OCRController {
Filenames.toSimpleFileName(inputFile.getOriginalFilename()))
+ "_OCR.zip";
try (TempFile tempZipFile = new TempFile(tempFileManager, ".zip");
ZipOutputStream zipOut =
new ZipOutputStream(Files.newOutputStream(tempZipFile.getPath()))) {
tempZipFile = new TempFile(tempFileManager, ".zip");
try (ZipOutputStream zipOut =
new ZipOutputStream(Files.newOutputStream(tempZipFile.getPath()))) {
// Add PDF file to the zip
ZipEntry pdfEntry = new ZipEntry(outputFilename);
zipOut.putNextEntry(pdfEntry);
zipOut.write(pdfBytes);
Files.copy(tempOutputFile.getPath(), zipOut);
zipOut.closeEntry();
// Add text file to the zip
@@ -189,16 +189,28 @@ public class OCRController {
zipOut.closeEntry();
zipOut.finish();
byte[] zipBytes = Files.readAllBytes(tempZipFile.getPath());
// Return the zip file containing both the PDF and the text file
return WebResponseUtils.bytesToWebResponse(
zipBytes, outputZipFilename, MediaType.APPLICATION_OCTET_STREAM);
}
// The intermediate PDF temp file is no longer needed; only the zip is streamed.
tempOutputFile.close();
pdfOwnershipTransferred = true;
ResponseEntity<StreamingResponseBody> response =
WebResponseUtils.fileToWebResponse(
tempZipFile, outputZipFilename, MediaType.APPLICATION_OCTET_STREAM);
zipOwnershipTransferred = true;
return response;
} else {
// Return the OCR processed PDF as a response
return WebResponseUtils.bytesToWebResponse(pdfBytes, outputFilename);
ResponseEntity<StreamingResponseBody> response =
WebResponseUtils.pdfFileToWebResponse(tempOutputFile, outputFilename);
pdfOwnershipTransferred = true;
return response;
}
} finally {
if (!pdfOwnershipTransferred) {
tempOutputFile.close();
}
if (tempZipFile != null && !zipOwnershipTransferred) {
tempZipFile.close();
}
}
}
@@ -1,6 +1,5 @@
package stirling.software.SPDF.controller.api.misc;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import org.apache.pdfbox.pdmodel.PDDocument;
@@ -12,6 +11,7 @@ import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;
import io.swagger.v3.oas.annotations.Operation;
@@ -24,6 +24,8 @@ import stirling.software.common.annotations.AutoJobPostMapping;
import stirling.software.common.annotations.api.MiscApi;
import stirling.software.common.service.CustomPDFDocumentFactory;
import stirling.software.common.util.GeneralUtils;
import stirling.software.common.util.TempFile;
import stirling.software.common.util.TempFileManager;
import stirling.software.common.util.WebResponseUtils;
@MiscApi
@@ -32,6 +34,7 @@ import stirling.software.common.util.WebResponseUtils;
public class OverlayImageController {
private final CustomPDFDocumentFactory pdfDocumentFactory;
private final TempFileManager tempFileManager;
@AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/add-image")
@Operation(
@@ -42,7 +45,8 @@ public class OverlayImageController {
+ "SVG files are rendered as vector graphics for crisp output at any resolution. "
+ "The image can be overlaid on every page of the PDF if specified. "
+ "Input:PDF/IMAGE/SVG Output:PDF Type:SISO")
public ResponseEntity<byte[]> overlayImage(@ModelAttribute OverlayImageRequest request) {
public ResponseEntity<StreamingResponseBody> overlayImage(
@ModelAttribute OverlayImageRequest request) {
MultipartFile pdfFile = request.getFileInput();
MultipartFile imageFile = request.getImageFile();
float x = request.getX();
@@ -82,14 +86,17 @@ public class OverlayImageController {
}
}
ByteArrayOutputStream baos = new ByteArrayOutputStream();
document.save(baos);
byte[] result = baos.toByteArray();
TempFile tempOut = tempFileManager.createManagedTempFile(".pdf");
try {
document.save(tempOut.getFile());
} catch (IOException e) {
tempOut.close();
throw e;
}
log.info("PDF with overlaid image successfully created");
return WebResponseUtils.bytesToWebResponse(
result,
return WebResponseUtils.pdfFileToWebResponse(
tempOut,
GeneralUtils.generateFilename(
pdfFile.getOriginalFilename(), "_overlayed.pdf"));
}
@@ -1,7 +1,6 @@
package stirling.software.SPDF.controller.api.misc;
import java.awt.Color;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.List;
import java.util.Locale;
@@ -16,6 +15,7 @@ import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;
import io.github.pixee.security.Filenames;
import io.swagger.v3.oas.annotations.Operation;
@@ -28,6 +28,8 @@ import stirling.software.common.annotations.AutoJobPostMapping;
import stirling.software.common.annotations.api.MiscApi;
import stirling.software.common.service.CustomPDFDocumentFactory;
import stirling.software.common.util.GeneralUtils;
import stirling.software.common.util.TempFile;
import stirling.software.common.util.TempFileManager;
import stirling.software.common.util.WebResponseUtils;
@MiscApi
@@ -35,6 +37,7 @@ import stirling.software.common.util.WebResponseUtils;
public class PageNumbersController {
private final CustomPDFDocumentFactory pdfDocumentFactory;
private final TempFileManager tempFileManager;
@AutoJobPostMapping(value = "/add-page-numbers", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
@StandardPdfResponse
@@ -43,8 +46,8 @@ public class PageNumbersController {
description =
"This operation takes an input PDF file and adds page numbers to it. Input:PDF"
+ " Output:PDF Type:SISO")
public ResponseEntity<byte[]> addPageNumbers(@ModelAttribute AddPageNumbersRequest request)
throws IOException {
public ResponseEntity<StreamingResponseBody> addPageNumbers(
@ModelAttribute AddPageNumbersRequest request) throws IOException {
MultipartFile file = request.getFileInput();
String customMargin = request.getCustomMargin();
@@ -175,11 +178,16 @@ public class PageNumbersController {
pageNumber++;
}
ByteArrayOutputStream baos = new ByteArrayOutputStream();
document.save(baos);
TempFile tempOut = tempFileManager.createManagedTempFile(".pdf");
try {
document.save(tempOut.getFile());
} catch (IOException e) {
tempOut.close();
throw e;
}
return WebResponseUtils.bytesToWebResponse(
baos.toByteArray(),
return WebResponseUtils.pdfFileToWebResponse(
tempOut,
GeneralUtils.generateFilename(
file.getOriginalFilename(), "_page_numbers_added.pdf"));
}
@@ -1,6 +1,5 @@
package stirling.software.SPDF.controller.api.misc;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
@@ -17,6 +16,7 @@ import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;
import io.swagger.v3.oas.annotations.Operation;
@@ -29,6 +29,8 @@ import stirling.software.common.model.api.PDFFile;
import stirling.software.common.service.CustomPDFDocumentFactory;
import stirling.software.common.util.ExceptionUtils;
import stirling.software.common.util.GeneralUtils;
import stirling.software.common.util.TempFile;
import stirling.software.common.util.TempFileManager;
import stirling.software.common.util.WebResponseUtils;
@GeneralApi
@@ -37,6 +39,7 @@ import stirling.software.common.util.WebResponseUtils;
public class RemoveImagesController {
private final CustomPDFDocumentFactory pdfDocumentFactory;
private final TempFileManager tempFileManager;
@AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/remove-image-pdf")
@Operation(
@@ -44,7 +47,8 @@ public class RemoveImagesController {
description =
"This endpoint removes all embedded images from a PDF file and returns the"
+ " modified document. Input:PDF Output:PDF Type:SISO")
public ResponseEntity<byte[]> removeImages(@ModelAttribute PDFFile request) throws IOException {
public ResponseEntity<StreamingResponseBody> removeImages(@ModelAttribute PDFFile request)
throws IOException {
MultipartFile inputFile = request.getFileInput();
@@ -60,12 +64,16 @@ public class RemoveImagesController {
log.info("Removed {} images from PDF with {} pages", imagesRemoved, totalPages);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
pdfDoc.save(baos);
byte[] pdfContent = baos.toByteArray();
TempFile tempOut = tempFileManager.createManagedTempFile(".pdf");
try {
pdfDoc.save(tempOut.getFile());
} catch (IOException e) {
tempOut.close();
throw e;
}
return WebResponseUtils.bytesToWebResponse(
pdfContent,
return WebResponseUtils.pdfFileToWebResponse(
tempOut,
GeneralUtils.generateFilename(
inputFile.getOriginalFilename(), "_images_removed.pdf"));
@@ -8,6 +8,7 @@ import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;
import io.swagger.v3.oas.annotations.Operation;
@@ -53,13 +54,12 @@ public class RepairController {
"This endpoint repairs a given PDF file by running Ghostscript (primary), qpdf (fallback), or PDFBox (if no external tools available). The PDF is"
+ " first saved to a temporary location, repaired, read back, and then"
+ " returned as a response. Input:PDF Output:PDF Type:SISO")
public ResponseEntity<byte[]> repairPdf(@ModelAttribute PDFFile file)
public ResponseEntity<StreamingResponseBody> repairPdf(@ModelAttribute PDFFile file)
throws IOException, InterruptedException {
MultipartFile inputFile = file.getFileInput();
// Use TempFile with try-with-resources for automatic cleanup
try (TempFile tempInputFile = new TempFile(tempFileManager, ".pdf");
TempFile tempOutputFile = new TempFile(tempFileManager, ".pdf")) {
TempFile tempOutputFile = new TempFile(tempFileManager, ".pdf");
try (TempFile tempInputFile = new TempFile(tempFileManager, ".pdf")) {
// Save the uploaded file to the temporary location
inputFile.transferTo(tempInputFile.getFile());
@@ -121,14 +121,17 @@ public class RepairController {
}
}
// Read the repaired PDF file
byte[] pdfBytes = pdfDocumentFactory.loadToBytes(tempOutputFile.getFile());
// Return the repaired PDF as a response
return WebResponseUtils.bytesToWebResponse(
pdfBytes,
// Return the repaired PDF as a streaming response
return WebResponseUtils.pdfFileToWebResponse(
tempOutputFile,
GeneralUtils.generateFilename(
inputFile.getOriginalFilename(), "_repaired.pdf"));
} catch (IOException | InterruptedException e) {
tempOutputFile.close();
throw e;
} catch (RuntimeException e) {
tempOutputFile.close();
throw e;
}
}
}
@@ -1,11 +1,15 @@
package stirling.software.SPDF.controller.api.misc;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.StandardCopyOption;
import org.springframework.core.io.InputStreamResource;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;
import io.swagger.v3.oas.annotations.Operation;
@@ -16,6 +20,8 @@ import stirling.software.SPDF.service.misc.ReplaceAndInvertColorService;
import stirling.software.common.annotations.AutoJobPostMapping;
import stirling.software.common.annotations.api.MiscApi;
import stirling.software.common.util.GeneralUtils;
import stirling.software.common.util.TempFile;
import stirling.software.common.util.TempFileManager;
import stirling.software.common.util.WebResponseUtils;
@MiscApi
@@ -23,6 +29,7 @@ import stirling.software.common.util.WebResponseUtils;
public class ReplaceAndInvertColorController {
private final ReplaceAndInvertColorService replaceAndInvertColorService;
private final TempFileManager tempFileManager;
@AutoJobPostMapping(
consumes = MediaType.MULTIPART_FORM_DATA_VALUE,
@@ -32,7 +39,7 @@ public class ReplaceAndInvertColorController {
description =
"This endpoint accepts a PDF file and provides options to invert all colors, replace"
+ " text and background colors, or convert to CMYK color space for printing. Input:PDF Output:PDF Type:SISO")
public ResponseEntity<byte[]> replaceAndInvertColor(
public ResponseEntity<StreamingResponseBody> replaceAndInvertColor(
@ModelAttribute ReplaceAndInvertColorRequest request) throws IOException {
InputStreamResource resource =
@@ -47,7 +54,15 @@ public class ReplaceAndInvertColorController {
String filename =
GeneralUtils.generateFilename(
request.getFileInput().getOriginalFilename(), "_inverted.pdf");
return WebResponseUtils.bytesToWebResponse(
resource.getContentAsByteArray(), filename, MediaType.APPLICATION_PDF);
TempFile tempOut = tempFileManager.createManagedTempFile(".pdf");
try (InputStream in = resource.getInputStream()) {
Files.copy(in, tempOut.getFile().toPath(), StandardCopyOption.REPLACE_EXISTING);
} catch (IOException e) {
tempOut.close();
throw e;
}
return WebResponseUtils.pdfFileToWebResponse(tempOut, filename);
}
}
@@ -6,7 +6,6 @@ import java.awt.RenderingHints;
import java.awt.geom.AffineTransform;
import java.awt.image.BufferedImage;
import java.awt.image.DataBufferInt;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.file.Files;
@@ -35,6 +34,7 @@ import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;
import io.swagger.v3.oas.annotations.Operation;
@@ -51,6 +51,7 @@ import stirling.software.common.service.CustomPDFDocumentFactory;
import stirling.software.common.util.ApplicationContextProvider;
import stirling.software.common.util.ExceptionUtils;
import stirling.software.common.util.GeneralUtils;
import stirling.software.common.util.TempFileManager;
import stirling.software.common.util.WebResponseUtils;
@MiscApi
@@ -59,6 +60,7 @@ import stirling.software.common.util.WebResponseUtils;
public class ScannerEffectController {
private final CustomPDFDocumentFactory pdfDocumentFactory;
private final TempFileManager tempFileManager;
private static final int MAX_IMAGE_WIDTH = 8192;
private static final int MAX_IMAGE_HEIGHT = 8192;
private static final long MAX_IMAGE_PIXELS = 16_777_216; // 4096x4096
@@ -562,8 +564,8 @@ public class ScannerEffectController {
summary = "Apply scanner effect to PDF",
description =
"Applies various effects to simulate a scanned document, including rotation, noise, and edge softening. Input:PDF Output:PDF Type:SISO")
public ResponseEntity<byte[]> scannerEffect(@Valid @ModelAttribute ScannerEffectRequest request)
throws IOException {
public ResponseEntity<StreamingResponseBody> scannerEffect(
@Valid @ModelAttribute ScannerEffectRequest request) throws IOException {
MultipartFile file = request.getFileInput();
List<Path> tempFiles = new ArrayList<>();
@@ -624,8 +626,7 @@ public class ScannerEffectController {
sharedPdfBytes != null
? pdfDocumentFactory.load(sharedPdfBytes)
: pdfDocumentFactory.load(processingInput);
PDDocument outputDocument = new PDDocument();
ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
PDDocument outputDocument = new PDDocument()) {
int totalPages = document.getNumberOfPages();
if (totalPages == 0) {
@@ -708,12 +709,11 @@ public class ScannerEffectController {
writeProcessedPagesToDocument(processedPages, outputDocument);
outputDocument.save(outputStream);
return WebResponseUtils.bytesToWebResponse(
outputStream.toByteArray(),
return WebResponseUtils.pdfDocToWebResponse(
outputDocument,
GeneralUtils.generateFilename(
file.getOriginalFilename(), "_scanner_effect.pdf"));
file.getOriginalFilename(), "_scanner_effect.pdf"),
tempFileManager);
}
}
} finally {
@@ -1,6 +1,7 @@
package stirling.software.SPDF.controller.api.misc;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.Map;
import org.apache.pdfbox.pdmodel.PDDocument;
@@ -10,6 +11,7 @@ import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;
import io.github.pixee.security.Filenames;
import io.swagger.v3.oas.annotations.Operation;
@@ -21,6 +23,8 @@ import stirling.software.common.annotations.AutoJobPostMapping;
import stirling.software.common.annotations.api.MiscApi;
import stirling.software.common.model.api.PDFFile;
import stirling.software.common.service.CustomPDFDocumentFactory;
import stirling.software.common.util.TempFile;
import stirling.software.common.util.TempFileManager;
import stirling.software.common.util.WebResponseUtils;
@MiscApi
@@ -28,13 +32,15 @@ import stirling.software.common.util.WebResponseUtils;
public class ShowJavascript {
private final CustomPDFDocumentFactory pdfDocumentFactory;
private final TempFileManager tempFileManager;
@AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/show-javascript")
@JavaScriptResponse
@Operation(
summary = "Grabs all JS from a PDF and returns a single JS file with all code",
description = "desc. Input:PDF Output:JS Type:SISO")
public ResponseEntity<byte[]> extractHeader(@ModelAttribute PDFFile file) throws Exception {
public ResponseEntity<StreamingResponseBody> extractHeader(@ModelAttribute PDFFile file)
throws Exception {
MultipartFile inputFile = file.getFileInput();
StringBuilder script = new StringBuilder();
boolean foundScript = false;
@@ -77,8 +83,17 @@ public class ShowJavascript {
.append("' does not contain Javascript");
}
return WebResponseUtils.bytesToWebResponse(
script.toString().getBytes(StandardCharsets.UTF_8),
TempFile tempOut = tempFileManager.createManagedTempFile(".js");
try {
Files.write(
tempOut.getFile().toPath(),
script.toString().getBytes(StandardCharsets.UTF_8));
} catch (Exception e) {
tempOut.close();
throw e;
}
return WebResponseUtils.fileToWebResponse(
tempOut,
Filenames.toSimpleFileName(inputFile.getOriginalFilename()) + ".js",
MediaType.TEXT_PLAIN);
}
@@ -37,6 +37,7 @@ import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.InitBinder;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;
import io.swagger.v3.oas.annotations.Operation;
@@ -92,7 +93,7 @@ public class StampController {
"This endpoint adds a stamp to a given PDF file. Users can specify the stamp"
+ " type (text or image), rotation, opacity, width spacer, and height"
+ " spacer. Input:PDF Output:PDF Type:SISO")
public ResponseEntity<byte[]> addStamp(@ModelAttribute AddStampRequest request)
public ResponseEntity<StreamingResponseBody> addStamp(@ModelAttribute AddStampRequest request)
throws IOException, Exception {
MultipartFile pdfFile = request.getFileInput();
String pdfFileName = pdfFile.getOriginalFilename();
@@ -199,7 +200,8 @@ public class StampController {
// Return the stamped PDF as a response
return WebResponseUtils.pdfDocToWebResponse(
document,
GeneralUtils.generateFilename(pdfFile.getOriginalFilename(), "_stamped.pdf"));
GeneralUtils.generateFilename(pdfFile.getOriginalFilename(), "_stamped.pdf"),
tempFileManager);
}
}
@@ -13,6 +13,7 @@ import org.apache.pdfbox.pdmodel.interactive.form.PDField;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;
import io.github.pixee.security.Filenames;
import io.swagger.v3.oas.annotations.Operation;
@@ -26,15 +27,19 @@ import stirling.software.common.model.api.PDFFile;
import stirling.software.common.service.CustomPDFDocumentFactory;
import stirling.software.common.util.GeneralUtils;
import stirling.software.common.util.RegexPatternUtils;
import stirling.software.common.util.TempFileManager;
import stirling.software.common.util.WebResponseUtils;
@MiscApi
@Slf4j
public class UnlockPDFFormsController {
private final CustomPDFDocumentFactory pdfDocumentFactory;
private final TempFileManager tempFileManager;
public UnlockPDFFormsController(CustomPDFDocumentFactory pdfDocumentFactory) {
public UnlockPDFFormsController(
CustomPDFDocumentFactory pdfDocumentFactory, TempFileManager tempFileManager) {
this.pdfDocumentFactory = pdfDocumentFactory;
this.tempFileManager = tempFileManager;
}
@AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/unlock-pdf-forms")
@@ -44,7 +49,7 @@ public class UnlockPDFFormsController {
description =
"Removing read-only property from form fields making them fillable"
+ "Input:PDF, Output:PDF. Type:SISO")
public ResponseEntity<byte[]> unlockPDFForms(@ModelAttribute PDFFile file) {
public ResponseEntity<StreamingResponseBody> unlockPDFForms(@ModelAttribute PDFFile file) {
try (PDDocument document = pdfDocumentFactory.load(file)) {
PDAcroForm acroForm = document.getDocumentCatalog().getAcroForm();
@@ -116,7 +121,7 @@ public class UnlockPDFFormsController {
GeneralUtils.generateFilename(
file.getFileInput().getOriginalFilename(), "_unlocked_forms.pdf");
return WebResponseUtils.pdfDocToWebResponse(
document, Filenames.toSimpleFileName(mergedFileName));
document, Filenames.toSimpleFileName(mergedFileName), tempFileManager);
} catch (Exception e) {
log.error(e.getMessage(), e);
}
@@ -64,6 +64,7 @@ import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;
import io.micrometer.common.util.StringUtils;
import io.swagger.v3.oas.annotations.Operation;
@@ -78,6 +79,8 @@ import stirling.software.common.service.CustomPDFDocumentFactory;
import stirling.software.common.service.ServerCertificateServiceInterface;
import stirling.software.common.util.ExceptionUtils;
import stirling.software.common.util.GeneralUtils;
import stirling.software.common.util.TempFile;
import stirling.software.common.util.TempFileManager;
import stirling.software.common.util.WebResponseUtils;
@RestController
@@ -104,13 +107,15 @@ public class CertSignController {
private final CustomPDFDocumentFactory pdfDocumentFactory;
private final ServerCertificateServiceInterface serverCertificateService;
private final TempFileManager tempFileManager;
public CertSignController(
CustomPDFDocumentFactory pdfDocumentFactory,
@Autowired(required = false)
ServerCertificateServiceInterface serverCertificateService) {
@Autowired(required = false) ServerCertificateServiceInterface serverCertificateService,
TempFileManager tempFileManager) {
this.pdfDocumentFactory = pdfDocumentFactory;
this.serverCertificateService = serverCertificateService;
this.tempFileManager = tempFileManager;
}
public static void sign(
@@ -163,8 +168,8 @@ public class CertSignController {
"This endpoint accepts a PDF file, a digital certificate and related"
+ " information to sign the PDF. It then returns the digitally signed PDF"
+ " file. Input:PDF Output:PDF Type:SISO")
public ResponseEntity<byte[]> signPDFWithCert(@ModelAttribute SignPDFWithCertRequest request)
throws Exception {
public ResponseEntity<StreamingResponseBody> signPDFWithCert(
@ModelAttribute SignPDFWithCertRequest request) throws Exception {
MultipartFile pdf = request.getFileInput();
String certType = request.getCertType();
MultipartFile privateKeyFile = request.getPrivateKeyFile();
@@ -246,22 +251,26 @@ public class CertSignController {
}
CreateSignature createSignature = new CreateSignature(ks, keystorePassword.toCharArray());
ByteArrayOutputStream baos = new ByteArrayOutputStream();
sign(
pdfDocumentFactory,
pdf,
baos,
createSignature,
showSignature,
pageNumber,
name,
location,
reason,
showLogo);
TempFile signedOut = tempFileManager.createManagedTempFile(".pdf");
try (OutputStream os = new FileOutputStream(signedOut.getFile())) {
sign(
pdfDocumentFactory,
pdf,
os,
createSignature,
showSignature,
pageNumber,
name,
location,
reason,
showLogo);
} catch (IOException e) {
signedOut.close();
throw e;
}
// Return the signed PDF
return WebResponseUtils.bytesToWebResponse(
baos.toByteArray(),
GeneralUtils.generateFilename(pdf.getOriginalFilename(), "_signed.pdf"));
return WebResponseUtils.pdfFileToWebResponse(
signedOut, GeneralUtils.generateFilename(pdf.getOriginalFilename(), "_signed.pdf"));
}
private MultipartFile validateFilePresent(
@@ -9,6 +9,7 @@ import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;
import io.swagger.v3.oas.annotations.Operation;
@@ -22,6 +23,7 @@ import stirling.software.common.annotations.api.SecurityApi;
import stirling.software.common.service.CustomPDFDocumentFactory;
import stirling.software.common.util.ExceptionUtils;
import stirling.software.common.util.GeneralUtils;
import stirling.software.common.util.TempFileManager;
import stirling.software.common.util.WebResponseUtils;
@SecurityApi
@@ -29,6 +31,7 @@ import stirling.software.common.util.WebResponseUtils;
public class PasswordController {
private final CustomPDFDocumentFactory pdfDocumentFactory;
private final TempFileManager tempFileManager;
@AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/remove-password")
@StandardPdfResponse
@@ -37,8 +40,8 @@ public class PasswordController {
description =
"This endpoint removes the password from a protected PDF file. Users need to"
+ " provide the existing password. Input:PDF Output:PDF Type:SISO")
public ResponseEntity<byte[]> removePassword(@ModelAttribute PDFPasswordRequest request)
throws IOException {
public ResponseEntity<StreamingResponseBody> removePassword(
@ModelAttribute PDFPasswordRequest request) throws IOException {
MultipartFile fileInput = request.getFileInput();
String password = request.getPassword();
@@ -47,7 +50,8 @@ public class PasswordController {
return WebResponseUtils.pdfDocToWebResponse(
document,
GeneralUtils.generateFilename(
fileInput.getOriginalFilename(), "_password_removed.pdf"));
fileInput.getOriginalFilename(), "_password_removed.pdf"),
tempFileManager);
} catch (IOException e) {
// Handle password errors specifically
if (ExceptionUtils.isPasswordError(e)) {
@@ -66,8 +70,8 @@ public class PasswordController {
"This endpoint adds password protection to a PDF file. Users can specify a set"
+ " of permissions that should be applied to the file. Input:PDF"
+ " Output:PDF")
public ResponseEntity<byte[]> addPassword(@ModelAttribute AddPasswordRequest request)
throws IOException {
public ResponseEntity<StreamingResponseBody> addPassword(
@ModelAttribute AddPasswordRequest request) throws IOException {
MultipartFile fileInput = request.getFileInput();
String ownerPassword = request.getOwnerPassword();
String password = request.getPassword();
@@ -108,11 +112,13 @@ public class PasswordController {
return WebResponseUtils.pdfDocToWebResponse(
document,
GeneralUtils.generateFilename(
fileInput.getOriginalFilename(), "_permissions.pdf"));
fileInput.getOriginalFilename(), "_permissions.pdf"),
tempFileManager);
return WebResponseUtils.pdfDocToWebResponse(
document,
GeneralUtils.generateFilename(
fileInput.getOriginalFilename(), "_passworded.pdf"));
fileInput.getOriginalFilename(), "_passworded.pdf"),
tempFileManager);
}
}
}
@@ -1,7 +1,6 @@
package stirling.software.SPDF.controller.api.security;
import java.awt.Color;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
@@ -40,6 +39,7 @@ import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.InitBinder;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;
import io.github.pixee.security.Filenames;
import io.swagger.v3.oas.annotations.Operation;
@@ -64,6 +64,8 @@ import stirling.software.common.service.CustomPDFDocumentFactory;
import stirling.software.common.util.ExceptionUtils;
import stirling.software.common.util.GeneralUtils;
import stirling.software.common.util.PdfUtils;
import stirling.software.common.util.TempFile;
import stirling.software.common.util.TempFileManager;
import stirling.software.common.util.WebResponseUtils;
import stirling.software.common.util.propertyeditor.StringToArrayListPropertyEditor;
@@ -85,6 +87,7 @@ public class RedactController {
private static final COSString EMPTY_COS_STRING = new COSString("");
private final CustomPDFDocumentFactory pdfDocumentFactory;
private final TempFileManager tempFileManager;
private String removeFileExtension(String filename) {
return GeneralUtils.removeExtension(filename);
@@ -105,8 +108,8 @@ public class RedactController {
"This endpoint redacts content from a PDF file based on manually specified areas. "
+ "Users can specify areas to redact and optionally convert the PDF to an image. "
+ "Input:PDF Output:PDF Type:SISO")
public ResponseEntity<byte[]> redactPDF(@ModelAttribute ManualRedactPdfRequest request)
throws IOException {
public ResponseEntity<StreamingResponseBody> redactPDF(
@ModelAttribute ManualRedactPdfRequest request) throws IOException {
MultipartFile file = request.getFileInput();
List<RedactionArea> redactionAreas = request.getRedactions();
@@ -120,30 +123,24 @@ public class RedactController {
if (Boolean.TRUE.equals(request.getConvertPDFToImage())) {
try (PDDocument convertedPdf = PdfUtils.convertPdfToPdfImage(document)) {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
convertedPdf.save(baos);
byte[] pdfContent = baos.toByteArray();
return WebResponseUtils.bytesToWebResponse(
pdfContent,
return WebResponseUtils.pdfDocToWebResponse(
convertedPdf,
removeFileExtension(
Objects.requireNonNull(
Filenames.toSimpleFileName(
file.getOriginalFilename())))
+ "_redacted.pdf");
+ "_redacted.pdf",
tempFileManager);
}
}
ByteArrayOutputStream baos = new ByteArrayOutputStream();
document.save(baos);
byte[] pdfContent = baos.toByteArray();
return WebResponseUtils.bytesToWebResponse(
pdfContent,
return WebResponseUtils.pdfDocToWebResponse(
document,
removeFileExtension(
Objects.requireNonNull(
Filenames.toSimpleFileName(file.getOriginalFilename())))
+ "_redacted.pdf");
+ "_redacted.pdf",
tempFileManager);
}
}
@@ -504,7 +501,8 @@ public class RedactController {
"This endpoint automatically redacts text from a PDF file based on specified patterns. "
+ "Users can provide text patterns to redact, with options for regex and whole word matching. "
+ "Input:PDF Output:PDF Type:SISO")
public ResponseEntity<byte[]> redactPdf(@ModelAttribute RedactPdfRequest request) {
public ResponseEntity<StreamingResponseBody> redactPdf(
@ModelAttribute RedactPdfRequest request) {
String[] listOfText = request.getListOfText().split("\n");
boolean useRegex = Boolean.TRUE.equals(request.getUseRegex());
boolean wholeWordSearchBool = Boolean.TRUE.equals(request.getWholeWordSearch());
@@ -545,20 +543,15 @@ public class RedactController {
if (allFoundTextsByPage.isEmpty()) {
log.info("No text found matching redaction patterns");
byte[] originalContent;
try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
document.save(baos);
originalContent = baos.toByteArray();
}
return WebResponseUtils.bytesToWebResponse(
originalContent,
return WebResponseUtils.pdfDocToWebResponse(
document,
removeFileExtension(
Objects.requireNonNull(
Filenames.toSimpleFileName(
request.getFileInput()
.getOriginalFilename())))
+ "_redacted.pdf");
+ "_redacted.pdf",
tempFileManager);
}
boolean fallbackToBoxOnlyMode;
@@ -587,7 +580,7 @@ public class RedactController {
findTextToRedact(
fallbackDocument, listOfText, useRegex, wholeWordSearchBool);
byte[] pdfContent =
TempFile finalized =
finalizeRedaction(
fallbackDocument,
allFoundTextsByPage,
@@ -596,8 +589,8 @@ public class RedactController {
request.getConvertPDFToImage(),
false); // Box-only mode, use original box sizes
return WebResponseUtils.bytesToWebResponse(
pdfContent,
return WebResponseUtils.pdfFileToWebResponse(
finalized,
removeFileExtension(
Objects.requireNonNull(
Filenames.toSimpleFileName(
@@ -606,7 +599,7 @@ public class RedactController {
+ "_redacted.pdf");
}
byte[] pdfContent =
TempFile finalized =
finalizeRedaction(
document,
allFoundTextsByPage,
@@ -615,8 +608,8 @@ public class RedactController {
request.getConvertPDFToImage(),
true); // Text removal mode, use reduced box sizes
return WebResponseUtils.bytesToWebResponse(
pdfContent,
return WebResponseUtils.pdfFileToWebResponse(
finalized,
removeFileExtension(
Objects.requireNonNull(
Filenames.toSimpleFileName(
@@ -733,7 +726,7 @@ public class RedactController {
}
}
private byte[] finalizeRedaction(
private TempFile finalizeRedaction(
PDDocument document,
Map<Integer, List<PDFText>> allFoundTextsByPage,
String colorString,
@@ -759,29 +752,37 @@ public class RedactController {
try (PDDocument convertedPdf = PdfUtils.convertPdfToPdfImage(document)) {
cleanDocumentMetadata(convertedPdf);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
convertedPdf.save(baos);
byte[] out = baos.toByteArray();
TempFile tempOut = tempFileManager.createManagedTempFile(".pdf");
try {
convertedPdf.save(tempOut.getFile());
} catch (IOException e) {
tempOut.close();
throw e;
}
log.info(
"Redaction finalized (image mode): {} pages ➜ {} KB",
convertedPdf.getNumberOfPages(),
out.length / 1024);
tempOut.getFile().length() / 1024);
return out;
return tempOut;
}
}
ByteArrayOutputStream baos = new ByteArrayOutputStream();
document.save(baos);
byte[] out = baos.toByteArray();
TempFile tempOut = tempFileManager.createManagedTempFile(".pdf");
try {
document.save(tempOut.getFile());
} catch (IOException e) {
tempOut.close();
throw e;
}
log.info(
"Redaction finalized: {} pages ➜ {} KB",
document.getNumberOfPages(),
out.length / 1024);
tempOut.getFile().length() / 1024);
return out;
return tempOut;
}
private void cleanDocumentMetadata(PDDocument document) {
@@ -11,6 +11,7 @@ import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;
import io.swagger.v3.oas.annotations.Operation;
@@ -22,6 +23,7 @@ import stirling.software.common.annotations.api.SecurityApi;
import stirling.software.common.model.api.PDFFile;
import stirling.software.common.service.CustomPDFDocumentFactory;
import stirling.software.common.util.GeneralUtils;
import stirling.software.common.util.TempFileManager;
import stirling.software.common.util.WebResponseUtils;
@SecurityApi
@@ -29,6 +31,7 @@ import stirling.software.common.util.WebResponseUtils;
public class RemoveCertSignController {
private final CustomPDFDocumentFactory pdfDocumentFactory;
private final TempFileManager tempFileManager;
@AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/remove-cert-sign")
@StandardPdfResponse
@@ -37,7 +40,7 @@ public class RemoveCertSignController {
description =
"This endpoint accepts a PDF file and returns the PDF file without the digital"
+ " signature. Input:PDF, Output:PDF Type:SISO")
public ResponseEntity<byte[]> removeCertSignPDF(@ModelAttribute PDFFile request)
public ResponseEntity<StreamingResponseBody> removeCertSignPDF(@ModelAttribute PDFFile request)
throws Exception {
MultipartFile pdf = request.getFileInput();
@@ -63,7 +66,8 @@ public class RemoveCertSignController {
// Return the modified PDF as a response
return WebResponseUtils.pdfDocToWebResponse(
document,
GeneralUtils.generateFilename(pdf.getOriginalFilename(), "_unsigned.pdf"));
GeneralUtils.generateFilename(pdf.getOriginalFilename(), "_unsigned.pdf"),
tempFileManager);
}
}
}
@@ -1,6 +1,5 @@
package stirling.software.SPDF.controller.api.security;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.List;
@@ -29,6 +28,7 @@ import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;
import io.swagger.v3.oas.annotations.Operation;
@@ -41,6 +41,7 @@ import stirling.software.common.annotations.AutoJobPostMapping;
import stirling.software.common.annotations.api.SecurityApi;
import stirling.software.common.service.CustomPDFDocumentFactory;
import stirling.software.common.util.GeneralUtils;
import stirling.software.common.util.TempFileManager;
import stirling.software.common.util.WebResponseUtils;
@Slf4j
@@ -49,6 +50,7 @@ import stirling.software.common.util.WebResponseUtils;
public class SanitizeController {
private final CustomPDFDocumentFactory pdfDocumentFactory;
private final TempFileManager tempFileManager;
@AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/sanitize-pdf")
@StandardPdfResponse
@@ -57,8 +59,8 @@ public class SanitizeController {
description =
"This endpoint processes a PDF file and removes specific elements based on the"
+ " provided options. Input:PDF Output:PDF Type:SISO")
public ResponseEntity<byte[]> sanitizePDF(@ModelAttribute SanitizePdfRequest request)
throws IOException {
public ResponseEntity<StreamingResponseBody> sanitizePDF(
@ModelAttribute SanitizePdfRequest request) throws IOException {
MultipartFile inputFile = request.getFileInput();
boolean removeJavaScript = Boolean.TRUE.equals(request.getRemoveJavaScript());
boolean removeEmbeddedFiles = Boolean.TRUE.equals(request.getRemoveEmbeddedFiles());
@@ -92,14 +94,11 @@ public class SanitizeController {
sanitizeFonts(document);
}
// Save the sanitized document to output stream
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
document.save(outputStream);
return WebResponseUtils.bytesToWebResponse(
outputStream.toByteArray(),
return WebResponseUtils.pdfDocToWebResponse(
document,
GeneralUtils.generateFilename(
inputFile.getOriginalFilename(), "_sanitized.pdf"));
inputFile.getOriginalFilename(), "_sanitized.pdf"),
tempFileManager);
}
}
@@ -1,6 +1,5 @@
package stirling.software.SPDF.controller.api.security;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
@@ -33,6 +32,7 @@ import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;
import io.swagger.v3.oas.annotations.Operation;
@@ -46,6 +46,8 @@ import stirling.software.common.annotations.api.SecurityApi;
import stirling.software.common.model.ApplicationProperties;
import stirling.software.common.service.CustomPDFDocumentFactory;
import stirling.software.common.util.GeneralUtils;
import stirling.software.common.util.TempFile;
import stirling.software.common.util.TempFileManager;
import stirling.software.common.util.WebResponseUtils;
@Slf4j
@@ -74,6 +76,7 @@ public class TimestampController {
private final CustomPDFDocumentFactory pdfDocumentFactory;
private final ApplicationProperties applicationProperties;
private final TempFileManager tempFileManager;
@AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/timestamp-pdf")
@StandardPdfResponse
@@ -84,8 +87,8 @@ public class TimestampController {
+ " document timestamp into the PDF. Only a SHA-256 hash of the"
+ " document is sent to the TSA — the PDF itself never leaves the"
+ " server. Input:PDF Output:PDF Type:SISO")
public ResponseEntity<byte[]> timestampPdf(@ModelAttribute TimestampPdfRequest request)
throws Exception {
public ResponseEntity<StreamingResponseBody> timestampPdf(
@ModelAttribute TimestampPdfRequest request) throws Exception {
MultipartFile inputFile = request.getFileInput();
ApplicationProperties.Security.Timestamp tsConfig =
applicationProperties.getSecurity().getTimestamp();
@@ -124,9 +127,10 @@ public class TimestampController {
+ " via settings.yml (security.timestamp.customTsaUrls).");
}
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
try (PDDocument document = pdfDocumentFactory.load(inputFile)) {
TempFile tempOutputFile = tempFileManager.createManagedTempFile(".pdf");
try (PDDocument document = pdfDocumentFactory.load(inputFile);
OutputStream outputStream =
java.nio.file.Files.newOutputStream(tempOutputFile.getPath())) {
PDSignature signature = new PDSignature();
signature.setType(COSName.DOC_TIME_STAMP);
signature.setFilter(PDSignature.FILTER_ADOBE_PPKLITE);
@@ -136,10 +140,13 @@ public class TimestampController {
document.addSignature(signature, content -> requestTimestampToken(content, tsaUrl));
document.saveIncremental(outputStream);
} catch (Exception e) {
tempOutputFile.close();
throw e;
}
return WebResponseUtils.bytesToWebResponse(
outputStream.toByteArray(),
return WebResponseUtils.pdfFileToWebResponse(
tempOutputFile,
GeneralUtils.generateFilename(inputFile.getOriginalFilename(), "_timestamped.pdf"));
}
@@ -163,7 +170,8 @@ public class TimestampController {
TimeStampRequest tsaRequest = generator.generate(digestAlgorithm, hash, nonce);
byte[] requestBytes = tsaRequest.getEncoded();
// Contact the TSA server (redirects disabled to prevent SSRF via redirect)
// Contact the TSA server tsaUrl is validated against an allowlist above,
// and redirects are disabled below to prevent SSRF via redirect.
connection = (HttpURLConnection) URI.create(tsaUrl).toURL().openConnection();
connection.setInstanceFollowRedirects(false);
connection.setDoOutput(true);
@@ -30,6 +30,7 @@ import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.InitBinder;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;
import io.swagger.v3.oas.annotations.Operation;
@@ -45,6 +46,7 @@ import stirling.software.common.service.CustomPDFDocumentFactory;
import stirling.software.common.util.GeneralUtils;
import stirling.software.common.util.PdfUtils;
import stirling.software.common.util.RegexPatternUtils;
import stirling.software.common.util.TempFileManager;
import stirling.software.common.util.WebResponseUtils;
@SecurityApi
@@ -52,6 +54,7 @@ import stirling.software.common.util.WebResponseUtils;
public class WatermarkController {
private final CustomPDFDocumentFactory pdfDocumentFactory;
private final TempFileManager tempFileManager;
@InitBinder
public void initBinder(WebDataBinder binder) {
@@ -73,8 +76,8 @@ public class WatermarkController {
"This endpoint adds a watermark to a given PDF file. Users can specify the"
+ " watermark type (text or image), rotation, opacity, width spacer, and"
+ " height spacer. Input:PDF Output:PDF Type:SISO")
public ResponseEntity<byte[]> addWatermark(@Valid @ModelAttribute AddWatermarkRequest request)
throws IOException, Exception {
public ResponseEntity<StreamingResponseBody> addWatermark(
@Valid @ModelAttribute AddWatermarkRequest request) throws IOException, Exception {
MultipartFile pdfFile = request.getFileInput();
String pdfFileName = pdfFile.getOriginalFilename();
if (pdfFileName != null && (pdfFileName.contains("..") || pdfFileName.startsWith("/"))) {
@@ -151,14 +154,16 @@ public class WatermarkController {
return WebResponseUtils.pdfDocToWebResponse(
convertedPdf,
GeneralUtils.generateFilename(
pdfFile.getOriginalFilename(), "_watermarked.pdf"));
pdfFile.getOriginalFilename(), "_watermarked.pdf"),
tempFileManager);
}
} else {
// Return the watermarked PDF as a response
return WebResponseUtils.pdfDocToWebResponse(
document,
GeneralUtils.generateFilename(
pdfFile.getOriginalFilename(), "_watermarked.pdf"));
pdfFile.getOriginalFilename(), "_watermarked.pdf"),
tempFileManager);
}
}
}
@@ -11,21 +11,18 @@ import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.service.SharedSignatureService;
import stirling.software.common.service.PersonalSignatureServiceInterface;
import stirling.software.common.service.UserServiceInterface;
/**
* Unified signature image controller that works for both authenticated and unauthenticated users.
* Uses composition pattern: - Core SharedSignatureService (always available): reads shared
* signatures - PersonalSignatureService (proprietary, optional): reads personal signatures For
* authenticated signature management (save/delete), see proprietary SignatureController.
*/
@Slf4j
@RestController
@RequestMapping("/api/v1/general")
@Tag(name = "Signature Assets", description = "Retrieve saved signature images")
public class SignatureImageController {
private final SharedSignatureService sharedSignatureService;
@@ -4,6 +4,7 @@ import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;
import io.swagger.v3.oas.annotations.Operation;
@@ -28,7 +29,7 @@ public class ConvertPDFToMarkdown {
summary = "Convert PDF to Markdown",
description =
"This endpoint converts a PDF file to Markdown format. Input:PDF Output:Markdown Type:SISO")
public ResponseEntity<byte[]> processPdfToMarkdown(@ModelAttribute PDFFile file)
public ResponseEntity<StreamingResponseBody> processPdfToMarkdown(@ModelAttribute PDFFile file)
throws Exception {
MultipartFile inputFile = file.getFileInput();
PDFToFile pdfToFile = new PDFToFile(tempFileManager);
@@ -25,6 +25,11 @@ server.http2.enabled=true
# Enable virtual threads (Java 21+, pinning fix in Java 25)
spring.threads.virtual.enabled=true
# Only run security filters on REQUEST and ERROR dispatches (not ASYNC).
# StreamingResponseBody triggers an ASYNC dispatch on completion; without this,
# Spring Security re-evaluates authorization after the response is already committed.
spring.security.filter.dispatcher-types=REQUEST,ERROR
# Response compression
server.compression.enabled=true
server.compression.min-response-size=1024
@@ -1,8 +1,10 @@
package stirling.software.SPDF.controller.api;
import static org.assertj.core.api.Assertions.*;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.*;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
@@ -11,6 +13,7 @@ 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.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.api.io.TempDir;
@@ -21,17 +24,47 @@ import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.mock.web.MockMultipartFile;
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;
import stirling.software.SPDF.model.api.general.BookletImpositionRequest;
import stirling.software.common.service.CustomPDFDocumentFactory;
import stirling.software.common.util.TempFile;
import stirling.software.common.util.TempFileManager;
@ExtendWith(MockitoExtension.class)
class BookletImpositionControllerTest {
private static ResponseEntity<StreamingResponseBody> streamingOk(byte[] bytes) {
return ResponseEntity.ok(out -> out.write(bytes));
}
private static byte[] drainBody(ResponseEntity<StreamingResponseBody> response)
throws java.io.IOException {
java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream();
response.getBody().writeTo(baos);
return baos.toByteArray();
}
@TempDir Path tempDir;
@Mock private CustomPDFDocumentFactory pdfDocumentFactory;
@Mock private TempFileManager tempFileManager;
@InjectMocks private BookletImpositionController controller;
@BeforeEach
void setUp() throws Exception {
lenient()
.when(tempFileManager.createManagedTempFile(anyString()))
.thenAnswer(
inv -> {
File f =
Files.createTempFile("test", inv.<String>getArgument(0))
.toFile();
TempFile tf = mock(TempFile.class);
lenient().when(tf.getFile()).thenReturn(f);
lenient().when(tf.getPath()).thenReturn(f.toPath());
return tf;
});
}
private MockMultipartFile createRealPdf(int numPages) throws IOException {
Path path = tempDir.resolve("test.pdf");
try (PDDocument doc = new PDDocument()) {
@@ -61,11 +94,12 @@ class BookletImpositionControllerTest {
when(pdfDocumentFactory.load(file)).thenReturn(sourceDoc);
when(pdfDocumentFactory.createNewDocumentBasedOnOldDocument(sourceDoc)).thenReturn(newDoc);
ResponseEntity<byte[]> response = controller.createBookletImposition(request);
ResponseEntity<StreamingResponseBody> response =
controller.createBookletImposition(request);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(response.getBody()).isNotEmpty();
try (PDDocument result = Loader.loadPDF(response.getBody())) {
assertThat(drainBody(response)).isNotEmpty();
try (PDDocument result = Loader.loadPDF(drainBody(response))) {
assertThat(result.getNumberOfPages()).isGreaterThan(0);
}
}
@@ -92,10 +126,11 @@ class BookletImpositionControllerTest {
when(pdfDocumentFactory.load(file)).thenReturn(sourceDoc);
when(pdfDocumentFactory.createNewDocumentBasedOnOldDocument(sourceDoc)).thenReturn(newDoc);
ResponseEntity<byte[]> response = controller.createBookletImposition(request);
ResponseEntity<StreamingResponseBody> response =
controller.createBookletImposition(request);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(response.getBody()).isNotEmpty();
assertThat(drainBody(response)).isNotEmpty();
}
@Test
@@ -109,7 +144,8 @@ class BookletImpositionControllerTest {
when(pdfDocumentFactory.load(file)).thenReturn(sourceDoc);
when(pdfDocumentFactory.createNewDocumentBasedOnOldDocument(sourceDoc)).thenReturn(newDoc);
ResponseEntity<byte[]> response = controller.createBookletImposition(request);
ResponseEntity<StreamingResponseBody> response =
controller.createBookletImposition(request);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
}
@@ -126,7 +162,8 @@ class BookletImpositionControllerTest {
when(pdfDocumentFactory.load(file)).thenReturn(sourceDoc);
when(pdfDocumentFactory.createNewDocumentBasedOnOldDocument(sourceDoc)).thenReturn(newDoc);
ResponseEntity<byte[]> response = controller.createBookletImposition(request);
ResponseEntity<StreamingResponseBody> response =
controller.createBookletImposition(request);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
}
@@ -143,7 +180,8 @@ class BookletImpositionControllerTest {
when(pdfDocumentFactory.load(file)).thenReturn(sourceDoc);
when(pdfDocumentFactory.createNewDocumentBasedOnOldDocument(sourceDoc)).thenReturn(newDoc);
ResponseEntity<byte[]> response = controller.createBookletImposition(request);
ResponseEntity<StreamingResponseBody> response =
controller.createBookletImposition(request);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
}
@@ -160,7 +198,8 @@ class BookletImpositionControllerTest {
when(pdfDocumentFactory.load(file)).thenReturn(sourceDoc);
when(pdfDocumentFactory.createNewDocumentBasedOnOldDocument(sourceDoc)).thenReturn(newDoc);
ResponseEntity<byte[]> response = controller.createBookletImposition(request);
ResponseEntity<StreamingResponseBody> response =
controller.createBookletImposition(request);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
}
@@ -177,7 +216,8 @@ class BookletImpositionControllerTest {
when(pdfDocumentFactory.load(file)).thenReturn(sourceDoc);
when(pdfDocumentFactory.createNewDocumentBasedOnOldDocument(sourceDoc)).thenReturn(newDoc);
ResponseEntity<byte[]> response = controller.createBookletImposition(request);
ResponseEntity<StreamingResponseBody> response =
controller.createBookletImposition(request);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
}
@@ -192,7 +232,8 @@ class BookletImpositionControllerTest {
when(pdfDocumentFactory.load(file)).thenReturn(sourceDoc);
when(pdfDocumentFactory.createNewDocumentBasedOnOldDocument(sourceDoc)).thenReturn(newDoc);
ResponseEntity<byte[]> response = controller.createBookletImposition(request);
ResponseEntity<StreamingResponseBody> response =
controller.createBookletImposition(request);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
}
@@ -220,7 +261,8 @@ class BookletImpositionControllerTest {
when(pdfDocumentFactory.load(file)).thenReturn(sourceDoc);
when(pdfDocumentFactory.createNewDocumentBasedOnOldDocument(sourceDoc)).thenReturn(newDoc);
ResponseEntity<byte[]> response = controller.createBookletImposition(request);
ResponseEntity<StreamingResponseBody> response =
controller.createBookletImposition(request);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
}
@@ -1,9 +1,11 @@
package stirling.software.SPDF.controller.api;
import static org.assertj.core.api.Assertions.*;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.*;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Method;
import java.nio.file.Files;
@@ -29,21 +31,47 @@ import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.mock.web.MockMultipartFile;
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;
import stirling.software.SPDF.model.api.general.CropPdfForm;
import stirling.software.common.service.CustomPDFDocumentFactory;
import stirling.software.common.util.TempFile;
import stirling.software.common.util.TempFileManager;
@ExtendWith(MockitoExtension.class)
@DisplayName("CropController Tests")
class CropControllerTest {
private static ResponseEntity<StreamingResponseBody> streamingOk(byte[] bytes) {
return ResponseEntity.ok(out -> out.write(bytes));
}
private static byte[] drainBody(ResponseEntity<StreamingResponseBody> response)
throws java.io.IOException {
java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream();
response.getBody().writeTo(baos);
return baos.toByteArray();
}
@TempDir Path tempDir;
@Mock private CustomPDFDocumentFactory pdfDocumentFactory;
@Mock private TempFileManager tempFileManager;
@InjectMocks private CropController cropController;
private TestPdfFactory pdfFactory;
@BeforeEach
void setUp() {
void setUp() throws Exception {
lenient()
.when(tempFileManager.createManagedTempFile(anyString()))
.thenAnswer(
inv -> {
File f =
Files.createTempFile("test", inv.<String>getArgument(0))
.toFile();
TempFile tf = mock(TempFile.class);
lenient().when(tf.getFile()).thenReturn(f);
lenient().when(tf.getPath()).thenReturn(f.toPath());
return tf;
});
pdfFactory = new TestPdfFactory();
}
@@ -177,7 +205,7 @@ class CropControllerTest {
when(pdfDocumentFactory.createNewDocumentBasedOnOldDocument(mockDocument))
.thenReturn(newDocument);
ResponseEntity<byte[]> response = cropController.cropPdf(request);
ResponseEntity<StreamingResponseBody> response = cropController.cropPdf(request);
assertThat(response)
.isNotNull()
@@ -214,7 +242,7 @@ class CropControllerTest {
when(pdfDocumentFactory.createNewDocumentBasedOnOldDocument(mockDocument))
.thenReturn(newDocument);
ResponseEntity<byte[]> response = cropController.cropPdf(request);
ResponseEntity<StreamingResponseBody> response = cropController.cropPdf(request);
assertThat(response).isNotNull();
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
@@ -234,7 +262,7 @@ class CropControllerTest {
private TestPdfFactory autoCropPdfFactory;
@BeforeEach
void setUp() {
void setUp() throws Exception {
autoCropPdfFactory = new TestPdfFactory();
}
@@ -254,13 +282,13 @@ class CropControllerTest {
when(pdfDocumentFactory.createNewDocumentBasedOnOldDocument(sourceDoc))
.thenReturn(newDoc);
ResponseEntity<byte[]> response = cropController.cropPdf(request);
ResponseEntity<StreamingResponseBody> response = cropController.cropPdf(request);
assertThat(response).isNotNull();
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(response.getBody()).isNotEmpty();
assertThat(drainBody(response)).isNotEmpty();
try (PDDocument result = Loader.loadPDF(response.getBody())) {
try (PDDocument result = Loader.loadPDF(drainBody(response))) {
assertThat(result.getNumberOfPages()).isEqualTo(1);
PDPage page = result.getPage(0);
@@ -285,13 +313,13 @@ class CropControllerTest {
when(pdfDocumentFactory.createNewDocumentBasedOnOldDocument(sourceDoc))
.thenReturn(newDoc);
ResponseEntity<byte[]> response = cropController.cropPdf(request);
ResponseEntity<StreamingResponseBody> response = cropController.cropPdf(request);
assertThat(response).isNotNull();
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
Assertions.assertNotNull(response.getBody());
try (PDDocument result = Loader.loadPDF(response.getBody())) {
try (PDDocument result = Loader.loadPDF(drainBody(response))) {
assertThat(result.getNumberOfPages()).isEqualTo(1);
}
}
@@ -648,7 +676,7 @@ class CropControllerTest {
when(pdfDocumentFactory.createNewDocumentBasedOnOldDocument(mockDocument))
.thenReturn(newDocument);
ResponseEntity<byte[]> response = cropController.cropPdf(request);
ResponseEntity<StreamingResponseBody> response = cropController.cropPdf(request);
assertThat(response).isNotNull();
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
@@ -676,7 +704,7 @@ class CropControllerTest {
when(pdfDocumentFactory.createNewDocumentBasedOnOldDocument(mockDocument))
.thenReturn(newDocument);
ResponseEntity<byte[]> response = cropController.cropPdf(request);
ResponseEntity<StreamingResponseBody> response = cropController.cropPdf(request);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
verify(mockDocument, times(1)).close();
@@ -1,10 +1,12 @@
package stirling.software.SPDF.controller.api;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.*;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.lang.reflect.Method;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
@@ -26,10 +28,13 @@ import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.mock.web.MockMultipartFile;
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;
import stirling.software.SPDF.controller.api.EditTableOfContentsController.BookmarkItem;
import stirling.software.SPDF.model.api.EditTableOfContentsRequest;
import stirling.software.common.service.CustomPDFDocumentFactory;
import stirling.software.common.util.TempFile;
import stirling.software.common.util.TempFileManager;
import tools.jackson.core.type.TypeReference;
import tools.jackson.databind.ObjectMapper;
@@ -40,6 +45,7 @@ class EditTableOfContentsControllerTest {
@Mock private CustomPDFDocumentFactory pdfDocumentFactory;
@Mock private ObjectMapper objectMapper;
@Mock private TempFileManager tempFileManager;
@InjectMocks private EditTableOfContentsController editTableOfContentsController;
@@ -53,7 +59,19 @@ class EditTableOfContentsControllerTest {
private PDOutlineItem mockOutlineItem;
@BeforeEach
void setUp() {
void setUp() throws Exception {
lenient()
.when(tempFileManager.createManagedTempFile(anyString()))
.thenAnswer(
inv -> {
File f =
Files.createTempFile("test", inv.<String>getArgument(0))
.toFile();
TempFile tf = mock(TempFile.class);
lenient().when(tf.getFile()).thenReturn(f);
lenient().when(tf.getPath()).thenReturn(f.toPath());
return tf;
});
mockFile =
new MockMultipartFile(
"file",
@@ -226,18 +244,19 @@ class EditTableOfContentsControllerTest {
when(mockDocument.getNumberOfPages()).thenReturn(5);
when(mockDocument.getPage(0)).thenReturn(mockPage1);
// Mock saving behavior
doAnswer(
invocation -> {
ByteArrayOutputStream baos = invocation.getArgument(0);
baos.write("mocked pdf content".getBytes());
lenient()
.doAnswer(
inv -> {
File f = inv.getArgument(0);
java.nio.file.Files.write(f.toPath(), "mock pdf".getBytes());
return null;
})
.when(mockDocument)
.save(any(ByteArrayOutputStream.class));
.save(any(File.class));
// When
ResponseEntity<byte[]> result = editTableOfContentsController.editTableOfContents(request);
ResponseEntity<StreamingResponseBody> result =
editTableOfContentsController.editTableOfContents(request);
// Then
assertNotNull(result);
@@ -289,17 +308,19 @@ class EditTableOfContentsControllerTest {
when(mockDocument.getPage(0)).thenReturn(mockPage1);
when(mockDocument.getPage(1)).thenReturn(mockPage2);
doAnswer(
invocation -> {
ByteArrayOutputStream baos = invocation.getArgument(0);
baos.write("mocked pdf content".getBytes());
lenient()
.doAnswer(
inv -> {
File f = inv.getArgument(0);
java.nio.file.Files.write(f.toPath(), "mock pdf".getBytes());
return null;
})
.when(mockDocument)
.save(any(ByteArrayOutputStream.class));
.save(any(File.class));
// When
ResponseEntity<byte[]> result = editTableOfContentsController.editTableOfContents(request);
ResponseEntity<StreamingResponseBody> result =
editTableOfContentsController.editTableOfContents(request);
// Then
assertNotNull(result);
@@ -341,17 +362,19 @@ class EditTableOfContentsControllerTest {
when(mockDocument.getPage(0)).thenReturn(mockPage1); // For negative page number
when(mockDocument.getPage(4)).thenReturn(mockPage2); // For page number exceeding bounds
doAnswer(
invocation -> {
ByteArrayOutputStream baos = invocation.getArgument(0);
baos.write("mocked pdf content".getBytes());
lenient()
.doAnswer(
inv -> {
File f = inv.getArgument(0);
java.nio.file.Files.write(f.toPath(), "mock pdf".getBytes());
return null;
})
.when(mockDocument)
.save(any(ByteArrayOutputStream.class));
.save(any(File.class));
// When
ResponseEntity<byte[]> result = editTableOfContentsController.editTableOfContents(request);
ResponseEntity<StreamingResponseBody> result =
editTableOfContentsController.editTableOfContents(request);
// Then
assertNotNull(result);
@@ -1,5 +1,12 @@
package stirling.software.SPDF.controller.api;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.lenient;
import static org.mockito.Mockito.mock;
import java.io.File;
import java.nio.file.Files;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage;
import org.junit.jupiter.api.Assertions;
@@ -15,14 +22,28 @@ import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.mock.web.MockMultipartFile;
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;
import stirling.software.SPDF.model.api.general.MergeMultiplePagesRequest;
import stirling.software.common.service.CustomPDFDocumentFactory;
import stirling.software.common.util.TempFile;
import stirling.software.common.util.TempFileManager;
@ExtendWith(MockitoExtension.class)
class MultiPageLayoutControllerTest {
private static ResponseEntity<StreamingResponseBody> streamingOk(byte[] bytes) {
return ResponseEntity.ok(out -> out.write(bytes));
}
private static byte[] drainBody(ResponseEntity<StreamingResponseBody> response)
throws java.io.IOException {
java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream();
response.getBody().writeTo(baos);
return baos.toByteArray();
}
@Mock private CustomPDFDocumentFactory pdfDocumentFactory;
@Mock private TempFileManager tempFileManager;
@InjectMocks private MultiPageLayoutController controller;
@@ -30,7 +51,19 @@ class MultiPageLayoutControllerTest {
private MockMultipartFile fileNoExt;
@BeforeEach
void setup() {
void setup() throws Exception {
lenient()
.when(tempFileManager.createManagedTempFile(anyString()))
.thenAnswer(
inv -> {
File f =
Files.createTempFile("test", inv.<String>getArgument(0))
.toFile();
TempFile tf = mock(TempFile.class);
lenient().when(tf.getFile()).thenReturn(f);
lenient().when(tf.getPath()).thenReturn(f.toPath());
return tf;
});
fileWithExt =
new MockMultipartFile(
"fileInput", "test.pdf", "application/pdf", new byte[] {1, 2, 3});
@@ -64,11 +97,11 @@ class MultiPageLayoutControllerTest {
req.setAddBorder(Boolean.FALSE);
req.setFileInput(fileWithExt);
ResponseEntity<byte[]> resp = controller.mergeMultiplePagesIntoOne(req);
ResponseEntity<StreamingResponseBody> resp = controller.mergeMultiplePagesIntoOne(req);
Assertions.assertEquals(HttpStatus.OK, resp.getStatusCode());
Assertions.assertEquals(MediaType.APPLICATION_PDF, resp.getHeaders().getContentType());
Assertions.assertNotNull(resp.getBody());
Assertions.assertTrue(resp.getBody().length > 0);
Assertions.assertTrue(drainBody(resp).length > 0);
Assertions.assertEquals(
"test_multi_page_layout.pdf",
resp.getHeaders().getContentDisposition().getFilename());
@@ -89,11 +122,11 @@ class MultiPageLayoutControllerTest {
req.setAddBorder(Boolean.TRUE);
req.setFileInput(fileWithExt);
ResponseEntity<byte[]> resp = controller.mergeMultiplePagesIntoOne(req);
ResponseEntity<StreamingResponseBody> resp = controller.mergeMultiplePagesIntoOne(req);
Assertions.assertEquals(HttpStatus.OK, resp.getStatusCode());
Assertions.assertEquals(MediaType.APPLICATION_PDF, resp.getHeaders().getContentType());
Assertions.assertNotNull(resp.getBody());
Assertions.assertTrue(resp.getBody().length > 0);
Assertions.assertTrue(drainBody(resp).length > 0);
}
@Test
@@ -112,7 +145,7 @@ class MultiPageLayoutControllerTest {
req.setAddBorder(Boolean.TRUE);
req.setFileInput(fileNoExt);
ResponseEntity<byte[]> resp = controller.mergeMultiplePagesIntoOne(req);
ResponseEntity<StreamingResponseBody> resp = controller.mergeMultiplePagesIntoOne(req);
Assertions.assertEquals(
"name_multi_page_layout.pdf",
resp.getHeaders().getContentDisposition().getFilename());
@@ -2,16 +2,22 @@ package stirling.software.SPDF.controller.api;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.lenient;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import java.io.ByteArrayOutputStream;
import java.io.File;
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;
@@ -24,17 +30,47 @@ import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.mock.web.MockMultipartFile;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;
import stirling.software.SPDF.model.api.general.OverlayPdfsRequest;
import stirling.software.common.service.CustomPDFDocumentFactory;
import stirling.software.common.util.TempFile;
import stirling.software.common.util.TempFileManager;
@ExtendWith(MockitoExtension.class)
class PdfOverlayControllerTest {
private static ResponseEntity<StreamingResponseBody> streamingOk(byte[] bytes) {
return ResponseEntity.ok(out -> out.write(bytes));
}
private static byte[] drainBody(ResponseEntity<StreamingResponseBody> response)
throws java.io.IOException {
java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream();
response.getBody().writeTo(baos);
return baos.toByteArray();
}
@TempDir Path tempDir;
@Mock private CustomPDFDocumentFactory pdfDocumentFactory;
@Mock private TempFileManager tempFileManager;
@InjectMocks private PdfOverlayController controller;
@BeforeEach
void setUp() throws Exception {
lenient()
.when(tempFileManager.createManagedTempFile(anyString()))
.thenAnswer(
inv -> {
File f =
Files.createTempFile("test", inv.<String>getArgument(0))
.toFile();
TempFile tf = mock(TempFile.class);
lenient().when(tf.getFile()).thenReturn(f);
lenient().when(tf.getPath()).thenReturn(f.toPath());
return tf;
});
}
private byte[] createPdf(int numPages) throws IOException {
try (PDDocument doc = new PDDocument()) {
for (int i = 0; i < numPages; i++) {
@@ -71,12 +107,12 @@ class PdfOverlayControllerTest {
when(pdfDocumentFactory.load(any(MultipartFile.class)))
.thenAnswer(inv -> Loader.loadPDF(((MultipartFile) inv.getArgument(0)).getBytes()));
ResponseEntity<byte[]> response = controller.overlayPdfs(request);
ResponseEntity<StreamingResponseBody> response = controller.overlayPdfs(request);
assertNotNull(response);
assertEquals(HttpStatus.OK, response.getStatusCode());
assertNotNull(response.getBody());
assertTrue(response.getBody().length > 0);
assertTrue(drainBody(response).length > 0);
}
@Test
@@ -105,7 +141,7 @@ class PdfOverlayControllerTest {
when(pdfDocumentFactory.load(any(MultipartFile.class)))
.thenAnswer(inv -> Loader.loadPDF(((MultipartFile) inv.getArgument(0)).getBytes()));
ResponseEntity<byte[]> response = controller.overlayPdfs(request);
ResponseEntity<StreamingResponseBody> response = controller.overlayPdfs(request);
assertNotNull(response);
assertEquals(HttpStatus.OK, response.getStatusCode());
@@ -137,7 +173,7 @@ class PdfOverlayControllerTest {
when(pdfDocumentFactory.load(any(MultipartFile.class)))
.thenAnswer(inv -> Loader.loadPDF(((MultipartFile) inv.getArgument(0)).getBytes()));
ResponseEntity<byte[]> response = controller.overlayPdfs(request);
ResponseEntity<StreamingResponseBody> response = controller.overlayPdfs(request);
assertNotNull(response);
assertEquals(HttpStatus.OK, response.getStatusCode());
@@ -168,7 +204,7 @@ class PdfOverlayControllerTest {
when(pdfDocumentFactory.load(any(MultipartFile.class)))
.thenAnswer(inv -> Loader.loadPDF(((MultipartFile) inv.getArgument(0)).getBytes()));
ResponseEntity<byte[]> response = controller.overlayPdfs(request);
ResponseEntity<StreamingResponseBody> response = controller.overlayPdfs(request);
assertNotNull(response);
assertEquals(HttpStatus.OK, response.getStatusCode());
@@ -258,7 +294,7 @@ class PdfOverlayControllerTest {
when(pdfDocumentFactory.load(any(MultipartFile.class)))
.thenAnswer(inv -> Loader.loadPDF(((MultipartFile) inv.getArgument(0)).getBytes()));
ResponseEntity<byte[]> response = controller.overlayPdfs(request);
ResponseEntity<StreamingResponseBody> response = controller.overlayPdfs(request);
assertNotNull(response);
assertEquals(HttpStatus.OK, response.getStatusCode());
@@ -291,7 +327,7 @@ class PdfOverlayControllerTest {
when(pdfDocumentFactory.load(any(MultipartFile.class)))
.thenAnswer(inv -> Loader.loadPDF(((MultipartFile) inv.getArgument(0)).getBytes()));
ResponseEntity<byte[]> response = controller.overlayPdfs(request);
ResponseEntity<StreamingResponseBody> response = controller.overlayPdfs(request);
assertNotNull(response);
assertEquals(HttpStatus.OK, response.getStatusCode());
@@ -1,12 +1,16 @@
package stirling.software.SPDF.controller.api;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.*;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
@@ -15,18 +19,38 @@ import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.mock.web.MockMultipartFile;
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;
import stirling.software.SPDF.model.api.PDFWithPageNums;
import stirling.software.SPDF.model.api.general.RearrangePagesRequest;
import stirling.software.common.service.CustomPDFDocumentFactory;
import stirling.software.common.util.TempFile;
import stirling.software.common.util.TempFileManager;
@ExtendWith(MockitoExtension.class)
class RearrangePagesPDFControllerTest {
@Mock private CustomPDFDocumentFactory pdfDocumentFactory;
@Mock private TempFileManager tempFileManager;
@InjectMocks private RearrangePagesPDFController controller;
@BeforeEach
void setUp() throws Exception {
lenient()
.when(tempFileManager.createManagedTempFile(anyString()))
.thenAnswer(
inv -> {
File f =
Files.createTempFile("test", inv.<String>getArgument(0))
.toFile();
TempFile tf = mock(TempFile.class);
lenient().when(tf.getFile()).thenReturn(f);
lenient().when(tf.getPath()).thenReturn(f.toPath());
return tf;
});
}
private MockMultipartFile createMockPdf() {
return new MockMultipartFile(
"fileInput", "test.pdf", MediaType.APPLICATION_PDF_VALUE, new byte[] {1, 2, 3});
@@ -43,7 +67,7 @@ class RearrangePagesPDFControllerTest {
when(pdfDocumentFactory.load(file)).thenReturn(mockDoc);
when(mockDoc.getNumberOfPages()).thenReturn(5);
ResponseEntity<byte[]> response = controller.deletePages(request);
ResponseEntity<StreamingResponseBody> response = controller.deletePages(request);
assertNotNull(response);
assertEquals(200, response.getStatusCode().value());
@@ -73,7 +97,7 @@ class RearrangePagesPDFControllerTest {
when(pdfDocumentFactory.createNewDocumentBasedOnOldDocument(mockDoc))
.thenReturn(mockNewDoc);
ResponseEntity<byte[]> response = controller.rearrangePages(request);
ResponseEntity<StreamingResponseBody> response = controller.rearrangePages(request);
assertNotNull(response);
assertEquals(200, response.getStatusCode().value());
@@ -103,7 +127,7 @@ class RearrangePagesPDFControllerTest {
when(pdfDocumentFactory.createNewDocumentBasedOnOldDocument(mockDoc))
.thenReturn(mockNewDoc);
ResponseEntity<byte[]> response = controller.rearrangePages(request);
ResponseEntity<StreamingResponseBody> response = controller.rearrangePages(request);
assertNotNull(response);
verify(mockNewDoc).addPage(page1);
@@ -131,7 +155,7 @@ class RearrangePagesPDFControllerTest {
when(pdfDocumentFactory.createNewDocumentBasedOnOldDocument(mockDoc))
.thenReturn(mockNewDoc);
ResponseEntity<byte[]> response = controller.rearrangePages(request);
ResponseEntity<StreamingResponseBody> response = controller.rearrangePages(request);
assertNotNull(response);
verify(mockNewDoc).addPage(page0);
@@ -157,7 +181,7 @@ class RearrangePagesPDFControllerTest {
when(pdfDocumentFactory.createNewDocumentBasedOnOldDocument(mockDoc))
.thenReturn(mockNewDoc);
ResponseEntity<byte[]> response = controller.rearrangePages(request);
ResponseEntity<StreamingResponseBody> response = controller.rearrangePages(request);
assertNotNull(response);
assertEquals(200, response.getStatusCode().value());
@@ -184,7 +208,7 @@ class RearrangePagesPDFControllerTest {
when(pdfDocumentFactory.createNewDocumentBasedOnOldDocument(mockDoc))
.thenReturn(mockNewDoc);
ResponseEntity<byte[]> response = controller.rearrangePages(request);
ResponseEntity<StreamingResponseBody> response = controller.rearrangePages(request);
assertNotNull(response);
assertEquals(200, response.getStatusCode().value());
@@ -208,7 +232,7 @@ class RearrangePagesPDFControllerTest {
when(pdfDocumentFactory.createNewDocumentBasedOnOldDocument(mockDoc))
.thenReturn(mockNewDoc);
ResponseEntity<byte[]> response = controller.rearrangePages(request);
ResponseEntity<StreamingResponseBody> response = controller.rearrangePages(request);
assertNotNull(response);
assertEquals(200, response.getStatusCode().value());
@@ -232,7 +256,7 @@ class RearrangePagesPDFControllerTest {
when(pdfDocumentFactory.createNewDocumentBasedOnOldDocument(mockDoc))
.thenReturn(mockNewDoc);
ResponseEntity<byte[]> response = controller.rearrangePages(request);
ResponseEntity<StreamingResponseBody> response = controller.rearrangePages(request);
assertNotNull(response);
assertEquals(200, response.getStatusCode().value());
@@ -260,7 +284,7 @@ class RearrangePagesPDFControllerTest {
when(pdfDocumentFactory.createNewDocumentBasedOnOldDocument(mockDoc))
.thenReturn(mockNewDoc);
ResponseEntity<byte[]> response = controller.rearrangePages(request);
ResponseEntity<StreamingResponseBody> response = controller.rearrangePages(request);
assertNotNull(response);
assertEquals(200, response.getStatusCode().value());
@@ -284,7 +308,7 @@ class RearrangePagesPDFControllerTest {
when(pdfDocumentFactory.createNewDocumentBasedOnOldDocument(mockDoc))
.thenReturn(mockNewDoc);
ResponseEntity<byte[]> response = controller.rearrangePages(request);
ResponseEntity<StreamingResponseBody> response = controller.rearrangePages(request);
assertNotNull(response);
// 2 pages * 3 duplicates = 6 addPage calls
@@ -309,7 +333,7 @@ class RearrangePagesPDFControllerTest {
when(pdfDocumentFactory.createNewDocumentBasedOnOldDocument(mockDoc))
.thenReturn(mockNewDoc);
ResponseEntity<byte[]> response = controller.rearrangePages(request);
ResponseEntity<StreamingResponseBody> response = controller.rearrangePages(request);
assertNotNull(response);
assertEquals(200, response.getStatusCode().value());
@@ -1,15 +1,20 @@
package stirling.software.SPDF.controller.api;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.lenient;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.PDPageTree;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
@@ -18,17 +23,37 @@ import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.mock.web.MockMultipartFile;
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;
import stirling.software.SPDF.model.api.general.RotatePDFRequest;
import stirling.software.common.service.CustomPDFDocumentFactory;
import stirling.software.common.util.TempFile;
import stirling.software.common.util.TempFileManager;
@ExtendWith(MockitoExtension.class)
public class RotationControllerTest {
@Mock private CustomPDFDocumentFactory pdfDocumentFactory;
@Mock private TempFileManager tempFileManager;
@InjectMocks private RotationController rotationController;
@BeforeEach
void setUp() throws Exception {
lenient()
.when(tempFileManager.createManagedTempFile(anyString()))
.thenAnswer(
inv -> {
File f =
Files.createTempFile("test", inv.<String>getArgument(0))
.toFile();
TempFile tf = mock(TempFile.class);
lenient().when(tf.getFile()).thenReturn(f);
lenient().when(tf.getPath()).thenReturn(f.toPath());
return tf;
});
}
@Test
public void testRotatePDF() throws IOException {
// Create a mock file
@@ -50,7 +75,7 @@ public class RotationControllerTest {
when(mockPage.getRotation()).thenReturn(0);
// Act
ResponseEntity<byte[]> response = rotationController.rotatePDF(request);
ResponseEntity<StreamingResponseBody> response = rotationController.rotatePDF(request);
// Assert
verify(mockPage).setRotation(90);
@@ -2,8 +2,10 @@ package stirling.software.SPDF.controller.api;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.*;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
@@ -11,6 +13,7 @@ import java.nio.file.Path;
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.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.api.io.TempDir;
@@ -21,17 +24,47 @@ import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.mock.web.MockMultipartFile;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;
import stirling.software.SPDF.model.api.general.ScalePagesRequest;
import stirling.software.common.service.CustomPDFDocumentFactory;
import stirling.software.common.util.TempFile;
import stirling.software.common.util.TempFileManager;
@ExtendWith(MockitoExtension.class)
class ScalePagesControllerTest {
private static ResponseEntity<StreamingResponseBody> streamingOk(byte[] bytes) {
return ResponseEntity.ok(out -> out.write(bytes));
}
private static byte[] drainBody(ResponseEntity<StreamingResponseBody> response)
throws java.io.IOException {
java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream();
response.getBody().writeTo(baos);
return baos.toByteArray();
}
@TempDir Path tempDir;
@Mock private CustomPDFDocumentFactory pdfDocumentFactory;
@Mock private TempFileManager tempFileManager;
@InjectMocks private ScalePagesController controller;
@BeforeEach
void setUp() throws Exception {
lenient()
.when(tempFileManager.createManagedTempFile(anyString()))
.thenAnswer(
inv -> {
File f =
Files.createTempFile("test", inv.<String>getArgument(0))
.toFile();
TempFile tf = mock(TempFile.class);
lenient().when(tf.getFile()).thenReturn(f);
lenient().when(tf.getPath()).thenReturn(f.toPath());
return tf;
});
}
private byte[] createRealPdf(PDRectangle pageSize, int numPages) throws IOException {
try (PDDocument doc = new PDDocument()) {
for (int i = 0; i < numPages; i++) {
@@ -68,12 +101,12 @@ class ScalePagesControllerTest {
setupFactory();
ResponseEntity<byte[]> response = controller.scalePages(request);
ResponseEntity<StreamingResponseBody> response = controller.scalePages(request);
assertNotNull(response);
assertEquals(200, response.getStatusCode().value());
assertNotNull(response.getBody());
assertTrue(response.getBody().length > 0);
assertTrue(drainBody(response).length > 0);
}
@Test
@@ -90,7 +123,7 @@ class ScalePagesControllerTest {
setupFactory();
ResponseEntity<byte[]> response = controller.scalePages(request);
ResponseEntity<StreamingResponseBody> response = controller.scalePages(request);
assertNotNull(response);
assertEquals(200, response.getStatusCode().value());
@@ -110,7 +143,7 @@ class ScalePagesControllerTest {
setupFactory();
ResponseEntity<byte[]> response = controller.scalePages(request);
ResponseEntity<StreamingResponseBody> response = controller.scalePages(request);
assertNotNull(response);
assertEquals(200, response.getStatusCode().value());
@@ -130,7 +163,7 @@ class ScalePagesControllerTest {
setupFactory();
ResponseEntity<byte[]> response = controller.scalePages(request);
ResponseEntity<StreamingResponseBody> response = controller.scalePages(request);
assertNotNull(response);
assertEquals(200, response.getStatusCode().value());
@@ -150,7 +183,7 @@ class ScalePagesControllerTest {
setupFactory();
ResponseEntity<byte[]> response = controller.scalePages(request);
ResponseEntity<StreamingResponseBody> response = controller.scalePages(request);
assertNotNull(response);
assertEquals(200, response.getStatusCode().value());
@@ -187,7 +220,7 @@ class ScalePagesControllerTest {
setupFactory();
ResponseEntity<byte[]> response = controller.scalePages(request);
ResponseEntity<StreamingResponseBody> response = controller.scalePages(request);
assertNotNull(response);
assertEquals(200, response.getStatusCode().value());
@@ -207,7 +240,7 @@ class ScalePagesControllerTest {
setupFactory();
ResponseEntity<byte[]> response = controller.scalePages(request);
ResponseEntity<StreamingResponseBody> response = controller.scalePages(request);
assertNotNull(response);
assertEquals(200, response.getStatusCode().value());
@@ -248,7 +281,7 @@ class ScalePagesControllerTest {
setupFactory();
ResponseEntity<byte[]> response = controller.scalePages(request);
ResponseEntity<StreamingResponseBody> response = controller.scalePages(request);
assertNotNull(response);
assertEquals(200, response.getStatusCode().value());
@@ -4,8 +4,11 @@ import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.lenient;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
@@ -31,6 +34,7 @@ import org.springframework.web.multipart.MultipartFile;
import stirling.software.SPDF.model.api.SplitPdfBySectionsRequest;
import stirling.software.common.service.CustomPDFDocumentFactory;
import stirling.software.common.util.TempFile;
import stirling.software.common.util.TempFileManager;
@ExtendWith(MockitoExtension.class)
@@ -44,6 +48,18 @@ class SplitPdfBySectionsControllerTest {
@BeforeEach
void setUp() throws IOException {
lenient()
.when(tempFileManager.createManagedTempFile(anyString()))
.thenAnswer(
inv -> {
File f =
Files.createTempFile("test", inv.<String>getArgument(0))
.toFile();
TempFile tf = mock(TempFile.class);
lenient().when(tf.getFile()).thenReturn(f);
lenient().when(tf.getPath()).thenReturn(f.toPath());
return tf;
});
when(tempFileManager.createTempFile(anyString()))
.thenAnswer(
inv -> {
@@ -5,7 +5,10 @@ import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.lenient;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import java.io.File;
@@ -19,6 +22,7 @@ import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Stream;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
@@ -29,6 +33,7 @@ import org.mockito.Mockito;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.http.ResponseEntity;
import org.springframework.mock.web.MockMultipartFile;
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;
import stirling.software.SPDF.config.EndpointConfiguration;
import stirling.software.SPDF.model.api.converters.ConvertEbookToPdfRequest;
@@ -37,11 +42,22 @@ import stirling.software.common.util.GeneralUtils;
import stirling.software.common.util.ProcessExecutor;
import stirling.software.common.util.ProcessExecutor.ProcessExecutorResult;
import stirling.software.common.util.ProcessExecutor.Processes;
import stirling.software.common.util.TempFile;
import stirling.software.common.util.TempFileManager;
import stirling.software.common.util.WebResponseUtils;
@ExtendWith(MockitoExtension.class)
class ConvertEbookToPDFControllerTest {
private static ResponseEntity<StreamingResponseBody> streamingOk(byte[] bytes) {
return ResponseEntity.ok(out -> out.write(bytes));
}
private static byte[] drainBody(ResponseEntity<StreamingResponseBody> response)
throws java.io.IOException {
java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream();
response.getBody().writeTo(baos);
return baos.toByteArray();
}
@Mock private CustomPDFDocumentFactory pdfDocumentFactory;
@Mock private TempFileManager tempFileManager;
@@ -49,6 +65,22 @@ class ConvertEbookToPDFControllerTest {
@InjectMocks private ConvertEbookToPDFController controller;
@BeforeEach
void setUp() throws Exception {
lenient()
.when(tempFileManager.createManagedTempFile(anyString()))
.thenAnswer(
inv -> {
File f =
Files.createTempFile("test", inv.<String>getArgument(0))
.toFile();
TempFile tf = mock(TempFile.class);
lenient().when(tf.getFile()).thenReturn(f);
lenient().when(tf.getPath()).thenReturn(f.toPath());
return tf;
});
}
@Test
void convertEbookToPdf_buildsCalibreCommandAndCleansUp() throws Exception {
when(endpointConfiguration.isGroupEnabled("Calibre")).thenReturn(true);
@@ -113,16 +145,14 @@ class ConvertEbookToPDFControllerTest {
return execResult;
});
ResponseEntity<byte[]> expectedResponse = ResponseEntity.ok("result".getBytes());
wr.when(
() ->
WebResponseUtils.pdfDocToWebResponse(
mockDocument, "ebook_convertedToPDF.pdf"))
ResponseEntity<StreamingResponseBody> expectedResponse =
streamingOk("result".getBytes());
wr.when(() -> WebResponseUtils.pdfFileToWebResponse(any(TempFile.class), anyString()))
.thenReturn(expectedResponse);
gu.when(() -> GeneralUtils.generateFilename("ebook.epub", "_convertedToPDF.pdf"))
.thenReturn("ebook_convertedToPDF.pdf");
ResponseEntity<byte[]> response = controller.convertEbookToPdf(request);
ResponseEntity<StreamingResponseBody> response = controller.convertEbookToPdf(request);
assertSame(expectedResponse, response);
@@ -232,14 +262,11 @@ class ConvertEbookToPDFControllerTest {
gu.when(() -> GeneralUtils.optimizePdfWithGhostscript(Mockito.any(byte[].class)))
.thenReturn(optimizedBytes);
ResponseEntity<byte[]> expectedResponse = ResponseEntity.ok(optimizedBytes);
wr.when(
() ->
WebResponseUtils.bytesToWebResponse(
optimizedBytes, "ebook_convertedToPDF.pdf"))
ResponseEntity<StreamingResponseBody> expectedResponse = streamingOk(optimizedBytes);
wr.when(() -> WebResponseUtils.pdfFileToWebResponse(any(TempFile.class), anyString()))
.thenReturn(expectedResponse);
ResponseEntity<byte[]> response = controller.convertEbookToPdf(request);
ResponseEntity<StreamingResponseBody> response = controller.convertEbookToPdf(request);
assertSame(expectedResponse, response);
gu.verify(() -> GeneralUtils.optimizePdfWithGhostscript(Mockito.any(byte[].class)));
@@ -4,12 +4,18 @@ import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.lenient;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
@@ -21,17 +27,29 @@ import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.mock.web.MockMultipartFile;
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;
import stirling.software.common.configuration.RuntimePathConfig;
import stirling.software.common.model.api.converters.EmlToPdfRequest;
import stirling.software.common.service.CustomPDFDocumentFactory;
import stirling.software.common.util.CustomHtmlSanitizer;
import stirling.software.common.util.EmlToPdf;
import stirling.software.common.util.TempFile;
import stirling.software.common.util.TempFileManager;
import stirling.software.common.util.WebResponseUtils;
@ExtendWith(MockitoExtension.class)
class ConvertEmlToPDFTest {
private static ResponseEntity<StreamingResponseBody> streamingOk(byte[] bytes) {
return ResponseEntity.ok(out -> out.write(bytes));
}
private static byte[] drainBody(ResponseEntity<StreamingResponseBody> response)
throws java.io.IOException {
java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream();
response.getBody().writeTo(baos);
return baos.toByteArray();
}
@Mock private CustomPDFDocumentFactory pdfDocumentFactory;
@Mock private RuntimePathConfig runtimePathConfig;
@@ -40,35 +58,51 @@ class ConvertEmlToPDFTest {
@InjectMocks private ConvertEmlToPDF controller;
@BeforeEach
void setUp() throws Exception {
lenient()
.when(tempFileManager.createManagedTempFile(anyString()))
.thenAnswer(
inv -> {
File f =
Files.createTempFile("test", inv.<String>getArgument(0))
.toFile();
TempFile tf = mock(TempFile.class);
lenient().when(tf.getFile()).thenReturn(f);
lenient().when(tf.getPath()).thenReturn(f.toPath());
return tf;
});
}
@Test
void convertEmlToPdf_emptyFileReturnsBadRequest() {
void convertEmlToPdf_emptyFileReturnsBadRequest() throws java.io.IOException {
MockMultipartFile emptyFile =
new MockMultipartFile("fileInput", "test.eml", "message/rfc822", new byte[0]);
EmlToPdfRequest request = new EmlToPdfRequest();
request.setFileInput(emptyFile);
ResponseEntity<byte[]> response = controller.convertEmlToPdf(request);
ResponseEntity<StreamingResponseBody> response = controller.convertEmlToPdf(request);
assertEquals(HttpStatus.BAD_REQUEST, response.getStatusCode());
assertTrue(
new String(response.getBody(), StandardCharsets.UTF_8)
new String(drainBody(response), StandardCharsets.UTF_8)
.contains("No file provided"));
}
@Test
void convertEmlToPdf_nullFilenameReturnsBadRequest() {
void convertEmlToPdf_nullFilenameReturnsBadRequest() throws java.io.IOException {
MockMultipartFile file =
new MockMultipartFile("fileInput", null, "message/rfc822", "content".getBytes());
EmlToPdfRequest request = new EmlToPdfRequest();
request.setFileInput(file);
ResponseEntity<byte[]> response = controller.convertEmlToPdf(request);
ResponseEntity<StreamingResponseBody> response = controller.convertEmlToPdf(request);
assertEquals(HttpStatus.BAD_REQUEST, response.getStatusCode());
assertTrue(
new String(response.getBody(), StandardCharsets.UTF_8).contains("valid filename"));
new String(drainBody(response), StandardCharsets.UTF_8).contains("valid filename"));
}
@Test
@@ -79,24 +113,24 @@ class ConvertEmlToPDFTest {
EmlToPdfRequest request = new EmlToPdfRequest();
request.setFileInput(file);
ResponseEntity<byte[]> response = controller.convertEmlToPdf(request);
ResponseEntity<StreamingResponseBody> response = controller.convertEmlToPdf(request);
assertEquals(HttpStatus.BAD_REQUEST, response.getStatusCode());
}
@Test
void convertEmlToPdf_invalidFileTypeReturnsBadRequest() {
void convertEmlToPdf_invalidFileTypeReturnsBadRequest() throws java.io.IOException {
MockMultipartFile file =
new MockMultipartFile("fileInput", "test.txt", "text/plain", "content".getBytes());
EmlToPdfRequest request = new EmlToPdfRequest();
request.setFileInput(file);
ResponseEntity<byte[]> response = controller.convertEmlToPdf(request);
ResponseEntity<StreamingResponseBody> response = controller.convertEmlToPdf(request);
assertEquals(HttpStatus.BAD_REQUEST, response.getStatusCode());
assertTrue(
new String(response.getBody(), StandardCharsets.UTF_8)
new String(drainBody(response), StandardCharsets.UTF_8)
.contains("valid EML or MSG"));
}
@@ -112,7 +146,7 @@ class ConvertEmlToPDFTest {
when(runtimePathConfig.getWeasyPrintPath()).thenReturn("/usr/bin/weasyprint");
ResponseEntity<byte[]> expectedResponse = ResponseEntity.ok(pdfBytes);
ResponseEntity<StreamingResponseBody> expectedResponse = streamingOk(pdfBytes);
try (MockedStatic<EmlToPdf> emlMock = Mockito.mockStatic(EmlToPdf.class);
MockedStatic<WebResponseUtils> wrMock =
@@ -132,14 +166,14 @@ class ConvertEmlToPDFTest {
wrMock.when(
() ->
WebResponseUtils.bytesToWebResponse(
pdfBytes, "test.eml.pdf", MediaType.APPLICATION_PDF))
WebResponseUtils.pdfFileToWebResponse(
any(TempFile.class), anyString()))
.thenReturn(expectedResponse);
ResponseEntity<byte[]> response = controller.convertEmlToPdf(request);
ResponseEntity<StreamingResponseBody> response = controller.convertEmlToPdf(request);
assertEquals(HttpStatus.OK, response.getStatusCode());
assertArrayEquals(pdfBytes, response.getBody());
assertArrayEquals(pdfBytes, drainBody(response));
}
}
@@ -154,8 +188,8 @@ class ConvertEmlToPDFTest {
request.setFileInput(file);
request.setDownloadHtml(true);
ResponseEntity<byte[]> expectedResponse =
ResponseEntity.ok(htmlContent.getBytes(StandardCharsets.UTF_8));
ResponseEntity<StreamingResponseBody> expectedResponse =
streamingOk(htmlContent.getBytes(StandardCharsets.UTF_8));
try (MockedStatic<EmlToPdf> emlMock = Mockito.mockStatic(EmlToPdf.class);
MockedStatic<WebResponseUtils> wrMock =
@@ -171,13 +205,11 @@ class ConvertEmlToPDFTest {
wrMock.when(
() ->
WebResponseUtils.bytesToWebResponse(
htmlContent.getBytes(StandardCharsets.UTF_8),
"test.eml.html",
MediaType.TEXT_HTML))
WebResponseUtils.fileToWebResponse(
any(TempFile.class), anyString(), any(MediaType.class)))
.thenReturn(expectedResponse);
ResponseEntity<byte[]> response = controller.convertEmlToPdf(request);
ResponseEntity<StreamingResponseBody> response = controller.convertEmlToPdf(request);
assertEquals(HttpStatus.OK, response.getStatusCode());
}
@@ -203,11 +235,11 @@ class ConvertEmlToPDFTest {
eq(customHtmlSanitizer)))
.thenThrow(new IOException("Parse error"));
ResponseEntity<byte[]> response = controller.convertEmlToPdf(request);
ResponseEntity<StreamingResponseBody> response = controller.convertEmlToPdf(request);
assertEquals(HttpStatus.INTERNAL_SERVER_ERROR, response.getStatusCode());
assertTrue(
new String(response.getBody(), StandardCharsets.UTF_8)
new String(drainBody(response), StandardCharsets.UTF_8)
.contains("HTML conversion failed"));
}
}
@@ -231,11 +263,11 @@ class ConvertEmlToPDFTest {
any(), any(), any(), any(), any(), any(), any()))
.thenReturn(null);
ResponseEntity<byte[]> response = controller.convertEmlToPdf(request);
ResponseEntity<StreamingResponseBody> response = controller.convertEmlToPdf(request);
assertEquals(HttpStatus.INTERNAL_SERVER_ERROR, response.getStatusCode());
assertTrue(
new String(response.getBody(), StandardCharsets.UTF_8)
new String(drainBody(response), StandardCharsets.UTF_8)
.contains("empty output"));
}
}
@@ -255,7 +287,7 @@ class ConvertEmlToPDFTest {
when(runtimePathConfig.getWeasyPrintPath()).thenReturn("/usr/bin/weasyprint");
ResponseEntity<byte[]> expectedResponse = ResponseEntity.ok(pdfBytes);
ResponseEntity<StreamingResponseBody> expectedResponse = streamingOk(pdfBytes);
try (MockedStatic<EmlToPdf> emlMock = Mockito.mockStatic(EmlToPdf.class);
MockedStatic<WebResponseUtils> wrMock =
@@ -269,13 +301,11 @@ class ConvertEmlToPDFTest {
wrMock.when(
() ->
WebResponseUtils.bytesToWebResponse(
any(byte[].class),
any(String.class),
any(MediaType.class)))
WebResponseUtils.pdfFileToWebResponse(
any(TempFile.class), anyString()))
.thenReturn(expectedResponse);
ResponseEntity<byte[]> response = controller.convertEmlToPdf(request);
ResponseEntity<StreamingResponseBody> response = controller.convertEmlToPdf(request);
assertEquals(HttpStatus.OK, response.getStatusCode());
}
@@ -300,11 +330,12 @@ class ConvertEmlToPDFTest {
any(), any(), any(), any(), any(), any(), any()))
.thenThrow(new InterruptedException("interrupted"));
ResponseEntity<byte[]> response = controller.convertEmlToPdf(request);
ResponseEntity<StreamingResponseBody> response = controller.convertEmlToPdf(request);
assertEquals(HttpStatus.INTERNAL_SERVER_ERROR, response.getStatusCode());
assertTrue(
new String(response.getBody(), StandardCharsets.UTF_8).contains("interrupted"));
new String(drainBody(response), StandardCharsets.UTF_8)
.contains("interrupted"));
}
}
}
@@ -3,9 +3,16 @@ package stirling.software.SPDF.controller.api.converters;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.lenient;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import java.io.File;
import java.nio.file.Files;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
@@ -16,6 +23,7 @@ import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.mock.web.MockMultipartFile;
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;
import stirling.software.common.configuration.RuntimePathConfig;
import stirling.software.common.model.api.converters.HTMLToPdfRequest;
@@ -23,11 +31,22 @@ import stirling.software.common.service.CustomPDFDocumentFactory;
import stirling.software.common.util.CustomHtmlSanitizer;
import stirling.software.common.util.FileToPdf;
import stirling.software.common.util.GeneralUtils;
import stirling.software.common.util.TempFile;
import stirling.software.common.util.TempFileManager;
import stirling.software.common.util.WebResponseUtils;
@ExtendWith(MockitoExtension.class)
class ConvertHtmlToPDFTest {
private static ResponseEntity<StreamingResponseBody> streamingOk(byte[] bytes) {
return ResponseEntity.ok(out -> out.write(bytes));
}
private static byte[] drainBody(ResponseEntity<StreamingResponseBody> response)
throws java.io.IOException {
java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream();
response.getBody().writeTo(baos);
return baos.toByteArray();
}
@Mock private CustomPDFDocumentFactory pdfDocumentFactory;
@Mock private RuntimePathConfig runtimePathConfig;
@@ -36,6 +55,22 @@ class ConvertHtmlToPDFTest {
@InjectMocks private ConvertHtmlToPDF controller;
@BeforeEach
void setUp() throws Exception {
lenient()
.when(tempFileManager.createManagedTempFile(anyString()))
.thenAnswer(
inv -> {
File f =
Files.createTempFile("test", inv.<String>getArgument(0))
.toFile();
TempFile tf = mock(TempFile.class);
lenient().when(tf.getFile()).thenReturn(f);
lenient().when(tf.getPath()).thenReturn(f.toPath());
return tf;
});
}
@Test
void htmlToPdf_nullFileInputThrows() {
HTMLToPdfRequest request = new HTMLToPdfRequest();
@@ -69,7 +104,7 @@ class ConvertHtmlToPDFTest {
when(pdfDocumentFactory.createNewBytesBasedOnOldDocument(pdfBytes))
.thenReturn(processedPdf);
ResponseEntity<byte[]> expectedResponse = ResponseEntity.ok(processedPdf);
ResponseEntity<StreamingResponseBody> expectedResponse = streamingOk(processedPdf);
try (MockedStatic<FileToPdf> ftpMock = Mockito.mockStatic(FileToPdf.class);
MockedStatic<GeneralUtils> guMock = Mockito.mockStatic(GeneralUtils.class);
@@ -90,10 +125,13 @@ class ConvertHtmlToPDFTest {
guMock.when(() -> GeneralUtils.generateFilename("test.html", ".pdf"))
.thenReturn("test.pdf");
wrMock.when(() -> WebResponseUtils.bytesToWebResponse(processedPdf, "test.pdf"))
wrMock.when(
() ->
WebResponseUtils.pdfFileToWebResponse(
any(TempFile.class), anyString()))
.thenReturn(expectedResponse);
ResponseEntity<byte[]> response = controller.HtmlToPdf(request);
ResponseEntity<StreamingResponseBody> response = controller.HtmlToPdf(request);
assertEquals(HttpStatus.OK, response.getStatusCode());
}
@@ -114,7 +152,7 @@ class ConvertHtmlToPDFTest {
when(pdfDocumentFactory.createNewBytesBasedOnOldDocument(pdfBytes))
.thenReturn(processedPdf);
ResponseEntity<byte[]> expectedResponse = ResponseEntity.ok(processedPdf);
ResponseEntity<StreamingResponseBody> expectedResponse = streamingOk(processedPdf);
try (MockedStatic<FileToPdf> ftpMock = Mockito.mockStatic(FileToPdf.class);
MockedStatic<GeneralUtils> guMock = Mockito.mockStatic(GeneralUtils.class);
@@ -135,10 +173,13 @@ class ConvertHtmlToPDFTest {
guMock.when(() -> GeneralUtils.generateFilename("archive.zip", ".pdf"))
.thenReturn("archive.pdf");
wrMock.when(() -> WebResponseUtils.bytesToWebResponse(processedPdf, "archive.pdf"))
wrMock.when(
() ->
WebResponseUtils.pdfFileToWebResponse(
any(TempFile.class), anyString()))
.thenReturn(expectedResponse);
ResponseEntity<byte[]> response = controller.HtmlToPdf(request);
ResponseEntity<StreamingResponseBody> response = controller.HtmlToPdf(request);
assertEquals(HttpStatus.OK, response.getStatusCode());
}
@@ -14,6 +14,7 @@ import org.mockito.Mockito;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.http.ResponseEntity;
import org.springframework.mock.web.MockMultipartFile;
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;
import stirling.software.SPDF.config.EndpointConfiguration;
import stirling.software.SPDF.model.api.converters.ConvertToPdfRequest;
@@ -25,6 +26,16 @@ import stirling.software.common.util.WebResponseUtils;
@ExtendWith(MockitoExtension.class)
class ConvertImgPDFControllerTest {
private static ResponseEntity<StreamingResponseBody> streamingOk(byte[] bytes) {
return ResponseEntity.ok(out -> out.write(bytes));
}
private static byte[] drainBody(ResponseEntity<StreamingResponseBody> response)
throws java.io.IOException {
java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream();
response.getBody().writeTo(baos);
return baos.toByteArray();
}
@Mock private CustomPDFDocumentFactory pdfDocumentFactory;
@Mock private TempFileManager tempFileManager;
@@ -4,10 +4,17 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.isNull;
import static org.mockito.Mockito.lenient;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import java.io.File;
import java.nio.file.Files;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
@@ -18,6 +25,7 @@ import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.mock.web.MockMultipartFile;
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;
import stirling.software.common.configuration.RuntimePathConfig;
import stirling.software.common.model.api.GeneralFile;
@@ -25,11 +33,22 @@ import stirling.software.common.service.CustomPDFDocumentFactory;
import stirling.software.common.util.CustomHtmlSanitizer;
import stirling.software.common.util.FileToPdf;
import stirling.software.common.util.GeneralUtils;
import stirling.software.common.util.TempFile;
import stirling.software.common.util.TempFileManager;
import stirling.software.common.util.WebResponseUtils;
@ExtendWith(MockitoExtension.class)
class ConvertMarkdownToPdfTest {
private static ResponseEntity<StreamingResponseBody> streamingOk(byte[] bytes) {
return ResponseEntity.ok(out -> out.write(bytes));
}
private static byte[] drainBody(ResponseEntity<StreamingResponseBody> response)
throws java.io.IOException {
java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream();
response.getBody().writeTo(baos);
return baos.toByteArray();
}
@Mock private CustomPDFDocumentFactory pdfDocumentFactory;
@Mock private RuntimePathConfig runtimePathConfig;
@@ -38,6 +57,22 @@ class ConvertMarkdownToPdfTest {
@InjectMocks private ConvertMarkdownToPdf controller;
@BeforeEach
void setUp() throws Exception {
lenient()
.when(tempFileManager.createManagedTempFile(anyString()))
.thenAnswer(
inv -> {
File f =
Files.createTempFile("test", inv.<String>getArgument(0))
.toFile();
TempFile tf = mock(TempFile.class);
lenient().when(tf.getFile()).thenReturn(f);
lenient().when(tf.getPath()).thenReturn(f.toPath());
return tf;
});
}
@Test
void markdownToPdf_nullFileInputThrows() {
GeneralFile generalFile = new GeneralFile();
@@ -71,7 +106,7 @@ class ConvertMarkdownToPdfTest {
when(pdfDocumentFactory.createNewBytesBasedOnOldDocument(any(byte[].class)))
.thenReturn(processedPdf);
ResponseEntity<byte[]> expectedResponse = ResponseEntity.ok(processedPdf);
ResponseEntity<StreamingResponseBody> expectedResponse = streamingOk(processedPdf);
try (MockedStatic<FileToPdf> ftpMock = Mockito.mockStatic(FileToPdf.class);
MockedStatic<GeneralUtils> guMock = Mockito.mockStatic(GeneralUtils.class);
@@ -92,10 +127,13 @@ class ConvertMarkdownToPdfTest {
guMock.when(() -> GeneralUtils.generateFilename("readme.md", ".pdf"))
.thenReturn("readme.pdf");
wrMock.when(() -> WebResponseUtils.bytesToWebResponse(processedPdf, "readme.pdf"))
wrMock.when(
() ->
WebResponseUtils.pdfFileToWebResponse(
any(TempFile.class), anyString()))
.thenReturn(expectedResponse);
ResponseEntity<byte[]> response = controller.markdownToPdf(generalFile);
ResponseEntity<StreamingResponseBody> response = controller.markdownToPdf(generalFile);
assertEquals(HttpStatus.OK, response.getStatusCode());
}
@@ -3,12 +3,15 @@ package stirling.software.SPDF.controller.api.converters;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.lenient;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
@@ -18,6 +21,7 @@ import java.util.List;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Stream;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
@@ -29,6 +33,7 @@ import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.mock.web.MockMultipartFile;
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;
import stirling.software.SPDF.config.EndpointConfiguration;
import stirling.software.SPDF.model.api.converters.ConvertPdfToEpubRequest;
@@ -38,10 +43,21 @@ import stirling.software.common.util.GeneralUtils;
import stirling.software.common.util.ProcessExecutor;
import stirling.software.common.util.ProcessExecutor.ProcessExecutorResult;
import stirling.software.common.util.ProcessExecutor.Processes;
import stirling.software.common.util.TempFile;
import stirling.software.common.util.TempFileManager;
@ExtendWith(MockitoExtension.class)
class ConvertPDFToEpubControllerTest {
private static ResponseEntity<StreamingResponseBody> streamingOk(byte[] bytes) {
return ResponseEntity.ok(out -> out.write(bytes));
}
private static byte[] drainBody(ResponseEntity<StreamingResponseBody> response)
throws java.io.IOException {
java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream();
response.getBody().writeTo(baos);
return baos.toByteArray();
}
private static final MediaType EPUB_MEDIA_TYPE = MediaType.valueOf("application/epub+zip");
@@ -50,6 +66,22 @@ class ConvertPDFToEpubControllerTest {
@InjectMocks private ConvertPDFToEpubController controller;
@BeforeEach
void setUp() throws Exception {
lenient()
.when(tempFileManager.createManagedTempFile(anyString()))
.thenAnswer(
inv -> {
File f =
Files.createTempFile("test", inv.<String>getArgument(0))
.toFile();
TempFile tf = mock(TempFile.class);
lenient().when(tf.getFile()).thenReturn(f);
lenient().when(tf.getPath()).thenReturn(f.toPath());
return tf;
});
}
@Test
void convertPdfToEpub_buildsGoldenCommandAndCleansUp() throws Exception {
when(endpointConfiguration.isGroupEnabled("Calibre")).thenReturn(true);
@@ -110,7 +142,7 @@ class ConvertPDFToEpubControllerTest {
gu.when(() -> GeneralUtils.generateFilename("novel.pdf", "_convertedToEPUB.epub"))
.thenReturn("novel_convertedToEPUB.epub");
ResponseEntity<byte[]> response = controller.convertPdfToEpub(request);
ResponseEntity<StreamingResponseBody> response = controller.convertPdfToEpub(request);
List<String> command = commandCaptor.getValue();
assertEquals(13, command.size());
@@ -134,7 +166,7 @@ class ConvertPDFToEpubControllerTest {
assertEquals(
"novel_convertedToEPUB.epub",
response.getHeaders().getContentDisposition().getFilename());
assertEquals("epub", new String(response.getBody(), StandardCharsets.UTF_8));
assertEquals("epub", new String(drainBody(response), StandardCharsets.UTF_8));
verify(tempFileManager).deleteTempDirectory(workingDir);
assertEquals(workingDir, deletedDir.get());
@@ -202,7 +234,7 @@ class ConvertPDFToEpubControllerTest {
gu.when(() -> GeneralUtils.generateFilename("story.pdf", "_convertedToEPUB.epub"))
.thenReturn("story_convertedToEPUB.epub");
ResponseEntity<byte[]> response = controller.convertPdfToEpub(request);
ResponseEntity<StreamingResponseBody> response = controller.convertPdfToEpub(request);
List<String> command = commandCaptor.getValue();
assertTrue(command.stream().noneMatch(arg -> "--chapter".equals(arg)));
@@ -220,7 +252,7 @@ class ConvertPDFToEpubControllerTest {
assertEquals(
"story_convertedToEPUB.epub",
response.getHeaders().getContentDisposition().getFilename());
assertEquals("epub", new String(response.getBody(), StandardCharsets.UTF_8));
assertEquals("epub", new String(drainBody(response), StandardCharsets.UTF_8));
} finally {
deleteIfExists(workingDir);
}
@@ -287,7 +319,7 @@ class ConvertPDFToEpubControllerTest {
gu.when(() -> GeneralUtils.generateFilename("book.pdf", "_convertedToAZW3.azw3"))
.thenReturn("book_convertedToAZW3.azw3");
ResponseEntity<byte[]> response = controller.convertPdfToEpub(request);
ResponseEntity<StreamingResponseBody> response = controller.convertPdfToEpub(request);
List<String> command = commandCaptor.getValue();
assertEquals("ebook-convert", command.get(0));
@@ -308,7 +340,7 @@ class ConvertPDFToEpubControllerTest {
assertEquals(
"book_convertedToAZW3.azw3",
response.getHeaders().getContentDisposition().getFilename());
assertEquals("azw3", new String(response.getBody(), StandardCharsets.UTF_8));
assertEquals("azw3", new String(drainBody(response), StandardCharsets.UTF_8));
verify(tempFileManager).deleteTempDirectory(workingDir);
} finally {
@@ -2,11 +2,17 @@ package stirling.software.SPDF.controller.api.converters;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.lenient;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import java.io.File;
import java.nio.file.Files;
import java.util.List;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
@@ -17,18 +23,38 @@ import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.mock.web.MockMultipartFile;
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;
import stirling.software.SPDF.model.api.PDFWithPageNums;
import stirling.software.common.service.CustomPDFDocumentFactory;
import stirling.software.common.util.GeneralUtils;
import stirling.software.common.util.TempFile;
import stirling.software.common.util.TempFileManager;
@ExtendWith(MockitoExtension.class)
class ConvertPDFToExcelControllerTest {
@Mock private CustomPDFDocumentFactory pdfDocumentFactory;
@Mock private TempFileManager tempFileManager;
@InjectMocks private ConvertPDFToExcelController controller;
@BeforeEach
void setUp() throws Exception {
lenient()
.when(tempFileManager.createManagedTempFile(anyString()))
.thenAnswer(
inv -> {
File f =
Files.createTempFile("test", inv.<String>getArgument(0))
.toFile();
TempFile tf = mock(TempFile.class);
lenient().when(tf.getFile()).thenReturn(f);
lenient().when(tf.getPath()).thenReturn(f.toPath());
return tf;
});
}
@Test
void pdfToExcel_noTablesReturnsNoContent() throws Exception {
MockMultipartFile pdfFile =
@@ -55,7 +81,7 @@ class ConvertPDFToExcelControllerTest {
Mockito.eq(true)))
.thenReturn(List.of(1));
ResponseEntity<byte[]> response = controller.pdfToExcel(request);
ResponseEntity<StreamingResponseBody> response = controller.pdfToExcel(request);
// tabula may or may not find tables in an empty page
assertNotNull(response);
@@ -4,10 +4,16 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.lenient;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import java.io.File;
import java.nio.file.Files;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
@@ -18,6 +24,7 @@ import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.mock.web.MockMultipartFile;
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;
import stirling.software.SPDF.model.api.converters.PdfToPresentationRequest;
import stirling.software.SPDF.model.api.converters.PdfToTextOrRTFRequest;
@@ -27,11 +34,22 @@ import stirling.software.common.model.api.PDFFile;
import stirling.software.common.service.CustomPDFDocumentFactory;
import stirling.software.common.util.GeneralUtils;
import stirling.software.common.util.PDFToFile;
import stirling.software.common.util.TempFile;
import stirling.software.common.util.TempFileManager;
import stirling.software.common.util.WebResponseUtils;
@ExtendWith(MockitoExtension.class)
class ConvertPDFToOfficeTest {
private static ResponseEntity<StreamingResponseBody> streamingOk(byte[] bytes) {
return ResponseEntity.ok(out -> out.write(bytes));
}
private static byte[] drainBody(ResponseEntity<StreamingResponseBody> response)
throws java.io.IOException {
java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream();
response.getBody().writeTo(baos);
return baos.toByteArray();
}
@Mock private CustomPDFDocumentFactory pdfDocumentFactory;
@Mock private TempFileManager tempFileManager;
@@ -39,6 +57,22 @@ class ConvertPDFToOfficeTest {
@InjectMocks private ConvertPDFToOffice controller;
@BeforeEach
void setUp() throws Exception {
lenient()
.when(tempFileManager.createManagedTempFile(anyString()))
.thenAnswer(
inv -> {
File f =
Files.createTempFile("test", inv.<String>getArgument(0))
.toFile();
TempFile tf = mock(TempFile.class);
lenient().when(tf.getFile()).thenReturn(f);
lenient().when(tf.getPath()).thenReturn(f.toPath());
return tf;
});
}
private MockMultipartFile createPdfFile() {
return new MockMultipartFile(
"fileInput", "document.pdf", "application/pdf", "pdf-content".getBytes());
@@ -51,7 +85,8 @@ class ConvertPDFToOfficeTest {
request.setFileInput(pdfFile);
request.setOutputFormat("pptx");
ResponseEntity<byte[]> expectedResponse = ResponseEntity.ok("pptx-content".getBytes());
ResponseEntity<StreamingResponseBody> expectedResponse =
streamingOk("pptx-content".getBytes());
try (MockedStatic<PDFToFile> mock =
Mockito.mockStatic(PDFToFile.class, Mockito.CALLS_REAL_METHODS)) {
@@ -80,7 +115,8 @@ class ConvertPDFToOfficeTest {
realDoc.addPage(new org.apache.pdfbox.pdmodel.PDPage());
when(pdfDocumentFactory.load(pdfFile)).thenReturn(realDoc);
ResponseEntity<byte[]> expectedResponse = ResponseEntity.ok("text content".getBytes());
ResponseEntity<StreamingResponseBody> expectedResponse =
streamingOk("text content".getBytes());
try (MockedStatic<GeneralUtils> guMock = Mockito.mockStatic(GeneralUtils.class);
MockedStatic<WebResponseUtils> wrMock =
@@ -91,13 +127,12 @@ class ConvertPDFToOfficeTest {
wrMock.when(
() ->
WebResponseUtils.bytesToWebResponse(
any(byte[].class),
eq("document.txt"),
eq(MediaType.TEXT_PLAIN)))
WebResponseUtils.fileToWebResponse(
any(TempFile.class), anyString(), any(MediaType.class)))
.thenReturn(expectedResponse);
ResponseEntity<byte[]> response = controller.processPdfToRTForTXT(request);
ResponseEntity<StreamingResponseBody> response =
controller.processPdfToRTForTXT(request);
assertSame(expectedResponse, response);
}
@@ -4,34 +4,67 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.lenient;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockedStatic;
import org.mockito.Mockito;
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 org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;
import stirling.software.SPDF.service.PdfJsonConversionService;
import stirling.software.common.model.api.GeneralFile;
import stirling.software.common.model.api.PDFFile;
import stirling.software.common.util.WebResponseUtils;
import stirling.software.common.util.TempFile;
import stirling.software.common.util.TempFileManager;
@ExtendWith(MockitoExtension.class)
class ConvertPdfJsonControllerTest {
@Mock private PdfJsonConversionService pdfJsonConversionService;
@Mock private TempFileManager tempFileManager;
@InjectMocks private ConvertPdfJsonController controller;
@BeforeEach
void setUp() throws Exception {
lenient()
.when(tempFileManager.createManagedTempFile(anyString()))
.thenAnswer(
inv -> {
File f =
Files.createTempFile("test", inv.<String>getArgument(0))
.toFile();
TempFile tf = mock(TempFile.class);
lenient().when(tf.getFile()).thenReturn(f);
lenient().when(tf.getPath()).thenReturn(f.toPath());
return tf;
});
}
private static byte[] drainBody(ResponseEntity<StreamingResponseBody> response)
throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
response.getBody().writeTo(baos);
return baos.toByteArray();
}
@Test
void convertPdfToJson_nullFileInputThrows() {
PDFFile request = new PDFFile();
@@ -51,19 +84,11 @@ class ConvertPdfJsonControllerTest {
when(pdfJsonConversionService.convertPdfToJson(pdfFile, false)).thenReturn(jsonBytes);
ResponseEntity<byte[]> expectedResponse = ResponseEntity.ok(jsonBytes);
ResponseEntity<StreamingResponseBody> response =
controller.convertPdfToJson(request, false);
try (MockedStatic<WebResponseUtils> wrMock = Mockito.mockStatic(WebResponseUtils.class)) {
wrMock.when(
() ->
WebResponseUtils.bytesToWebResponse(
jsonBytes, "doc.json", MediaType.APPLICATION_JSON))
.thenReturn(expectedResponse);
ResponseEntity<byte[]> response = controller.convertPdfToJson(request, false);
assertEquals(HttpStatus.OK, response.getStatusCode());
}
assertEquals(HttpStatus.OK, response.getStatusCode());
assertNotNull(response.getBody());
}
@Test
@@ -77,20 +102,10 @@ class ConvertPdfJsonControllerTest {
when(pdfJsonConversionService.convertPdfToJson(pdfFile, true)).thenReturn(jsonBytes);
ResponseEntity<byte[]> expectedResponse = ResponseEntity.ok(jsonBytes);
ResponseEntity<StreamingResponseBody> response = controller.convertPdfToJson(request, true);
try (MockedStatic<WebResponseUtils> wrMock = Mockito.mockStatic(WebResponseUtils.class)) {
wrMock.when(
() ->
WebResponseUtils.bytesToWebResponse(
jsonBytes, "doc.json", MediaType.APPLICATION_JSON))
.thenReturn(expectedResponse);
ResponseEntity<byte[]> response = controller.convertPdfToJson(request, true);
assertEquals(HttpStatus.OK, response.getStatusCode());
verify(pdfJsonConversionService).convertPdfToJson(pdfFile, true);
}
assertEquals(HttpStatus.OK, response.getStatusCode());
verify(pdfJsonConversionService).convertPdfToJson(pdfFile, true);
}
@Test
@@ -112,16 +127,10 @@ class ConvertPdfJsonControllerTest {
when(pdfJsonConversionService.convertJsonToPdf(jsonFile)).thenReturn(pdfBytes);
ResponseEntity<byte[]> expectedResponse = ResponseEntity.ok(pdfBytes);
ResponseEntity<StreamingResponseBody> response = controller.convertJsonToPdf(request);
try (MockedStatic<WebResponseUtils> wrMock = Mockito.mockStatic(WebResponseUtils.class)) {
wrMock.when(() -> WebResponseUtils.bytesToWebResponse(pdfBytes, "doc.pdf"))
.thenReturn(expectedResponse);
ResponseEntity<byte[]> response = controller.convertJsonToPdf(request);
assertEquals(HttpStatus.OK, response.getStatusCode());
}
assertEquals(HttpStatus.OK, response.getStatusCode());
assertNotNull(response.getBody());
}
@Test
@@ -144,7 +153,7 @@ class ConvertPdfJsonControllerTest {
when(pdfJsonConversionService.extractDocumentMetadata(eq(pdfFile), any(String.class)))
.thenReturn(jsonBytes);
ResponseEntity<byte[]> response = controller.extractPdfMetadata(request);
ResponseEntity<StreamingResponseBody> response = controller.extractPdfMetadata(request);
assertEquals(HttpStatus.OK, response.getStatusCode());
assertEquals(MediaType.APPLICATION_JSON, response.getHeaders().getContentType());
@@ -168,19 +177,10 @@ class ConvertPdfJsonControllerTest {
when(pdfJsonConversionService.extractSinglePage(jobId, 1)).thenReturn(jsonBytes);
ResponseEntity<byte[]> expectedResponse = ResponseEntity.ok(jsonBytes);
ResponseEntity<StreamingResponseBody> response = controller.extractSinglePage(jobId, 1);
try (MockedStatic<WebResponseUtils> wrMock = Mockito.mockStatic(WebResponseUtils.class)) {
wrMock.when(
() ->
WebResponseUtils.bytesToWebResponse(
jsonBytes, "page_1.json", MediaType.APPLICATION_JSON))
.thenReturn(expectedResponse);
ResponseEntity<byte[]> response = controller.extractSinglePage(jobId, 1);
assertEquals(HttpStatus.OK, response.getStatusCode());
}
assertEquals(HttpStatus.OK, response.getStatusCode());
assertNotNull(response.getBody());
}
@Test
@@ -190,20 +190,9 @@ class ConvertPdfJsonControllerTest {
when(pdfJsonConversionService.extractPageFonts(jobId, 1)).thenReturn(jsonBytes);
ResponseEntity<byte[]> expectedResponse = ResponseEntity.ok(jsonBytes);
ResponseEntity<StreamingResponseBody> response = controller.extractPageFonts(jobId, 1);
try (MockedStatic<WebResponseUtils> wrMock = Mockito.mockStatic(WebResponseUtils.class)) {
wrMock.when(
() ->
WebResponseUtils.bytesToWebResponse(
jsonBytes,
"page_fonts_1.json",
MediaType.APPLICATION_JSON))
.thenReturn(expectedResponse);
ResponseEntity<byte[]> response = controller.extractPageFonts(jobId, 1);
assertEquals(HttpStatus.OK, response.getStatusCode());
}
assertEquals(HttpStatus.OK, response.getStatusCode());
assertNotNull(response.getBody());
}
}
@@ -3,11 +3,17 @@ package stirling.software.SPDF.controller.api.converters;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.lenient;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
@@ -16,20 +22,31 @@ import org.mockito.MockedStatic;
import org.mockito.Mockito;
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 org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;
import stirling.software.SPDF.model.api.converters.SvgToPdfRequest;
import stirling.software.SPDF.utils.SvgToPdf;
import stirling.software.common.service.CustomPDFDocumentFactory;
import stirling.software.common.util.GeneralUtils;
import stirling.software.common.util.SvgSanitizer;
import stirling.software.common.util.TempFile;
import stirling.software.common.util.TempFileManager;
import stirling.software.common.util.WebResponseUtils;
@ExtendWith(MockitoExtension.class)
class ConvertSvgToPDFTest {
private static ResponseEntity<StreamingResponseBody> streamingOk(byte[] bytes) {
return ResponseEntity.ok(out -> out.write(bytes));
}
private static byte[] drainBody(ResponseEntity<StreamingResponseBody> response)
throws java.io.IOException {
java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream();
response.getBody().writeTo(baos);
return baos.toByteArray();
}
@Mock private CustomPDFDocumentFactory pdfDocumentFactory;
@Mock private SvgSanitizer svgSanitizer;
@@ -37,16 +54,32 @@ class ConvertSvgToPDFTest {
@InjectMocks private ConvertSvgToPDF controller;
@BeforeEach
void setUp() throws Exception {
lenient()
.when(tempFileManager.createManagedTempFile(anyString()))
.thenAnswer(
inv -> {
File f =
Files.createTempFile("test", inv.<String>getArgument(0))
.toFile();
TempFile tf = mock(TempFile.class);
lenient().when(tf.getFile()).thenReturn(f);
lenient().when(tf.getPath()).thenReturn(f.toPath());
return tf;
});
}
@Test
void convertSvgToPdf_nullFilesReturnsBadRequest() {
void convertSvgToPdf_nullFilesReturnsBadRequest() throws java.io.IOException {
SvgToPdfRequest request = new SvgToPdfRequest();
request.setFileInput(null);
ResponseEntity<byte[]> response = controller.convertSvgToPdf(request);
ResponseEntity<StreamingResponseBody> response = controller.convertSvgToPdf(request);
assertEquals(HttpStatus.BAD_REQUEST, response.getStatusCode());
assertTrue(
new String(response.getBody(), StandardCharsets.UTF_8)
new String(drainBody(response), StandardCharsets.UTF_8)
.contains("No files provided"));
}
@@ -55,7 +88,7 @@ class ConvertSvgToPDFTest {
SvgToPdfRequest request = new SvgToPdfRequest();
request.setFileInput(new MockMultipartFile[0]);
ResponseEntity<byte[]> response = controller.convertSvgToPdf(request);
ResponseEntity<StreamingResponseBody> response = controller.convertSvgToPdf(request);
assertEquals(HttpStatus.BAD_REQUEST, response.getStatusCode());
}
@@ -69,10 +102,11 @@ class ConvertSvgToPDFTest {
request.setFileInput(new MockMultipartFile[] {txtFile});
request.setCombineIntoSinglePdf(false);
ResponseEntity<byte[]> response = controller.convertSvgToPdf(request);
ResponseEntity<StreamingResponseBody> response = controller.convertSvgToPdf(request);
assertEquals(HttpStatus.BAD_REQUEST, response.getStatusCode());
assertTrue(new String(response.getBody(), StandardCharsets.UTF_8).contains("No valid SVG"));
assertTrue(
new String(drainBody(response), StandardCharsets.UTF_8).contains("No valid SVG"));
}
@Test
@@ -84,7 +118,7 @@ class ConvertSvgToPDFTest {
request.setFileInput(new MockMultipartFile[] {emptyFile});
request.setCombineIntoSinglePdf(false);
ResponseEntity<byte[]> response = controller.convertSvgToPdf(request);
ResponseEntity<StreamingResponseBody> response = controller.convertSvgToPdf(request);
assertEquals(HttpStatus.BAD_REQUEST, response.getStatusCode());
}
@@ -107,7 +141,7 @@ class ConvertSvgToPDFTest {
when(pdfDocumentFactory.createNewBytesBasedOnOldDocument(pdfBytes))
.thenReturn(processedPdf);
ResponseEntity<byte[]> expectedResponse = ResponseEntity.ok(processedPdf);
ResponseEntity<StreamingResponseBody> expectedResponse = streamingOk(processedPdf);
try (MockedStatic<SvgToPdf> svgMock = Mockito.mockStatic(SvgToPdf.class);
MockedStatic<GeneralUtils> guMock = Mockito.mockStatic(GeneralUtils.class);
@@ -121,11 +155,11 @@ class ConvertSvgToPDFTest {
wrMock.when(
() ->
WebResponseUtils.bytesToWebResponse(
processedPdf, "drawing.pdf", MediaType.APPLICATION_PDF))
WebResponseUtils.pdfFileToWebResponse(
any(TempFile.class), anyString()))
.thenReturn(expectedResponse);
ResponseEntity<byte[]> response = controller.convertSvgToPdf(request);
ResponseEntity<StreamingResponseBody> response = controller.convertSvgToPdf(request);
assertEquals(HttpStatus.OK, response.getStatusCode());
}
@@ -154,7 +188,7 @@ class ConvertSvgToPDFTest {
when(pdfDocumentFactory.createNewBytesBasedOnOldDocument(combinedPdf))
.thenReturn(processedPdf);
ResponseEntity<byte[]> expectedResponse = ResponseEntity.ok(processedPdf);
ResponseEntity<StreamingResponseBody> expectedResponse = streamingOk(processedPdf);
try (MockedStatic<SvgToPdf> svgMock = Mockito.mockStatic(SvgToPdf.class);
MockedStatic<GeneralUtils> guMock = Mockito.mockStatic(GeneralUtils.class);
@@ -168,13 +202,11 @@ class ConvertSvgToPDFTest {
wrMock.when(
() ->
WebResponseUtils.bytesToWebResponse(
processedPdf,
"a_combined.pdf",
MediaType.APPLICATION_PDF))
WebResponseUtils.pdfFileToWebResponse(
any(TempFile.class), anyString()))
.thenReturn(expectedResponse);
ResponseEntity<byte[]> response = controller.convertSvgToPdf(request);
ResponseEntity<StreamingResponseBody> response = controller.convertSvgToPdf(request);
assertEquals(HttpStatus.OK, response.getStatusCode());
}
@@ -189,7 +221,7 @@ class ConvertSvgToPDFTest {
request.setFileInput(new MockMultipartFile[] {nullNameFile});
request.setCombineIntoSinglePdf(false);
ResponseEntity<byte[]> response = controller.convertSvgToPdf(request);
ResponseEntity<StreamingResponseBody> response = controller.convertSvgToPdf(request);
assertEquals(HttpStatus.BAD_REQUEST, response.getStatusCode());
}
@@ -206,7 +238,7 @@ class ConvertSvgToPDFTest {
when(svgSanitizer.sanitize(svgContent)).thenThrow(new IOException("sanitization error"));
ResponseEntity<byte[]> response = controller.convertSvgToPdf(request);
ResponseEntity<StreamingResponseBody> response = controller.convertSvgToPdf(request);
assertEquals(HttpStatus.BAD_REQUEST, response.getStatusCode());
}
@@ -4,9 +4,10 @@ import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.lenient;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Method;
@@ -34,6 +35,7 @@ import org.springframework.http.ResponseEntity;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;
import stirling.software.SPDF.model.api.converters.UrlToPdfRequest;
import stirling.software.common.configuration.RuntimePathConfig;
@@ -43,13 +45,25 @@ import stirling.software.common.util.GeneralUtils;
import stirling.software.common.util.ProcessExecutor;
import stirling.software.common.util.ProcessExecutor.ProcessExecutorResult;
import stirling.software.common.util.ProcessExecutor.Processes;
import stirling.software.common.util.WebResponseUtils;
import stirling.software.common.util.TempFile;
import stirling.software.common.util.TempFileManager;
public class ConvertWebsiteToPdfTest {
private static ResponseEntity<StreamingResponseBody> streamingOk(byte[] bytes) {
return ResponseEntity.ok(out -> out.write(bytes));
}
private static byte[] drainBody(ResponseEntity<StreamingResponseBody> response)
throws java.io.IOException {
java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream();
response.getBody().writeTo(baos);
return baos.toByteArray();
}
private static final Pattern PDF_FILENAME_PATTERN = Pattern.compile("[A-Za-z0-9_]+\\.pdf");
@Mock private CustomPDFDocumentFactory pdfDocumentFactory;
@Mock private RuntimePathConfig runtimePathConfig;
@Mock private TempFileManager tempFileManager;
private ApplicationProperties applicationProperties;
private ConvertWebsiteToPDF sut;
@@ -58,6 +72,18 @@ public class ConvertWebsiteToPdfTest {
@BeforeEach
void setUp() throws Exception {
mocks = MockitoAnnotations.openMocks(this);
lenient()
.when(tempFileManager.createManagedTempFile(anyString()))
.thenAnswer(
inv -> {
File f =
Files.createTempFile("test", inv.<String>getArgument(0))
.toFile();
TempFile tf = mock(TempFile.class);
lenient().when(tf.getFile()).thenReturn(f);
lenient().when(tf.getPath()).thenReturn(f.toPath());
return tf;
});
// Enable feature (adjust structure for your project if necessary)
applicationProperties = new ApplicationProperties();
@@ -68,7 +94,12 @@ public class ConvertWebsiteToPdfTest {
when(pdfDocumentFactory.load(any(File.class))).thenReturn(new PDDocument());
// Build SUT
sut = new ConvertWebsiteToPDF(pdfDocumentFactory, runtimePathConfig, applicationProperties);
sut =
new ConvertWebsiteToPDF(
pdfDocumentFactory,
runtimePathConfig,
applicationProperties,
tempFileManager);
// Provide RequestContext for ServletUriComponentsBuilder
MockHttpServletRequest req = new MockHttpServletRequest();
@@ -172,38 +203,29 @@ public class ConvertWebsiteToPdfTest {
request.setUrlInput("https://example.com");
try (MockedStatic<ProcessExecutor> pe = Mockito.mockStatic(ProcessExecutor.class);
MockedStatic<WebResponseUtils> wr = Mockito.mockStatic(WebResponseUtils.class);
MockedStatic<GeneralUtils> gu = Mockito.mockStatic(GeneralUtils.class);
MockedStatic<HttpClient> httpClient = mockHttpClientReturning("<html></html>")) {
// Force URL checks to be positive
gu.when(() -> GeneralUtils.isValidURL("https://example.com")).thenReturn(true);
gu.when(() -> GeneralUtils.isURLReachable("https://example.com")).thenReturn(true);
gu.when(() -> GeneralUtils.convertToFileName(anyString())).thenReturn("example_com");
gu.when(() -> GeneralUtils.generateFilename(anyString(), anyString()))
.thenAnswer(inv -> inv.<String>getArgument(0) + inv.<String>getArgument(1));
// correct ProcessExecutor!
ProcessExecutor mockExec = Mockito.mock(ProcessExecutor.class);
pe.when(() -> ProcessExecutor.getInstance(Processes.WEASYPRINT)).thenReturn(mockExec);
@SuppressWarnings("unchecked")
ArgumentCaptor<List<String>> cmdCaptor = ArgumentCaptor.forClass(List.class);
// Return value of correct type
ProcessExecutorResult dummyResult = Mockito.mock(ProcessExecutorResult.class);
when(mockExec.runCommandWithOutputHandling(cmdCaptor.capture()))
.thenReturn(dummyResult);
ResponseEntity<byte[]> fakeResponse = ResponseEntity.ok(new byte[0]);
wr.when(
() ->
WebResponseUtils.baosToWebResponse(
any(ByteArrayOutputStream.class), any()))
.thenReturn(fakeResponse);
// Act
ResponseEntity<?> resp = sut.urlToPdf(request);
// Assert Response OK
// Assert
assertNotNull(resp);
assertEquals(HttpStatus.OK, resp.getStatusCode());
// Assert WeasyPrint command correct
@@ -236,17 +258,19 @@ public class ConvertWebsiteToPdfTest {
try (MockedStatic<GeneralUtils> gu = Mockito.mockStatic(GeneralUtils.class);
MockedStatic<ProcessExecutor> pe = Mockito.mockStatic(ProcessExecutor.class);
MockedStatic<WebResponseUtils> wr = Mockito.mockStatic(WebResponseUtils.class);
MockedStatic<Files> files = Mockito.mockStatic(Files.class);
MockedStatic<HttpClient> httpClient = mockHttpClientReturning("<html></html>")) {
// Force URL checks to be positive
gu.when(() -> GeneralUtils.isValidURL("https://example.com")).thenReturn(true);
gu.when(() -> GeneralUtils.isURLReachable("https://example.com")).thenReturn(true);
gu.when(() -> GeneralUtils.convertToFileName(anyString())).thenReturn("example_com");
gu.when(() -> GeneralUtils.generateFilename(anyString(), anyString()))
.thenAnswer(inv -> inv.<String>getArgument(0) + inv.<String>getArgument(1));
// Force temp files + provoke delete error
files.when(() -> Files.createTempFile("url_input_", ".html")).thenReturn(htmlTemp);
files.when(() -> Files.createTempFile("output_", ".pdf")).thenReturn(preCreatedTemp);
files.when(() -> Files.createTempFile(eq("test"), anyString()))
.thenReturn(preCreatedTemp);
files.when(
() ->
Files.writeString(
@@ -257,26 +281,20 @@ public class ConvertWebsiteToPdfTest {
files.when(() -> Files.deleteIfExists(htmlTemp)).thenReturn(true);
files.when(() -> Files.deleteIfExists(preCreatedTemp))
.thenThrow(new IOException("fail delete"));
files.when(() -> Files.exists(preCreatedTemp)).thenReturn(true); // for the assert
files.when(() -> Files.exists(preCreatedTemp)).thenReturn(true);
files.when(() -> Files.size(any(Path.class))).thenReturn(100L);
files.when(() -> Files.copy(any(Path.class), any(java.io.OutputStream.class)))
.thenReturn(0L);
files.when(() -> Files.newOutputStream(any(Path.class)))
.thenAnswer(inv -> new java.io.ByteArrayOutputStream());
// ProcessExecutor
ProcessExecutor mockExec = Mockito.mock(ProcessExecutor.class);
pe.when(() -> ProcessExecutor.getInstance(Processes.WEASYPRINT)).thenReturn(mockExec);
ProcessExecutorResult dummy = Mockito.mock(ProcessExecutorResult.class);
when(mockExec.runCommandWithOutputHandling(Mockito.<List>any())).thenReturn(dummy);
// WebResponseUtils
ResponseEntity<byte[]> fakeResponse = ResponseEntity.ok(new byte[0]);
wr.when(
() ->
WebResponseUtils.baosToWebResponse(
any(ByteArrayOutputStream.class), any()))
.thenReturn(fakeResponse);
// Act: should not throw and should return a Response
ResponseEntity<?> resp = assertDoesNotThrow(() -> sut.urlToPdf(request));
// Assert
assertNotNull(resp, "Response should not be null");
assertEquals(HttpStatus.OK, resp.getStatusCode());
assertTrue(
@@ -3,11 +3,13 @@ package stirling.software.SPDF.controller.api.converters;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.lenient;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.when;
import java.io.File;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Path;
@@ -25,11 +27,13 @@ import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.mock.web.MockMultipartFile;
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;
import stirling.software.SPDF.config.EndpointConfiguration;
import stirling.software.SPDF.model.api.converters.PdfVectorExportRequest;
import stirling.software.common.util.ProcessExecutor;
import stirling.software.common.util.ProcessExecutor.ProcessExecutorResult;
import stirling.software.common.util.TempFile;
import stirling.software.common.util.TempFileManager;
@ExtendWith(MockitoExtension.class)
@@ -44,6 +48,18 @@ class PdfVectorExportControllerTest {
@BeforeEach
void setup() throws Exception {
lenient()
.when(tempFileManager.createManagedTempFile(anyString()))
.thenAnswer(
inv -> {
File f =
Files.createTempFile("test", inv.<String>getArgument(0))
.toFile();
TempFile tf = mock(TempFile.class);
lenient().when(tf.getFile()).thenReturn(f);
lenient().when(tf.getPath()).thenReturn(f.toPath());
return tf;
});
when(tempFileManager.createTempFile(any()))
.thenAnswer(
invocation -> {
@@ -106,7 +122,8 @@ class PdfVectorExportControllerTest {
PdfVectorExportRequest request = new PdfVectorExportRequest();
request.setFileInput(file);
ResponseEntity<byte[]> response = controller.convertGhostscriptInputsToPdf(request);
ResponseEntity<StreamingResponseBody> response =
controller.convertGhostscriptInputsToPdf(request);
assertThat(response.getStatusCode()).isEqualTo(org.springframework.http.HttpStatus.OK);
assertThat(response.getHeaders().getContentType()).isEqualTo(MediaType.APPLICATION_PDF);
@@ -123,11 +140,14 @@ class PdfVectorExportControllerTest {
PdfVectorExportRequest request = new PdfVectorExportRequest();
request.setFileInput(file);
ResponseEntity<byte[]> response = controller.convertGhostscriptInputsToPdf(request);
ResponseEntity<StreamingResponseBody> response =
controller.convertGhostscriptInputsToPdf(request);
assertThat(response.getStatusCode()).isEqualTo(org.springframework.http.HttpStatus.OK);
assertThat(response.getHeaders().getContentType()).isEqualTo(MediaType.APPLICATION_PDF);
assertThat(response.getBody()).contains(content);
java.io.ByteArrayOutputStream baosVerify = new java.io.ByteArrayOutputStream();
response.getBody().writeTo(baosVerify);
assertThat(baosVerify.toByteArray()).contains(content);
}
@Test
@@ -17,6 +17,7 @@ import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.mock.web.MockMultipartFile;
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;
import stirling.software.SPDF.model.api.PDFComparisonAndCount;
import stirling.software.SPDF.model.api.PDFWithPageNums;
@@ -26,12 +27,24 @@ import stirling.software.SPDF.model.api.filter.PageRotationRequest;
import stirling.software.SPDF.model.api.filter.PageSizeRequest;
import stirling.software.common.service.CustomPDFDocumentFactory;
import stirling.software.common.util.PdfUtils;
import stirling.software.common.util.TempFileManager;
import stirling.software.common.util.WebResponseUtils;
@ExtendWith(MockitoExtension.class)
class FilterControllerTest {
private static ResponseEntity<StreamingResponseBody> streamingOk(byte[] bytes) {
return ResponseEntity.ok(out -> out.write(bytes));
}
private static byte[] drainBody(ResponseEntity<StreamingResponseBody> response)
throws java.io.IOException {
java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream();
response.getBody().writeTo(baos);
return baos.toByteArray();
}
@Mock private CustomPDFDocumentFactory pdfDocumentFactory;
@Mock private TempFileManager tempFileManager;
@InjectMocks private FilterController filterController;
@@ -59,19 +72,22 @@ class FilterControllerTest {
PDDocument mockDoc = mock(PDDocument.class);
when(pdfDocumentFactory.load(mockFile)).thenReturn(mockDoc);
ResponseEntity<byte[]> expectedResponse = ResponseEntity.ok(new byte[] {1, 2, 3});
ResponseEntity<StreamingResponseBody> expectedResponse = streamingOk(new byte[] {1, 2, 3});
try (MockedStatic<PdfUtils> pdfUtilsMock = mockStatic(PdfUtils.class);
MockedStatic<WebResponseUtils> webMock = mockStatic(WebResponseUtils.class)) {
pdfUtilsMock.when(() -> PdfUtils.hasText(mockDoc, "all", "hello")).thenReturn(true);
webMock.when(() -> WebResponseUtils.pdfDocToWebResponse(mockDoc, "test.pdf"))
webMock.when(
() ->
WebResponseUtils.pdfDocToWebResponse(
mockDoc, "test.pdf", tempFileManager))
.thenReturn(expectedResponse);
ResponseEntity<byte[]> result = filterController.containsText(request);
ResponseEntity<StreamingResponseBody> result = filterController.containsText(request);
assertEquals(HttpStatus.OK, result.getStatusCode());
assertArrayEquals(new byte[] {1, 2, 3}, result.getBody());
assertArrayEquals(new byte[] {1, 2, 3}, drainBody(result));
}
}
@@ -88,7 +104,7 @@ class FilterControllerTest {
try (MockedStatic<PdfUtils> pdfUtilsMock = mockStatic(PdfUtils.class)) {
pdfUtilsMock.when(() -> PdfUtils.hasText(mockDoc, "all", "missing")).thenReturn(false);
ResponseEntity<byte[]> result = filterController.containsText(request);
ResponseEntity<StreamingResponseBody> result = filterController.containsText(request);
assertEquals(HttpStatus.NO_CONTENT, result.getStatusCode());
assertNull(result.getBody());
@@ -106,19 +122,22 @@ class FilterControllerTest {
PDDocument mockDoc = mock(PDDocument.class);
when(pdfDocumentFactory.load(mockFile)).thenReturn(mockDoc);
ResponseEntity<byte[]> expectedResponse = ResponseEntity.ok(new byte[] {4, 5, 6});
ResponseEntity<StreamingResponseBody> expectedResponse = streamingOk(new byte[] {4, 5, 6});
try (MockedStatic<PdfUtils> pdfUtilsMock = mockStatic(PdfUtils.class);
MockedStatic<WebResponseUtils> webMock = mockStatic(WebResponseUtils.class)) {
pdfUtilsMock.when(() -> PdfUtils.hasImages(mockDoc, "all")).thenReturn(true);
webMock.when(() -> WebResponseUtils.pdfDocToWebResponse(mockDoc, "test.pdf"))
webMock.when(
() ->
WebResponseUtils.pdfDocToWebResponse(
mockDoc, "test.pdf", tempFileManager))
.thenReturn(expectedResponse);
ResponseEntity<byte[]> result = filterController.containsImage(request);
ResponseEntity<StreamingResponseBody> result = filterController.containsImage(request);
assertEquals(HttpStatus.OK, result.getStatusCode());
assertArrayEquals(new byte[] {4, 5, 6}, result.getBody());
assertArrayEquals(new byte[] {4, 5, 6}, drainBody(result));
}
}
@@ -134,7 +153,7 @@ class FilterControllerTest {
try (MockedStatic<PdfUtils> pdfUtilsMock = mockStatic(PdfUtils.class)) {
pdfUtilsMock.when(() -> PdfUtils.hasImages(mockDoc, "1")).thenReturn(false);
ResponseEntity<byte[]> result = filterController.containsImage(request);
ResponseEntity<StreamingResponseBody> result = filterController.containsImage(request);
assertEquals(HttpStatus.NO_CONTENT, result.getStatusCode());
}
@@ -2,10 +2,13 @@ package stirling.software.SPDF.controller.api.form;
import static org.assertj.core.api.Assertions.*;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.*;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage;
@@ -22,8 +25,11 @@ import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.mock.web.MockMultipartFile;
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;
import stirling.software.common.service.CustomPDFDocumentFactory;
import stirling.software.common.util.TempFile;
import stirling.software.common.util.TempFileManager;
import tools.jackson.databind.ObjectMapper;
import tools.jackson.databind.json.JsonMapper;
@@ -31,8 +37,19 @@ import tools.jackson.databind.json.JsonMapper;
@ExtendWith(MockitoExtension.class)
@DisplayName("FormFillController Tests")
class FormFillControllerTest {
private static ResponseEntity<StreamingResponseBody> streamingOk(byte[] bytes) {
return ResponseEntity.ok(out -> out.write(bytes));
}
private static byte[] drainBody(ResponseEntity<StreamingResponseBody> response)
throws java.io.IOException {
java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream();
response.getBody().writeTo(baos);
return baos.toByteArray();
}
@Mock private CustomPDFDocumentFactory pdfDocumentFactory;
@Mock private TempFileManager tempFileManager;
private ObjectMapper realObjectMapper;
@@ -40,6 +57,18 @@ class FormFillControllerTest {
@BeforeEach
void setUp() throws Exception {
lenient()
.when(tempFileManager.createManagedTempFile(anyString()))
.thenAnswer(
inv -> {
File f =
Files.createTempFile("test", inv.<String>getArgument(0))
.toFile();
TempFile tf = mock(TempFile.class);
lenient().when(tf.getFile()).thenReturn(f);
lenient().when(tf.getPath()).thenReturn(f.toPath());
return tf;
});
realObjectMapper = JsonMapper.builder().build();
// Inject real ObjectMapper via reflection since @InjectMocks uses the mock
var field = FormFillController.class.getDeclaredField("objectMapper");
@@ -203,7 +232,8 @@ class FormFillControllerTest {
when(pdfDocumentFactory.load(eq(file))).thenReturn(doc);
byte[] payload = "{\"field1\":\"value1\"}".getBytes();
ResponseEntity<byte[]> response = controller.fillForm(file, payload, false);
ResponseEntity<StreamingResponseBody> response =
controller.fillForm(file, payload, false);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(response.getBody()).isNotNull();
@@ -216,7 +246,7 @@ class FormFillControllerTest {
PDDocument doc = createMinimalPdf();
when(pdfDocumentFactory.load(eq(file))).thenReturn(doc);
ResponseEntity<byte[]> response = controller.fillForm(file, null, false);
ResponseEntity<StreamingResponseBody> response = controller.fillForm(file, null, false);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
}
@@ -257,7 +287,7 @@ class FormFillControllerTest {
when(pdfDocumentFactory.load(eq(file))).thenReturn(doc);
byte[] payload = "[\"field1\"]".getBytes();
ResponseEntity<byte[]> response = controller.deleteFields(file, payload);
ResponseEntity<StreamingResponseBody> response = controller.deleteFields(file, payload);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
}
@@ -293,7 +323,8 @@ class FormFillControllerTest {
String json =
"[{\"targetName\":\"f1\",\"name\":null,\"label\":null,\"type\":null,"
+ "\"required\":null,\"multiSelect\":null,\"options\":null,\"defaultValue\":\"newVal\",\"tooltip\":null}]";
ResponseEntity<byte[]> response = controller.modifyFields(file, json.getBytes());
ResponseEntity<StreamingResponseBody> response =
controller.modifyFields(file, json.getBytes());
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
}
@@ -1,9 +1,12 @@
package stirling.software.SPDF.controller.api.misc;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.*;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.util.List;
import org.apache.pdfbox.pdmodel.PDDocument;
@@ -19,18 +22,32 @@ import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.mock.web.MockMultipartFile;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;
import stirling.software.SPDF.model.api.misc.AddAttachmentRequest;
import stirling.software.SPDF.service.AttachmentServiceInterface;
import stirling.software.common.service.CustomPDFDocumentFactory;
import stirling.software.common.util.TempFile;
import stirling.software.common.util.TempFileManager;
import stirling.software.common.util.WebResponseUtils;
@ExtendWith(MockitoExtension.class)
class AttachmentControllerTest {
private static ResponseEntity<StreamingResponseBody> streamingOk(byte[] bytes) {
return ResponseEntity.ok(out -> out.write(bytes));
}
private static byte[] drainBody(ResponseEntity<StreamingResponseBody> response)
throws java.io.IOException {
java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream();
response.getBody().writeTo(baos);
return baos.toByteArray();
}
@Mock private CustomPDFDocumentFactory pdfDocumentFactory;
@Mock private AttachmentServiceInterface pdfAttachmentService;
@Mock private TempFileManager tempFileManager;
@InjectMocks private AttachmentController attachmentController;
@@ -42,7 +59,19 @@ class AttachmentControllerTest {
private PDDocument modifiedMockDocument;
@BeforeEach
void setUp() {
void setUp() throws Exception {
lenient()
.when(tempFileManager.createManagedTempFile(anyString()))
.thenAnswer(
inv -> {
File f =
Files.createTempFile("test", inv.<String>getArgument(0))
.toFile();
TempFile tf = mock(TempFile.class);
lenient().when(tf.getFile()).thenReturn(f);
lenient().when(tf.getPath()).thenReturn(f.toPath());
return tf;
});
pdfFile =
new MockMultipartFile(
"fileInput",
@@ -71,8 +100,8 @@ class AttachmentControllerTest {
List<MultipartFile> attachments = List.of(attachment1, attachment2);
request.setAttachments(attachments);
request.setFileInput(pdfFile);
ResponseEntity<byte[]> expectedResponse =
ResponseEntity.ok("modified PDF content".getBytes());
ResponseEntity<StreamingResponseBody> expectedResponse =
streamingOk("modified PDF content".getBytes());
when(pdfDocumentFactory.load(request, false)).thenReturn(mockDocument);
when(pdfAttachmentService.addAttachment(mockDocument, attachments))
@@ -84,10 +113,13 @@ class AttachmentControllerTest {
.when(
() ->
WebResponseUtils.pdfDocToWebResponse(
eq(mockDocument), eq("test_with_attachments.pdf")))
any(PDDocument.class),
anyString(),
any(TempFileManager.class)))
.thenReturn(expectedResponse);
ResponseEntity<byte[]> response = attachmentController.addAttachments(request);
ResponseEntity<StreamingResponseBody> response =
attachmentController.addAttachments(request);
assertNotNull(response);
assertEquals(HttpStatus.OK, response.getStatusCode());
@@ -102,8 +134,8 @@ class AttachmentControllerTest {
List<MultipartFile> attachments = List.of(attachment1);
request.setAttachments(attachments);
request.setFileInput(pdfFile);
ResponseEntity<byte[]> expectedResponse =
ResponseEntity.ok("modified PDF content".getBytes());
ResponseEntity<StreamingResponseBody> expectedResponse =
streamingOk("modified PDF content".getBytes());
when(pdfDocumentFactory.load(request, false)).thenReturn(mockDocument);
when(pdfAttachmentService.addAttachment(mockDocument, attachments))
@@ -115,10 +147,13 @@ class AttachmentControllerTest {
.when(
() ->
WebResponseUtils.pdfDocToWebResponse(
eq(mockDocument), eq("test_with_attachments.pdf")))
any(PDDocument.class),
anyString(),
any(TempFileManager.class)))
.thenReturn(expectedResponse);
ResponseEntity<byte[]> response = attachmentController.addAttachments(request);
ResponseEntity<StreamingResponseBody> response =
attachmentController.addAttachments(request);
assertNotNull(response);
assertEquals(HttpStatus.OK, response.getStatusCode());
@@ -1,8 +1,10 @@
package stirling.software.SPDF.controller.api.misc;
import static org.assertj.core.api.Assertions.*;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.*;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
@@ -14,6 +16,7 @@ import org.apache.pdfbox.pdmodel.PDPageContentStream;
import org.apache.pdfbox.pdmodel.common.PDRectangle;
import org.apache.pdfbox.pdmodel.font.PDType1Font;
import org.apache.pdfbox.pdmodel.font.Standard14Fonts;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.api.io.TempDir;
@@ -24,17 +27,47 @@ import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.mock.web.MockMultipartFile;
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;
import stirling.software.SPDF.model.api.misc.ExtractHeaderRequest;
import stirling.software.common.service.CustomPDFDocumentFactory;
import stirling.software.common.util.TempFile;
import stirling.software.common.util.TempFileManager;
@ExtendWith(MockitoExtension.class)
class AutoRenameControllerTest {
private static ResponseEntity<StreamingResponseBody> streamingOk(byte[] bytes) {
return ResponseEntity.ok(out -> out.write(bytes));
}
private static byte[] drainBody(ResponseEntity<StreamingResponseBody> response)
throws java.io.IOException {
java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream();
response.getBody().writeTo(baos);
return baos.toByteArray();
}
@TempDir Path tempDir;
@Mock private CustomPDFDocumentFactory pdfDocumentFactory;
@Mock private TempFileManager tempFileManager;
@InjectMocks private AutoRenameController controller;
@BeforeEach
void setUp() throws Exception {
lenient()
.when(tempFileManager.createManagedTempFile(anyString()))
.thenAnswer(
inv -> {
File f =
Files.createTempFile("test", inv.<String>getArgument(0))
.toFile();
TempFile tf = mock(TempFile.class);
lenient().when(tf.getFile()).thenReturn(f);
lenient().when(tf.getPath()).thenReturn(f.toPath());
return tf;
});
}
private MockMultipartFile createPdfWithText(String text, float fontSize) throws IOException {
Path path = tempDir.resolve("test.pdf");
try (PDDocument doc = new PDDocument()) {
@@ -68,10 +101,10 @@ class AutoRenameControllerTest {
PDDocument doc = Loader.loadPDF(file.getBytes());
when(pdfDocumentFactory.load(file)).thenReturn(doc);
ResponseEntity<byte[]> response = controller.extractHeader(request);
ResponseEntity<StreamingResponseBody> response = controller.extractHeader(request);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(response.getBody()).isNotEmpty();
assertThat(drainBody(response)).isNotEmpty();
String contentDisposition = response.getHeaders().getFirst("Content-Disposition");
assertThat(contentDisposition).contains(".pdf");
}
@@ -94,7 +127,7 @@ class AutoRenameControllerTest {
PDDocument doc = Loader.loadPDF(file.getBytes());
when(pdfDocumentFactory.load(file)).thenReturn(doc);
ResponseEntity<byte[]> response = controller.extractHeader(request);
ResponseEntity<StreamingResponseBody> response = controller.extractHeader(request);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
}
@@ -107,7 +140,7 @@ class AutoRenameControllerTest {
PDDocument doc = Loader.loadPDF(file.getBytes());
when(pdfDocumentFactory.load(file)).thenReturn(doc);
ResponseEntity<byte[]> response = controller.extractHeader(request);
ResponseEntity<StreamingResponseBody> response = controller.extractHeader(request);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
}
@@ -155,7 +188,7 @@ class AutoRenameControllerTest {
PDDocument doc = Loader.loadPDF(file.getBytes());
when(pdfDocumentFactory.load(file)).thenReturn(doc);
ResponseEntity<byte[]> response = controller.extractHeader(request);
ResponseEntity<StreamingResponseBody> response = controller.extractHeader(request);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
// The largest font text should be used as title (URL-encoded in Content-Disposition)
@@ -173,7 +206,7 @@ class AutoRenameControllerTest {
PDDocument doc = Loader.loadPDF(file.getBytes());
when(pdfDocumentFactory.load(file)).thenReturn(doc);
ResponseEntity<byte[]> response = controller.extractHeader(request);
ResponseEntity<StreamingResponseBody> response = controller.extractHeader(request);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
// Should fallback to original filename since header is too long
@@ -189,7 +222,7 @@ class AutoRenameControllerTest {
PDDocument doc = Loader.loadPDF(file.getBytes());
when(pdfDocumentFactory.load(file)).thenReturn(doc);
ResponseEntity<byte[]> response = controller.extractHeader(request);
ResponseEntity<StreamingResponseBody> response = controller.extractHeader(request);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
// Special characters should be sanitized
@@ -215,7 +248,7 @@ class AutoRenameControllerTest {
PDDocument doc = Loader.loadPDF(file.getBytes());
when(pdfDocumentFactory.load(file)).thenReturn(doc);
ResponseEntity<byte[]> response = controller.extractHeader(request);
ResponseEntity<StreamingResponseBody> response = controller.extractHeader(request);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
}
@@ -1,8 +1,10 @@
package stirling.software.SPDF.controller.api.misc;
import static org.assertj.core.api.Assertions.*;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.*;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
@@ -14,6 +16,7 @@ import org.apache.pdfbox.pdmodel.PDPageContentStream;
import org.apache.pdfbox.pdmodel.common.PDRectangle;
import org.apache.pdfbox.pdmodel.font.PDType1Font;
import org.apache.pdfbox.pdmodel.font.Standard14Fonts;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.api.io.TempDir;
@@ -24,17 +27,47 @@ import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.mock.web.MockMultipartFile;
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;
import stirling.software.common.model.api.PDFFile;
import stirling.software.common.service.CustomPDFDocumentFactory;
import stirling.software.common.util.TempFile;
import stirling.software.common.util.TempFileManager;
@ExtendWith(MockitoExtension.class)
class DecompressPdfControllerTest {
private static ResponseEntity<StreamingResponseBody> streamingOk(byte[] bytes) {
return ResponseEntity.ok(out -> out.write(bytes));
}
private static byte[] drainBody(ResponseEntity<StreamingResponseBody> response)
throws java.io.IOException {
java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream();
response.getBody().writeTo(baos);
return baos.toByteArray();
}
@TempDir Path tempDir;
@Mock private CustomPDFDocumentFactory pdfDocumentFactory;
@Mock private TempFileManager tempFileManager;
@InjectMocks private DecompressPdfController controller;
@BeforeEach
void setUp() throws Exception {
lenient()
.when(tempFileManager.createManagedTempFile(anyString()))
.thenAnswer(
inv -> {
File f =
Files.createTempFile("test", inv.<String>getArgument(0))
.toFile();
TempFile tf = mock(TempFile.class);
lenient().when(tf.getFile()).thenReturn(f);
lenient().when(tf.getPath()).thenReturn(f.toPath());
return tf;
});
}
private MockMultipartFile createRealPdf(String content) throws IOException {
Path path = tempDir.resolve("test.pdf");
try (PDDocument doc = new PDDocument()) {
@@ -64,12 +97,12 @@ class DecompressPdfControllerTest {
PDDocument doc = Loader.loadPDF(file.getBytes());
when(pdfDocumentFactory.load(file)).thenReturn(doc);
ResponseEntity<byte[]> response = controller.decompressPdf(request);
ResponseEntity<StreamingResponseBody> response = controller.decompressPdf(request);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(response.getBody()).isNotEmpty();
assertThat(drainBody(response)).isNotEmpty();
// Verify the result is a valid PDF
try (PDDocument result = Loader.loadPDF(response.getBody())) {
try (PDDocument result = Loader.loadPDF(drainBody(response))) {
assertThat(result.getNumberOfPages()).isEqualTo(1);
}
}
@@ -83,10 +116,10 @@ class DecompressPdfControllerTest {
PDDocument doc = Loader.loadPDF(file.getBytes());
when(pdfDocumentFactory.load(file)).thenReturn(doc);
ResponseEntity<byte[]> response = controller.decompressPdf(request);
ResponseEntity<StreamingResponseBody> response = controller.decompressPdf(request);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(response.getBody()).isNotEmpty();
assertThat(drainBody(response)).isNotEmpty();
}
@Test
@@ -114,7 +147,7 @@ class DecompressPdfControllerTest {
PDDocument doc = Loader.loadPDF(file.getBytes());
when(pdfDocumentFactory.load(file)).thenReturn(doc);
ResponseEntity<byte[]> response = controller.decompressPdf(request);
ResponseEntity<StreamingResponseBody> response = controller.decompressPdf(request);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
String contentDisposition = response.getHeaders().getFirst("Content-Disposition");
@@ -150,10 +183,10 @@ class DecompressPdfControllerTest {
PDDocument doc = Loader.loadPDF(file.getBytes());
when(pdfDocumentFactory.load(file)).thenReturn(doc);
ResponseEntity<byte[]> response = controller.decompressPdf(request);
ResponseEntity<StreamingResponseBody> response = controller.decompressPdf(request);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
try (PDDocument result = Loader.loadPDF(response.getBody())) {
try (PDDocument result = Loader.loadPDF(drainBody(response))) {
assertThat(result.getNumberOfPages()).isEqualTo(3);
}
}
@@ -167,11 +200,11 @@ class DecompressPdfControllerTest {
PDDocument doc = Loader.loadPDF(file.getBytes());
when(pdfDocumentFactory.load(file)).thenReturn(doc);
ResponseEntity<byte[]> response = controller.decompressPdf(request);
ResponseEntity<StreamingResponseBody> response = controller.decompressPdf(request);
assertThat(response.getBody()).isNotNull();
// Decompressed PDF should generally be larger or equal to compressed
assertThat(response.getBody().length).isGreaterThan(0);
assertThat(drainBody(response).length).isGreaterThan(0);
}
@Test
@@ -183,7 +216,7 @@ class DecompressPdfControllerTest {
PDDocument doc = Loader.loadPDF(file.getBytes());
when(pdfDocumentFactory.load(file)).thenReturn(doc);
ResponseEntity<byte[]> response = controller.decompressPdf(request);
ResponseEntity<StreamingResponseBody> response = controller.decompressPdf(request);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
}
@@ -1,8 +1,10 @@
package stirling.software.SPDF.controller.api.misc;
import static org.assertj.core.api.Assertions.*;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.*;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
@@ -16,6 +18,7 @@ import org.apache.pdfbox.pdmodel.common.PDRectangle;
import org.apache.pdfbox.pdmodel.font.PDType1Font;
import org.apache.pdfbox.pdmodel.font.Standard14Fonts;
import org.apache.pdfbox.pdmodel.interactive.form.PDAcroForm;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.api.io.TempDir;
@@ -26,17 +29,47 @@ import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.mock.web.MockMultipartFile;
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;
import stirling.software.SPDF.model.api.misc.FlattenRequest;
import stirling.software.common.service.CustomPDFDocumentFactory;
import stirling.software.common.util.TempFile;
import stirling.software.common.util.TempFileManager;
@ExtendWith(MockitoExtension.class)
class FlattenControllerTest {
private static ResponseEntity<StreamingResponseBody> streamingOk(byte[] bytes) {
return ResponseEntity.ok(out -> out.write(bytes));
}
private static byte[] drainBody(ResponseEntity<StreamingResponseBody> response)
throws java.io.IOException {
java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream();
response.getBody().writeTo(baos);
return baos.toByteArray();
}
@TempDir Path tempDir;
@Mock private CustomPDFDocumentFactory pdfDocumentFactory;
@Mock private TempFileManager tempFileManager;
@InjectMocks private FlattenController controller;
@BeforeEach
void setUp() throws Exception {
lenient()
.when(tempFileManager.createManagedTempFile(anyString()))
.thenAnswer(
inv -> {
File f =
Files.createTempFile("test", inv.<String>getArgument(0))
.toFile();
TempFile tf = mock(TempFile.class);
lenient().when(tf.getFile()).thenReturn(f);
lenient().when(tf.getPath()).thenReturn(f.toPath());
return tf;
});
}
private MockMultipartFile createPdf() throws IOException {
Path path = tempDir.resolve("test.pdf");
try (PDDocument doc = new PDDocument()) {
@@ -65,10 +98,10 @@ class FlattenControllerTest {
PDDocument doc = Loader.loadPDF(file.getBytes());
when(pdfDocumentFactory.load(file)).thenReturn(doc);
ResponseEntity<byte[]> response = controller.flatten(request);
ResponseEntity<StreamingResponseBody> response = controller.flatten(request);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(response.getBody()).isNotEmpty();
assertThat(drainBody(response)).isNotEmpty();
}
@Test
@@ -85,7 +118,7 @@ class FlattenControllerTest {
when(doc.getDocumentCatalog()).thenReturn(catalog);
when(catalog.getAcroForm()).thenReturn(null);
ResponseEntity<byte[]> response = controller.flatten(request);
ResponseEntity<StreamingResponseBody> response = controller.flatten(request);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
verify(doc).close();
@@ -105,7 +138,7 @@ class FlattenControllerTest {
when(doc.getDocumentCatalog()).thenReturn(catalog);
when(catalog.getAcroForm()).thenReturn(form);
ResponseEntity<byte[]> response = controller.flatten(request);
ResponseEntity<StreamingResponseBody> response = controller.flatten(request);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
verify(form).flatten();
@@ -137,10 +170,10 @@ class FlattenControllerTest {
when(pdfDocumentFactory.load(file)).thenReturn(doc);
when(pdfDocumentFactory.createNewDocumentBasedOnOldDocument(doc)).thenReturn(newDoc);
ResponseEntity<byte[]> response = controller.flatten(request);
ResponseEntity<StreamingResponseBody> response = controller.flatten(request);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(response.getBody()).isNotEmpty();
assertThat(drainBody(response)).isNotEmpty();
}
@Test
@@ -155,7 +188,7 @@ class FlattenControllerTest {
when(pdfDocumentFactory.load(file)).thenReturn(doc);
when(pdfDocumentFactory.createNewDocumentBasedOnOldDocument(doc)).thenReturn(newDoc);
ResponseEntity<byte[]> response = controller.flatten(request);
ResponseEntity<StreamingResponseBody> response = controller.flatten(request);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
}
@@ -173,7 +206,7 @@ class FlattenControllerTest {
when(pdfDocumentFactory.load(file)).thenReturn(doc);
when(pdfDocumentFactory.createNewDocumentBasedOnOldDocument(doc)).thenReturn(newDoc);
ResponseEntity<byte[]> response = controller.flatten(request);
ResponseEntity<StreamingResponseBody> response = controller.flatten(request);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
}
@@ -191,7 +224,7 @@ class FlattenControllerTest {
when(pdfDocumentFactory.load(file)).thenReturn(doc);
when(pdfDocumentFactory.createNewDocumentBasedOnOldDocument(doc)).thenReturn(newDoc);
ResponseEntity<byte[]> response = controller.flatten(request);
ResponseEntity<StreamingResponseBody> response = controller.flatten(request);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
}
@@ -2,11 +2,14 @@ package stirling.software.SPDF.controller.api.misc;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.*;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import javax.imageio.ImageIO;
@@ -24,15 +27,29 @@ import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.mock.web.MockMultipartFile;
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;
import stirling.software.SPDF.model.api.misc.OverlayImageRequest;
import stirling.software.common.service.CustomPDFDocumentFactory;
import stirling.software.common.util.TempFile;
import stirling.software.common.util.TempFileManager;
import stirling.software.common.util.WebResponseUtils;
@ExtendWith(MockitoExtension.class)
class OverlayImageControllerTest {
private static ResponseEntity<StreamingResponseBody> streamingOk(byte[] bytes) {
return ResponseEntity.ok(out -> out.write(bytes));
}
private static byte[] drainBody(ResponseEntity<StreamingResponseBody> response)
throws java.io.IOException {
java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream();
response.getBody().writeTo(baos);
return baos.toByteArray();
}
@Mock private CustomPDFDocumentFactory pdfDocumentFactory;
@Mock private TempFileManager tempFileManager;
@InjectMocks private OverlayImageController controller;
@@ -41,6 +58,18 @@ class OverlayImageControllerTest {
@BeforeEach
void setUp() throws IOException {
lenient()
.when(tempFileManager.createManagedTempFile(anyString()))
.thenAnswer(
inv -> {
File f =
Files.createTempFile("test", inv.<String>getArgument(0))
.toFile();
TempFile tf = mock(TempFile.class);
lenient().when(tf.getFile()).thenReturn(f);
lenient().when(tf.getPath()).thenReturn(f.toPath());
return tf;
});
pdfFile =
new MockMultipartFile(
"fileInput",
@@ -79,12 +108,16 @@ class OverlayImageControllerTest {
try (MockedStatic<WebResponseUtils> mockedWebResponse =
mockStatic(WebResponseUtils.class)) {
ResponseEntity<byte[]> expectedResponse = ResponseEntity.ok("result".getBytes());
ResponseEntity<StreamingResponseBody> expectedResponse =
streamingOk("result".getBytes());
mockedWebResponse
.when(() -> WebResponseUtils.bytesToWebResponse(any(byte[].class), anyString()))
.when(
() ->
WebResponseUtils.pdfFileToWebResponse(
any(TempFile.class), anyString()))
.thenReturn(expectedResponse);
ResponseEntity<byte[]> response = controller.overlayImage(request);
ResponseEntity<StreamingResponseBody> response = controller.overlayImage(request);
assertNotNull(response);
assertEquals(HttpStatus.OK, response.getStatusCode());
@@ -103,7 +136,7 @@ class OverlayImageControllerTest {
when(pdfDocumentFactory.load(any(byte[].class))).thenThrow(new IOException("bad PDF"));
ResponseEntity<byte[]> response = controller.overlayImage(request);
ResponseEntity<StreamingResponseBody> response = controller.overlayImage(request);
assertEquals(HttpStatus.BAD_REQUEST, response.getStatusCode());
}
@@ -124,12 +157,16 @@ class OverlayImageControllerTest {
try (MockedStatic<WebResponseUtils> mockedWebResponse =
mockStatic(WebResponseUtils.class)) {
ResponseEntity<byte[]> expectedResponse = ResponseEntity.ok("result".getBytes());
ResponseEntity<StreamingResponseBody> expectedResponse =
streamingOk("result".getBytes());
mockedWebResponse
.when(() -> WebResponseUtils.bytesToWebResponse(any(byte[].class), anyString()))
.when(
() ->
WebResponseUtils.pdfFileToWebResponse(
any(TempFile.class), anyString()))
.thenReturn(expectedResponse);
ResponseEntity<byte[]> response = controller.overlayImage(request);
ResponseEntity<StreamingResponseBody> response = controller.overlayImage(request);
assertNotNull(response);
assertEquals(HttpStatus.OK, response.getStatusCode());
@@ -152,12 +189,16 @@ class OverlayImageControllerTest {
try (MockedStatic<WebResponseUtils> mockedWebResponse =
mockStatic(WebResponseUtils.class)) {
ResponseEntity<byte[]> expectedResponse = ResponseEntity.ok("result".getBytes());
ResponseEntity<StreamingResponseBody> expectedResponse =
streamingOk("result".getBytes());
mockedWebResponse
.when(() -> WebResponseUtils.bytesToWebResponse(any(byte[].class), anyString()))
.when(
() ->
WebResponseUtils.pdfFileToWebResponse(
any(TempFile.class), anyString()))
.thenReturn(expectedResponse);
ResponseEntity<byte[]> response = controller.overlayImage(request);
ResponseEntity<StreamingResponseBody> response = controller.overlayImage(request);
assertNotNull(response);
assertEquals(HttpStatus.OK, response.getStatusCode());
@@ -180,13 +221,17 @@ class OverlayImageControllerTest {
try (MockedStatic<WebResponseUtils> mockedWebResponse =
mockStatic(WebResponseUtils.class)) {
ResponseEntity<byte[]> expectedResponse = ResponseEntity.ok("result".getBytes());
ResponseEntity<StreamingResponseBody> expectedResponse =
streamingOk("result".getBytes());
mockedWebResponse
.when(() -> WebResponseUtils.bytesToWebResponse(any(byte[].class), anyString()))
.when(
() ->
WebResponseUtils.pdfFileToWebResponse(
any(TempFile.class), anyString()))
.thenReturn(expectedResponse);
// Should not throw - coordinates are passed to contentStream.drawImage
ResponseEntity<byte[]> response = controller.overlayImage(request);
ResponseEntity<StreamingResponseBody> response = controller.overlayImage(request);
assertNotNull(response);
assertEquals(HttpStatus.OK, response.getStatusCode());
@@ -2,10 +2,13 @@ package stirling.software.SPDF.controller.api.misc;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.*;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@@ -19,17 +22,31 @@ import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.mock.web.MockMultipartFile;
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;
import stirling.software.SPDF.model.api.misc.ReplaceAndInvertColorRequest;
import stirling.software.SPDF.service.misc.ReplaceAndInvertColorService;
import stirling.software.common.model.api.misc.HighContrastColorCombination;
import stirling.software.common.model.api.misc.ReplaceAndInvert;
import stirling.software.common.util.TempFile;
import stirling.software.common.util.TempFileManager;
import stirling.software.common.util.WebResponseUtils;
@ExtendWith(MockitoExtension.class)
class ReplaceAndInvertColorControllerTest {
private static ResponseEntity<StreamingResponseBody> streamingOk(byte[] bytes) {
return ResponseEntity.ok(out -> out.write(bytes));
}
private static byte[] drainBody(ResponseEntity<StreamingResponseBody> response)
throws java.io.IOException {
java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream();
response.getBody().writeTo(baos);
return baos.toByteArray();
}
@Mock private ReplaceAndInvertColorService replaceAndInvertColorService;
@Mock private TempFileManager tempFileManager;
@InjectMocks private ReplaceAndInvertColorController controller;
@@ -37,7 +54,19 @@ class ReplaceAndInvertColorControllerTest {
private ReplaceAndInvertColorRequest request;
@BeforeEach
void setUp() {
void setUp() throws Exception {
lenient()
.when(tempFileManager.createManagedTempFile(anyString()))
.thenAnswer(
inv -> {
File f =
Files.createTempFile("test", inv.<String>getArgument(0))
.toFile();
TempFile tf = mock(TempFile.class);
lenient().when(tf.getFile()).thenReturn(f);
lenient().when(tf.getPath()).thenReturn(f.toPath());
return tf;
});
pdfFile =
new MockMultipartFile(
"fileInput",
@@ -67,17 +96,16 @@ class ReplaceAndInvertColorControllerTest {
try (MockedStatic<WebResponseUtils> mockedWebResponse =
mockStatic(WebResponseUtils.class)) {
ResponseEntity<byte[]> expectedResponse = ResponseEntity.ok(resultBytes);
ResponseEntity<StreamingResponseBody> expectedResponse = streamingOk(resultBytes);
mockedWebResponse
.when(
() ->
WebResponseUtils.bytesToWebResponse(
any(byte[].class),
anyString(),
eq(MediaType.APPLICATION_PDF)))
WebResponseUtils.pdfFileToWebResponse(
any(TempFile.class), anyString()))
.thenReturn(expectedResponse);
ResponseEntity<byte[]> response = controller.replaceAndInvertColor(request);
ResponseEntity<StreamingResponseBody> response =
controller.replaceAndInvertColor(request);
assertNotNull(response);
assertEquals(HttpStatus.OK, response.getStatusCode());
@@ -104,17 +132,16 @@ class ReplaceAndInvertColorControllerTest {
try (MockedStatic<WebResponseUtils> mockedWebResponse =
mockStatic(WebResponseUtils.class)) {
ResponseEntity<byte[]> expectedResponse = ResponseEntity.ok(resultBytes);
ResponseEntity<StreamingResponseBody> expectedResponse = streamingOk(resultBytes);
mockedWebResponse
.when(
() ->
WebResponseUtils.bytesToWebResponse(
any(byte[].class),
anyString(),
eq(MediaType.APPLICATION_PDF)))
WebResponseUtils.pdfFileToWebResponse(
any(TempFile.class), anyString()))
.thenReturn(expectedResponse);
ResponseEntity<byte[]> response = controller.replaceAndInvertColor(request);
ResponseEntity<StreamingResponseBody> response =
controller.replaceAndInvertColor(request);
assertNotNull(response);
assertEquals(HttpStatus.OK, response.getStatusCode());
@@ -139,17 +166,16 @@ class ReplaceAndInvertColorControllerTest {
try (MockedStatic<WebResponseUtils> mockedWebResponse =
mockStatic(WebResponseUtils.class)) {
ResponseEntity<byte[]> expectedResponse = ResponseEntity.ok(resultBytes);
ResponseEntity<StreamingResponseBody> expectedResponse = streamingOk(resultBytes);
mockedWebResponse
.when(
() ->
WebResponseUtils.bytesToWebResponse(
any(byte[].class),
anyString(),
eq(MediaType.APPLICATION_PDF)))
WebResponseUtils.pdfFileToWebResponse(
any(TempFile.class), anyString()))
.thenReturn(expectedResponse);
ResponseEntity<byte[]> response = controller.replaceAndInvertColor(request);
ResponseEntity<StreamingResponseBody> response =
controller.replaceAndInvertColor(request);
assertNotNull(response);
assertEquals(HttpStatus.OK, response.getStatusCode());
@@ -179,24 +205,18 @@ class ReplaceAndInvertColorControllerTest {
try (MockedStatic<WebResponseUtils> mockedWebResponse =
mockStatic(WebResponseUtils.class)) {
ResponseEntity<byte[]> expectedResponse = ResponseEntity.ok(resultBytes);
ResponseEntity<StreamingResponseBody> expectedResponse = streamingOk(resultBytes);
mockedWebResponse
.when(
() ->
WebResponseUtils.bytesToWebResponse(
any(byte[].class),
contains("_inverted.pdf"),
eq(MediaType.APPLICATION_PDF)))
WebResponseUtils.pdfFileToWebResponse(
any(TempFile.class), anyString()))
.thenReturn(expectedResponse);
controller.replaceAndInvertColor(request);
mockedWebResponse.verify(
() ->
WebResponseUtils.bytesToWebResponse(
any(byte[].class),
contains("_inverted.pdf"),
eq(MediaType.APPLICATION_PDF)));
() -> WebResponseUtils.pdfFileToWebResponse(any(TempFile.class), anyString()));
}
}
}
@@ -1,9 +1,11 @@
package stirling.software.SPDF.controller.api.misc;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.*;
import java.nio.charset.StandardCharsets;
import java.io.File;
import java.nio.file.Files;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDDocumentCatalog;
@@ -21,15 +23,29 @@ import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.mock.web.MockMultipartFile;
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;
import stirling.software.common.model.api.PDFFile;
import stirling.software.common.service.CustomPDFDocumentFactory;
import stirling.software.common.util.TempFile;
import stirling.software.common.util.TempFileManager;
import stirling.software.common.util.WebResponseUtils;
@ExtendWith(MockitoExtension.class)
class ShowJavascriptTest {
private static ResponseEntity<StreamingResponseBody> streamingOk(byte[] bytes) {
return ResponseEntity.ok(out -> out.write(bytes));
}
private static byte[] drainBody(ResponseEntity<StreamingResponseBody> response)
throws java.io.IOException {
java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream();
response.getBody().writeTo(baos);
return baos.toByteArray();
}
@Mock private CustomPDFDocumentFactory pdfDocumentFactory;
@Mock private TempFileManager tempFileManager;
@InjectMocks private ShowJavascript showJavascript;
@@ -37,7 +53,19 @@ class ShowJavascriptTest {
private PDFFile request;
@BeforeEach
void setUp() {
void setUp() throws Exception {
lenient()
.when(tempFileManager.createManagedTempFile(anyString()))
.thenAnswer(
inv -> {
File f =
Files.createTempFile("test", inv.<String>getArgument(0))
.toFile();
TempFile tf = mock(TempFile.class);
lenient().when(tf.getFile()).thenReturn(f);
lenient().when(tf.getPath()).thenReturn(f.toPath());
return tf;
});
pdfFile =
new MockMultipartFile(
"fileInput",
@@ -58,31 +86,25 @@ class ShowJavascriptTest {
try (MockedStatic<WebResponseUtils> mockedWebResponse =
mockStatic(WebResponseUtils.class)) {
ResponseEntity<byte[]> expectedResponse = ResponseEntity.ok("no js".getBytes());
ResponseEntity<StreamingResponseBody> expectedResponse =
streamingOk("no js".getBytes());
mockedWebResponse
.when(
() ->
WebResponseUtils.bytesToWebResponse(
any(byte[].class),
WebResponseUtils.fileToWebResponse(
any(TempFile.class),
eq("test.pdf.js"),
eq(MediaType.TEXT_PLAIN)))
.thenReturn(expectedResponse);
ResponseEntity<byte[]> response = showJavascript.extractHeader(request);
ResponseEntity<StreamingResponseBody> response = showJavascript.extractHeader(request);
assertNotNull(response);
assertEquals(HttpStatus.OK, response.getStatusCode());
// Verify the bytes passed contain the "does not contain" message
mockedWebResponse.verify(
() ->
WebResponseUtils.bytesToWebResponse(
argThat(
bytes -> {
String content =
new String(bytes, StandardCharsets.UTF_8);
return content.contains(
"does not contain Javascript");
}),
WebResponseUtils.fileToWebResponse(
any(TempFile.class),
eq("test.pdf.js"),
eq(MediaType.TEXT_PLAIN)));
}
@@ -107,30 +129,24 @@ class ShowJavascriptTest {
try (MockedStatic<WebResponseUtils> mockedWebResponse =
mockStatic(WebResponseUtils.class)) {
ResponseEntity<byte[]> expectedResponse = ResponseEntity.ok("js content".getBytes());
ResponseEntity<StreamingResponseBody> expectedResponse =
streamingOk("js content".getBytes());
mockedWebResponse
.when(
() ->
WebResponseUtils.bytesToWebResponse(
any(byte[].class),
WebResponseUtils.fileToWebResponse(
any(TempFile.class),
eq("test.pdf.js"),
eq(MediaType.TEXT_PLAIN)))
.thenReturn(expectedResponse);
ResponseEntity<byte[]> response = showJavascript.extractHeader(request);
ResponseEntity<StreamingResponseBody> response = showJavascript.extractHeader(request);
assertNotNull(response);
// Verify the bytes passed contain the script content
mockedWebResponse.verify(
() ->
WebResponseUtils.bytesToWebResponse(
argThat(
bytes -> {
String content =
new String(bytes, StandardCharsets.UTF_8);
return content.contains("alert('hello');")
&& content.contains("Script1");
}),
WebResponseUtils.fileToWebResponse(
any(TempFile.class),
eq("test.pdf.js"),
eq(MediaType.TEXT_PLAIN)));
}
@@ -144,31 +160,24 @@ class ShowJavascriptTest {
try (MockedStatic<WebResponseUtils> mockedWebResponse =
mockStatic(WebResponseUtils.class)) {
ResponseEntity<byte[]> expectedResponse = ResponseEntity.ok("no js".getBytes());
ResponseEntity<StreamingResponseBody> expectedResponse =
streamingOk("no js".getBytes());
mockedWebResponse
.when(
() ->
WebResponseUtils.bytesToWebResponse(
any(byte[].class),
WebResponseUtils.fileToWebResponse(
any(TempFile.class),
anyString(),
eq(MediaType.TEXT_PLAIN)))
.thenReturn(expectedResponse);
ResponseEntity<byte[]> response = showJavascript.extractHeader(request);
ResponseEntity<StreamingResponseBody> response = showJavascript.extractHeader(request);
assertNotNull(response);
mockedWebResponse.verify(
() ->
WebResponseUtils.bytesToWebResponse(
argThat(
bytes -> {
String content =
new String(bytes, StandardCharsets.UTF_8);
return content.contains(
"does not contain Javascript");
}),
anyString(),
eq(MediaType.TEXT_PLAIN)));
WebResponseUtils.fileToWebResponse(
any(TempFile.class), anyString(), eq(MediaType.TEXT_PLAIN)));
}
}
@@ -191,31 +200,24 @@ class ShowJavascriptTest {
try (MockedStatic<WebResponseUtils> mockedWebResponse =
mockStatic(WebResponseUtils.class)) {
ResponseEntity<byte[]> expectedResponse = ResponseEntity.ok("no js".getBytes());
ResponseEntity<StreamingResponseBody> expectedResponse =
streamingOk("no js".getBytes());
mockedWebResponse
.when(
() ->
WebResponseUtils.bytesToWebResponse(
any(byte[].class),
WebResponseUtils.fileToWebResponse(
any(TempFile.class),
anyString(),
eq(MediaType.TEXT_PLAIN)))
.thenReturn(expectedResponse);
ResponseEntity<byte[]> response = showJavascript.extractHeader(request);
ResponseEntity<StreamingResponseBody> response = showJavascript.extractHeader(request);
assertNotNull(response);
mockedWebResponse.verify(
() ->
WebResponseUtils.bytesToWebResponse(
argThat(
bytes -> {
String content =
new String(bytes, StandardCharsets.UTF_8);
return content.contains(
"does not contain Javascript");
}),
anyString(),
eq(MediaType.TEXT_PLAIN)));
WebResponseUtils.fileToWebResponse(
any(TempFile.class), anyString(), eq(MediaType.TEXT_PLAIN)));
}
}
@@ -2,8 +2,12 @@ package stirling.software.SPDF.controller.api.misc;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.*;
import java.io.File;
import java.nio.file.Files;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.interactive.form.PDAcroForm;
@@ -14,22 +18,38 @@ import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.http.ResponseEntity;
import org.springframework.mock.web.MockMultipartFile;
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;
import stirling.software.common.model.api.PDFFile;
import stirling.software.common.service.CustomPDFDocumentFactory;
import stirling.software.common.util.TempFile;
import stirling.software.common.util.TempFileManager;
@ExtendWith(MockitoExtension.class)
class UnlockPDFFormsControllerTest {
@Mock private CustomPDFDocumentFactory pdfDocumentFactory;
@Mock private TempFileManager tempFileManager;
private UnlockPDFFormsController controller;
private MockMultipartFile mockPdfFile;
@BeforeEach
void setUp() {
controller = new UnlockPDFFormsController(pdfDocumentFactory);
void setUp() throws Exception {
lenient()
.when(tempFileManager.createManagedTempFile(anyString()))
.thenAnswer(
inv -> {
File f =
Files.createTempFile("test", inv.<String>getArgument(0))
.toFile();
TempFile tf = mock(TempFile.class);
lenient().when(tf.getFile()).thenReturn(f);
lenient().when(tf.getPath()).thenReturn(f.toPath());
return tf;
});
controller = new UnlockPDFFormsController(pdfDocumentFactory, tempFileManager);
mockPdfFile =
new MockMultipartFile(
"fileInput",
@@ -47,7 +67,7 @@ class UnlockPDFFormsControllerTest {
PDFFile file = new PDFFile();
file.setFileInput(mockPdfFile);
ResponseEntity<byte[]> response = controller.unlockPDFForms(file);
ResponseEntity<StreamingResponseBody> response = controller.unlockPDFForms(file);
assertNotNull(response);
assertEquals(200, response.getStatusCode().value());
@@ -65,7 +85,7 @@ class UnlockPDFFormsControllerTest {
PDFFile file = new PDFFile();
file.setFileInput(mockPdfFile);
ResponseEntity<byte[]> response = controller.unlockPDFForms(file);
ResponseEntity<StreamingResponseBody> response = controller.unlockPDFForms(file);
assertNotNull(response);
assertEquals(200, response.getStatusCode().value());
@@ -79,7 +99,7 @@ class UnlockPDFFormsControllerTest {
PDFFile file = new PDFFile();
file.setFileInput(mockPdfFile);
ResponseEntity<byte[]> response = controller.unlockPDFForms(file);
ResponseEntity<StreamingResponseBody> response = controller.unlockPDFForms(file);
// Controller catches exceptions and returns null
assertNull(response);
@@ -94,7 +114,7 @@ class UnlockPDFFormsControllerTest {
PDFFile file = new PDFFile();
file.setFileInput(mockPdfFile);
ResponseEntity<byte[]> response = controller.unlockPDFForms(file);
ResponseEntity<StreamingResponseBody> response = controller.unlockPDFForms(file);
assertNotNull(response);
String contentDisposition = response.getHeaders().getFirst("Content-Disposition");
@@ -113,7 +133,7 @@ class UnlockPDFFormsControllerTest {
PDFFile file = new PDFFile();
file.setFileInput(mockPdfFile);
ResponseEntity<byte[]> response = controller.unlockPDFForms(file);
ResponseEntity<StreamingResponseBody> response = controller.unlockPDFForms(file);
assertNotNull(response);
assertTrue(acroForm.getNeedAppearances());

Some files were not shown because too many files have changed in this diff Show More