diff --git a/frontend/public/locales/en-US/translation.json b/frontend/public/locales/en-US/translation.json
index 944a1df22..00cfe7265 100644
--- a/frontend/public/locales/en-US/translation.json
+++ b/frontend/public/locales/en-US/translation.json
@@ -1737,5 +1737,48 @@
"text": "To make these permissions unchangeable, use the Add Password tool to set an owner password."
}
}
+ },
+ "watermark": {
+ "completed": "Watermark added",
+ "submit": "Add Watermark",
+ "filenamePrefix": "watermarked",
+ "error": {
+ "failed": "An error occurred while adding watermark to the PDF."
+ },
+ "watermarkType": {
+ "text": "Text",
+ "image": "Image"
+ },
+ "settings": {
+ "type": "Watermark Type",
+ "text": {
+ "label": "Watermark Text",
+ "placeholder": "Enter watermark text"
+ },
+ "image": {
+ "label": "Watermark Image",
+ "choose": "Choose Image",
+ "selected": "Selected: {{filename}}"
+ },
+ "fontSize": "Font Size",
+ "position": "Position",
+ "rotation": "Rotation (degrees)",
+ "opacity": "Opacity (%)",
+ "spacing": {
+ "width": "Width Spacing",
+ "height": "Height Spacing"
+ }
+ },
+ "positions": {
+ "topLeft": "Top Left",
+ "topCenter": "Top Center",
+ "topRight": "Top Right",
+ "centerLeft": "Center Left",
+ "center": "Center",
+ "centerRight": "Center Right",
+ "bottomLeft": "Bottom Left",
+ "bottomCenter": "Bottom Center",
+ "bottomRight": "Bottom Right"
+ }
}
}
diff --git a/frontend/src/components/tools/addWatermark/AddWatermarkSettings.tsx b/frontend/src/components/tools/addWatermark/AddWatermarkSettings.tsx
new file mode 100644
index 000000000..3dcc63bfe
--- /dev/null
+++ b/frontend/src/components/tools/addWatermark/AddWatermarkSettings.tsx
@@ -0,0 +1,174 @@
+import React, { useRef } from "react";
+import { Button, Stack, Text, NumberInput, Select, TextInput, FileButton } from "@mantine/core";
+import { useTranslation } from "react-i18next";
+
+interface AddWatermarkParameters {
+ watermarkType: 'text' | 'image';
+ watermarkText: string;
+ watermarkImage?: File;
+ fontSize: number;
+ rotation: number;
+ opacity: number;
+ widthSpacer: number;
+ heightSpacer: number;
+ position: string;
+ overrideX?: number;
+ overrideY?: number;
+}
+
+interface AddWatermarkSettingsProps {
+ parameters: AddWatermarkParameters;
+ onParameterChange: (key: keyof AddWatermarkParameters, value: any) => void;
+ disabled?: boolean;
+}
+
+const AddWatermarkSettings = ({ parameters, onParameterChange, disabled = false }: AddWatermarkSettingsProps) => {
+ const { t } = useTranslation();
+ const resetRef = useRef<() => void>(null);
+
+ const positionOptions = [
+ { value: 'topLeft', label: 'Top Left' },
+ { value: 'topCenter', label: 'Top Center' },
+ { value: 'topRight', label: 'Top Right' },
+ { value: 'centerLeft', label: 'Center Left' },
+ { value: 'center', label: 'Center' },
+ { value: 'centerRight', label: 'Center Right' },
+ { value: 'bottomLeft', label: 'Bottom Left' },
+ { value: 'bottomCenter', label: 'Bottom Center' },
+ { value: 'bottomRight', label: 'Bottom Right' }
+ ];
+
+ return (
+
+ {/* Watermark Type Selection */}
+
+ Watermark Type
+
+
+
+
+
+
+ {/* Text Watermark Settings */}
+ {parameters.watermarkType === 'text' && (
+
+ Watermark Text
+ onParameterChange('watermarkText', e.target.value)}
+ disabled={disabled}
+ />
+
+ Font Size
+ onParameterChange('fontSize', value || 12)}
+ min={8}
+ max={72}
+ disabled={disabled}
+ />
+
+ )}
+
+ {/* Image Watermark Settings */}
+ {parameters.watermarkType === 'image' && (
+
+ Watermark Image
+ onParameterChange('watermarkImage', file)}
+ accept="image/*"
+ disabled={disabled}
+ >
+ {(props) => (
+
+ )}
+
+ {parameters.watermarkImage && (
+
+ Selected: {parameters.watermarkImage.name}
+
+ )}
+
+ )}
+
+ {/* Position Settings */}
+
+ Position
+
+
+ {/* Appearance Settings */}
+
+ Rotation (degrees)
+ onParameterChange('rotation', value || 0)}
+ min={-360}
+ max={360}
+ disabled={disabled}
+ />
+
+ Opacity (%)
+ onParameterChange('opacity', value || 50)}
+ min={0}
+ max={100}
+ disabled={disabled}
+ />
+
+
+ {/* Spacing Settings */}
+
+ Width Spacing
+ onParameterChange('widthSpacer', value || 50)}
+ min={0}
+ max={200}
+ disabled={disabled}
+ />
+
+ Height Spacing
+ onParameterChange('heightSpacer', value || 50)}
+ min={0}
+ max={200}
+ disabled={disabled}
+ />
+
+
+ );
+};
+
+export default AddWatermarkSettings;
\ No newline at end of file
diff --git a/frontend/src/hooks/tools/addWatermark/useAddWatermarkOperation.ts b/frontend/src/hooks/tools/addWatermark/useAddWatermarkOperation.ts
new file mode 100644
index 000000000..9251d91d5
--- /dev/null
+++ b/frontend/src/hooks/tools/addWatermark/useAddWatermarkOperation.ts
@@ -0,0 +1,44 @@
+import { useTranslation } from 'react-i18next';
+import { useToolOperation } from '../shared/useToolOperation';
+import { createStandardErrorHandler } from '../../../utils/toolErrorHandler';
+import { AddWatermarkParameters } from './useAddWatermarkParameters';
+
+const buildFormData = (parameters: AddWatermarkParameters, file: File): FormData => {
+ const formData = new FormData();
+ formData.append("fileInput", file);
+
+ if (parameters.watermarkType === 'text') {
+ formData.append("watermarkText", parameters.watermarkText);
+ } else if (parameters.watermarkImage) {
+ formData.append("watermarkImage", parameters.watermarkImage);
+ }
+
+ formData.append("fontSize", parameters.fontSize.toString());
+ formData.append("rotation", parameters.rotation.toString());
+ formData.append("opacity", (parameters.opacity / 100).toString()); // Convert percentage to decimal
+ formData.append("widthSpacer", parameters.widthSpacer.toString());
+ formData.append("heightSpacer", parameters.heightSpacer.toString());
+ formData.append("position", parameters.position);
+
+ if (parameters.overrideX !== undefined) {
+ formData.append("overrideX", parameters.overrideX.toString());
+ }
+ if (parameters.overrideY !== undefined) {
+ formData.append("overrideY", parameters.overrideY.toString());
+ }
+
+ return formData;
+};
+
+export const useAddWatermarkOperation = () => {
+ const { t } = useTranslation();
+
+ return useToolOperation({
+ operationType: 'watermark',
+ endpoint: '/api/v1/security/add-watermark',
+ buildFormData,
+ filePrefix: t('watermark.filenamePrefix', 'watermarked') + '_',
+ multiFileEndpoint: false, // Individual API calls per file
+ getErrorMessage: createStandardErrorHandler(t('watermark.error.failed', 'An error occurred while adding watermark to the PDF.'))
+ });
+};
\ No newline at end of file
diff --git a/frontend/src/hooks/tools/addWatermark/useAddWatermarkParameters.ts b/frontend/src/hooks/tools/addWatermark/useAddWatermarkParameters.ts
new file mode 100644
index 000000000..4f3405221
--- /dev/null
+++ b/frontend/src/hooks/tools/addWatermark/useAddWatermarkParameters.ts
@@ -0,0 +1,56 @@
+import { useState, useCallback } from 'react';
+
+export interface AddWatermarkParameters {
+ watermarkType: 'text' | 'image';
+ watermarkText: string;
+ watermarkImage?: File;
+ fontSize: number;
+ rotation: number;
+ opacity: number;
+ widthSpacer: number;
+ heightSpacer: number;
+ position: string;
+ overrideX?: number;
+ overrideY?: number;
+}
+
+const defaultParameters: AddWatermarkParameters = {
+ watermarkType: 'text',
+ watermarkText: '',
+ fontSize: 12,
+ rotation: 0,
+ opacity: 50,
+ widthSpacer: 50,
+ heightSpacer: 50,
+ position: 'center'
+};
+
+export const useAddWatermarkParameters = () => {
+ const [parameters, setParameters] = useState(defaultParameters);
+
+ const updateParameter = useCallback((
+ key: K,
+ value: AddWatermarkParameters[K]
+ ) => {
+ setParameters(prev => ({ ...prev, [key]: value }));
+ }, []);
+
+ const resetParameters = useCallback(() => {
+ setParameters(defaultParameters);
+ }, []);
+
+ const validateParameters = useCallback((): boolean => {
+ if (parameters.watermarkType === 'text') {
+ return parameters.watermarkText.trim().length > 0;
+ } else {
+ return parameters.watermarkImage !== undefined;
+ }
+ }, [parameters]);
+
+ return {
+ parameters,
+ updateParameter,
+ resetParameters,
+ validateParameters
+ };
+};
\ No newline at end of file
diff --git a/frontend/src/hooks/useToolManagement.tsx b/frontend/src/hooks/useToolManagement.tsx
index aac9c5421..7c7dfb947 100644
--- a/frontend/src/hooks/useToolManagement.tsx
+++ b/frontend/src/hooks/useToolManagement.tsx
@@ -6,6 +6,7 @@ import SwapHorizIcon from "@mui/icons-material/SwapHoriz";
import ApiIcon from "@mui/icons-material/Api";
import CleaningServicesIcon from "@mui/icons-material/CleaningServices";
import LockIcon from "@mui/icons-material/Lock";
+import BrandingWatermarkIcon from "@mui/icons-material/BrandingWatermark";
import { useMultipleEndpointsEnabled } from "./useEndpointConfig";
import { Tool, ToolDefinition, BaseToolProps, ToolRegistry } from "../types/tool";
@@ -104,6 +105,15 @@ const toolDefinitions: Record = {
description: "Change document restrictions and permissions",
endpoints: ["add-password"]
},
+ watermark: {
+ id: "watermark",
+ icon: ,
+ component: React.lazy(() => import("../tools/AddWatermark")),
+ maxFiles: -1,
+ category: "security",
+ description: "Add text or image watermarks to PDF files",
+ endpoints: ["add-watermark"]
+ },
};
diff --git a/frontend/src/tools/AddWatermark.tsx b/frontend/src/tools/AddWatermark.tsx
new file mode 100644
index 000000000..dc549745d
--- /dev/null
+++ b/frontend/src/tools/AddWatermark.tsx
@@ -0,0 +1,167 @@
+import React, { useEffect, useMemo } from "react";
+import { Button, Stack, Text } from "@mantine/core";
+import { useTranslation } from "react-i18next";
+import DownloadIcon from "@mui/icons-material/Download";
+import { useEndpointEnabled } from "../hooks/useEndpointConfig";
+import { useFileContext } from "../contexts/FileContext";
+import { useToolFileSelection } from "../contexts/FileSelectionContext";
+
+import ToolStep, { ToolStepContainer } from "../components/tools/shared/ToolStep";
+import OperationButton from "../components/tools/shared/OperationButton";
+import ErrorNotification from "../components/tools/shared/ErrorNotification";
+import FileStatusIndicator from "../components/tools/shared/FileStatusIndicator";
+import ResultsPreview from "../components/tools/shared/ResultsPreview";
+
+import AddWatermarkSettings from "../components/tools/addWatermark/AddWatermarkSettings";
+
+import { useAddWatermarkParameters } from "../hooks/tools/addWatermark/useAddWatermarkParameters";
+import { useAddWatermarkOperation } from "../hooks/tools/addWatermark/useAddWatermarkOperation";
+import { BaseToolProps } from "../types/tool";
+
+const AddWatermark = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
+ const { t } = useTranslation();
+ const { setCurrentMode } = useFileContext();
+ const { selectedFiles } = useToolFileSelection();
+
+ const watermarkParams = useAddWatermarkParameters();
+ const watermarkOperation = useAddWatermarkOperation();
+
+ // Endpoint validation
+ const { enabled: endpointEnabled, loading: endpointLoading } = useEndpointEnabled("add-watermark");
+
+ useEffect(() => {
+ watermarkOperation.resetResults();
+ onPreviewFile?.(null);
+ }, [watermarkParams.parameters, selectedFiles]);
+
+ const handleAddWatermark = async () => {
+ try {
+ await watermarkOperation.executeOperation(
+ watermarkParams.parameters,
+ selectedFiles
+ );
+ if (watermarkOperation.files && onComplete) {
+ onComplete(watermarkOperation.files);
+ }
+ } catch (error) {
+ if (onError) {
+ onError(error instanceof Error ? error.message : 'Add watermark operation failed');
+ }
+ }
+ };
+
+ const handleThumbnailClick = (file: File) => {
+ onPreviewFile?.(file);
+ sessionStorage.setItem('previousMode', 'watermark');
+ setCurrentMode('viewer');
+ };
+
+ const handleSettingsReset = () => {
+ watermarkOperation.resetResults();
+ onPreviewFile?.(null);
+ setCurrentMode('watermark');
+ };
+
+ const hasFiles = selectedFiles.length > 0;
+ const hasResults = watermarkOperation.files.length > 0 || watermarkOperation.downloadUrl !== null;
+ const filesCollapsed = hasFiles;
+ const settingsCollapsed = hasResults;
+
+ const previewResults = useMemo(() =>
+ watermarkOperation.files?.map((file, index) => ({
+ file,
+ thumbnail: watermarkOperation.thumbnails[index]
+ })) || [],
+ [watermarkOperation.files, watermarkOperation.thumbnails]
+ );
+
+ return (
+
+
+ {/* Files Step */}
+
+
+
+
+ {/* Settings Step */}
+
+
+
+
+
+
+
+
+ {/* Results Step */}
+
+
+ {watermarkOperation.status && (
+ {watermarkOperation.status}
+ )}
+
+
+
+ {watermarkOperation.downloadUrl && (
+ }
+ color="green"
+ fullWidth
+ mb="md"
+ >
+ {t("download", "Download")}
+
+ )}
+
+
+
+
+
+
+ );
+}
+
+export default AddWatermark;
\ No newline at end of file
diff --git a/frontend/src/types/fileContext.ts b/frontend/src/types/fileContext.ts
index dec7b03a1..a17b48f3c 100644
--- a/frontend/src/types/fileContext.ts
+++ b/frontend/src/types/fileContext.ts
@@ -16,7 +16,8 @@ export type ModeType =
| 'convert'
| 'sanitize'
| 'addPassword'
- | 'changePermissions';
+ | 'changePermissions'
+ | 'watermark';
export type ViewType = 'viewer' | 'pageEditor' | 'fileEditor';