mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2025-09-22 19:46:39 +00:00
remove sig stuff from this PR
This commit is contained in:
parent
32e6ec2ea9
commit
9ddcd7ff6a
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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");
|
||||
|
@ -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 {}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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 {}
|
||||
|
@ -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 {}
|
||||
|
@ -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 {}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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,
|
||||
|
@ -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";
|
||||
}
|
@ -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;
|
||||
|
||||
|
@ -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
|
||||
|
@ -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());
|
||||
}
|
||||
}
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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",
|
||||
|
@ -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;
|
@ -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;
|
@ -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;
|
@ -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;
|
@ -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;
|
@ -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.")
|
||||
}
|
||||
]
|
||||
};
|
||||
};
|
@ -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")
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
};
|
@ -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>.")
|
||||
}
|
||||
]
|
||||
};
|
||||
};
|
@ -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")
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
};
|
@ -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"),
|
||||
|
@ -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.'))
|
||||
});
|
||||
};
|
@ -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;
|
||||
},
|
||||
});
|
||||
};
|
@ -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.'))
|
||||
});
|
||||
};
|
@ -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;
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
@ -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';
|
@ -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;
|
@ -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;
|
@ -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)
|
||||
|
Loading…
x
Reference in New Issue
Block a user