remove sig stuff from this PR

This commit is contained in:
Anthony Stirling 2025-09-21 15:10:20 +01:00
parent 32e6ec2ea9
commit 9ddcd7ff6a
35 changed files with 106 additions and 2197 deletions

View File

@ -1,66 +0,0 @@
package stirling.software.common.service;
import java.io.InputStream;
import java.security.KeyStore;
import java.security.cert.X509Certificate;
import java.util.Date;
public interface ServerCertificateService {
boolean isEnabled();
boolean hasServerCertificate();
void initializeServerCertificate();
KeyStore getServerKeyStore() throws Exception;
String getServerCertificatePassword();
X509Certificate getServerCertificate() throws Exception;
byte[] getServerCertificatePublicKey() throws Exception;
void uploadServerCertificate(InputStream p12Stream, String password) throws Exception;
void deleteServerCertificate() throws Exception;
ServerCertificateInfo getServerCertificateInfo() throws Exception;
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;
}
}
}

View File

@ -237,7 +237,6 @@ public class EndpointConfiguration {
addEndpointToGroup("PageOps", "pdf-organizer");
addEndpointToGroup("PageOps", "rotate-pdf");
addEndpointToGroup("PageOps", "multi-page-layout");
addEndpointToGroup("PageOps", "booklet-imposition");
addEndpointToGroup("PageOps", "scale-pages");
addEndpointToGroup("PageOps", "crop");
addEndpointToGroup("PageOps", "extract-page");
@ -367,7 +366,6 @@ public class EndpointConfiguration {
addEndpointToGroup("Java", "cert-sign");
addEndpointToGroup("Java", "remove-cert-sign");
addEndpointToGroup("Java", "multi-page-layout");
addEndpointToGroup("Java", "booklet-imposition");
addEndpointToGroup("Java", "scale-pages");
addEndpointToGroup("Java", "add-page-numbers");
addEndpointToGroup("Java", "auto-rename");

View File

@ -21,14 +21,38 @@ import io.swagger.v3.oas.annotations.responses.ApiResponses;
@ApiResponse(
responseCode = "200",
description = "PDF tables extracted successfully to CSV format",
content = {
@Content(
mediaType = "text/csv",
schema =
@Schema(
type = "string",
format = "binary",
description =
"CSV file containing extracted table data")),
@Content(
mediaType = "application/zip",
schema =
@Schema(
type = "string",
format = "binary",
description =
"ZIP archive containing multiple CSV files when multiple tables are extracted"))
}),
@ApiResponse(
responseCode = "400",
description = "Invalid PDF file or no tables found for extraction",
content =
@Content(
mediaType = "text/csv",
schema =
@Schema(
type = "string",
format = "binary",
description =
"CSV file containing extracted table data")))
mediaType = "application/json",
schema = @Schema(implementation = ErrorResponse.class))),
@ApiResponse(responseCode = "204", description = "No tables found in the PDF"),
@ApiResponse(
responseCode = "500",
description = "Internal server error during table extraction",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = ErrorResponse.class)))
})
public @interface CsvConversionResponse {}

View File

@ -25,6 +25,6 @@ public class ErrorResponse {
@Schema(
description = "Request path where the error occurred",
example = "/api/v1/general/rotate-pdf")
example = "/api/v1/{endpoint-path}")
private String path;
}

View File

@ -28,6 +28,20 @@ import io.swagger.v3.oas.annotations.responses.ApiResponses;
@Schema(
type = "object",
description =
"JSON object containing the requested data or analysis results")))
"JSON object containing the requested data or analysis results"))),
@ApiResponse(
responseCode = "400",
description = "Invalid PDF file or request parameters",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = ErrorResponse.class))),
@ApiResponse(
responseCode = "500",
description = "Internal server error during processing",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = ErrorResponse.class)))
})
public @interface JsonDataResponse {}

View File

@ -52,6 +52,20 @@ import io.swagger.v3.oas.annotations.responses.ApiResponses;
type = "string",
format = "binary",
description = "Single image file (JPEG)"))
})
}),
@ApiResponse(
responseCode = "400",
description = "Invalid PDF file or request parameters",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = ErrorResponse.class))),
@ApiResponse(
responseCode = "500",
description = "Internal server error during processing",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = ErrorResponse.class)))
})
public @interface MultiFileResponse {}

View File

@ -28,6 +28,20 @@ import io.swagger.v3.oas.annotations.responses.ApiResponses;
@Schema(
type = "string",
format = "binary",
description = "The processed PDF file")))
description = "The processed PDF file"))),
@ApiResponse(
responseCode = "400",
description = "Invalid PDF file or request parameters",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = ErrorResponse.class))),
@ApiResponse(
responseCode = "500",
description = "Internal server error during processing",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = ErrorResponse.class)))
})
public @interface StandardPdfResponse {}

View File

@ -1,27 +0,0 @@
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.common.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);
}
}
}

View File

@ -1,206 +0,0 @@
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.multipart.MultipartFile;
import io.github.pixee.security.Filenames;
import io.swagger.v3.oas.annotations.Operation;
import lombok.RequiredArgsConstructor;
import stirling.software.SPDF.config.swagger.StandardPdfResponse;
import stirling.software.SPDF.model.api.general.BookletImpositionRequest;
import stirling.software.common.annotations.AutoJobPostMapping;
import stirling.software.common.annotations.api.GeneralApi;
import stirling.software.common.service.CustomPDFDocumentFactory;
import stirling.software.common.util.WebResponseUtils;
@GeneralApi
@RequiredArgsConstructor
public class BookletImpositionController {
private final CustomPDFDocumentFactory pdfDocumentFactory;
@AutoJobPostMapping(value = "/booklet-imposition", consumes = "multipart/form-data")
@StandardPdfResponse
@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;
}
}

View File

