From 614d410dceff1ca00b4e947acb80627fa7a02341 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bal=C3=A1zs=20Sz=C3=BCcs?= <127139797+balazs-szucs@users.noreply.github.com> Date: Thu, 16 Oct 2025 23:22:36 +0200 Subject: [PATCH] feat(conversion): add PDF to Vector Image conversions (#4651) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # 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 image image Closes: #4491 --- ## 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 --- README.md | 2 + .../software/common/util/ExceptionUtils.java | 5 + .../SPDF/config/EndpointConfiguration.java | 6 + .../converters/PdfVectorExportController.java | 237 ++++++++++++++++++ .../web/ConverterWebController.java | 14 ++ .../converters/PdfVectorExportRequest.java | 29 +++ .../main/resources/messages_en_GB.properties | 17 ++ .../templates/convert/pdf-to-vector.html | 46 ++++ .../templates/convert/vector-to-pdf.html | 41 +++ .../templates/fragments/navElements.html | 12 + .../PdfVectorExportControllerTest.java | 146 +++++++++++ .../PdfVectorExportRequestTest.java | 47 ++++ 12 files changed, 602 insertions(+) create mode 100644 app/core/src/main/java/stirling/software/SPDF/controller/api/converters/PdfVectorExportController.java create mode 100644 app/core/src/main/java/stirling/software/SPDF/model/api/converters/PdfVectorExportRequest.java create mode 100644 app/core/src/main/resources/templates/convert/pdf-to-vector.html create mode 100644 app/core/src/main/resources/templates/convert/vector-to-pdf.html create mode 100644 app/core/src/test/java/stirling/software/SPDF/controller/api/converters/PdfVectorExportControllerTest.java create mode 100644 app/core/src/test/java/stirling/software/SPDF/model/api/converters/PdfVectorExportRequestTest.java diff --git a/README.md b/README.md index f29782d1a..28fce8df2 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/app/common/src/main/java/stirling/software/common/util/ExceptionUtils.java b/app/common/src/main/java/stirling/software/common/util/ExceptionUtils.java index 9f795bb46..b5e200aff 100644 --- a/app/common/src/main/java/stirling/software/common/util/ExceptionUtils.java +++ b/app/common/src/main/java/stirling/software/common/util/ExceptionUtils.java @@ -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. * diff --git a/app/core/src/main/java/stirling/software/SPDF/config/EndpointConfiguration.java b/app/core/src/main/java/stirling/software/SPDF/config/EndpointConfiguration.java index 02eb82163..f07f05537 100644 --- a/app/core/src/main/java/stirling/software/SPDF/config/EndpointConfiguration.java +++ b/app/core/src/main/java/stirling/software/SPDF/config/EndpointConfiguration.java @@ -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"); diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/PdfVectorExportController.java b/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/PdfVectorExportController.java new file mode 100644 index 000000000..754b3c634 --- /dev/null +++ b/app/core/src/main/java/stirling/software/SPDF/controller/api/converters/PdfVectorExportController.java @@ -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 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 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 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 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 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"); + } + } +} diff --git a/app/core/src/main/java/stirling/software/SPDF/controller/web/ConverterWebController.java b/app/core/src/main/java/stirling/software/SPDF/controller/web/ConverterWebController.java index 970e0719a..076cc9f09 100644 --- a/app/core/src/main/java/stirling/software/SPDF/controller/web/ConverterWebController.java +++ b/app/core/src/main/java/stirling/software/SPDF/controller/web/ConverterWebController.java @@ -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) { diff --git a/app/core/src/main/java/stirling/software/SPDF/model/api/converters/PdfVectorExportRequest.java b/app/core/src/main/java/stirling/software/SPDF/model/api/converters/PdfVectorExportRequest.java new file mode 100644 index 000000000..398d62673 --- /dev/null +++ b/app/core/src/main/java/stirling/software/SPDF/model/api/converters/PdfVectorExportRequest.java @@ -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; +} diff --git a/app/core/src/main/resources/messages_en_GB.properties b/app/core/src/main/resources/messages_en_GB.properties index 22007db4d..c60f48eb1 100644 --- a/app/core/src/main/resources/messages_en_GB.properties +++ b/app/core/src/main/resources/messages_en_GB.properties @@ -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 diff --git a/app/core/src/main/resources/templates/convert/pdf-to-vector.html b/app/core/src/main/resources/templates/convert/pdf-to-vector.html new file mode 100644 index 000000000..b4efe95e6 --- /dev/null +++ b/app/core/src/main/resources/templates/convert/pdf-to-vector.html @@ -0,0 +1,46 @@ + + + + + + + + + +
+
+ +

+
+
+
+
+ stroke_full + +
+
+
+
+
+ + +
+ +
+

+
+
+
+
+ +
+ + + \ No newline at end of file diff --git a/app/core/src/main/resources/templates/convert/vector-to-pdf.html b/app/core/src/main/resources/templates/convert/vector-to-pdf.html new file mode 100644 index 000000000..d7c112fa9 --- /dev/null +++ b/app/core/src/main/resources/templates/convert/vector-to-pdf.html @@ -0,0 +1,41 @@ + + + + + + + + + +
+
+ +

+
+
+
+
+ picture_as_pdf + +
+
+
+
+
+ + +
+ +
+

+
+
+
+
+ +
+ + + \ No newline at end of file diff --git a/app/core/src/main/resources/templates/fragments/navElements.html b/app/core/src/main/resources/templates/fragments/navElements.html index 1441a1969..5c7eb9590 100644 --- a/app/core/src/main/resources/templates/fragments/navElements.html +++ b/app/core/src/main/resources/templates/fragments/navElements.html @@ -68,6 +68,9 @@
+
+
@@ -107,6 +110,9 @@
+
+
@@ -135,6 +141,9 @@
+
+
@@ -163,6 +172,9 @@
+
+
diff --git a/app/core/src/test/java/stirling/software/SPDF/controller/api/converters/PdfVectorExportControllerTest.java b/app/core/src/test/java/stirling/software/SPDF/controller/api/converters/PdfVectorExportControllerTest.java new file mode 100644 index 000000000..2c0e4c7ed --- /dev/null +++ b/app/core/src/test/java/stirling/software/SPDF/controller/api/converters/PdfVectorExportControllerTest.java @@ -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 tempPaths = new ArrayList<>(); + @Mock private TempFileManager tempFileManager; + @Mock private EndpointConfiguration endpointConfiguration; + @Mock private ProcessExecutor ghostscriptExecutor; + @InjectMocks private PdfVectorExportController controller; + private Map originalExecutors; + + @BeforeEach + void setup() throws Exception { + when(tempFileManager.createTempFile(any())) + .thenAnswer( + invocation -> { + String suffix = invocation.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 instances = + (Map) 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 instances = + (Map) 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 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 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)); + } +} diff --git a/app/core/src/test/java/stirling/software/SPDF/model/api/converters/PdfVectorExportRequestTest.java b/app/core/src/test/java/stirling/software/SPDF/model/api/converters/PdfVectorExportRequestTest.java new file mode 100644 index 000000000..e7d609b68 --- /dev/null +++ b/app/core/src/test/java/stirling/software/SPDF/model/api/converters/PdfVectorExportRequestTest.java @@ -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> violations = validator.validate(request); + + assertThat(violations).isEmpty(); + } + + @Test + void whenOutputFormatInvalid_thenConstraintViolation() { + PdfVectorExportRequest request = new PdfVectorExportRequest(); + request.setOutputFormat("svg"); + + Set> violations = validator.validate(request); + + assertThat(violations).hasSize(1); + assertThat(violations.iterator().next().getPropertyPath().toString()) + .isEqualTo("outputFormat"); + } +}