Overlay PDF tool (#4620)

# Description of Changes

- Added the OverlayPDF tool

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.
This commit is contained in:
EthanHealy01 2025-10-10 14:35:09 +01:00 committed by GitHub
parent b695e3900e
commit a339f71116
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 525 additions and 19 deletions

View File

@ -598,11 +598,6 @@
"title": "Redact", "title": "Redact",
"desc": "Redacts (blacks out) 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)"
}, },
"overlayPdfs": {
"tags": "overlay,combine,stack",
"title": "Overlay PDFs",
"desc": "Overlays PDFs on-top of another PDF"
},
"splitBySections": { "splitBySections": {
"tags": "split,sections,divide", "tags": "split,sections,divide",
"title": "Split PDF by Sections", "title": "Split PDF by Sections",
@ -2550,11 +2545,15 @@
"overlay-pdfs": { "overlay-pdfs": {
"tags": "Overlay", "tags": "Overlay",
"header": "Overlay PDF Files", "header": "Overlay PDF Files",
"title": "Overlay PDFs",
"desc": "Overlay one PDF on top of another",
"baseFile": { "baseFile": {
"label": "Select Base PDF File" "label": "Select Base PDF File"
}, },
"overlayFiles": { "overlayFiles": {
"label": "Select Overlay PDF Files" "label": "Select Overlay PDF Files",
"placeholder": "Choose PDF(s)...",
"addMore": "Add more PDFs..."
}, },
"mode": { "mode": {
"label": "Select Overlay Mode", "label": "Select Overlay Mode",
@ -2564,14 +2563,49 @@
}, },
"counts": { "counts": {
"label": "Overlay Counts (for Fixed Repeat Mode)", "label": "Overlay Counts (for Fixed Repeat Mode)",
"placeholder": "Enter comma-separated counts (e.g., 2,3,1)" "placeholder": "Enter comma-separated counts (e.g., 2,3,1)",
"item": "Count for file"
}, },
"position": { "position": {
"label": "Select Overlay Position", "label": "Select Overlay Position",
"foreground": "Foreground", "foreground": "Foreground",
"background": "Background" "background": "Background"
}, },
"submit": "Submit" "submit": "Submit",
"settings": {
"title": "Settings"
},
"results": {
"title": "Overlay Results"
},
"tooltip": {
"header": {
"title": "Overlay PDFs Overview"
},
"description": {
"title": "Description",
"text": "Combine a base PDF with one or more overlay PDFs. Overlays can be applied page-by-page in different modes and placed in the foreground or background."
},
"mode": {
"title": "Overlay Mode",
"text": "Choose how to distribute overlay pages across the base PDF pages.",
"sequential": "Sequential Overlay: Use pages from the first overlay PDF until it ends, then move to the next.",
"interleaved": "Interleaved Overlay: Take one page from each overlay in turn.",
"fixedRepeat": "Fixed Repeat Overlay: Take a set number of pages from each overlay before moving to the next. Use Counts to set the numbers."
},
"position": {
"title": "Overlay Position",
"text": "Foreground places the overlay on top of the page. Background places it behind."
},
"overlayFiles": {
"title": "Overlay Files",
"text": "Select one or more PDFs to overlay on the base. The order of these files affects how pages are applied in Sequential and Fixed Repeat modes."
},
"counts": {
"title": "Counts (Fixed Repeat only)",
"text": "Provide a positive number for each overlay file showing how many pages to take before moving to the next. Required when mode is Fixed Repeat."
}
}
}, },
"split-by-sections": { "split-by-sections": {
"tags": "Section Split, Divide, Customize,Customise", "tags": "Section Split, Divide, Customize,Customise",

View File

@ -1521,11 +1521,15 @@
"overlay-pdfs": { "overlay-pdfs": {
"tags": "Overlay", "tags": "Overlay",
"header": "Overlay PDF Files", "header": "Overlay PDF Files",
"title": "Overlay PDFs",
"desc": "Overlay one PDF on top of another",
"baseFile": { "baseFile": {
"label": "Select Base PDF File" "label": "Select Base PDF File"
}, },
"overlayFiles": { "overlayFiles": {
"label": "Select Overlay PDF Files" "label": "Select Overlay PDF Files",
"placeholder": "Choose PDF(s)...",
"addMore": "Add more PDFs..."
}, },
"mode": { "mode": {
"label": "Select Overlay Mode", "label": "Select Overlay Mode",
@ -1535,14 +1539,49 @@
}, },
"counts": { "counts": {
"label": "Overlay Counts (for Fixed Repeat Mode)", "label": "Overlay Counts (for Fixed Repeat Mode)",
"placeholder": "Enter comma-separated counts (e.g., 2,3,1)" "placeholder": "Enter comma-separated counts (e.g., 2,3,1)",
"item": "Count for file"
}, },
"position": { "position": {
"label": "Select Overlay Position", "label": "Select Overlay Position",
"foreground": "Foreground", "foreground": "Foreground",
"background": "Background" "background": "Background"
}, },
"submit": "Submit" "submit": "Submit",
"settings": {
"title": "Settings"
},
"results": {
"title": "Overlay Results"
},
"tooltip": {
"header": {
"title": "Overlay PDFs Overview"
},
"description": {
"title": "Description",
"text": "Combine a base PDF with one or more overlay PDFs. Overlays can be applied page-by-page in different modes and placed in the foreground or background."
},
"mode": {
"title": "Overlay Mode",
"text": "Choose how to distribute overlay pages across the base PDF pages.",
"sequential": "Sequential Overlay: Use pages from the first overlay PDF until it ends, then move to the next.",
"interleaved": "Interleaved Overlay: Take one page from each overlay in turn.",
"fixedRepeat": "Fixed Repeat Overlay: Take a set number of pages from each overlay before moving to the next. Use Counts to set the numbers."
},
"position": {
"title": "Overlay Position",
"text": "Foreground places the overlay on top of the page. Background places it behind."
},
"overlayFiles": {
"title": "Overlay Files",
"text": "Select one or more PDFs to overlay on the base. The order of these files affects how pages are applied in Sequential and Fixed Repeat modes."
},
"counts": {
"title": "Counts (Fixed Repeat only)",
"text": "Provide a positive number for each overlay file showing how many pages to take before moving to the next. Required when mode is Fixed Repeat."
}
}
}, },
"split-by-sections": { "split-by-sections": {
"tags": "Section Split, Divide, Customize", "tags": "Section Split, Divide, Customize",

View File

@ -125,6 +125,7 @@ const FileManager: React.FC<FileManagerProps> = ({ selectedTool }) => {
radius="md" radius="md"
className="overflow-hidden p-0" className="overflow-hidden p-0"
withCloseButton={false} withCloseButton={false}
zIndex={1100}
styles={{ styles={{
content: { content: {
position: 'relative', position: 'relative',

View File

@ -125,6 +125,7 @@ const FilePickerModal = ({
title={t("fileUpload.selectFromStorage", "Select Files from Storage")} title={t("fileUpload.selectFromStorage", "Select Files from Storage")}
size="lg" size="lg"
scrollAreaComponent={ScrollArea.Autosize} scrollAreaComponent={ScrollArea.Autosize}
zIndex={1100}
> >
<Stack gap="md"> <Stack gap="md">
{storedFiles.length === 0 ? ( {storedFiles.length === 0 ? (

View File

@ -0,0 +1,50 @@
.fileListContainer {
max-height: 16.25rem;
overflow-y: auto;
overflow-x: hidden;
width: 100%;
}
.fileItem {
border: 1px solid var(--mantine-color-gray-3);
border-radius: var(--mantine-radius-sm);
align-items: center;
width: 100%;
}
.fileNameContainer {
flex: 1;
min-width: 0;
}
.fileName {
font-size: var(--mantine-font-size-sm);
font-weight: 400;
line-height: 1.2;
display: -webkit-box;
-webkit-line-clamp: 1;
line-clamp: 1;
-webkit-box-orient: vertical;
overflow: hidden;
white-space: normal;
word-break: break-word;
}
.fileSize {
flex-shrink: 0;
}
.removeButton {
flex-shrink: 0;
}
.countLabel {
width: 140px;
flex-shrink: 0;
}
.fileGroup {
flex: 1;
min-width: 0;
align-items: center;
}

View File

@ -0,0 +1,178 @@
import { Stack, Text, Group, Select, SegmentedControl, NumberInput, Button, ActionIcon, Divider } from '@mantine/core';
import { useTranslation } from 'react-i18next';
import { type OverlayPdfsParameters, type OverlayMode } from '../../../hooks/tools/overlayPdfs/useOverlayPdfsParameters';
import LocalIcon from '../../shared/LocalIcon';
import { useFilesModalContext } from '../../../contexts/FilesModalContext';
import styles from './OverlayPdfsSettings.module.css';
interface OverlayPdfsSettingsProps {
parameters: OverlayPdfsParameters;
onParameterChange: <K extends keyof OverlayPdfsParameters>(key: K, value: OverlayPdfsParameters[K]) => void;
disabled?: boolean;
}
export default function OverlayPdfsSettings({ parameters, onParameterChange, disabled = false }: OverlayPdfsSettingsProps) {
const { t } = useTranslation();
const { openFilesModal } = useFilesModalContext();
const handleOverlayFilesChange = (files: File[]) => {
onParameterChange('overlayFiles', files);
// Reset counts to match number of files if in FixedRepeatOverlay
if (parameters.overlayMode === 'FixedRepeatOverlay') {
const nextCounts = files.map((_, i) => parameters.counts[i] && parameters.counts[i] > 0 ? parameters.counts[i] : 1);
onParameterChange('counts', nextCounts);
}
};
const handleModeChange = (mode: OverlayMode) => {
onParameterChange('overlayMode', mode);
if (mode !== 'FixedRepeatOverlay') {
onParameterChange('counts', []);
} else if (parameters.overlayFiles?.length > 0) {
onParameterChange('counts', parameters.overlayFiles.map((_, i) => parameters.counts[i] && parameters.counts[i] > 0 ? parameters.counts[i] : 1));
}
};
const handleOpenOverlayFilesModal = () => {
if (disabled) return;
openFilesModal({
customHandler: (files: File[]) => {
handleOverlayFilesChange([...(parameters.overlayFiles || []), ...files]);
}
});
};
return (
<Stack gap="md">
<Stack gap="xs">
<Text size="sm" fw={500}>{t('overlay-pdfs.mode.label', 'Overlay Mode')}</Text>
<Select
data={[
{ value: 'SequentialOverlay', label: t('overlay-pdfs.mode.sequential', 'Sequential Overlay') },
{ value: 'InterleavedOverlay', label: t('overlay-pdfs.mode.interleaved', 'Interleaved Overlay') },
{ value: 'FixedRepeatOverlay', label: t('overlay-pdfs.mode.fixedRepeat', 'Fixed Repeat Overlay') },
]}
value={parameters.overlayMode}
onChange={(v) => handleModeChange((v || 'SequentialOverlay') as OverlayMode)}
disabled={disabled}
/>
</Stack>
<Divider />
<Stack gap="xs">
<Text size="sm" fw={500}>{t('overlay-pdfs.position.label', 'Overlay Position')}</Text>
<SegmentedControl
value={String(parameters.overlayPosition)}
onChange={(v) => onParameterChange('overlayPosition', (v === '1' ? 1 : 0) as 0 | 1)}
data={[
{ label: t('overlay-pdfs.position.foreground', 'Foreground'), value: '0' },
{ label: t('overlay-pdfs.position.background', 'Background'), value: '1' },
]}
disabled={disabled}
/>
</Stack>
{parameters.overlayMode === 'FixedRepeatOverlay' && (
<>
<Divider />
<Stack gap="xs">
<Text size="sm" fw={500}>{t('overlay-pdfs.counts.label', 'Overlay Counts')}</Text>
{parameters.overlayFiles?.length > 0 ? (
<Stack gap="xs">
{parameters.overlayFiles.map((_, index) => (
<Group key={index} gap="xs" wrap="nowrap">
<Text size="sm" className={styles.countLabel}>
{t('overlay-pdfs.counts.item', 'Count for file')} {index + 1}
</Text>
<NumberInput
min={1}
step={1}
value={parameters.counts[index] ?? 1}
onChange={(value) => {
const next = [...(parameters.counts || [])];
next[index] = Number(value) || 1;
onParameterChange('counts', next);
}}
disabled={disabled}
/>
</Group>
))}
</Stack>
) : (
<Text size="sm" c="dimmed">
{t('overlay-pdfs.counts.noFiles', 'Add overlay files to configure counts')}
</Text>
)}
</Stack>
</>
)}
<Divider />
<Stack gap="xs">
<Text size="sm" fw={500}>{t('overlay-pdfs.overlayFiles.label', 'Overlay Files')}</Text>
<Button
size="xs"
color="blue"
onClick={handleOpenOverlayFilesModal}
disabled={disabled}
leftSection={<LocalIcon icon="add" width="14" height="14" />}
fullWidth
>
{parameters.overlayFiles?.length > 0
? t('overlay-pdfs.overlayFiles.addMore', 'Add more PDFs...')
: t('overlay-pdfs.overlayFiles.placeholder', 'Choose PDF(s)...')}
</Button>
{parameters.overlayFiles?.length > 0 && (() => {
return (
<div className={styles.fileListContainer}>
<Stack gap="xs">
{parameters.overlayFiles.map((file, index) => (
<Group
key={index}
justify="space-between"
p="xs"
className={styles.fileItem}
>
<Group gap="xs" className={styles.fileGroup}>
<div className={styles.fileNameContainer}>
<div
className={styles.fileName}
title={file.name}
>
{file.name}
</div>
</div>
<Text size="xs" c="dimmed" className={styles.fileSize}>
({(file.size / 1024).toFixed(1)} KB)
</Text>
</Group>
<ActionIcon
size="sm"
variant="subtle"
color="red"
className={styles.removeButton}
onClick={() => {
const next = (parameters.overlayFiles || []).filter((_, i) => i !== index);
handleOverlayFilesChange(next);
}}
disabled={disabled}
>
<LocalIcon icon="close-rounded" width="14" height="14" />
</ActionIcon>
</Group>
))}
</Stack>
</div>
);
})()}
</Stack>
</Stack>
);
}

View File

@ -0,0 +1,56 @@
import { useTranslation } from 'react-i18next';
import { TooltipContent } from '../../types/tips';
export const useOverlayPdfsTips = (): TooltipContent => {
const { t } = useTranslation();
return {
header: {
title: t('overlay-pdfs.tooltip.header.title', 'Overlay PDFs Overview')
},
tips: [
{
title: t('overlay-pdfs.tooltip.description.title', 'Description'),
description: t(
'overlay-pdfs.tooltip.description.text',
'Combine a base PDF with one or more overlay PDFs. Overlays can be applied page-by-page in different modes and placed in the foreground or background.'
)
},
{
title: t('overlay-pdfs.tooltip.mode.title', 'Overlay Mode'),
description: t(
'overlay-pdfs.tooltip.mode.text',
'Choose how to distribute overlay pages across the base PDF pages.'
),
bullets: [
t('overlay-pdfs.tooltip.mode.sequential', 'Sequential Overlay: Use pages from the first overlay PDF until it ends, then move to the next.'),
t('overlay-pdfs.tooltip.mode.interleaved', 'Interleaved Overlay: Take one page from each overlay in turn.'),
t('overlay-pdfs.tooltip.mode.fixedRepeat', 'Fixed Repeat Overlay: Take a set number of pages from each overlay before moving to the next. Use Counts to set the numbers.')
]
},
{
title: t('overlay-pdfs.tooltip.position.title', 'Overlay Position'),
description: t(
'overlay-pdfs.tooltip.position.text',
'Foreground places the overlay on top of the page. Background places it behind.'
)
},
{
title: t('overlay-pdfs.tooltip.overlayFiles.title', 'Overlay Files'),
description: t(
'overlay-pdfs.tooltip.overlayFiles.text',
'Select one or more PDFs to overlay on the base. The order of these files affects how pages are applied in Sequential and Fixed Repeat modes.'
)
},
{
title: t('overlay-pdfs.tooltip.counts.title', 'Counts (Fixed Repeat only)'),
description: t(
'overlay-pdfs.tooltip.counts.text',
'Provide a positive number for each overlay file showing how many pages to take before moving to the next. Required when mode is Fixed Repeat.'
)
}
]
};
};

View File

@ -66,6 +66,10 @@ import { extractImagesOperationConfig } from "../hooks/tools/extractImages/useEx
import { replaceColorOperationConfig } from "../hooks/tools/replaceColor/useReplaceColorOperation"; import { replaceColorOperationConfig } from "../hooks/tools/replaceColor/useReplaceColorOperation";
import { removePagesOperationConfig } from "../hooks/tools/removePages/useRemovePagesOperation"; import { removePagesOperationConfig } from "../hooks/tools/removePages/useRemovePagesOperation";
import { removeBlanksOperationConfig } from "../hooks/tools/removeBlanks/useRemoveBlanksOperation"; import { removeBlanksOperationConfig } from "../hooks/tools/removeBlanks/useRemoveBlanksOperation";
import { overlayPdfsOperationConfig } from "../hooks/tools/overlayPdfs/useOverlayPdfsOperation";
import { adjustPageScaleOperationConfig } from "../hooks/tools/adjustPageScale/useAdjustPageScaleOperation";
import { scannerImageSplitOperationConfig } from "../hooks/tools/scannerImageSplit/useScannerImageSplitOperation";
import { addPageNumbersOperationConfig } from "../components/tools/addPageNumbers/useAddPageNumbersOperation";
import CompressSettings from "../components/tools/compress/CompressSettings"; import CompressSettings from "../components/tools/compress/CompressSettings";
import AddPasswordSettings from "../components/tools/addPassword/AddPasswordSettings"; import AddPasswordSettings from "../components/tools/addPassword/AddPasswordSettings";
import RemovePasswordSettings from "../components/tools/removePassword/RemovePasswordSettings"; import RemovePasswordSettings from "../components/tools/removePassword/RemovePasswordSettings";
@ -81,16 +85,14 @@ import Redact from "../tools/Redact";
import AdjustPageScale from "../tools/AdjustPageScale"; import AdjustPageScale from "../tools/AdjustPageScale";
import ReplaceColor from "../tools/ReplaceColor"; import ReplaceColor from "../tools/ReplaceColor";
import ScannerImageSplit from "../tools/ScannerImageSplit"; import ScannerImageSplit from "../tools/ScannerImageSplit";
import OverlayPdfs from "../tools/OverlayPdfs";
import { ToolId } from "../types/toolId"; import { ToolId } from "../types/toolId";
import MergeSettings from '../components/tools/merge/MergeSettings'; import MergeSettings from '../components/tools/merge/MergeSettings';
import { adjustPageScaleOperationConfig } from "../hooks/tools/adjustPageScale/useAdjustPageScaleOperation";
import { scannerImageSplitOperationConfig } from "../hooks/tools/scannerImageSplit/useScannerImageSplitOperation";
import AdjustPageScaleSettings from "../components/tools/adjustPageScale/AdjustPageScaleSettings"; import AdjustPageScaleSettings from "../components/tools/adjustPageScale/AdjustPageScaleSettings";
import ScannerImageSplitSettings from "../components/tools/scannerImageSplit/ScannerImageSplitSettings"; import ScannerImageSplitSettings from "../components/tools/scannerImageSplit/ScannerImageSplitSettings";
import ChangeMetadataSingleStep from "../components/tools/changeMetadata/ChangeMetadataSingleStep"; import ChangeMetadataSingleStep from "../components/tools/changeMetadata/ChangeMetadataSingleStep";
import SignSettings from "../components/tools/sign/SignSettings"; import SignSettings from "../components/tools/sign/SignSettings";
import AddPageNumbers from "../tools/AddPageNumbers"; import AddPageNumbers from "../tools/AddPageNumbers";
import { addPageNumbersOperationConfig } from "../components/tools/addPageNumbers/useAddPageNumbersOperation";
import RemoveAnnotations from "../tools/RemoveAnnotations"; import RemoveAnnotations from "../tools/RemoveAnnotations";
import PageLayoutSettings from "../components/tools/pageLayout/PageLayoutSettings"; import PageLayoutSettings from "../components/tools/pageLayout/PageLayoutSettings";
import ExtractImages from "../tools/ExtractImages"; import ExtractImages from "../tools/ExtractImages";
@ -105,6 +107,7 @@ import AddAttachmentsSettings from "../components/tools/addAttachments/AddAttach
import RemovePagesSettings from "../components/tools/removePages/RemovePagesSettings"; import RemovePagesSettings from "../components/tools/removePages/RemovePagesSettings";
import RemoveBlanksSettings from "../components/tools/removeBlanks/RemoveBlanksSettings"; import RemoveBlanksSettings from "../components/tools/removeBlanks/RemoveBlanksSettings";
import AddPageNumbersAutomationSettings from "../components/tools/addPageNumbers/AddPageNumbersAutomationSettings"; import AddPageNumbersAutomationSettings from "../components/tools/addPageNumbers/AddPageNumbersAutomationSettings";
import OverlayPdfsSettings from "../components/tools/overlayPdfs/OverlayPdfsSettings";
const showPlaceholderTools = true; // Show all tools; grey out unavailable ones in UI const showPlaceholderTools = true; // Show all tools; grey out unavailable ones in UI
@ -704,13 +707,14 @@ export function useFlatToolRegistry(): ToolRegistry {
}, },
overlayPdfs: { overlayPdfs: {
icon: <LocalIcon icon="layers-rounded" width="1.5rem" height="1.5rem" />, icon: <LocalIcon icon="layers-rounded" width="1.5rem" height="1.5rem" />,
name: t("home.overlayPdfs.title", "Overlay PDFs"), name: t("home.overlay-pdfs.title", "Overlay PDFs"),
component: null, component: OverlayPdfs,
description: t("home.overlayPdfs.desc", "Overlay one PDF on top of another"), description: t("home.overlay-pdfs.desc", "Overlay one PDF on top of another"),
categoryId: ToolCategoryId.ADVANCED_TOOLS, categoryId: ToolCategoryId.ADVANCED_TOOLS,
subcategoryId: SubcategoryId.ADVANCED_FORMATTING, subcategoryId: SubcategoryId.ADVANCED_FORMATTING,
synonyms: getSynonyms(t, "overlayPdfs"), operationConfig: overlayPdfsOperationConfig,
automationSettings: null synonyms: getSynonyms(t, "overlay-pdfs"),
automationSettings: OverlayPdfsSettings
}, },
replaceColor: { replaceColor: {
icon: <LocalIcon icon="format-color-fill-rounded" width="1.5rem" height="1.5rem" />, icon: <LocalIcon icon="format-color-fill-rounded" width="1.5rem" height="1.5rem" />,

View File

@ -0,0 +1,46 @@
import { useTranslation } from 'react-i18next';
import { useToolOperation, ToolType, type ToolOperationConfig } from '../shared/useToolOperation';
import { createStandardErrorHandler } from '../../../utils/toolErrorHandler';
import { type OverlayPdfsParameters } from './useOverlayPdfsParameters';
const buildFormData = (parameters: OverlayPdfsParameters, file: File): FormData => {
const formData = new FormData();
formData.append('fileInput', file);
// Overlay files
for (const overlay of parameters.overlayFiles || []) {
formData.append('overlayFiles', overlay);
}
// Mode and position
formData.append('overlayMode', parameters.overlayMode);
formData.append('overlayPosition', String(parameters.overlayPosition));
// Counts (only relevant for FixedRepeatOverlay, server accepts repeated 'counts' fields)
if (parameters.overlayMode === 'FixedRepeatOverlay') {
for (const count of parameters.counts || []) {
formData.append('counts', String(count));
}
}
return formData;
};
export const overlayPdfsOperationConfig: ToolOperationConfig<OverlayPdfsParameters> = {
toolType: ToolType.singleFile,
buildFormData,
operationType: 'overlayPdfs',
endpoint: '/api/v1/general/overlay-pdfs'
};
export const useOverlayPdfsOperation = () => {
const { t } = useTranslation();
return useToolOperation<OverlayPdfsParameters>({
...overlayPdfsOperationConfig,
getErrorMessage: createStandardErrorHandler(
t('overlay-pdfs.error.failed', 'An error occurred while overlaying PDFs.')
),
});
};

View File

@ -0,0 +1,37 @@
import { BaseParameters } from '../../../types/parameters';
import { useBaseParameters, type BaseParametersHook } from '../shared/useBaseParameters';
export type OverlayMode = 'SequentialOverlay' | 'InterleavedOverlay' | 'FixedRepeatOverlay';
export interface OverlayPdfsParameters extends BaseParameters {
overlayFiles: File[];
overlayMode: OverlayMode;
overlayPosition: 0 | 1;
counts: number[];
}
export const defaultParameters: OverlayPdfsParameters = {
overlayFiles: [],
overlayMode: 'SequentialOverlay',
overlayPosition: 0,
counts: []
};
export type OverlayPdfsParametersHook = BaseParametersHook<OverlayPdfsParameters>;
export const useOverlayPdfsParameters = (): OverlayPdfsParametersHook => {
return useBaseParameters<OverlayPdfsParameters>({
defaultParameters,
endpointName: 'overlay-pdfs',
validateFn: (params) => {
if (!params.overlayFiles || params.overlayFiles.length === 0) return false;
if (params.overlayMode === 'FixedRepeatOverlay') {
if (!params.counts || params.counts.length !== params.overlayFiles.length) return false;
if (params.counts.some((c) => !Number.isFinite(c) || c <= 0)) return false;
}
return true;
},
});
};

View File

@ -0,0 +1,60 @@
import { useTranslation } from 'react-i18next';
import { createToolFlow } from '../components/tools/shared/createToolFlow';
import { useBaseTool } from '../hooks/tools/shared/useBaseTool';
import { BaseToolProps, ToolComponent } from '../types/tool';
import OverlayPdfsSettings from '../components/tools/overlayPdfs/OverlayPdfsSettings';
import { useOverlayPdfsParameters } from '../hooks/tools/overlayPdfs/useOverlayPdfsParameters';
import { useOverlayPdfsOperation } from '../hooks/tools/overlayPdfs/useOverlayPdfsOperation';
import { useOverlayPdfsTips } from '../components/tooltips/useOverlayPdfsTips';
const OverlayPdfs = (props: BaseToolProps) => {
const { t } = useTranslation();
const base = useBaseTool(
'overlay-pdfs',
useOverlayPdfsParameters,
useOverlayPdfsOperation,
props
);
const overlayTips = useOverlayPdfsTips();
return createToolFlow({
files: {
selectedFiles: base.selectedFiles,
isCollapsed: base.hasResults,
},
steps: [
{
title: t('overlay-pdfs.settings.title', 'Settings'),
isCollapsed: base.settingsCollapsed,
onCollapsedClick: base.settingsCollapsed ? base.handleSettingsReset : undefined,
tooltip: overlayTips,
content: (
<OverlayPdfsSettings
parameters={base.params.parameters}
onParameterChange={base.params.updateParameter}
disabled={base.endpointLoading}
/>
),
},
],
executeButton: {
text: t('overlay-pdfs.submit', 'Overlay and Review'),
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('overlay-pdfs.results.title', 'Overlay Results'),
onFileClick: base.handleThumbnailClick,
onUndo: base.handleUndo,
},
});
};
export default OverlayPdfs as ToolComponent;