@ -59,11 +59,14 @@ import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.InitBinder;
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.micrometer.common.util.StringUtils;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@ -71,14 +74,14 @@ import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.config.swagger.StandardPdfResponse;
import stirling.software.SPDF.model.api.security.SignPDFWithCertRequest;
import stirling.software.common.annotations.AutoJobPostMapping;
import stirling.software.common.annotations.api.SecurityApi;
import stirling.software.common.service.CustomPDFDocumentFactory;
import stirling.software.common.service.ServerCertificateService;
import stirling.software.common.util.ExceptionUtils;
import stirling.software.common.util.WebResponseUtils;
@SecurityApi
@RestController
@RequestMapping("/api/v1/security")
@Slf4j
@Tag(name = "Security", description = "Security APIs")
@RequiredArgsConstructor
public class CertSignController {
@ -99,7 +102,6 @@ public class CertSignController {
}
private final CustomPDFDocumentFactory pdfDocumentFactory;
private final ServerCertificateService serverCertificateService;
private static void sign(
CustomPDFDocumentFactory pdfDocumentFactory,
@ -137,7 +139,12 @@ public class CertSignController {
}
}
@AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/cert-sign")
@AutoJobPostMapping(
consumes = {
MediaType.MULTIPART_FORM_DATA_VALUE,
MediaType.APPLICATION_FORM_URLENCODED_VALUE
},
value = "/cert-sign")
@StandardPdfResponse
@Operation(
summary = "Sign PDF with a Digital Certificate",
@ -170,7 +177,6 @@ public class CertSignController {
}
KeyStore ks = null;
String keystorePassword = password;
switch (certType) {
case "PEM":
@ -189,19 +195,6 @@ public class CertSignController {
ks = KeyStore.getInstance("JKS");
ks.load(jksfile.getInputStream(), password.toCharArray());
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:
throw ExceptionUtils.createIllegalArgumentException(
"error.invalidArgument",
@ -209,7 +202,7 @@ public class CertSignController {
"certificate type: " + certType);
}
CreateSignature createSignature = new CreateSignature(ks, keystorePassword.toCharArray());
CreateSignature createSignature = new CreateSignature(ks, password.toCharArray());
ByteArrayOutputStream baos = new ByteArrayOutputStream();
sign(
pdfDocumentFactory,

View File

@ -1,39 +0,0 @@
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 = "integer",
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";
}

View File

@ -15,7 +15,7 @@ public class SignPDFWithCertRequest extends PDFFile {
@Schema(
description = "The type of the digital certificate",
allowableValues = {"PEM", "PKCS12", "JKS", "SERVER"},
allowableValues = {"PEM", "PKCS12", "JKS"},
requiredMode = Schema.RequiredMode.REQUIRED)
private String certType;

View File

@ -114,11 +114,6 @@ system:
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)
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:
urlSecurity:
enabled: true # Enable URL security restrictions for HTML processing

View File

@ -1,141 +0,0 @@
package stirling.software.proprietary.security.controller.api;
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 lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import stirling.software.common.annotations.api.AdminServerCertificateApi;
import stirling.software.common.service.ServerCertificateService;
@AdminServerCertificateApi
@Slf4j
@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(value = "/upload", consumes = "multipart/form-data")
@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());
}
}

View File

@ -1,216 +0,0 @@
package stirling.software.proprietary.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
implements stirling.software.common.service.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());
}
}
}

View File

