mirror of
https://github.com/Frooodle/Stirling-PDF.git
synced 2026-02-17 13:52:14 +01:00
Merge branch 'V2' into feature/V2/AddStamp
This commit is contained in:
@@ -427,6 +427,10 @@
|
||||
"title": "Flatten",
|
||||
"desc": "Remove all interactive elements and forms from a PDF"
|
||||
},
|
||||
"certSign": {
|
||||
"title": "Sign with Certificate",
|
||||
"desc": "Signs a PDF with a Certificate/Key (PEM/P12)"
|
||||
},
|
||||
"repair": {
|
||||
"title": "Repair",
|
||||
"desc": "Tries to repair a corrupt/broken PDF"
|
||||
@@ -443,10 +447,6 @@
|
||||
"title": "Compare",
|
||||
"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)"
|
||||
},
|
||||
"removeCertSign": {
|
||||
"title": "Remove Certificate Sign",
|
||||
"desc": "Remove certificate signature from PDF"
|
||||
@@ -455,6 +455,10 @@
|
||||
"title": "Multi-Page Layout",
|
||||
"desc": "Merge multiple pages of a PDF document into a single page"
|
||||
},
|
||||
"bookletImposition": {
|
||||
"title": "Booklet Imposition",
|
||||
"desc": "Create booklets with proper page ordering and multi-page layout for printing and binding"
|
||||
},
|
||||
"scalePages": {
|
||||
"title": "Adjust page size/scale",
|
||||
"desc": "Change the size/scale of a page and/or its contents."
|
||||
@@ -1179,7 +1183,9 @@
|
||||
},
|
||||
"pageSelection": {
|
||||
"tooltip": {
|
||||
"header": { "title": "Page Selection Guide" },
|
||||
"header": {
|
||||
"title": "Page Selection Guide"
|
||||
},
|
||||
"basic": {
|
||||
"title": "Basic Usage",
|
||||
"text": "Select specific pages from your PDF document using simple syntax.",
|
||||
@@ -1213,11 +1219,15 @@
|
||||
"comma": "Comma: , or | — combine selections (e.g., 1-10, 20)",
|
||||
"not": "NOT: ! or \"not\" — exclude pages (e.g., 3n & not 30)"
|
||||
},
|
||||
"examples": { "title": "Examples" }
|
||||
"examples": {
|
||||
"title": "Examples"
|
||||
}
|
||||
}
|
||||
},
|
||||
"bulkSelection": {
|
||||
"header": { "title": "Page Selection Guide" },
|
||||
"header": {
|
||||
"title": "Page Selection Guide"
|
||||
},
|
||||
"syntax": {
|
||||
"title": "Syntax Basics",
|
||||
"text": "Use numbers, ranges, keywords, and progressions (n starts at 0). Parentheses are supported.",
|
||||
@@ -1769,23 +1779,124 @@
|
||||
}
|
||||
},
|
||||
"certSign": {
|
||||
"tags": "authenticate,PEM,P12,official,encrypt",
|
||||
"tags": "authenticate,PEM,P12,official,encrypt,sign,certificate,PKCS12,JKS,server,manual,auto",
|
||||
"title": "Certificate Signing",
|
||||
"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"
|
||||
"filenamePrefix": "signed",
|
||||
"signMode": {
|
||||
"stepTitle": "Sign Mode",
|
||||
"tooltip": {
|
||||
"header": {
|
||||
"title": "About PDF Signatures"
|
||||
},
|
||||
"overview": {
|
||||
"title": "How signatures work",
|
||||
"text": "Both modes seal the document (any edits are flagged as tampering) and record who/when/how for auditing. Viewer trust depends on the certificate chain."
|
||||
},
|
||||
"manual": {
|
||||
"title": "Manual - Bring your certificate",
|
||||
"text": "Use your own certificate files for brand-aligned identity. Can display <b>Trusted</b> when your CA/chain is recognised.",
|
||||
"use": "Use for: customer-facing, legal, compliance."
|
||||
},
|
||||
"auto": {
|
||||
"title": "Auto - Zero-setup, instant system seal",
|
||||
"text": "Signs with a server <b>self-signed</b> certificate. Same <b>tamper-evident seal</b> and <b>audit trail</b>; typically shows <b>Unverified</b> in viewers.",
|
||||
"use": "Use when: you need speed and consistent internal identity across reviews and records."
|
||||
},
|
||||
"rule": {
|
||||
"title": "Rule of thumb",
|
||||
"text": "Need recipient <b>Trusted</b> status? <b>Manual</b>. Need a fast, tamper-evident seal and audit trail with no setup? <b>Auto</b>."
|
||||
}
|
||||
}
|
||||
},
|
||||
"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": {
|
||||
"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": "PFX (.pfx) – Microsoft's version of PKCS12",
|
||||
"bullet3": "PEM – separate private-key and certificate .pem files",
|
||||
"bullet4": "JKS – Java .jks keystore for dev / CI-CD workflows"
|
||||
},
|
||||
"convert": {
|
||||
"title": "Key not listed?",
|
||||
"text": "Convert your file to a Java keystore (.jks) with keytool, then pick JKS."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"removeCertSign": {
|
||||
"tags": "authenticate,PEM,P12,official,decrypt",
|
||||
@@ -1813,6 +1924,99 @@
|
||||
"addBorder": "Add Borders",
|
||||
"submit": "Submit"
|
||||
},
|
||||
"bookletImposition": {
|
||||
"tags": "booklet,imposition,printing,binding,folding,signature",
|
||||
"title": "Booklet Imposition",
|
||||
"header": "Booklet Imposition",
|
||||
"submit": "Create Booklet",
|
||||
"spineLocation": {
|
||||
"label": "Spine Location",
|
||||
"left": "Left (Standard)",
|
||||
"right": "Right (RTL)"
|
||||
},
|
||||
"doubleSided": {
|
||||
"label": "Double-sided printing",
|
||||
"tooltip": "Creates both front and back sides for proper booklet printing"
|
||||
},
|
||||
"manualDuplex": {
|
||||
"title": "Manual Duplex Mode",
|
||||
"instructions": "For printers without automatic duplex. You'll need to run this twice:"
|
||||
},
|
||||
"duplexPass": {
|
||||
"label": "Print Pass",
|
||||
"first": "1st Pass",
|
||||
"second": "2nd Pass",
|
||||
"firstInstructions": "Prints front sides → stack face-down → run again with 2nd Pass",
|
||||
"secondInstructions": "Load printed stack face-down → prints back sides"
|
||||
},
|
||||
"rtlBinding": {
|
||||
"label": "Right-to-left binding",
|
||||
"tooltip": "For Arabic, Hebrew, or other right-to-left languages"
|
||||
},
|
||||
"addBorder": {
|
||||
"label": "Add borders around pages",
|
||||
"tooltip": "Adds borders around each page section to help with cutting and alignment"
|
||||
},
|
||||
"addGutter": {
|
||||
"label": "Add gutter margin",
|
||||
"tooltip": "Adds inner margin space for binding"
|
||||
},
|
||||
"gutterSize": {
|
||||
"label": "Gutter size (points)"
|
||||
},
|
||||
"flipOnShortEdge": {
|
||||
"label": "Flip on short edge (automatic duplex only)",
|
||||
"tooltip": "Enable for short-edge duplex printing (automatic duplex only - ignored in manual mode)",
|
||||
"manualNote": "Not needed in manual mode - you flip the stack yourself"
|
||||
},
|
||||
"advanced": {
|
||||
"toggle": "Advanced Options"
|
||||
},
|
||||
"paperSizeNote": "Paper size is automatically derived from your first page.",
|
||||
"tooltip": {
|
||||
"header": {
|
||||
"title": "Booklet Creation Guide"
|
||||
},
|
||||
"description": {
|
||||
"title": "What is Booklet Imposition?",
|
||||
"text": "Creates professional booklets by arranging pages in the correct printing order. Your PDF pages are placed 2-up on landscape sheets so when folded and bound, they read in proper sequence like a real book."
|
||||
},
|
||||
"example": {
|
||||
"title": "Example: 8-Page Booklet",
|
||||
"text": "Your 8-page document becomes 2 sheets:",
|
||||
"bullet1": "Sheet 1 Front: Pages 8, 1 | Back: Pages 2, 7",
|
||||
"bullet2": "Sheet 2 Front: Pages 6, 3 | Back: Pages 4, 5",
|
||||
"bullet3": "When folded & stacked: Reads 1→2→3→4→5→6→7→8"
|
||||
},
|
||||
"printing": {
|
||||
"title": "How to Print & Assemble",
|
||||
"text": "Follow these steps for perfect booklets:",
|
||||
"bullet1": "Print double-sided with 'Flip on long edge'",
|
||||
"bullet2": "Stack sheets in order, fold in half",
|
||||
"bullet3": "Staple or bind along the folded spine",
|
||||
"bullet4": "For short-edge printers: Enable 'Flip on short edge' option"
|
||||
},
|
||||
"manualDuplex": {
|
||||
"title": "Manual Duplex (Single-sided Printers)",
|
||||
"text": "For printers without automatic duplex:",
|
||||
"bullet1": "Turn OFF 'Double-sided printing'",
|
||||
"bullet2": "Select '1st Pass' → Print → Stack face-down",
|
||||
"bullet3": "Select '2nd Pass' → Load stack → Print backs",
|
||||
"bullet4": "Fold and assemble as normal"
|
||||
},
|
||||
"advanced": {
|
||||
"title": "Advanced Options",
|
||||
"text": "Fine-tune your booklet:",
|
||||
"bullet1": "Right-to-Left Binding: For Arabic, Hebrew, or RTL languages",
|
||||
"bullet2": "Borders: Shows cut lines for trimming",
|
||||
"bullet3": "Gutter Margin: Adds space for binding/stapling",
|
||||
"bullet4": "Short-edge Flip: Only for automatic duplex printers"
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"failed": "An error occurred while creating the booklet imposition."
|
||||
}
|
||||
},
|
||||
"scalePages": {
|
||||
"title": "Adjust page-scale",
|
||||
"header": "Adjust page-scale",
|
||||
@@ -2568,20 +2772,14 @@
|
||||
"actualSize": "Actual Size"
|
||||
},
|
||||
"viewer": {
|
||||
"noPdfLoaded": "No PDF loaded. Click to upload a PDF.",
|
||||
"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",
|
||||
"firstPage": "First Page",
|
||||
"lastPage": "Last Page",
|
||||
"previousPage": "Previous Page",
|
||||
"nextPage": "Next Page",
|
||||
"pageNavigation": "Page Navigation",
|
||||
"currentPage": "Current Page",
|
||||
"totalPages": "Total Pages"
|
||||
"zoomIn": "Zoom In",
|
||||
"zoomOut": "Zoom Out",
|
||||
"singlePageView": "Single Page View",
|
||||
"dualPageView": "Dual Page View"
|
||||
},
|
||||
"rightRail": {
|
||||
"closeSelected": "Close Selected Files",
|
||||
@@ -2965,15 +3163,5 @@
|
||||
"processImages": "Process Images",
|
||||
"processImagesDesc": "Converts multiple image files into a single PDF document, then applies OCR technology to extract searchable text from the images."
|
||||
}
|
||||
},
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -495,6 +495,10 @@
|
||||
"title": "Multi-Page Layout",
|
||||
"desc": "Merge multiple pages of a PDF document into a single page"
|
||||
},
|
||||
"bookletImposition": {
|
||||
"title": "Booklet Imposition",
|
||||
"desc": "Create booklets with proper page ordering and multi-page layout for printing and binding"
|
||||
},
|
||||
"scalePages": {
|
||||
"title": "Adjust page size/scale",
|
||||
"desc": "Change the size/scale of a page and/or its contents."
|
||||
@@ -1230,6 +1234,17 @@
|
||||
"addBorder": "Add Borders",
|
||||
"submit": "Submit"
|
||||
},
|
||||
"bookletImposition": {
|
||||
"tags": "booklet,imposition,printing,binding,folding,signature",
|
||||
"title": "Booklet Imposition",
|
||||
"header": "Booklet Imposition",
|
||||
"submit": "Create Booklet",
|
||||
"files": {
|
||||
},
|
||||
"error": {
|
||||
"failed": "An error occurred while creating the booklet imposition."
|
||||
}
|
||||
},
|
||||
"scalePages": {
|
||||
"tags": "resize,modify,dimension,adapt",
|
||||
"title": "Adjust page-scale",
|
||||
|
||||
@@ -18,7 +18,7 @@ interface ButtonSelectorProps<T> {
|
||||
textClassName?: string;
|
||||
}
|
||||
|
||||
const ButtonSelector = <T extends string>({
|
||||
const ButtonSelector = <T extends string | number>({
|
||||
value,
|
||||
onChange,
|
||||
options,
|
||||
@@ -51,7 +51,10 @@ const ButtonSelector = <T extends string>({
|
||||
flex: fullWidth ? 1 : undefined,
|
||||
height: 'auto',
|
||||
minHeight: '2.5rem',
|
||||
fontSize: 'var(--mantine-font-size-sm)'
|
||||
fontSize: 'var(--mantine-font-size-sm)',
|
||||
lineHeight: '1.4',
|
||||
paddingTop: '0.5rem',
|
||||
paddingBottom: '0.5rem'
|
||||
}}
|
||||
>
|
||||
<FitText
|
||||
|
||||
@@ -0,0 +1,179 @@
|
||||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Stack, Text, Divider, Collapse, Button, NumberInput } from "@mantine/core";
|
||||
import { BookletImpositionParameters } from "../../../hooks/tools/bookletImposition/useBookletImpositionParameters";
|
||||
import ButtonSelector from "../../shared/ButtonSelector";
|
||||
|
||||
interface BookletImpositionSettingsProps {
|
||||
parameters: BookletImpositionParameters;
|
||||
onParameterChange: (key: keyof BookletImpositionParameters, value: any) => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
const BookletImpositionSettings = ({ parameters, onParameterChange, disabled = false }: BookletImpositionSettingsProps) => {
|
||||
const { t } = useTranslation();
|
||||
const [advancedOpen, setAdvancedOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<Stack gap="md">
|
||||
<Divider ml='-md'></Divider>
|
||||
|
||||
|
||||
{/* Double Sided */}
|
||||
<Stack gap="sm">
|
||||
<label
|
||||
style={{ display: 'flex', alignItems: 'center', gap: 'var(--mantine-spacing-xs)' }}
|
||||
title={t('bookletImposition.doubleSided.tooltip', 'Creates both front and back sides for proper booklet printing')}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={parameters.doubleSided}
|
||||
onChange={(e) => {
|
||||
const isDoubleSided = e.target.checked;
|
||||
onParameterChange('doubleSided', isDoubleSided);
|
||||
// Reset to BOTH when turning double-sided back on
|
||||
if (isDoubleSided) {
|
||||
onParameterChange('duplexPass', 'BOTH');
|
||||
} else {
|
||||
// Default to FIRST pass when going to manual duplex
|
||||
onParameterChange('duplexPass', 'FIRST');
|
||||
}
|
||||
}}
|
||||
disabled={disabled}
|
||||
/>
|
||||
<Text size="sm">{t('bookletImposition.doubleSided.label', 'Double-sided printing')}</Text>
|
||||
</label>
|
||||
|
||||
{/* Manual Duplex Pass Selection - only show when double-sided is OFF */}
|
||||
{!parameters.doubleSided && (
|
||||
<Stack gap="xs" ml="lg">
|
||||
<Text size="sm" fw={500} c="orange">
|
||||
{t('bookletImposition.manualDuplex.title', 'Manual Duplex Mode')}
|
||||
</Text>
|
||||
<Text size="xs" c="dimmed">
|
||||
{t('bookletImposition.manualDuplex.instructions', 'For printers without automatic duplex. You\'ll need to run this twice:')}
|
||||
</Text>
|
||||
|
||||
<ButtonSelector
|
||||
label={t('bookletImposition.duplexPass.label', 'Print Pass')}
|
||||
value={parameters.duplexPass}
|
||||
onChange={(value) => onParameterChange('duplexPass', value)}
|
||||
options={[
|
||||
{ value: 'FIRST', label: t('bookletImposition.duplexPass.first', '1st Pass') },
|
||||
{ value: 'SECOND', label: t('bookletImposition.duplexPass.second', '2nd Pass') }
|
||||
]}
|
||||
disabled={disabled}
|
||||
/>
|
||||
|
||||
<Text size="xs" c="blue" fs="italic">
|
||||
{parameters.duplexPass === 'FIRST'
|
||||
? t('bookletImposition.duplexPass.firstInstructions', 'Prints front sides → stack face-down → run again with 2nd Pass')
|
||||
: t('bookletImposition.duplexPass.secondInstructions', 'Load printed stack face-down → prints back sides')
|
||||
}
|
||||
</Text>
|
||||
</Stack>
|
||||
)}
|
||||
</Stack>
|
||||
|
||||
<Divider />
|
||||
|
||||
{/* Advanced Options */}
|
||||
<Stack gap="sm">
|
||||
<Button
|
||||
variant="subtle"
|
||||
onClick={() => setAdvancedOpen(!advancedOpen)}
|
||||
disabled={disabled}
|
||||
>
|
||||
{t('bookletImposition.advanced.toggle', 'Advanced Options')} {advancedOpen ? '▲' : '▼'}
|
||||
</Button>
|
||||
|
||||
<Collapse in={advancedOpen}>
|
||||
<Stack gap="md" mt="md">
|
||||
{/* Right-to-Left Binding */}
|
||||
<label
|
||||
style={{ display: 'flex', alignItems: 'center', gap: 'var(--mantine-spacing-xs)' }}
|
||||
title={t('bookletImposition.rtlBinding.tooltip', 'For Arabic, Hebrew, or other right-to-left languages')}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={parameters.spineLocation === 'RIGHT'}
|
||||
onChange={(e) => onParameterChange('spineLocation', e.target.checked ? 'RIGHT' : 'LEFT')}
|
||||
disabled={disabled}
|
||||
/>
|
||||
<Text size="sm">{t('bookletImposition.rtlBinding.label', 'Right-to-left binding')}</Text>
|
||||
</label>
|
||||
|
||||
{/* Add Border Option */}
|
||||
<label
|
||||
style={{ display: 'flex', alignItems: 'center', gap: 'var(--mantine-spacing-xs)' }}
|
||||
title={t('bookletImposition.addBorder.tooltip', 'Adds borders around each page section to help with cutting and alignment')}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={parameters.addBorder}
|
||||
onChange={(e) => onParameterChange('addBorder', e.target.checked)}
|
||||
disabled={disabled}
|
||||
/>
|
||||
<Text size="sm">{t('bookletImposition.addBorder.label', 'Add borders around pages')}</Text>
|
||||
</label>
|
||||
|
||||
{/* Gutter Margin */}
|
||||
<Stack gap="xs">
|
||||
<label
|
||||
style={{ display: 'flex', alignItems: 'center', gap: 'var(--mantine-spacing-xs)' }}
|
||||
title={t('bookletImposition.addGutter.tooltip', 'Adds inner margin space for binding')}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={parameters.addGutter}
|
||||
onChange={(e) => onParameterChange('addGutter', e.target.checked)}
|
||||
disabled={disabled}
|
||||
/>
|
||||
<Text size="sm">{t('bookletImposition.addGutter.label', 'Add gutter margin')}</Text>
|
||||
</label>
|
||||
|
||||
{parameters.addGutter && (
|
||||
<NumberInput
|
||||
label={t('bookletImposition.gutterSize.label', 'Gutter size (points)')}
|
||||
value={parameters.gutterSize}
|
||||
onChange={(value) => onParameterChange('gutterSize', value || 12)}
|
||||
min={6}
|
||||
max={72}
|
||||
step={6}
|
||||
disabled={disabled}
|
||||
size="sm"
|
||||
/>
|
||||
)}
|
||||
</Stack>
|
||||
|
||||
{/* Flip on Short Edge */}
|
||||
<label
|
||||
style={{ display: 'flex', alignItems: 'center', gap: 'var(--mantine-spacing-xs)' }}
|
||||
title={!parameters.doubleSided
|
||||
? t('bookletImposition.flipOnShortEdge.manualNote', 'Not needed in manual mode - you flip the stack yourself')
|
||||
: t('bookletImposition.flipOnShortEdge.tooltip', 'Enable for short-edge duplex printing (automatic duplex only - ignored in manual mode)')
|
||||
}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={parameters.flipOnShortEdge}
|
||||
onChange={(e) => onParameterChange('flipOnShortEdge', e.target.checked)}
|
||||
disabled={disabled || !parameters.doubleSided}
|
||||
/>
|
||||
<Text size="sm" c={!parameters.doubleSided ? "dimmed" : undefined}>
|
||||
{t('bookletImposition.flipOnShortEdge.label', 'Flip on short edge')}
|
||||
</Text>
|
||||
</label>
|
||||
|
||||
{/* Paper Size Note */}
|
||||
<Text size="xs" c="dimmed" fs="italic">
|
||||
{t('bookletImposition.paperSizeNote', 'Paper size is automatically derived from your first page.')}
|
||||
</Text>
|
||||
</Stack>
|
||||
</Collapse>
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default BookletImpositionSettings;
|
||||
@@ -0,0 +1,95 @@
|
||||
import { Stack, Text, TextInput } from "@mantine/core";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { CertSignParameters } from "../../../hooks/tools/certSign/useCertSignParameters";
|
||||
import FileUploadButton from "../../shared/FileUploadButton";
|
||||
|
||||
interface CertificateFilesSettingsProps {
|
||||
parameters: CertSignParameters;
|
||||
onParameterChange: (key: keyof CertSignParameters, value: any) => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
const CertificateFilesSettings = ({ parameters, onParameterChange, disabled = false }: CertificateFilesSettingsProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Stack gap="md">
|
||||
{/* Certificate Files based on type */}
|
||||
{parameters.certType === 'PEM' && (
|
||||
<Stack gap="sm">
|
||||
<FileUploadButton
|
||||
file={parameters.privateKeyFile}
|
||||
onChange={(file) => onParameterChange('privateKeyFile', file || undefined)}
|
||||
accept=".pem,.der,.key"
|
||||
disabled={disabled}
|
||||
placeholder={t('certSign.choosePrivateKey', 'Choose Private Key File')}
|
||||
/>
|
||||
{parameters.privateKeyFile && (
|
||||
<FileUploadButton
|
||||
file={parameters.certFile}
|
||||
onChange={(file) => onParameterChange('certFile', file || undefined)}
|
||||
accept=".pem,.der,.crt,.cer"
|
||||
disabled={disabled}
|
||||
placeholder={t('certSign.chooseCertificate', 'Choose Certificate File')}
|
||||
/>
|
||||
)}
|
||||
</Stack>
|
||||
)}
|
||||
|
||||
{parameters.certType === 'PKCS12' && (
|
||||
<FileUploadButton
|
||||
file={parameters.p12File}
|
||||
onChange={(file) => onParameterChange('p12File', file || undefined)}
|
||||
accept=".p12"
|
||||
disabled={disabled}
|
||||
placeholder={t('certSign.chooseP12File', 'Choose PKCS12 File')}
|
||||
/>
|
||||
)}
|
||||
|
||||
{parameters.certType === 'PFX' && (
|
||||
<FileUploadButton
|
||||
file={parameters.p12File}
|
||||
onChange={(file) => onParameterChange('p12File', file || undefined)}
|
||||
accept=".pfx"
|
||||
disabled={disabled}
|
||||
placeholder={t('certSign.choosePfxFile', 'Choose PFX File')}
|
||||
/>
|
||||
)}
|
||||
|
||||
{parameters.certType === 'JKS' && (
|
||||
<FileUploadButton
|
||||
file={parameters.jksFile}
|
||||
onChange={(file) => onParameterChange('jksFile', file || undefined)}
|
||||
accept=".jks,.keystore"
|
||||
disabled={disabled}
|
||||
placeholder={t('certSign.chooseJksFile', 'Choose JKS File')}
|
||||
/>
|
||||
)}
|
||||
|
||||
{parameters.signMode === 'AUTO' && (
|
||||
<Text c="dimmed" size="sm">
|
||||
{t('certSign.serverCertMessage', 'Using server certificate - no files or password required')}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
{/* Password - only show when files are uploaded */}
|
||||
{parameters.certType && (
|
||||
(parameters.certType === 'PEM' && parameters.privateKeyFile && parameters.certFile) ||
|
||||
(parameters.certType === 'PKCS12' && parameters.p12File) ||
|
||||
(parameters.certType === 'PFX' && parameters.p12File) ||
|
||||
(parameters.certType === 'JKS' && parameters.jksFile)
|
||||
) && (
|
||||
<TextInput
|
||||
label={t('certSign.password', 'Certificate Password')}
|
||||
placeholder={t('certSign.passwordOptional', 'Leave empty if no password')}
|
||||
type="password"
|
||||
value={parameters.password}
|
||||
onChange={(event) => onParameterChange('password', event.currentTarget.value)}
|
||||
disabled={disabled}
|
||||
/>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default CertificateFilesSettings;
|
||||
@@ -0,0 +1,70 @@
|
||||
import { Stack, Button } from "@mantine/core";
|
||||
import { CertSignParameters } from "../../../hooks/tools/certSign/useCertSignParameters";
|
||||
|
||||
interface CertificateFormatSettingsProps {
|
||||
parameters: CertSignParameters;
|
||||
onParameterChange: (key: keyof CertSignParameters, value: any) => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
const CertificateFormatSettings = ({ parameters, onParameterChange, disabled = false }: CertificateFormatSettingsProps) => {
|
||||
|
||||
return (
|
||||
<Stack gap="md">
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '4px' }}>
|
||||
{/* First row - PKCS#12 and PFX */}
|
||||
<div style={{ display: 'flex', gap: '4px' }}>
|
||||
<Button
|
||||
variant={parameters.certType === 'PKCS12' ? 'filled' : 'outline'}
|
||||
color={parameters.certType === 'PKCS12' ? 'blue' : 'var(--text-muted)'}
|
||||
onClick={() => onParameterChange('certType', 'PKCS12')}
|
||||
disabled={disabled}
|
||||
style={{ flex: 1, height: 'auto', minHeight: '40px', fontSize: '11px' }}
|
||||
>
|
||||
<div style={{ textAlign: 'center', lineHeight: '1.1', fontSize: '11px' }}>
|
||||
PKCS12
|
||||
</div>
|
||||
</Button>
|
||||
<Button
|
||||
variant={parameters.certType === 'PFX' ? 'filled' : 'outline'}
|
||||
color={parameters.certType === 'PFX' ? 'blue' : 'var(--text-muted)'}
|
||||
onClick={() => onParameterChange('certType', 'PFX')}
|
||||
disabled={disabled}
|
||||
style={{ flex: 1, height: 'auto', minHeight: '40px', fontSize: '11px' }}
|
||||
>
|
||||
<div style={{ textAlign: 'center', lineHeight: '1.1', fontSize: '11px' }}>
|
||||
PFX
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
{/* Second row - PEM and JKS */}
|
||||
<div style={{ display: 'flex', gap: '4px' }}>
|
||||
<Button
|
||||
variant={parameters.certType === 'PEM' ? 'filled' : 'outline'}
|
||||
color={parameters.certType === 'PEM' ? 'blue' : 'var(--text-muted)'}
|
||||
onClick={() => onParameterChange('certType', 'PEM')}
|
||||
disabled={disabled}
|
||||
style={{ flex: 1, height: 'auto', minHeight: '40px', fontSize: '11px' }}
|
||||
>
|
||||
<div style={{ textAlign: 'center', lineHeight: '1.1', fontSize: '11px' }}>
|
||||
PEM
|
||||
</div>
|
||||
</Button>
|
||||
<Button
|
||||
variant={parameters.certType === 'JKS' ? 'filled' : 'outline'}
|
||||
color={parameters.certType === 'JKS' ? 'blue' : 'var(--text-muted)'}
|
||||
onClick={() => onParameterChange('certType', 'JKS')}
|
||||
disabled={disabled}
|
||||
style={{ flex: 1, height: 'auto', minHeight: '40px', fontSize: '11px' }}
|
||||
>
|
||||
<div style={{ textAlign: 'center', lineHeight: '1.1', fontSize: '11px' }}>
|
||||
JKS
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default CertificateFormatSettings;
|
||||
@@ -0,0 +1,62 @@
|
||||
import { Stack, Button } from "@mantine/core";
|
||||
import { CertSignParameters } from "../../../hooks/tools/certSign/useCertSignParameters";
|
||||
import { useAppConfig } from "../../../hooks/useAppConfig";
|
||||
|
||||
interface CertificateTypeSettingsProps {
|
||||
parameters: CertSignParameters;
|
||||
onParameterChange: (key: keyof CertSignParameters, value: any) => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
const CertificateTypeSettings = ({ parameters, onParameterChange, disabled = false }: CertificateTypeSettingsProps) => {
|
||||
const { config } = useAppConfig();
|
||||
const isServerCertificateEnabled = config?.serverCertificateEnabled ?? false;
|
||||
|
||||
// Reset to MANUAL if AUTO is selected but feature is disabled
|
||||
if (parameters.signMode === 'AUTO' && !isServerCertificateEnabled) {
|
||||
onParameterChange('signMode', 'MANUAL');
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack gap="md">
|
||||
<div style={{ display: 'flex', gap: '4px' }}>
|
||||
<Button
|
||||
variant={parameters.signMode === 'MANUAL' ? 'filled' : 'outline'}
|
||||
color={parameters.signMode === 'MANUAL' ? 'blue' : 'var(--text-muted)'}
|
||||
onClick={() => {
|
||||
onParameterChange('signMode', 'MANUAL');
|
||||
// Reset cert type when switching to manual
|
||||
if (parameters.signMode === 'AUTO') {
|
||||
onParameterChange('certType', '');
|
||||
}
|
||||
}}
|
||||
disabled={disabled}
|
||||
style={{ flex: 1, height: 'auto', minHeight: '40px', fontSize: '11px' }}
|
||||
>
|
||||
<div style={{ textAlign: 'center', lineHeight: '1.1', fontSize: '11px' }}>
|
||||
Manual
|
||||
</div>
|
||||
</Button>
|
||||
{isServerCertificateEnabled && (
|
||||
<Button
|
||||
variant={parameters.signMode === 'AUTO' ? 'filled' : 'outline'}
|
||||
color={parameters.signMode === 'AUTO' ? 'green' : 'var(--text-muted)'}
|
||||
onClick={() => {
|
||||
onParameterChange('signMode', 'AUTO');
|
||||
// Clear cert type and files when switching to auto
|
||||
onParameterChange('certType', '');
|
||||
}}
|
||||
disabled={disabled}
|
||||
style={{ flex: 1, height: 'auto', minHeight: '40px', fontSize: '11px' }}
|
||||
>
|
||||
<div style={{ textAlign: 'center', lineHeight: '1.1', fontSize: '11px' }}>
|
||||
Auto (server)
|
||||
</div>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default CertificateTypeSettings;
|
||||
@@ -0,0 +1,110 @@
|
||||
import { Stack, Text, Button, TextInput, NumberInput } from "@mantine/core";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { CertSignParameters } from "../../../hooks/tools/certSign/useCertSignParameters";
|
||||
|
||||
interface SignatureAppearanceSettingsProps {
|
||||
parameters: CertSignParameters;
|
||||
onParameterChange: (key: keyof CertSignParameters, value: any) => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
const SignatureAppearanceSettings = ({ parameters, onParameterChange, disabled = false }: SignatureAppearanceSettingsProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Stack gap="md">
|
||||
{/* Signature Visibility */}
|
||||
<Stack gap="sm">
|
||||
<div style={{ display: 'flex', gap: '4px' }}>
|
||||
<Button
|
||||
variant={!parameters.showSignature ? 'filled' : 'outline'}
|
||||
color={!parameters.showSignature ? 'blue' : 'var(--text-muted)'}
|
||||
onClick={() => onParameterChange('showSignature', false)}
|
||||
disabled={disabled}
|
||||
style={{ flex: 1, height: 'auto', minHeight: '40px', fontSize: '11px' }}
|
||||
>
|
||||
<div style={{ textAlign: 'center', lineHeight: '1.1', fontSize: '11px' }}>
|
||||
{t('certSign.appearance.invisible', 'Invisible')}
|
||||
</div>
|
||||
</Button>
|
||||
<Button
|
||||
variant={parameters.showSignature ? 'filled' : 'outline'}
|
||||
color={parameters.showSignature ? 'blue' : 'var(--text-muted)'}
|
||||
onClick={() => onParameterChange('showSignature', true)}
|
||||
disabled={disabled}
|
||||
style={{ flex: 1, height: 'auto', minHeight: '40px', fontSize: '11px' }}
|
||||
>
|
||||
<div style={{ textAlign: 'center', lineHeight: '1.1', fontSize: '11px' }}>
|
||||
{t('certSign.appearance.visible', 'Visible')}
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
</Stack>
|
||||
|
||||
{/* Visible Signature Options */}
|
||||
{parameters.showSignature && (
|
||||
<Stack gap="sm">
|
||||
<Text size="sm" fw={500}>
|
||||
{t('certSign.appearance.options.title', 'Signature Details')}
|
||||
</Text>
|
||||
<TextInput
|
||||
label={t('certSign.reason', 'Reason')}
|
||||
value={parameters.reason}
|
||||
onChange={(event) => onParameterChange('reason', event.currentTarget.value)}
|
||||
disabled={disabled}
|
||||
/>
|
||||
<TextInput
|
||||
label={t('certSign.location', 'Location')}
|
||||
value={parameters.location}
|
||||
onChange={(event) => onParameterChange('location', event.currentTarget.value)}
|
||||
disabled={disabled}
|
||||
/>
|
||||
<TextInput
|
||||
label={t('certSign.name', 'Name')}
|
||||
value={parameters.name}
|
||||
onChange={(event) => onParameterChange('name', event.currentTarget.value)}
|
||||
disabled={disabled}
|
||||
/>
|
||||
<NumberInput
|
||||
label={t('certSign.pageNumber', 'Page Number')}
|
||||
value={parameters.pageNumber}
|
||||
onChange={(value) => onParameterChange('pageNumber', value || 1)}
|
||||
min={1}
|
||||
disabled={disabled}
|
||||
/>
|
||||
<Stack gap="xs">
|
||||
<Text size="sm" fw={500}>
|
||||
{t('certSign.logoTitle', 'Logo')}
|
||||
</Text>
|
||||
<div style={{ display: 'flex', gap: '4px' }}>
|
||||
<Button
|
||||
variant={!parameters.showLogo ? 'filled' : 'outline'}
|
||||
color={!parameters.showLogo ? 'blue' : 'var(--text-muted)'}
|
||||
onClick={() => onParameterChange('showLogo', false)}
|
||||
disabled={disabled}
|
||||
style={{ flex: 1, height: 'auto', minHeight: '40px', fontSize: '11px' }}
|
||||
>
|
||||
<div style={{ textAlign: 'center', lineHeight: '1.1', fontSize: '11px' }}>
|
||||
{t('certSign.noLogo', 'No Logo')}
|
||||
</div>
|
||||
</Button>
|
||||
<Button
|
||||
variant={parameters.showLogo ? 'filled' : 'outline'}
|
||||
color={parameters.showLogo ? 'blue' : 'var(--text-muted)'}
|
||||
onClick={() => onParameterChange('showLogo', true)}
|
||||
disabled={disabled}
|
||||
style={{ flex: 1, height: 'auto', minHeight: '40px', fontSize: '11px' }}
|
||||
>
|
||||
<div style={{ textAlign: 'center', lineHeight: '1.1', fontSize: '11px' }}>
|
||||
{t('certSign.showLogo', 'Show Logo')}
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
</Stack>
|
||||
</Stack>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default SignatureAppearanceSettings;
|
||||
57
frontend/src/components/tooltips/useBookletImpositionTips.ts
Normal file
57
frontend/src/components/tooltips/useBookletImpositionTips.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { TooltipContent } from '../../types/tips';
|
||||
|
||||
export const useBookletImpositionTips = (): TooltipContent => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return {
|
||||
header: {
|
||||
title: t("bookletImposition.tooltip.header.title", "Booklet Creation Guide")
|
||||
},
|
||||
tips: [
|
||||
{
|
||||
title: t("bookletImposition.tooltip.description.title", "What is Booklet Imposition?"),
|
||||
description: t("bookletImposition.tooltip.description.text", "Creates professional booklets by arranging pages in the correct printing order. Your PDF pages are placed 2-up on landscape sheets so when folded and bound, they read in proper sequence like a real book.")
|
||||
},
|
||||
{
|
||||
title: t("bookletImposition.tooltip.example.title", "Example: 8-Page Booklet"),
|
||||
description: t("bookletImposition.tooltip.example.text", "Your 8-page document becomes 2 sheets:"),
|
||||
bullets: [
|
||||
t("bookletImposition.tooltip.example.bullet1", "Sheet 1 Front: Pages 8, 1 | Back: Pages 2, 7"),
|
||||
t("bookletImposition.tooltip.example.bullet2", "Sheet 2 Front: Pages 6, 3 | Back: Pages 4, 5"),
|
||||
t("bookletImposition.tooltip.example.bullet3", "When folded & stacked: Reads 1→2→3→4→5→6→7→8")
|
||||
]
|
||||
},
|
||||
{
|
||||
title: t("bookletImposition.tooltip.printing.title", "How to Print & Assemble"),
|
||||
description: t("bookletImposition.tooltip.printing.text", "Follow these steps for perfect booklets:"),
|
||||
bullets: [
|
||||
t("bookletImposition.tooltip.printing.bullet1", "Print double-sided with 'Flip on long edge'"),
|
||||
t("bookletImposition.tooltip.printing.bullet2", "Stack sheets in order, fold in half"),
|
||||
t("bookletImposition.tooltip.printing.bullet3", "Staple or bind along the folded spine"),
|
||||
t("bookletImposition.tooltip.printing.bullet4", "For short-edge printers: Enable 'Flip on short edge' option")
|
||||
]
|
||||
},
|
||||
{
|
||||
title: t("bookletImposition.tooltip.manualDuplex.title", "Manual Duplex (Single-sided Printers)"),
|
||||
description: t("bookletImposition.tooltip.manualDuplex.text", "For printers without automatic duplex:"),
|
||||
bullets: [
|
||||
t("bookletImposition.tooltip.manualDuplex.bullet1", "Turn OFF 'Double-sided printing'"),
|
||||
t("bookletImposition.tooltip.manualDuplex.bullet2", "Select '1st Pass' → Print → Stack face-down"),
|
||||
t("bookletImposition.tooltip.manualDuplex.bullet3", "Select '2nd Pass' → Load stack → Print backs"),
|
||||
t("bookletImposition.tooltip.manualDuplex.bullet4", "Fold and assemble as normal")
|
||||
]
|
||||
},
|
||||
{
|
||||
title: t("bookletImposition.tooltip.advanced.title", "Advanced Options"),
|
||||
description: t("bookletImposition.tooltip.advanced.text", "Fine-tune your booklet:"),
|
||||
bullets: [
|
||||
t("bookletImposition.tooltip.advanced.bullet1", "Right-to-Left Binding: For Arabic, Hebrew, or RTL languages"),
|
||||
t("bookletImposition.tooltip.advanced.bullet2", "Borders: Shows cut lines for trimming"),
|
||||
t("bookletImposition.tooltip.advanced.bullet3", "Gutter Margin: Adds space for binding/stapling"),
|
||||
t("bookletImposition.tooltip.advanced.bullet4", "Short-edge Flip: Only for automatic duplex printers")
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
};
|
||||
45
frontend/src/components/tooltips/useCertSignTooltips.ts
Normal file
45
frontend/src/components/tooltips/useCertSignTooltips.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
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")
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
};
|
||||
32
frontend/src/components/tooltips/useCertificateTypeTips.ts
Normal file
32
frontend/src/components/tooltips/useCertificateTypeTips.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { TooltipContent } from '../../types/tips';
|
||||
|
||||
export const useCertificateTypeTips = (): TooltipContent => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return {
|
||||
header: {
|
||||
title: t("certSign.certType.tooltip.header.title", "About Certificate Types")
|
||||
},
|
||||
tips: [
|
||||
{
|
||||
title: t("certSign.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.")
|
||||
},
|
||||
{
|
||||
title: t("certSign.certType.tooltip.which.title", "Which option should I use?"),
|
||||
description: t("certSign.certType.tooltip.which.text", "Choose the format that matches your certificate file:"),
|
||||
bullets: [
|
||||
t("certSign.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("certSign.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")
|
||||
]
|
||||
},
|
||||
{
|
||||
title: t("certSign.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.")
|
||||
}
|
||||
]
|
||||
};
|
||||
};
|
||||
36
frontend/src/components/tooltips/useSignModeTips.ts
Normal file
36
frontend/src/components/tooltips/useSignModeTips.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { TooltipContent } from '../../types/tips';
|
||||
|
||||
export const useSignModeTips = (): TooltipContent => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return {
|
||||
header: {
|
||||
title: t("certSign.signMode.tooltip.header.title", "About PDF Signatures")
|
||||
},
|
||||
tips: [
|
||||
{
|
||||
title: t("certSign.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.")
|
||||
},
|
||||
{
|
||||
title: t("certSign.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."),
|
||||
bullets: [
|
||||
t("certSign.signMode.tooltip.manual.use", "Use for: customer-facing, legal, compliance.")
|
||||
]
|
||||
},
|
||||
{
|
||||
title: t("certSign.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."),
|
||||
bullets: [
|
||||
t("certSign.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"),
|
||||
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>.")
|
||||
}
|
||||
]
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,33 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { TooltipContent } from '../../types/tips';
|
||||
|
||||
export const useSignatureAppearanceTips = (): TooltipContent => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return {
|
||||
header: {
|
||||
title: t("certSign.appearance.tooltip.header.title", "About Signature Appearance")
|
||||
},
|
||||
tips: [
|
||||
{
|
||||
title: t("certSign.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."),
|
||||
bullets: [
|
||||
t("certSign.appearance.tooltip.invisible.bullet1", "Provides security without visual changes"),
|
||||
t("certSign.appearance.tooltip.invisible.bullet2", "Meets legal requirements for digital signing"),
|
||||
t("certSign.appearance.tooltip.invisible.bullet3", "Doesn't affect document layout or design")
|
||||
]
|
||||
},
|
||||
{
|
||||
title: t("certSign.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."),
|
||||
bullets: [
|
||||
t("certSign.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("certSign.appearance.tooltip.visible.bullet3", "Choose which page to place the signature"),
|
||||
t("certSign.appearance.tooltip.visible.bullet4", "Optional logo can be included")
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
};
|
||||
@@ -20,6 +20,8 @@ import AutoRename from "../tools/AutoRename";
|
||||
import SingleLargePage from "../tools/SingleLargePage";
|
||||
import UnlockPdfForms from "../tools/UnlockPdfForms";
|
||||
import RemoveCertificateSign from "../tools/RemoveCertificateSign";
|
||||
import CertSign from "../tools/CertSign";
|
||||
import BookletImposition from "../tools/BookletImposition";
|
||||
import Flatten from "../tools/Flatten";
|
||||
import Rotate from "../tools/Rotate";
|
||||
import ChangeMetadata from "../tools/ChangeMetadata";
|
||||
@@ -38,6 +40,8 @@ import { ocrOperationConfig } from "../hooks/tools/ocr/useOCROperation";
|
||||
import { convertOperationConfig } from "../hooks/tools/convert/useConvertOperation";
|
||||
import { removeCertificateSignOperationConfig } from "../hooks/tools/removeCertificateSign/useRemoveCertificateSignOperation";
|
||||
import { changePermissionsOperationConfig } from "../hooks/tools/changePermissions/useChangePermissionsOperation";
|
||||
import { certSignOperationConfig } from "../hooks/tools/certSign/useCertSignOperation";
|
||||
import { bookletImpositionOperationConfig } from "../hooks/tools/bookletImposition/useBookletImpositionOperation";
|
||||
import { mergeOperationConfig } from '../hooks/tools/merge/useMergeOperation';
|
||||
import { autoRenameOperationConfig } from "../hooks/tools/autoRename/useAutoRenameOperation";
|
||||
import { flattenOperationConfig } from "../hooks/tools/flatten/useFlattenOperation";
|
||||
@@ -56,6 +60,8 @@ import AddWatermarkSingleStepSettings from "../components/tools/addWatermark/Add
|
||||
import OCRSettings from "../components/tools/ocr/OCRSettings";
|
||||
import ConvertSettings from "../components/tools/convert/ConvertSettings";
|
||||
import ChangePermissionsSettings from "../components/tools/changePermissions/ChangePermissionsSettings";
|
||||
import CertificateTypeSettings from "../components/tools/certSign/CertificateTypeSettings";
|
||||
import BookletImpositionSettings from "../components/tools/bookletImposition/BookletImpositionSettings";
|
||||
import FlattenSettings from "../components/tools/flatten/FlattenSettings";
|
||||
import RedactSingleStepSettings from "../components/tools/redact/RedactSingleStepSettings";
|
||||
import RotateSettings from "../components/tools/rotate/RotateSettings";
|
||||
@@ -161,11 +167,15 @@ export function useFlatToolRegistry(): ToolRegistry {
|
||||
|
||||
certSign: {
|
||||
icon: <LocalIcon icon="workspace-premium-rounded" width="1.5rem" height="1.5rem" />,
|
||||
name: t("home.certSign.title", "Sign with Certificate"),
|
||||
component: null,
|
||||
description: t("home.certSign.desc", "Signs a PDF with a Certificate/Key (PEM/P12)"),
|
||||
name: t("home.certSign.title", "Certificate Sign"),
|
||||
component: CertSign,
|
||||
description: t("home.certSign.desc", "Sign PDF documents using digital certificates"),
|
||||
categoryId: ToolCategoryId.STANDARD_TOOLS,
|
||||
subcategoryId: SubcategoryId.SIGNING,
|
||||
maxFiles: -1,
|
||||
endpoints: ["cert-sign"],
|
||||
operationConfig: certSignOperationConfig,
|
||||
settingsComponent: CertificateTypeSettings,
|
||||
},
|
||||
sign: {
|
||||
icon: <LocalIcon icon="signature-rounded" width="1.5rem" height="1.5rem" />,
|
||||
@@ -272,8 +282,6 @@ export function useFlatToolRegistry(): ToolRegistry {
|
||||
operationConfig: changePermissionsOperationConfig,
|
||||
settingsComponent: ChangePermissionsSettings,
|
||||
},
|
||||
// Verification
|
||||
|
||||
getPdfInfo: {
|
||||
icon: <LocalIcon icon="fact-check-rounded" width="1.5rem" height="1.5rem" />,
|
||||
name: t("home.getPdfInfo.title", "Get ALL Info on PDF"),
|
||||
@@ -395,7 +403,18 @@ export function useFlatToolRegistry(): ToolRegistry {
|
||||
categoryId: ToolCategoryId.STANDARD_TOOLS,
|
||||
subcategoryId: SubcategoryId.PAGE_FORMATTING,
|
||||
},
|
||||
bookletImposition: {
|
||||
icon: <LocalIcon icon="menu-book-rounded" width="1.5rem" height="1.5rem" />,
|
||||
name: t("home.bookletImposition.title", "Booklet Imposition"),
|
||||
component: BookletImposition,
|
||||
operationConfig: bookletImpositionOperationConfig,
|
||||
settingsComponent: BookletImpositionSettings,
|
||||
description: t("home.bookletImposition.desc", "Create booklets with proper page ordering and multi-page layout for printing and binding"),
|
||||
categoryId: ToolCategoryId.STANDARD_TOOLS,
|
||||
subcategoryId: SubcategoryId.PAGE_FORMATTING,
|
||||
},
|
||||
pdfToSinglePage: {
|
||||
|
||||
icon: <LocalIcon icon="looks-one-outline-rounded" width="1.5rem" height="1.5rem" />,
|
||||
name: t("home.pdfToSinglePage.title", "PDF to Single Large Page"),
|
||||
component: SingleLargePage,
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useToolOperation, ToolType } from '../shared/useToolOperation';
|
||||
import { createStandardErrorHandler } from '../../../utils/toolErrorHandler';
|
||||
import { BookletImpositionParameters, defaultParameters } from './useBookletImpositionParameters';
|
||||
|
||||
// Static configuration that can be used by both the hook and automation executor
|
||||
export const buildBookletImpositionFormData = (parameters: BookletImpositionParameters, file: File): FormData => {
|
||||
const formData = new FormData();
|
||||
formData.append("fileInput", file);
|
||||
formData.append("pagesPerSheet", parameters.pagesPerSheet.toString());
|
||||
formData.append("addBorder", parameters.addBorder.toString());
|
||||
formData.append("spineLocation", parameters.spineLocation);
|
||||
formData.append("addGutter", parameters.addGutter.toString());
|
||||
formData.append("gutterSize", parameters.gutterSize.toString());
|
||||
formData.append("doubleSided", parameters.doubleSided.toString());
|
||||
formData.append("duplexPass", parameters.duplexPass);
|
||||
formData.append("flipOnShortEdge", parameters.flipOnShortEdge.toString());
|
||||
return formData;
|
||||
};
|
||||
|
||||
// Static configuration object
|
||||
export const bookletImpositionOperationConfig = {
|
||||
toolType: ToolType.singleFile,
|
||||
buildFormData: buildBookletImpositionFormData,
|
||||
operationType: 'bookletImposition',
|
||||
endpoint: '/api/v1/general/booklet-imposition',
|
||||
defaultParameters,
|
||||
} as const;
|
||||
|
||||
export const useBookletImpositionOperation = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return useToolOperation<BookletImpositionParameters>({
|
||||
...bookletImpositionOperationConfig,
|
||||
getErrorMessage: createStandardErrorHandler(t('bookletImposition.error.failed', 'An error occurred while creating the booklet imposition.'))
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,36 @@
|
||||
import { BaseParameters } from '../../../types/parameters';
|
||||
import { useBaseParameters, BaseParametersHook } from '../shared/useBaseParameters';
|
||||
|
||||
export interface BookletImpositionParameters extends BaseParameters {
|
||||
pagesPerSheet: 2;
|
||||
addBorder: boolean;
|
||||
spineLocation: 'LEFT' | 'RIGHT';
|
||||
addGutter: boolean;
|
||||
gutterSize: number;
|
||||
doubleSided: boolean;
|
||||
duplexPass: 'BOTH' | 'FIRST' | 'SECOND';
|
||||
flipOnShortEdge: boolean;
|
||||
}
|
||||
|
||||
export const defaultParameters: BookletImpositionParameters = {
|
||||
pagesPerSheet: 2,
|
||||
addBorder: false,
|
||||
spineLocation: 'LEFT',
|
||||
addGutter: false,
|
||||
gutterSize: 12,
|
||||
doubleSided: true,
|
||||
duplexPass: 'BOTH',
|
||||
flipOnShortEdge: false,
|
||||
};
|
||||
|
||||
export type BookletImpositionParametersHook = BaseParametersHook<BookletImpositionParameters>;
|
||||
|
||||
export const useBookletImpositionParameters = (): BookletImpositionParametersHook => {
|
||||
return useBaseParameters({
|
||||
defaultParameters,
|
||||
endpointName: 'booklet-imposition',
|
||||
validateFn: (params) => {
|
||||
return params.pagesPerSheet === 2;
|
||||
},
|
||||
});
|
||||
};
|
||||
71
frontend/src/hooks/tools/certSign/useCertSignOperation.ts
Normal file
71
frontend/src/hooks/tools/certSign/useCertSignOperation.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ToolType, useToolOperation } from '../shared/useToolOperation';
|
||||
import { createStandardErrorHandler } from '../../../utils/toolErrorHandler';
|
||||
import { CertSignParameters, defaultParameters } from './useCertSignParameters';
|
||||
|
||||
// Build form data for signing
|
||||
export const buildCertSignFormData = (parameters: CertSignParameters, file: File): FormData => {
|
||||
const formData = new FormData();
|
||||
formData.append('fileInput', file);
|
||||
|
||||
// Handle sign mode
|
||||
if (parameters.signMode === 'AUTO') {
|
||||
formData.append('certType', 'SERVER');
|
||||
} else {
|
||||
formData.append('certType', parameters.certType);
|
||||
formData.append('password', parameters.password);
|
||||
|
||||
// Add certificate files based on type (only for manual mode)
|
||||
switch (parameters.certType) {
|
||||
case 'PEM':
|
||||
if (parameters.privateKeyFile) {
|
||||
formData.append('privateKeyFile', parameters.privateKeyFile);
|
||||
}
|
||||
if (parameters.certFile) {
|
||||
formData.append('certFile', parameters.certFile);
|
||||
}
|
||||
break;
|
||||
case 'PKCS12':
|
||||
if (parameters.p12File) {
|
||||
formData.append('p12File', parameters.p12File);
|
||||
}
|
||||
break;
|
||||
case 'JKS':
|
||||
if (parameters.jksFile) {
|
||||
formData.append('jksFile', parameters.jksFile);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Add signature appearance options if enabled
|
||||
if (parameters.showSignature) {
|
||||
formData.append('showSignature', 'true');
|
||||
formData.append('reason', parameters.reason);
|
||||
formData.append('location', parameters.location);
|
||||
formData.append('name', parameters.name);
|
||||
formData.append('pageNumber', parameters.pageNumber.toString());
|
||||
formData.append('showLogo', parameters.showLogo.toString());
|
||||
}
|
||||
|
||||
return formData;
|
||||
};
|
||||
|
||||
// Static configuration object
|
||||
export const certSignOperationConfig = {
|
||||
toolType: ToolType.singleFile,
|
||||
buildFormData: buildCertSignFormData,
|
||||
operationType: 'certSign',
|
||||
endpoint: '/api/v1/security/cert-sign',
|
||||
multiFileEndpoint: false,
|
||||
defaultParameters,
|
||||
} as const;
|
||||
|
||||
export const useCertSignOperation = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return useToolOperation<CertSignParameters>({
|
||||
...certSignOperationConfig,
|
||||
getErrorMessage: createStandardErrorHandler(t('certSign.error.failed', 'An error occurred while processing signatures.'))
|
||||
});
|
||||
};
|
||||
67
frontend/src/hooks/tools/certSign/useCertSignParameters.ts
Normal file
67
frontend/src/hooks/tools/certSign/useCertSignParameters.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import { BaseParameters } from '../../../types/parameters';
|
||||
import { useBaseParameters, BaseParametersHook } from '../shared/useBaseParameters';
|
||||
|
||||
export interface CertSignParameters extends BaseParameters {
|
||||
// Sign mode selection
|
||||
signMode: 'MANUAL' | 'AUTO';
|
||||
// Certificate signing options (only for manual mode)
|
||||
certType: '' | 'PEM' | 'PKCS12' | 'PFX' | 'JKS';
|
||||
privateKeyFile?: File;
|
||||
certFile?: File;
|
||||
p12File?: File;
|
||||
jksFile?: File;
|
||||
password: string;
|
||||
|
||||
// Signature appearance options
|
||||
showSignature: boolean;
|
||||
reason: string;
|
||||
location: string;
|
||||
name: string;
|
||||
pageNumber: number;
|
||||
showLogo: boolean;
|
||||
}
|
||||
|
||||
export const defaultParameters: CertSignParameters = {
|
||||
signMode: 'MANUAL',
|
||||
certType: '',
|
||||
password: '',
|
||||
showSignature: false,
|
||||
reason: '',
|
||||
location: '',
|
||||
name: '',
|
||||
pageNumber: 1,
|
||||
showLogo: true,
|
||||
};
|
||||
|
||||
export type CertSignParametersHook = BaseParametersHook<CertSignParameters>;
|
||||
|
||||
export const useCertSignParameters = (): CertSignParametersHook => {
|
||||
return useBaseParameters({
|
||||
defaultParameters,
|
||||
endpointName: 'cert-sign',
|
||||
validateFn: (params) => {
|
||||
// Auto mode (server certificate) - no additional validation needed
|
||||
if (params.signMode === 'AUTO') {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Manual mode - requires certificate type and files
|
||||
if (!params.certType) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check for required files based on cert type
|
||||
switch (params.certType) {
|
||||
case 'PEM':
|
||||
return !!(params.privateKeyFile && params.certFile);
|
||||
case 'PKCS12':
|
||||
case 'PFX':
|
||||
return !!params.p12File;
|
||||
case 'JKS':
|
||||
return !!params.jksFile;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -23,6 +23,7 @@ export interface AppConfig {
|
||||
license?: string;
|
||||
GoogleDriveEnabled?: boolean;
|
||||
SSOAutoLogin?: boolean;
|
||||
serverCertificateEnabled?: boolean;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
|
||||
140
frontend/src/services/signatureDetectionService.ts
Normal file
140
frontend/src/services/signatureDetectionService.ts
Normal file
@@ -0,0 +1,140 @@
|
||||
/**
|
||||
* Service for detecting signatures in PDF files using PDF.js
|
||||
* This provides a quick client-side check to determine if a PDF contains signatures
|
||||
* without needing to make API calls
|
||||
*/
|
||||
|
||||
// PDF.js types (simplified)
|
||||
declare global {
|
||||
interface Window {
|
||||
pdfjsLib?: any;
|
||||
}
|
||||
}
|
||||
|
||||
export interface SignatureDetectionResult {
|
||||
hasSignatures: boolean;
|
||||
signatureCount?: number;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export interface FileSignatureStatus {
|
||||
file: File;
|
||||
result: SignatureDetectionResult;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect signatures in a single PDF file using PDF.js
|
||||
*/
|
||||
const detectSignaturesInFile = async (file: File): Promise<SignatureDetectionResult> => {
|
||||
try {
|
||||
// Ensure PDF.js is available
|
||||
if (!window.pdfjsLib) {
|
||||
return {
|
||||
hasSignatures: false,
|
||||
error: 'PDF.js not available'
|
||||
};
|
||||
}
|
||||
|
||||
// Convert file to ArrayBuffer
|
||||
const arrayBuffer = await file.arrayBuffer();
|
||||
|
||||
// Load the PDF document
|
||||
const pdf = await window.pdfjsLib.getDocument({ data: arrayBuffer }).promise;
|
||||
|
||||
let totalSignatures = 0;
|
||||
|
||||
// Check each page for signature annotations
|
||||
for (let pageNum = 1; pageNum <= pdf.numPages; pageNum++) {
|
||||
const page = await pdf.getPage(pageNum);
|
||||
const annotations = await page.getAnnotations();
|
||||
|
||||
// Count signature annotations (Type: /Sig)
|
||||
const signatureAnnotations = annotations.filter((annotation: any) =>
|
||||
annotation.subtype === 'Widget' &&
|
||||
annotation.fieldType === 'Sig'
|
||||
);
|
||||
|
||||
totalSignatures += signatureAnnotations.length;
|
||||
}
|
||||
|
||||
// Also check for document-level signatures in AcroForm
|
||||
const metadata = await pdf.getMetadata();
|
||||
if (metadata?.info?.Signature || metadata?.metadata?.has('dc:signature')) {
|
||||
totalSignatures = Math.max(totalSignatures, 1);
|
||||
}
|
||||
|
||||
// Clean up PDF.js document
|
||||
pdf.destroy();
|
||||
|
||||
return {
|
||||
hasSignatures: totalSignatures > 0,
|
||||
signatureCount: totalSignatures
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
console.warn('PDF signature detection failed:', error);
|
||||
return {
|
||||
hasSignatures: false,
|
||||
signatureCount: 0,
|
||||
error: error instanceof Error ? error.message : 'Detection failed'
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Detect if PDF files contain signatures using PDF.js client-side processing
|
||||
*/
|
||||
export const detectSignaturesInFiles = async (files: File[]): Promise<FileSignatureStatus[]> => {
|
||||
const results: FileSignatureStatus[] = [];
|
||||
|
||||
for (const file of files) {
|
||||
const result = await detectSignaturesInFile(file);
|
||||
results.push({ file, result });
|
||||
}
|
||||
|
||||
return results;
|
||||
};
|
||||
|
||||
/**
|
||||
* Hook for managing signature detection state
|
||||
*/
|
||||
export const useSignatureDetection = () => {
|
||||
const [detectionResults, setDetectionResults] = React.useState<FileSignatureStatus[]>([]);
|
||||
const [isDetecting, setIsDetecting] = React.useState(false);
|
||||
|
||||
const detectSignatures = async (files: File[]) => {
|
||||
if (files.length === 0) {
|
||||
setDetectionResults([]);
|
||||
return;
|
||||
}
|
||||
|
||||
setIsDetecting(true);
|
||||
try {
|
||||
const results = await detectSignaturesInFiles(files);
|
||||
setDetectionResults(results);
|
||||
} finally {
|
||||
setIsDetecting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const getFileSignatureStatus = (file: File): SignatureDetectionResult | null => {
|
||||
const result = detectionResults.find(r => r.file === file);
|
||||
return result ? result.result : null;
|
||||
};
|
||||
|
||||
const hasAnySignatures = detectionResults.some(r => r.result.hasSignatures);
|
||||
const totalSignatures = detectionResults.reduce((sum, r) => sum + (r.result.signatureCount || 0), 0);
|
||||
|
||||
return {
|
||||
detectionResults,
|
||||
isDetecting,
|
||||
detectSignatures,
|
||||
getFileSignatureStatus,
|
||||
hasAnySignatures,
|
||||
totalSignatures,
|
||||
reset: () => setDetectionResults([])
|
||||
};
|
||||
};
|
||||
|
||||
// Import React for the hook
|
||||
import React from 'react';
|
||||
59
frontend/src/tools/BookletImposition.tsx
Normal file
59
frontend/src/tools/BookletImposition.tsx
Normal file
@@ -0,0 +1,59 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { createToolFlow } from "../components/tools/shared/createToolFlow";
|
||||
import BookletImpositionSettings from "../components/tools/bookletImposition/BookletImpositionSettings";
|
||||
import { useBookletImpositionParameters } from "../hooks/tools/bookletImposition/useBookletImpositionParameters";
|
||||
import { useBookletImpositionOperation } from "../hooks/tools/bookletImposition/useBookletImpositionOperation";
|
||||
import { useBaseTool } from "../hooks/tools/shared/useBaseTool";
|
||||
import { useBookletImpositionTips } from "../components/tooltips/useBookletImpositionTips";
|
||||
import { BaseToolProps, ToolComponent } from "../types/tool";
|
||||
|
||||
const BookletImposition = (props: BaseToolProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const base = useBaseTool(
|
||||
'bookletImposition',
|
||||
useBookletImpositionParameters,
|
||||
useBookletImpositionOperation,
|
||||
props
|
||||
);
|
||||
|
||||
const bookletTips = useBookletImpositionTips();
|
||||
|
||||
return createToolFlow({
|
||||
files: {
|
||||
selectedFiles: base.selectedFiles,
|
||||
isCollapsed: base.hasResults,
|
||||
},
|
||||
steps: [
|
||||
{
|
||||
title: "Settings",
|
||||
isCollapsed: base.settingsCollapsed,
|
||||
onCollapsedClick: base.settingsCollapsed ? base.handleSettingsReset : undefined,
|
||||
tooltip: bookletTips,
|
||||
content: (
|
||||
<BookletImpositionSettings
|
||||
parameters={base.params.parameters}
|
||||
onParameterChange={base.params.updateParameter}
|
||||
disabled={base.endpointLoading}
|
||||
/>
|
||||
),
|
||||
},
|
||||
],
|
||||
executeButton: {
|
||||
text: t("bookletImposition.submit", "Create Booklet"),
|
||||
isVisible: !base.hasResults,
|
||||
loadingText: t("loading"),
|
||||
onClick: base.handleExecute,
|
||||
disabled: !base.params.validateParameters() || !base.hasFiles || !base.endpointEnabled,
|
||||
},
|
||||
review: {
|
||||
isVisible: base.hasResults,
|
||||
operation: base.operation,
|
||||
title: t("bookletImposition.title", "Booklet Imposition Results"),
|
||||
onFileClick: base.handleThumbnailClick,
|
||||
onUndo: base.handleUndo,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export default BookletImposition as ToolComponent;
|
||||
131
frontend/src/tools/CertSign.tsx
Normal file
131
frontend/src/tools/CertSign.tsx
Normal file
@@ -0,0 +1,131 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { createToolFlow } from "../components/tools/shared/createToolFlow";
|
||||
import CertificateTypeSettings from "../components/tools/certSign/CertificateTypeSettings";
|
||||
import CertificateFormatSettings from "../components/tools/certSign/CertificateFormatSettings";
|
||||
import CertificateFilesSettings from "../components/tools/certSign/CertificateFilesSettings";
|
||||
import SignatureAppearanceSettings from "../components/tools/certSign/SignatureAppearanceSettings";
|
||||
import { useCertSignParameters } from "../hooks/tools/certSign/useCertSignParameters";
|
||||
import { useCertSignOperation } from "../hooks/tools/certSign/useCertSignOperation";
|
||||
import { useCertificateTypeTips } from "../components/tooltips/useCertificateTypeTips";
|
||||
import { useSignatureAppearanceTips } from "../components/tooltips/useSignatureAppearanceTips";
|
||||
import { useSignModeTips } from "../components/tooltips/useSignModeTips";
|
||||
import { useBaseTool } from "../hooks/tools/shared/useBaseTool";
|
||||
import { BaseToolProps, ToolComponent } from "../types/tool";
|
||||
|
||||
const CertSign = (props: BaseToolProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const base = useBaseTool(
|
||||
'certSign',
|
||||
useCertSignParameters,
|
||||
useCertSignOperation,
|
||||
props
|
||||
);
|
||||
|
||||
const certTypeTips = useCertificateTypeTips();
|
||||
const appearanceTips = useSignatureAppearanceTips();
|
||||
const signModeTips = useSignModeTips();
|
||||
|
||||
// Check if certificate files are configured for appearance step
|
||||
const areCertFilesConfigured = () => {
|
||||
const params = base.params.parameters;
|
||||
|
||||
// Auto mode (server certificate) - always configured
|
||||
if (params.signMode === 'AUTO') {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Manual mode - check for required files based on cert type
|
||||
switch (params.certType) {
|
||||
case 'PEM':
|
||||
return !!(params.privateKeyFile && params.certFile);
|
||||
case 'PKCS12':
|
||||
case 'PFX':
|
||||
return !!params.p12File;
|
||||
case 'JKS':
|
||||
return !!params.jksFile;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
return createToolFlow({
|
||||
forceStepNumbers: true,
|
||||
files: {
|
||||
selectedFiles: base.selectedFiles,
|
||||
isCollapsed: base.hasResults,
|
||||
},
|
||||
steps: [
|
||||
{
|
||||
title: t("certSign.signMode.stepTitle", "Sign Mode"),
|
||||
isCollapsed: base.settingsCollapsed,
|
||||
onCollapsedClick: base.settingsCollapsed ? base.handleSettingsReset : undefined,
|
||||
tooltip: signModeTips,
|
||||
content: (
|
||||
<CertificateTypeSettings
|
||||
parameters={base.params.parameters}
|
||||
onParameterChange={base.params.updateParameter}
|
||||
disabled={base.endpointLoading}
|
||||
/>
|
||||
),
|
||||
},
|
||||
...(base.params.parameters.signMode === 'MANUAL' ? [{
|
||||
title: t("certSign.certTypeStep.stepTitle", "Certificate Format"),
|
||||
isCollapsed: base.settingsCollapsed,
|
||||
onCollapsedClick: base.settingsCollapsed ? base.handleSettingsReset : undefined,
|
||||
tooltip: certTypeTips,
|
||||
content: (
|
||||
<CertificateFormatSettings
|
||||
parameters={base.params.parameters}
|
||||
onParameterChange={base.params.updateParameter}
|
||||
disabled={base.endpointLoading}
|
||||
/>
|
||||
),
|
||||
}] : []),
|
||||
...(base.params.parameters.signMode === 'MANUAL' ? [{
|
||||
title: t("certSign.certFiles.stepTitle", "Certificate Files"),
|
||||
isCollapsed: base.settingsCollapsed,
|
||||
onCollapsedClick: base.settingsCollapsed ? base.handleSettingsReset : undefined,
|
||||
content: (
|
||||
<CertificateFilesSettings
|
||||
parameters={base.params.parameters}
|
||||
onParameterChange={base.params.updateParameter}
|
||||
disabled={base.endpointLoading}
|
||||
/>
|
||||
),
|
||||
}] : []),
|
||||
{
|
||||
title: t("certSign.appearance.stepTitle", "Signature Appearance"),
|
||||
isCollapsed: base.settingsCollapsed || !areCertFilesConfigured(),
|
||||
onCollapsedClick: (base.settingsCollapsed || !areCertFilesConfigured()) ? base.handleSettingsReset : undefined,
|
||||
tooltip: appearanceTips,
|
||||
content: (
|
||||
<SignatureAppearanceSettings
|
||||
parameters={base.params.parameters}
|
||||
onParameterChange={base.params.updateParameter}
|
||||
disabled={base.endpointLoading}
|
||||
/>
|
||||
),
|
||||
},
|
||||
],
|
||||
executeButton: {
|
||||
text: t("certSign.sign.submit", "Sign PDF"),
|
||||
isVisible: !base.hasResults,
|
||||
loadingText: t("loading"),
|
||||
onClick: base.handleExecute,
|
||||
disabled: !base.params.validateParameters() || !base.hasFiles || !base.endpointEnabled,
|
||||
},
|
||||
review: {
|
||||
isVisible: base.hasResults,
|
||||
operation: base.operation,
|
||||
title: t("certSign.sign.results", "Signed PDF"),
|
||||
onFileClick: base.handleThumbnailClick,
|
||||
onUndo: base.handleUndo,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// Static method to get the operation hook for automation
|
||||
CertSign.tool = () => useCertSignOperation;
|
||||
|
||||
export default CertSign as ToolComponent;
|
||||
@@ -55,6 +55,7 @@ const TOOL_IDS = [
|
||||
'devFolderScanning',
|
||||
'devSsoGuide',
|
||||
'devAirgapped',
|
||||
'bookletImposition',
|
||||
] as const;
|
||||
|
||||
// Tool identity - what PDF operation we're performing (type-safe)
|
||||
|
||||
@@ -31,4 +31,7 @@ export const URL_TO_TOOL_MAP: Record<string, ToolId> = {
|
||||
'/unlock-pdf-forms': 'unlockPDFForms',
|
||||
'/remove-certificate-sign': 'removeCertSign',
|
||||
'/remove-cert-sign': 'removeCertSign',
|
||||
'/cert-sign': 'certSign',
|
||||
'/manage-signatures': 'certSign',
|
||||
'/booklet-imposition': 'bookletImposition',
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user