mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2025-09-18 09:29:24 +00:00
booklet and server sign
This commit is contained in:
parent
6a09ec6091
commit
e00baeb033
@ -237,6 +237,7 @@ public class EndpointConfiguration {
|
|||||||
addEndpointToGroup("PageOps", "pdf-organizer");
|
addEndpointToGroup("PageOps", "pdf-organizer");
|
||||||
addEndpointToGroup("PageOps", "rotate-pdf");
|
addEndpointToGroup("PageOps", "rotate-pdf");
|
||||||
addEndpointToGroup("PageOps", "multi-page-layout");
|
addEndpointToGroup("PageOps", "multi-page-layout");
|
||||||
|
addEndpointToGroup("PageOps", "booklet-imposition");
|
||||||
addEndpointToGroup("PageOps", "scale-pages");
|
addEndpointToGroup("PageOps", "scale-pages");
|
||||||
addEndpointToGroup("PageOps", "crop");
|
addEndpointToGroup("PageOps", "crop");
|
||||||
addEndpointToGroup("PageOps", "extract-page");
|
addEndpointToGroup("PageOps", "extract-page");
|
||||||
@ -366,6 +367,7 @@ public class EndpointConfiguration {
|
|||||||
addEndpointToGroup("Java", "cert-sign");
|
addEndpointToGroup("Java", "cert-sign");
|
||||||
addEndpointToGroup("Java", "remove-cert-sign");
|
addEndpointToGroup("Java", "remove-cert-sign");
|
||||||
addEndpointToGroup("Java", "multi-page-layout");
|
addEndpointToGroup("Java", "multi-page-layout");
|
||||||
|
addEndpointToGroup("Java", "booklet-imposition");
|
||||||
addEndpointToGroup("Java", "scale-pages");
|
addEndpointToGroup("Java", "scale-pages");
|
||||||
addEndpointToGroup("Java", "add-page-numbers");
|
addEndpointToGroup("Java", "add-page-numbers");
|
||||||
addEndpointToGroup("Java", "auto-rename");
|
addEndpointToGroup("Java", "auto-rename");
|
||||||
|
@ -0,0 +1,27 @@
|
|||||||
|
package stirling.software.SPDF.configuration;
|
||||||
|
|
||||||
|
import org.springframework.boot.context.event.ApplicationReadyEvent;
|
||||||
|
import org.springframework.context.event.EventListener;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
import stirling.software.SPDF.service.ServerCertificateService;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@Slf4j
|
||||||
|
public class ServerCertificateInitializer {
|
||||||
|
|
||||||
|
private final ServerCertificateService serverCertificateService;
|
||||||
|
|
||||||
|
@EventListener(ApplicationReadyEvent.class)
|
||||||
|
public void initializeServerCertificate() {
|
||||||
|
try {
|
||||||
|
serverCertificateService.initializeServerCertificate();
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Failed to initialize server certificate", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,186 @@
|
|||||||
|
package stirling.software.SPDF.controller.api;
|
||||||
|
|
||||||
|
import java.awt.*;
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.apache.pdfbox.multipdf.LayerUtility;
|
||||||
|
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||||
|
import org.apache.pdfbox.pdmodel.PDPage;
|
||||||
|
import org.apache.pdfbox.pdmodel.PDPageContentStream;
|
||||||
|
import org.apache.pdfbox.pdmodel.common.PDRectangle;
|
||||||
|
import org.apache.pdfbox.pdmodel.graphics.form.PDFormXObject;
|
||||||
|
import org.apache.pdfbox.util.Matrix;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
|
import io.github.pixee.security.Filenames;
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
|
||||||
|
import stirling.software.SPDF.model.api.general.BookletImpositionRequest;
|
||||||
|
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||||
|
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||||
|
import stirling.software.common.util.WebResponseUtils;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/v1/general")
|
||||||
|
@Tag(name = "General", description = "General APIs")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class BookletImpositionController {
|
||||||
|
|
||||||
|
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
||||||
|
|
||||||
|
@AutoJobPostMapping(value = "/booklet-imposition", consumes = "multipart/form-data")
|
||||||
|
@Operation(
|
||||||
|
summary = "Create a booklet with proper page imposition",
|
||||||
|
description =
|
||||||
|
"This operation combines page reordering for booklet printing with multi-page layout. "
|
||||||
|
+ "It rearranges pages in the correct order for booklet printing and places multiple pages "
|
||||||
|
+ "on each sheet for proper folding and binding. Input:PDF Output:PDF Type:SISO")
|
||||||
|
public ResponseEntity<byte[]> createBookletImposition(
|
||||||
|
@ModelAttribute BookletImpositionRequest request) throws IOException {
|
||||||
|
|
||||||
|
MultipartFile file = request.getFileInput();
|
||||||
|
String bookletType = request.getBookletType();
|
||||||
|
int pagesPerSheet = request.getPagesPerSheet();
|
||||||
|
boolean addBorder = Boolean.TRUE.equals(request.getAddBorder());
|
||||||
|
String pageOrientation = request.getPageOrientation();
|
||||||
|
|
||||||
|
// Validate pages per sheet for booklet
|
||||||
|
if (pagesPerSheet != 2 && pagesPerSheet != 4) {
|
||||||
|
throw new IllegalArgumentException("pagesPerSheet must be 2 or 4 for booklet imposition");
|
||||||
|
}
|
||||||
|
|
||||||
|
PDDocument sourceDocument = pdfDocumentFactory.load(file);
|
||||||
|
int totalPages = sourceDocument.getNumberOfPages();
|
||||||
|
|
||||||
|
// Step 1: Reorder pages for booklet (reusing logic from RearrangePagesPDFController)
|
||||||
|
List<Integer> bookletOrder = getBookletPageOrder(bookletType, totalPages);
|
||||||
|
|
||||||
|
// Step 2: Create new document with multi-page layout (reusing logic from MultiPageLayoutController)
|
||||||
|
PDDocument newDocument = createBookletWithLayout(sourceDocument, bookletOrder, pagesPerSheet, addBorder, pageOrientation);
|
||||||
|
|
||||||
|
sourceDocument.close();
|
||||||
|
|
||||||
|
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||||
|
newDocument.save(baos);
|
||||||
|
newDocument.close();
|
||||||
|
|
||||||
|
byte[] result = baos.toByteArray();
|
||||||
|
return WebResponseUtils.bytesToWebResponse(
|
||||||
|
result,
|
||||||
|
Filenames.toSimpleFileName(file.getOriginalFilename()).replaceFirst("[.][^.]+$", "")
|
||||||
|
+ "_booklet.pdf");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reused logic from RearrangePagesPDFController
|
||||||
|
private List<Integer> getBookletPageOrder(String bookletType, int totalPages) {
|
||||||
|
if ("SIDE_STITCH_BOOKLET".equals(bookletType)) {
|
||||||
|
return sideStitchBookletSort(totalPages);
|
||||||
|
} else {
|
||||||
|
return bookletSort(totalPages);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<Integer> bookletSort(int totalPages) {
|
||||||
|
List<Integer> newPageOrder = new ArrayList<>();
|
||||||
|
for (int i = 0; i < totalPages / 2; i++) {
|
||||||
|
newPageOrder.add(i);
|
||||||
|
newPageOrder.add(totalPages - i - 1);
|
||||||
|
}
|
||||||
|
return newPageOrder;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<Integer> sideStitchBookletSort(int totalPages) {
|
||||||
|
List<Integer> newPageOrder = new ArrayList<>();
|
||||||
|
for (int i = 0; i < (totalPages + 3) / 4; i++) {
|
||||||
|
int begin = i * 4;
|
||||||
|
newPageOrder.add(Math.min(begin + 3, totalPages - 1));
|
||||||
|
newPageOrder.add(Math.min(begin, totalPages - 1));
|
||||||
|
newPageOrder.add(Math.min(begin + 1, totalPages - 1));
|
||||||
|
newPageOrder.add(Math.min(begin + 2, totalPages - 1));
|
||||||
|
}
|
||||||
|
return newPageOrder;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reused and adapted logic from MultiPageLayoutController
|
||||||
|
private PDDocument createBookletWithLayout(PDDocument sourceDocument, List<Integer> pageOrder,
|
||||||
|
int pagesPerSheet, boolean addBorder, String pageOrientation) throws IOException {
|
||||||
|
|
||||||
|
PDDocument newDocument = pdfDocumentFactory.createNewDocumentBasedOnOldDocument(sourceDocument);
|
||||||
|
|
||||||
|
int cols = pagesPerSheet == 2 ? 2 : 2; // 2x1 for 2 pages, 2x2 for 4 pages
|
||||||
|
int rows = pagesPerSheet == 2 ? 1 : 2;
|
||||||
|
|
||||||
|
int currentPageIndex = 0;
|
||||||
|
int totalOrderedPages = pageOrder.size();
|
||||||
|
|
||||||
|
while (currentPageIndex < totalOrderedPages) {
|
||||||
|
// Use landscape orientation for booklets (A4 landscape -> A5 portrait when folded)
|
||||||
|
PDRectangle pageSize = "LANDSCAPE".equals(pageOrientation) ?
|
||||||
|
new PDRectangle(PDRectangle.A4.getHeight(), PDRectangle.A4.getWidth()) : PDRectangle.A4;
|
||||||
|
PDPage newPage = new PDPage(pageSize);
|
||||||
|
newDocument.addPage(newPage);
|
||||||
|
|
||||||
|
float cellWidth = newPage.getMediaBox().getWidth() / cols;
|
||||||
|
float cellHeight = newPage.getMediaBox().getHeight() / rows;
|
||||||
|
|
||||||
|
PDPageContentStream contentStream = new PDPageContentStream(
|
||||||
|
newDocument, newPage, PDPageContentStream.AppendMode.APPEND, true, true);
|
||||||
|
LayerUtility layerUtility = new LayerUtility(newDocument);
|
||||||
|
|
||||||
|
if (addBorder) {
|
||||||
|
contentStream.setLineWidth(1.5f);
|
||||||
|
contentStream.setStrokingColor(Color.BLACK);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Place pages on the current sheet
|
||||||
|
for (int sheetPosition = 0; sheetPosition < pagesPerSheet && currentPageIndex < totalOrderedPages; sheetPosition++) {
|
||||||
|
int sourcePageIndex = pageOrder.get(currentPageIndex);
|
||||||
|
PDPage sourcePage = sourceDocument.getPage(sourcePageIndex);
|
||||||
|
PDRectangle rect = sourcePage.getMediaBox();
|
||||||
|
|
||||||
|
float scaleWidth = cellWidth / rect.getWidth();
|
||||||
|
float scaleHeight = cellHeight / rect.getHeight();
|
||||||
|
float scale = Math.min(scaleWidth, scaleHeight);
|
||||||
|
|
||||||
|
int rowIndex = sheetPosition / cols;
|
||||||
|
int colIndex = sheetPosition % cols;
|
||||||
|
|
||||||
|
float x = colIndex * cellWidth + (cellWidth - rect.getWidth() * scale) / 2;
|
||||||
|
float y = newPage.getMediaBox().getHeight()
|
||||||
|
- ((rowIndex + 1) * cellHeight - (cellHeight - rect.getHeight() * scale) / 2);
|
||||||
|
|
||||||
|
contentStream.saveGraphicsState();
|
||||||
|
contentStream.transform(Matrix.getTranslateInstance(x, y));
|
||||||
|
contentStream.transform(Matrix.getScaleInstance(scale, scale));
|
||||||
|
|
||||||
|
PDFormXObject formXObject = layerUtility.importPageAsForm(sourceDocument, sourcePageIndex);
|
||||||
|
contentStream.drawForm(formXObject);
|
||||||
|
|
||||||
|
contentStream.restoreGraphicsState();
|
||||||
|
|
||||||
|
if (addBorder) {
|
||||||
|
float borderX = colIndex * cellWidth;
|
||||||
|
float borderY = newPage.getMediaBox().getHeight() - (rowIndex + 1) * cellHeight;
|
||||||
|
contentStream.addRect(borderX, borderY, cellWidth, cellHeight);
|
||||||
|
contentStream.stroke();
|
||||||
|
}
|
||||||
|
|
||||||
|
currentPageIndex++;
|
||||||
|
}
|
||||||
|
|
||||||
|
contentStream.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
return newDocument;
|
||||||
|
}
|
||||||
|
}
|
@ -72,6 +72,7 @@ import lombok.RequiredArgsConstructor;
|
|||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
import stirling.software.SPDF.model.api.security.SignPDFWithCertRequest;
|
import stirling.software.SPDF.model.api.security.SignPDFWithCertRequest;
|
||||||
|
import stirling.software.SPDF.service.ServerCertificateService;
|
||||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||||
import stirling.software.common.util.ExceptionUtils;
|
import stirling.software.common.util.ExceptionUtils;
|
||||||
@ -101,6 +102,7 @@ public class CertSignController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
||||||
|
private final ServerCertificateService serverCertificateService;
|
||||||
|
|
||||||
private static void sign(
|
private static void sign(
|
||||||
CustomPDFDocumentFactory pdfDocumentFactory,
|
CustomPDFDocumentFactory pdfDocumentFactory,
|
||||||
@ -175,6 +177,7 @@ public class CertSignController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
KeyStore ks = null;
|
KeyStore ks = null;
|
||||||
|
String keystorePassword = password;
|
||||||
|
|
||||||
switch (certType) {
|
switch (certType) {
|
||||||
case "PEM":
|
case "PEM":
|
||||||
@ -193,6 +196,19 @@ public class CertSignController {
|
|||||||
ks = KeyStore.getInstance("JKS");
|
ks = KeyStore.getInstance("JKS");
|
||||||
ks.load(jksfile.getInputStream(), password.toCharArray());
|
ks.load(jksfile.getInputStream(), password.toCharArray());
|
||||||
break;
|
break;
|
||||||
|
case "SERVER":
|
||||||
|
if (!serverCertificateService.isEnabled()) {
|
||||||
|
throw ExceptionUtils.createIllegalArgumentException(
|
||||||
|
"error.serverCertificateDisabled",
|
||||||
|
"Server certificate feature is disabled");
|
||||||
|
}
|
||||||
|
if (!serverCertificateService.hasServerCertificate()) {
|
||||||
|
throw ExceptionUtils.createIllegalArgumentException(
|
||||||
|
"error.serverCertificateNotFound", "No server certificate configured");
|
||||||
|
}
|
||||||
|
ks = serverCertificateService.getServerKeyStore();
|
||||||
|
keystorePassword = serverCertificateService.getServerCertificatePassword();
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
throw ExceptionUtils.createIllegalArgumentException(
|
throw ExceptionUtils.createIllegalArgumentException(
|
||||||
"error.invalidArgument",
|
"error.invalidArgument",
|
||||||
@ -200,7 +216,7 @@ public class CertSignController {
|
|||||||
"certificate type: " + certType);
|
"certificate type: " + certType);
|
||||||
}
|
}
|
||||||
|
|
||||||
CreateSignature createSignature = new CreateSignature(ks, password.toCharArray());
|
CreateSignature createSignature = new CreateSignature(ks, keystorePassword.toCharArray());
|
||||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||||
sign(
|
sign(
|
||||||
pdfDocumentFactory,
|
pdfDocumentFactory,
|
||||||
|
@ -0,0 +1,145 @@
|
|||||||
|
package stirling.software.SPDF.controller.api.security;
|
||||||
|
|
||||||
|
import org.springframework.http.HttpHeaders;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.security.access.prepost.PreAuthorize;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.Parameter;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
import stirling.software.SPDF.service.ServerCertificateService;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/v1/admin/server-certificate")
|
||||||
|
@Slf4j
|
||||||
|
@Tag(
|
||||||
|
name = "Admin - Server Certificate",
|
||||||
|
description = "Admin APIs for server certificate management")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@PreAuthorize("hasRole('ADMIN')")
|
||||||
|
public class ServerCertificateController {
|
||||||
|
|
||||||
|
private final ServerCertificateService serverCertificateService;
|
||||||
|
|
||||||
|
@GetMapping("/info")
|
||||||
|
@Operation(
|
||||||
|
summary = "Get server certificate information",
|
||||||
|
description = "Returns information about the current server certificate")
|
||||||
|
public ResponseEntity<ServerCertificateService.ServerCertificateInfo>
|
||||||
|
getServerCertificateInfo() {
|
||||||
|
try {
|
||||||
|
ServerCertificateService.ServerCertificateInfo info =
|
||||||
|
serverCertificateService.getServerCertificateInfo();
|
||||||
|
return ResponseEntity.ok(info);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Failed to get server certificate info", e);
|
||||||
|
return ResponseEntity.internalServerError().build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/upload")
|
||||||
|
@Operation(
|
||||||
|
summary = "Upload server certificate",
|
||||||
|
description =
|
||||||
|
"Upload a new PKCS12 certificate file to be used as the server certificate")
|
||||||
|
public ResponseEntity<String> uploadServerCertificate(
|
||||||
|
@Parameter(description = "PKCS12 certificate file", required = true)
|
||||||
|
@RequestParam("file")
|
||||||
|
MultipartFile file,
|
||||||
|
@Parameter(description = "Certificate password", required = true)
|
||||||
|
@RequestParam("password")
|
||||||
|
String password) {
|
||||||
|
|
||||||
|
if (file.isEmpty()) {
|
||||||
|
return ResponseEntity.badRequest().body("Certificate file cannot be empty");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!file.getOriginalFilename().toLowerCase().endsWith(".p12")
|
||||||
|
&& !file.getOriginalFilename().toLowerCase().endsWith(".pfx")) {
|
||||||
|
return ResponseEntity.badRequest()
|
||||||
|
.body("Only PKCS12 (.p12 or .pfx) files are supported");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
serverCertificateService.uploadServerCertificate(file.getInputStream(), password);
|
||||||
|
return ResponseEntity.ok("Server certificate uploaded successfully");
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
log.warn("Invalid certificate upload: {}", e.getMessage());
|
||||||
|
return ResponseEntity.badRequest().body(e.getMessage());
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Failed to upload server certificate", e);
|
||||||
|
return ResponseEntity.internalServerError().body("Failed to upload server certificate");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@DeleteMapping
|
||||||
|
@Operation(
|
||||||
|
summary = "Delete server certificate",
|
||||||
|
description = "Delete the current server certificate")
|
||||||
|
public ResponseEntity<String> deleteServerCertificate() {
|
||||||
|
try {
|
||||||
|
serverCertificateService.deleteServerCertificate();
|
||||||
|
return ResponseEntity.ok("Server certificate deleted successfully");
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Failed to delete server certificate", e);
|
||||||
|
return ResponseEntity.internalServerError().body("Failed to delete server certificate");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/generate")
|
||||||
|
@Operation(
|
||||||
|
summary = "Generate new server certificate",
|
||||||
|
description = "Generate a new self-signed server certificate")
|
||||||
|
public ResponseEntity<String> generateServerCertificate() {
|
||||||
|
try {
|
||||||
|
serverCertificateService.deleteServerCertificate(); // Remove existing if any
|
||||||
|
serverCertificateService.initializeServerCertificate(); // Generate new
|
||||||
|
return ResponseEntity.ok("New server certificate generated successfully");
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Failed to generate server certificate", e);
|
||||||
|
return ResponseEntity.internalServerError()
|
||||||
|
.body("Failed to generate server certificate");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/public-key")
|
||||||
|
@Operation(
|
||||||
|
summary = "Download server certificate public key",
|
||||||
|
description =
|
||||||
|
"Download the public key of the server certificate for validation purposes")
|
||||||
|
public ResponseEntity<byte[]> getServerCertificatePublicKey() {
|
||||||
|
try {
|
||||||
|
if (!serverCertificateService.hasServerCertificate()) {
|
||||||
|
return ResponseEntity.notFound().build();
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] publicKey = serverCertificateService.getServerCertificatePublicKey();
|
||||||
|
|
||||||
|
return ResponseEntity.ok()
|
||||||
|
.header(
|
||||||
|
HttpHeaders.CONTENT_DISPOSITION,
|
||||||
|
"attachment; filename=\"server-cert.crt\"")
|
||||||
|
.contentType(MediaType.APPLICATION_OCTET_STREAM)
|
||||||
|
.body(publicKey);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Failed to get server certificate public key", e);
|
||||||
|
return ResponseEntity.internalServerError().build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/enabled")
|
||||||
|
@Operation(
|
||||||
|
summary = "Check if server certificate feature is enabled",
|
||||||
|
description =
|
||||||
|
"Returns whether the server certificate feature is enabled in configuration")
|
||||||
|
public ResponseEntity<Boolean> isServerCertificateEnabled() {
|
||||||
|
return ResponseEntity.ok(serverCertificateService.isEnabled());
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,40 @@
|
|||||||
|
package stirling.software.SPDF.model.api.general;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
|
||||||
|
import stirling.software.common.model.api.PDFFile;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
public class BookletImpositionRequest extends PDFFile {
|
||||||
|
|
||||||
|
@Schema(
|
||||||
|
description = "The booklet type to create.",
|
||||||
|
type = "string",
|
||||||
|
defaultValue = "BOOKLET",
|
||||||
|
requiredMode = Schema.RequiredMode.REQUIRED,
|
||||||
|
allowableValues = {"BOOKLET", "SIDE_STITCH_BOOKLET"})
|
||||||
|
private String bookletType = "BOOKLET";
|
||||||
|
|
||||||
|
@Schema(
|
||||||
|
description = "The number of pages to fit onto a single sheet in the output PDF.",
|
||||||
|
type = "number",
|
||||||
|
defaultValue = "2",
|
||||||
|
requiredMode = Schema.RequiredMode.REQUIRED,
|
||||||
|
allowableValues = {"2", "4"})
|
||||||
|
private int pagesPerSheet = 2;
|
||||||
|
|
||||||
|
@Schema(description = "Boolean for if you wish to add border around the pages")
|
||||||
|
private Boolean addBorder = false;
|
||||||
|
|
||||||
|
@Schema(
|
||||||
|
description = "The page orientation for the output booklet sheets.",
|
||||||
|
type = "string",
|
||||||
|
defaultValue = "LANDSCAPE",
|
||||||
|
requiredMode = Schema.RequiredMode.REQUIRED,
|
||||||
|
allowableValues = {"LANDSCAPE", "PORTRAIT"})
|
||||||
|
private String pageOrientation = "LANDSCAPE";
|
||||||
|
}
|
@ -15,7 +15,7 @@ public class SignPDFWithCertRequest extends PDFFile {
|
|||||||
|
|
||||||
@Schema(
|
@Schema(
|
||||||
description = "The type of the digital certificate",
|
description = "The type of the digital certificate",
|
||||||
allowableValues = {"PEM", "PKCS12", "JKS"},
|
allowableValues = {"PEM", "PKCS12", "JKS", "SERVER"},
|
||||||
requiredMode = Schema.RequiredMode.REQUIRED)
|
requiredMode = Schema.RequiredMode.REQUIRED)
|
||||||
private String certType;
|
private String certType;
|
||||||
|
|
||||||
|
@ -0,0 +1,252 @@
|
|||||||
|
package stirling.software.SPDF.service;
|
||||||
|
|
||||||
|
import java.io.*;
|
||||||
|
import java.math.BigInteger;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.Paths;
|
||||||
|
import java.security.*;
|
||||||
|
import java.security.cert.Certificate;
|
||||||
|
import java.security.cert.X509Certificate;
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
import org.bouncycastle.asn1.x500.X500Name;
|
||||||
|
import org.bouncycastle.cert.X509CertificateHolder;
|
||||||
|
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
|
||||||
|
import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder;
|
||||||
|
import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
||||||
|
import org.bouncycastle.operator.ContentSigner;
|
||||||
|
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
import stirling.software.common.configuration.InstallationPathConfig;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@Slf4j
|
||||||
|
public class ServerCertificateService {
|
||||||
|
|
||||||
|
private static final String KEYSTORE_FILENAME = "server-certificate.p12";
|
||||||
|
private static final String KEYSTORE_ALIAS = "stirling-pdf-server";
|
||||||
|
private static final String DEFAULT_PASSWORD = "stirling-pdf-server-cert";
|
||||||
|
|
||||||
|
@Value("${system.serverCertificate.enabled:false}")
|
||||||
|
private boolean enabled;
|
||||||
|
|
||||||
|
@Value("${system.serverCertificate.organizationName:Stirling-PDF}")
|
||||||
|
private String organizationName;
|
||||||
|
|
||||||
|
@Value("${system.serverCertificate.validity:365}")
|
||||||
|
private int validityDays;
|
||||||
|
|
||||||
|
@Value("${system.serverCertificate.regenerateOnStartup:false}")
|
||||||
|
private boolean regenerateOnStartup;
|
||||||
|
|
||||||
|
static {
|
||||||
|
Security.addProvider(new BouncyCastleProvider());
|
||||||
|
}
|
||||||
|
|
||||||
|
private Path getKeystorePath() {
|
||||||
|
return Paths.get(InstallationPathConfig.getConfigPath(), KEYSTORE_FILENAME);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isEnabled() {
|
||||||
|
return enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean hasServerCertificate() {
|
||||||
|
return Files.exists(getKeystorePath());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void initializeServerCertificate() {
|
||||||
|
if (!enabled) {
|
||||||
|
log.debug("Server certificate feature is disabled");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Path keystorePath = getKeystorePath();
|
||||||
|
|
||||||
|
if (!Files.exists(keystorePath) || regenerateOnStartup) {
|
||||||
|
try {
|
||||||
|
generateServerCertificate();
|
||||||
|
log.info("Generated new server certificate at: {}", keystorePath);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Failed to generate server certificate", e);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.info("Server certificate already exists at: {}", keystorePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public KeyStore getServerKeyStore() throws Exception {
|
||||||
|
if (!enabled || !hasServerCertificate()) {
|
||||||
|
throw new IllegalStateException("Server certificate is not available");
|
||||||
|
}
|
||||||
|
|
||||||
|
KeyStore keyStore = KeyStore.getInstance("PKCS12");
|
||||||
|
try (FileInputStream fis = new FileInputStream(getKeystorePath().toFile())) {
|
||||||
|
keyStore.load(fis, DEFAULT_PASSWORD.toCharArray());
|
||||||
|
}
|
||||||
|
return keyStore;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getServerCertificatePassword() {
|
||||||
|
return DEFAULT_PASSWORD;
|
||||||
|
}
|
||||||
|
|
||||||
|
public X509Certificate getServerCertificate() throws Exception {
|
||||||
|
KeyStore keyStore = getServerKeyStore();
|
||||||
|
return (X509Certificate) keyStore.getCertificate(KEYSTORE_ALIAS);
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] getServerCertificatePublicKey() throws Exception {
|
||||||
|
X509Certificate cert = getServerCertificate();
|
||||||
|
return cert.getEncoded();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void uploadServerCertificate(InputStream p12Stream, String password) throws Exception {
|
||||||
|
// Validate the uploaded certificate
|
||||||
|
KeyStore uploadedKeyStore = KeyStore.getInstance("PKCS12");
|
||||||
|
uploadedKeyStore.load(p12Stream, password.toCharArray());
|
||||||
|
|
||||||
|
// Find the first private key entry
|
||||||
|
String alias = null;
|
||||||
|
for (String a : java.util.Collections.list(uploadedKeyStore.aliases())) {
|
||||||
|
if (uploadedKeyStore.isKeyEntry(a)) {
|
||||||
|
alias = a;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (alias == null) {
|
||||||
|
throw new IllegalArgumentException("No private key found in uploaded certificate");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create new keystore with our standard alias and password
|
||||||
|
KeyStore newKeyStore = KeyStore.getInstance("PKCS12");
|
||||||
|
newKeyStore.load(null, null);
|
||||||
|
|
||||||
|
PrivateKey privateKey = (PrivateKey) uploadedKeyStore.getKey(alias, password.toCharArray());
|
||||||
|
Certificate[] chain = uploadedKeyStore.getCertificateChain(alias);
|
||||||
|
|
||||||
|
newKeyStore.setKeyEntry(KEYSTORE_ALIAS, privateKey, DEFAULT_PASSWORD.toCharArray(), chain);
|
||||||
|
|
||||||
|
// Save to server keystore location
|
||||||
|
Path keystorePath = getKeystorePath();
|
||||||
|
Files.createDirectories(keystorePath.getParent());
|
||||||
|
|
||||||
|
try (FileOutputStream fos = new FileOutputStream(keystorePath.toFile())) {
|
||||||
|
newKeyStore.store(fos, DEFAULT_PASSWORD.toCharArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info("Server certificate updated from uploaded file");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void deleteServerCertificate() throws Exception {
|
||||||
|
Path keystorePath = getKeystorePath();
|
||||||
|
if (Files.exists(keystorePath)) {
|
||||||
|
Files.delete(keystorePath);
|
||||||
|
log.info("Server certificate deleted");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public ServerCertificateInfo getServerCertificateInfo() throws Exception {
|
||||||
|
if (!hasServerCertificate()) {
|
||||||
|
return new ServerCertificateInfo(false, null, null, null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
X509Certificate cert = getServerCertificate();
|
||||||
|
return new ServerCertificateInfo(
|
||||||
|
true,
|
||||||
|
cert.getSubjectX500Principal().getName(),
|
||||||
|
cert.getIssuerX500Principal().getName(),
|
||||||
|
cert.getNotBefore(),
|
||||||
|
cert.getNotAfter());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void generateServerCertificate() throws Exception {
|
||||||
|
// Generate key pair
|
||||||
|
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA", "BC");
|
||||||
|
keyPairGenerator.initialize(2048, new SecureRandom());
|
||||||
|
KeyPair keyPair = keyPairGenerator.generateKeyPair();
|
||||||
|
|
||||||
|
// Certificate details
|
||||||
|
X500Name subject =
|
||||||
|
new X500Name(
|
||||||
|
"CN=" + organizationName + " Server, O=" + organizationName + ", C=US");
|
||||||
|
BigInteger serialNumber = BigInteger.valueOf(System.currentTimeMillis());
|
||||||
|
Date notBefore = new Date();
|
||||||
|
Date notAfter = new Date(notBefore.getTime() + ((long) validityDays * 24 * 60 * 60 * 1000));
|
||||||
|
|
||||||
|
// Build certificate
|
||||||
|
JcaX509v3CertificateBuilder certBuilder =
|
||||||
|
new JcaX509v3CertificateBuilder(
|
||||||
|
subject, serialNumber, notBefore, notAfter, subject, keyPair.getPublic());
|
||||||
|
|
||||||
|
// Sign certificate
|
||||||
|
ContentSigner signer =
|
||||||
|
new JcaContentSignerBuilder("SHA256WithRSA")
|
||||||
|
.setProvider("BC")
|
||||||
|
.build(keyPair.getPrivate());
|
||||||
|
|
||||||
|
X509CertificateHolder certHolder = certBuilder.build(signer);
|
||||||
|
X509Certificate cert =
|
||||||
|
new JcaX509CertificateConverter().setProvider("BC").getCertificate(certHolder);
|
||||||
|
|
||||||
|
// Create keystore
|
||||||
|
KeyStore keyStore = KeyStore.getInstance("PKCS12");
|
||||||
|
keyStore.load(null, null);
|
||||||
|
keyStore.setKeyEntry(
|
||||||
|
KEYSTORE_ALIAS,
|
||||||
|
keyPair.getPrivate(),
|
||||||
|
DEFAULT_PASSWORD.toCharArray(),
|
||||||
|
new Certificate[] {cert});
|
||||||
|
|
||||||
|
// Save keystore
|
||||||
|
Path keystorePath = getKeystorePath();
|
||||||
|
Files.createDirectories(keystorePath.getParent());
|
||||||
|
|
||||||
|
try (FileOutputStream fos = new FileOutputStream(keystorePath.toFile())) {
|
||||||
|
keyStore.store(fos, DEFAULT_PASSWORD.toCharArray());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class ServerCertificateInfo {
|
||||||
|
private final boolean exists;
|
||||||
|
private final String subject;
|
||||||
|
private final String issuer;
|
||||||
|
private final Date validFrom;
|
||||||
|
private final Date validTo;
|
||||||
|
|
||||||
|
public ServerCertificateInfo(
|
||||||
|
boolean exists, String subject, String issuer, Date validFrom, Date validTo) {
|
||||||
|
this.exists = exists;
|
||||||
|
this.subject = subject;
|
||||||
|
this.issuer = issuer;
|
||||||
|
this.validFrom = validFrom;
|
||||||
|
this.validTo = validTo;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isExists() {
|
||||||
|
return exists;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getSubject() {
|
||||||
|
return subject;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getIssuer() {
|
||||||
|
return issuer;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Date getValidFrom() {
|
||||||
|
return validFrom;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Date getValidTo() {
|
||||||
|
return validTo;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -114,6 +114,11 @@ system:
|
|||||||
enableUrlToPDF: false # Set to 'true' to enable URL to PDF, INTERNAL ONLY, known security issues, should not be used externally
|
enableUrlToPDF: false # Set to 'true' to enable URL to PDF, INTERNAL ONLY, known security issues, should not be used externally
|
||||||
disableSanitize: false # set to true to disable Sanitize HTML; (can lead to injections in HTML)
|
disableSanitize: false # set to true to disable Sanitize HTML; (can lead to injections in HTML)
|
||||||
maxDPI: 500 # Maximum allowed DPI for PDF to image conversion
|
maxDPI: 500 # Maximum allowed DPI for PDF to image conversion
|
||||||
|
serverCertificate:
|
||||||
|
enabled: true # Enable server-side certificate for "Sign with Stirling-PDF" option
|
||||||
|
organizationName: Stirling-PDF # Organization name for generated certificates
|
||||||
|
validity: 365 # Certificate validity in days
|
||||||
|
regenerateOnStartup: false # Generate new certificate on each startup
|
||||||
html:
|
html:
|
||||||
urlSecurity:
|
urlSecurity:
|
||||||
enabled: true # Enable URL security restrictions for HTML processing
|
enabled: true # Enable URL security restrictions for HTML processing
|
||||||
|
@ -450,6 +450,10 @@
|
|||||||
"title": "Sign with Certificate",
|
"title": "Sign with Certificate",
|
||||||
"desc": "Signs a PDF with a Certificate/Key (PEM/P12)"
|
"desc": "Signs a PDF with a Certificate/Key (PEM/P12)"
|
||||||
},
|
},
|
||||||
|
"manageSignatures": {
|
||||||
|
"title": "Manage Signatures",
|
||||||
|
"desc": "Sign PDFs with certificates using manual or server-managed keys"
|
||||||
|
},
|
||||||
"removeCertSign": {
|
"removeCertSign": {
|
||||||
"title": "Remove Certificate Sign",
|
"title": "Remove Certificate Sign",
|
||||||
"desc": "Remove certificate signature from PDF"
|
"desc": "Remove certificate signature from PDF"
|
||||||
@ -458,6 +462,10 @@
|
|||||||
"title": "Multi-Page Layout",
|
"title": "Multi-Page Layout",
|
||||||
"desc": "Merge multiple pages of a PDF document into a single page"
|
"desc": "Merge multiple pages of a PDF document into a single page"
|
||||||
},
|
},
|
||||||
|
"bookletImposition": {
|
||||||
|
"title": "Booklet Imposition",
|
||||||
|
"desc": "Create booklets with proper page ordering and multi-page layout for printing and binding"
|
||||||
|
},
|
||||||
"scalePages": {
|
"scalePages": {
|
||||||
"title": "Adjust page size/scale",
|
"title": "Adjust page size/scale",
|
||||||
"desc": "Change the size/scale of a page and/or its contents."
|
"desc": "Change the size/scale of a page and/or its contents."
|
||||||
@ -1390,6 +1398,33 @@
|
|||||||
"showLogo": "Show Logo",
|
"showLogo": "Show Logo",
|
||||||
"submit": "Sign PDF"
|
"submit": "Sign PDF"
|
||||||
},
|
},
|
||||||
|
"manageSignatures": {
|
||||||
|
"tags": "sign,certificate,PEM,PKCS12,JKS,server,manual,auto",
|
||||||
|
"title": "Manage Signatures",
|
||||||
|
"header": "Sign PDFs with Certificates",
|
||||||
|
"files": {
|
||||||
|
"placeholder": "Select PDF files to sign with certificates"
|
||||||
|
},
|
||||||
|
"signMode": {
|
||||||
|
"stepTitle": "Sign Mode"
|
||||||
|
},
|
||||||
|
"certType": {
|
||||||
|
"stepTitle": "Certificate Format"
|
||||||
|
},
|
||||||
|
"certFiles": {
|
||||||
|
"stepTitle": "Certificate Files"
|
||||||
|
},
|
||||||
|
"appearance": {
|
||||||
|
"stepTitle": "Signature Appearance"
|
||||||
|
},
|
||||||
|
"sign": {
|
||||||
|
"submit": "Sign PDF",
|
||||||
|
"results": "Signed PDF"
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"failed": "An error occurred whilst signing the PDF."
|
||||||
|
}
|
||||||
|
},
|
||||||
"removeCertSign": {
|
"removeCertSign": {
|
||||||
"tags": "authenticate,PEM,P12,official,decrypt",
|
"tags": "authenticate,PEM,P12,official,decrypt",
|
||||||
"title": "Remove Certificate Signature",
|
"title": "Remove Certificate Signature",
|
||||||
@ -1416,6 +1451,18 @@
|
|||||||
"addBorder": "Add Borders",
|
"addBorder": "Add Borders",
|
||||||
"submit": "Submit"
|
"submit": "Submit"
|
||||||
},
|
},
|
||||||
|
"bookletImposition": {
|
||||||
|
"tags": "booklet,imposition,printing,binding,folding,signature",
|
||||||
|
"title": "Booklet Imposition",
|
||||||
|
"header": "Booklet Imposition",
|
||||||
|
"submit": "Create Booklet",
|
||||||
|
"files": {
|
||||||
|
"placeholder": "Select PDF files to create booklet impositions from"
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"failed": "An error occurred while creating the booklet imposition."
|
||||||
|
}
|
||||||
|
},
|
||||||
"scalePages": {
|
"scalePages": {
|
||||||
"tags": "resize,modify,dimension,adapt",
|
"tags": "resize,modify,dimension,adapt",
|
||||||
"title": "Adjust page-scale",
|
"title": "Adjust page-scale",
|
||||||
|
@ -495,6 +495,10 @@
|
|||||||
"title": "Multi-Page Layout",
|
"title": "Multi-Page Layout",
|
||||||
"desc": "Merge multiple pages of a PDF document into a single page"
|
"desc": "Merge multiple pages of a PDF document into a single page"
|
||||||
},
|
},
|
||||||
|
"bookletImposition": {
|
||||||
|
"title": "Booklet Imposition",
|
||||||
|
"desc": "Create booklets with proper page ordering and multi-page layout for printing and binding"
|
||||||
|
},
|
||||||
"scalePages": {
|
"scalePages": {
|
||||||
"title": "Adjust page size/scale",
|
"title": "Adjust page size/scale",
|
||||||
"desc": "Change the size/scale of a page and/or its contents."
|
"desc": "Change the size/scale of a page and/or its contents."
|
||||||
@ -1097,6 +1101,18 @@
|
|||||||
"addBorder": "Add Borders",
|
"addBorder": "Add Borders",
|
||||||
"submit": "Submit"
|
"submit": "Submit"
|
||||||
},
|
},
|
||||||
|
"bookletImposition": {
|
||||||
|
"tags": "booklet,imposition,printing,binding,folding,signature",
|
||||||
|
"title": "Booklet Imposition",
|
||||||
|
"header": "Booklet Imposition",
|
||||||
|
"submit": "Create Booklet",
|
||||||
|
"files": {
|
||||||
|
"placeholder": "Select PDF files to create booklet impositions from"
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"failed": "An error occurred while creating the booklet imposition."
|
||||||
|
}
|
||||||
|
},
|
||||||
"scalePages": {
|
"scalePages": {
|
||||||
"tags": "resize,modify,dimension,adapt",
|
"tags": "resize,modify,dimension,adapt",
|
||||||
"title": "Adjust page-scale",
|
"title": "Adjust page-scale",
|
||||||
|
@ -0,0 +1,149 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { Button, Stack, Text, Divider } from "@mantine/core";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { BookletImpositionParameters } from "../../../hooks/tools/bookletImposition/useBookletImpositionParameters";
|
||||||
|
|
||||||
|
interface BookletImpositionSettingsProps {
|
||||||
|
parameters: BookletImpositionParameters;
|
||||||
|
onParameterChange: (key: keyof BookletImpositionParameters, value: any) => void;
|
||||||
|
disabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const BookletImpositionSettings = ({ parameters, onParameterChange, disabled = false }: BookletImpositionSettingsProps) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack gap="md">
|
||||||
|
<Divider ml='-md'></Divider>
|
||||||
|
|
||||||
|
{/* Booklet Type */}
|
||||||
|
<Stack gap="sm">
|
||||||
|
<Text size="sm" fw={500}>Booklet Type</Text>
|
||||||
|
<div style={{ display: 'flex', gap: '4px' }}>
|
||||||
|
<Button
|
||||||
|
variant={parameters.bookletType === 'BOOKLET' ? 'filled' : 'outline'}
|
||||||
|
color={parameters.bookletType === 'BOOKLET' ? 'blue' : 'var(--text-muted)'}
|
||||||
|
onClick={() => onParameterChange('bookletType', 'BOOKLET')}
|
||||||
|
disabled={disabled}
|
||||||
|
style={{ flex: 1, height: 'auto', minHeight: '40px', fontSize: '11px' }}
|
||||||
|
>
|
||||||
|
<div style={{ textAlign: 'center', lineHeight: '1.1', fontSize: '11px' }}>
|
||||||
|
Standard<br />Booklet
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant={parameters.bookletType === 'SIDE_STITCH_BOOKLET' ? 'filled' : 'outline'}
|
||||||
|
color={parameters.bookletType === 'SIDE_STITCH_BOOKLET' ? 'blue' : 'var(--text-muted)'}
|
||||||
|
onClick={() => onParameterChange('bookletType', 'SIDE_STITCH_BOOKLET')}
|
||||||
|
disabled={disabled}
|
||||||
|
style={{ flex: 1, height: 'auto', minHeight: '40px', fontSize: '11px' }}
|
||||||
|
>
|
||||||
|
<div style={{ textAlign: 'center', lineHeight: '1.1', fontSize: '11px' }}>
|
||||||
|
Side-Stitch<br />Booklet
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<Text size="xs" c="dimmed">
|
||||||
|
{parameters.bookletType === 'BOOKLET'
|
||||||
|
? "Standard booklet for saddle-stitched binding (fold in half)"
|
||||||
|
: "Side-stitched booklet for binding along the edge"}
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
<Divider />
|
||||||
|
|
||||||
|
{/* Pages Per Sheet */}
|
||||||
|
<Stack gap="sm">
|
||||||
|
<Text size="sm" fw={500}>Pages Per Sheet</Text>
|
||||||
|
<div style={{ display: 'flex', gap: '4px' }}>
|
||||||
|
<Button
|
||||||
|
variant={parameters.pagesPerSheet === 2 ? 'filled' : 'outline'}
|
||||||
|
color={parameters.pagesPerSheet === 2 ? 'blue' : 'var(--text-muted)'}
|
||||||
|
onClick={() => onParameterChange('pagesPerSheet', 2)}
|
||||||
|
disabled={disabled}
|
||||||
|
style={{ flex: 1, height: 'auto', minHeight: '40px', fontSize: '11px' }}
|
||||||
|
>
|
||||||
|
<div style={{ textAlign: 'center', lineHeight: '1.1', fontSize: '11px' }}>
|
||||||
|
2 Pages<br />Per Sheet
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant={parameters.pagesPerSheet === 4 ? 'filled' : 'outline'}
|
||||||
|
color={parameters.pagesPerSheet === 4 ? 'blue' : 'var(--text-muted)'}
|
||||||
|
onClick={() => onParameterChange('pagesPerSheet', 4)}
|
||||||
|
disabled={disabled}
|
||||||
|
style={{ flex: 1, height: 'auto', minHeight: '40px', fontSize: '11px' }}
|
||||||
|
>
|
||||||
|
<div style={{ textAlign: 'center', lineHeight: '1.1', fontSize: '11px' }}>
|
||||||
|
4 Pages<br />Per Sheet
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<Text size="xs" c="dimmed">
|
||||||
|
{parameters.pagesPerSheet === 2
|
||||||
|
? "Two pages side by side on each sheet (most common)"
|
||||||
|
: "Four pages arranged in a 2x2 grid on each sheet"}
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
<Divider />
|
||||||
|
|
||||||
|
{/* Page Orientation */}
|
||||||
|
<Stack gap="sm">
|
||||||
|
<Text size="sm" fw={500}>Page Orientation</Text>
|
||||||
|
<div style={{ display: 'flex', gap: '4px' }}>
|
||||||
|
<Button
|
||||||
|
variant={parameters.pageOrientation === 'LANDSCAPE' ? 'filled' : 'outline'}
|
||||||
|
color={parameters.pageOrientation === 'LANDSCAPE' ? 'blue' : 'var(--text-muted)'}
|
||||||
|
onClick={() => onParameterChange('pageOrientation', 'LANDSCAPE')}
|
||||||
|
disabled={disabled}
|
||||||
|
style={{ flex: 1, height: 'auto', minHeight: '40px', fontSize: '11px' }}
|
||||||
|
>
|
||||||
|
<div style={{ textAlign: 'center', lineHeight: '1.1', fontSize: '11px' }}>
|
||||||
|
Landscape<br />(Recommended)
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant={parameters.pageOrientation === 'PORTRAIT' ? 'filled' : 'outline'}
|
||||||
|
color={parameters.pageOrientation === 'PORTRAIT' ? 'blue' : 'var(--text-muted)'}
|
||||||
|
onClick={() => onParameterChange('pageOrientation', 'PORTRAIT')}
|
||||||
|
disabled={disabled}
|
||||||
|
style={{ flex: 1, height: 'auto', minHeight: '40px', fontSize: '11px' }}
|
||||||
|
>
|
||||||
|
<div style={{ textAlign: 'center', lineHeight: '1.1', fontSize: '11px' }}>
|
||||||
|
Portrait
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<Text size="xs" c="dimmed">
|
||||||
|
{parameters.pageOrientation === 'LANDSCAPE'
|
||||||
|
? "A4 landscape → A5 portrait when folded (standard booklet size)"
|
||||||
|
: "A4 portrait → A6 when folded (smaller booklet)"}
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
<Divider />
|
||||||
|
|
||||||
|
{/* Add Border Option */}
|
||||||
|
<Stack gap="sm">
|
||||||
|
<label
|
||||||
|
style={{ display: 'flex', alignItems: 'center', gap: '8px' }}
|
||||||
|
title="Adds borders around each page section to help with cutting and alignment"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={parameters.addBorder}
|
||||||
|
onChange={(e) => onParameterChange('addBorder', e.target.checked)}
|
||||||
|
disabled={disabled}
|
||||||
|
/>
|
||||||
|
<Text size="sm">Add borders around pages</Text>
|
||||||
|
</label>
|
||||||
|
<Text size="xs" c="dimmed" style={{ marginLeft: '24px' }}>
|
||||||
|
Helpful for cutting and alignment when printing
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default BookletImpositionSettings;
|
@ -56,6 +56,12 @@ const CertificateFilesSettings = ({ parameters, onParameterChange, disabled = fa
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{parameters.certType === 'SERVER' && (
|
||||||
|
<Text c="dimmed" size="sm">
|
||||||
|
{t('manageSignatures.signing.serverCertMessage', 'Using server certificate - no files or password required')}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Password - only show when files are uploaded */}
|
{/* Password - only show when files are uploaded */}
|
||||||
{parameters.certType && (
|
{parameters.certType && (
|
||||||
(parameters.certType === 'PEM' && parameters.privateKeyFile && parameters.certFile) ||
|
(parameters.certType === 'PEM' && parameters.privateKeyFile && parameters.certFile) ||
|
||||||
|
@ -0,0 +1,67 @@
|
|||||||
|
import { Stack, Button, Text } from "@mantine/core";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { ManageSignaturesParameters } from "../../../hooks/tools/manageSignatures/useManageSignaturesParameters";
|
||||||
|
|
||||||
|
interface CertificateFormatSettingsProps {
|
||||||
|
parameters: ManageSignaturesParameters;
|
||||||
|
onParameterChange: (key: keyof ManageSignaturesParameters, value: any) => void;
|
||||||
|
disabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const CertificateFormatSettings = ({ parameters, onParameterChange, disabled = false }: CertificateFormatSettingsProps) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack gap="md">
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '4px' }}>
|
||||||
|
{/* First row - PKCS#12 and PEM */}
|
||||||
|
<div style={{ display: 'flex', gap: '4px' }}>
|
||||||
|
<Button
|
||||||
|
variant={parameters.certType === 'PKCS12' ? 'filled' : 'outline'}
|
||||||
|
color={parameters.certType === 'PKCS12' ? 'blue' : 'var(--text-muted)'}
|
||||||
|
onClick={() => onParameterChange('certType', 'PKCS12')}
|
||||||
|
disabled={disabled}
|
||||||
|
style={{ flex: 1, height: 'auto', minHeight: '40px', fontSize: '11px' }}
|
||||||
|
>
|
||||||
|
<div style={{ textAlign: 'center', lineHeight: '1.1', fontSize: '11px' }}>
|
||||||
|
PKCS#12<br />(Single file)
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant={parameters.certType === 'PEM' ? 'filled' : 'outline'}
|
||||||
|
color={parameters.certType === 'PEM' ? 'blue' : 'var(--text-muted)'}
|
||||||
|
onClick={() => onParameterChange('certType', 'PEM')}
|
||||||
|
disabled={disabled}
|
||||||
|
style={{ flex: 1, height: 'auto', minHeight: '40px', fontSize: '11px' }}
|
||||||
|
>
|
||||||
|
<div style={{ textAlign: 'center', lineHeight: '1.1', fontSize: '11px' }}>
|
||||||
|
PEM<br />(Key + Cert files)
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
{/* Second row - JKS spanning full width */}
|
||||||
|
<div style={{ display: 'flex', gap: '4px' }}>
|
||||||
|
<Button
|
||||||
|
variant={parameters.certType === 'JKS' ? 'filled' : 'outline'}
|
||||||
|
color={parameters.certType === 'JKS' ? 'blue' : 'var(--text-muted)'}
|
||||||
|
onClick={() => onParameterChange('certType', 'JKS')}
|
||||||
|
disabled={disabled}
|
||||||
|
style={{ width: '100%', height: 'auto', minHeight: '40px', fontSize: '11px' }}
|
||||||
|
>
|
||||||
|
<div style={{ textAlign: 'center', lineHeight: '1.1', fontSize: '11px' }}>
|
||||||
|
JKS<br />(Java KeyStore)
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Text size="xs" c="dimmed">
|
||||||
|
{parameters.certType === 'PKCS12' && "Upload a single .p12/.pfx file containing both certificate and private key"}
|
||||||
|
{parameters.certType === 'PEM' && "Upload separate certificate (.crt/.pem) and private key (.key/.pem) files"}
|
||||||
|
{parameters.certType === 'JKS' && "Upload a Java KeyStore (.jks) file"}
|
||||||
|
{!parameters.certType && "Choose the format of your certificate files"}
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CertificateFormatSettings;
|
@ -1,4 +1,4 @@
|
|||||||
import { Stack, Button } from "@mantine/core";
|
import { Stack, Button, Text, Divider } from "@mantine/core";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { ManageSignaturesParameters } from "../../../hooks/tools/manageSignatures/useManageSignaturesParameters";
|
import { ManageSignaturesParameters } from "../../../hooks/tools/manageSignatures/useManageSignaturesParameters";
|
||||||
|
|
||||||
@ -13,46 +13,45 @@ const CertificateTypeSettings = ({ parameters, onParameterChange, disabled = fal
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack gap="md">
|
<Stack gap="md">
|
||||||
{/* Certificate Type Selection */}
|
<div style={{ display: 'flex', gap: '4px' }}>
|
||||||
<Stack gap="sm">
|
<Button
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '4px' }}>
|
variant={parameters.signMode === 'MANUAL' ? 'filled' : 'outline'}
|
||||||
<div style={{ display: 'flex', gap: '4px' }}>
|
color={parameters.signMode === 'MANUAL' ? 'blue' : 'var(--text-muted)'}
|
||||||
<Button
|
onClick={() => {
|
||||||
variant={parameters.certType === 'PKCS12' ? 'filled' : 'outline'}
|
onParameterChange('signMode', 'MANUAL');
|
||||||
color={parameters.certType === 'PKCS12' ? 'blue' : 'var(--text-muted)'}
|
// Reset cert type when switching to manual
|
||||||
onClick={() => onParameterChange('certType', 'PKCS12')}
|
if (parameters.signMode === 'AUTO') {
|
||||||
disabled={disabled}
|
onParameterChange('certType', '');
|
||||||
style={{ flex: 1, height: 'auto', minHeight: '40px', fontSize: '11px' }}
|
}
|
||||||
>
|
}}
|
||||||
<div style={{ textAlign: 'center', lineHeight: '1.1', fontSize: '11px' }}>
|
disabled={disabled}
|
||||||
PKCS#12
|
style={{ flex: 1, height: 'auto', minHeight: '40px', fontSize: '11px' }}
|
||||||
</div>
|
>
|
||||||
</Button>
|
<div style={{ textAlign: 'center', lineHeight: '1.1', fontSize: '11px' }}>
|
||||||
<Button
|
Manual<br />(Provide Files)
|
||||||
variant={parameters.certType === 'PEM' ? 'filled' : 'outline'}
|
|
||||||
color={parameters.certType === 'PEM' ? 'blue' : 'var(--text-muted)'}
|
|
||||||
onClick={() => onParameterChange('certType', 'PEM')}
|
|
||||||
disabled={disabled}
|
|
||||||
style={{ flex: 1, height: 'auto', minHeight: '40px', fontSize: '11px' }}
|
|
||||||
>
|
|
||||||
<div style={{ textAlign: 'center', lineHeight: '1.1', fontSize: '11px' }}>
|
|
||||||
PEM
|
|
||||||
</div>
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
<Button
|
</Button>
|
||||||
variant={parameters.certType === 'JKS' ? 'filled' : 'outline'}
|
<Button
|
||||||
color={parameters.certType === 'JKS' ? 'blue' : 'var(--text-muted)'}
|
variant={parameters.signMode === 'AUTO' ? 'filled' : 'outline'}
|
||||||
onClick={() => onParameterChange('certType', 'JKS')}
|
color={parameters.signMode === 'AUTO' ? 'green' : 'var(--text-muted)'}
|
||||||
disabled={disabled}
|
onClick={() => {
|
||||||
style={{ width: '100%', height: 'auto', minHeight: '40px', fontSize: '11px' }}
|
onParameterChange('signMode', 'AUTO');
|
||||||
>
|
// Clear cert type and files when switching to auto
|
||||||
<div style={{ textAlign: 'center', lineHeight: '1.1', fontSize: '11px' }}>
|
onParameterChange('certType', '');
|
||||||
JKS
|
}}
|
||||||
</div>
|
disabled={disabled}
|
||||||
</Button>
|
style={{ flex: 1, height: 'auto', minHeight: '40px', fontSize: '11px' }}
|
||||||
</div>
|
>
|
||||||
</Stack>
|
<div style={{ textAlign: 'center', lineHeight: '1.1', fontSize: '11px' }}>
|
||||||
|
Auto<br />(Server Certificate)
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<Text size="xs" c="dimmed">
|
||||||
|
{parameters.signMode === 'MANUAL'
|
||||||
|
? "Upload your own certificate files for signing"
|
||||||
|
: "Use the server's pre-configured certificate"}
|
||||||
|
</Text>
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -19,7 +19,8 @@ export const useCertificateTypeTips = (): TooltipContent => {
|
|||||||
bullets: [
|
bullets: [
|
||||||
t("manageSignatures.certType.tooltip.which.bullet1", "PKCS#12 (.p12 / .pfx) – one combined file (most common)"),
|
t("manageSignatures.certType.tooltip.which.bullet1", "PKCS#12 (.p12 / .pfx) – one combined file (most common)"),
|
||||||
t("manageSignatures.certType.tooltip.which.bullet2", "PEM – separate private-key and certificate .pem files"),
|
t("manageSignatures.certType.tooltip.which.bullet2", "PEM – separate private-key and certificate .pem files"),
|
||||||
t("manageSignatures.certType.tooltip.which.bullet3", "JKS – Java .jks keystore for dev / CI-CD workflows")
|
t("manageSignatures.certType.tooltip.which.bullet3", "JKS – Java .jks keystore for dev / CI-CD workflows"),
|
||||||
|
t("manageSignatures.certType.tooltip.which.bullet4", "SERVER – use server's certificate (no files needed)")
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -33,10 +33,11 @@ export const useManageSignaturesTooltips = (): TooltipContent => {
|
|||||||
title: t("manageSignatures.tooltip.signing.title", "Adding Signatures"),
|
title: t("manageSignatures.tooltip.signing.title", "Adding Signatures"),
|
||||||
description: t("manageSignatures.tooltip.signing.text", "To sign a PDF, you need a digital certificate (like PEM, PKCS12, or JKS). You can choose to make the signature visible on the document or keep it invisible for security only."),
|
description: t("manageSignatures.tooltip.signing.text", "To sign a PDF, you need a digital certificate (like PEM, PKCS12, or JKS). You can choose to make the signature visible on the document or keep it invisible for security only."),
|
||||||
bullets: [
|
bullets: [
|
||||||
t("manageSignatures.tooltip.signing.bullet1", "Supports PEM, PKCS12, and JKS certificate formats"),
|
t("manageSignatures.tooltip.signing.bullet1", "Supports PEM, PKCS12, JKS, and server certificate formats"),
|
||||||
t("manageSignatures.tooltip.signing.bullet2", "Option to show or hide signature on the PDF"),
|
t("manageSignatures.tooltip.signing.bullet2", "Option to show or hide signature on the PDF"),
|
||||||
t("manageSignatures.tooltip.signing.bullet3", "Add reason, location, and signer name"),
|
t("manageSignatures.tooltip.signing.bullet3", "Add reason, location, and signer name"),
|
||||||
t("manageSignatures.tooltip.signing.bullet4", "Choose which page to place visible signatures")
|
t("manageSignatures.tooltip.signing.bullet4", "Choose which page to place visible signatures"),
|
||||||
|
t("manageSignatures.tooltip.signing.bullet5", "Use server certificate for simple 'Sign with Stirling-PDF' option")
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
@ -16,6 +16,7 @@ import SingleLargePage from "../tools/SingleLargePage";
|
|||||||
import UnlockPdfForms from "../tools/UnlockPdfForms";
|
import UnlockPdfForms from "../tools/UnlockPdfForms";
|
||||||
import RemoveCertificateSign from "../tools/RemoveCertificateSign";
|
import RemoveCertificateSign from "../tools/RemoveCertificateSign";
|
||||||
import ManageSignatures from "../tools/ManageSignatures";
|
import ManageSignatures from "../tools/ManageSignatures";
|
||||||
|
import BookletImposition from "../tools/BookletImposition";
|
||||||
import { compressOperationConfig } from "../hooks/tools/compress/useCompressOperation";
|
import { compressOperationConfig } from "../hooks/tools/compress/useCompressOperation";
|
||||||
import { splitOperationConfig } from "../hooks/tools/split/useSplitOperation";
|
import { splitOperationConfig } from "../hooks/tools/split/useSplitOperation";
|
||||||
import { addPasswordOperationConfig } from "../hooks/tools/addPassword/useAddPasswordOperation";
|
import { addPasswordOperationConfig } from "../hooks/tools/addPassword/useAddPasswordOperation";
|
||||||
@ -30,6 +31,7 @@ import { convertOperationConfig } from "../hooks/tools/convert/useConvertOperati
|
|||||||
import { removeCertificateSignOperationConfig } from "../hooks/tools/removeCertificateSign/useRemoveCertificateSignOperation";
|
import { removeCertificateSignOperationConfig } from "../hooks/tools/removeCertificateSign/useRemoveCertificateSignOperation";
|
||||||
import { changePermissionsOperationConfig } from "../hooks/tools/changePermissions/useChangePermissionsOperation";
|
import { changePermissionsOperationConfig } from "../hooks/tools/changePermissions/useChangePermissionsOperation";
|
||||||
import { manageSignaturesOperationConfig } from "../hooks/tools/manageSignatures/useManageSignaturesOperation";
|
import { manageSignaturesOperationConfig } from "../hooks/tools/manageSignatures/useManageSignaturesOperation";
|
||||||
|
import { bookletImpositionOperationConfig } from "../hooks/tools/bookletImposition/useBookletImpositionOperation";
|
||||||
import CompressSettings from "../components/tools/compress/CompressSettings";
|
import CompressSettings from "../components/tools/compress/CompressSettings";
|
||||||
import SplitSettings from "../components/tools/split/SplitSettings";
|
import SplitSettings from "../components/tools/split/SplitSettings";
|
||||||
import AddPasswordSettings from "../components/tools/addPassword/AddPasswordSettings";
|
import AddPasswordSettings from "../components/tools/addPassword/AddPasswordSettings";
|
||||||
@ -42,6 +44,7 @@ import OCRSettings from "../components/tools/ocr/OCRSettings";
|
|||||||
import ConvertSettings from "../components/tools/convert/ConvertSettings";
|
import ConvertSettings from "../components/tools/convert/ConvertSettings";
|
||||||
import ChangePermissionsSettings from "../components/tools/changePermissions/ChangePermissionsSettings";
|
import ChangePermissionsSettings from "../components/tools/changePermissions/ChangePermissionsSettings";
|
||||||
import CertificateTypeSettings from "../components/tools/manageSignatures/CertificateTypeSettings";
|
import CertificateTypeSettings from "../components/tools/manageSignatures/CertificateTypeSettings";
|
||||||
|
import BookletImpositionSettings from "../components/tools/bookletImposition/BookletImpositionSettings";
|
||||||
import { ToolId } from "../types/toolId";
|
import { ToolId } from "../types/toolId";
|
||||||
|
|
||||||
const showPlaceholderTools = true; // Show all tools; grey out unavailable ones in UI
|
const showPlaceholderTools = true; // Show all tools; grey out unavailable ones in UI
|
||||||
@ -353,6 +356,16 @@ export function useFlatToolRegistry(): ToolRegistry {
|
|||||||
categoryId: ToolCategoryId.STANDARD_TOOLS,
|
categoryId: ToolCategoryId.STANDARD_TOOLS,
|
||||||
subcategoryId: SubcategoryId.PAGE_FORMATTING,
|
subcategoryId: SubcategoryId.PAGE_FORMATTING,
|
||||||
},
|
},
|
||||||
|
"booklet-imposition": {
|
||||||
|
icon: <LocalIcon icon="menu-book-rounded" width="1.5rem" height="1.5rem" />,
|
||||||
|
name: t("home.bookletImposition.title", "Booklet Imposition"),
|
||||||
|
component: BookletImposition,
|
||||||
|
operationConfig: bookletImpositionOperationConfig,
|
||||||
|
settingsComponent: BookletImpositionSettings,
|
||||||
|
description: t("home.bookletImposition.desc", "Create booklets with proper page ordering and multi-page layout for printing and binding"),
|
||||||
|
categoryId: ToolCategoryId.STANDARD_TOOLS,
|
||||||
|
subcategoryId: SubcategoryId.PAGE_FORMATTING,
|
||||||
|
},
|
||||||
"single-large-page": {
|
"single-large-page": {
|
||||||
icon: <LocalIcon icon="looks-one-outline-rounded" width="1.5rem" height="1.5rem" />,
|
icon: <LocalIcon icon="looks-one-outline-rounded" width="1.5rem" height="1.5rem" />,
|
||||||
name: t("home.pdfToSinglePage.title", "PDF to Single Large Page"),
|
name: t("home.pdfToSinglePage.title", "PDF to Single Large Page"),
|
||||||
|
@ -0,0 +1,34 @@
|
|||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { useToolOperation, ToolOperationConfig, ToolType } from '../shared/useToolOperation';
|
||||||
|
import { createStandardErrorHandler } from '../../../utils/toolErrorHandler';
|
||||||
|
import { BookletImpositionParameters, defaultParameters } from './useBookletImpositionParameters';
|
||||||
|
|
||||||
|
// Static configuration that can be used by both the hook and automation executor
|
||||||
|
export const buildBookletImpositionFormData = (parameters: BookletImpositionParameters, file: File): FormData => {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append("fileInput", file);
|
||||||
|
formData.append("bookletType", parameters.bookletType);
|
||||||
|
formData.append("pagesPerSheet", parameters.pagesPerSheet.toString());
|
||||||
|
formData.append("addBorder", parameters.addBorder.toString());
|
||||||
|
formData.append("pageOrientation", parameters.pageOrientation);
|
||||||
|
return formData;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Static configuration object
|
||||||
|
export const bookletImpositionOperationConfig = {
|
||||||
|
toolType: ToolType.singleFile,
|
||||||
|
buildFormData: buildBookletImpositionFormData,
|
||||||
|
operationType: 'bookletImposition',
|
||||||
|
endpoint: '/api/v1/general/booklet-imposition',
|
||||||
|
filePrefix: 'booklet_',
|
||||||
|
defaultParameters,
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const useBookletImpositionOperation = () => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
return useToolOperation<BookletImpositionParameters>({
|
||||||
|
...bookletImpositionOperationConfig,
|
||||||
|
getErrorMessage: createStandardErrorHandler(t('bookletImposition.error.failed', 'An error occurred while creating the booklet imposition.'))
|
||||||
|
});
|
||||||
|
};
|
@ -0,0 +1,28 @@
|
|||||||
|
import { BaseParameters } from '../../../types/parameters';
|
||||||
|
import { useBaseParameters, BaseParametersHook } from '../shared/useBaseParameters';
|
||||||
|
|
||||||
|
export interface BookletImpositionParameters extends BaseParameters {
|
||||||
|
bookletType: 'BOOKLET' | 'SIDE_STITCH_BOOKLET';
|
||||||
|
pagesPerSheet: 2 | 4;
|
||||||
|
addBorder: boolean;
|
||||||
|
pageOrientation: 'LANDSCAPE' | 'PORTRAIT';
|
||||||
|
}
|
||||||
|
|
||||||
|
export const defaultParameters: BookletImpositionParameters = {
|
||||||
|
bookletType: 'BOOKLET',
|
||||||
|
pagesPerSheet: 2,
|
||||||
|
addBorder: false,
|
||||||
|
pageOrientation: 'LANDSCAPE',
|
||||||
|
};
|
||||||
|
|
||||||
|
export type BookletImpositionParametersHook = BaseParametersHook<BookletImpositionParameters>;
|
||||||
|
|
||||||
|
export const useBookletImpositionParameters = (): BookletImpositionParametersHook => {
|
||||||
|
return useBaseParameters({
|
||||||
|
defaultParameters,
|
||||||
|
endpointName: 'booklet-imposition',
|
||||||
|
validateFn: (params) => {
|
||||||
|
return params.pagesPerSheet === 2 || params.pagesPerSheet === 4;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
@ -7,29 +7,35 @@ import { ManageSignaturesParameters, defaultParameters } from './useManageSignat
|
|||||||
export const buildManageSignaturesFormData = (parameters: ManageSignaturesParameters, file: File): FormData => {
|
export const buildManageSignaturesFormData = (parameters: ManageSignaturesParameters, file: File): FormData => {
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('fileInput', file);
|
formData.append('fileInput', file);
|
||||||
formData.append('certType', parameters.certType);
|
|
||||||
formData.append('password', parameters.password);
|
|
||||||
|
|
||||||
// Add certificate files based on type
|
// Handle sign mode
|
||||||
switch (parameters.certType) {
|
if (parameters.signMode === 'AUTO') {
|
||||||
case 'PEM':
|
formData.append('certType', 'SERVER');
|
||||||
if (parameters.privateKeyFile) {
|
} else {
|
||||||
formData.append('privateKeyFile', parameters.privateKeyFile);
|
formData.append('certType', parameters.certType);
|
||||||
}
|
formData.append('password', parameters.password);
|
||||||
if (parameters.certFile) {
|
|
||||||
formData.append('certFile', parameters.certFile);
|
// Add certificate files based on type (only for manual mode)
|
||||||
}
|
switch (parameters.certType) {
|
||||||
break;
|
case 'PEM':
|
||||||
case 'PKCS12':
|
if (parameters.privateKeyFile) {
|
||||||
if (parameters.p12File) {
|
formData.append('privateKeyFile', parameters.privateKeyFile);
|
||||||
formData.append('p12File', parameters.p12File);
|
}
|
||||||
}
|
if (parameters.certFile) {
|
||||||
break;
|
formData.append('certFile', parameters.certFile);
|
||||||
case 'JKS':
|
}
|
||||||
if (parameters.jksFile) {
|
break;
|
||||||
formData.append('jksFile', parameters.jksFile);
|
case 'PKCS12':
|
||||||
}
|
if (parameters.p12File) {
|
||||||
break;
|
formData.append('p12File', parameters.p12File);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'JKS':
|
||||||
|
if (parameters.jksFile) {
|
||||||
|
formData.append('jksFile', parameters.jksFile);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add signature appearance options if enabled
|
// Add signature appearance options if enabled
|
||||||
|
@ -2,7 +2,9 @@ import { BaseParameters } from '../../../types/parameters';
|
|||||||
import { useBaseParameters, BaseParametersHook } from '../shared/useBaseParameters';
|
import { useBaseParameters, BaseParametersHook } from '../shared/useBaseParameters';
|
||||||
|
|
||||||
export interface ManageSignaturesParameters extends BaseParameters {
|
export interface ManageSignaturesParameters extends BaseParameters {
|
||||||
// Certificate signing options
|
// Sign mode selection
|
||||||
|
signMode: 'MANUAL' | 'AUTO';
|
||||||
|
// Certificate signing options (only for manual mode)
|
||||||
certType: '' | 'PEM' | 'PKCS12' | 'JKS';
|
certType: '' | 'PEM' | 'PKCS12' | 'JKS';
|
||||||
privateKeyFile?: File;
|
privateKeyFile?: File;
|
||||||
certFile?: File;
|
certFile?: File;
|
||||||
@ -20,6 +22,7 @@ export interface ManageSignaturesParameters extends BaseParameters {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const defaultParameters: ManageSignaturesParameters = {
|
export const defaultParameters: ManageSignaturesParameters = {
|
||||||
|
signMode: 'MANUAL',
|
||||||
certType: '',
|
certType: '',
|
||||||
password: '',
|
password: '',
|
||||||
showSignature: false,
|
showSignature: false,
|
||||||
@ -37,7 +40,12 @@ export const useManageSignaturesParameters = (): ManageSignaturesParametersHook
|
|||||||
defaultParameters,
|
defaultParameters,
|
||||||
endpointName: 'manage-signatures',
|
endpointName: 'manage-signatures',
|
||||||
validateFn: (params) => {
|
validateFn: (params) => {
|
||||||
// Requires certificate type
|
// Auto mode (server certificate) - no additional validation needed
|
||||||
|
if (params.signMode === 'AUTO') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Manual mode - requires certificate type and files
|
||||||
if (!params.certType) {
|
if (!params.certType) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
56
frontend/src/tools/BookletImposition.tsx
Normal file
56
frontend/src/tools/BookletImposition.tsx
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { createToolFlow } from "../components/tools/shared/createToolFlow";
|
||||||
|
import BookletImpositionSettings from "../components/tools/bookletImposition/BookletImpositionSettings";
|
||||||
|
import { useBookletImpositionParameters } from "../hooks/tools/bookletImposition/useBookletImpositionParameters";
|
||||||
|
import { useBookletImpositionOperation } from "../hooks/tools/bookletImposition/useBookletImpositionOperation";
|
||||||
|
import { useBaseTool } from "../hooks/tools/shared/useBaseTool";
|
||||||
|
import { BaseToolProps, ToolComponent } from "../types/tool";
|
||||||
|
|
||||||
|
const BookletImposition = (props: BaseToolProps) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const base = useBaseTool(
|
||||||
|
'bookletImposition',
|
||||||
|
useBookletImpositionParameters,
|
||||||
|
useBookletImpositionOperation,
|
||||||
|
props
|
||||||
|
);
|
||||||
|
|
||||||
|
return createToolFlow({
|
||||||
|
files: {
|
||||||
|
selectedFiles: base.selectedFiles,
|
||||||
|
isCollapsed: base.hasResults,
|
||||||
|
placeholder: t("bookletImposition.files.placeholder", "Select PDF files to create booklet impositions from"),
|
||||||
|
},
|
||||||
|
steps: [
|
||||||
|
{
|
||||||
|
title: "Settings",
|
||||||
|
isCollapsed: base.settingsCollapsed,
|
||||||
|
onCollapsedClick: base.settingsCollapsed ? base.handleSettingsReset : undefined,
|
||||||
|
content: (
|
||||||
|
<BookletImpositionSettings
|
||||||
|
parameters={base.params.parameters}
|
||||||
|
onParameterChange={base.params.updateParameter}
|
||||||
|
disabled={base.endpointLoading}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
executeButton: {
|
||||||
|
text: t("bookletImposition.submit", "Create Booklet"),
|
||||||
|
isVisible: !base.hasResults,
|
||||||
|
loadingText: t("loading"),
|
||||||
|
onClick: base.handleExecute,
|
||||||
|
disabled: !base.params.validateParameters() || !base.hasFiles || !base.endpointEnabled,
|
||||||
|
},
|
||||||
|
review: {
|
||||||
|
isVisible: base.hasResults,
|
||||||
|
operation: base.operation,
|
||||||
|
title: t("bookletImposition.title", "Booklet Imposition Results"),
|
||||||
|
onFileClick: base.handleThumbnailClick,
|
||||||
|
onUndo: base.handleUndo,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export default BookletImposition as ToolComponent;
|
@ -1,6 +1,7 @@
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { createToolFlow } from "../components/tools/shared/createToolFlow";
|
import { createToolFlow } from "../components/tools/shared/createToolFlow";
|
||||||
import CertificateTypeSettings from "../components/tools/manageSignatures/CertificateTypeSettings";
|
import CertificateTypeSettings from "../components/tools/manageSignatures/CertificateTypeSettings";
|
||||||
|
import CertificateFormatSettings from "../components/tools/manageSignatures/CertificateFormatSettings";
|
||||||
import CertificateFilesSettings from "../components/tools/manageSignatures/CertificateFilesSettings";
|
import CertificateFilesSettings from "../components/tools/manageSignatures/CertificateFilesSettings";
|
||||||
import SignatureAppearanceSettings from "../components/tools/manageSignatures/SignatureAppearanceSettings";
|
import SignatureAppearanceSettings from "../components/tools/manageSignatures/SignatureAppearanceSettings";
|
||||||
import { useManageSignaturesParameters } from "../hooks/tools/manageSignatures/useManageSignaturesParameters";
|
import { useManageSignaturesParameters } from "../hooks/tools/manageSignatures/useManageSignaturesParameters";
|
||||||
@ -26,6 +27,13 @@ const ManageSignatures = (props: BaseToolProps) => {
|
|||||||
// Check if certificate files are configured for appearance step
|
// Check if certificate files are configured for appearance step
|
||||||
const areCertFilesConfigured = () => {
|
const areCertFilesConfigured = () => {
|
||||||
const params = base.params.parameters;
|
const params = base.params.parameters;
|
||||||
|
|
||||||
|
// Auto mode (server certificate) - always configured
|
||||||
|
if (params.signMode === 'AUTO') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Manual mode - check for required files based on cert type
|
||||||
switch (params.certType) {
|
switch (params.certType) {
|
||||||
case 'PEM':
|
case 'PEM':
|
||||||
return !!(params.privateKeyFile && params.certFile);
|
return !!(params.privateKeyFile && params.certFile);
|
||||||
@ -47,10 +55,9 @@ const ManageSignatures = (props: BaseToolProps) => {
|
|||||||
},
|
},
|
||||||
steps: [
|
steps: [
|
||||||
{
|
{
|
||||||
title: t("manageSignatures.certType.stepTitle", "Certificate Type"),
|
title: t("manageSignatures.signMode.stepTitle", "Sign Mode"),
|
||||||
isCollapsed: base.settingsCollapsed,
|
isCollapsed: base.settingsCollapsed,
|
||||||
onCollapsedClick: base.settingsCollapsed ? base.handleSettingsReset : undefined,
|
onCollapsedClick: base.settingsCollapsed ? base.handleSettingsReset : undefined,
|
||||||
tooltip: certTypeTips,
|
|
||||||
content: (
|
content: (
|
||||||
<CertificateTypeSettings
|
<CertificateTypeSettings
|
||||||
parameters={base.params.parameters}
|
parameters={base.params.parameters}
|
||||||
@ -59,7 +66,20 @@ const ManageSignatures = (props: BaseToolProps) => {
|
|||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
...(base.params.parameters.signMode === 'MANUAL' ? [{
|
||||||
|
title: t("manageSignatures.certType.stepTitle", "Certificate Format"),
|
||||||
|
isCollapsed: base.settingsCollapsed,
|
||||||
|
onCollapsedClick: base.settingsCollapsed ? base.handleSettingsReset : undefined,
|
||||||
|
tooltip: certTypeTips,
|
||||||
|
content: (
|
||||||
|
<CertificateFormatSettings
|
||||||
|
parameters={base.params.parameters}
|
||||||
|
onParameterChange={base.params.updateParameter}
|
||||||
|
disabled={base.endpointLoading}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
}] : []),
|
||||||
|
...(base.params.parameters.signMode === 'MANUAL' ? [{
|
||||||
title: t("manageSignatures.certFiles.stepTitle", "Certificate Files"),
|
title: t("manageSignatures.certFiles.stepTitle", "Certificate Files"),
|
||||||
isCollapsed: base.settingsCollapsed,
|
isCollapsed: base.settingsCollapsed,
|
||||||
onCollapsedClick: base.settingsCollapsed ? base.handleSettingsReset : undefined,
|
onCollapsedClick: base.settingsCollapsed ? base.handleSettingsReset : undefined,
|
||||||
@ -70,7 +90,7 @@ const ManageSignatures = (props: BaseToolProps) => {
|
|||||||
disabled={base.endpointLoading}
|
disabled={base.endpointLoading}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
},
|
}] : []),
|
||||||
{
|
{
|
||||||
title: t("manageSignatures.appearance.stepTitle", "Signature Appearance"),
|
title: t("manageSignatures.appearance.stepTitle", "Signature Appearance"),
|
||||||
isCollapsed: base.settingsCollapsed || !areCertFilesConfigured(),
|
isCollapsed: base.settingsCollapsed || !areCertFilesConfigured(),
|
||||||
|
@ -7,7 +7,7 @@ const TOOL_IDS = [
|
|||||||
'detect-split-scanned-photos',
|
'detect-split-scanned-photos',
|
||||||
'edit-table-of-contents',
|
'edit-table-of-contents',
|
||||||
'scanner-effect',
|
'scanner-effect',
|
||||||
'auto-rename-pdf-file', 'multi-page-layout', 'adjust-page-size-scale', 'adjust-contrast', 'cropPdf', 'single-large-page', 'multi-tool',
|
'auto-rename-pdf-file', 'multi-page-layout', 'booklet-imposition', 'adjust-page-size-scale', 'adjust-contrast', 'cropPdf', 'single-large-page', 'multi-tool',
|
||||||
'repair', 'compare', 'addPageNumbers', 'redact',
|
'repair', 'compare', 'addPageNumbers', 'redact',
|
||||||
'flatten', 'remove-certificate-sign',
|
'flatten', 'remove-certificate-sign',
|
||||||
'unlock-pdf-forms', 'compress', 'extract-page', 'reorganize-pages', 'extract-images',
|
'unlock-pdf-forms', 'compress', 'extract-page', 'reorganize-pages', 'extract-images',
|
||||||
|
Loading…
x
Reference in New Issue
Block a user