@ -426,10 +426,6 @@
"title": "Flatten",
"desc": "Remove all interactive elements and forms from a PDF"
},
"manageSignatures": {
"title": "Sign with Certificate",
"desc": "Add digital signatures to PDF documents using certificates"
},
"repair": {
"title": "Repair",
"desc": "Tries to repair a corrupt/broken PDF"
@ -450,10 +446,6 @@
"title": "Sign with Certificate",
"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": {
"title": "Remove Certificate Sign",
"desc": "Remove certificate signature from PDF"
@ -462,10 +454,6 @@
"title": "Multi-Page Layout",
"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": {
"title": "Adjust page size/scale",
"desc": "Change the size/scale of a page and/or its contents."
@ -1560,98 +1548,7 @@
"location": "Location",
"name": "Name",
"showLogo": "Show Logo",
"submit": "Sign PDF",
"files": {
"placeholder": "Select PDF files to sign with certificates"
},
"signMode": {
"stepTitle": "Sign Mode"
},
"certTypeStep": {
"stepTitle": "Certificate Format"
},
"certFiles": {
"stepTitle": "Certificate Files"
},
"appearance": {
"stepTitle": "Signature Appearance",
"title": "Signature Appearance",
"invisible": "Invisible",
"visible": "Visible",
"options": {
"title": "Signature Details"
}
},
"sign": {
"submit": "Sign PDF",
"results": "Signed PDF"
},
"error": {
"failed": "An error occurred whilst signing the PDF."
},
"choosePrivateKey": "Choose Private Key File",
"chooseCertificate": "Choose Certificate File",
"chooseP12File": "Choose PKCS12 File",
"choosePfxFile": "Choose PFX File",
"chooseJksFile": "Choose JKS File",
"passwordOptional": "Leave empty if no password",
"serverCertMessage": "Using server certificate - no files or password required",
"pageNumber": "Page Number",
"logoTitle": "Logo",
"noLogo": "No Logo"
},
"manageSignatures": {
"tags": "sign,certificate,PEM,PKCS12,JKS,server,manual,auto",
"title": "Manage Signatures",
"desc": "Sign PDFs with certificates using manual or server-managed keys",
"signMode": {
"tooltip": {
"header": {
"title": "About PDF Signatures"
},
"overview": {
"title": "How signatures work",
"text": "Both modes seal the document (any edits are flagged as tampering) and record who/when/how for auditing. Viewer trust depends on the certificate chain."
},
"manual": {
"title": "Manual - Bring your certificate",
"text": "Use your own certificate files for brand-aligned identity. Can display <b>Trusted</b> when your CA/chain is recognised.",
"use": "Use for: customer-facing, legal, compliance."
},
"auto": {
"title": "Auto - Zero-setup, instant system seal",
"text": "Signs with a server <b>self-signed</b> certificate. Same <b>tamper-evident seal</b> and <b>audit trail</b>; typically shows <b>Unverified</b> in viewers.",
"use": "Use when: you need speed and consistent internal identity across reviews and records."
},
"rule": {
"title": "Rule of thumb",
"text": "Need recipient <b>Trusted</b> status? <b>Manual</b>. Need a fast, tamper-evident seal and audit trail with no setup? <b>Auto</b>."
}
}
},
"certType": {
"tooltip": {
"header": {
"title": "About Certificate Types"
},
"what": {
"title": "What's a certificate?",
"text": "It's a secure ID for your signature that proves you signed. Unless you're required to sign via certificate, we recommend using another secure method like Type, Draw, or Upload."
},
"which": {
"title": "Which option should I use?",
"text": "Choose the format that matches your certificate file:",
"bullet1": "PKCS12 (.p12) one combined file (most common)",
"bullet2": "PFX (.pfx) Microsoft's version of PKCS12",
"bullet3": "PEM separate private-key and certificate .pem files",
"bullet4": "JKS Java .jks keystore for dev / CI-CD workflows"
},
"convert": {
"title": "Key not listed?",
"text": "Convert your file to a Java keystore (.jks) with keytool, then pick JKS."
}
}
}
"submit": "Sign PDF"
},
"removeCertSign": {
"tags": "authenticate,PEM,P12,official,decrypt",
@ -1679,18 +1576,6 @@
"addBorder": "Add Borders",
"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": {
"title": "Adjust page-scale",
"header": "Adjust page-scale",
@ -2807,146 +2692,5 @@
"processImages": "Process Images",
"processImagesDesc": "Converts multiple image files into a single PDF document, then applies OCR technology to extract searchable text from the images."
}
},
"manageSignatures": {
"title": "Sign with Certificate",
"filenamePrefix": "signed",
"files": {
"placeholder": "Select PDF files to sign with certificates"
},
"fileStatus": {
"stepTitle": "File Status"
},
"fileNavigation": "File {{current}} of {{total}}",
"hasSignatures": "Contains {{count}} signature(s)",
"noSignatures": "No signatures detected",
"signed": "Signed",
"certType": {
"stepTitle": "Certificate Type",
"tooltip": {
"header": {
"title": "About Certificate Types"
},
"what": {
"title": "What's a certificate?",
"text": "It's a secure ID for your signature that proves you signed. Unless you're required to sign via certificate, we recommend using another secure method like Type, Draw, or Upload."
},
"which": {
"title": "Which option should I use?",
"text": "Choose the format that matches your certificate file:",
"bullet1": "PKCS#12 (.p12 / .pfx) one combined file (most common)",
"bullet2": "PEM separate private-key and certificate .pem files",
"bullet3": "JKS Java .jks keystore for dev / CI-CD workflows"
},
"convert": {
"title": "Key not listed?",
"text": "Convert your file to a Java keystore (.jks) with keytool, then pick JKS."
}
}
},
"certFiles": {
"stepTitle": "Certificate Files"
},
"appearance": {
"stepTitle": "Signature Appearance",
"title": "Signature Appearance",
"invisible": "Invisible",
"visible": "Visible",
"options": {
"title": "Signature Details"
},
"tooltip": {
"header": {
"title": "About Signature Appearance"
},
"invisible": {
"title": "Invisible Signatures",
"text": "The signature is added to the PDF for security but won't be visible when viewing the document. Perfect for legal requirements without changing the document's appearance.",
"bullet1": "Provides security without visual changes",
"bullet2": "Meets legal requirements for digital signing",
"bullet3": "Doesn't affect document layout or design"
},
"visible": {
"title": "Visible Signatures",
"text": "Shows a signature block on the PDF with your name, date, and optional details. Useful when you want readers to clearly see the document is signed.",
"bullet1": "Shows signer name and date on the document",
"bullet2": "Can include reason and location for signing",
"bullet3": "Choose which page to place the signature",
"bullet4": "Optional logo can be included"
}
}
},
"mode": {
"title": "Action",
"validate": "Check for Signatures",
"viewEdit": "View/Edit Signatures",
"sign": "Add New Signature"
},
"validation": {
"title": "Validation Options",
"customCert": "Custom Certificate (Optional)",
"customCert.desc": "Upload a custom certificate for validation"
},
"signing": {
"title": "Certificate Settings",
"certType": "Certificate Type",
"choosePrivateKey": "Choose Private Key File",
"chooseCertificate": "Choose Certificate File",
"chooseP12File": "Choose PKCS12 File",
"chooseJksFile": "Choose JKS File",
"password": "Certificate Password",
"passwordOptional": "Leave empty if no password",
"showSignature": "Show visible signature on PDF",
"reason": "Reason for Signing",
"location": "Location",
"name": "Signer Name",
"pageNumber": "Page Number",
"logoTitle": "Logo",
"noLogo": "No Logo",
"showLogo": "Show Logo"
},
"validate": {
"submit": "Validate Signatures",
"results": "Signature Validation Results"
},
"sign": {
"submit": "Sign PDF",
"results": "Signed PDF"
},
"results": {
"title": "Signature Results"
},
"error": {
"failed": "An error occurred whilst processing signatures."
},
"tooltip": {
"header": {
"title": "About Managing Signatures"
},
"overview": {
"title": "What can this tool do?",
"text": "This tool lets you check if your PDFs are digitally signed and add new digital signatures. Digital signatures prove who created or approved a document and show if it has been changed since signing.",
"bullet1": "Check existing signatures and their validity",
"bullet2": "View detailed information about signers and certificates",
"bullet3": "Add new digital signatures to secure your documents",
"bullet4": "Multiple files supported with easy navigation"
},
"validation": {
"title": "Checking Signatures",
"text": "When you check signatures, the tool tells you if they're valid, who signed the document, when it was signed, and whether the document has been changed since signing.",
"bullet1": "Shows if signatures are valid or invalid",
"bullet2": "Displays signer information and signing date",
"bullet3": "Checks if the document was modified after signing",
"bullet4": "Can use custom certificates for verification"
},
"signing": {
"title": "Adding Signatures",
"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.",
"bullet1": "Supports PEM, PKCS12, and JKS certificate formats",
"bullet2": "Option to show or hide signature on the PDF",
"bullet3": "Add reason, location, and signer name",
"bullet4": "Choose which page to place visible signatures"
}
}
}
}

View File

@ -495,10 +495,6 @@
"title": "Multi-Page Layout",
"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": {
"title": "Adjust page size/scale",
"desc": "Change the size/scale of a page and/or its contents."
@ -1101,18 +1097,6 @@
"addBorder": "Add Borders",
"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": {
"tags": "resize,modify,dimension,adapt",
"title": "Adjust page-scale",

View File

@ -1,149 +0,0 @@
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;

View File

@ -1,95 +0,0 @@
import { Stack, Text, TextInput } from "@mantine/core";
import { useTranslation } from "react-i18next";
import { ManageSignaturesParameters } from "../../../hooks/tools/manageSignatures/useManageSignaturesParameters";
import FileUploadButton from "../../shared/FileUploadButton";
interface CertificateFilesSettingsProps {
parameters: ManageSignaturesParameters;
onParameterChange: (key: keyof ManageSignaturesParameters, value: any) => void;
disabled?: boolean;
}
const CertificateFilesSettings = ({ parameters, onParameterChange, disabled = false }: CertificateFilesSettingsProps) => {
const { t } = useTranslation();
return (
<Stack gap="md">
{/* Certificate Files based on type */}
{parameters.certType === 'PEM' && (
<Stack gap="sm">
<FileUploadButton
file={parameters.privateKeyFile}
onChange={(file) => onParameterChange('privateKeyFile', file || undefined)}
accept=".pem,.der,.key"
disabled={disabled}
placeholder={t('certSign.choosePrivateKey', 'Choose Private Key File')}
/>
{parameters.privateKeyFile && (
<FileUploadButton
file={parameters.certFile}
onChange={(file) => onParameterChange('certFile', file || undefined)}
accept=".pem,.der,.crt,.cer"
disabled={disabled}
placeholder={t('certSign.chooseCertificate', 'Choose Certificate File')}
/>
)}
</Stack>
)}
{parameters.certType === 'PKCS12' && (
<FileUploadButton
file={parameters.p12File}
onChange={(file) => onParameterChange('p12File', file || undefined)}
accept=".p12"
disabled={disabled}
placeholder={t('certSign.chooseP12File', 'Choose PKCS12 File')}
/>
)}
{parameters.certType === 'PFX' && (
<FileUploadButton
file={parameters.p12File}
onChange={(file) => onParameterChange('p12File', file || undefined)}
accept=".pfx"
disabled={disabled}
placeholder={t('certSign.choosePfxFile', 'Choose PFX File')}
/>
)}
{parameters.certType === 'JKS' && (
<FileUploadButton
file={parameters.jksFile}
onChange={(file) => onParameterChange('jksFile', file || undefined)}
accept=".jks,.keystore"
disabled={disabled}
placeholder={t('certSign.chooseJksFile', 'Choose JKS File')}
/>
)}
{parameters.certType === 'SERVER' && (
<Text c="dimmed" size="sm">
{t('certSign.serverCertMessage', 'Using server certificate - no files or password required')}
</Text>
)}
{/* Password - only show when files are uploaded */}
{parameters.certType && (
(parameters.certType === 'PEM' && parameters.privateKeyFile && parameters.certFile) ||
(parameters.certType === 'PKCS12' && parameters.p12File) ||
(parameters.certType === 'PFX' && parameters.p12File) ||
(parameters.certType === 'JKS' && parameters.jksFile)
) && (
<TextInput
label={t('certSign.password', 'Certificate Password')}
placeholder={t('certSign.passwordOptional', 'Leave empty if no password')}
type="password"
value={parameters.password}
onChange={(event) => onParameterChange('password', event.currentTarget.value)}
disabled={disabled}
/>
)}
</Stack>
);
};
export default CertificateFilesSettings;

View File

@ -1,72 +0,0 @@
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 PFX */}
<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' }}>
PKCS12
</div>
</Button>
<Button
variant={parameters.certType === 'PFX' ? 'filled' : 'outline'}
color={parameters.certType === 'PFX' ? 'blue' : 'var(--text-muted)'}
onClick={() => onParameterChange('certType', 'PFX')}
disabled={disabled}
style={{ flex: 1, height: 'auto', minHeight: '40px', fontSize: '11px' }}
>
<div style={{ textAlign: 'center', lineHeight: '1.1', fontSize: '11px' }}>
PFX
</div>
</Button>
</div>
{/* Second row - PEM and JKS */}
<div style={{ display: 'flex', gap: '4px' }}>
<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
</div>
</Button>
<Button
variant={parameters.certType === 'JKS' ? 'filled' : 'outline'}
color={parameters.certType === 'JKS' ? 'blue' : 'var(--text-muted)'}
onClick={() => onParameterChange('certType', 'JKS')}
disabled={disabled}
style={{ flex: 1, height: 'auto', minHeight: '40px', fontSize: '11px' }}
>
<div style={{ textAlign: 'center', lineHeight: '1.1', fontSize: '11px' }}>
JKS
</div>
</Button>
</div>
</div>
</Stack>
);
};
export default CertificateFormatSettings;

