feat(conversion): add PDF to Vector Image conversions (#4651)

# Description of Changes


This pull request adds support for converting between PDF and vector
formats (EPS, PS, PCL, XPS) using Ghostscript, including both backend
API endpoints and frontend UI integration. It introduces new
controllers, request models, configuration, and user interface elements
for these conversion features.

### Backend
* Added `PdfVectorExportController` with endpoints for converting PDF to
vector formats and vector formats to PDF, using Ghostscript for
processing.
(`app/core/src/main/java/stirling/software/SPDF/controller/api/converters/PdfVectorExportController.java`)
* Introduced `PdfVectorExportRequest` model to support new conversion
options and parameters.
(`app/core/src/main/java/stirling/software/SPDF/model/api/converters/PdfVectorExportRequest.java`)
* Added a utility method for Ghostscript conversion exceptions.
(`app/common/src/main/java/stirling/software/common/util/ExceptionUtils.java`)

### Configuration
* Registered new endpoints and alternatives for PDF/vector conversion in
the `EndpointConfiguration`.
(`app/core/src/main/java/stirling/software/SPDF/config/EndpointConfiguration.java`)

### Frontend
* Added Thymeleaf templates for "PDF to Vector" and "Vector to PDF"
conversion forms.
(`app/core/src/main/resources/templates/convert/pdf-to-vector.html`,
`app/core/src/main/resources/templates/convert/vector-to-pdf.html`)
* Integrated new conversion tools into the navigation bar and feature
groups.
(`app/core/src/main/resources/templates/fragments/navElements.html`)
* Added controller routes for the new conversion forms.
(`app/core/src/main/java/stirling/software/SPDF/controller/web/ConverterWebController.java`)

### UI
<img width="629" height="551" alt="image"
src="https://github.com/user-attachments/assets/37491db7-1ae8-47d4-b69b-412bf7b02acf"
/>
<img width="629" height="551" alt="image"
src="https://github.com/user-attachments/assets/b33d3d40-5f26-415f-bd60-467f23701003"
/>




Closes: #4491

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

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

Closes #(issue_number)
-->

---

## Checklist

### General

- [x] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [x] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [x] I have performed a self-review of my own code
- [x] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### UI Changes (if applicable)

- [x] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [x] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.

---------

Signed-off-by: Balázs Szücs <bszucs1209@gmail.com>
This commit is contained in:
Balázs Szücs 2025-10-16 23:22:36 +02:00 committed by GitHub
parent a259378451
commit 614d410dce
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 602 additions and 0 deletions

View File

@ -52,6 +52,7 @@ All documentation available at [https://docs.stirlingpdf.com/](https://docs.stir
- **CBZ to PDF**: Convert comic book archives
- **CBR to PDF**: Convert comic book rar archives
- **Email to PDF**: Convert email files to PDF
- **Vector Image to PDF**: Convert vector images (PS, EPS, EPSF) to PDF format
#### Convert from PDF
- **PDF to Word**: Convert to documet (docx, doc, odt) format
@ -65,6 +66,7 @@ All documentation available at [https://docs.stirlingpdf.com/](https://docs.stir
- **PDF to Markdown**: Convert PDF to Markdown
- **PDF to CBZ**: Convert to comic book archive
- **PDF to CBR**: Convert to comic book rar archive
- **PDF to Vector Image**: Convert PDF to vector image (EPS, PS, PCL, XPS) format
#### Sign & Security
- **Sign**: Add digital signatures

View File

@ -225,6 +225,11 @@ public class ExceptionUtils {
return createIOException("error.commandFailed", "{0} command failed", cause, "QPDF");
}
public static IOException createGhostscriptConversionException(String outputType) {
return createIOException(
"error.commandFailed", "{0} command failed", null, "Ghostscript " + outputType);
}
/**
* Check if an exception indicates a corrupted PDF and wrap it with appropriate message.
*

View File

@ -260,6 +260,8 @@ public class EndpointConfiguration {
addEndpointToGroup("Convert", "pdf-to-csv");
addEndpointToGroup("Convert", "pdf-to-markdown");
addEndpointToGroup("Convert", "eml-to-pdf");
addEndpointToGroup("Convert", "pdf-to-vector");
addEndpointToGroup("Convert", "vector-to-pdf");
// Adding endpoints to "Security" group
addEndpointToGroup("Security", "add-password");
@ -373,6 +375,8 @@ public class EndpointConfiguration {
addEndpointToGroup("Java", "extract-page");
addEndpointToGroup("Java", "pdf-to-single-page");
addEndpointToGroup("Java", "markdown-to-pdf");
addEndpointToGroup("Java", "vector-to-pdf");
addEndpointToGroup("Java", "pdf-to-vector");
addEndpointToGroup("Java", "show-javascript");
addEndpointToGroup("Java", "auto-redact");
addEndpointToGroup("Java", "redact");
@ -403,6 +407,8 @@ public class EndpointConfiguration {
addEndpointToGroup("Ghostscript", "compress-pdf");
addEndpointToGroup("Ghostscript", "crop");
addEndpointToGroup("Ghostscript", "replace-invert-pdf");
addEndpointToGroup("Ghostscript", "pdf-to-vector");
addEndpointToGroup("Ghostscript", "vector-to-pdf");
/* tesseract */
addEndpointToGroup("tesseract", "ocr-pdf");

View File

@ -0,0 +1,237 @@
package stirling.software.SPDF.controller.api.converters;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import org.apache.commons.io.FilenameUtils;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.config.EndpointConfiguration;
import stirling.software.SPDF.model.api.converters.PdfVectorExportRequest;
import stirling.software.common.util.ExceptionUtils;
import stirling.software.common.util.GeneralUtils;
import stirling.software.common.util.ProcessExecutor;
import stirling.software.common.util.ProcessExecutor.ProcessExecutorResult;
import stirling.software.common.util.TempFile;
import stirling.software.common.util.TempFileManager;
import stirling.software.common.util.WebResponseUtils;
@RestController
@RequestMapping("/api/v1/convert")
@Slf4j
@Tag(name = "Convert", description = "Convert APIs")
@RequiredArgsConstructor
public class PdfVectorExportController {
private static final MediaType PDF_MEDIA_TYPE = MediaType.APPLICATION_PDF;
private static final Set<String> GHOSTSCRIPT_INPUTS =
Set.of("ps", "eps", "epsf"); // PCL/PXL/XPS require GhostPDL (gpcl6/gxps)
private final TempFileManager tempFileManager;
private final EndpointConfiguration endpointConfiguration;
@PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/vector/pdf")
@Operation(
summary = "Convert PostScript formats to PDF",
description =
"Converts PostScript vector inputs (PS, EPS, EPSF) to PDF using Ghostscript."
+ " Input:PS/EPS Output:PDF Type:SISO")
public ResponseEntity<byte[]> convertGhostscriptInputsToPdf(
@Valid @ModelAttribute PdfVectorExportRequest request) throws Exception {
String originalName =
request.getFileInput() != null
? request.getFileInput().getOriginalFilename()
: null;
String extension =
originalName != null
? FilenameUtils.getExtension(originalName).toLowerCase(Locale.ROOT)
: "";
try (TempFile inputTemp =
new TempFile(tempFileManager, extension.isEmpty() ? "" : "." + extension);
TempFile outputTemp = new TempFile(tempFileManager, ".pdf")) {
request.getFileInput().transferTo(inputTemp.getFile());
if (GHOSTSCRIPT_INPUTS.contains(extension)) {
boolean prepress = request.getPrepress() != null && request.getPrepress();
runGhostscriptToPdf(inputTemp.getPath(), outputTemp.getPath(), prepress);
} else if ("pdf".equals(extension)) {
Files.copy(
inputTemp.getPath(),
outputTemp.getPath(),
StandardCopyOption.REPLACE_EXISTING);
} else {
throw ExceptionUtils.createIllegalArgumentException(
"error.invalidFormat",
"Unsupported Ghostscript input format {0}",
extension);
}
byte[] pdfBytes = Files.readAllBytes(outputTemp.getPath());
String outputName = GeneralUtils.generateFilename(originalName, "_converted.pdf");
return WebResponseUtils.bytesToWebResponse(pdfBytes, outputName, PDF_MEDIA_TYPE);
}
}
@PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/pdf/vector")
@Operation(
summary = "Convert PDF to vector format",
description =
"Converts PDF to Ghostscript vector formats (EPS, PS, PCL, or XPS)."
+ " Input:PDF Output:VECTOR Type:SISO")
public ResponseEntity<byte[]> convertPdfToVector(
@Valid @ModelAttribute PdfVectorExportRequest request) throws Exception {
String originalName =
request.getFileInput() != null
? request.getFileInput().getOriginalFilename()
: null;
String outputFormat = request.getOutputFormat();
if (outputFormat == null || outputFormat.isEmpty()) {
outputFormat = "eps";
}
outputFormat = outputFormat.toLowerCase(Locale.ROOT);
try (TempFile inputTemp = new TempFile(tempFileManager, ".pdf");
TempFile outputTemp = new TempFile(tempFileManager, "." + outputFormat)) {
request.getFileInput().transferTo(inputTemp.getFile());
runGhostscriptPdfToVector(inputTemp.getPath(), outputTemp.getPath(), outputFormat);
byte[] vectorBytes = Files.readAllBytes(outputTemp.getPath());
String outputName =
GeneralUtils.generateFilename(originalName, "_converted." + outputFormat);
MediaType mediaType;
switch (outputFormat.toLowerCase(Locale.ROOT)) {
case "eps":
case "ps":
mediaType = MediaType.parseMediaType("application/postscript");
break;
case "pcl":
mediaType = MediaType.parseMediaType("application/vnd.hp-PCL");
break;
case "xps":
mediaType = MediaType.parseMediaType("application/vnd.ms-xpsdocument");
break;
default:
mediaType = MediaType.APPLICATION_OCTET_STREAM;
}
return WebResponseUtils.bytesToWebResponse(vectorBytes, outputName, mediaType);
}
}
private void runGhostscriptPdfToVector(Path inputPath, Path outputPath, String outputFormat)
throws IOException, InterruptedException {
if (!endpointConfiguration.isGroupEnabled("Ghostscript")) {
throw ExceptionUtils.createGhostscriptConversionException(outputFormat);
}
List<String> command = new ArrayList<>();
command.add("gs");
// Set device based on output format
String device;
switch (outputFormat.toLowerCase(Locale.ROOT)) {
case "eps":
device = "eps2write";
break;
case "ps":
device = "ps2write";
break;
case "pcl":
device = "pxlcolor"; // PCL XL color
break;
case "xps":
device = "xpswrite";
break;
default:
throw ExceptionUtils.createIllegalArgumentException(
"error.invalidFormat", "Unsupported output format: {0}", outputFormat);
}
command.add("-sDEVICE=" + device);
command.add("-dNOPAUSE");
command.add("-dBATCH");
command.add("-dSAFER");
command.add("-sOutputFile=" + outputPath.toAbsolutePath());
command.add(inputPath.toAbsolutePath().toString());
log.debug("Executing Ghostscript command: {}", String.join(" ", command));
ProcessExecutorResult result =
ProcessExecutor.getInstance(ProcessExecutor.Processes.GHOSTSCRIPT)
.runCommandWithOutputHandling(command);
if (result.getRc() != 0) {
log.error(
"Ghostscript PDF to {} conversion failed with rc={} and messages={}. Command: {}",
outputFormat.toUpperCase(),
result.getRc(),
result.getMessages(),
String.join(" ", command));
throw ExceptionUtils.createGhostscriptConversionException(outputFormat);
}
}
private void runGhostscriptToPdf(Path inputPath, Path outputPath, boolean prepress)
throws IOException, InterruptedException {
if (!endpointConfiguration.isGroupEnabled("Ghostscript")) {
throw ExceptionUtils.createGhostscriptConversionException("pdfwrite");
}
List<String> command = new ArrayList<>();
command.add("gs");
command.add("-sDEVICE=pdfwrite");
command.add("-dNOPAUSE");
command.add("-dBATCH");
command.add("-dSAFER");
command.add("-dCompatibilityLevel=1.4");
if (prepress) {
command.add("-dPDFSETTINGS=/prepress");
}
command.add("-sOutputFile=" + outputPath.toAbsolutePath());
command.add(inputPath.toAbsolutePath().toString());
log.debug("Executing Ghostscript PostScript-to-PDF command: {}", String.join(" ", command));
ProcessExecutorResult result =
ProcessExecutor.getInstance(ProcessExecutor.Processes.GHOSTSCRIPT)
.runCommandWithOutputHandling(command);
if (result.getRc() != 0) {
log.error(
"Ghostscript PostScript-to-PDF conversion failed with rc={} and messages={}. Command: {}",
result.getRc(),
result.getMessages(),
String.join(" ", command));
throw ExceptionUtils.createGhostscriptConversionException("pdfwrite");
}
}
}

View File

@ -166,6 +166,20 @@ public class ConverterWebController {
return "convert/pdf-to-pdfa";
}
@GetMapping("/pdf-to-vector")
@Hidden
public String pdfToVectorForm(Model model) {
model.addAttribute("currentPage", "pdf-to-vector");
return "convert/pdf-to-vector";
}
@GetMapping("/vector-to-pdf")
@Hidden
public String vectorToPdfForm(Model model) {
model.addAttribute("currentPage", "vector-to-pdf");
return "convert/vector-to-pdf";
}
@GetMapping("/eml-to-pdf")
@Hidden
public String convertEmlToPdfForm(Model model) {

View File

@ -0,0 +1,29 @@
package stirling.software.SPDF.model.api.converters;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.Pattern;
import lombok.Data;
import lombok.EqualsAndHashCode;
import stirling.software.common.model.api.PDFFile;
@Data
@EqualsAndHashCode(callSuper = true)
public class PdfVectorExportRequest extends PDFFile {
@Schema(
description = "Target vector format extension",
allowableValues = {"eps", "ps", "pcl", "xps"},
defaultValue = "eps")
@Pattern(regexp = "(?i)(eps|ps|pcl|xps)")
private String outputFormat = "eps";
@Schema(
description = "Apply Ghostscript prepress settings",
requiredMode = Schema.RequiredMode.REQUIRED,
allowableValues = {"true", "false"},
defaultValue = "false")
private Boolean prepress;
}

View File

@ -1959,3 +1959,20 @@ editTableOfContents.desc.1=This tool allows you to add or edit the table of cont
editTableOfContents.desc.2=You can create a hierarchical structure by adding child bookmarks to parent bookmarks.
editTableOfContents.desc.3=Each bookmark requires a title and target page number.
editTableOfContents.submit=Apply Table of Contents
home.pdfToVector.title=PDF to Vector Image
home.pdfToVector.desc=Convert PDF to vector formats (EPS, PS, PCL, XPS) using Ghostscript
pdfToVector.tags=conversion,vector,eps,ps,postscript,ghostscript,pcl,xps
home.vectorToPdf.title=Vector Image to PDF
home.vectorToPdf.desc=Convert PostScript (PS, EPS, EPSF) files to PDF using Ghostscript
vectorToPdf.tags=conversion,ghostscript,ps,eps,postscript
vectorToPdf.title=Vector Image to PDF
vectorToPdf.header=Vector Image to PDF
vectorToPdf.description=Convert PostScript vector formats (PS, EPS, EPSF) or PDF files to PDF using Ghostscript.
vectorToPdf.prepress=Apply prepress optimizations (/prepress)
vectorToPdf.submit=Convert
pdfToVector.title=PDF to Vector Image
pdfToVector.header=PDF to Vector Image
pdfToVector.description=Convert a PDF into Ghostscript-generated vector formats (EPS, PS, PCL, or XPS).
pdfToVector.outputFormat=Output format
pdfToVector.submit=Convert

View File

@ -0,0 +1,46 @@
<!DOCTYPE html>
<html th:data-language="${#locale.toString()}" th:dir="#{language.direction}" th:lang="${#locale.language}"
xmlns:th="https://www.thymeleaf.org">
<head>
<th:block th:insert="~{fragments/common :: head(title=#{pdfToVector.title}, header=#{pdfToVector.header})}"></th:block>
</head>
<body>
<th:block th:insert="~{fragments/common :: game}"></th:block>
<div id="page-container">
<div id="content-wrap">
<th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
<br><br>
<div class="container">
<div class="row justify-content-center">
<div class="col-md-6 bg-card">
<div class="tool-header">
<span class="material-symbols-rounded tool-header-icon convert">stroke_full</span>
<span class="tool-header-text" th:text="#{pdfToVector.header}"></span>
</div>
<form enctype="multipart/form-data" method="post" th:action="@{'/api/v1/convert/pdf/vector'}">
<div
th:replace="~{fragments/common :: fileSelector(name='fileInput', multipleInputsForSingleRequest=false, accept='application/pdf')}">
</div>
<div class="mb-3">
<label for="outputFormat" th:text="#{pdfToVector.outputFormat}"></label>
<select class="form-control" id="outputFormat" name="outputFormat">
<option selected value="eps">EPS (Encapsulated PostScript)</option>
<option value="ps">PS (PostScript)</option>
<option value="pcl">PCL (Printer Command Language)</option>
<option value="xps">XPS (XML Paper Specification)</option>
</select>
</div>
<button class="btn btn-primary" id="submitBtn" th:text="#{pdfToVector.submit}" type="submit"></button>
</form>
<p class="mt-3" th:text="#{pdfToVector.description}"></p>
</div>
</div>
</div>
</div>
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
</div>
</body>
</html>

View File

@ -0,0 +1,41 @@
<!DOCTYPE html>
<html th:data-language="${#locale.toString()}" th:dir="#{language.direction}" th:lang="${#locale.language}"
xmlns:th="https://www.thymeleaf.org">
<head>
<th:block th:insert="~{fragments/common :: head(title=#{vectorToPdf.title}, header=#{vectorToPdf.header})}"></th:block>
</head>
<body>
<th:block th:insert="~{fragments/common :: game}"></th:block>
<div id="page-container">
<div id="content-wrap">
<th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
<br><br>
<div class="container">
<div class="row justify-content-center">
<div class="col-md-6 bg-card">
<div class="tool-header">
<span class="material-symbols-rounded tool-header-icon convertto">picture_as_pdf</span>
<span class="tool-header-text" th:text="#{vectorToPdf.header}"></span>
</div>
<form enctype="multipart/form-data" method="post" th:action="@{'/api/v1/convert/vector/pdf'}">
<div
th:replace="~{fragments/common :: fileSelector(name='fileInput', multipleInputsForSingleRequest=false, accept='.ps,.eps,.epsf,application/pdf')}">
</div>
<div class="form-check mb-3">
<input id="prepress" name="prepress" type="checkbox" value="true">
<label for="prepress" th:text="#{vectorToPdf.prepress}"></label>
</div>
<button class="btn btn-primary" id="submitBtn" th:text="#{vectorToPdf.submit}" type="submit"></button>
</form>
<p class="mt-3" th:text="#{vectorToPdf.description}"></p>
</div>
</div>
</div>
</div>
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
</div>
</body>
</html>

View File

@ -68,6 +68,9 @@
<div
th:replace="~{fragments/navbarEntry :: navbarEntry('eml-to-pdf', 'email', 'home.EMLToPDF.title', 'home.EMLToPDF.desc', 'EMLToPDF.tags', 'convertto')}">
</div>
<div
th:replace="~{fragments/navbarEntry :: navbarEntry('vector-to-pdf', 'picture_as_pdf', 'home.vectorToPdf.title', 'home.vectorToPdf.desc', 'vectorToPdf.tags', 'convertto')}">
</div>
</div>
</div>
<div id="groupConvertFrom" class="feature-group">
@ -107,6 +110,9 @@
<div
th:replace="~{fragments/navbarEntry :: navbarEntry ('pdf-to-markdown', 'markdown_copy', 'home.PDFToMarkdown.title', 'home.PDFToMarkdown.desc', 'PDFToMarkdown.tags', 'convert')}">
</div>
<div
th:replace="~{fragments/navbarEntry :: navbarEntry('pdf-to-vector', 'stroke_full', 'home.pdfToVector.title', 'home.pdfToVector.desc', 'pdfToVector.tags', 'convert')}">
</div>
</div>
</div>
@ -135,6 +141,9 @@
<div
th:replace="~{fragments/navbarEntry :: navbarEntry('markdown-to-pdf', 'markdown', 'home.MarkdownToPDF.title', 'home.MarkdownToPDF.desc', 'MarkdownToPDF.tags', 'convertto')}">
</div>
<div
th:replace="~{fragments/navbarEntry :: navbarEntry('vector-to-pdf', 'picture_as_pdf', 'home.vectorToPdf.title', 'home.vectorToPdf.desc', 'vectorToPdf.tags', 'convertto')}">
</div>
</div>
<div th:replace="~{fragments/featureGroupHeader :: featureGroupHeader(groupTitle=#{navbar.sections.convertFrom})}">
</div>
@ -163,6 +172,9 @@
<div
th:replace="~{fragments/navbarEntry :: navbarEntry('pdf-to-csv', 'csv', 'home.tableExtraxt.title', 'home.tableExtraxt.desc', 'tableExtraxt.tags', 'convert')}">
</div>
<div
th:replace="~{fragments/navbarEntry :: navbarEntry('pdf-to-vector', 'stroke_full', 'home.pdfToVector.title', 'home.pdfToVector.desc', 'pdfToVector.tags', 'convert')}">
</div>
</div>
</div>
<div id="groupSecurity" class="feature-group">

View File

@ -0,0 +1,146 @@
package stirling.software.SPDF.controller.api.converters;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.lenient;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.when;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.mock.web.MockMultipartFile;
import stirling.software.SPDF.config.EndpointConfiguration;
import stirling.software.SPDF.model.api.converters.PdfVectorExportRequest;
import stirling.software.common.util.ProcessExecutor;
import stirling.software.common.util.ProcessExecutor.ProcessExecutorResult;
import stirling.software.common.util.TempFileManager;
@ExtendWith(MockitoExtension.class)
class PdfVectorExportControllerTest {
private final List<Path> tempPaths = new ArrayList<>();
@Mock private TempFileManager tempFileManager;
@Mock private EndpointConfiguration endpointConfiguration;
@Mock private ProcessExecutor ghostscriptExecutor;
@InjectMocks private PdfVectorExportController controller;
private Map<ProcessExecutor.Processes, ProcessExecutor> originalExecutors;
@BeforeEach
void setup() throws Exception {
when(tempFileManager.createTempFile(any()))
.thenAnswer(
invocation -> {
String suffix = invocation.<String>getArgument(0);
Path path =
Files.createTempFile(
"vector_test", suffix == null ? "" : suffix);
tempPaths.add(path);
return path.toFile();
});
Field instancesField = ProcessExecutor.class.getDeclaredField("instances");
instancesField.setAccessible(true);
@SuppressWarnings("unchecked")
Map<ProcessExecutor.Processes, ProcessExecutor> instances =
(Map<ProcessExecutor.Processes, ProcessExecutor>) instancesField.get(null);
originalExecutors = Map.copyOf(instances);
instances.clear();
instances.put(ProcessExecutor.Processes.GHOSTSCRIPT, ghostscriptExecutor);
}
@AfterEach
void tearDown() throws Exception {
Field instancesField = ProcessExecutor.class.getDeclaredField("instances");
instancesField.setAccessible(true);
@SuppressWarnings("unchecked")
Map<ProcessExecutor.Processes, ProcessExecutor> instances =
(Map<ProcessExecutor.Processes, ProcessExecutor>) instancesField.get(null);
instances.clear();
if (originalExecutors != null) {
instances.putAll(originalExecutors);
}
reset(ghostscriptExecutor, tempFileManager, endpointConfiguration);
for (Path path : tempPaths) {
Files.deleteIfExists(path);
}
tempPaths.clear();
}
private ProcessExecutorResult mockResult(int rc) {
ProcessExecutorResult result = mock(ProcessExecutorResult.class);
lenient().when(result.getRc()).thenReturn(rc);
lenient().when(result.getMessages()).thenReturn("");
return result;
}
@Test
void convertGhostscript_psToPdf_success() throws Exception {
when(endpointConfiguration.isGroupEnabled("Ghostscript")).thenReturn(true);
ProcessExecutorResult result = mockResult(0);
when(ghostscriptExecutor.runCommandWithOutputHandling(any())).thenReturn(result);
MockMultipartFile file =
new MockMultipartFile(
"fileInput",
"sample.ps",
MediaType.APPLICATION_OCTET_STREAM_VALUE,
new byte[] {1});
PdfVectorExportRequest request = new PdfVectorExportRequest();
request.setFileInput(file);
ResponseEntity<byte[]> response = controller.convertGhostscriptInputsToPdf(request);
assertThat(response.getStatusCode()).isEqualTo(org.springframework.http.HttpStatus.OK);
assertThat(response.getHeaders().getContentType()).isEqualTo(MediaType.APPLICATION_PDF);
}
@Test
void convertGhostscript_pdfPassThrough_success() throws Exception {
when(endpointConfiguration.isGroupEnabled("Ghostscript")).thenReturn(false);
byte[] content = new byte[] {1};
MockMultipartFile file =
new MockMultipartFile(
"fileInput", "input.pdf", MediaType.APPLICATION_PDF_VALUE, content);
PdfVectorExportRequest request = new PdfVectorExportRequest();
request.setFileInput(file);
ResponseEntity<byte[]> response = controller.convertGhostscriptInputsToPdf(request);
assertThat(response.getStatusCode()).isEqualTo(org.springframework.http.HttpStatus.OK);
assertThat(response.getHeaders().getContentType()).isEqualTo(MediaType.APPLICATION_PDF);
assertThat(response.getBody()).contains(content);
}
@Test
void convertGhostscript_unsupportedFormatThrows() throws Exception {
when(endpointConfiguration.isGroupEnabled("Ghostscript")).thenReturn(false);
MockMultipartFile file =
new MockMultipartFile(
"fileInput", "vector.svg", MediaType.APPLICATION_XML_VALUE, new byte[] {1});
PdfVectorExportRequest request = new PdfVectorExportRequest();
request.setFileInput(file);
assertThrows(
IllegalArgumentException.class,
() -> controller.convertGhostscriptInputsToPdf(request));
}
}

View File

@ -0,0 +1,47 @@
package stirling.software.SPDF.model.api.converters;
import static org.assertj.core.api.Assertions.assertThat;
import java.util.Set;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import jakarta.validation.ConstraintViolation;
import jakarta.validation.Validation;
import jakarta.validation.Validator;
import jakarta.validation.ValidatorFactory;
public class PdfVectorExportRequestTest {
private static Validator validator;
@BeforeAll
static void setUpValidator() {
try (ValidatorFactory factory = Validation.buildDefaultValidatorFactory()) {
validator = factory.getValidator();
}
}
@Test
void whenOutputFormatValid_thenNoViolations() {
PdfVectorExportRequest request = new PdfVectorExportRequest();
request.setOutputFormat("EPS");
Set<ConstraintViolation<PdfVectorExportRequest>> violations = validator.validate(request);
assertThat(violations).isEmpty();
}
@Test
void whenOutputFormatInvalid_thenConstraintViolation() {
PdfVectorExportRequest request = new PdfVectorExportRequest();
request.setOutputFormat("svg");
Set<ConstraintViolation<PdfVectorExportRequest>> violations = validator.validate(request);
assertThat(violations).hasSize(1);
assertThat(violations.iterator().next().getPropertyPath().toString())
.isEqualTo("outputFormat");
}
}