mirror of
https://github.com/Frooodle/Stirling-PDF.git
synced 2025-11-16 01:21:16 +01:00
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:
parent
b695e3900e
commit
a339f71116
@ -598,11 +598,6 @@
|
||||
"title": "Redact",
|
||||
"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": {
|
||||
"tags": "split,sections,divide",
|
||||
"title": "Split PDF by Sections",
|
||||
@ -2550,11 +2545,15 @@
|
||||
"overlay-pdfs": {
|
||||
"tags": "Overlay",
|
||||
"header": "Overlay PDF Files",
|
||||
"title": "Overlay PDFs",
|
||||
"desc": "Overlay one PDF on top of another",
|
||||
"baseFile": {
|
||||
"label": "Select Base PDF File"
|
||||
},
|
||||
"overlayFiles": {
|
||||
"label": "Select Overlay PDF Files"
|
||||
"label": "Select Overlay PDF Files",
|
||||
"placeholder": "Choose PDF(s)...",
|
||||
"addMore": "Add more PDFs..."
|
||||
},
|
||||
"mode": {
|
||||
"label": "Select Overlay Mode",
|
||||
@ -2564,14 +2563,49 @@
|
||||
},
|
||||
"counts": {
|
||||
"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": {
|
||||
"label": "Select Overlay Position",
|
||||
"foreground": "Foreground",
|
||||
"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": {
|
||||
"tags": "Section Split, Divide, Customize,Customise",
|
||||
|
||||
@ -1521,11 +1521,15 @@
|
||||
"overlay-pdfs": {
|
||||
"tags": "Overlay",
|
||||
"header": "Overlay PDF Files",
|
||||
"title": "Overlay PDFs",
|
||||
"desc": "Overlay one PDF on top of another",
|
||||
"baseFile": {
|
||||
"label": "Select Base PDF File"
|
||||
},
|
||||
"overlayFiles": {
|
||||
"label": "Select Overlay PDF Files"
|
||||
"label": "Select Overlay PDF Files",
|
||||
"placeholder": "Choose PDF(s)...",
|
||||
"addMore": "Add more PDFs..."
|
||||
},
|
||||
"mode": {
|
||||
"label": "Select Overlay Mode",
|
||||
@ -1535,14 +1539,49 @@
|
||||
},
|
||||
"counts": {
|
||||
"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": {
|
||||
"label": "Select Overlay Position",
|
||||
"foreground": "Foreground",
|
||||
"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": {
|
||||
"tags": "Section Split, Divide, Customize",
|
||||
|
||||
@ -125,6 +125,7 @@ const FileManager: React.FC<FileManagerProps> = ({ selectedTool }) => {
|
||||
radius="md"
|
||||
className="overflow-hidden p-0"
|
||||
withCloseButton={false}
|
||||
zIndex={1100}
|
||||
styles={{
|
||||
content: {
|
||||
position: 'relative',
|
||||
|
||||
@ -125,6 +125,7 @@ const FilePickerModal = ({
|
||||
title={t("fileUpload.selectFromStorage", "Select Files from Storage")}
|
||||
size="lg"
|
||||
scrollAreaComponent={ScrollArea.Autosize}
|
||||
zIndex={1100}
|
||||
>
|
||||
<Stack gap="md">
|
||||
{storedFiles.length === 0 ? (
|
||||
|
||||
@ -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;
|
||||
}
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
56
frontend/src/components/tooltips/useOverlayPdfsTips.ts
Normal file
56
frontend/src/components/tooltips/useOverlayPdfsTips.ts
Normal 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.'
|
||||
)
|
||||
}
|
||||
]
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@ -66,6 +66,10 @@ import { extractImagesOperationConfig } from "../hooks/tools/extractImages/useEx
|
||||
import { replaceColorOperationConfig } from "../hooks/tools/replaceColor/useReplaceColorOperation";
|
||||
import { removePagesOperationConfig } from "../hooks/tools/removePages/useRemovePagesOperation";
|
||||
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 AddPasswordSettings from "../components/tools/addPassword/AddPasswordSettings";
|
||||
import RemovePasswordSettings from "../components/tools/removePassword/RemovePasswordSettings";
|
||||
@ -81,16 +85,14 @@ import Redact from "../tools/Redact";
|
||||
import AdjustPageScale from "../tools/AdjustPageScale";
|
||||
import ReplaceColor from "../tools/ReplaceColor";
|
||||
import ScannerImageSplit from "../tools/ScannerImageSplit";
|
||||
import OverlayPdfs from "../tools/OverlayPdfs";
|
||||
import { ToolId } from "../types/toolId";
|
||||
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 ScannerImageSplitSettings from "../components/tools/scannerImageSplit/ScannerImageSplitSettings";
|
||||
import ChangeMetadataSingleStep from "../components/tools/changeMetadata/ChangeMetadataSingleStep";
|
||||
import SignSettings from "../components/tools/sign/SignSettings";
|
||||
import AddPageNumbers from "../tools/AddPageNumbers";
|
||||
import { addPageNumbersOperationConfig } from "../components/tools/addPageNumbers/useAddPageNumbersOperation";
|
||||
import RemoveAnnotations from "../tools/RemoveAnnotations";
|
||||
import PageLayoutSettings from "../components/tools/pageLayout/PageLayoutSettings";
|
||||
import ExtractImages from "../tools/ExtractImages";
|
||||
@ -105,6 +107,7 @@ import AddAttachmentsSettings from "../components/tools/addAttachments/AddAttach
|
||||
import RemovePagesSettings from "../components/tools/removePages/RemovePagesSettings";
|
||||
import RemoveBlanksSettings from "../components/tools/removeBlanks/RemoveBlanksSettings";
|
||||
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
|
||||
|
||||
@ -704,13 +707,14 @@ export function useFlatToolRegistry(): ToolRegistry {
|
||||
},
|
||||
overlayPdfs: {
|
||||
icon: <LocalIcon icon="layers-rounded" width="1.5rem" height="1.5rem" />,
|
||||
name: t("home.overlayPdfs.title", "Overlay PDFs"),
|
||||
component: null,
|
||||
description: t("home.overlayPdfs.desc", "Overlay one PDF on top of another"),
|
||||
name: t("home.overlay-pdfs.title", "Overlay PDFs"),
|
||||
component: OverlayPdfs,
|
||||
description: t("home.overlay-pdfs.desc", "Overlay one PDF on top of another"),
|
||||
categoryId: ToolCategoryId.ADVANCED_TOOLS,
|
||||
subcategoryId: SubcategoryId.ADVANCED_FORMATTING,
|
||||
synonyms: getSynonyms(t, "overlayPdfs"),
|
||||
automationSettings: null
|
||||
operationConfig: overlayPdfsOperationConfig,
|
||||
synonyms: getSynonyms(t, "overlay-pdfs"),
|
||||
automationSettings: OverlayPdfsSettings
|
||||
},
|
||||
replaceColor: {
|
||||
icon: <LocalIcon icon="format-color-fill-rounded" width="1.5rem" height="1.5rem" />,
|
||||
|
||||
@ -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.')
|
||||
),
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@ -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;
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
60
frontend/src/tools/OverlayPdfs.tsx
Normal file
60
frontend/src/tools/OverlayPdfs.tsx
Normal 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;
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user