View File

@ -1,54 +0,0 @@
import { Stack, Button, Text, Divider } from "@mantine/core";
import { useTranslation } from "react-i18next";
import { ManageSignaturesParameters } from "../../../hooks/tools/manageSignatures/useManageSignaturesParameters";
interface CertificateTypeSettingsProps {
parameters: ManageSignaturesParameters;
onParameterChange: (key: keyof ManageSignaturesParameters, value: any) => void;
disabled?: boolean;
}
const CertificateTypeSettings = ({ parameters, onParameterChange, disabled = false }: CertificateTypeSettingsProps) => {
const { t } = useTranslation();
return (
<Stack gap="md">
<div style={{ display: 'flex', gap: '4px' }}>
<Button
variant={parameters.signMode === 'MANUAL' ? 'filled' : 'outline'}
color={parameters.signMode === 'MANUAL' ? 'blue' : 'var(--text-muted)'}
onClick={() => {
onParameterChange('signMode', 'MANUAL');
// Reset cert type when switching to manual
if (parameters.signMode === 'AUTO') {
onParameterChange('certType', '');
}
}}
disabled={disabled}
style={{ flex: 1, height: 'auto', minHeight: '40px', fontSize: '11px' }}
>
<div style={{ textAlign: 'center', lineHeight: '1.1', fontSize: '11px' }}>
Manual
</div>
</Button>
<Button
variant={parameters.signMode === 'AUTO' ? 'filled' : 'outline'}
color={parameters.signMode === 'AUTO' ? 'green' : 'var(--text-muted)'}
onClick={() => {
onParameterChange('signMode', 'AUTO');
// Clear cert type and files when switching to auto
onParameterChange('certType', '');
}}
disabled={disabled}
style={{ flex: 1, height: 'auto', minHeight: '40px', fontSize: '11px' }}
>
<div style={{ textAlign: 'center', lineHeight: '1.1', fontSize: '11px' }}>
Auto
</div>
</Button>
</div>
</Stack>
);
};
export default CertificateTypeSettings;

View File

