mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2025-09-23 03:56:20 +00:00
Compare commits
No commits in common. "464ab0dee08c2af20f1a496e39dc8d77ebb3d421" and "b126fb3bd6fbd73f613e4d41fd0d7ee5a78aeaa7" have entirely different histories.
464ab0dee0
...
b126fb3bd6
@ -18,9 +18,7 @@ public class PDFFile {
|
|||||||
@Schema(description = "The input PDF file", format = "binary")
|
@Schema(description = "The input PDF file", format = "binary")
|
||||||
private MultipartFile fileInput;
|
private MultipartFile fileInput;
|
||||||
|
|
||||||
@Schema(
|
@Schema(description = "File ID for server-side files (can be used instead of fileInput if job was previously done on file in async mode)")
|
||||||
description =
|
|
||||||
"File ID for server-side files (can be used instead of fileInput if job was previously done on file in async mode)")
|
|
||||||
private String fileId;
|
private String fileId;
|
||||||
|
|
||||||
@AssertTrue(message = "Either fileInput or fileId must be provided")
|
@AssertTrue(message = "Either fileInput or fileId must be provided")
|
||||||
|
@ -9,6 +9,7 @@ import org.springframework.web.bind.annotation.GetMapping;
|
|||||||
import org.springframework.web.bind.annotation.RequestParam;
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
|
|
||||||
import io.swagger.v3.oas.annotations.Hidden;
|
import io.swagger.v3.oas.annotations.Hidden;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
|
||||||
import stirling.software.SPDF.config.EndpointConfiguration;
|
import stirling.software.SPDF.config.EndpointConfiguration;
|
||||||
import stirling.software.common.annotations.api.ConfigApi;
|
import stirling.software.common.annotations.api.ConfigApi;
|
||||||
@ -17,6 +18,7 @@ import stirling.software.common.model.ApplicationProperties;
|
|||||||
import stirling.software.common.service.ServerCertificateServiceInterface;
|
import stirling.software.common.service.ServerCertificateServiceInterface;
|
||||||
|
|
||||||
@ConfigApi
|
@ConfigApi
|
||||||
|
@RequiredArgsConstructor
|
||||||
@Hidden
|
@Hidden
|
||||||
public class ConfigController {
|
public class ConfigController {
|
||||||
|
|
||||||
|
@ -427,9 +427,9 @@
|
|||||||
"title": "Flatten",
|
"title": "Flatten",
|
||||||
"desc": "Remove all interactive elements and forms from a PDF"
|
"desc": "Remove all interactive elements and forms from a PDF"
|
||||||
},
|
},
|
||||||
"certSign": {
|
"manageSignatures": {
|
||||||
"title": "Sign with Certificate",
|
"title": "Sign with Certificate",
|
||||||
"desc": "Signs a PDF with a Certificate/Key (PEM/P12)"
|
"desc": "Add digital signatures to PDF documents using certificates"
|
||||||
},
|
},
|
||||||
"repair": {
|
"repair": {
|
||||||
"title": "Repair",
|
"title": "Repair",
|
||||||
@ -447,6 +447,14 @@
|
|||||||
"title": "Compare",
|
"title": "Compare",
|
||||||
"desc": "Compares and shows the differences between 2 PDF Documents"
|
"desc": "Compares and shows the differences between 2 PDF Documents"
|
||||||
},
|
},
|
||||||
|
"certSign": {
|
||||||
|
"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": {
|
"removeCertSign": {
|
||||||
"title": "Remove Certificate Sign",
|
"title": "Remove Certificate Sign",
|
||||||
"desc": "Remove certificate signature from PDF"
|
"desc": "Remove certificate signature from PDF"
|
||||||
@ -1183,9 +1191,7 @@
|
|||||||
},
|
},
|
||||||
"pageSelection": {
|
"pageSelection": {
|
||||||
"tooltip": {
|
"tooltip": {
|
||||||
"header": {
|
"header": { "title": "Page Selection Guide" },
|
||||||
"title": "Page Selection Guide"
|
|
||||||
},
|
|
||||||
"basic": {
|
"basic": {
|
||||||
"title": "Basic Usage",
|
"title": "Basic Usage",
|
||||||
"text": "Select specific pages from your PDF document using simple syntax.",
|
"text": "Select specific pages from your PDF document using simple syntax.",
|
||||||
@ -1219,15 +1225,11 @@
|
|||||||
"comma": "Comma: , or | — combine selections (e.g., 1-10, 20)",
|
"comma": "Comma: , or | — combine selections (e.g., 1-10, 20)",
|
||||||
"not": "NOT: ! or \"not\" — exclude pages (e.g., 3n & not 30)"
|
"not": "NOT: ! or \"not\" — exclude pages (e.g., 3n & not 30)"
|
||||||
},
|
},
|
||||||
"examples": {
|
"examples": { "title": "Examples" }
|
||||||
"title": "Examples"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"bulkSelection": {
|
"bulkSelection": {
|
||||||
"header": {
|
"header": { "title": "Page Selection Guide" },
|
||||||
"title": "Page Selection Guide"
|
|
||||||
},
|
|
||||||
"syntax": {
|
"syntax": {
|
||||||
"title": "Syntax Basics",
|
"title": "Syntax Basics",
|
||||||
"text": "Use numbers, ranges, keywords, and progressions (n starts at 0). Parentheses are supported.",
|
"text": "Use numbers, ranges, keywords, and progressions (n starts at 0). Parentheses are supported.",
|
||||||
@ -1779,11 +1781,67 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"certSign": {
|
"certSign": {
|
||||||
"tags": "authenticate,PEM,P12,official,encrypt,sign,certificate,PKCS12,JKS,server,manual,auto",
|
"tags": "authenticate,PEM,P12,official,encrypt",
|
||||||
"title": "Certificate Signing",
|
"title": "Certificate Signing",
|
||||||
"filenamePrefix": "signed",
|
"header": "Sign a PDF with your certificate (Work in progress)",
|
||||||
|
"selectPDF": "Select a PDF File for Signing:",
|
||||||
|
"jksNote": "Note: If your certificate type is not listed below, please convert it to a Java Keystore (.jks) file using the keytool command line tool. Then, choose the .jks file option below.",
|
||||||
|
"selectKey": "Select Your Private Key File (PKCS#8 format, could be .pem or .der):",
|
||||||
|
"selectCert": "Select Your Certificate File (X.509 format, could be .pem or .der):",
|
||||||
|
"selectP12": "Select Your PKCS#12 Keystore File (.p12 or .pfx) (Optional, If provided, it should contain your private key and certificate):",
|
||||||
|
"selectJKS": "Select Your Java Keystore File (.jks or .keystore):",
|
||||||
|
"certType": "Certificate Type",
|
||||||
|
"password": "Enter Your Keystore or Private Key Password (If Any):",
|
||||||
|
"showSig": "Show Signature",
|
||||||
|
"reason": "Reason",
|
||||||
|
"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": {
|
"signMode": {
|
||||||
"stepTitle": "Sign Mode",
|
|
||||||
"tooltip": {
|
"tooltip": {
|
||||||
"header": {
|
"header": {
|
||||||
"title": "About PDF Signatures"
|
"title": "About PDF Signatures"
|
||||||
@ -1808,72 +1866,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"certTypeStep": {
|
|
||||||
"stepTitle": "Certificate Format"
|
|
||||||
},
|
|
||||||
"certFiles": {
|
|
||||||
"stepTitle": "Certificate Files"
|
|
||||||
},
|
|
||||||
"appearance": {
|
|
||||||
"stepTitle": "Signature Appearance",
|
|
||||||
"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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"sign": {
|
|
||||||
"submit": "Sign PDF",
|
|
||||||
"results": "Signed PDF"
|
|
||||||
},
|
|
||||||
"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, JKS, and server 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",
|
|
||||||
"bullet5": "Use server certificate for simple 'Sign with Stirling-PDF' option"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"certType": {
|
"certType": {
|
||||||
"tooltip": {
|
"tooltip": {
|
||||||
"header": {
|
"header": {
|
||||||
@ -1886,7 +1878,7 @@
|
|||||||
"which": {
|
"which": {
|
||||||
"title": "Which option should I use?",
|
"title": "Which option should I use?",
|
||||||
"text": "Choose the format that matches your certificate file:",
|
"text": "Choose the format that matches your certificate file:",
|
||||||
"bullet1": "PKCS#12 (.p12 / .pfx) – one combined file (most common)",
|
"bullet1": "PKCS12 (.p12) – one combined file (most common)",
|
||||||
"bullet2": "PFX (.pfx) – Microsoft's version of PKCS12",
|
"bullet2": "PFX (.pfx) – Microsoft's version of PKCS12",
|
||||||
"bullet3": "PEM – separate private-key and certificate .pem files",
|
"bullet3": "PEM – separate private-key and certificate .pem files",
|
||||||
"bullet4": "JKS – Java .jks keystore for dev / CI-CD workflows"
|
"bullet4": "JKS – Java .jks keystore for dev / CI-CD workflows"
|
||||||
@ -2101,36 +2093,7 @@
|
|||||||
"tags": "trim,shrink,edit,shape",
|
"tags": "trim,shrink,edit,shape",
|
||||||
"title": "Crop",
|
"title": "Crop",
|
||||||
"header": "Crop PDF",
|
"header": "Crop PDF",
|
||||||
"submit": "Apply Crop",
|
"submit": "Submit"
|
||||||
"noFileSelected": "Select a PDF file to begin cropping",
|
|
||||||
"preview": {
|
|
||||||
"title": "Crop Area Selection"
|
|
||||||
},
|
|
||||||
"reset": "Reset to full PDF",
|
|
||||||
"coordinates": {
|
|
||||||
"title": "Position and Size",
|
|
||||||
"x": "X Position",
|
|
||||||
"y": "Y Position",
|
|
||||||
"width": "Width",
|
|
||||||
"height": "Height"
|
|
||||||
},
|
|
||||||
"error": {
|
|
||||||
"invalidArea": "Crop area extends beyond PDF boundaries",
|
|
||||||
"failed": "Failed to crop PDF"
|
|
||||||
},
|
|
||||||
"steps": {
|
|
||||||
"selectArea": "Select Crop Area"
|
|
||||||
},
|
|
||||||
"tooltip": {
|
|
||||||
"title": "How to Crop PDFs",
|
|
||||||
"description": "Select the area to crop from your PDF by dragging and resizing the blue overlay on the thumbnail.",
|
|
||||||
"drag": "Drag the overlay to move the crop area",
|
|
||||||
"resize": "Drag the corner and edge handles to resize",
|
|
||||||
"precision": "Use coordinate inputs for precise positioning"
|
|
||||||
},
|
|
||||||
"results": {
|
|
||||||
"title": "Crop Results"
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"autoSplitPDF": {
|
"autoSplitPDF": {
|
||||||
"tags": "QR-based,separate,scan-segment,organize",
|
"tags": "QR-based,separate,scan-segment,organize",
|
||||||
@ -2770,14 +2733,20 @@
|
|||||||
"actualSize": "Actual Size"
|
"actualSize": "Actual Size"
|
||||||
},
|
},
|
||||||
"viewer": {
|
"viewer": {
|
||||||
"firstPage": "First Page",
|
"noPdfLoaded": "No PDF loaded. Click to upload a PDF.",
|
||||||
"lastPage": "Last Page",
|
"choosePdf": "Choose PDF",
|
||||||
|
"noPagesToDisplay": "No pages to display.",
|
||||||
|
"singlePageView": "Single Page View",
|
||||||
|
"dualPageView": "Dual Page View",
|
||||||
|
"hideSidebars": "Hide Sidebars",
|
||||||
|
"showSidebars": "Show Sidebars",
|
||||||
|
"zoomOut": "Zoom out",
|
||||||
|
"zoomIn": "Zoom in",
|
||||||
"previousPage": "Previous Page",
|
"previousPage": "Previous Page",
|
||||||
"nextPage": "Next Page",
|
"nextPage": "Next Page",
|
||||||
"zoomIn": "Zoom In",
|
"pageNavigation": "Page Navigation",
|
||||||
"zoomOut": "Zoom Out",
|
"currentPage": "Current Page",
|
||||||
"singlePageView": "Single Page View",
|
"totalPages": "Total Pages"
|
||||||
"dualPageView": "Dual Page View"
|
|
||||||
},
|
},
|
||||||
"rightRail": {
|
"rightRail": {
|
||||||
"closeSelected": "Close Selected Files",
|
"closeSelected": "Close Selected Files",
|
||||||
@ -3161,5 +3130,156 @@
|
|||||||
"processImages": "Process Images",
|
"processImages": "Process Images",
|
||||||
"processImagesDesc": "Converts multiple image files into a single PDF document, then applies OCR technology to extract searchable text from the 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"viewer": {
|
||||||
|
"firstPage": "First Page",
|
||||||
|
"lastPage": "Last Page",
|
||||||
|
"previousPage": "Previous Page",
|
||||||
|
"nextPage": "Next Page",
|
||||||
|
"zoomIn": "Zoom In",
|
||||||
|
"zoomOut": "Zoom Out",
|
||||||
|
"singlePageView": "Single Page View",
|
||||||
|
"dualPageView": "Dual Page View"
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,300 +0,0 @@
|
|||||||
import React, { useRef, useState, useCallback, useEffect } from 'react';
|
|
||||||
import { Box, useMantineTheme, MantineTheme } from '@mantine/core';
|
|
||||||
import {
|
|
||||||
PDFBounds,
|
|
||||||
Rectangle,
|
|
||||||
domToPDFCoordinates,
|
|
||||||
pdfToDOMCoordinates,
|
|
||||||
constrainDOMRectToThumbnail,
|
|
||||||
isPointInThumbnail
|
|
||||||
} from '../../../utils/cropCoordinates';
|
|
||||||
import { type ResizeHandle } from '../../../constants/cropConstants';
|
|
||||||
|
|
||||||
interface CropAreaSelectorProps {
|
|
||||||
/** PDF bounds for coordinate conversion */
|
|
||||||
pdfBounds: PDFBounds;
|
|
||||||
/** Current crop area in PDF coordinates */
|
|
||||||
cropArea: Rectangle;
|
|
||||||
/** Callback when crop area changes */
|
|
||||||
onCropAreaChange: (cropArea: Rectangle) => void;
|
|
||||||
/** Whether the selector is disabled */
|
|
||||||
disabled?: boolean;
|
|
||||||
/** Child content (typically the PDF thumbnail) */
|
|
||||||
children: React.ReactNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
const CropAreaSelector: React.FC<CropAreaSelectorProps> = ({
|
|
||||||
pdfBounds,
|
|
||||||
cropArea,
|
|
||||||
onCropAreaChange,
|
|
||||||
disabled = false,
|
|
||||||
children
|
|
||||||
}) => {
|
|
||||||
const theme = useMantineTheme();
|
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
|
||||||
const overlayRef = useRef<HTMLDivElement>(null);
|
|
||||||
|
|
||||||
const [isDragging, setIsDragging] = useState(false);
|
|
||||||
const [isResizing, setIsResizing] = useState<ResizeHandle>(null);
|
|
||||||
const [dragStart, setDragStart] = useState({ x: 0, y: 0 });
|
|
||||||
const [initialCropArea, setInitialCropArea] = useState<Rectangle>(cropArea);
|
|
||||||
|
|
||||||
// Convert PDF crop area to DOM coordinates for display
|
|
||||||
const domRect = pdfToDOMCoordinates(cropArea, pdfBounds);
|
|
||||||
|
|
||||||
// Handle mouse down on overlay (start dragging or resizing)
|
|
||||||
const handleOverlayMouseDown = useCallback((e: React.MouseEvent) => {
|
|
||||||
if (disabled || !containerRef.current) return;
|
|
||||||
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
|
|
||||||
const rect = containerRef.current.getBoundingClientRect();
|
|
||||||
const x = e.clientX - rect.left;
|
|
||||||
const y = e.clientY - rect.top;
|
|
||||||
|
|
||||||
// Check if we're clicking on a resize handle first (higher priority)
|
|
||||||
const handle = getResizeHandle(x, y, domRect);
|
|
||||||
|
|
||||||
if (handle) {
|
|
||||||
setIsResizing(handle);
|
|
||||||
setInitialCropArea(cropArea);
|
|
||||||
setIsDragging(false); // Ensure we're not dragging when resizing
|
|
||||||
} else if (isPointInCropArea(x, y, domRect)) {
|
|
||||||
// Only allow dragging if we're not on a resize handle
|
|
||||||
setIsDragging(true);
|
|
||||||
setIsResizing(null); // Ensure we're not resizing when dragging
|
|
||||||
setDragStart({ x: x - domRect.x, y: y - domRect.y });
|
|
||||||
}
|
|
||||||
}, [disabled, cropArea, domRect]);
|
|
||||||
|
|
||||||
// Handle mouse down on container (start new selection)
|
|
||||||
const handleContainerMouseDown = useCallback((e: React.MouseEvent) => {
|
|
||||||
if (disabled || !containerRef.current) return;
|
|
||||||
|
|
||||||
const rect = containerRef.current.getBoundingClientRect();
|
|
||||||
const x = e.clientX - rect.left;
|
|
||||||
const y = e.clientY - rect.top;
|
|
||||||
|
|
||||||
// Only start new selection if clicking within thumbnail area
|
|
||||||
if (!isPointInThumbnail(x, y, pdfBounds)) return;
|
|
||||||
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
|
|
||||||
// Start new crop selection
|
|
||||||
const newDomRect: Rectangle = { x, y, width: 20, height: 20 };
|
|
||||||
const constrainedRect = constrainDOMRectToThumbnail(newDomRect, pdfBounds);
|
|
||||||
const newCropArea = domToPDFCoordinates(constrainedRect, pdfBounds);
|
|
||||||
|
|
||||||
onCropAreaChange(newCropArea);
|
|
||||||
setIsResizing('se'); // Start resizing from the southeast corner
|
|
||||||
setInitialCropArea(newCropArea);
|
|
||||||
}, [disabled, pdfBounds, onCropAreaChange]);
|
|
||||||
|
|
||||||
// Handle mouse move
|
|
||||||
const handleMouseMove = useCallback((e: MouseEvent) => {
|
|
||||||
if (disabled || !containerRef.current) return;
|
|
||||||
|
|
||||||
const rect = containerRef.current.getBoundingClientRect();
|
|
||||||
const x = e.clientX - rect.left;
|
|
||||||
const y = e.clientY - rect.top;
|
|
||||||
|
|
||||||
if (isDragging) {
|
|
||||||
// Dragging the entire crop area
|
|
||||||
const newX = x - dragStart.x;
|
|
||||||
const newY = y - dragStart.y;
|
|
||||||
|
|
||||||
const newDomRect: Rectangle = {
|
|
||||||
x: newX,
|
|
||||||
y: newY,
|
|
||||||
width: domRect.width,
|
|
||||||
height: domRect.height
|
|
||||||
};
|
|
||||||
|
|
||||||
const constrainedRect = constrainDOMRectToThumbnail(newDomRect, pdfBounds);
|
|
||||||
const newCropArea = domToPDFCoordinates(constrainedRect, pdfBounds);
|
|
||||||
onCropAreaChange(newCropArea);
|
|
||||||
|
|
||||||
} else if (isResizing) {
|
|
||||||
// Resizing the crop area
|
|
||||||
const newDomRect = calculateResizedRect(isResizing, domRect, x, y);
|
|
||||||
const constrainedRect = constrainDOMRectToThumbnail(newDomRect, pdfBounds);
|
|
||||||
const newCropArea = domToPDFCoordinates(constrainedRect, pdfBounds);
|
|
||||||
onCropAreaChange(newCropArea);
|
|
||||||
}
|
|
||||||
}, [disabled, isDragging, isResizing, dragStart, domRect, initialCropArea, pdfBounds, onCropAreaChange]);
|
|
||||||
|
|
||||||
// Handle mouse up
|
|
||||||
const handleMouseUp = useCallback(() => {
|
|
||||||
setIsDragging(false);
|
|
||||||
setIsResizing(null);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// Add global mouse event listeners
|
|
||||||
useEffect(() => {
|
|
||||||
if (isDragging || isResizing) {
|
|
||||||
document.addEventListener('mousemove', handleMouseMove);
|
|
||||||
document.addEventListener('mouseup', handleMouseUp);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
document.removeEventListener('mousemove', handleMouseMove);
|
|
||||||
document.removeEventListener('mouseup', handleMouseUp);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}, [isDragging, isResizing, handleMouseMove, handleMouseUp]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Box
|
|
||||||
ref={containerRef}
|
|
||||||
style={{
|
|
||||||
position: 'relative',
|
|
||||||
width: '100%',
|
|
||||||
height: '100%',
|
|
||||||
cursor: 'crosshair',
|
|
||||||
userSelect: 'none'
|
|
||||||
}}
|
|
||||||
onMouseDown={handleContainerMouseDown}
|
|
||||||
>
|
|
||||||
{/* PDF Thumbnail Content */}
|
|
||||||
{children}
|
|
||||||
|
|
||||||
{/* Crop Area Overlay */}
|
|
||||||
{!disabled && (
|
|
||||||
<Box
|
|
||||||
ref={overlayRef}
|
|
||||||
style={{
|
|
||||||
position: 'absolute',
|
|
||||||
left: domRect.x,
|
|
||||||
top: domRect.y,
|
|
||||||
width: domRect.width,
|
|
||||||
height: domRect.height,
|
|
||||||
border: `2px solid ${theme.other.crop.overlayBorder}`,
|
|
||||||
backgroundColor: theme.other.crop.overlayBackground,
|
|
||||||
cursor: 'move',
|
|
||||||
pointerEvents: 'auto'
|
|
||||||
}}
|
|
||||||
onMouseDown={handleOverlayMouseDown}
|
|
||||||
>
|
|
||||||
{/* Resize Handles */}
|
|
||||||
{renderResizeHandles(disabled, theme)}
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Helper functions
|
|
||||||
|
|
||||||
function getResizeHandle(x: number, y: number, domRect: Rectangle): ResizeHandle {
|
|
||||||
const handleSize = 8;
|
|
||||||
const tolerance = handleSize;
|
|
||||||
|
|
||||||
// Corner handles (check these first, they have priority)
|
|
||||||
if (isNear(x, domRect.x, tolerance) && isNear(y, domRect.y, tolerance)) return 'nw';
|
|
||||||
if (isNear(x, domRect.x + domRect.width, tolerance) && isNear(y, domRect.y, tolerance)) return 'ne';
|
|
||||||
if (isNear(x, domRect.x, tolerance) && isNear(y, domRect.y + domRect.height, tolerance)) return 'sw';
|
|
||||||
if (isNear(x, domRect.x + domRect.width, tolerance) && isNear(y, domRect.y + domRect.height, tolerance)) return 'se';
|
|
||||||
|
|
||||||
// Edge handles (only if not in corner area)
|
|
||||||
if (isNear(x, domRect.x + domRect.width / 2, tolerance) && isNear(y, domRect.y, tolerance)) return 'n';
|
|
||||||
if (isNear(x, domRect.x + domRect.width, tolerance) && isNear(y, domRect.y + domRect.height / 2, tolerance)) return 'e';
|
|
||||||
if (isNear(x, domRect.x + domRect.width / 2, tolerance) && isNear(y, domRect.y + domRect.height, tolerance)) return 's';
|
|
||||||
if (isNear(x, domRect.x, tolerance) && isNear(y, domRect.y + domRect.height / 2, tolerance)) return 'w';
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function isNear(a: number, b: number, tolerance: number): boolean {
|
|
||||||
return Math.abs(a - b) <= tolerance;
|
|
||||||
}
|
|
||||||
|
|
||||||
function isPointInCropArea(x: number, y: number, domRect: Rectangle): boolean {
|
|
||||||
return x >= domRect.x && x <= domRect.x + domRect.width &&
|
|
||||||
y >= domRect.y && y <= domRect.y + domRect.height;
|
|
||||||
}
|
|
||||||
|
|
||||||
function calculateResizedRect(
|
|
||||||
handle: ResizeHandle,
|
|
||||||
currentRect: Rectangle,
|
|
||||||
mouseX: number,
|
|
||||||
mouseY: number,
|
|
||||||
): Rectangle {
|
|
||||||
let { x, y, width, height } = currentRect;
|
|
||||||
|
|
||||||
switch (handle) {
|
|
||||||
case 'nw':
|
|
||||||
width += x - mouseX;
|
|
||||||
height += y - mouseY;
|
|
||||||
x = mouseX;
|
|
||||||
y = mouseY;
|
|
||||||
break;
|
|
||||||
case 'ne':
|
|
||||||
width = mouseX - x;
|
|
||||||
height += y - mouseY;
|
|
||||||
y = mouseY;
|
|
||||||
break;
|
|
||||||
case 'sw':
|
|
||||||
width += x - mouseX;
|
|
||||||
height = mouseY - y;
|
|
||||||
x = mouseX;
|
|
||||||
break;
|
|
||||||
case 'se':
|
|
||||||
width = mouseX - x;
|
|
||||||
height = mouseY - y;
|
|
||||||
break;
|
|
||||||
case 'n':
|
|
||||||
height += y - mouseY;
|
|
||||||
y = mouseY;
|
|
||||||
break;
|
|
||||||
case 'e':
|
|
||||||
width = mouseX - x;
|
|
||||||
break;
|
|
||||||
case 's':
|
|
||||||
height = mouseY - y;
|
|
||||||
break;
|
|
||||||
case 'w':
|
|
||||||
width += x - mouseX;
|
|
||||||
x = mouseX;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Enforce minimum size
|
|
||||||
width = Math.max(10, width);
|
|
||||||
height = Math.max(10, height);
|
|
||||||
|
|
||||||
return { x, y, width, height };
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderResizeHandles(disabled: boolean, theme: MantineTheme) {
|
|
||||||
if (disabled) return null;
|
|
||||||
|
|
||||||
const handleSize = 8;
|
|
||||||
const handleStyle = {
|
|
||||||
position: 'absolute' as const,
|
|
||||||
width: handleSize,
|
|
||||||
height: handleSize,
|
|
||||||
backgroundColor: theme.other.crop.handleColor,
|
|
||||||
border: `1px solid ${theme.other.crop.handleBorder}`,
|
|
||||||
borderRadius: '2px',
|
|
||||||
pointerEvents: 'auto' as const
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{/* Corner handles */}
|
|
||||||
<Box style={{ ...handleStyle, left: -handleSize/2, top: -handleSize/2, cursor: 'nw-resize' }} />
|
|
||||||
<Box style={{ ...handleStyle, right: -handleSize/2, top: -handleSize/2, cursor: 'ne-resize' }} />
|
|
||||||
<Box style={{ ...handleStyle, left: -handleSize/2, bottom: -handleSize/2, cursor: 'sw-resize' }} />
|
|
||||||
<Box style={{ ...handleStyle, right: -handleSize/2, bottom: -handleSize/2, cursor: 'se-resize' }} />
|
|
||||||
|
|
||||||
{/* Edge handles */}
|
|
||||||
<Box style={{ ...handleStyle, left: '50%', marginLeft: -handleSize/2, top: -handleSize/2, cursor: 'n-resize' }} />
|
|
||||||
<Box style={{ ...handleStyle, right: -handleSize/2, top: '50%', marginTop: -handleSize/2, cursor: 'e-resize' }} />
|
|
||||||
<Box style={{ ...handleStyle, left: '50%', marginLeft: -handleSize/2, bottom: -handleSize/2, cursor: 's-resize' }} />
|
|
||||||
<Box style={{ ...handleStyle, left: -handleSize/2, top: '50%', marginTop: -handleSize/2, cursor: 'w-resize' }} />
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default CropAreaSelector;
|
|
@ -1,262 +0,0 @@
|
|||||||
import { useMemo, useState, useEffect } from "react";
|
|
||||||
import { Stack, Text, Box, Group, NumberInput, ActionIcon, Center, Alert } from "@mantine/core";
|
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
import RestartAltIcon from "@mui/icons-material/RestartAlt";
|
|
||||||
import { CropParametersHook } from "../../../hooks/tools/crop/useCropParameters";
|
|
||||||
import { useSelectedFiles } from "../../../contexts/file/fileHooks";
|
|
||||||
import CropAreaSelector from "./CropAreaSelector";
|
|
||||||
import { DEFAULT_CROP_AREA } from "../../../constants/cropConstants";
|
|
||||||
import { PAGE_SIZES } from "../../../constants/pageSizeConstants";
|
|
||||||
import {
|
|
||||||
calculatePDFBounds,
|
|
||||||
PDFBounds,
|
|
||||||
Rectangle
|
|
||||||
} from "../../../utils/cropCoordinates";
|
|
||||||
import { pdfWorkerManager } from "../../../services/pdfWorkerManager";
|
|
||||||
import DocumentThumbnail from "../../shared/filePreview/DocumentThumbnail";
|
|
||||||
|
|
||||||
interface CropSettingsProps {
|
|
||||||
parameters: CropParametersHook;
|
|
||||||
disabled?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
const CONTAINER_SIZE = 250; // Fit within actual pane width
|
|
||||||
|
|
||||||
const CropSettings = ({ parameters, disabled = false }: CropSettingsProps) => {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const { selectedFiles, selectedFileStubs } = useSelectedFiles();
|
|
||||||
|
|
||||||
// Get the first selected file for preview
|
|
||||||
const selectedStub = useMemo(() => {
|
|
||||||
return selectedFileStubs.length > 0 ? selectedFileStubs[0] : null;
|
|
||||||
}, [selectedFileStubs]);
|
|
||||||
|
|
||||||
// Get the first selected file for PDF processing
|
|
||||||
const selectedFile = useMemo(() => {
|
|
||||||
return selectedFiles.length > 0 ? selectedFiles[0] : null;
|
|
||||||
}, [selectedFiles]);
|
|
||||||
|
|
||||||
// Get thumbnail for the selected file
|
|
||||||
const [thumbnail, setThumbnail] = useState<string | null>(null);
|
|
||||||
const [pdfBounds, setPdfBounds] = useState<PDFBounds | null>(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const loadPDFDimensions = async () => {
|
|
||||||
if (!selectedStub || !selectedFile) {
|
|
||||||
setPdfBounds(null);
|
|
||||||
setThumbnail(null);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setThumbnail(selectedStub.thumbnailUrl || null);
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Get PDF dimensions from the actual file
|
|
||||||
const arrayBuffer = await selectedFile.arrayBuffer();
|
|
||||||
|
|
||||||
// Load PDF to get actual dimensions
|
|
||||||
const pdf = await pdfWorkerManager.createDocument(arrayBuffer, {
|
|
||||||
disableAutoFetch: true,
|
|
||||||
disableStream: true,
|
|
||||||
stopAtErrors: false
|
|
||||||
});
|
|
||||||
|
|
||||||
const firstPage = await pdf.getPage(1);
|
|
||||||
const viewport = firstPage.getViewport({ scale: 1 });
|
|
||||||
|
|
||||||
const pdfWidth = viewport.width;
|
|
||||||
const pdfHeight = viewport.height;
|
|
||||||
|
|
||||||
const bounds = calculatePDFBounds(pdfWidth, pdfHeight, CONTAINER_SIZE, CONTAINER_SIZE);
|
|
||||||
setPdfBounds(bounds);
|
|
||||||
|
|
||||||
// Initialize crop area to full PDF if parameters are still default
|
|
||||||
if (parameters.parameters.cropArea === DEFAULT_CROP_AREA) {
|
|
||||||
parameters.resetToFullPDF(bounds);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cleanup PDF
|
|
||||||
pdfWorkerManager.destroyDocument(pdf);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to load PDF dimensions:', error);
|
|
||||||
// Fallback to A4 dimensions if PDF loading fails
|
|
||||||
const bounds = calculatePDFBounds(PAGE_SIZES.A4.width, PAGE_SIZES.A4.height, CONTAINER_SIZE, CONTAINER_SIZE);
|
|
||||||
setPdfBounds(bounds);
|
|
||||||
|
|
||||||
if (parameters.parameters.cropArea.width === PAGE_SIZES.A4.width && parameters.parameters.cropArea.height === PAGE_SIZES.A4.height) {
|
|
||||||
parameters.resetToFullPDF(bounds);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
loadPDFDimensions();
|
|
||||||
}, [selectedStub, selectedFile, parameters]);
|
|
||||||
|
|
||||||
// Current crop area
|
|
||||||
const cropArea = parameters.getCropArea();
|
|
||||||
|
|
||||||
|
|
||||||
// Handle crop area changes from the selector
|
|
||||||
const handleCropAreaChange = (newCropArea: Rectangle) => {
|
|
||||||
if (pdfBounds) {
|
|
||||||
parameters.setCropArea(newCropArea, pdfBounds);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Handle manual coordinate input changes
|
|
||||||
const handleCoordinateChange = (field: keyof Rectangle, value: number | string) => {
|
|
||||||
const numValue = typeof value === 'string' ? parseFloat(value) : value;
|
|
||||||
if (isNaN(numValue)) return;
|
|
||||||
|
|
||||||
const newCropArea = { ...cropArea, [field]: numValue };
|
|
||||||
if (pdfBounds) {
|
|
||||||
parameters.setCropArea(newCropArea, pdfBounds);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Reset to full PDF
|
|
||||||
const handleReset = () => {
|
|
||||||
if (pdfBounds) {
|
|
||||||
parameters.resetToFullPDF(pdfBounds);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
if (!selectedStub || !pdfBounds) {
|
|
||||||
return (
|
|
||||||
<Center style={{ height: '200px' }}>
|
|
||||||
<Text color="dimmed">
|
|
||||||
{t("crop.noFileSelected", "Select a PDF file to begin cropping")}
|
|
||||||
</Text>
|
|
||||||
</Center>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const isCropValid = parameters.isCropAreaValid(pdfBounds);
|
|
||||||
const isFullCrop = parameters.isFullPDFCrop(pdfBounds);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Stack gap="md">
|
|
||||||
{/* PDF Preview with Crop Selector */}
|
|
||||||
<Stack gap="xs">
|
|
||||||
<Group justify="space-between" align="center">
|
|
||||||
<Text size="sm" fw={500}>
|
|
||||||
{t("crop.preview.title", "Crop Area Selection")}
|
|
||||||
</Text>
|
|
||||||
<ActionIcon
|
|
||||||
variant="outline"
|
|
||||||
onClick={handleReset}
|
|
||||||
disabled={disabled || isFullCrop}
|
|
||||||
title={t("crop.reset", "Reset to full PDF")}
|
|
||||||
aria-label={t("crop.reset", "Reset to full PDF")}
|
|
||||||
>
|
|
||||||
<RestartAltIcon style={{ fontSize: '1rem' }} />
|
|
||||||
</ActionIcon>
|
|
||||||
</Group>
|
|
||||||
|
|
||||||
<Center>
|
|
||||||
<Box
|
|
||||||
style={{
|
|
||||||
width: CONTAINER_SIZE,
|
|
||||||
height: CONTAINER_SIZE,
|
|
||||||
border: '1px solid var(--mantine-color-gray-3)',
|
|
||||||
borderRadius: '8px',
|
|
||||||
backgroundColor: 'var(--mantine-color-gray-0)',
|
|
||||||
overflow: 'hidden',
|
|
||||||
position: 'relative'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<CropAreaSelector
|
|
||||||
pdfBounds={pdfBounds}
|
|
||||||
cropArea={cropArea}
|
|
||||||
onCropAreaChange={handleCropAreaChange}
|
|
||||||
disabled={disabled}
|
|
||||||
>
|
|
||||||
<DocumentThumbnail
|
|
||||||
file={selectedStub}
|
|
||||||
thumbnail={thumbnail}
|
|
||||||
style={{
|
|
||||||
width: pdfBounds.thumbnailWidth,
|
|
||||||
height: pdfBounds.thumbnailHeight,
|
|
||||||
position: 'absolute',
|
|
||||||
left: pdfBounds.offsetX,
|
|
||||||
top: pdfBounds.offsetY
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</CropAreaSelector>
|
|
||||||
</Box>
|
|
||||||
</Center>
|
|
||||||
|
|
||||||
</Stack>
|
|
||||||
|
|
||||||
{/* Manual Coordinate Input */}
|
|
||||||
<Stack gap="xs">
|
|
||||||
<Text size="sm" fw={500}>
|
|
||||||
{t("crop.coordinates.title", "Position and Size")}
|
|
||||||
</Text>
|
|
||||||
|
|
||||||
<Group grow>
|
|
||||||
<NumberInput
|
|
||||||
label={t("crop.coordinates.x", "X Position")}
|
|
||||||
value={Math.round(cropArea.x * 10) / 10}
|
|
||||||
onChange={(value) => handleCoordinateChange('x', value)}
|
|
||||||
disabled={disabled}
|
|
||||||
min={0}
|
|
||||||
max={pdfBounds.actualWidth}
|
|
||||||
step={0.1}
|
|
||||||
decimalScale={1}
|
|
||||||
size="xs"
|
|
||||||
/>
|
|
||||||
<NumberInput
|
|
||||||
label={t("crop.coordinates.y", "Y Position")}
|
|
||||||
value={Math.round(cropArea.y * 10) / 10}
|
|
||||||
onChange={(value) => handleCoordinateChange('y', value)}
|
|
||||||
disabled={disabled}
|
|
||||||
min={0}
|
|
||||||
max={pdfBounds.actualHeight}
|
|
||||||
step={0.1}
|
|
||||||
decimalScale={1}
|
|
||||||
size="xs"
|
|
||||||
/>
|
|
||||||
</Group>
|
|
||||||
|
|
||||||
<Group grow>
|
|
||||||
<NumberInput
|
|
||||||
label={t("crop.coordinates.width", "Width")}
|
|
||||||
value={Math.round(cropArea.width * 10) / 10}
|
|
||||||
onChange={(value) => handleCoordinateChange('width', value)}
|
|
||||||
disabled={disabled}
|
|
||||||
min={0.1}
|
|
||||||
max={pdfBounds.actualWidth}
|
|
||||||
step={0.1}
|
|
||||||
decimalScale={1}
|
|
||||||
size="xs"
|
|
||||||
/>
|
|
||||||
<NumberInput
|
|
||||||
label={t("crop.coordinates.height", "Height")}
|
|
||||||
value={Math.round(cropArea.height * 10) / 10}
|
|
||||||
onChange={(value) => handleCoordinateChange('height', value)}
|
|
||||||
disabled={disabled}
|
|
||||||
min={0.1}
|
|
||||||
max={pdfBounds.actualHeight}
|
|
||||||
step={0.1}
|
|
||||||
decimalScale={1}
|
|
||||||
size="xs"
|
|
||||||
/>
|
|
||||||
</Group>
|
|
||||||
|
|
||||||
|
|
||||||
{/* Validation Alert */}
|
|
||||||
{!isCropValid && (
|
|
||||||
<Alert color="red" variant="light">
|
|
||||||
<Text size="xs">
|
|
||||||
{t("crop.error.invalidArea", "Crop area extends beyond PDF boundaries")}
|
|
||||||
</Text>
|
|
||||||
</Alert>
|
|
||||||
)}
|
|
||||||
</Stack>
|
|
||||||
</Stack>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default CropSettings;
|
|
@ -1,11 +1,11 @@
|
|||||||
import { Stack, Text, TextInput } from "@mantine/core";
|
import { Stack, Text, TextInput } from "@mantine/core";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { CertSignParameters } from "../../../hooks/tools/certSign/useCertSignParameters";
|
import { ManageSignaturesParameters } from "../../../hooks/tools/manageSignatures/useManageSignaturesParameters";
|
||||||
import FileUploadButton from "../../shared/FileUploadButton";
|
import FileUploadButton from "../../shared/FileUploadButton";
|
||||||
|
|
||||||
interface CertificateFilesSettingsProps {
|
interface CertificateFilesSettingsProps {
|
||||||
parameters: CertSignParameters;
|
parameters: ManageSignaturesParameters;
|
||||||
onParameterChange: (key: keyof CertSignParameters, value: any) => void;
|
onParameterChange: (key: keyof ManageSignaturesParameters, value: any) => void;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
@ -1,9 +1,9 @@
|
|||||||
import { Stack, Button } from "@mantine/core";
|
import { Stack, Button } from "@mantine/core";
|
||||||
import { CertSignParameters } from "../../../hooks/tools/certSign/useCertSignParameters";
|
import { ManageSignaturesParameters } from "../../../hooks/tools/manageSignatures/useManageSignaturesParameters";
|
||||||
|
|
||||||
interface CertificateFormatSettingsProps {
|
interface CertificateFormatSettingsProps {
|
||||||
parameters: CertSignParameters;
|
parameters: ManageSignaturesParameters;
|
||||||
onParameterChange: (key: keyof CertSignParameters, value: any) => void;
|
onParameterChange: (key: keyof ManageSignaturesParameters, value: any) => void;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
@ -1,10 +1,10 @@
|
|||||||
import { Stack, Button } from "@mantine/core";
|
import { Stack, Button } from "@mantine/core";
|
||||||
import { CertSignParameters } from "../../../hooks/tools/certSign/useCertSignParameters";
|
import { ManageSignaturesParameters } from "../../../hooks/tools/manageSignatures/useManageSignaturesParameters";
|
||||||
import { useAppConfig } from "../../../hooks/useAppConfig";
|
import { useAppConfig } from "../../../hooks/useAppConfig";
|
||||||
|
|
||||||
interface CertificateTypeSettingsProps {
|
interface CertificateTypeSettingsProps {
|
||||||
parameters: CertSignParameters;
|
parameters: ManageSignaturesParameters;
|
||||||
onParameterChange: (key: keyof CertSignParameters, value: any) => void;
|
onParameterChange: (key: keyof ManageSignaturesParameters, value: any) => void;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
@ -1,10 +1,10 @@
|
|||||||
import { Stack, Text, Button, TextInput, NumberInput } from "@mantine/core";
|
import { Stack, Text, Button, TextInput, NumberInput } from "@mantine/core";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { CertSignParameters } from "../../../hooks/tools/certSign/useCertSignParameters";
|
import { ManageSignaturesParameters } from "../../../hooks/tools/manageSignatures/useManageSignaturesParameters";
|
||||||
|
|
||||||
interface SignatureAppearanceSettingsProps {
|
interface SignatureAppearanceSettingsProps {
|
||||||
parameters: CertSignParameters;
|
parameters: ManageSignaturesParameters;
|
||||||
onParameterChange: (key: keyof CertSignParameters, value: any) => void;
|
onParameterChange: (key: keyof ManageSignaturesParameters, value: any) => void;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
@ -1,45 +0,0 @@
|
|||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
import { TooltipContent } from '../../types/tips';
|
|
||||||
|
|
||||||
export const useCertSignTooltips = (): TooltipContent => {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
|
|
||||||
return {
|
|
||||||
header: {
|
|
||||||
title: t("certSign.tooltip.header.title", "About Managing Signatures")
|
|
||||||
},
|
|
||||||
tips: [
|
|
||||||
{
|
|
||||||
title: t("certSign.tooltip.overview.title", "What can this tool do?"),
|
|
||||||
description: t("certSign.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("certSign.tooltip.overview.bullet1", "Check existing signatures and their validity"),
|
|
||||||
t("certSign.tooltip.overview.bullet2", "View detailed information about signers and certificates"),
|
|
||||||
t("certSign.tooltip.overview.bullet3", "Add new digital signatures to secure your documents"),
|
|
||||||
t("certSign.tooltip.overview.bullet4", "Multiple files supported with easy navigation")
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: t("certSign.tooltip.validation.title", "Checking Signatures"),
|
|
||||||
description: t("certSign.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("certSign.tooltip.validation.bullet1", "Shows if signatures are valid or invalid"),
|
|
||||||
t("certSign.tooltip.validation.bullet2", "Displays signer information and signing date"),
|
|
||||||
t("certSign.tooltip.validation.bullet3", "Checks if the document was modified after signing"),
|
|
||||||
t("certSign.tooltip.validation.bullet4", "Can use custom certificates for verification")
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: t("certSign.tooltip.signing.title", "Adding Signatures"),
|
|
||||||
description: t("certSign.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("certSign.tooltip.signing.bullet1", "Supports PEM, PKCS12, JKS, and server certificate formats"),
|
|
||||||
t("certSign.tooltip.signing.bullet2", "Option to show or hide signature on the PDF"),
|
|
||||||
t("certSign.tooltip.signing.bullet3", "Add reason, location, and signer name"),
|
|
||||||
t("certSign.tooltip.signing.bullet4", "Choose which page to place visible signatures"),
|
|
||||||
t("certSign.tooltip.signing.bullet5", "Use server certificate for simple 'Sign with Stirling-PDF' option")
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
};
|
|
||||||
};
|
|
@ -6,26 +6,26 @@ export const useCertificateTypeTips = (): TooltipContent => {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
header: {
|
header: {
|
||||||
title: t("certSign.certType.tooltip.header.title", "About Certificate Types")
|
title: t("manageSignatures.certType.tooltip.header.title", "About Certificate Types")
|
||||||
},
|
},
|
||||||
tips: [
|
tips: [
|
||||||
{
|
{
|
||||||
title: t("certSign.certType.tooltip.what.title", "What's a certificate?"),
|
title: t("manageSignatures.certType.tooltip.what.title", "What's a certificate?"),
|
||||||
description: t("certSign.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.")
|
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("certSign.certType.tooltip.which.title", "Which option should I use?"),
|
title: t("manageSignatures.certType.tooltip.which.title", "Which option should I use?"),
|
||||||
description: t("certSign.certType.tooltip.which.text", "Choose the format that matches your certificate file:"),
|
description: t("manageSignatures.certType.tooltip.which.text", "Choose the format that matches your certificate file:"),
|
||||||
bullets: [
|
bullets: [
|
||||||
t("certSign.certType.tooltip.which.bullet1", "PKCS12 (.p12) – one combined file (most common)"),
|
t("manageSignatures.certType.tooltip.which.bullet1", "PKCS12 (.p12) – one combined file (most common)"),
|
||||||
t("certSign.certType.tooltip.which.bullet2", "PFX (.pfx) – Microsoft's version of PKCS12"),
|
t("manageSignatures.certType.tooltip.which.bullet2", "PFX (.pfx) – Microsoft's version of PKCS12"),
|
||||||
t("certSign.certType.tooltip.which.bullet3", "PEM – separate private-key and certificate .pem files"),
|
t("manageSignatures.certType.tooltip.which.bullet3", "PEM – separate private-key and certificate .pem files"),
|
||||||
t("certSign.certType.tooltip.which.bullet4", "JKS – Java .jks keystore for dev / CI-CD workflows")
|
t("manageSignatures.certType.tooltip.which.bullet4", "JKS – Java .jks keystore for dev / CI-CD workflows")
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: t("certSign.certType.tooltip.convert.title", "Key not listed?"),
|
title: t("manageSignatures.certType.tooltip.convert.title", "Key not listed?"),
|
||||||
description: t("certSign.certType.tooltip.convert.text", "Convert your file to a Java keystore (.jks) with keytool, then pick JKS.")
|
description: t("manageSignatures.certType.tooltip.convert.text", "Convert your file to a Java keystore (.jks) with keytool, then pick JKS.")
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
@ -1,21 +0,0 @@
|
|||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
|
|
||||||
export function useCropTooltips() {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
|
|
||||||
return {
|
|
||||||
header: {
|
|
||||||
title: t("crop.tooltip.title", "How to Crop PDFs")
|
|
||||||
},
|
|
||||||
tips: [
|
|
||||||
{
|
|
||||||
description: t("crop.tooltip.description", "Select the area to crop from your PDF by dragging and resizing the blue overlay on the thumbnail."),
|
|
||||||
bullets: [
|
|
||||||
t("crop.tooltip.drag", "Drag the overlay to move the crop area"),
|
|
||||||
t("crop.tooltip.resize", "Drag the corner and edge handles to resize"),
|
|
||||||
t("crop.tooltip.precision", "Use coordinate inputs for precise positioning"),
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
};
|
|
||||||
}
|
|
@ -0,0 +1,45 @@
|
|||||||
|
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")
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
};
|
@ -6,30 +6,30 @@ export const useSignModeTips = (): TooltipContent => {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
header: {
|
header: {
|
||||||
title: t("certSign.signMode.tooltip.header.title", "About PDF Signatures")
|
title: t("manageSignatures.signMode.tooltip.header.title", "About PDF Signatures")
|
||||||
},
|
},
|
||||||
tips: [
|
tips: [
|
||||||
{
|
{
|
||||||
title: t("certSign.signMode.tooltip.overview.title", "How signatures work"),
|
title: t("manageSignatures.signMode.tooltip.overview.title", "How signatures work"),
|
||||||
description: t("certSign.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.")
|
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("certSign.signMode.tooltip.manual.title", "Manual - Bring your certificate"),
|
title: t("manageSignatures.signMode.tooltip.manual.title", "Manual - Bring your certificate"),
|
||||||
description: t("certSign.signMode.tooltip.manual.text", "Use your own certificate files for brand-aligned identity. Can display <b>Trusted</b> when your CA/chain is recognized."),
|
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: [
|
bullets: [
|
||||||
t("certSign.signMode.tooltip.manual.use", "Use for: customer-facing, legal, compliance.")
|
t("manageSignatures.signMode.tooltip.manual.use", "Use for: customer-facing, legal, compliance.")
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: t("certSign.signMode.tooltip.auto.title", "Auto - Zero-setup, instant system seal"),
|
title: t("manageSignatures.signMode.tooltip.auto.title", "Auto - Zero-setup, instant system seal"),
|
||||||
description: t("certSign.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."),
|
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: [
|
bullets: [
|
||||||
t("certSign.signMode.tooltip.auto.use", "Use when: you need speed and consistent internal identity across reviews and records.")
|
t("manageSignatures.signMode.tooltip.auto.use", "Use when: you need speed and consistent internal identity across reviews and records.")
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: t("certSign.signMode.tooltip.rule.title", "Rule of thumb"),
|
title: t("manageSignatures.signMode.tooltip.rule.title", "Rule of thumb"),
|
||||||
description: t("certSign.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>.")
|
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>.")
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
@ -6,26 +6,26 @@ export const useSignatureAppearanceTips = (): TooltipContent => {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
header: {
|
header: {
|
||||||
title: t("certSign.appearance.tooltip.header.title", "About Signature Appearance")
|
title: t("manageSignatures.appearance.tooltip.header.title", "About Signature Appearance")
|
||||||
},
|
},
|
||||||
tips: [
|
tips: [
|
||||||
{
|
{
|
||||||
title: t("certSign.appearance.tooltip.invisible.title", "Invisible Signatures"),
|
title: t("manageSignatures.appearance.tooltip.invisible.title", "Invisible Signatures"),
|
||||||
description: t("certSign.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."),
|
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: [
|
bullets: [
|
||||||
t("certSign.appearance.tooltip.invisible.bullet1", "Provides security without visual changes"),
|
t("manageSignatures.appearance.tooltip.invisible.bullet1", "Provides security without visual changes"),
|
||||||
t("certSign.appearance.tooltip.invisible.bullet2", "Meets legal requirements for digital signing"),
|
t("manageSignatures.appearance.tooltip.invisible.bullet2", "Meets legal requirements for digital signing"),
|
||||||
t("certSign.appearance.tooltip.invisible.bullet3", "Doesn't affect document layout or design")
|
t("manageSignatures.appearance.tooltip.invisible.bullet3", "Doesn't affect document layout or design")
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: t("certSign.appearance.tooltip.visible.title", "Visible Signatures"),
|
title: t("manageSignatures.appearance.tooltip.visible.title", "Visible Signatures"),
|
||||||
description: t("certSign.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."),
|
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: [
|
bullets: [
|
||||||
t("certSign.appearance.tooltip.visible.bullet1", "Shows signer name and date on the document"),
|
t("manageSignatures.appearance.tooltip.visible.bullet1", "Shows signer name and date on the document"),
|
||||||
t("certSign.appearance.tooltip.visible.bullet2", "Can include reason and location for signing"),
|
t("manageSignatures.appearance.tooltip.visible.bullet2", "Can include reason and location for signing"),
|
||||||
t("certSign.appearance.tooltip.visible.bullet3", "Choose which page to place the signature"),
|
t("manageSignatures.appearance.tooltip.visible.bullet3", "Choose which page to place the signature"),
|
||||||
t("certSign.appearance.tooltip.visible.bullet4", "Optional logo can be included")
|
t("manageSignatures.appearance.tooltip.visible.bullet4", "Optional logo can be included")
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
@ -1,12 +0,0 @@
|
|||||||
import { PAGE_SIZES } from "./pageSizeConstants";
|
|
||||||
|
|
||||||
// Default crop area (covers entire page)
|
|
||||||
export const DEFAULT_CROP_AREA = {
|
|
||||||
x: 0,
|
|
||||||
y: 0,
|
|
||||||
width: PAGE_SIZES.A4.width,
|
|
||||||
height: PAGE_SIZES.A4.height,
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
|
|
||||||
export type ResizeHandle = 'nw' | 'ne' | 'sw' | 'se' | 'n' | 'e' | 's' | 'w' | null;
|
|
@ -1,8 +0,0 @@
|
|||||||
// Default PDF page sizes in points (1 point = 1/72 inch)
|
|
||||||
export const PAGE_SIZES = {
|
|
||||||
A4: { width: 595, height: 842 },
|
|
||||||
LETTER: { width: 612, height: 792 },
|
|
||||||
A3: { width: 842, height: 1191 },
|
|
||||||
A5: { width: 420, height: 595 },
|
|
||||||
LEGAL: { width: 612, height: 1008 },
|
|
||||||
} as const;
|
|
@ -19,12 +19,11 @@ import AutoRename from "../tools/AutoRename";
|
|||||||
import SingleLargePage from "../tools/SingleLargePage";
|
import SingleLargePage from "../tools/SingleLargePage";
|
||||||
import UnlockPdfForms from "../tools/UnlockPdfForms";
|
import UnlockPdfForms from "../tools/UnlockPdfForms";
|
||||||
import RemoveCertificateSign from "../tools/RemoveCertificateSign";
|
import RemoveCertificateSign from "../tools/RemoveCertificateSign";
|
||||||
import CertSign from "../tools/CertSign";
|
import ManageSignatures from "../tools/ManageSignatures";
|
||||||
import BookletImposition from "../tools/BookletImposition";
|
import BookletImposition from "../tools/BookletImposition";
|
||||||
import Flatten from "../tools/Flatten";
|
import Flatten from "../tools/Flatten";
|
||||||
import Rotate from "../tools/Rotate";
|
import Rotate from "../tools/Rotate";
|
||||||
import ChangeMetadata from "../tools/ChangeMetadata";
|
import ChangeMetadata from "../tools/ChangeMetadata";
|
||||||
import Crop from "../tools/Crop";
|
|
||||||
import { compressOperationConfig } from "../hooks/tools/compress/useCompressOperation";
|
import { compressOperationConfig } from "../hooks/tools/compress/useCompressOperation";
|
||||||
import { splitOperationConfig } from "../hooks/tools/split/useSplitOperation";
|
import { splitOperationConfig } from "../hooks/tools/split/useSplitOperation";
|
||||||
import { addPasswordOperationConfig } from "../hooks/tools/addPassword/useAddPasswordOperation";
|
import { addPasswordOperationConfig } from "../hooks/tools/addPassword/useAddPasswordOperation";
|
||||||
@ -38,7 +37,7 @@ import { ocrOperationConfig } from "../hooks/tools/ocr/useOCROperation";
|
|||||||
import { convertOperationConfig } from "../hooks/tools/convert/useConvertOperation";
|
import { convertOperationConfig } from "../hooks/tools/convert/useConvertOperation";
|
||||||
import { removeCertificateSignOperationConfig } from "../hooks/tools/removeCertificateSign/useRemoveCertificateSignOperation";
|
import { removeCertificateSignOperationConfig } from "../hooks/tools/removeCertificateSign/useRemoveCertificateSignOperation";
|
||||||
import { changePermissionsOperationConfig } from "../hooks/tools/changePermissions/useChangePermissionsOperation";
|
import { changePermissionsOperationConfig } from "../hooks/tools/changePermissions/useChangePermissionsOperation";
|
||||||
import { certSignOperationConfig } from "../hooks/tools/certSign/useCertSignOperation";
|
import { manageSignaturesOperationConfig } from "../hooks/tools/manageSignatures/useManageSignaturesOperation";
|
||||||
import { bookletImpositionOperationConfig } from "../hooks/tools/bookletImposition/useBookletImpositionOperation";
|
import { bookletImpositionOperationConfig } from "../hooks/tools/bookletImposition/useBookletImpositionOperation";
|
||||||
import { mergeOperationConfig } from '../hooks/tools/merge/useMergeOperation';
|
import { mergeOperationConfig } from '../hooks/tools/merge/useMergeOperation';
|
||||||
import { autoRenameOperationConfig } from "../hooks/tools/autoRename/useAutoRenameOperation";
|
import { autoRenameOperationConfig } from "../hooks/tools/autoRename/useAutoRenameOperation";
|
||||||
@ -46,7 +45,6 @@ import { flattenOperationConfig } from "../hooks/tools/flatten/useFlattenOperati
|
|||||||
import { redactOperationConfig } from "../hooks/tools/redact/useRedactOperation";
|
import { redactOperationConfig } from "../hooks/tools/redact/useRedactOperation";
|
||||||
import { rotateOperationConfig } from "../hooks/tools/rotate/useRotateOperation";
|
import { rotateOperationConfig } from "../hooks/tools/rotate/useRotateOperation";
|
||||||
import { changeMetadataOperationConfig } from "../hooks/tools/changeMetadata/useChangeMetadataOperation";
|
import { changeMetadataOperationConfig } from "../hooks/tools/changeMetadata/useChangeMetadataOperation";
|
||||||
import { cropOperationConfig } from "../hooks/tools/crop/useCropOperation";
|
|
||||||
import CompressSettings from "../components/tools/compress/CompressSettings";
|
import CompressSettings from "../components/tools/compress/CompressSettings";
|
||||||
import SplitSettings from "../components/tools/split/SplitSettings";
|
import SplitSettings from "../components/tools/split/SplitSettings";
|
||||||
import AddPasswordSettings from "../components/tools/addPassword/AddPasswordSettings";
|
import AddPasswordSettings from "../components/tools/addPassword/AddPasswordSettings";
|
||||||
@ -58,7 +56,7 @@ import AddWatermarkSingleStepSettings from "../components/tools/addWatermark/Add
|
|||||||
import OCRSettings from "../components/tools/ocr/OCRSettings";
|
import OCRSettings from "../components/tools/ocr/OCRSettings";
|
||||||
import ConvertSettings from "../components/tools/convert/ConvertSettings";
|
import ConvertSettings from "../components/tools/convert/ConvertSettings";
|
||||||
import ChangePermissionsSettings from "../components/tools/changePermissions/ChangePermissionsSettings";
|
import ChangePermissionsSettings from "../components/tools/changePermissions/ChangePermissionsSettings";
|
||||||
import CertificateTypeSettings from "../components/tools/certSign/CertificateTypeSettings";
|
import CertificateTypeSettings from "../components/tools/manageSignatures/CertificateTypeSettings";
|
||||||
import BookletImpositionSettings from "../components/tools/bookletImposition/BookletImpositionSettings";
|
import BookletImpositionSettings from "../components/tools/bookletImposition/BookletImpositionSettings";
|
||||||
import FlattenSettings from "../components/tools/flatten/FlattenSettings";
|
import FlattenSettings from "../components/tools/flatten/FlattenSettings";
|
||||||
import RedactSingleStepSettings from "../components/tools/redact/RedactSingleStepSettings";
|
import RedactSingleStepSettings from "../components/tools/redact/RedactSingleStepSettings";
|
||||||
@ -70,7 +68,6 @@ import MergeSettings from '../components/tools/merge/MergeSettings';
|
|||||||
import { adjustPageScaleOperationConfig } from "../hooks/tools/adjustPageScale/useAdjustPageScaleOperation";
|
import { adjustPageScaleOperationConfig } from "../hooks/tools/adjustPageScale/useAdjustPageScaleOperation";
|
||||||
import AdjustPageScaleSettings from "../components/tools/adjustPageScale/AdjustPageScaleSettings";
|
import AdjustPageScaleSettings from "../components/tools/adjustPageScale/AdjustPageScaleSettings";
|
||||||
import ChangeMetadataSingleStep from "../components/tools/changeMetadata/ChangeMetadataSingleStep";
|
import ChangeMetadataSingleStep from "../components/tools/changeMetadata/ChangeMetadataSingleStep";
|
||||||
import CropSettings from "../components/tools/crop/CropSettings";
|
|
||||||
|
|
||||||
const showPlaceholderTools = true; // Show all tools; grey out unavailable ones in UI
|
const showPlaceholderTools = true; // Show all tools; grey out unavailable ones in UI
|
||||||
|
|
||||||
@ -163,16 +160,16 @@ export function useFlatToolRegistry(): ToolRegistry {
|
|||||||
const allTools: ToolRegistry = {
|
const allTools: ToolRegistry = {
|
||||||
// Signing
|
// Signing
|
||||||
|
|
||||||
certSign: {
|
manageSignatures: {
|
||||||
icon: <LocalIcon icon="workspace-premium-rounded" width="1.5rem" height="1.5rem" />,
|
icon: <LocalIcon icon="workspace-premium-rounded" width="1.5rem" height="1.5rem" />,
|
||||||
name: t("home.certSign.title", "Certificate Sign"),
|
name: t("home.certSign.title", "Certificate Sign"),
|
||||||
component: CertSign,
|
component: ManageSignatures,
|
||||||
description: t("home.certSign.desc", "Sign PDF documents using digital certificates"),
|
description: t("home.certSign.desc", "Sign PDF documents using digital certificates"),
|
||||||
categoryId: ToolCategoryId.STANDARD_TOOLS,
|
categoryId: ToolCategoryId.STANDARD_TOOLS,
|
||||||
subcategoryId: SubcategoryId.SIGNING,
|
subcategoryId: SubcategoryId.SIGNING,
|
||||||
maxFiles: -1,
|
maxFiles: -1,
|
||||||
endpoints: ["cert-sign"],
|
endpoints: ["cert-sign"],
|
||||||
operationConfig: certSignOperationConfig,
|
operationConfig: manageSignaturesOperationConfig,
|
||||||
settingsComponent: CertificateTypeSettings,
|
settingsComponent: CertificateTypeSettings,
|
||||||
},
|
},
|
||||||
sign: {
|
sign: {
|
||||||
@ -325,14 +322,10 @@ export function useFlatToolRegistry(): ToolRegistry {
|
|||||||
crop: {
|
crop: {
|
||||||
icon: <LocalIcon icon="crop-rounded" width="1.5rem" height="1.5rem" />,
|
icon: <LocalIcon icon="crop-rounded" width="1.5rem" height="1.5rem" />,
|
||||||
name: t("home.crop.title", "Crop PDF"),
|
name: t("home.crop.title", "Crop PDF"),
|
||||||
component: Crop,
|
component: null,
|
||||||
description: t("home.crop.desc", "Crop a PDF to reduce its size (maintains text!)"),
|
description: t("home.crop.desc", "Crop a PDF to reduce its size (maintains text!)"),
|
||||||
categoryId: ToolCategoryId.STANDARD_TOOLS,
|
categoryId: ToolCategoryId.STANDARD_TOOLS,
|
||||||
subcategoryId: SubcategoryId.PAGE_FORMATTING,
|
subcategoryId: SubcategoryId.PAGE_FORMATTING,
|
||||||
maxFiles: -1,
|
|
||||||
endpoints: ["crop"],
|
|
||||||
operationConfig: cropOperationConfig,
|
|
||||||
settingsComponent: CropSettings,
|
|
||||||
},
|
},
|
||||||
rotate: {
|
rotate: {
|
||||||
icon: <LocalIcon icon="rotate-right-rounded" width="1.5rem" height="1.5rem" />,
|
icon: <LocalIcon icon="rotate-right-rounded" width="1.5rem" height="1.5rem" />,
|
||||||
|
@ -1,39 +0,0 @@
|
|||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
import { useToolOperation, ToolType } from '../shared/useToolOperation';
|
|
||||||
import { createStandardErrorHandler } from '../../../utils/toolErrorHandler';
|
|
||||||
import { CropParameters, defaultParameters } from './useCropParameters';
|
|
||||||
|
|
||||||
// Static configuration that can be used by both the hook and automation executor
|
|
||||||
export const buildCropFormData = (parameters: CropParameters, file: File): FormData => {
|
|
||||||
const formData = new FormData();
|
|
||||||
formData.append("fileInput", file);
|
|
||||||
const cropArea = parameters.cropArea;
|
|
||||||
|
|
||||||
// Backend expects precise float values for PDF coordinates
|
|
||||||
formData.append("x", cropArea.x.toString());
|
|
||||||
formData.append("y", cropArea.y.toString());
|
|
||||||
formData.append("width", cropArea.width.toString());
|
|
||||||
formData.append("height", cropArea.height.toString());
|
|
||||||
|
|
||||||
return formData;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Static configuration object
|
|
||||||
export const cropOperationConfig = {
|
|
||||||
toolType: ToolType.singleFile,
|
|
||||||
buildFormData: buildCropFormData,
|
|
||||||
operationType: 'crop',
|
|
||||||
endpoint: '/api/v1/general/crop',
|
|
||||||
defaultParameters,
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
export const useCropOperation = () => {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
|
|
||||||
return useToolOperation<CropParameters>({
|
|
||||||
...cropOperationConfig,
|
|
||||||
getErrorMessage: createStandardErrorHandler(
|
|
||||||
t('crop.error.failed', 'An error occurred while cropping the PDF.')
|
|
||||||
)
|
|
||||||
});
|
|
||||||
};
|
|
@ -1,141 +0,0 @@
|
|||||||
import { BaseParameters } from '../../../types/parameters';
|
|
||||||
import { useBaseParameters, BaseParametersHook } from '../shared/useBaseParameters';
|
|
||||||
import { useCallback } from 'react';
|
|
||||||
import { Rectangle, PDFBounds, constrainCropAreaToPDF, createFullPDFCropArea, roundCropArea, isRectangle } from '../../../utils/cropCoordinates';
|
|
||||||
import { DEFAULT_CROP_AREA } from '../../../constants/cropConstants';
|
|
||||||
|
|
||||||
export interface CropParameters extends BaseParameters {
|
|
||||||
cropArea: Rectangle;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const defaultParameters: CropParameters = {
|
|
||||||
cropArea: DEFAULT_CROP_AREA,
|
|
||||||
};
|
|
||||||
|
|
||||||
export type CropParametersHook = BaseParametersHook<CropParameters> & {
|
|
||||||
/** Set crop area with PDF bounds validation */
|
|
||||||
setCropArea: (cropArea: Rectangle, pdfBounds?: PDFBounds) => void;
|
|
||||||
/** Get current crop area as CropArea object */
|
|
||||||
getCropArea: () => Rectangle;
|
|
||||||
/** Reset to full PDF dimensions */
|
|
||||||
resetToFullPDF: (pdfBounds: PDFBounds) => void;
|
|
||||||
/** Check if current crop area is valid for the PDF */
|
|
||||||
isCropAreaValid: (pdfBounds?: PDFBounds) => boolean;
|
|
||||||
/** Check if crop area covers the entire PDF */
|
|
||||||
isFullPDFCrop: (pdfBounds?: PDFBounds) => boolean;
|
|
||||||
/** Update crop area with constraints applied */
|
|
||||||
updateCropAreaConstrained: (cropArea: Partial<Rectangle>, pdfBounds?: PDFBounds) => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useCropParameters = (): CropParametersHook => {
|
|
||||||
const baseHook = useBaseParameters({
|
|
||||||
defaultParameters,
|
|
||||||
endpointName: 'crop',
|
|
||||||
validateFn: (params) => {
|
|
||||||
const rect = params.cropArea;
|
|
||||||
// Basic validation - coordinates and dimensions must be positive
|
|
||||||
return rect.x >= 0 &&
|
|
||||||
rect.y >= 0 &&
|
|
||||||
rect.width > 0 &&
|
|
||||||
rect.height > 0;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Get current crop area as CropArea object
|
|
||||||
const getCropArea = useCallback((): Rectangle => {
|
|
||||||
return baseHook.parameters.cropArea;
|
|
||||||
}, [baseHook.parameters]);
|
|
||||||
|
|
||||||
// Set crop area with optional PDF bounds validation
|
|
||||||
const setCropArea = useCallback((cropArea: Rectangle, pdfBounds?: PDFBounds) => {
|
|
||||||
let finalCropArea = roundCropArea(cropArea);
|
|
||||||
|
|
||||||
// Apply PDF bounds constraints if provided
|
|
||||||
if (pdfBounds) {
|
|
||||||
finalCropArea = constrainCropAreaToPDF(finalCropArea, pdfBounds);
|
|
||||||
}
|
|
||||||
baseHook.updateParameter('cropArea', finalCropArea);
|
|
||||||
}, [baseHook]);
|
|
||||||
|
|
||||||
// Reset to cover entire PDF
|
|
||||||
const resetToFullPDF = useCallback((pdfBounds: PDFBounds) => {
|
|
||||||
const fullCropArea = createFullPDFCropArea(pdfBounds);
|
|
||||||
setCropArea(fullCropArea);
|
|
||||||
}, [setCropArea]);
|
|
||||||
|
|
||||||
// Check if current crop area is valid for the given PDF bounds
|
|
||||||
const isCropAreaValid = useCallback((pdfBounds?: PDFBounds): boolean => {
|
|
||||||
const cropArea = getCropArea();
|
|
||||||
|
|
||||||
// Basic validation
|
|
||||||
if (cropArea.x < 0 || cropArea.y < 0 || cropArea.width <= 0 || cropArea.height <= 0) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// PDF bounds validation if provided
|
|
||||||
if (pdfBounds) {
|
|
||||||
const tolerance = 0.01; // Small tolerance for floating point precision
|
|
||||||
return cropArea.x + cropArea.width <= pdfBounds.actualWidth + tolerance &&
|
|
||||||
cropArea.y + cropArea.height <= pdfBounds.actualHeight + tolerance;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}, [getCropArea]);
|
|
||||||
|
|
||||||
// Check if crop area covers the entire PDF
|
|
||||||
const isFullPDFCrop = useCallback((pdfBounds?: PDFBounds): boolean => {
|
|
||||||
if (!pdfBounds) return false;
|
|
||||||
|
|
||||||
const cropArea = getCropArea();
|
|
||||||
const tolerance = 0.5; // Allow 0.5 point tolerance for floating point precision
|
|
||||||
|
|
||||||
return Math.abs(cropArea.x) < tolerance &&
|
|
||||||
Math.abs(cropArea.y) < tolerance &&
|
|
||||||
Math.abs(cropArea.width - pdfBounds.actualWidth) < tolerance &&
|
|
||||||
Math.abs(cropArea.height - pdfBounds.actualHeight) < tolerance;
|
|
||||||
}, [getCropArea]);
|
|
||||||
|
|
||||||
// Update crop area with constraints applied
|
|
||||||
const updateCropAreaConstrained = useCallback((
|
|
||||||
partialCropArea: Partial<Rectangle>,
|
|
||||||
pdfBounds?: PDFBounds
|
|
||||||
) => {
|
|
||||||
const currentCropArea = getCropArea();
|
|
||||||
const newCropArea = { ...currentCropArea, ...partialCropArea };
|
|
||||||
setCropArea(newCropArea, pdfBounds);
|
|
||||||
}, [getCropArea, setCropArea]);
|
|
||||||
|
|
||||||
// Enhanced validation that considers PDF bounds
|
|
||||||
const validateParameters = useCallback((pdfBounds?: PDFBounds): boolean => {
|
|
||||||
return baseHook.validateParameters() && isCropAreaValid(pdfBounds);
|
|
||||||
}, [baseHook, isCropAreaValid]);
|
|
||||||
|
|
||||||
// Override updateParameter to ensure positive values
|
|
||||||
const updateParameter = useCallback(<K extends keyof CropParameters>(
|
|
||||||
parameter: K,
|
|
||||||
value: CropParameters[K]
|
|
||||||
) => {
|
|
||||||
|
|
||||||
if(isRectangle(value)) {
|
|
||||||
value.x = Math.max(0.1, value.x); // Minimum 0.1 point
|
|
||||||
value.x = Math.max(0.1, value.y); // Minimum 0.1 point
|
|
||||||
value.width = Math.max(0, value.width); // Minimum 0 point
|
|
||||||
value.height = Math.max(0, value.height); // Minimum 0 point
|
|
||||||
}
|
|
||||||
|
|
||||||
baseHook.updateParameter(parameter, value);
|
|
||||||
}, [baseHook]);
|
|
||||||
|
|
||||||
|
|
||||||
return {
|
|
||||||
...baseHook,
|
|
||||||
updateParameter,
|
|
||||||
validateParameters: () => validateParameters(),
|
|
||||||
setCropArea,
|
|
||||||
getCropArea,
|
|
||||||
resetToFullPDF,
|
|
||||||
isCropAreaValid,
|
|
||||||
isFullPDFCrop,
|
|
||||||
updateCropAreaConstrained,
|
|
||||||
};
|
|
||||||
};
|
|
@ -1,10 +1,10 @@
|
|||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { ToolType, useToolOperation } from '../shared/useToolOperation';
|
import { ToolType, useToolOperation } from '../shared/useToolOperation';
|
||||||
import { createStandardErrorHandler } from '../../../utils/toolErrorHandler';
|
import { createStandardErrorHandler } from '../../../utils/toolErrorHandler';
|
||||||
import { CertSignParameters, defaultParameters } from './useCertSignParameters';
|
import { ManageSignaturesParameters, defaultParameters } from './useManageSignaturesParameters';
|
||||||
|
|
||||||
// Build form data for signing
|
// Build form data for signing
|
||||||
export const buildCertSignFormData = (parameters: CertSignParameters, file: File): FormData => {
|
export const buildManageSignaturesFormData = (parameters: ManageSignaturesParameters, file: File): FormData => {
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('fileInput', file);
|
formData.append('fileInput', file);
|
||||||
|
|
||||||
@ -52,20 +52,20 @@ export const buildCertSignFormData = (parameters: CertSignParameters, file: File
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Static configuration object
|
// Static configuration object
|
||||||
export const certSignOperationConfig = {
|
export const manageSignaturesOperationConfig = {
|
||||||
toolType: ToolType.singleFile,
|
toolType: ToolType.singleFile,
|
||||||
buildFormData: buildCertSignFormData,
|
buildFormData: buildManageSignaturesFormData,
|
||||||
operationType: 'certSign',
|
operationType: 'manageSignatures',
|
||||||
endpoint: '/api/v1/security/cert-sign',
|
endpoint: '/api/v1/security/cert-sign',
|
||||||
multiFileEndpoint: false,
|
multiFileEndpoint: false,
|
||||||
defaultParameters,
|
defaultParameters,
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const useCertSignOperation = () => {
|
export const useManageSignaturesOperation = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return useToolOperation<CertSignParameters>({
|
return useToolOperation<ManageSignaturesParameters>({
|
||||||
...certSignOperationConfig,
|
...manageSignaturesOperationConfig,
|
||||||
getErrorMessage: createStandardErrorHandler(t('certSign.error.failed', 'An error occurred while processing signatures.'))
|
getErrorMessage: createStandardErrorHandler(t('manageSignatures.error.failed', 'An error occurred while processing signatures.'))
|
||||||
});
|
});
|
||||||
};
|
};
|
@ -1,7 +1,7 @@
|
|||||||
import { BaseParameters } from '../../../types/parameters';
|
import { BaseParameters } from '../../../types/parameters';
|
||||||
import { useBaseParameters, BaseParametersHook } from '../shared/useBaseParameters';
|
import { useBaseParameters, BaseParametersHook } from '../shared/useBaseParameters';
|
||||||
|
|
||||||
export interface CertSignParameters extends BaseParameters {
|
export interface ManageSignaturesParameters extends BaseParameters {
|
||||||
// Sign mode selection
|
// Sign mode selection
|
||||||
signMode: 'MANUAL' | 'AUTO';
|
signMode: 'MANUAL' | 'AUTO';
|
||||||
// Certificate signing options (only for manual mode)
|
// Certificate signing options (only for manual mode)
|
||||||
@ -21,7 +21,7 @@ export interface CertSignParameters extends BaseParameters {
|
|||||||
showLogo: boolean;
|
showLogo: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const defaultParameters: CertSignParameters = {
|
export const defaultParameters: ManageSignaturesParameters = {
|
||||||
signMode: 'MANUAL',
|
signMode: 'MANUAL',
|
||||||
certType: '',
|
certType: '',
|
||||||
password: '',
|
password: '',
|
||||||
@ -33,12 +33,12 @@ export const defaultParameters: CertSignParameters = {
|
|||||||
showLogo: true,
|
showLogo: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
export type CertSignParametersHook = BaseParametersHook<CertSignParameters>;
|
export type ManageSignaturesParametersHook = BaseParametersHook<ManageSignaturesParameters>;
|
||||||
|
|
||||||
export const useCertSignParameters = (): CertSignParametersHook => {
|
export const useManageSignaturesParameters = (): ManageSignaturesParametersHook => {
|
||||||
return useBaseParameters({
|
return useBaseParameters({
|
||||||
defaultParameters,
|
defaultParameters,
|
||||||
endpointName: 'cert-sign',
|
endpointName: 'manage-signatures',
|
||||||
validateFn: (params) => {
|
validateFn: (params) => {
|
||||||
// Auto mode (server certificate) - no additional validation needed
|
// Auto mode (server certificate) - no additional validation needed
|
||||||
if (params.signMode === 'AUTO') {
|
if (params.signMode === 'AUTO') {
|
@ -64,16 +64,6 @@ export const mantineTheme = createTheme({
|
|||||||
xl: 'var(--shadow-xl)',
|
xl: 'var(--shadow-xl)',
|
||||||
},
|
},
|
||||||
|
|
||||||
// Custom variables for specific components
|
|
||||||
other: {
|
|
||||||
crop: {
|
|
||||||
overlayBorder: 'var(--color-primary-500)',
|
|
||||||
overlayBackground: 'rgba(59, 130, 246, 0.1)', // Blue with 10% opacity
|
|
||||||
handleColor: 'var(--color-primary-500)',
|
|
||||||
handleBorder: 'var(--bg-surface)',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
// Component customizations
|
// Component customizations
|
||||||
components: {
|
components: {
|
||||||
Button: {
|
Button: {
|
||||||
|
@ -1,59 +0,0 @@
|
|||||||
import { useTranslation } from "react-i18next";
|
|
||||||
import { createToolFlow } from "../components/tools/shared/createToolFlow";
|
|
||||||
import CropSettings from "../components/tools/crop/CropSettings";
|
|
||||||
import { useCropParameters } from "../hooks/tools/crop/useCropParameters";
|
|
||||||
import { useCropOperation } from "../hooks/tools/crop/useCropOperation";
|
|
||||||
import { useBaseTool } from "../hooks/tools/shared/useBaseTool";
|
|
||||||
import { useCropTooltips } from "../components/tooltips/useCropTooltips";
|
|
||||||
import { BaseToolProps, ToolComponent } from "../types/tool";
|
|
||||||
|
|
||||||
const Crop = (props: BaseToolProps) => {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
|
|
||||||
const base = useBaseTool(
|
|
||||||
'crop',
|
|
||||||
useCropParameters,
|
|
||||||
useCropOperation,
|
|
||||||
props
|
|
||||||
);
|
|
||||||
|
|
||||||
const tooltips = useCropTooltips();
|
|
||||||
|
|
||||||
return createToolFlow({
|
|
||||||
files: {
|
|
||||||
selectedFiles: base.selectedFiles,
|
|
||||||
isCollapsed: base.hasResults,
|
|
||||||
minFiles: 1,
|
|
||||||
},
|
|
||||||
steps: [
|
|
||||||
{
|
|
||||||
title: t("crop.steps.selectArea", "Select Crop Area"),
|
|
||||||
isCollapsed: !base.hasFiles, // Collapsed until files selected
|
|
||||||
onCollapsedClick: base.hasResults ? base.handleSettingsReset : undefined,
|
|
||||||
tooltip: tooltips,
|
|
||||||
content: (
|
|
||||||
<CropSettings
|
|
||||||
parameters={base.params}
|
|
||||||
disabled={base.endpointLoading}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
executeButton: {
|
|
||||||
text: t("crop.submit", "Apply Crop"),
|
|
||||||
loadingText: t("loading"),
|
|
||||||
onClick: base.handleExecute,
|
|
||||||
isVisible: !base.hasResults,
|
|
||||||
disabled: !base.params.validateParameters() || !base.hasFiles || !base.endpointEnabled,
|
|
||||||
},
|
|
||||||
review: {
|
|
||||||
isVisible: base.hasResults,
|
|
||||||
operation: base.operation,
|
|
||||||
title: t("crop.results.title", "Crop Results"),
|
|
||||||
onFileClick: base.handleThumbnailClick,
|
|
||||||
onUndo: base.handleUndo,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Crop as ToolComponent;
|
|
@ -1,24 +1,24 @@
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { createToolFlow } from "../components/tools/shared/createToolFlow";
|
import { createToolFlow } from "../components/tools/shared/createToolFlow";
|
||||||
import CertificateTypeSettings from "../components/tools/certSign/CertificateTypeSettings";
|
import CertificateTypeSettings from "../components/tools/manageSignatures/CertificateTypeSettings";
|
||||||
import CertificateFormatSettings from "../components/tools/certSign/CertificateFormatSettings";
|
import CertificateFormatSettings from "../components/tools/manageSignatures/CertificateFormatSettings";
|
||||||
import CertificateFilesSettings from "../components/tools/certSign/CertificateFilesSettings";
|
import CertificateFilesSettings from "../components/tools/manageSignatures/CertificateFilesSettings";
|
||||||
import SignatureAppearanceSettings from "../components/tools/certSign/SignatureAppearanceSettings";
|
import SignatureAppearanceSettings from "../components/tools/manageSignatures/SignatureAppearanceSettings";
|
||||||
import { useCertSignParameters } from "../hooks/tools/certSign/useCertSignParameters";
|
import { useManageSignaturesParameters } from "../hooks/tools/manageSignatures/useManageSignaturesParameters";
|
||||||
import { useCertSignOperation } from "../hooks/tools/certSign/useCertSignOperation";
|
import { useManageSignaturesOperation } from "../hooks/tools/manageSignatures/useManageSignaturesOperation";
|
||||||
import { useCertificateTypeTips } from "../components/tooltips/useCertificateTypeTips";
|
import { useCertificateTypeTips } from "../components/tooltips/useCertificateTypeTips";
|
||||||
import { useSignatureAppearanceTips } from "../components/tooltips/useSignatureAppearanceTips";
|
import { useSignatureAppearanceTips } from "../components/tooltips/useSignatureAppearanceTips";
|
||||||
import { useSignModeTips } from "../components/tooltips/useSignModeTips";
|
import { useSignModeTips } from "../components/tooltips/useSignModeTips";
|
||||||
import { useBaseTool } from "../hooks/tools/shared/useBaseTool";
|
import { useBaseTool } from "../hooks/tools/shared/useBaseTool";
|
||||||
import { BaseToolProps, ToolComponent } from "../types/tool";
|
import { BaseToolProps, ToolComponent } from "../types/tool";
|
||||||
|
|
||||||
const CertSign = (props: BaseToolProps) => {
|
const ManageSignatures = (props: BaseToolProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const base = useBaseTool(
|
const base = useBaseTool(
|
||||||
'certSign',
|
'manageSignatures',
|
||||||
useCertSignParameters,
|
useManageSignaturesParameters,
|
||||||
useCertSignOperation,
|
useManageSignaturesOperation,
|
||||||
props
|
props
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -126,6 +126,6 @@ const CertSign = (props: BaseToolProps) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Static method to get the operation hook for automation
|
// Static method to get the operation hook for automation
|
||||||
CertSign.tool = () => useCertSignOperation;
|
ManageSignatures.tool = () => useManageSignaturesOperation;
|
||||||
|
|
||||||
export default CertSign as ToolComponent;
|
export default ManageSignatures as ToolComponent;
|
@ -56,6 +56,7 @@ const TOOL_IDS = [
|
|||||||
'devSsoGuide',
|
'devSsoGuide',
|
||||||
'devAirgapped',
|
'devAirgapped',
|
||||||
'bookletImposition',
|
'bookletImposition',
|
||||||
|
'manageSignatures',
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
// Tool identity - what PDF operation we're performing (type-safe)
|
// Tool identity - what PDF operation we're performing (type-safe)
|
||||||
|
@ -1,218 +0,0 @@
|
|||||||
/**
|
|
||||||
* Utility functions for crop coordinate conversion and PDF bounds handling
|
|
||||||
*/
|
|
||||||
|
|
||||||
export interface PDFBounds {
|
|
||||||
/** PDF width in points (actual PDF dimensions) */
|
|
||||||
actualWidth: number;
|
|
||||||
/** PDF height in points (actual PDF dimensions) */
|
|
||||||
actualHeight: number;
|
|
||||||
/** Thumbnail display width in pixels */
|
|
||||||
thumbnailWidth: number;
|
|
||||||
/** Thumbnail display height in pixels */
|
|
||||||
thumbnailHeight: number;
|
|
||||||
/** Horizontal offset for centering thumbnail in container */
|
|
||||||
offsetX: number;
|
|
||||||
/** Vertical offset for centering thumbnail in container */
|
|
||||||
offsetY: number;
|
|
||||||
/** Scale factor: thumbnailSize / actualSize */
|
|
||||||
scale: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Rectangle {
|
|
||||||
/** X coordinate */
|
|
||||||
x: number;
|
|
||||||
/** Y coordinate */
|
|
||||||
y: number;
|
|
||||||
/** Width */
|
|
||||||
width: number;
|
|
||||||
/** Height */
|
|
||||||
height: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Runtime type guard */
|
|
||||||
export function isRectangle(value: unknown): value is Rectangle {
|
|
||||||
if (value === null || typeof value !== "object") return false;
|
|
||||||
|
|
||||||
const r = value as Record<string, unknown>;
|
|
||||||
const isNum = (n: unknown): n is number =>
|
|
||||||
typeof n === "number" && Number.isFinite(n);
|
|
||||||
|
|
||||||
return (
|
|
||||||
isNum(r.x) &&
|
|
||||||
isNum(r.y) &&
|
|
||||||
isNum(r.width) &&
|
|
||||||
isNum(r.height) &&
|
|
||||||
r.width >= 0 &&
|
|
||||||
r.height >= 0
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Calculate PDF bounds for coordinate conversion based on thumbnail dimensions
|
|
||||||
*/
|
|
||||||
export const calculatePDFBounds = (
|
|
||||||
actualPDFWidth: number,
|
|
||||||
actualPDFHeight: number,
|
|
||||||
containerWidth: number,
|
|
||||||
containerHeight: number
|
|
||||||
): PDFBounds => {
|
|
||||||
// Calculate scale to fit PDF within container while maintaining aspect ratio
|
|
||||||
const scaleX = containerWidth / actualPDFWidth;
|
|
||||||
const scaleY = containerHeight / actualPDFHeight;
|
|
||||||
const scale = Math.min(scaleX, scaleY);
|
|
||||||
|
|
||||||
// Calculate actual thumbnail display size
|
|
||||||
const thumbnailWidth = actualPDFWidth * scale;
|
|
||||||
const thumbnailHeight = actualPDFHeight * scale;
|
|
||||||
|
|
||||||
// Calculate centering offsets - these represent where the thumbnail is positioned within the container
|
|
||||||
const offsetX = (containerWidth - thumbnailWidth) / 2;
|
|
||||||
const offsetY = (containerHeight - thumbnailHeight) / 2;
|
|
||||||
|
|
||||||
return {
|
|
||||||
actualWidth: actualPDFWidth,
|
|
||||||
actualHeight: actualPDFHeight,
|
|
||||||
thumbnailWidth,
|
|
||||||
thumbnailHeight,
|
|
||||||
offsetX,
|
|
||||||
offsetY,
|
|
||||||
scale
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Convert DOM coordinates (relative to container) to PDF coordinates
|
|
||||||
* Handles coordinate system conversion (DOM uses top-left, PDF uses bottom-left origin)
|
|
||||||
*/
|
|
||||||
export const domToPDFCoordinates = (
|
|
||||||
domRect: Rectangle,
|
|
||||||
pdfBounds: PDFBounds
|
|
||||||
): Rectangle => {
|
|
||||||
// Convert DOM coordinates to thumbnail-relative coordinates
|
|
||||||
const thumbX = domRect.x - pdfBounds.offsetX;
|
|
||||||
const thumbY = domRect.y - pdfBounds.offsetY;
|
|
||||||
|
|
||||||
// Convert to PDF coordinates (scale and flip Y-axis)
|
|
||||||
const pdfX = thumbX / pdfBounds.scale;
|
|
||||||
const pdfY = pdfBounds.actualHeight - ((thumbY + domRect.height) / pdfBounds.scale);
|
|
||||||
const pdfWidth = domRect.width / pdfBounds.scale;
|
|
||||||
const pdfHeight = domRect.height / pdfBounds.scale;
|
|
||||||
|
|
||||||
return {
|
|
||||||
x: pdfX,
|
|
||||||
y: pdfY,
|
|
||||||
width: pdfWidth,
|
|
||||||
height: pdfHeight
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Convert PDF coordinates to DOM coordinates (relative to container)
|
|
||||||
*/
|
|
||||||
export const pdfToDOMCoordinates = (
|
|
||||||
cropArea: Rectangle,
|
|
||||||
pdfBounds: PDFBounds
|
|
||||||
): Rectangle => {
|
|
||||||
// Convert PDF coordinates to thumbnail coordinates (scale and flip Y-axis)
|
|
||||||
const thumbX = cropArea.x * pdfBounds.scale;
|
|
||||||
const thumbY = (pdfBounds.actualHeight - cropArea.y - cropArea.height) * pdfBounds.scale;
|
|
||||||
const thumbWidth = cropArea.width * pdfBounds.scale;
|
|
||||||
const thumbHeight = cropArea.height * pdfBounds.scale;
|
|
||||||
|
|
||||||
// Add container offsets to get DOM coordinates
|
|
||||||
return {
|
|
||||||
x: thumbX + pdfBounds.offsetX,
|
|
||||||
y: thumbY + pdfBounds.offsetY,
|
|
||||||
width: thumbWidth,
|
|
||||||
height: thumbHeight
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Constrain a crop area to stay within PDF bounds
|
|
||||||
*/
|
|
||||||
export const constrainCropAreaToPDF = (
|
|
||||||
cropArea: Rectangle,
|
|
||||||
pdfBounds: PDFBounds
|
|
||||||
): Rectangle => {
|
|
||||||
// Ensure crop area doesn't extend beyond PDF boundaries
|
|
||||||
const maxX = Math.max(0, pdfBounds.actualWidth - cropArea.width);
|
|
||||||
const maxY = Math.max(0, pdfBounds.actualHeight - cropArea.height);
|
|
||||||
|
|
||||||
return {
|
|
||||||
x: Math.max(0, Math.min(cropArea.x, maxX)),
|
|
||||||
y: Math.max(0, Math.min(cropArea.y, maxY)),
|
|
||||||
width: Math.min(cropArea.width, pdfBounds.actualWidth - Math.max(0, cropArea.x)),
|
|
||||||
height: Math.min(cropArea.height, pdfBounds.actualHeight - Math.max(0, cropArea.y))
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Constrain DOM coordinates to stay within thumbnail bounds
|
|
||||||
*/
|
|
||||||
export const constrainDOMRectToThumbnail = (
|
|
||||||
domRect: Rectangle,
|
|
||||||
pdfBounds: PDFBounds
|
|
||||||
): Rectangle => {
|
|
||||||
const thumbnailLeft = pdfBounds.offsetX;
|
|
||||||
const thumbnailTop = pdfBounds.offsetY;
|
|
||||||
const thumbnailRight = pdfBounds.offsetX + pdfBounds.thumbnailWidth;
|
|
||||||
const thumbnailBottom = pdfBounds.offsetY + pdfBounds.thumbnailHeight;
|
|
||||||
|
|
||||||
// Constrain position
|
|
||||||
const maxX = Math.max(thumbnailLeft, thumbnailRight - domRect.width);
|
|
||||||
const maxY = Math.max(thumbnailTop, thumbnailBottom - domRect.height);
|
|
||||||
|
|
||||||
const constrainedX = Math.max(thumbnailLeft, Math.min(domRect.x, maxX));
|
|
||||||
const constrainedY = Math.max(thumbnailTop, Math.min(domRect.y, maxY));
|
|
||||||
|
|
||||||
// Constrain size to fit within thumbnail bounds from current position
|
|
||||||
const maxWidth = thumbnailRight - constrainedX;
|
|
||||||
const maxHeight = thumbnailBottom - constrainedY;
|
|
||||||
|
|
||||||
return {
|
|
||||||
x: constrainedX,
|
|
||||||
y: constrainedY,
|
|
||||||
width: Math.min(domRect.width, maxWidth),
|
|
||||||
height: Math.min(domRect.height, maxHeight)
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if a point is within the thumbnail area (not just the container)
|
|
||||||
*/
|
|
||||||
export const isPointInThumbnail = (
|
|
||||||
x: number,
|
|
||||||
y: number,
|
|
||||||
pdfBounds: PDFBounds
|
|
||||||
): boolean => {
|
|
||||||
return x >= pdfBounds.offsetX &&
|
|
||||||
x <= pdfBounds.offsetX + pdfBounds.thumbnailWidth &&
|
|
||||||
y >= pdfBounds.offsetY &&
|
|
||||||
y <= pdfBounds.offsetY + pdfBounds.thumbnailHeight;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a default crop area that covers the entire PDF
|
|
||||||
*/
|
|
||||||
export const createFullPDFCropArea = (pdfBounds: PDFBounds): Rectangle => {
|
|
||||||
return {
|
|
||||||
x: 0,
|
|
||||||
y: 0,
|
|
||||||
width: pdfBounds.actualWidth,
|
|
||||||
height: pdfBounds.actualHeight
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Round crop coordinates to reasonable precision (0.1 point)
|
|
||||||
*/
|
|
||||||
export const roundCropArea = (cropArea: Rectangle): Rectangle => {
|
|
||||||
return {
|
|
||||||
x: Math.round(cropArea.x * 10) / 10,
|
|
||||||
y: Math.round(cropArea.y * 10) / 10,
|
|
||||||
width: Math.round(cropArea.width * 10) / 10,
|
|
||||||
height: Math.round(cropArea.height * 10) / 10
|
|
||||||
};
|
|
||||||
};
|
|
@ -31,7 +31,7 @@ export const URL_TO_TOOL_MAP: Record<string, ToolId> = {
|
|||||||
'/unlock-pdf-forms': 'unlockPDFForms',
|
'/unlock-pdf-forms': 'unlockPDFForms',
|
||||||
'/remove-certificate-sign': 'removeCertSign',
|
'/remove-certificate-sign': 'removeCertSign',
|
||||||
'/remove-cert-sign': 'removeCertSign',
|
'/remove-cert-sign': 'removeCertSign',
|
||||||
'/cert-sign': 'certSign',
|
'/cert-sign': 'manageSignatures',
|
||||||
'/manage-signatures': 'certSign',
|
'/manage-signatures': 'manageSignatures',
|
||||||
'/booklet-imposition': 'bookletImposition',
|
'/booklet-imposition': 'bookletImposition',
|
||||||
};
|
};
|
||||||
|
Binary file not shown.
Loading…
x
Reference in New Issue
Block a user