From 6e6e77fb85f3ee2e58174d5b33040b860e9abf9a Mon Sep 17 00:00:00 2001 From: Anthony Stirling <77850077+frooodle@users.noreply.github.com> Date: Thu, 25 Sep 2025 20:21:11 +0100 Subject: [PATCH] invert! --- .../public/locales/en-GB/translation.json | 47 +++++++- .../replaceColor/ReplaceColorSettings.tsx | 108 ++++++++++++++++++ .../tooltips/useReplaceColorTips.ts | 40 +++++++ .../src/data/useTranslatedToolRegistry.tsx | 9 +- .../replaceColor/useReplaceColorOperation.ts | 38 ++++++ .../replaceColor/useReplaceColorParameters.ts | 29 +++++ frontend/src/tools/ReplaceColor.tsx | 58 ++++++++++ 7 files changed, 327 insertions(+), 2 deletions(-) create mode 100644 frontend/src/components/tools/replaceColor/ReplaceColorSettings.tsx create mode 100644 frontend/src/components/tooltips/useReplaceColorTips.ts create mode 100644 frontend/src/hooks/tools/replaceColor/useReplaceColorOperation.ts create mode 100644 frontend/src/hooks/tools/replaceColor/useReplaceColorParameters.ts create mode 100644 frontend/src/tools/ReplaceColor.tsx diff --git a/frontend/public/locales/en-GB/translation.json b/frontend/public/locales/en-GB/translation.json index 6b2b527cf..e6e211eba 100644 --- a/frontend/public/locales/en-GB/translation.json +++ b/frontend/public/locales/en-GB/translation.json @@ -621,6 +621,10 @@ "title": "Replace & Invert Colour", "desc": "Replace or invert colours in PDF documents" }, + "replaceColor": { + "title": "Replace & Invert Colour", + "desc": "Replace or invert colours in PDF documents" + }, "devApi": { "tags": "API,development,documentation", "title": "API", @@ -2509,7 +2513,7 @@ "3": "Custom(Customised colours)", "4": "Full-Invert(Invert all colours)", "5": "High contrast colour options", - "6": "white text on black background", + "6": "White text on black background", "7": "Black text on white background", "8": "Yellow text on black background", "9": "Green text on black background", @@ -2518,6 +2522,47 @@ }, "submit": "Replace" }, + "replaceColor": { + "labels": { + "settings": "Settings", + "colourOperation": "Colour operation" + }, + "options": { + "highContrast": "High contrast", + "invertAll": "Invert all colours", + "custom": "Custom" + }, + "tooltip": { + "header": { + "title": "Replace & Invert Colour Settings Overview" + }, + "description": { + "title": "Description", + "text": "Transform PDF colours to improve readability and accessibility. Choose from high contrast presets, invert all colours, or create custom colour schemes." + }, + "highContrast": { + "title": "High Contrast", + "text": "Apply predefined high contrast colour combinations designed for better readability and accessibility compliance.", + "bullet1": "White text on black background - Classic dark mode", + "bullet2": "Black text on white background - Standard high contrast", + "bullet3": "Yellow text on black background - High visibility option", + "bullet4": "Green text on black background - Alternative high contrast" + }, + "invertAll": { + "title": "Invert All Colours", + "text": "Completely invert all colours in the PDF, creating a negative-like effect. Useful for creating dark mode versions of documents or reducing eye strain in low-light conditions." + }, + "custom": { + "title": "Custom Colours", + "text": "Define your own text and background colours using the colour pickers. Perfect for creating branded documents or specific accessibility requirements.", + "bullet1": "Text colour - Choose the colour for text elements", + "bullet2": "Background colour - Set the background colour for the document" + } + }, + "error": { + "failed": "An error occurred while processing the colour replacement." + } + }, "replaceColorPdf": { "tags": "Replace Colour,Page operations,Back end,server side" }, diff --git a/frontend/src/components/tools/replaceColor/ReplaceColorSettings.tsx b/frontend/src/components/tools/replaceColor/ReplaceColorSettings.tsx new file mode 100644 index 000000000..8630d1811 --- /dev/null +++ b/frontend/src/components/tools/replaceColor/ReplaceColorSettings.tsx @@ -0,0 +1,108 @@ +import React from "react"; +import { Stack, Text, Select, ColorInput } from "@mantine/core"; +import { useTranslation } from "react-i18next"; +import { ReplaceColorParameters } from "../../../hooks/tools/replaceColor/useReplaceColorParameters"; + +interface ReplaceColorSettingsProps { + parameters: ReplaceColorParameters; + onParameterChange: (key: K, value: ReplaceColorParameters[K]) => void; + disabled?: boolean; +} + +const ReplaceColorSettings = ({ parameters, onParameterChange, disabled = false }: ReplaceColorSettingsProps) => { + const { t } = useTranslation(); + + const replaceAndInvertOptions = [ + { + value: 'HIGH_CONTRAST_COLOR', + label: t('replaceColor.options.highContrast', 'High contrast') + }, + { + value: 'FULL_INVERSION', + label: t('replaceColor.options.invertAll', 'Invert all colours') + }, + { + value: 'CUSTOM_COLOR', + label: t('replaceColor.options.custom', 'Custom') + } + ]; + + const highContrastOptions = [ + { + value: 'WHITE_TEXT_ON_BLACK', + label: t('replace-color.selectText.6', 'White text on black background') + }, + { + value: 'BLACK_TEXT_ON_WHITE', + label: t('replace-color.selectText.7', 'Black text on white background') + }, + { + value: 'YELLOW_TEXT_ON_BLACK', + label: t('replace-color.selectText.8', 'Yellow text on black background') + }, + { + value: 'GREEN_TEXT_ON_BLACK', + label: t('replace-color.selectText.9', 'Green text on black background') + } + ]; + + return ( + + + + {t('replaceColor.labels.colourOperation', 'Colour operation')} + + value && onParameterChange('highContrastColorCombination', value as ReplaceColorParameters['highContrastColorCombination'])} + data={highContrastOptions} + disabled={disabled} + /> + + )} + + {parameters.replaceAndInvertOption === 'CUSTOM_COLOR' && ( + <> + + + {t('replace-color.selectText.10', 'Choose text Color')} + + onParameterChange('textColor', value)} + format="hex" + disabled={disabled} + /> + + + + + {t('replace-color.selectText.11', 'Choose background Color')} + + onParameterChange('backGroundColor', value)} + format="hex" + disabled={disabled} + /> + + + )} + + ); +}; + +export default ReplaceColorSettings; \ No newline at end of file diff --git a/frontend/src/components/tooltips/useReplaceColorTips.ts b/frontend/src/components/tooltips/useReplaceColorTips.ts new file mode 100644 index 000000000..58cc5f9d5 --- /dev/null +++ b/frontend/src/components/tooltips/useReplaceColorTips.ts @@ -0,0 +1,40 @@ +import { useTranslation } from 'react-i18next'; +import { TooltipContent } from '../../types/tips'; + +export const useReplaceColorTips = (): TooltipContent => { + const { t } = useTranslation(); + + return { + header: { + title: t("replaceColor.tooltip.header.title", "Replace & Invert Colour Settings Overview") + }, + tips: [ + { + title: t("replaceColor.tooltip.description.title", "Description"), + description: t("replaceColor.tooltip.description.text", "Transform PDF colours to improve readability and accessibility. Choose from high contrast presets, invert all colours, or create custom colour schemes.") + }, + { + title: t("replaceColor.tooltip.highContrast.title", "High Contrast"), + description: t("replaceColor.tooltip.highContrast.text", "Apply predefined high contrast colour combinations designed for better readability and accessibility compliance."), + bullets: [ + t("replaceColor.tooltip.highContrast.bullet1", "White text on black background - Classic dark mode"), + t("replaceColor.tooltip.highContrast.bullet2", "Black text on white background - Standard high contrast"), + t("replaceColor.tooltip.highContrast.bullet3", "Yellow text on black background - High visibility option"), + t("replaceColor.tooltip.highContrast.bullet4", "Green text on black background - Alternative high contrast") + ] + }, + { + title: t("replaceColor.tooltip.invertAll.title", "Invert All Colours"), + description: t("replaceColor.tooltip.invertAll.text", "Completely invert all colours in the PDF, creating a negative-like effect. Useful for creating dark mode versions of documents or reducing eye strain in low-light conditions.") + }, + { + title: t("replaceColor.tooltip.custom.title", "Custom Colours"), + description: t("replaceColor.tooltip.custom.text", "Define your own text and background colours using the colour pickers. Perfect for creating branded documents or specific accessibility requirements."), + bullets: [ + t("replaceColor.tooltip.custom.bullet1", "Text colour - Choose the colour for text elements"), + t("replaceColor.tooltip.custom.bullet2", "Background colour - Set the background colour for the document") + ] + } + ] + }; +}; \ No newline at end of file diff --git a/frontend/src/data/useTranslatedToolRegistry.tsx b/frontend/src/data/useTranslatedToolRegistry.tsx index 5400b5e98..65bda0e0c 100644 --- a/frontend/src/data/useTranslatedToolRegistry.tsx +++ b/frontend/src/data/useTranslatedToolRegistry.tsx @@ -50,6 +50,7 @@ import { redactOperationConfig } from "../hooks/tools/redact/useRedactOperation" import { rotateOperationConfig } from "../hooks/tools/rotate/useRotateOperation"; import { changeMetadataOperationConfig } from "../hooks/tools/changeMetadata/useChangeMetadataOperation"; import { cropOperationConfig } from "../hooks/tools/crop/useCropOperation"; +import { replaceColorOperationConfig } from "../hooks/tools/replaceColor/useReplaceColorOperation"; import CompressSettings from "../components/tools/compress/CompressSettings"; import SplitSettings from "../components/tools/split/SplitSettings"; import AddPasswordSettings from "../components/tools/addPassword/AddPasswordSettings"; @@ -68,12 +69,14 @@ import RedactSingleStepSettings from "../components/tools/redact/RedactSingleSte import RotateSettings from "../components/tools/rotate/RotateSettings"; import Redact from "../tools/Redact"; import AdjustPageScale from "../tools/AdjustPageScale"; +import ReplaceColor from "../tools/ReplaceColor"; import { ToolId } from "../types/toolId"; import MergeSettings from '../components/tools/merge/MergeSettings'; import { adjustPageScaleOperationConfig } from "../hooks/tools/adjustPageScale/useAdjustPageScaleOperation"; import AdjustPageScaleSettings from "../components/tools/adjustPageScale/AdjustPageScaleSettings"; import ChangeMetadataSingleStep from "../components/tools/changeMetadata/ChangeMetadataSingleStep"; import CropSettings from "../components/tools/crop/CropSettings"; +import ReplaceColorSettings from "../components/tools/replaceColor/ReplaceColorSettings"; const showPlaceholderTools = true; // Show all tools; grey out unavailable ones in UI @@ -642,10 +645,14 @@ export function useFlatToolRegistry(): ToolRegistry { replaceColorPdf: { icon: , name: t("home.replaceColorPdf.title", "Replace & Invert Color"), - component: null, + component: ReplaceColor, description: t("home.replaceColorPdf.desc", "Replace or invert colors in PDF documents"), categoryId: ToolCategoryId.ADVANCED_TOOLS, subcategoryId: SubcategoryId.ADVANCED_FORMATTING, + maxFiles: -1, + endpoints: ["replace-invert-pdf"], + operationConfig: replaceColorOperationConfig, + settingsComponent: ReplaceColorSettings, synonyms: getSynonyms(t, "replaceColorPdf"), }, addImage: { diff --git a/frontend/src/hooks/tools/replaceColor/useReplaceColorOperation.ts b/frontend/src/hooks/tools/replaceColor/useReplaceColorOperation.ts new file mode 100644 index 000000000..e2a101a1c --- /dev/null +++ b/frontend/src/hooks/tools/replaceColor/useReplaceColorOperation.ts @@ -0,0 +1,38 @@ +import { useTranslation } from 'react-i18next'; +import { ToolType, useToolOperation } from '../shared/useToolOperation'; +import { createStandardErrorHandler } from '../../../utils/toolErrorHandler'; +import { ReplaceColorParameters, defaultParameters } from './useReplaceColorParameters'; + +export const buildReplaceColorFormData = (parameters: ReplaceColorParameters, file: File): FormData => { + const formData = new FormData(); + formData.append('fileInput', file); + + formData.append('replaceAndInvertOption', parameters.replaceAndInvertOption); + + if (parameters.replaceAndInvertOption === 'HIGH_CONTRAST_COLOR') { + formData.append('highContrastColorCombination', parameters.highContrastColorCombination); + } else if (parameters.replaceAndInvertOption === 'CUSTOM_COLOR') { + formData.append('textColor', parameters.textColor); + formData.append('backGroundColor', parameters.backGroundColor); + } + + return formData; +}; + +export const replaceColorOperationConfig = { + toolType: ToolType.singleFile, + buildFormData: buildReplaceColorFormData, + operationType: 'replaceColor', + endpoint: '/api/v1/misc/replace-invert-pdf', + multiFileEndpoint: false, + defaultParameters, +} as const; + +export const useReplaceColorOperation = () => { + const { t } = useTranslation(); + + return useToolOperation({ + ...replaceColorOperationConfig, + getErrorMessage: createStandardErrorHandler(t('replaceColor.error.failed', 'An error occurred while processing the color replacement.')) + }); +}; \ No newline at end of file diff --git a/frontend/src/hooks/tools/replaceColor/useReplaceColorParameters.ts b/frontend/src/hooks/tools/replaceColor/useReplaceColorParameters.ts new file mode 100644 index 000000000..1c1d07b93 --- /dev/null +++ b/frontend/src/hooks/tools/replaceColor/useReplaceColorParameters.ts @@ -0,0 +1,29 @@ +import { BaseParameters } from '../../../types/parameters'; +import { useBaseParameters, BaseParametersHook } from '../shared/useBaseParameters'; + +export interface ReplaceColorParameters extends BaseParameters { + replaceAndInvertOption: 'HIGH_CONTRAST_COLOR' | 'CUSTOM_COLOR' | 'FULL_INVERSION'; + highContrastColorCombination: 'WHITE_TEXT_ON_BLACK' | 'BLACK_TEXT_ON_WHITE' | 'YELLOW_TEXT_ON_BLACK' | 'GREEN_TEXT_ON_BLACK'; + textColor: string; + backGroundColor: string; +} + +export const defaultParameters: ReplaceColorParameters = { + replaceAndInvertOption: 'HIGH_CONTRAST_COLOR', + highContrastColorCombination: 'WHITE_TEXT_ON_BLACK', + textColor: '#000000', + backGroundColor: '#ffffff', +}; + +export type ReplaceColorParametersHook = BaseParametersHook; + +export const useReplaceColorParameters = (): ReplaceColorParametersHook => { + return useBaseParameters({ + defaultParameters, + endpointName: 'replace-invert-pdf', + validateFn: (params) => { + // All parameters are always valid as they have defaults + return true; + }, + }); +}; \ No newline at end of file diff --git a/frontend/src/tools/ReplaceColor.tsx b/frontend/src/tools/ReplaceColor.tsx new file mode 100644 index 000000000..5486fc591 --- /dev/null +++ b/frontend/src/tools/ReplaceColor.tsx @@ -0,0 +1,58 @@ +import { useTranslation } from "react-i18next"; +import { createToolFlow } from "../components/tools/shared/createToolFlow"; +import ReplaceColorSettings from "../components/tools/replaceColor/ReplaceColorSettings"; +import { useReplaceColorParameters } from "../hooks/tools/replaceColor/useReplaceColorParameters"; +import { useReplaceColorOperation } from "../hooks/tools/replaceColor/useReplaceColorOperation"; +import { useBaseTool } from "../hooks/tools/shared/useBaseTool"; +import { BaseToolProps, ToolComponent } from "../types/tool"; +import { useReplaceColorTips } from "../components/tooltips/useReplaceColorTips"; + +const ReplaceColor = (props: BaseToolProps) => { + const { t } = useTranslation(); + const replaceColorTips = useReplaceColorTips(); + + const base = useBaseTool( + 'replaceColor', + useReplaceColorParameters, + useReplaceColorOperation, + props + ); + + return createToolFlow({ + files: { + selectedFiles: base.selectedFiles, + isCollapsed: base.hasResults, + }, + steps: [ + { + title: t("replaceColor.labels.settings", "Settings"), + isCollapsed: base.settingsCollapsed, + onCollapsedClick: base.settingsCollapsed ? base.handleSettingsReset : undefined, + tooltip: replaceColorTips, + content: ( + + ), + }, + ], + executeButton: { + text: t("replace-color.submit", "Replace"), + 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("replace-color.title", "Replace-Invert-Color"), + onFileClick: base.handleThumbnailClick, + onUndo: base.handleUndo, + }, + }); +}; + +export default ReplaceColor as ToolComponent; \ No newline at end of file