@ -1,110 +0,0 @@
import { Stack, Text, Button, TextInput, NumberInput } from "@mantine/core";
import { useTranslation } from "react-i18next";
import { ManageSignaturesParameters } from "../../../hooks/tools/manageSignatures/useManageSignaturesParameters";
interface SignatureAppearanceSettingsProps {
parameters: ManageSignaturesParameters;
onParameterChange: (key: keyof ManageSignaturesParameters, value: any) => void;
disabled?: boolean;
}
const SignatureAppearanceSettings = ({ parameters, onParameterChange, disabled = false }: SignatureAppearanceSettingsProps) => {
const { t } = useTranslation();
return (
<Stack gap="md">
{/* Signature Visibility */}
<Stack gap="sm">
<div style={{ display: 'flex', gap: '4px' }}>
<Button
variant={!parameters.showSignature ? 'filled' : 'outline'}
color={!parameters.showSignature ? 'blue' : 'var(--text-muted)'}
onClick={() => onParameterChange('showSignature', false)}
disabled={disabled}
style={{ flex: 1, height: 'auto', minHeight: '40px', fontSize: '11px' }}
>
<div style={{ textAlign: 'center', lineHeight: '1.1', fontSize: '11px' }}>
{t('certSign.appearance.invisible', 'Invisible')}
</div>
</Button>
<Button
variant={parameters.showSignature ? 'filled' : 'outline'}
color={parameters.showSignature ? 'blue' : 'var(--text-muted)'}
onClick={() => onParameterChange('showSignature', true)}
disabled={disabled}
style={{ flex: 1, height: 'auto', minHeight: '40px', fontSize: '11px' }}
>
<div style={{ textAlign: 'center', lineHeight: '1.1', fontSize: '11px' }}>
{t('certSign.appearance.visible', 'Visible')}
</div>
</Button>
</div>
</Stack>
{/* Visible Signature Options */}
{parameters.showSignature && (
<Stack gap="sm">
<Text size="sm" fw={500}>
{t('certSign.appearance.options.title', 'Signature Details')}
</Text>
<TextInput
label={t('certSign.reason', 'Reason')}
value={parameters.reason}
onChange={(event) => onParameterChange('reason', event.currentTarget.value)}
disabled={disabled}
/>
<TextInput
label={t('certSign.location', 'Location')}
value={parameters.location}
onChange={(event) => onParameterChange('location', event.currentTarget.value)}
disabled={disabled}
/>
<TextInput
label={t('certSign.name', 'Name')}
value={parameters.name}
onChange={(event) => onParameterChange('name', event.currentTarget.value)}
disabled={disabled}
/>
<NumberInput
label={t('certSign.pageNumber', 'Page Number')}
value={parameters.pageNumber}
onChange={(value) => onParameterChange('pageNumber', value || 1)}
min={1}
disabled={disabled}
/>
<Stack gap="xs">
<Text size="sm" fw={500}>
{t('certSign.logoTitle', 'Logo')}
</Text>
<div style={{ display: 'flex', gap: '4px' }}>
<Button
variant={!parameters.showLogo ? 'filled' : 'outline'}
color={!parameters.showLogo ? 'blue' : 'var(--text-muted)'}
onClick={() => onParameterChange('showLogo', false)}
disabled={disabled}
style={{ flex: 1, height: 'auto', minHeight: '40px', fontSize: '11px' }}
>
<div style={{ textAlign: 'center', lineHeight: '1.1', fontSize: '11px' }}>
{t('certSign.noLogo', 'No Logo')}
</div>
</Button>
<Button
variant={parameters.showLogo ? 'filled' : 'outline'}
color={parameters.showLogo ? 'blue' : 'var(--text-muted)'}
onClick={() => onParameterChange('showLogo', true)}
disabled={disabled}
style={{ flex: 1, height: 'auto', minHeight: '40px', fontSize: '11px' }}
>
<div style={{ textAlign: 'center', lineHeight: '1.1', fontSize: '11px' }}>
{t('certSign.showLogo', 'Show Logo')}
</div>
</Button>
</div>
</Stack>
</Stack>
)}
</Stack>
);
};
export default SignatureAppearanceSettings;

View File

@ -1,32 +0,0 @@
import { useTranslation } from 'react-i18next';
import { TooltipContent } from '../../types/tips';
export const useCertificateTypeTips = (): TooltipContent => {
const { t } = useTranslation();
return {
header: {
title: t("manageSignatures.certType.tooltip.header.title", "About Certificate Types")
},
tips: [
{
title: t("manageSignatures.certType.tooltip.what.title", "What's a certificate?"),
description: t("manageSignatures.certType.tooltip.what.text", "It's a secure ID for your signature that proves you signed. Unless you're required to sign via certificate, we recommend using another secure method like Type, Draw, or Upload.")
},
{
title: t("manageSignatures.certType.tooltip.which.title", "Which option should I use?"),
description: t("manageSignatures.certType.tooltip.which.text", "Choose the format that matches your certificate file:"),
bullets: [
t("manageSignatures.certType.tooltip.which.bullet1", "PKCS12 (.p12) one combined file (most common)"),
t("manageSignatures.certType.tooltip.which.bullet2", "PFX (.pfx) Microsoft's version of PKCS12"),
t("manageSignatures.certType.tooltip.which.bullet3", "PEM separate private-key and certificate .pem files"),
t("manageSignatures.certType.tooltip.which.bullet4", "JKS Java .jks keystore for dev / CI-CD workflows")
]
},
{
title: t("manageSignatures.certType.tooltip.convert.title", "Key not listed?"),
description: t("manageSignatures.certType.tooltip.convert.text", "Convert your file to a Java keystore (.jks) with keytool, then pick JKS.")
}
]
};
};

View File

@ -1,45 +0,0 @@
import { useTranslation } from 'react-i18next';
import { TooltipContent } from '../../types/tips';
export const useManageSignaturesTooltips = (): TooltipContent => {
const { t } = useTranslation();
return {
header: {
title: t("manageSignatures.tooltip.header.title", "About Managing Signatures")
},
tips: [
{
title: t("manageSignatures.tooltip.overview.title", "What can this tool do?"),
description: t("manageSignatures.tooltip.overview.text", "This tool lets you check if your PDFs are digitally signed and add new digital signatures. Digital signatures prove who created or approved a document and show if it has been changed since signing."),
bullets: [
t("manageSignatures.tooltip.overview.bullet1", "Check existing signatures and their validity"),
t("manageSignatures.tooltip.overview.bullet2", "View detailed information about signers and certificates"),
t("manageSignatures.tooltip.overview.bullet3", "Add new digital signatures to secure your documents"),
t("manageSignatures.tooltip.overview.bullet4", "Multiple files supported with easy navigation")
]
},
{
title: t("manageSignatures.tooltip.validation.title", "Checking Signatures"),
description: t("manageSignatures.tooltip.validation.text", "When you check signatures, the tool tells you if they're valid, who signed the document, when it was signed, and whether the document has been changed since signing."),
bullets: [
t("manageSignatures.tooltip.validation.bullet1", "Shows if signatures are valid or invalid"),
t("manageSignatures.tooltip.validation.bullet2", "Displays signer information and signing date"),
t("manageSignatures.tooltip.validation.bullet3", "Checks if the document was modified after signing"),
t("manageSignatures.tooltip.validation.bullet4", "Can use custom certificates for verification")
]
},
{
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."),
bullets: [
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.bullet3", "Add reason, location, and signer name"),
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")
]
}
]
};
};

View File

@ -1,36 +0,0 @@
import { useTranslation } from 'react-i18next';
import { TooltipContent } from '../../types/tips';
export const useSignModeTips = (): TooltipContent => {
const { t } = useTranslation();
return {
header: {
title: t("manageSignatures.signMode.tooltip.header.title", "About PDF Signatures")
},
tips: [
{
title: t("manageSignatures.signMode.tooltip.overview.title", "How signatures work"),
description: t("manageSignatures.signMode.tooltip.overview.text", "Both modes seal the document (any edits are flagged as tampering) and record who/when/how for auditing. Viewer trust depends on the certificate chain.")
},
{
title: t("manageSignatures.signMode.tooltip.manual.title", "Manual - Bring your certificate"),
description: t("manageSignatures.signMode.tooltip.manual.text", "Use your own certificate files for brand-aligned identity. Can display <b>Trusted</b> when your CA/chain is recognized."),
bullets: [
t("manageSignatures.signMode.tooltip.manual.use", "Use for: customer-facing, legal, compliance.")
]
},
{
title: t("manageSignatures.signMode.tooltip.auto.title", "Auto - Zero-setup, instant system seal"),
description: t("manageSignatures.signMode.tooltip.auto.text", "Signs with a server <b>self-signed</b> certificate. Same <b>tamper-evident seal</b> and <b>audit trail</b>; typically shows <b>Unverified</b> in viewers."),
bullets: [
t("manageSignatures.signMode.tooltip.auto.use", "Use when: you need speed and consistent internal identity across reviews and records.")
]
},
{
title: t("manageSignatures.signMode.tooltip.rule.title", "Rule of thumb"),
description: t("manageSignatures.signMode.tooltip.rule.text", "Need recipient <b>Trusted</b> status? <b>Manual</b>. Need a fast, tamper-evident seal and audit trail with no setup? <b>Auto</b>.")
}
]
};
};

