Feature/adjust colors contrast tool (#4544)

# Description of Changes

- Addition of the "Adjust Colors/Contrast" 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-02 17:48:39 +01:00 committed by GitHub
parent 247f82b5a7
commit 458bb641b5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 562 additions and 8 deletions

View File

@ -547,7 +547,7 @@
"adjustContrast": {
"tags": "contrast,brightness,saturation",
"title": "Adjust Colours/Contrast",
"desc": "Adjust Contrast, Saturation and Brightness of a PDF"
"desc": "Adjust Colors/Contrast, Saturation and Brightness of a PDF"
},
"crop": {
"tags": "trim,cut,resize",
@ -2791,8 +2791,9 @@
"submit": "Sanitize PDF"
},
"adjustContrast": {
"title": "Adjust Contrast",
"header": "Adjust Contrast",
"title": "Adjust Colors/Contrast",
"header": "Adjust Colors/Contrast",
"basic": "Basic Adjustments",
"contrast": "Contrast:",
"brightness": "Brightness:",
"saturation": "Saturation:",

View File

@ -562,7 +562,7 @@
"adjustContrast": {
"tags": "contrast,brightness,saturation",
"title": "Adjust Colors/Contrast",
"desc": "Adjust Contrast, Saturation and Brightness of a PDF"
"desc": "Adjust Colors/Contrast, Saturation and Brightness of a PDF"
},
"crop": {
"tags": "trim,cut,resize",
@ -1712,8 +1712,9 @@
"submit": "Sanitize PDF"
},
"adjustContrast": {
"title": "Adjust Contrast",
"header": "Adjust Contrast",
"title": "Adjust Colors/Contrast",
"header": "Adjust Colors/Contrast",
"basic": "Basic Adjustments",
"contrast": "Contrast:",
"brightness": "Brightness:",
"saturation": "Saturation:",

View File

@ -0,0 +1,44 @@
import React from 'react';
import { Slider, Text, Group, NumberInput } from '@mantine/core';
interface Props {
label: string;
value: number;
onChange: (value: number) => void;
disabled?: boolean;
min?: number;
max?: number;
step?: number;
}
export default function SliderWithInput({
label,
value,
onChange,
disabled,
min = 0,
max = 200,
step = 1,
}: Props) {
return (
<div>
<Text size="sm" fw={600} mb={4}>{label}: {Math.round(value)}%</Text>
<Group gap="sm" align="center">
<div style={{ flex: 1 }}>
<Slider min={min} max={max} step={step} value={value} onChange={onChange} disabled={disabled} />
</div>
<NumberInput
value={value}
onChange={(v) => onChange(Number(v) || 0)}
min={min}
max={max}
step={step}
disabled={disabled}
style={{ width: 90 }}
/>
</Group>
</div>
);
}

View File

@ -0,0 +1,25 @@
import React from 'react';
import { Stack } from '@mantine/core';
import { useTranslation } from 'react-i18next';
import { AdjustContrastParameters } from '../../../hooks/tools/adjustContrast/useAdjustContrastParameters';
import SliderWithInput from '../../shared/sliderWithInput/SliderWithInput';
interface Props {
parameters: AdjustContrastParameters;
onParameterChange: <K extends keyof AdjustContrastParameters>(key: K, value: AdjustContrastParameters[K]) => void;
disabled?: boolean;
}
export default function AdjustContrastBasicSettings({ parameters, onParameterChange, disabled }: Props) {
const { t } = useTranslation();
return (
<Stack gap="md">
<SliderWithInput label={t('adjustContrast.contrast', 'Contrast')} value={parameters.contrast} onChange={(v) => onParameterChange('contrast', v as any)} disabled={disabled} />
<SliderWithInput label={t('adjustContrast.brightness', 'Brightness')} value={parameters.brightness} onChange={(v) => onParameterChange('brightness', v as any)} disabled={disabled} />
<SliderWithInput label={t('adjustContrast.saturation', 'Saturation')} value={parameters.saturation} onChange={(v) => onParameterChange('saturation', v as any)} disabled={disabled} />
</Stack>
);
}

View File

@ -0,0 +1,25 @@
import React from 'react';
import { Stack } from '@mantine/core';
import { useTranslation } from 'react-i18next';
import { AdjustContrastParameters } from '../../../hooks/tools/adjustContrast/useAdjustContrastParameters';
import SliderWithInput from '../../shared/sliderWithInput/SliderWithInput';
interface Props {
parameters: AdjustContrastParameters;
onParameterChange: <K extends keyof AdjustContrastParameters>(key: K, value: AdjustContrastParameters[K]) => void;
disabled?: boolean;
}
export default function AdjustContrastColorSettings({ parameters, onParameterChange, disabled }: Props) {
const { t } = useTranslation();
return (
<Stack gap="md">
<SliderWithInput label={t('adjustContrast.red', 'Red')} value={parameters.red} onChange={(v) => onParameterChange('red', v as any)} disabled={disabled} />
<SliderWithInput label={t('adjustContrast.green', 'Green')} value={parameters.green} onChange={(v) => onParameterChange('green', v as any)} disabled={disabled} />
<SliderWithInput label={t('adjustContrast.blue', 'Blue')} value={parameters.blue} onChange={(v) => onParameterChange('blue', v as any)} disabled={disabled} />
</Stack>
);
}

View File

@ -0,0 +1,93 @@
import React, { useEffect, useRef, useState } from 'react';
import { AdjustContrastParameters } from '../../../hooks/tools/adjustContrast/useAdjustContrastParameters';
import { useThumbnailGeneration } from '../../../hooks/useThumbnailGeneration';
import ObscuredOverlay from '../../shared/ObscuredOverlay';
import { Text } from '@mantine/core';
import { useTranslation } from 'react-i18next';
import { applyAdjustmentsToCanvas } from './utils';
interface Props {
file: File | null;
parameters: AdjustContrastParameters;
}
export default function AdjustContrastPreview({ file, parameters }: Props) {
const { t } = useTranslation();
const containerRef = useRef<HTMLDivElement>(null);
const canvasRef = useRef<HTMLCanvasElement>(null);
const [thumb, setThumb] = useState<string | null>(null);
const { requestThumbnail } = useThumbnailGeneration();
useEffect(() => {
let active = true;
const load = async () => {
if (!file || file.type !== 'application/pdf') { setThumb(null); return; }
const id = `${file.name}:${file.size}:${file.lastModified}:page:1`;
const tUrl = await requestThumbnail(id, file, 1);
if (active) setThumb(tUrl || null);
};
load();
return () => { active = false; };
}, [file, requestThumbnail]);
useEffect(() => {
const revoked: string | null = null;
const render = async () => {
if (!thumb || !canvasRef.current) return;
const img = new Image();
img.crossOrigin = 'anonymous';
img.src = thumb;
await new Promise<void>((resolve, reject) => {
img.onload = () => resolve();
img.onerror = () => reject();
});
// Draw thumbnail to a source canvas
const src = document.createElement('canvas');
src.width = img.naturalWidth;
src.height = img.naturalHeight;
const sctx = src.getContext('2d');
if (!sctx) return;
sctx.drawImage(img, 0, 0);
// Apply accurate pixel adjustments
const adjusted = applyAdjustmentsToCanvas(src, parameters);
// Draw adjusted onto display canvas
const display = canvasRef.current;
display.width = adjusted.width;
display.height = adjusted.height;
const dctx = display.getContext('2d');
if (!dctx) return;
dctx.clearRect(0, 0, display.width, display.height);
dctx.drawImage(adjusted, 0, 0);
};
render();
return () => {
if (revoked) URL.revokeObjectURL(revoked);
};
}, [thumb, parameters]);
return (
<div>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 6 }}>
<div style={{ flex: 1, height: 1, background: 'var(--border-color)' }} />
<div style={{ fontSize: 12, color: 'var(--text-color-muted)' }}>{t('common.preview', 'Preview')}</div>
<div style={{ flex: 1, height: 1, background: 'var(--border-color)' }} />
</div>
<ObscuredOverlay
obscured={!thumb}
overlayMessage={<Text size="sm" c="white" fw={600}>{t('adjustContrast.noPreview', 'Select a PDF to preview')}</Text>}
borderRadius={6}
>
<div ref={containerRef} style={{ aspectRatio: '8.5/11', width: '100%', border: '1px solid var(--border-color)', borderRadius: 8, overflow: 'hidden' }}>
{thumb && (
<canvas ref={canvasRef} style={{ width: '100%', height: '100%' }} />
)}
</div>
</ObscuredOverlay>
</div>
);
}

View File

@ -0,0 +1,31 @@
import React from 'react';
import { Stack } from '@mantine/core';
import { AdjustContrastParameters } from '../../../hooks/tools/adjustContrast/useAdjustContrastParameters';
import AdjustContrastBasicSettings from './AdjustContrastBasicSettings';
import AdjustContrastColorSettings from './AdjustContrastColorSettings';
interface Props {
parameters: AdjustContrastParameters;
onParameterChange: <K extends keyof AdjustContrastParameters>(key: K, value: AdjustContrastParameters[K]) => void;
disabled?: boolean;
}
// Single-step settings used by Automate to configure Adjust Contrast in one panel
export default function AdjustContrastSingleStepSettings({ parameters, onParameterChange, disabled }: Props) {
return (
<Stack gap="lg">
<AdjustContrastBasicSettings
parameters={parameters}
onParameterChange={onParameterChange}
disabled={disabled}
/>
<AdjustContrastColorSettings
parameters={parameters}
onParameterChange={onParameterChange}
disabled={disabled}
/>
</Stack>
);
}

View File

@ -0,0 +1,79 @@
import { AdjustContrastParameters } from '../../../hooks/tools/adjustContrast/useAdjustContrastParameters';
export function applyAdjustmentsToCanvas(src: HTMLCanvasElement, params: AdjustContrastParameters): HTMLCanvasElement {
const out = document.createElement('canvas');
out.width = src.width;
out.height = src.height;
const ctx = out.getContext('2d');
if (!ctx) return src;
ctx.drawImage(src, 0, 0);
const imageData = ctx.getImageData(0, 0, out.width, out.height);
const data = imageData.data;
const contrast = params.contrast / 100; // 0..2
const brightness = params.brightness / 100; // 0..2
const saturation = params.saturation / 100; // 0..2
const redMul = params.red / 100; // 0..2
const greenMul = params.green / 100; // 0..2
const blueMul = params.blue / 100; // 0..2
const clamp = (v: number) => Math.min(255, Math.max(0, v));
for (let i = 0; i < data.length; i += 4) {
let r = data[i] * redMul;
let g = data[i + 1] * greenMul;
let b = data[i + 2] * blueMul;
// Contrast (centered at 128)
r = clamp((r - 128) * contrast + 128);
g = clamp((g - 128) * contrast + 128);
b = clamp((b - 128) * contrast + 128);
// Brightness
r = clamp(r * brightness);
g = clamp(g * brightness);
b = clamp(b * brightness);
// Saturation via HSL
const rn = r / 255, gn = g / 255, bn = b / 255;
const max = Math.max(rn, gn, bn); const min = Math.min(rn, gn, bn);
let h = 0, s = 0;
const l = (max + min) / 2;
if (max !== min) {
const d = max - min;
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
switch (max) {
case rn: h = (gn - bn) / d + (gn < bn ? 6 : 0); break;
case gn: h = (bn - rn) / d + 2; break;
default: h = (rn - gn) / d + 4; break;
}
h /= 6;
}
s = Math.min(1, Math.max(0, s * saturation));
const hue2rgb = (p: number, q: number, t: number) => {
if (t < 0) t += 1; if (t > 1) t -= 1;
if (t < 1/6) return p + (q - p) * 6 * t;
if (t < 1/2) return q;
if (t < 2/3) return p + (q - p) * (2/3 - t) * 6;
return p;
};
let r2: number, g2: number, b2: number;
if (s === 0) { r2 = g2 = b2 = l; }
else {
const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
const p = 2 * l - q;
r2 = hue2rgb(p, q, h + 1/3);
g2 = hue2rgb(p, q, h);
b2 = hue2rgb(p, q, h - 1/3);
}
data[i] = clamp(Math.round(r2 * 255));
data[i + 1] = clamp(Math.round(g2 * 255));
data[i + 2] = clamp(Math.round(b2 * 255));
}
ctx.putImageData(imageData, 0, 0);
return out;
}

View File

@ -54,6 +54,8 @@ export interface ToolFlowConfig {
title?: TitleConfig;
files: FilesStepConfig;
steps: MiddleStepConfig[];
// Optional preview content rendered between steps and the execute button
preview?: React.ReactNode;
executeButton?: ExecuteButtonConfig;
review: ReviewStepConfig;
forceStepNumbers?: boolean;
@ -90,6 +92,10 @@ export function createToolFlow(config: ToolFlowConfig) {
}, stepConfig.content)
)}
{/* Preview (outside steps, above execute button).
Hide when review is visible or when no files are selected. */}
{!config.review.isVisible && (config.files.selectedFiles?.length ?? 0) > 0 && config.preview}
{/* Execute Button */}
{config.executeButton && config.executeButton.isVisible !== false && (
<OperationButton

View File

@ -14,6 +14,9 @@ import ReorganizePages from "../tools/ReorganizePages";
import { reorganizePagesOperationConfig } from "../hooks/tools/reorganizePages/useReorganizePagesOperation";
import RemovePassword from "../tools/RemovePassword";
import { SubcategoryId, ToolCategoryId, ToolRegistry } from "./toolsTaxonomy";
import AdjustContrast from "../tools/AdjustContrast";
import AdjustContrastSingleStepSettings from "../components/tools/adjustContrast/AdjustContrastSingleStepSettings";
import { adjustContrastOperationConfig } from "../hooks/tools/adjustContrast/useAdjustContrastOperation";
import { getSynonyms } from "../utils/toolSynonyms";
import AddWatermark from "../tools/AddWatermark";
import AddStamp from "../tools/AddStamp";
@ -665,12 +668,13 @@ export function useFlatToolRegistry(): ToolRegistry {
adjustContrast: {
icon: <LocalIcon icon="palette" width="1.5rem" height="1.5rem" />,
name: t("home.adjustContrast.title", "Adjust Colors/Contrast"),
component: null,
component: AdjustContrast,
description: t("home.adjustContrast.desc", "Adjust colors and contrast of PDF documents"),
categoryId: ToolCategoryId.ADVANCED_TOOLS,
subcategoryId: SubcategoryId.ADVANCED_FORMATTING,
operationConfig: adjustContrastOperationConfig,
automationSettings: AdjustContrastSingleStepSettings,
synonyms: getSynonyms(t, "adjustContrast"),
automationSettings: null,
},
repair: {
icon: <LocalIcon icon="build-outline-rounded" width="1.5rem" height="1.5rem" />,

View File

@ -0,0 +1,94 @@
import { useTranslation } from 'react-i18next';
import { ToolType, useToolOperation } from '../shared/useToolOperation';
import { AdjustContrastParameters, defaultParameters } from './useAdjustContrastParameters';
import { PDFDocument as PDFLibDocument } from 'pdf-lib';
import { applyAdjustmentsToCanvas } from '../../../components/tools/adjustContrast/utils';
import { pdfWorkerManager } from '../../../services/pdfWorkerManager';
import { createFileFromApiResponse } from '../../../utils/fileResponseUtils';
async function renderPdfPageToCanvas(pdf: any, pageNumber: number, scale: number): Promise<HTMLCanvasElement> {
const page = await pdf.getPage(pageNumber);
const viewport = page.getViewport({ scale });
const canvas = document.createElement('canvas');
canvas.width = viewport.width;
canvas.height = viewport.height;
const ctx = canvas.getContext('2d');
if (!ctx) throw new Error('Canvas 2D context unavailable');
await page.render({ canvasContext: ctx, viewport }).promise;
return canvas;
}
// adjustment logic moved to shared util
// Render, adjust, and assemble all pages of a single PDF into a new PDF
async function buildAdjustedPdfForFile(file: File, params: AdjustContrastParameters): Promise<File> {
const arrayBuffer = await file.arrayBuffer();
const pdf = await pdfWorkerManager.createDocument(arrayBuffer, {});
const pageCount = pdf.numPages;
const newDoc = await PDFLibDocument.create();
for (let p = 1; p <= pageCount; p++) {
const srcCanvas = await renderPdfPageToCanvas(pdf, p, 2);
const adjusted = applyAdjustmentsToCanvas(srcCanvas, params);
const pngUrl = adjusted.toDataURL('image/png');
const res = await fetch(pngUrl);
const pngBytes = new Uint8Array(await res.arrayBuffer());
const embedded = await newDoc.embedPng(pngBytes);
const { width, height } = embedded.scale(1);
const page = newDoc.addPage([width, height]);
page.drawImage(embedded, { x: 0, y: 0, width, height });
}
const pdfBytes = await newDoc.save();
const out = createFileFromApiResponse(pdfBytes, { 'content-type': 'application/pdf' }, file.name);
pdfWorkerManager.destroyDocument(pdf);
return out;
}
async function processPdfClientSide(params: AdjustContrastParameters, files: File[]): Promise<File[]> {
// Limit concurrency to avoid exhausting memory/CPU while still getting speedups
// Heuristic: use up to 4 workers on capable machines, otherwise 2-3
let CONCURRENCY_LIMIT = 2;
if (typeof navigator !== 'undefined' && typeof navigator.hardwareConcurrency === 'number') {
if (navigator.hardwareConcurrency >= 8) CONCURRENCY_LIMIT = 4;
else if (navigator.hardwareConcurrency >= 4) CONCURRENCY_LIMIT = 3;
}
CONCURRENCY_LIMIT = Math.min(CONCURRENCY_LIMIT, files.length);
const mapWithConcurrency = async <T, R>(items: T[], limit: number, worker: (item: T, index: number) => Promise<R>): Promise<R[]> => {
const results: R[] = new Array(items.length);
let nextIndex = 0;
const workers = new Array(Math.min(limit, items.length)).fill(0).map(async () => {
let current = nextIndex++;
while (current < items.length) {
results[current] = await worker(items[current], current);
current = nextIndex++;
}
});
await Promise.all(workers);
return results;
};
return mapWithConcurrency(files, CONCURRENCY_LIMIT, (file) => buildAdjustedPdfForFile(file, params));
}
export const adjustContrastOperationConfig = {
toolType: ToolType.custom,
customProcessor: processPdfClientSide,
operationType: 'adjustContrast',
defaultParameters,
// Single-step settings component for Automate
settingsComponentPath: 'components/tools/adjustContrast/AdjustContrastSingleStepSettings',
} as const;
export const useAdjustContrastOperation = () => {
const { t } = useTranslation();
return useToolOperation<AdjustContrastParameters>({
...adjustContrastOperationConfig,
getErrorMessage: () => t('adjustContrast.error.failed', 'Failed to adjust colors/contrast')
});
};

View File

@ -0,0 +1,30 @@
import { useBaseParameters, BaseParametersHook } from '../shared/useBaseParameters';
export interface AdjustContrastParameters {
contrast: number; // 0-200 (%), 100 = neutral
brightness: number; // 0-200 (%), 100 = neutral
saturation: number; // 0-200 (%), 100 = neutral
red: number; // 0-200 (%), 100 = neutral
green: number; // 0-200 (%), 100 = neutral
blue: number; // 0-200 (%), 100 = neutral
}
export const defaultParameters: AdjustContrastParameters = {
contrast: 100,
brightness: 100,
saturation: 100,
red: 100,
green: 100,
blue: 100,
};
export type AdjustContrastParametersHook = BaseParametersHook<AdjustContrastParameters>;
export const useAdjustContrastParameters = (): AdjustContrastParametersHook => {
return useBaseParameters<AdjustContrastParameters>({
defaultParameters,
endpointName: '',
});
};

View File

@ -0,0 +1,121 @@
import { useTranslation } from 'react-i18next';
import React, { useEffect, useMemo, useState } from 'react';
import { createToolFlow } from '../components/tools/shared/createToolFlow';
import { BaseToolProps, ToolComponent } from '../types/tool';
import { useBaseTool } from '../hooks/tools/shared/useBaseTool';
import { useAdjustContrastParameters } from '../hooks/tools/adjustContrast/useAdjustContrastParameters';
import { useAdjustContrastOperation } from '../hooks/tools/adjustContrast/useAdjustContrastOperation';
import AdjustContrastBasicSettings from '../components/tools/adjustContrast/AdjustContrastBasicSettings';
import AdjustContrastColorSettings from '../components/tools/adjustContrast/AdjustContrastColorSettings';
import AdjustContrastPreview from '../components/tools/adjustContrast/AdjustContrastPreview';
import { useAccordionSteps } from '../hooks/tools/shared/useAccordionSteps';
import NavigationArrows from '../components/shared/filePreview/NavigationArrows';
const AdjustContrast = (props: BaseToolProps) => {
const { t } = useTranslation();
const base = useBaseTool(
'adjustContrast',
useAdjustContrastParameters,
useAdjustContrastOperation,
props
);
enum Step { NONE='none', BASIC='basic', COLORS='colors' }
const accordion = useAccordionSteps<Step>({
noneValue: Step.NONE,
initialStep: Step.BASIC,
stateConditions: { hasFiles: base.hasFiles, hasResults: base.hasResults },
afterResults: base.handleSettingsReset
});
// Track which selected file is being previewed. Clamp when selection changes.
const [previewIndex, setPreviewIndex] = useState(0);
const totalSelected = base.selectedFiles.length;
useEffect(() => {
if (previewIndex >= totalSelected) {
setPreviewIndex(Math.max(0, totalSelected - 1));
}
}, [totalSelected, previewIndex]);
const currentFile = useMemo(() => {
return totalSelected > 0 ? base.selectedFiles[previewIndex] : null;
}, [base.selectedFiles, previewIndex, totalSelected]);
const handlePrev = () => setPreviewIndex((i) => Math.max(0, i - 1));
const handleNext = () => setPreviewIndex((i) => Math.min(totalSelected - 1, i + 1));
return createToolFlow({
files: {
selectedFiles: base.selectedFiles,
isCollapsed: base.hasResults,
},
steps: [
{
title: t('adjustContrast.basic', 'Basic Adjustments'),
isCollapsed: accordion.getCollapsedState(Step.BASIC),
onCollapsedClick: () => accordion.handleStepToggle(Step.BASIC),
content: (
<AdjustContrastBasicSettings
parameters={base.params.parameters}
onParameterChange={base.params.updateParameter}
disabled={base.endpointLoading}
/>
),
},
{
title: t('adjustContrast.adjustColors', 'Adjust Colors'),
isCollapsed: accordion.getCollapsedState(Step.COLORS),
onCollapsedClick: () => accordion.handleStepToggle(Step.COLORS),
content: (
<AdjustContrastColorSettings
parameters={base.params.parameters}
onParameterChange={base.params.updateParameter}
disabled={base.endpointLoading}
/>
),
},
],
preview: (
<div>
<NavigationArrows
onPrevious={handlePrev}
onNext={handleNext}
disabled={totalSelected <= 1}
>
<div style={{ width: '100%' }}>
<AdjustContrastPreview
file={currentFile || null}
parameters={base.params.parameters}
/>
</div>
</NavigationArrows>
{totalSelected > 1 && (
<div style={{ textAlign: 'center', marginTop: 8, fontSize: 12, color: 'var(--text-color-muted)' }}>
{`${previewIndex + 1} of ${totalSelected}`}
</div>
)}
</div>
),
executeButton: {
text: t('adjustContrast.confirm', 'Confirm'),
isVisible: !base.hasResults,
loadingText: t('loading'),
onClick: base.handleExecute,
disabled: !base.hasFiles,
},
review: {
isVisible: base.hasResults,
operation: base.operation,
title: t('adjustContrast.results.title', 'Adjusted PDF'),
onFileClick: base.handleThumbnailClick,
onUndo: base.handleUndo,
},
forceStepNumbers: true,
});
};
export default AdjustContrast as ToolComponent;