mirror of
https://github.com/Frooodle/Stirling-PDF.git
synced 2025-11-01 01:21:18 +01:00
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:
parent
a259378451
commit
614d410dce
@ -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
|
||||
|
||||
@ -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.
|
||||
*
|
||||
|
||||
@ -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");
|
||||
|
||||
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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) {
|
||||
|
||||
@ -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;
|
||||
}
|
||||
@ -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
|
||||
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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">
|
||||
|
||||
@ -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));
|
||||
}
|
||||
}
|
||||
@ -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");
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user