View File

@ -1,33 +0,0 @@
import { useTranslation } from 'react-i18next';
import { TooltipContent } from '../../types/tips';
export const useSignatureAppearanceTips = (): TooltipContent => {
const { t } = useTranslation();
return {
header: {
title: t("manageSignatures.appearance.tooltip.header.title", "About Signature Appearance")
},
tips: [
{
title: t("manageSignatures.appearance.tooltip.invisible.title", "Invisible Signatures"),
description: t("manageSignatures.appearance.tooltip.invisible.text", "The signature is added to the PDF for security but won't be visible when viewing the document. Perfect for legal requirements without changing the document's appearance."),
bullets: [
t("manageSignatures.appearance.tooltip.invisible.bullet1", "Provides security without visual changes"),
t("manageSignatures.appearance.tooltip.invisible.bullet2", "Meets legal requirements for digital signing"),
t("manageSignatures.appearance.tooltip.invisible.bullet3", "Doesn't affect document layout or design")
]
},
{
title: t("manageSignatures.appearance.tooltip.visible.title", "Visible Signatures"),
description: t("manageSignatures.appearance.tooltip.visible.text", "Shows a signature block on the PDF with your name, date, and optional details. Useful when you want readers to clearly see the document is signed."),
bullets: [
t("manageSignatures.appearance.tooltip.visible.bullet1", "Shows signer name and date on the document"),
t("manageSignatures.appearance.tooltip.visible.bullet2", "Can include reason and location for signing"),
t("manageSignatures.appearance.tooltip.visible.bullet3", "Choose which page to place the signature"),
t("manageSignatures.appearance.tooltip.visible.bullet4", "Optional logo can be included")
]
}
]
};
};

View File

@ -17,8 +17,6 @@ import AutoRename from "../tools/AutoRename";
import SingleLargePage from "../tools/SingleLargePage";
import UnlockPdfForms from "../tools/UnlockPdfForms";
import RemoveCertificateSign from "../tools/RemoveCertificateSign";
import ManageSignatures from "../tools/ManageSignatures";
import BookletImposition from "../tools/BookletImposition";
import Flatten from "../tools/Flatten";
import { compressOperationConfig } from "../hooks/tools/compress/useCompressOperation";
import { splitOperationConfig } from "../hooks/tools/split/useSplitOperation";
@ -33,8 +31,6 @@ import { ocrOperationConfig } from "../hooks/tools/ocr/useOCROperation";
import { convertOperationConfig } from "../hooks/tools/convert/useConvertOperation";
import { removeCertificateSignOperationConfig } from "../hooks/tools/removeCertificateSign/useRemoveCertificateSignOperation";
import { changePermissionsOperationConfig } from "../hooks/tools/changePermissions/useChangePermissionsOperation";
import { manageSignaturesOperationConfig } from "../hooks/tools/manageSignatures/useManageSignaturesOperation";
import { bookletImpositionOperationConfig } from "../hooks/tools/bookletImposition/useBookletImpositionOperation";
import { mergeOperationConfig } from '../hooks/tools/merge/useMergeOperation';
import { autoRenameOperationConfig } from "../hooks/tools/autoRename/useAutoRenameOperation";
import { flattenOperationConfig } from "../hooks/tools/flatten/useFlattenOperation";
@ -50,8 +46,6 @@ import AddWatermarkSingleStepSettings from "../components/tools/addWatermark/Add
import OCRSettings from "../components/tools/ocr/OCRSettings";
import ConvertSettings from "../components/tools/convert/ConvertSettings";
import ChangePermissionsSettings from "../components/tools/changePermissions/ChangePermissionsSettings";
import CertificateTypeSettings from "../components/tools/manageSignatures/CertificateTypeSettings";
import BookletImpositionSettings from "../components/tools/bookletImposition/BookletImpositionSettings";
import FlattenSettings from "../components/tools/flatten/FlattenSettings";
import RedactSingleStepSettings from "../components/tools/redact/RedactSingleStepSettings";
import Redact from "../tools/Redact";
@ -154,15 +148,11 @@ export function useFlatToolRegistry(): ToolRegistry {
certSign: {
icon: <LocalIcon icon="workspace-premium-rounded" width="1.5rem" height="1.5rem" />,
name: t("home.certSign.title", "Certificate Sign"),
component: ManageSignatures,
description: t("home.certSign.desc", "Sign PDF documents using digital certificates"),
name: t("home.certSign.title", "Sign with Certificate"),
component: null,
description: t("home.certSign.desc", "Signs a PDF with a Certificate/Key (PEM/P12)"),
categoryId: ToolCategoryId.STANDARD_TOOLS,
subcategoryId: SubcategoryId.SIGNING,
maxFiles: -1,
endpoints: ["cert-sign"],
operationConfig: manageSignaturesOperationConfig,
settingsComponent: CertificateTypeSettings,
},
sign: {
icon: <LocalIcon icon="signature-rounded" width="1.5rem" height="1.5rem" />,
@ -268,14 +258,6 @@ export function useFlatToolRegistry(): ToolRegistry {
},
// Verification
"validate-pdf-signature": {
icon: <LocalIcon icon="verified-rounded" width="1.5rem" height="1.5rem" />,
name: t("home.validateSignature.title", "Validate PDF Signature"),
component: null,
description: t("home.validateSignature.desc", "Verify digital signatures and certificates in PDF documents"),
categoryId: ToolCategoryId.STANDARD_TOOLS,
subcategoryId: SubcategoryId.VERIFICATION,
},
"get-all-info-on-pdf": {
icon: <LocalIcon icon="fact-check-rounded" width="1.5rem" height="1.5rem" />,
name: t("home.getPdfInfo.title", "Get ALL Info on PDF"),
@ -284,6 +266,14 @@ export function useFlatToolRegistry(): ToolRegistry {
categoryId: ToolCategoryId.STANDARD_TOOLS,
subcategoryId: SubcategoryId.VERIFICATION,
},
"validate-pdf-signature": {
icon: <LocalIcon icon="verified-rounded" width="1.5rem" height="1.5rem" />,
name: t("home.validateSignature.title", "Validate PDF Signature"),
component: null,
description: t("home.validateSignature.desc", "Verify digital signatures and certificates in PDF documents"),
categoryId: ToolCategoryId.STANDARD_TOOLS,
subcategoryId: SubcategoryId.VERIFICATION,
},
// Document Review
@ -377,16 +367,6 @@ export function useFlatToolRegistry(): ToolRegistry {
categoryId: ToolCategoryId.STANDARD_TOOLS,
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": {
icon: <LocalIcon icon="looks-one-outline-rounded" width="1.5rem" height="1.5rem" />,
name: t("home.pdfToSinglePage.title", "PDF to Single Large Page"),

View File

@ -1,34 +0,0 @@
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.'))
});
};

View File

@ -1,28 +0,0 @@
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;
},
});
};

