Initial working version of Auto Redact

This commit is contained in:
James Brunton 2025-09-08 10:18:45 +01:00
parent 11d23a2d43
commit 1991cdbe07
8 changed files with 434 additions and 47 deletions

View File

@ -498,13 +498,9 @@
"title": "Show Javascript", "title": "Show Javascript",
"desc": "Searches and displays any JS injected into a PDF" "desc": "Searches and displays any JS injected into a PDF"
}, },
"autoRedact": {
"title": "Auto Redact",
"desc": "Auto Redacts(Blacks out) text in a PDF based on input text"
},
"redact": { "redact": {
"title": "Manual Redaction", "title": "Redact",
"desc": "Redacts a PDF based on selected text, drawn shapes and/or selected page(s)" "desc": "Redacts (blacks out) a PDF based on selected text, drawn shapes and/or selected page(s)"
}, },
"overlay-pdfs": { "overlay-pdfs": {
"title": "Overlay PDFs", "title": "Overlay PDFs",
@ -1583,24 +1579,33 @@
"downloadJS": "Download Javascript", "downloadJS": "Download Javascript",
"submit": "Show" "submit": "Show"
}, },
"autoRedact": { "redact": {
"tags": "Redact,Hide,black out,black,marker,hidden", "tags": "Redact,Hide,black out,black,marker,hidden,auto redact,manual redact",
"title": "Auto Redact", "title": "Redact",
"submit": "Redact",
"error": {
"failed": "An error occurred while redacting the PDF."
},
"modeSelector": {
"title": "Redaction Mode",
"automatic": "Automatic",
"automaticDesc": "Redact text based on search terms",
"manual": "Manual",
"manualDesc": "Click and drag to redact specific areas",
"manualComingSoon": "Manual redaction coming soon"
},
"auto": {
"header": "Auto Redact", "header": "Auto Redact",
"colorLabel": "Colour", "colorLabel": "Colour",
"textsToRedactLabel": "Text to Redact (line-separated)", "textsToRedactLabel": "Text to Redact (line-separated)",
"textsToRedactPlaceholder": "e.g. \\nConfidential \\nTop-Secret", "textsToRedactPlaceholder": "e.g. \nConfidential \nTop-Secret",
"useRegexLabel": "Use Regex", "useRegexLabel": "Use Regex",
"wholeWordSearchLabel": "Whole Word Search", "wholeWordSearchLabel": "Whole Word Search",
"customPaddingLabel": "Custom Extra Padding", "customPaddingLabel": "Custom Extra Padding",
"convertPDFToImageLabel": "Convert PDF to PDF-Image (Used to remove text behind the box)", "convertPDFToImageLabel": "Convert PDF to PDF-Image (Used to remove text behind the box)"
"submitButton": "Submit"
}, },
"redact": { "manual": {
"tags": "Redact,Hide,black out,black,marker,hidden,manual",
"title": "Manual Redaction",
"header": "Manual Redaction", "header": "Manual Redaction",
"submit": "Redact",
"textBasedRedaction": "Text based Redaction", "textBasedRedaction": "Text based Redaction",
"pageBasedRedaction": "Page-based Redaction", "pageBasedRedaction": "Page-based Redaction",
"convertPDFToImageLabel": "Convert PDF to PDF-Image (Used to remove text behind the box)", "convertPDFToImageLabel": "Convert PDF to PDF-Image (Used to remove text behind the box)",
@ -1627,6 +1632,7 @@
"colourPicker": "Colour Picker", "colourPicker": "Colour Picker",
"findCurrentOutlineItem": "Find current outline item", "findCurrentOutlineItem": "Find current outline item",
"applyChanges": "Apply Changes" "applyChanges": "Apply Changes"
}
}, },
"tableExtraxt": { "tableExtraxt": {
"tags": "CSV,Table Extraction,extract,convert" "tags": "CSV,Table Extraction,extract,convert"

View File

@ -0,0 +1,129 @@
import { Stack, Text, Textarea, Select, NumberInput, Divider } from "@mantine/core";
import { useTranslation } from "react-i18next";
import { RedactParameters } from "../../../hooks/tools/redact/useRedactParameters";
interface AutomaticRedactSettingsProps {
parameters: RedactParameters;
onParameterChange: <K extends keyof RedactParameters>(key: K, value: RedactParameters[K]) => void;
disabled?: boolean;
}
const AutomaticRedactSettings = ({ parameters, onParameterChange, disabled = false }: AutomaticRedactSettingsProps) => {
const { t } = useTranslation();
const colorOptions = [
{ value: '#000000', label: t('black', 'Black') },
{ value: '#FFFFFF', label: t('white', 'White') },
{ value: '#FF0000', label: t('red', 'Red') },
{ value: '#00FF00', label: t('green', 'Green') },
{ value: '#0000FF', label: t('blue', 'Blue') },
];
return (
<Stack gap="md">
<Divider ml='-md' />
{/* Text to Redact */}
<Stack gap="sm">
<Text size="sm" fw={500}>
{t('redact.auto.textsToRedactLabel', 'Text to Redact (line-separated)')}
</Text>
<Textarea
placeholder={t('redact.auto.textsToRedactPlaceholder', 'e.g. \nConfidential \nTop-Secret')}
value={parameters.listOfText}
onChange={(e) => onParameterChange('listOfText', e.target.value)}
disabled={disabled}
rows={4}
required
/>
</Stack>
<Divider />
{/* Redaction Color */}
<Stack gap="sm">
<Text size="sm" fw={500}>
{t('redact.auto.colorLabel', 'Color')}
</Text>
<Select
value={parameters.redactColor}
onChange={(value) => {
if (value) {
onParameterChange('redactColor', value);
}
}}
disabled={disabled}
data={colorOptions}
/>
</Stack>
<Divider />
{/* Search Options */}
<Stack gap="sm">
<Text size="sm" fw={500}>Search Options</Text>
<label
style={{ display: 'flex', alignItems: 'center', gap: '8px' }}
title="Use regular expressions for pattern matching"
>
<input
type="checkbox"
checked={parameters.useRegex}
onChange={(e) => onParameterChange('useRegex', e.target.checked)}
disabled={disabled}
/>
<Text size="sm">{t('redact.auto.useRegexLabel', 'Use Regex')}</Text>
</label>
<label
style={{ display: 'flex', alignItems: 'center', gap: '8px' }}
title="Match whole words only, not partial matches within words"
>
<input
type="checkbox"
checked={parameters.wholeWordSearch}
onChange={(e) => onParameterChange('wholeWordSearch', e.target.checked)}
disabled={disabled}
/>
<Text size="sm">{t('redact.auto.wholeWordSearchLabel', 'Whole Word Search')}</Text>
</label>
</Stack>
<Divider />
{/* Advanced Options */}
<Stack gap="sm">
<Text size="sm" fw={500}>Advanced Options</Text>
<Stack gap="sm">
<Text size="sm">{t('redact.auto.customPaddingLabel', 'Custom Extra Padding')}</Text>
<NumberInput
value={parameters.customPadding}
onChange={(value) => onParameterChange('customPadding', typeof value === 'number' ? value : 0.1)}
min={0}
max={10}
step={0.1}
disabled={disabled}
placeholder="0.1"
/>
</Stack>
<label
style={{ display: 'flex', alignItems: 'center', gap: '8px' }}
title="Convert PDF to PDF-Image to remove text behind the redaction box"
>
<input
type="checkbox"
checked={parameters.convertPDFToImage}
onChange={(e) => onParameterChange('convertPDFToImage', e.target.checked)}
disabled={disabled}
/>
<Text size="sm">{t('redact.auto.convertPDFToImageLabel', 'Convert PDF to PDF-Image (Used to remove text behind the box)')}</Text>
</label>
</Stack>
</Stack>
);
};
export default AutomaticRedactSettings;

View File

@ -0,0 +1,50 @@
import { useTranslation } from 'react-i18next';
import { Radio, Stack, Text, Tooltip } from '@mantine/core';
import { RedactMode } from '../../../hooks/tools/redact/useRedactParameters';
interface RedactModeSelectorProps {
mode: RedactMode;
onModeChange: (mode: RedactMode) => void;
disabled?: boolean;
}
export default function RedactModeSelector({ mode, onModeChange, disabled }: RedactModeSelectorProps) {
const { t } = useTranslation();
return (
<Stack gap="sm">
<Text size="sm" fw={600}>
{t('redact.modeSelector.title', 'Redaction Mode')}
</Text>
<Radio.Group
value={mode}
onChange={(value) => onModeChange(value as RedactMode)}
>
<Stack gap="xs">
<Radio
value="automatic"
label={t('redact.modeSelector.automatic', 'Automatic')}
description={t('redact.modeSelector.automaticDesc', 'Redact text based on search terms')}
disabled={disabled}
/>
<Tooltip
label={t('redact.modeSelector.manualComingSoon', 'Manual redaction coming soon')}
position="right"
>
<div>
<Radio
value="manual"
label={t('redact.modeSelector.manual', 'Manual')}
description={t('redact.modeSelector.manualDesc', 'Click and drag to redact specific areas')}
disabled={true}
style={{ opacity: 0.5 }}
/>
</div>
</Tooltip>
</Stack>
</Radio.Group>
</Stack>
);
}

View File

@ -0,0 +1,38 @@
import { Stack } from "@mantine/core";
import { RedactParameters } from "../../../hooks/tools/redact/useRedactParameters";
import RedactModeSelector from "./RedactModeSelector";
import AutomaticRedactSettings from "./AutomaticRedactSettings";
interface RedactSettingsProps {
parameters: RedactParameters;
onParameterChange: <K extends keyof RedactParameters>(key: K, value: RedactParameters[K]) => void;
disabled?: boolean;
}
const RedactSettings = ({ parameters, onParameterChange, disabled = false }: RedactSettingsProps) => {
return (
<Stack gap="md">
<RedactModeSelector
mode={parameters.mode}
onModeChange={(mode) => onParameterChange('mode', mode)}
disabled={disabled}
/>
{parameters.mode === 'automatic' && (
<AutomaticRedactSettings
parameters={parameters}
onParameterChange={onParameterChange}
disabled={disabled}
/>
)}
{parameters.mode === 'manual' && (
<div style={{ padding: '20px', textAlign: 'center', color: '#666' }}>
Manual redaction interface will be available here when implemented.
</div>
)}
</Stack>
);
};
export default RedactSettings;

View File

@ -32,6 +32,7 @@ import { removeCertificateSignOperationConfig } from "../hooks/tools/removeCerti
import { changePermissionsOperationConfig } from "../hooks/tools/changePermissions/useChangePermissionsOperation"; import { changePermissionsOperationConfig } from "../hooks/tools/changePermissions/useChangePermissionsOperation";
import { autoRenameOperationConfig } from "../hooks/tools/autoRename/useAutoRenameOperation"; import { autoRenameOperationConfig } from "../hooks/tools/autoRename/useAutoRenameOperation";
import { flattenOperationConfig } from "../hooks/tools/flatten/useFlattenOperation"; import { flattenOperationConfig } from "../hooks/tools/flatten/useFlattenOperation";
import { redactOperationConfig } from "../hooks/tools/redact/useRedactOperation";
import CompressSettings from "../components/tools/compress/CompressSettings"; import CompressSettings from "../components/tools/compress/CompressSettings";
import SplitSettings from "../components/tools/split/SplitSettings"; import SplitSettings from "../components/tools/split/SplitSettings";
import AddPasswordSettings from "../components/tools/addPassword/AddPasswordSettings"; import AddPasswordSettings from "../components/tools/addPassword/AddPasswordSettings";
@ -44,6 +45,8 @@ import OCRSettings from "../components/tools/ocr/OCRSettings";
import ConvertSettings from "../components/tools/convert/ConvertSettings"; import ConvertSettings from "../components/tools/convert/ConvertSettings";
import ChangePermissionsSettings from "../components/tools/changePermissions/ChangePermissionsSettings"; import ChangePermissionsSettings from "../components/tools/changePermissions/ChangePermissionsSettings";
import FlattenSettings from "../components/tools/flatten/FlattenSettings"; import FlattenSettings from "../components/tools/flatten/FlattenSettings";
import RedactSettings from "../components/tools/redact/RedactSettings";
import Redact from "../tools/Redact";
import { ToolId } from "../types/toolId"; import { ToolId } from "../types/toolId";
const showPlaceholderTools = true; // Show all tools; grey out unavailable ones in UI const showPlaceholderTools = true; // Show all tools; grey out unavailable ones in UI
@ -701,10 +704,14 @@ export function useFlatToolRegistry(): ToolRegistry {
redact: { redact: {
icon: <LocalIcon icon="visibility-off-rounded" width="1.5rem" height="1.5rem" />, icon: <LocalIcon icon="visibility-off-rounded" width="1.5rem" height="1.5rem" />,
name: t("home.redact.title", "Redact"), name: t("home.redact.title", "Redact"),
component: null, component: Redact,
description: t("home.redact.desc", "Permanently remove sensitive information from PDF documents"), description: t("home.redact.desc", "Permanently remove sensitive information from PDF documents"),
categoryId: ToolCategoryId.RECOMMENDED_TOOLS, categoryId: ToolCategoryId.RECOMMENDED_TOOLS,
subcategoryId: SubcategoryId.GENERAL, subcategoryId: SubcategoryId.GENERAL,
maxFiles: -1,
endpoints: ["auto-redact"],
operationConfig: redactOperationConfig,
settingsComponent: RedactSettings,
}, },
}; };

View File

@ -0,0 +1,50 @@
import { useTranslation } from 'react-i18next';
import { useToolOperation, ToolType } from '../shared/useToolOperation';
import { createStandardErrorHandler } from '../../../utils/toolErrorHandler';
import { RedactParameters, defaultParameters } from './useRedactParameters';
// Static configuration that can be used by both the hook and automation executor
export const buildRedactFormData = (parameters: RedactParameters, file: File): FormData => {
const formData = new FormData();
formData.append("fileInput", file);
if (parameters.mode === 'automatic') {
formData.append("listOfText", parameters.listOfText);
formData.append("useRegex", parameters.useRegex.toString());
formData.append("wholeWordSearch", parameters.wholeWordSearch.toString());
formData.append("redactColor", parameters.redactColor.replace('#', ''));
formData.append("customPadding", parameters.customPadding.toString());
formData.append("convertPDFToImage", parameters.convertPDFToImage.toString());
} else {
// Manual mode parameters would go here when implemented
throw new Error('Manual redaction not yet implemented');
}
return formData;
};
// Static configuration object
export const redactOperationConfig = {
toolType: ToolType.singleFile,
buildFormData: buildRedactFormData,
operationType: 'redact',
endpoint: (parameters: RedactParameters) => {
if (parameters.mode === 'automatic') {
return '/api/v1/security/auto-redact';
} else {
// Manual redaction endpoint would go here when implemented
throw new Error('Manual redaction not yet implemented');
}
},
filePrefix: 'redacted_',
defaultParameters,
} as const;
export const useRedactOperation = () => {
const { t } = useTranslation();
return useToolOperation<RedactParameters>({
...redactOperationConfig,
getErrorMessage: createStandardErrorHandler(t('redact.error.failed', 'An error occurred while redacting the PDF.'))
});
};

View File

@ -0,0 +1,45 @@
import { useBaseParameters, BaseParametersHook } from '../shared/useBaseParameters';
export type RedactMode = 'automatic' | 'manual';
export interface RedactParameters {
mode: RedactMode;
// Automatic redaction parameters
listOfText: string;
useRegex: boolean;
wholeWordSearch: boolean;
redactColor: string;
customPadding: number;
convertPDFToImage: boolean;
}
export const defaultParameters: RedactParameters = {
mode: 'automatic',
listOfText: '',
useRegex: false,
wholeWordSearch: false,
redactColor: '#000000',
customPadding: 0.1,
convertPDFToImage: true,
};
export const useRedactParameters = (): BaseParametersHook<RedactParameters> => {
return useBaseParameters<RedactParameters>({
defaultParameters,
endpointName: (params) => {
if (params.mode === 'automatic') {
return '/api/v1/security/auto-redact';
}
// Manual redaction endpoint would go here when implemented
throw new Error('Manual redaction not yet implemented');
},
validateFn: (params) => {
if (params.mode === 'automatic') {
return params.listOfText.trim().length > 0;
}
// Manual mode validation would go here when implemented
return false;
}
});
};

View File

@ -0,0 +1,62 @@
import { useTranslation } from "react-i18next";
import { createToolFlow } from "../components/tools/shared/createToolFlow";
import RedactSettings from "../components/tools/redact/RedactSettings";
import { useRedactParameters } from "../hooks/tools/redact/useRedactParameters";
import { useRedactOperation } from "../hooks/tools/redact/useRedactOperation";
import { useBaseTool } from "../hooks/tools/shared/useBaseTool";
import { BaseToolProps, ToolComponent } from "../types/tool";
const Redact = (props: BaseToolProps) => {
const { t } = useTranslation();
const base = useBaseTool(
'redact',
useRedactParameters,
useRedactOperation,
props
);
const isExecuteDisabled = () => {
if (base.params.parameters.mode === 'manual') {
return true; // Manual mode not implemented yet
}
return !base.params.validateParameters() || !base.hasFiles || !base.endpointEnabled;
};
return createToolFlow({
files: {
selectedFiles: base.selectedFiles,
isCollapsed: base.hasResults,
},
steps: [
{
title: "Settings",
isCollapsed: base.settingsCollapsed,
onCollapsedClick: base.settingsCollapsed ? base.handleSettingsReset : undefined,
content: (
<RedactSettings
parameters={base.params.parameters}
onParameterChange={base.params.updateParameter}
disabled={base.endpointLoading}
/>
),
},
],
executeButton: {
text: t("redact.submit", "Redact"),
isVisible: !base.hasResults,
loadingText: t("loading"),
onClick: base.handleExecute,
disabled: isExecuteDisabled(),
},
review: {
isVisible: base.hasResults,
operation: base.operation,
title: t("redact.title", "Redaction Results"),
onFileClick: base.handleThumbnailClick,
onUndo: base.handleUndo,
},
});
};
export default Redact as ToolComponent;