Booklet and server sign (#4371)

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
Co-authored-by: a <a>
Co-authored-by: ConnorYoh <40631091+ConnorYoh@users.noreply.github.com>
Co-authored-by: Reece Browne <74901996+reecebrowne@users.noreply.github.com>
This commit is contained in:
Anthony Stirling
2025-09-23 11:24:48 +01:00
committed by GitHub
parent c76edebf0f
commit 46a4a978fc
36 changed files with 2447 additions and 61 deletions

View File

@@ -15,7 +15,7 @@ interface ButtonSelectorProps<T> {
fullWidth?: boolean;
}
const ButtonSelector = <T extends string>({
const ButtonSelector = <T extends string | number>({
value,
onChange,
options,
@@ -45,7 +45,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'
}}
>
{option.label}

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View 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")
]
}
]
};
};

View 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")
]
}
]
};
};

View 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.")
}
]
};
};

View 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>.")
}
]
};
};

View File

@@ -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")
]
}
]
};
};

View File

@@ -19,6 +19,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";
@@ -36,6 +38,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";
@@ -54,6 +58,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";
@@ -159,11 +165,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" />,
@@ -267,8 +277,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"),
@@ -390,7 +398,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,

View File

@@ -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.'))
});
};

View File

@@ -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;
},
});
};

View 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.'))
});
};

View 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;
}
},
});
};

View File

@@ -23,6 +23,7 @@ export interface AppConfig {
license?: string;
GoogleDriveEnabled?: boolean;
SSOAutoLogin?: boolean;
serverCertificateEnabled?: boolean;
error?: string;
}

View 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';

View 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;

View 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;

View File

@@ -55,6 +55,7 @@ const TOOL_IDS = [
'devFolderScanning',
'devSsoGuide',
'devAirgapped',
'bookletImposition',
] as const;
// Tool identity - what PDF operation we're performing (type-safe)

View File

@@ -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',
};