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

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