View File

@ -1,73 +0,0 @@
import { useTranslation } from 'react-i18next';
import { ToolType, useToolOperation } from '../shared/useToolOperation';
import { createStandardErrorHandler } from '../../../utils/toolErrorHandler';
import { ManageSignaturesParameters, defaultParameters } from './useManageSignaturesParameters';
// Build form data for signing
export const buildManageSignaturesFormData = (parameters: ManageSignaturesParameters, file: File): FormData => {
const formData = new FormData();
formData.append('fileInput', file);
// Handle sign mode
if (parameters.signMode === 'AUTO') {
formData.append('certType', 'SERVER');
} else {
formData.append('certType', parameters.certType);
formData.append('password', parameters.password);
// Add certificate files based on type (only for manual mode)
switch (parameters.certType) {
case 'PEM':
if (parameters.privateKeyFile) {
formData.append('privateKeyFile', parameters.privateKeyFile);
}
if (parameters.certFile) {
formData.append('certFile', parameters.certFile);
}
break;
case 'PKCS12':
if (parameters.p12File) {
formData.append('p12File', parameters.p12File);
}
break;
case 'JKS':
if (parameters.jksFile) {
formData.append('jksFile', parameters.jksFile);
}
break;
}
}
// Add signature appearance options if enabled
if (parameters.showSignature) {
formData.append('showSignature', 'true');
formData.append('reason', parameters.reason);
formData.append('location', parameters.location);
formData.append('name', parameters.name);
formData.append('pageNumber', parameters.pageNumber.toString());
formData.append('showLogo', parameters.showLogo.toString());
}
return formData;
};
// Static configuration object
export const manageSignaturesOperationConfig = {
toolType: ToolType.singleFile,
buildFormData: buildManageSignaturesFormData,
operationType: 'manageSignatures',
endpoint: '/api/v1/security/cert-sign',
filePrefix: 'signed_', // Will be overridden in hook with translation
multiFileEndpoint: false,
defaultParameters,
} as const;
export const useManageSignaturesOperation = () => {
const { t } = useTranslation();
return useToolOperation<ManageSignaturesParameters>({
...manageSignaturesOperationConfig,
filePrefix: t('manageSignatures.filenamePrefix', 'signed') + '_',
getErrorMessage: createStandardErrorHandler(t('manageSignatures.error.failed', 'An error occurred while processing signatures.'))
});
};

View File

@ -1,67 +0,0 @@
import { BaseParameters } from '../../../types/parameters';
import { useBaseParameters, BaseParametersHook } from '../shared/useBaseParameters';
export interface ManageSignaturesParameters extends BaseParameters {
// Sign mode selection
signMode: 'MANUAL' | 'AUTO';
// Certificate signing options (only for manual mode)
certType: '' | 'PEM' | 'PKCS12' | 'PFX' | 'JKS';
privateKeyFile?: File;
certFile?: File;
p12File?: File;
jksFile?: File;
password: string;
// Signature appearance options
showSignature: boolean;
reason: string;
location: string;
name: string;
pageNumber: number;
showLogo: boolean;
}
export const defaultParameters: ManageSignaturesParameters = {
signMode: 'MANUAL',
certType: '',
password: '',
showSignature: false,
reason: '',
location: '',
name: '',
pageNumber: 1,
showLogo: true,
};
export type ManageSignaturesParametersHook = BaseParametersHook<ManageSignaturesParameters>;
export const useManageSignaturesParameters = (): ManageSignaturesParametersHook => {
return useBaseParameters({
defaultParameters,
endpointName: 'manage-signatures',
validateFn: (params) => {
// Auto mode (server certificate) - no additional validation needed
if (params.signMode === 'AUTO') {
return true;
}
// Manual mode - requires certificate type and files
if (!params.certType) {
return false;
}
// Check for required files based on cert type
switch (params.certType) {
case 'PEM':
return !!(params.privateKeyFile && params.certFile);
case 'PKCS12':
case 'PFX':
return !!params.p12File;
case 'JKS':
return !!params.jksFile;
default:
return false;
}
},
});
};

View File

@ -1,140 +0,0 @@
/**
* Service for detecting signatures in PDF files using PDF.js
* This provides a quick client-side check to determine if a PDF contains signatures
* without needing to make API calls
*/
// PDF.js types (simplified)
declare global {
interface Window {
pdfjsLib?: any;
}
}
export interface SignatureDetectionResult {
hasSignatures: boolean;
signatureCount?: number;
error?: string;
}
export interface FileSignatureStatus {
file: File;
result: SignatureDetectionResult;
}
/**
* Detect signatures in a single PDF file using PDF.js
*/
const detectSignaturesInFile = async (file: File): Promise<SignatureDetectionResult> => {
try {
// Ensure PDF.js is available
if (!window.pdfjsLib) {
return {
hasSignatures: false,
error: 'PDF.js not available'
};
}
// Convert file to ArrayBuffer
const arrayBuffer = await file.arrayBuffer();
// Load the PDF document
const pdf = await window.pdfjsLib.getDocument({ data: arrayBuffer }).promise;
let totalSignatures = 0;
// Check each page for signature annotations
for (let pageNum = 1; pageNum <= pdf.numPages; pageNum++) {
const page = await pdf.getPage(pageNum);
const annotations = await page.getAnnotations();
// Count signature annotations (Type: /Sig)
const signatureAnnotations = annotations.filter((annotation: any) =>
annotation.subtype === 'Widget' &&
annotation.fieldType === 'Sig'
);
totalSignatures += signatureAnnotations.length;
}
// Also check for document-level signatures in AcroForm
const metadata = await pdf.getMetadata();
if (metadata?.info?.Signature || metadata?.metadata?.has('dc:signature')) {
totalSignatures = Math.max(totalSignatures, 1);
}
// Clean up PDF.js document
pdf.destroy();
return {
hasSignatures: totalSignatures > 0,
signatureCount: totalSignatures
};
} catch (error) {
console.warn('PDF signature detection failed:', error);
return {
hasSignatures: false,
signatureCount: 0,
error: error instanceof Error ? error.message : 'Detection failed'
};
}
};
/**
* Detect if PDF files contain signatures using PDF.js client-side processing
*/
export const detectSignaturesInFiles = async (files: File[]): Promise<FileSignatureStatus[]> => {
const results: FileSignatureStatus[] = [];
for (const file of files) {
const result = await detectSignaturesInFile(file);
results.push({ file, result });
}
return results;
};
/**
* Hook for managing signature detection state
*/
export const useSignatureDetection = () => {
const [detectionResults, setDetectionResults] = React.useState<FileSignatureStatus[]>([]);
const [isDetecting, setIsDetecting] = React.useState(false);
const detectSignatures = async (files: File[]) => {
if (files.length === 0) {
setDetectionResults([]);
return;
}
setIsDetecting(true);
try {
const results = await detectSignaturesInFiles(files);
setDetectionResults(results);
} finally {
setIsDetecting(false);
}
};
const getFileSignatureStatus = (file: File): SignatureDetectionResult | null => {
const result = detectionResults.find(r => r.file === file);
return result ? result.result : null;
};
const hasAnySignatures = detectionResults.some(r => r.result.hasSignatures);
const totalSignatures = detectionResults.reduce((sum, r) => sum + (r.result.signatureCount || 0), 0);
return {
detectionResults,
isDetecting,
detectSignatures,
getFileSignatureStatus,
hasAnySignatures,
totalSignatures,
reset: () => setDetectionResults([])
};
};
// Import React for the hook
import React from 'react';

