mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2025-08-26 06:09:23 +00:00
Basic tool creation
This commit is contained in:
parent
129e4d00e9
commit
85b612bf22
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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 (
|
||||
<Stack gap="md">
|
||||
{/* Watermark Type Selection */}
|
||||
<Stack gap="sm">
|
||||
<Text size="sm" fw={500}>Watermark Type</Text>
|
||||
<div style={{ display: 'flex', gap: '4px' }}>
|
||||
<Button
|
||||
variant={parameters.watermarkType === 'text' ? 'filled' : 'outline'}
|
||||
color={parameters.watermarkType === 'text' ? 'blue' : 'gray'}
|
||||
onClick={() => onParameterChange('watermarkType', 'text')}
|
||||
disabled={disabled}
|
||||
style={{ flex: 1, height: 'auto', minHeight: '40px', fontSize: '11px' }}
|
||||
>
|
||||
<div style={{ textAlign: 'center', lineHeight: '1.1', fontSize: '11px' }}>
|
||||
Text
|
||||
</div>
|
||||
</Button>
|
||||
<Button
|
||||
variant={parameters.watermarkType === 'image' ? 'filled' : 'outline'}
|
||||
color={parameters.watermarkType === 'image' ? 'blue' : 'gray'}
|
||||
onClick={() => onParameterChange('watermarkType', 'image')}
|
||||
disabled={disabled}
|
||||
style={{ flex: 1, height: 'auto', minHeight: '40px', fontSize: '11px' }}
|
||||
>
|
||||
<div style={{ textAlign: 'center', lineHeight: '1.1', fontSize: '11px' }}>
|
||||
Image
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
</Stack>
|
||||
|
||||
{/* Text Watermark Settings */}
|
||||
{parameters.watermarkType === 'text' && (
|
||||
<Stack gap="sm">
|
||||
<Text size="sm" fw={500}>Watermark Text</Text>
|
||||
<TextInput
|
||||
placeholder="Enter watermark text"
|
||||
value={parameters.watermarkText}
|
||||
onChange={(e) => onParameterChange('watermarkText', e.target.value)}
|
||||
disabled={disabled}
|
||||
/>
|
||||
|
||||
<Text size="sm" fw={500}>Font Size</Text>
|
||||
<NumberInput
|
||||
value={parameters.fontSize}
|
||||
onChange={(value) => onParameterChange('fontSize', value || 12)}
|
||||
min={8}
|
||||
max={72}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</Stack>
|
||||
)}
|
||||
|
||||
{/* Image Watermark Settings */}
|
||||
{parameters.watermarkType === 'image' && (
|
||||
<Stack gap="sm">
|
||||
<Text size="sm" fw={500}>Watermark Image</Text>
|
||||
<FileButton
|
||||
resetRef={resetRef}
|
||||
onChange={(file) => onParameterChange('watermarkImage', file)}
|
||||
accept="image/*"
|
||||
disabled={disabled}
|
||||
>
|
||||
{(props) => (
|
||||
<Button {...props} variant="outline" fullWidth>
|
||||
{parameters.watermarkImage ? parameters.watermarkImage.name : 'Choose Image'}
|
||||
</Button>
|
||||
)}
|
||||
</FileButton>
|
||||
{parameters.watermarkImage && (
|
||||
<Text size="xs" c="dimmed">
|
||||
Selected: {parameters.watermarkImage.name}
|
||||
</Text>
|
||||
)}
|
||||
</Stack>
|
||||
)}
|
||||
|
||||
{/* Position Settings */}
|
||||
<Stack gap="sm">
|
||||
<Text size="sm" fw={500}>Position</Text>
|
||||
<Select
|
||||
value={parameters.position}
|
||||
onChange={(value) => value && onParameterChange('position', value)}
|
||||
data={positionOptions}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</Stack>
|
||||
|
||||
{/* Appearance Settings */}
|
||||
<Stack gap="sm">
|
||||
<Text size="sm" fw={500}>Rotation (degrees)</Text>
|
||||
<NumberInput
|
||||
value={parameters.rotation}
|
||||
onChange={(value) => onParameterChange('rotation', value || 0)}
|
||||
min={-360}
|
||||
max={360}
|
||||
disabled={disabled}
|
||||
/>
|
||||
|
||||
<Text size="sm" fw={500}>Opacity (%)</Text>
|
||||
<NumberInput
|
||||
value={parameters.opacity}
|
||||
onChange={(value) => onParameterChange('opacity', value || 50)}
|
||||
min={0}
|
||||
max={100}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</Stack>
|
||||
|
||||
{/* Spacing Settings */}
|
||||
<Stack gap="sm">
|
||||
<Text size="sm" fw={500}>Width Spacing</Text>
|
||||
<NumberInput
|
||||
value={parameters.widthSpacer}
|
||||
onChange={(value) => onParameterChange('widthSpacer', value || 50)}
|
||||
min={0}
|
||||
max={200}
|
||||
disabled={disabled}
|
||||
/>
|
||||
|
||||
<Text size="sm" fw={500}>Height Spacing</Text>
|
||||
<NumberInput
|
||||
value={parameters.heightSpacer}
|
||||
onChange={(value) => onParameterChange('heightSpacer', value || 50)}
|
||||
min={0}
|
||||
max={200}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddWatermarkSettings;
|
@ -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<AddWatermarkParameters>({
|
||||
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.'))
|
||||
});
|
||||
};
|
@ -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<AddWatermarkParameters>(defaultParameters);
|
||||
|
||||
const updateParameter = useCallback(<K extends keyof AddWatermarkParameters>(
|
||||
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
|
||||
};
|
||||
};
|
@ -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<string, ToolDefinition> = {
|
||||
description: "Change document restrictions and permissions",
|
||||
endpoints: ["add-password"]
|
||||
},
|
||||
watermark: {
|
||||
id: "watermark",
|
||||
icon: <BrandingWatermarkIcon />,
|
||||
component: React.lazy(() => import("../tools/AddWatermark")),
|
||||
maxFiles: -1,
|
||||
category: "security",
|
||||
description: "Add text or image watermarks to PDF files",
|
||||
endpoints: ["add-watermark"]
|
||||
},
|
||||
|
||||
};
|
||||
|
||||
|
167
frontend/src/tools/AddWatermark.tsx
Normal file
167
frontend/src/tools/AddWatermark.tsx
Normal file
@ -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 (
|
||||
<ToolStepContainer>
|
||||
<Stack gap="sm" h="100%" p="sm" style={{ overflow: 'auto' }}>
|
||||
{/* Files Step */}
|
||||
<ToolStep
|
||||
title="Files"
|
||||
isVisible={true}
|
||||
isCollapsed={filesCollapsed}
|
||||
isCompleted={filesCollapsed}
|
||||
completedMessage={hasFiles ?
|
||||
selectedFiles.length === 1
|
||||
? `Selected: ${selectedFiles[0].name}`
|
||||
: `Selected: ${selectedFiles.length} files`
|
||||
: undefined}
|
||||
>
|
||||
<FileStatusIndicator
|
||||
selectedFiles={selectedFiles}
|
||||
placeholder="Select a PDF file in the main view to get started"
|
||||
/>
|
||||
</ToolStep>
|
||||
|
||||
{/* Settings Step */}
|
||||
<ToolStep
|
||||
title="Settings"
|
||||
isVisible={hasFiles}
|
||||
isCollapsed={settingsCollapsed}
|
||||
isCompleted={settingsCollapsed}
|
||||
onCollapsedClick={settingsCollapsed ? handleSettingsReset : undefined}
|
||||
completedMessage={settingsCollapsed ? "Watermark added" : undefined}
|
||||
>
|
||||
<Stack gap="sm">
|
||||
<AddWatermarkSettings
|
||||
parameters={watermarkParams.parameters}
|
||||
onParameterChange={watermarkParams.updateParameter}
|
||||
disabled={endpointLoading}
|
||||
/>
|
||||
|
||||
<OperationButton
|
||||
onClick={handleAddWatermark}
|
||||
isLoading={watermarkOperation.isLoading}
|
||||
disabled={!watermarkParams.validateParameters() || !hasFiles || !endpointEnabled}
|
||||
loadingText={t("loading")}
|
||||
submitText="Add Watermark and Review"
|
||||
/>
|
||||
</Stack>
|
||||
</ToolStep>
|
||||
|
||||
{/* Results Step */}
|
||||
<ToolStep
|
||||
title="Results"
|
||||
isVisible={hasResults}
|
||||
>
|
||||
<Stack gap="sm">
|
||||
{watermarkOperation.status && (
|
||||
<Text size="sm" c="dimmed">{watermarkOperation.status}</Text>
|
||||
)}
|
||||
|
||||
<ErrorNotification
|
||||
error={watermarkOperation.errorMessage}
|
||||
onClose={watermarkOperation.clearError}
|
||||
/>
|
||||
|
||||
{watermarkOperation.downloadUrl && (
|
||||
<Button
|
||||
component="a"
|
||||
href={watermarkOperation.downloadUrl}
|
||||
download={watermarkOperation.downloadFilename}
|
||||
leftSection={<DownloadIcon />}
|
||||
color="green"
|
||||
fullWidth
|
||||
mb="md"
|
||||
>
|
||||
{t("download", "Download")}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<ResultsPreview
|
||||
files={previewResults}
|
||||
onFileClick={handleThumbnailClick}
|
||||
isGeneratingThumbnails={watermarkOperation.isGeneratingThumbnails}
|
||||
title="Watermark Results"
|
||||
/>
|
||||
</Stack>
|
||||
</ToolStep>
|
||||
</Stack>
|
||||
</ToolStepContainer>
|
||||
);
|
||||
}
|
||||
|
||||
export default AddWatermark;
|
@ -16,7 +16,8 @@ export type ModeType =
|
||||
| 'convert'
|
||||
| 'sanitize'
|
||||
| 'addPassword'
|
||||
| 'changePermissions';
|
||||
| 'changePermissions'
|
||||
| 'watermark';
|
||||
|
||||
export type ViewType = 'viewer' | 'pageEditor' | 'fileEditor';
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user