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"); + } +}