View File

@ -1,56 +0,0 @@
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;

View File

@ -1,132 +0,0 @@
import { useTranslation } from "react-i18next";
import { createToolFlow } from "../components/tools/shared/createToolFlow";
import CertificateTypeSettings from "../components/tools/manageSignatures/CertificateTypeSettings";
import CertificateFormatSettings from "../components/tools/manageSignatures/CertificateFormatSettings";
import CertificateFilesSettings from "../components/tools/manageSignatures/CertificateFilesSettings";
import SignatureAppearanceSettings from "../components/tools/manageSignatures/SignatureAppearanceSettings";
import { useManageSignaturesParameters } from "../hooks/tools/manageSignatures/useManageSignaturesParameters";
import { useManageSignaturesOperation } from "../hooks/tools/manageSignatures/useManageSignaturesOperation";
import { useCertificateTypeTips } from "../components/tooltips/useCertificateTypeTips";
import { useSignatureAppearanceTips } from "../components/tooltips/useSignatureAppearanceTips";
import { useSignModeTips } from "../components/tooltips/useSignModeTips";
import { useBaseTool } from "../hooks/tools/shared/useBaseTool";
import { BaseToolProps, ToolComponent } from "../types/tool";
const ManageSignatures = (props: BaseToolProps) => {
const { t } = useTranslation();
const base = useBaseTool(
'manageSignatures',
useManageSignaturesParameters,
useManageSignaturesOperation,
props
);
const certTypeTips = useCertificateTypeTips();
const appearanceTips = useSignatureAppearanceTips();
const signModeTips = useSignModeTips();
// Check if certificate files are configured for appearance step
const areCertFilesConfigured = () => {
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) {
case 'PEM':
return !!(params.privateKeyFile && params.certFile);
case 'PKCS12':
case 'PFX':
return !!params.p12File;
case 'JKS':
return !!params.jksFile;
default:
return false;
}
};
return createToolFlow({
forceStepNumbers: true,
files: {
selectedFiles: base.selectedFiles,
isCollapsed: base.hasResults,
placeholder: t("certSign.files.placeholder", "Select PDF files to sign with certificates"),
},
steps: [
{
title: t("certSign.signMode.stepTitle", "Sign Mode"),
isCollapsed: base.settingsCollapsed,
onCollapsedClick: base.settingsCollapsed ? base.handleSettingsReset : undefined,
tooltip: signModeTips,
content: (
<CertificateTypeSettings
parameters={base.params.parameters}
onParameterChange={base.params.updateParameter}
disabled={base.endpointLoading}
/>
),
},
...(base.params.parameters.signMode === 'MANUAL' ? [{
title: t("certSign.certTypeStep.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("certSign.certFiles.stepTitle", "Certificate Files"),
isCollapsed: base.settingsCollapsed,
onCollapsedClick: base.settingsCollapsed ? base.handleSettingsReset : undefined,
content: (
<CertificateFilesSettings
parameters={base.params.parameters}
onParameterChange={base.params.updateParameter}
disabled={base.endpointLoading}
/>
),
}] : []),
{
title: t("certSign.appearance.stepTitle", "Signature Appearance"),
isCollapsed: base.settingsCollapsed || !areCertFilesConfigured(),
onCollapsedClick: (base.settingsCollapsed || !areCertFilesConfigured()) ? base.handleSettingsReset : undefined,
tooltip: appearanceTips,
content: (
<SignatureAppearanceSettings
parameters={base.params.parameters}
onParameterChange={base.params.updateParameter}
disabled={base.endpointLoading}
/>
),
},
],
executeButton: {
text: t("certSign.sign.submit", "Sign PDF"),
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("certSign.sign.results", "Signed PDF"),
onFileClick: base.handleThumbnailClick,
onUndo: base.handleUndo,
},
});
};
// Static method to get the operation hook for automation
ManageSignatures.tool = () => useManageSignaturesOperation;
export default ManageSignatures as ToolComponent;

View File

@ -7,13 +7,13 @@ const TOOL_IDS = [
'detect-split-scanned-photos',
'edit-table-of-contents',
'scanner-effect',
'auto-rename-pdf-file', 'multi-page-layout', 'booklet-imposition', 'adjust-page-size-scale', 'adjust-contrast', 'cropPdf', 'single-large-page', 'multi-tool',
'auto-rename-pdf-file', 'multi-page-layout', 'adjust-page-size-scale', 'adjust-contrast', 'cropPdf', 'single-large-page', 'multi-tool',
'repair', 'compare', 'addPageNumbers', 'redact',
'flatten', 'remove-certificate-sign',
'unlock-pdf-forms', 'compress', 'extract-page', 'reorganize-pages', 'extract-images',
'add-stamp', 'add-attachments', 'change-metadata', 'overlay-pdfs',
'manage-certificates', 'get-all-info-on-pdf', 'read', 'automate', 'replace-and-invert-color',
'show-javascript', 'dev-api', 'dev-folder-scanning', 'dev-sso-guide', 'dev-airgapped', 'validate-pdf-signature'
'manage-certificates', 'get-all-info-on-pdf', 'validate-pdf-signature', 'read', 'automate', 'replace-and-invert-color',
'show-javascript', 'dev-api', 'dev-folder-scanning', 'dev-sso-guide', 'dev-airgapped'
] as const;
// Tool identity - what PDF operation we're performing (type-safe)