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",
|
"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",
|
||||||
|
|||||||
@ -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",
|
||||||
|
|||||||
@ -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',
|
||||||
|
|||||||
@ -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 ? (
|
||||||
|
|||||||
@ -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 { 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" />,
|
||||||
|
|||||||
@ -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