From 458bb641b5d6a565bbeebd0f27a88577332fa8c2 Mon Sep 17 00:00:00 2001
From: EthanHealy01 <80844253+EthanHealy01@users.noreply.github.com>
Date: Thu, 2 Oct 2025 17:48:39 +0100
Subject: [PATCH 1/4] 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.
---
.../public/locales/en-GB/translation.json | 7 +-
.../public/locales/en-US/translation.json | 7 +-
.../sliderWithInput/SliderWithInput.tsx | 44 +++++++
.../AdjustContrastBasicSettings.tsx | 25 ++++
.../AdjustContrastColorSettings.tsx | 25 ++++
.../adjustContrast/AdjustContrastPreview.tsx | 93 ++++++++++++++
.../AdjustContrastSingleStepSettings.tsx | 31 +++++
.../components/tools/adjustContrast/utils.ts | 79 ++++++++++++
.../tools/shared/createToolFlow.tsx | 6 +
.../src/data/useTranslatedToolRegistry.tsx | 8 +-
.../useAdjustContrastOperation.ts | 94 ++++++++++++++
.../useAdjustContrastParameters.ts | 30 +++++
frontend/src/tools/AdjustContrast.tsx | 121 ++++++++++++++++++
13 files changed, 562 insertions(+), 8 deletions(-)
create mode 100644 frontend/src/components/shared/sliderWithInput/SliderWithInput.tsx
create mode 100644 frontend/src/components/tools/adjustContrast/AdjustContrastBasicSettings.tsx
create mode 100644 frontend/src/components/tools/adjustContrast/AdjustContrastColorSettings.tsx
create mode 100644 frontend/src/components/tools/adjustContrast/AdjustContrastPreview.tsx
create mode 100644 frontend/src/components/tools/adjustContrast/AdjustContrastSingleStepSettings.tsx
create mode 100644 frontend/src/components/tools/adjustContrast/utils.ts
create mode 100644 frontend/src/hooks/tools/adjustContrast/useAdjustContrastOperation.ts
create mode 100644 frontend/src/hooks/tools/adjustContrast/useAdjustContrastParameters.ts
create mode 100644 frontend/src/tools/AdjustContrast.tsx
diff --git a/frontend/public/locales/en-GB/translation.json b/frontend/public/locales/en-GB/translation.json
index b1646e76f..2772ad15e 100644
--- a/frontend/public/locales/en-GB/translation.json
+++ b/frontend/public/locales/en-GB/translation.json
@@ -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:",
diff --git a/frontend/public/locales/en-US/translation.json b/frontend/public/locales/en-US/translation.json
index d20cca6ae..b4ac8302d 100644
--- a/frontend/public/locales/en-US/translation.json
+++ b/frontend/public/locales/en-US/translation.json
@@ -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:",
diff --git a/frontend/src/components/shared/sliderWithInput/SliderWithInput.tsx b/frontend/src/components/shared/sliderWithInput/SliderWithInput.tsx
new file mode 100644
index 000000000..e8e504ded
--- /dev/null
+++ b/frontend/src/components/shared/sliderWithInput/SliderWithInput.tsx
@@ -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 (
+
+
{label}: {Math.round(value)}%
+
+
+
+
+ onChange(Number(v) || 0)}
+ min={min}
+ max={max}
+ step={step}
+ disabled={disabled}
+ style={{ width: 90 }}
+ />
+
+
+ );
+}
+
+
diff --git a/frontend/src/components/tools/adjustContrast/AdjustContrastBasicSettings.tsx b/frontend/src/components/tools/adjustContrast/AdjustContrastBasicSettings.tsx
new file mode 100644
index 000000000..6993186bb
--- /dev/null
+++ b/frontend/src/components/tools/adjustContrast/AdjustContrastBasicSettings.tsx
@@ -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: (key: K, value: AdjustContrastParameters[K]) => void;
+ disabled?: boolean;
+}
+
+export default function AdjustContrastBasicSettings({ parameters, onParameterChange, disabled }: Props) {
+ const { t } = useTranslation();
+
+ return (
+
+ onParameterChange('contrast', v as any)} disabled={disabled} />
+ onParameterChange('brightness', v as any)} disabled={disabled} />
+ onParameterChange('saturation', v as any)} disabled={disabled} />
+
+ );
+}
+
+
diff --git a/frontend/src/components/tools/adjustContrast/AdjustContrastColorSettings.tsx b/frontend/src/components/tools/adjustContrast/AdjustContrastColorSettings.tsx
new file mode 100644
index 000000000..0f2722873
--- /dev/null
+++ b/frontend/src/components/tools/adjustContrast/AdjustContrastColorSettings.tsx
@@ -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: (key: K, value: AdjustContrastParameters[K]) => void;
+ disabled?: boolean;
+}
+
+export default function AdjustContrastColorSettings({ parameters, onParameterChange, disabled }: Props) {
+ const { t } = useTranslation();
+
+ return (
+
+ onParameterChange('red', v as any)} disabled={disabled} />
+ onParameterChange('green', v as any)} disabled={disabled} />
+ onParameterChange('blue', v as any)} disabled={disabled} />
+
+ );
+}
+
+
diff --git a/frontend/src/components/tools/adjustContrast/AdjustContrastPreview.tsx b/frontend/src/components/tools/adjustContrast/AdjustContrastPreview.tsx
new file mode 100644
index 000000000..a50fb98ca
--- /dev/null
+++ b/frontend/src/components/tools/adjustContrast/AdjustContrastPreview.tsx
@@ -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(null);
+ const canvasRef = useRef(null);
+ const [thumb, setThumb] = useState(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((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 (
+
+
+
+
{t('common.preview', 'Preview')}
+
+
+
{t('adjustContrast.noPreview', 'Select a PDF to preview')}}
+ borderRadius={6}
+ >
+
+ {thumb && (
+
+ )}
+
+
+
+ );
+}
+
+
diff --git a/frontend/src/components/tools/adjustContrast/AdjustContrastSingleStepSettings.tsx b/frontend/src/components/tools/adjustContrast/AdjustContrastSingleStepSettings.tsx
new file mode 100644
index 000000000..ce19b8012
--- /dev/null
+++ b/frontend/src/components/tools/adjustContrast/AdjustContrastSingleStepSettings.tsx
@@ -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: (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 (
+
+
+
+
+ );
+}
+
+
diff --git a/frontend/src/components/tools/adjustContrast/utils.ts b/frontend/src/components/tools/adjustContrast/utils.ts
new file mode 100644
index 000000000..16dfc199f
--- /dev/null
+++ b/frontend/src/components/tools/adjustContrast/utils.ts
@@ -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;
+}
+
+
diff --git a/frontend/src/components/tools/shared/createToolFlow.tsx b/frontend/src/components/tools/shared/createToolFlow.tsx
index 4724648c8..ffa4b0db6 100644
--- a/frontend/src/components/tools/shared/createToolFlow.tsx
+++ b/frontend/src/components/tools/shared/createToolFlow.tsx
@@ -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 && (
,
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: ,
diff --git a/frontend/src/hooks/tools/adjustContrast/useAdjustContrastOperation.ts b/frontend/src/hooks/tools/adjustContrast/useAdjustContrastOperation.ts
new file mode 100644
index 000000000..c52ed9864
--- /dev/null
+++ b/frontend/src/hooks/tools/adjustContrast/useAdjustContrastOperation.ts
@@ -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 {
+ 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 {
+ 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 {
+ // 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 (items: T[], limit: number, worker: (item: T, index: number) => Promise): Promise => {
+ 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({
+ ...adjustContrastOperationConfig,
+ getErrorMessage: () => t('adjustContrast.error.failed', 'Failed to adjust colors/contrast')
+ });
+};
+
diff --git a/frontend/src/hooks/tools/adjustContrast/useAdjustContrastParameters.ts b/frontend/src/hooks/tools/adjustContrast/useAdjustContrastParameters.ts
new file mode 100644
index 000000000..14dd543d8
--- /dev/null
+++ b/frontend/src/hooks/tools/adjustContrast/useAdjustContrastParameters.ts
@@ -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;
+
+export const useAdjustContrastParameters = (): AdjustContrastParametersHook => {
+ return useBaseParameters({
+ defaultParameters,
+ endpointName: '',
+ });
+};
+
+
diff --git a/frontend/src/tools/AdjustContrast.tsx b/frontend/src/tools/AdjustContrast.tsx
new file mode 100644
index 000000000..0de528a40
--- /dev/null
+++ b/frontend/src/tools/AdjustContrast.tsx
@@ -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({
+ 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: (
+
+ ),
+ },
+ {
+ title: t('adjustContrast.adjustColors', 'Adjust Colors'),
+ isCollapsed: accordion.getCollapsedState(Step.COLORS),
+ onCollapsedClick: () => accordion.handleStepToggle(Step.COLORS),
+ content: (
+
+ ),
+ },
+ ],
+ preview: (
+
+
+
+
+ {totalSelected > 1 && (
+
+ {`${previewIndex + 1} of ${totalSelected}`}
+
+ )}
+
+ ),
+ 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;
+
+
From 8aa6aff53acc86aae58d5da10b5bf396a253ab31 Mon Sep 17 00:00:00 2001
From: ConnorYoh <40631091+ConnorYoh@users.noreply.github.com>
Date: Thu, 2 Oct 2025 17:49:10 +0100
Subject: [PATCH 2/4] Config becomes account when enableLogin (#4585)
Co-authored-by: Connor Yoh
---
frontend/public/locales/en-GB/translation.json | 1 +
frontend/src/components/shared/QuickAccessBar.tsx | 6 ++++--
2 files changed, 5 insertions(+), 2 deletions(-)
diff --git a/frontend/public/locales/en-GB/translation.json b/frontend/public/locales/en-GB/translation.json
index 2772ad15e..71f0bb90b 100644
--- a/frontend/public/locales/en-GB/translation.json
+++ b/frontend/public/locales/en-GB/translation.json
@@ -3127,6 +3127,7 @@
"automate": "Automate",
"files": "Files",
"activity": "Activity",
+ "account": "Account",
"config": "Config",
"allTools": "All Tools"
},
diff --git a/frontend/src/components/shared/QuickAccessBar.tsx b/frontend/src/components/shared/QuickAccessBar.tsx
index b7206fa93..af9c8acff 100644
--- a/frontend/src/components/shared/QuickAccessBar.tsx
+++ b/frontend/src/components/shared/QuickAccessBar.tsx
@@ -13,6 +13,7 @@ import './quickAccessBar/QuickAccessBar.css';
import AllToolsNavButton from './AllToolsNavButton';
import ActiveToolButton from "./quickAccessBar/ActiveToolButton";
import AppConfigModal from './AppConfigModal';
+import { useAppConfig } from '../../hooks/useAppConfig';
import {
isNavButtonActive,
getNavButtonStyle,
@@ -25,6 +26,7 @@ const QuickAccessBar = forwardRef((_, ref) => {
const { openFilesModal, isFilesModalOpen } = useFilesModalContext();
const { handleReaderToggle, handleBackToTools, handleToolSelect, selectedToolKey, leftPanelView, toolRegistry, readerMode, resetTool } = useToolWorkflow();
const { getToolNavigation } = useSidebarNavigation();
+ const { config } = useAppConfig();
const [configModalOpen, setConfigModalOpen] = useState(false);
const [activeButton, setActiveButton] = useState('tools');
const scrollableRef = useRef(null);
@@ -151,8 +153,8 @@ const QuickAccessBar = forwardRef((_, ref) => {
//},
{
id: 'config',
- name: t("quickAccess.config", "Config"),
- icon: ,
+ name: config?.enableLogin ? t("quickAccess.account", "Account") : t("quickAccess.config", "Config"),
+ icon: config?.enableLogin ? : ,
size: 'lg',
type: 'modal',
onClick: () => {
From f9ac1bd62e5dec32135150e51d4975552057c3cb Mon Sep 17 00:00:00 2001
From: Reece Browne <74901996+reecebrowne@users.noreply.github.com>
Date: Thu, 2 Oct 2025 20:14:35 +0100
Subject: [PATCH 3/4] Feature/v2/navigate save prompt (#4586)
# Description of Changes
---
## 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.
---
.../src/components/pageEditor/PageEditor.tsx | 108 ++++++++----------
.../components/pageEditor/PageThumbnail.tsx | 1 +
.../pageEditor/commands/pageCommands.ts | 14 ++-
.../shared/NavigationWarningModal.tsx | 56 +++++----
.../src/components/viewer/EmbedPdfViewer.tsx | 107 +++++++++++++++--
.../components/viewer/SignatureAPIBridge.tsx | 2 +-
frontend/src/contexts/NavigationContext.tsx | 47 +++++++-
frontend/src/contexts/file/fileActions.ts | 22 +++-
.../services/documentManipulationService.ts | 55 ++++-----
.../services/enhancedPDFProcessingService.ts | 28 +++--
frontend/src/services/fileStubHelpers.ts | 34 ++++++
frontend/src/services/pdfExportHelpers.ts | 46 ++++++++
frontend/src/services/pdfExportService.ts | 20 +---
.../services/thumbnailGenerationService.ts | 2 +-
frontend/src/utils/thumbnailUtils.ts | 23 +++-
15 files changed, 396 insertions(+), 169 deletions(-)
create mode 100644 frontend/src/services/fileStubHelpers.ts
create mode 100644 frontend/src/services/pdfExportHelpers.ts
diff --git a/frontend/src/components/pageEditor/PageEditor.tsx b/frontend/src/components/pageEditor/PageEditor.tsx
index b24fb819a..ee77d142d 100644
--- a/frontend/src/components/pageEditor/PageEditor.tsx
+++ b/frontend/src/components/pageEditor/PageEditor.tsx
@@ -5,6 +5,8 @@ import { useNavigationGuard } from "../../contexts/NavigationContext";
import { PDFDocument, PageEditorFunctions } from "../../types/pageEditor";
import { pdfExportService } from "../../services/pdfExportService";
import { documentManipulationService } from "../../services/documentManipulationService";
+import { exportProcessedDocumentsToFiles } from "../../services/pdfExportHelpers";
+import { createStirlingFilesAndStubs } from "../../services/fileStubHelpers";
// Thumbnail generation is now handled by individual PageThumbnail components
import './PageEditor.module.css';
import PageThumbnail from './PageThumbnail';
@@ -524,66 +526,38 @@ const PageEditor = ({
try {
// Step 1: Apply DOM changes to document state first
const processedDocuments = documentManipulationService.applyDOMChangesToDocument(
- mergedPdfDocument || displayDocument, // Original order
- displayDocument, // Current display order (includes reordering)
- splitPositions // Position-based splits
+ mergedPdfDocument || displayDocument,
+ displayDocument,
+ splitPositions
);
- // Step 2: Check if we have multiple documents (splits) or single document
- if (Array.isArray(processedDocuments)) {
- // Multiple documents (splits) - export as ZIP
- const blobs: Blob[] = [];
- const filenames: string[] = [];
+ // Step 2: Export to files
+ const sourceFiles = getSourceFiles();
+ const exportFilename = getExportFilename();
+ const files = await exportProcessedDocumentsToFiles(processedDocuments, sourceFiles, exportFilename);
- const sourceFiles = getSourceFiles();
- const baseExportFilename = getExportFilename();
- const baseName = baseExportFilename.replace(/\.pdf$/i, '');
-
- for (let i = 0; i < processedDocuments.length; i++) {
- const doc = processedDocuments[i];
- const partFilename = `${baseName}_part_${i + 1}.pdf`;
-
- const result = sourceFiles
- ? await pdfExportService.exportPDFMultiFile(doc, sourceFiles, [], { filename: partFilename })
- : await pdfExportService.exportPDF(doc, [], { filename: partFilename });
- blobs.push(result.blob);
- filenames.push(result.filename);
- }
-
- // Create ZIP file
+ // Step 3: Download
+ if (files.length > 1) {
+ // Multiple files - create ZIP
const JSZip = await import('jszip');
const zip = new JSZip.default();
- blobs.forEach((blob, index) => {
- zip.file(filenames[index], blob);
+ files.forEach((file) => {
+ zip.file(file.name, file);
});
const zipBlob = await zip.generateAsync({ type: 'blob' });
- const zipFilename = baseExportFilename.replace(/\.pdf$/i, '.zip');
+ const exportFilename = getExportFilename();
+ const zipFilename = exportFilename.replace(/\.pdf$/i, '.zip');
pdfExportService.downloadFile(zipBlob, zipFilename);
- setHasUnsavedChanges(false); // Clear unsaved changes after successful export
} else {
- // Single document - regular export
- const sourceFiles = getSourceFiles();
- const exportFilename = getExportFilename();
- const result = sourceFiles
- ? await pdfExportService.exportPDFMultiFile(
- processedDocuments,
- sourceFiles,
- [],
- { selectedOnly: false, filename: exportFilename }
- )
- : await pdfExportService.exportPDF(
- processedDocuments,
- [],
- { selectedOnly: false, filename: exportFilename }
- );
-
- pdfExportService.downloadFile(result.blob, result.filename);
- setHasUnsavedChanges(false); // Clear unsaved changes after successful export
+ // Single file - download directly
+ const file = files[0];
+ pdfExportService.downloadFile(file, file.name);
}
+ setHasUnsavedChanges(false);
setExportLoading(false);
} catch (error) {
console.error('Export failed:', error);
@@ -592,21 +566,39 @@ const PageEditor = ({
}, [displayDocument, mergedPdfDocument, splitPositions, getSourceFiles, getExportFilename, setHasUnsavedChanges]);
// Apply DOM changes to document state using dedicated service
- const applyChanges = useCallback(() => {
+ const applyChanges = useCallback(async () => {
if (!displayDocument) return;
- // Pass current display document (which includes reordering) to get both reordering AND DOM changes
- const processedDocuments = documentManipulationService.applyDOMChangesToDocument(
- mergedPdfDocument || displayDocument, // Original order
- displayDocument, // Current display order (includes reordering)
- splitPositions // Position-based splits
- );
+ setExportLoading(true);
+ try {
+ // Step 1: Apply DOM changes to document state first
+ const processedDocuments = documentManipulationService.applyDOMChangesToDocument(
+ mergedPdfDocument || displayDocument,
+ displayDocument,
+ splitPositions
+ );
- // For apply changes, we only set the first document if it's an array (splits shouldn't affect document state)
- const documentToSet = Array.isArray(processedDocuments) ? processedDocuments[0] : processedDocuments;
- setEditedDocument(documentToSet);
+ // Step 2: Export to files
+ const sourceFiles = getSourceFiles();
+ const exportFilename = getExportFilename();
+ const files = await exportProcessedDocumentsToFiles(processedDocuments, sourceFiles, exportFilename);
- }, [displayDocument, mergedPdfDocument, splitPositions]);
+ // Step 3: Create StirlingFiles and stubs for version history
+ const parentStub = selectors.getStirlingFileStub(activeFileIds[0]);
+ if (!parentStub) throw new Error('Parent stub not found');
+
+ const { stirlingFiles, stubs } = await createStirlingFilesAndStubs(files, parentStub, 'multiTool');
+
+ // Step 4: Consume files (replace in context)
+ await actions.consumeFiles(activeFileIds, stirlingFiles, stubs);
+
+ setHasUnsavedChanges(false);
+ setExportLoading(false);
+ } catch (error) {
+ console.error('Apply changes failed:', error);
+ setExportLoading(false);
+ }
+ }, [displayDocument, mergedPdfDocument, splitPositions, activeFileIds, getSourceFiles, getExportFilename, actions, selectors, setHasUnsavedChanges]);
const closePdf = useCallback(() => {
@@ -793,7 +785,7 @@ const PageEditor = ({
{
- applyChanges();
+ await applyChanges();
}}
onExportAndContinue={async () => {
await onExportAll();
diff --git a/frontend/src/components/pageEditor/PageThumbnail.tsx b/frontend/src/components/pageEditor/PageThumbnail.tsx
index 6abda0c78..59e5819d9 100644
--- a/frontend/src/components/pageEditor/PageThumbnail.tsx
+++ b/frontend/src/components/pageEditor/PageThumbnail.tsx
@@ -375,6 +375,7 @@ const PageThumbnail: React.FC = ({
src={thumbnailUrl}
alt={`Page ${page.pageNumber}`}
draggable={false}
+ data-original-rotation={page.rotation}
style={{
width: '100%',
height: '100%',
diff --git a/frontend/src/components/pageEditor/commands/pageCommands.ts b/frontend/src/components/pageEditor/commands/pageCommands.ts
index 26cb9e09c..7ac6a8377 100644
--- a/frontend/src/components/pageEditor/commands/pageCommands.ts
+++ b/frontend/src/components/pageEditor/commands/pageCommands.ts
@@ -17,32 +17,34 @@ export class RotatePageCommand extends DOMCommand {
}
execute(): void {
- // Only update DOM for immediate visual feedback
const pageElement = document.querySelector(`[data-page-id="${this.pageId}"]`);
if (pageElement) {
const img = pageElement.querySelector('img');
if (img) {
- // Extract current rotation from transform property to match the animated CSS
const currentTransform = img.style.transform || '';
const rotateMatch = currentTransform.match(/rotate\(([^)]+)\)/);
const currentRotation = rotateMatch ? parseInt(rotateMatch[1]) : 0;
- const newRotation = currentRotation + this.degrees;
+ let newRotation = currentRotation + this.degrees;
+
+ newRotation = ((newRotation % 360) + 360) % 360;
+
img.style.transform = `rotate(${newRotation}deg)`;
}
}
}
undo(): void {
- // Only update DOM
const pageElement = document.querySelector(`[data-page-id="${this.pageId}"]`);
if (pageElement) {
const img = pageElement.querySelector('img');
if (img) {
- // Extract current rotation from transform property
const currentTransform = img.style.transform || '';
const rotateMatch = currentTransform.match(/rotate\(([^)]+)\)/);
const currentRotation = rotateMatch ? parseInt(rotateMatch[1]) : 0;
- const previousRotation = currentRotation - this.degrees;
+ let previousRotation = currentRotation - this.degrees;
+
+ previousRotation = ((previousRotation % 360) + 360) % 360;
+
img.style.transform = `rotate(${previousRotation}deg)`;
}
}
diff --git a/frontend/src/components/shared/NavigationWarningModal.tsx b/frontend/src/components/shared/NavigationWarningModal.tsx
index 203e66ff7..b1b935738 100644
--- a/frontend/src/components/shared/NavigationWarningModal.tsx
+++ b/frontend/src/components/shared/NavigationWarningModal.tsx
@@ -8,7 +8,7 @@ interface NavigationWarningModalProps {
}
const NavigationWarningModal = ({
- onApplyAndContinue: _onApplyAndContinue,
+ onApplyAndContinue,
onExportAndContinue
}: NavigationWarningModalProps) => {
@@ -30,6 +30,13 @@ const NavigationWarningModal = ({
confirmNavigation();
};
+ const handleApplyAndContinue = async () => {
+ if (onApplyAndContinue) {
+ await onApplyAndContinue();
+ }
+ setHasUnsavedChanges(false);
+ confirmNavigation();
+ };
const handleExportAndContinue = async () => {
if (onExportAndContinue) {
@@ -49,26 +56,25 @@ const NavigationWarningModal = ({
onClose={handleKeepWorking}
title={t("unsavedChangesTitle", "Unsaved Changes")}
centered
- size="lg"
+ size="xl"
closeOnClickOutside={false}
closeOnEscape={false}
>
-
-
+
+
{t("unsavedChanges", "You have unsaved changes to your PDF. What would you like to do?")}
+
+
+
-
-
-
-
+
- {/* TODO:: Add this back in when it works */}
- {/* {_onApplyAndContinue && (
+
+ {onExportAndContinue && (
+
+ )}
+
+ {onApplyAndContinue && (
- )} */}
-
- {onExportAndContinue && (
-
)}
diff --git a/frontend/src/components/viewer/EmbedPdfViewer.tsx b/frontend/src/components/viewer/EmbedPdfViewer.tsx
index f8a7102fa..f8c6d0e4f 100644
--- a/frontend/src/components/viewer/EmbedPdfViewer.tsx
+++ b/frontend/src/components/viewer/EmbedPdfViewer.tsx
@@ -1,16 +1,18 @@
-import React from 'react';
+import React, { useCallback, useEffect, useRef } from 'react';
import { Box, Center, Text, ActionIcon } from '@mantine/core';
import { useMantineTheme, useMantineColorScheme } from '@mantine/core';
import CloseIcon from '@mui/icons-material/Close';
-import { useFileState } from "../../contexts/FileContext";
+import { useFileState, useFileActions } from "../../contexts/FileContext";
import { useFileWithUrl } from "../../hooks/useFileWithUrl";
import { useViewer } from "../../contexts/ViewerContext";
import { LocalEmbedPDF } from './LocalEmbedPDF';
import { PdfViewerToolbar } from './PdfViewerToolbar';
import { ThumbnailSidebar } from './ThumbnailSidebar';
-import { useNavigationState } from '../../contexts/NavigationContext';
+import { useNavigationGuard, useNavigationState } from '../../contexts/NavigationContext';
import { useSignature } from '../../contexts/SignatureContext';
+import { createStirlingFilesAndStubs } from '../../services/fileStubHelpers';
+import NavigationWarningModal from '../shared/NavigationWarningModal';
export interface EmbedPdfViewerProps {
sidebarsVisible: boolean;
@@ -29,11 +31,33 @@ const EmbedPdfViewerContent = ({
const { colorScheme: _colorScheme } = useMantineColorScheme();
const viewerRef = React.useRef(null);
const [isViewerHovered, setIsViewerHovered] = React.useState(false);
- const { isThumbnailSidebarVisible, toggleThumbnailSidebar, zoomActions, spreadActions, panActions: _panActions, rotationActions: _rotationActions, getScrollState, getZoomState, getSpreadState, isAnnotationMode, isAnnotationsVisible } = useViewer();
+
+ const { isThumbnailSidebarVisible, toggleThumbnailSidebar, zoomActions, spreadActions, panActions: _panActions, rotationActions: _rotationActions, getScrollState, getZoomState, getSpreadState, getRotationState, isAnnotationMode, isAnnotationsVisible, exportActions } = useViewer();
const scrollState = getScrollState();
const zoomState = getZoomState();
const spreadState = getSpreadState();
+ const rotationState = getRotationState();
+
+ // Track initial rotation to detect changes
+ const initialRotationRef = useRef(null);
+ useEffect(() => {
+ if (initialRotationRef.current === null && rotationState.rotation !== undefined) {
+ initialRotationRef.current = rotationState.rotation;
+ }
+ }, [rotationState.rotation]);
+
+ // Get signature context
+ const { signatureApiRef, historyApiRef } = useSignature();
+
+ // Get current file from FileContext
+ const { selectors } = useFileState();
+ const { actions } = useFileActions();
+ const activeFiles = selectors.getFiles();
+ const activeFileIds = activeFiles.map(f => f.fileId);
+
+ // Navigation guard for unsaved changes
+ const { setHasUnsavedChanges, registerUnsavedChangesChecker, unregisterUnsavedChangesChecker } = useNavigationGuard();
// Check if we're in signature mode OR viewer annotation mode
const { selectedTool } = useNavigationState();
@@ -42,13 +66,6 @@ const EmbedPdfViewerContent = ({
// Enable annotations when: in sign mode, OR annotation mode is active, OR we want to show existing annotations
const shouldEnableAnnotations = isSignatureMode || isAnnotationMode || isAnnotationsVisible;
- // Get signature context
- const { signatureApiRef, historyApiRef } = useSignature();
-
- // Get current file from FileContext
- const { selectors } = useFileState();
- const activeFiles = selectors.getFiles();
-
// Determine which file to display
const currentFile = React.useMemo(() => {
if (previewFile) {
@@ -134,6 +151,65 @@ const EmbedPdfViewerContent = ({
};
}, [isViewerHovered]);
+ // Register checker for unsaved changes (annotations only for now)
+ useEffect(() => {
+ if (previewFile) {
+ return;
+ }
+
+ const checkForChanges = () => {
+ // Check for annotation changes via history
+ const hasAnnotationChanges = historyApiRef.current?.canUndo() || false;
+
+ console.log('[Viewer] Checking for unsaved changes:', {
+ hasAnnotationChanges
+ });
+ return hasAnnotationChanges;
+ };
+
+ console.log('[Viewer] Registering unsaved changes checker');
+ registerUnsavedChangesChecker(checkForChanges);
+
+ return () => {
+ console.log('[Viewer] Unregistering unsaved changes checker');
+ unregisterUnsavedChangesChecker();
+ };
+ }, [historyApiRef, previewFile, registerUnsavedChangesChecker, unregisterUnsavedChangesChecker]);
+
+ // Apply changes - save annotations to new file version
+ const applyChanges = useCallback(async () => {
+ if (!currentFile || activeFileIds.length === 0) return;
+
+ try {
+ console.log('[Viewer] Applying changes - exporting PDF with annotations');
+
+ // Step 1: Export PDF with annotations using EmbedPDF
+ const arrayBuffer = await exportActions.saveAsCopy();
+ if (!arrayBuffer) {
+ throw new Error('Failed to export PDF');
+ }
+
+ console.log('[Viewer] Exported PDF size:', arrayBuffer.byteLength);
+
+ // Step 2: Convert ArrayBuffer to File
+ const blob = new Blob([arrayBuffer], { type: 'application/pdf' });
+ const filename = currentFile.name || 'document.pdf';
+ const file = new File([blob], filename, { type: 'application/pdf' });
+
+ // Step 3: Create StirlingFiles and stubs for version history
+ const parentStub = selectors.getStirlingFileStub(activeFileIds[0]);
+ if (!parentStub) throw new Error('Parent stub not found');
+
+ const { stirlingFiles, stubs } = await createStirlingFilesAndStubs([file], parentStub, 'multiTool');
+
+ // Step 4: Consume files (replace in context)
+ await actions.consumeFiles(activeFileIds, stirlingFiles, stubs);
+
+ setHasUnsavedChanges(false);
+ } catch (error) {
+ console.error('Apply changes failed:', error);
+ }
+ }, [currentFile, activeFileIds, exportActions, actions, selectors, setHasUnsavedChanges]);
return (
+
+ {/* Navigation Warning Modal */}
+ {!previewFile && (
+ {
+ await applyChanges();
+ }}
+ />
+ )}
);
};
diff --git a/frontend/src/components/viewer/SignatureAPIBridge.tsx b/frontend/src/components/viewer/SignatureAPIBridge.tsx
index 59fbe43e6..6ff048575 100644
--- a/frontend/src/components/viewer/SignatureAPIBridge.tsx
+++ b/frontend/src/components/viewer/SignatureAPIBridge.tsx
@@ -28,7 +28,7 @@ export const SignatureAPIBridge = forwardRef(function SignatureAPI
useEffect(() => {
if (!annotationApi || (!isPlacementMode && !isAnnotationMode)) return;
- const handleKeyDown = (event: KeyboardEvent) => {
+ const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === 'Delete' || event.key === 'Backspace') {
const selectedAnnotation = annotationApi.getSelectedAnnotation?.();
diff --git a/frontend/src/contexts/NavigationContext.tsx b/frontend/src/contexts/NavigationContext.tsx
index b71664b6b..3d9b4849c 100644
--- a/frontend/src/contexts/NavigationContext.tsx
+++ b/frontend/src/contexts/NavigationContext.tsx
@@ -74,6 +74,8 @@ export interface NavigationContextActions {
setSelectedTool: (toolId: ToolId | null) => void;
setToolAndWorkbench: (toolId: ToolId | null, workbench: WorkbenchType) => void;
setHasUnsavedChanges: (hasChanges: boolean) => void;
+ registerUnsavedChangesChecker: (checker: () => boolean) => void;
+ unregisterUnsavedChangesChecker: () => void;
showNavigationWarning: (show: boolean) => void;
requestNavigation: (navigationFn: () => void) => void;
confirmNavigation: () => void;
@@ -106,11 +108,29 @@ export const NavigationProvider: React.FC<{
}> = ({ children }) => {
const [state, dispatch] = useReducer(navigationReducer, initialState);
const toolRegistry = useFlatToolRegistry();
+ const unsavedChangesCheckerRef = React.useRef<(() => boolean) | null>(null);
const actions: NavigationContextActions = {
setWorkbench: useCallback((workbench: WorkbenchType) => {
- // If we're leaving pageEditor workbench and have unsaved changes, request navigation
- if (state.workbench === 'pageEditor' && workbench !== 'pageEditor' && state.hasUnsavedChanges) {
+ // Check for unsaved changes using registered checker or state
+ const hasUnsavedChanges = unsavedChangesCheckerRef.current?.() || state.hasUnsavedChanges;
+ console.log('[NavigationContext] setWorkbench:', {
+ from: state.workbench,
+ to: workbench,
+ hasChecker: !!unsavedChangesCheckerRef.current,
+ hasUnsavedChanges
+ });
+
+ // If we're leaving pageEditor or viewer workbench and have unsaved changes, request navigation
+ const leavingWorkbenchWithChanges =
+ (state.workbench === 'pageEditor' && workbench !== 'pageEditor' && hasUnsavedChanges) ||
+ (state.workbench === 'viewer' && workbench !== 'viewer' && hasUnsavedChanges);
+
+ if (leavingWorkbenchWithChanges) {
+ // Update state to reflect unsaved changes so modal knows
+ if (!state.hasUnsavedChanges) {
+ dispatch({ type: 'SET_UNSAVED_CHANGES', payload: { hasChanges: true } });
+ }
const performWorkbenchChange = () => {
dispatch({ type: 'SET_WORKBENCH', payload: { workbench } });
};
@@ -126,8 +146,15 @@ export const NavigationProvider: React.FC<{
}, []),
setToolAndWorkbench: useCallback((toolId: ToolId | null, workbench: WorkbenchType) => {
- // If we're leaving pageEditor workbench and have unsaved changes, request navigation
- if (state.workbench === 'pageEditor' && workbench !== 'pageEditor' && state.hasUnsavedChanges) {
+ // Check for unsaved changes using registered checker or state
+ const hasUnsavedChanges = unsavedChangesCheckerRef.current?.() || state.hasUnsavedChanges;
+
+ // If we're leaving pageEditor or viewer workbench and have unsaved changes, request navigation
+ const leavingWorkbenchWithChanges =
+ (state.workbench === 'pageEditor' && workbench !== 'pageEditor' && hasUnsavedChanges) ||
+ (state.workbench === 'viewer' && workbench !== 'viewer' && hasUnsavedChanges);
+
+ if (leavingWorkbenchWithChanges) {
const performWorkbenchChange = () => {
dispatch({ type: 'SET_TOOL_AND_WORKBENCH', payload: { toolId, workbench } });
};
@@ -142,6 +169,14 @@ export const NavigationProvider: React.FC<{
dispatch({ type: 'SET_UNSAVED_CHANGES', payload: { hasChanges } });
}, []),
+ registerUnsavedChangesChecker: useCallback((checker: () => boolean) => {
+ unsavedChangesCheckerRef.current = checker;
+ }, []),
+
+ unregisterUnsavedChangesChecker: useCallback(() => {
+ unsavedChangesCheckerRef.current = null;
+ }, []),
+
showNavigationWarning: useCallback((show: boolean) => {
dispatch({ type: 'SHOW_NAVIGATION_WARNING', payload: { show } });
}, []),
@@ -254,6 +289,8 @@ export const useNavigationGuard = () => {
confirmNavigation: actions.confirmNavigation,
cancelNavigation: actions.cancelNavigation,
setHasUnsavedChanges: actions.setHasUnsavedChanges,
- setShowNavigationWarning: actions.showNavigationWarning
+ setShowNavigationWarning: actions.showNavigationWarning,
+ registerUnsavedChangesChecker: actions.registerUnsavedChangesChecker,
+ unregisterUnsavedChangesChecker: actions.unregisterUnsavedChangesChecker
};
};
diff --git a/frontend/src/contexts/file/fileActions.ts b/frontend/src/contexts/file/fileActions.ts
index 1c817132a..3f3ec07c7 100644
--- a/frontend/src/contexts/file/fileActions.ts
+++ b/frontend/src/contexts/file/fileActions.ts
@@ -57,13 +57,13 @@ const addFilesMutex = new SimpleMutex();
/**
* Helper to create ProcessedFile metadata structure
*/
-export function createProcessedFile(pageCount: number, thumbnail?: string) {
+export function createProcessedFile(pageCount: number, thumbnail?: string, pageRotations?: number[]) {
return {
totalPages: pageCount,
pages: Array.from({ length: pageCount }, (_, index) => ({
pageNumber: index + 1,
thumbnail: index === 0 ? thumbnail : undefined, // Only first page gets thumbnail initially
- rotation: 0,
+ rotation: pageRotations?.[index] ?? 0,
splitBefore: false
})),
thumbnailUrl: thumbnail,
@@ -82,8 +82,22 @@ export async function generateProcessedFileMetadata(file: File): Promise): PDFDocument | PDFDocument[] {
- console.log('DocumentManipulationService: Applying DOM changes to document');
- console.log('Original document page order:', pdfDocument.pages.map(p => p.pageNumber));
- console.log('Current display order:', currentDisplayOrder?.pages.map(p => p.pageNumber) || 'none provided');
- console.log('Split positions:', splitPositions ? Array.from(splitPositions).sort() : 'none');
-
// Use current display order (from React state) if provided, otherwise use original order
const baseDocument = currentDisplayOrder || pdfDocument;
- console.log('Using page order:', baseDocument.pages.map(p => p.pageNumber));
// Apply DOM changes to each page (rotation only now, splits are position-based)
let updatedPages = baseDocument.pages.map(page => this.applyPageChanges(page));
@@ -57,32 +51,25 @@ export class DocumentManipulationService {
private createSplitDocuments(document: PDFDocument): PDFDocument[] {
const documents: PDFDocument[] = [];
const splitPoints: number[] = [];
-
+
// Find split points
document.pages.forEach((page, index) => {
if (page.splitAfter) {
- console.log(`Found split marker at page ${page.pageNumber} (index ${index}), adding split point at ${index + 1}`);
splitPoints.push(index + 1);
}
});
-
+
// Add end point if not already there
if (splitPoints.length === 0 || splitPoints[splitPoints.length - 1] !== document.pages.length) {
splitPoints.push(document.pages.length);
}
-
- console.log('Final split points:', splitPoints);
- console.log('Total pages to split:', document.pages.length);
-
+
let startIndex = 0;
let partNumber = 1;
-
+
for (const endIndex of splitPoints) {
const segmentPages = document.pages.slice(startIndex, endIndex);
-
- console.log(`Creating split document ${partNumber}: pages ${startIndex}-${endIndex-1} (${segmentPages.length} pages)`);
- console.log(`Split document ${partNumber} page numbers:`, segmentPages.map(p => p.pageNumber));
-
+
if (segmentPages.length > 0) {
documents.push({
...document,
@@ -93,11 +80,10 @@ export class DocumentManipulationService {
});
partNumber++;
}
-
+
startIndex = endIndex;
}
-
- console.log(`Created ${documents.length} split documents`);
+
return documents;
}
@@ -108,7 +94,6 @@ export class DocumentManipulationService {
// Find the DOM element for this page
const pageElement = document.querySelector(`[data-page-id="${page.id}"]`);
if (!pageElement) {
- console.log(`Page ${page.pageNumber}: No DOM element found, keeping original state`);
return page;
}
@@ -116,8 +101,7 @@ export class DocumentManipulationService {
// Apply rotation changes from DOM
updatedPage.rotation = this.getRotationFromDOM(pageElement, page);
-
-
+
return updatedPage;
}
@@ -126,16 +110,21 @@ export class DocumentManipulationService {
*/
private getRotationFromDOM(pageElement: Element, originalPage: PDFPage): number {
const img = pageElement.querySelector('img');
- if (img && img.style.transform) {
- // Parse rotation from transform property (e.g., "rotate(90deg)" -> 90)
- const rotationMatch = img.style.transform.match(/rotate\((-?\d+)deg\)/);
- const domRotation = rotationMatch ? parseInt(rotationMatch[1]) : 0;
-
- console.log(`Page ${originalPage.pageNumber}: DOM rotation = ${domRotation}°, original = ${originalPage.rotation}°`);
- return domRotation;
+ if (img) {
+ const originalRotation = parseInt(img.getAttribute('data-original-rotation') || '0');
+
+ const currentTransform = img.style.transform || '';
+ const rotationMatch = currentTransform.match(/rotate\((-?\d+)deg\)/);
+ const visualRotation = rotationMatch ? parseInt(rotationMatch[1]) : originalRotation;
+
+ const userChange = ((visualRotation - originalRotation) % 360 + 360) % 360;
+
+ let finalRotation = (originalPage.rotation + userChange) % 360;
+ if (finalRotation === 360) finalRotation = 0;
+
+ return finalRotation;
}
-
- console.log(`Page ${originalPage.pageNumber}: No DOM rotation found, keeping original = ${originalPage.rotation}°`);
+
return originalPage.rotation;
}
diff --git a/frontend/src/services/enhancedPDFProcessingService.ts b/frontend/src/services/enhancedPDFProcessingService.ts
index bee6e200a..be58d0a6e 100644
--- a/frontend/src/services/enhancedPDFProcessingService.ts
+++ b/frontend/src/services/enhancedPDFProcessingService.ts
@@ -200,11 +200,13 @@ export class EnhancedPDFProcessingService {
const page = await pdf.getPage(i);
const thumbnail = await this.renderPageThumbnail(page, config.thumbnailQuality);
+ const rotation = page.rotate || 0;
+
pages.push({
id: `${createQuickKey(file)}-page-${i}`,
pageNumber: i,
thumbnail,
- rotation: 0,
+ rotation,
selected: false
});
@@ -254,7 +256,7 @@ export class EnhancedPDFProcessingService {
id: `${createQuickKey(file)}-page-${i}`,
pageNumber: i,
thumbnail,
- rotation: 0,
+ rotation: page.rotate || 0,
selected: false
});
@@ -265,11 +267,15 @@ export class EnhancedPDFProcessingService {
// Create placeholder pages for remaining pages
for (let i = priorityCount + 1; i <= totalPages; i++) {
+ // Load page just to get rotation
+ const page = await pdf.getPage(i);
+ const rotation = page.rotate || 0;
+
pages.push({
id: `${createQuickKey(file)}-page-${i}`,
pageNumber: i,
thumbnail: null, // Will be loaded lazily
- rotation: 0,
+ rotation,
selected: false
});
}
@@ -316,7 +322,7 @@ export class EnhancedPDFProcessingService {
id: `${createQuickKey(file)}-page-${i}`,
pageNumber: i,
thumbnail,
- rotation: 0,
+ rotation: page.rotate || 0,
selected: false
});
@@ -333,11 +339,15 @@ export class EnhancedPDFProcessingService {
// Create placeholders for remaining pages
for (let i = firstChunkEnd + 1; i <= totalPages; i++) {
+ // Load page just to get rotation
+ const page = await pdf.getPage(i);
+ const rotation = page.rotate || 0;
+
pages.push({
id: `${createQuickKey(file)}-page-${i}`,
pageNumber: i,
thumbnail: null,
- rotation: 0,
+ rotation,
selected: false
});
}
@@ -367,11 +377,15 @@ export class EnhancedPDFProcessingService {
// Create placeholder pages without thumbnails
const pages: PDFPage[] = [];
for (let i = 1; i <= totalPages; i++) {
+ // Load page just to get rotation
+ const page = await pdf.getPage(i);
+ const rotation = page.rotate || 0;
+
pages.push({
id: `${createQuickKey(file)}-page-${i}`,
pageNumber: i,
thumbnail: null,
- rotation: 0,
+ rotation,
selected: false
});
}
@@ -390,7 +404,7 @@ export class EnhancedPDFProcessingService {
const scales = { low: 0.2, medium: 0.5, high: 0.8 }; // Reduced low quality for page editor
const scale = scales[quality];
- const viewport = page.getViewport({ scale });
+ const viewport = page.getViewport({ scale, rotation: 0 });
const canvas = document.createElement('canvas');
canvas.width = viewport.width;
canvas.height = viewport.height;
diff --git a/frontend/src/services/fileStubHelpers.ts b/frontend/src/services/fileStubHelpers.ts
new file mode 100644
index 000000000..5f5545e76
--- /dev/null
+++ b/frontend/src/services/fileStubHelpers.ts
@@ -0,0 +1,34 @@
+import { StirlingFile, StirlingFileStub } from '../types/fileContext';
+import { createChildStub, generateProcessedFileMetadata } from '../contexts/file/fileActions';
+import { createStirlingFile } from '../types/fileContext';
+import { ToolId } from '../types/toolId';
+
+/**
+ * Create StirlingFiles and StirlingFileStubs from exported files
+ * Used when saving page editor changes to create version history
+ */
+export async function createStirlingFilesAndStubs(
+ files: File[],
+ parentStub: StirlingFileStub,
+ toolId: ToolId
+): Promise<{ stirlingFiles: StirlingFile[], stubs: StirlingFileStub[] }> {
+ const stirlingFiles: StirlingFile[] = [];
+ const stubs: StirlingFileStub[] = [];
+
+ for (const file of files) {
+ const processedFileMetadata = await generateProcessedFileMetadata(file);
+ const childStub = createChildStub(
+ parentStub,
+ { toolId, timestamp: Date.now() },
+ file,
+ processedFileMetadata?.thumbnailUrl,
+ processedFileMetadata
+ );
+
+ const stirlingFile = createStirlingFile(file, childStub.id);
+ stirlingFiles.push(stirlingFile);
+ stubs.push(childStub);
+ }
+
+ return { stirlingFiles, stubs };
+}
diff --git a/frontend/src/services/pdfExportHelpers.ts b/frontend/src/services/pdfExportHelpers.ts
new file mode 100644
index 000000000..fa6d4775f
--- /dev/null
+++ b/frontend/src/services/pdfExportHelpers.ts
@@ -0,0 +1,46 @@
+import { PDFDocument } from '../types/pageEditor';
+import { pdfExportService } from './pdfExportService';
+import { FileId } from '../types/file';
+
+/**
+ * Export processed documents to File objects
+ * Handles both single documents and split documents (multiple PDFs)
+ */
+export async function exportProcessedDocumentsToFiles(
+ processedDocuments: PDFDocument | PDFDocument[],
+ sourceFiles: Map | null,
+ exportFilename: string
+): Promise {
+ console.log('exportProcessedDocumentsToFiles called with:', {
+ isArray: Array.isArray(processedDocuments),
+ numDocs: Array.isArray(processedDocuments) ? processedDocuments.length : 1,
+ hasSourceFiles: sourceFiles !== null,
+ sourceFilesSize: sourceFiles?.size
+ });
+
+ if (Array.isArray(processedDocuments)) {
+ // Multiple documents (splits)
+ const files: File[] = [];
+ const baseName = exportFilename.replace(/\.pdf$/i, '');
+
+ for (let i = 0; i < processedDocuments.length; i++) {
+ const doc = processedDocuments[i];
+ const partFilename = `${baseName}_part_${i + 1}.pdf`;
+
+ const result = sourceFiles
+ ? await pdfExportService.exportPDFMultiFile(doc, sourceFiles, [], { selectedOnly: false, filename: partFilename })
+ : await pdfExportService.exportPDF(doc, [], { selectedOnly: false, filename: partFilename });
+
+ files.push(new File([result.blob], result.filename, { type: 'application/pdf' }));
+ }
+
+ return files;
+ } else {
+ // Single document
+ const result = sourceFiles
+ ? await pdfExportService.exportPDFMultiFile(processedDocuments, sourceFiles, [], { selectedOnly: false, filename: exportFilename })
+ : await pdfExportService.exportPDF(processedDocuments, [], { selectedOnly: false, filename: exportFilename });
+
+ return [new File([result.blob], result.filename, { type: 'application/pdf' })];
+ }
+}
diff --git a/frontend/src/services/pdfExportService.ts b/frontend/src/services/pdfExportService.ts
index 42ad672d2..d42eeeab2 100644
--- a/frontend/src/services/pdfExportService.ts
+++ b/frontend/src/services/pdfExportService.ts
@@ -98,10 +98,7 @@ export class PDFExportService {
// Create a blank page
const blankPage = newDoc.addPage(PageSizes.A4);
- // Apply rotation if needed
- if (page.rotation !== 0) {
- blankPage.setRotation(degrees(page.rotation));
- }
+ blankPage.setRotation(degrees(page.rotation));
} else if (page.originalFileId && loadedDocs.has(page.originalFileId)) {
// Get the correct source document for this page
const sourceDoc = loadedDocs.get(page.originalFileId)!;
@@ -111,10 +108,7 @@ export class PDFExportService {
// Copy the page from the correct source document
const [copiedPage] = await newDoc.copyPages(sourceDoc, [sourcePageIndex]);
- // Apply rotation
- if (page.rotation !== 0) {
- copiedPage.setRotation(degrees(page.rotation));
- }
+ copiedPage.setRotation(degrees(page.rotation));
newDoc.addPage(copiedPage);
}
@@ -147,10 +141,7 @@ export class PDFExportService {
// Create a blank page
const blankPage = newDoc.addPage(PageSizes.A4);
- // Apply rotation if needed
- if (page.rotation !== 0) {
- blankPage.setRotation(degrees(page.rotation));
- }
+ blankPage.setRotation(degrees(page.rotation));
} else {
// Get the original page from source document using originalPageNumber
const sourcePageIndex = page.originalPageNumber - 1;
@@ -159,10 +150,7 @@ export class PDFExportService {
// Copy the page
const [copiedPage] = await newDoc.copyPages(sourceDoc, [sourcePageIndex]);
- // Apply rotation
- if (page.rotation !== 0) {
- copiedPage.setRotation(degrees(page.rotation));
- }
+ copiedPage.setRotation(degrees(page.rotation));
newDoc.addPage(copiedPage);
}
diff --git a/frontend/src/services/thumbnailGenerationService.ts b/frontend/src/services/thumbnailGenerationService.ts
index fa0f17bdd..2d76a623f 100644
--- a/frontend/src/services/thumbnailGenerationService.ts
+++ b/frontend/src/services/thumbnailGenerationService.ts
@@ -164,7 +164,7 @@ export class ThumbnailGenerationService {
for (const pageNumber of batch) {
try {
const page = await pdf.getPage(pageNumber);
- const viewport = page.getViewport({ scale });
+ const viewport = page.getViewport({ scale, rotation: 0 });
const canvas = document.createElement('canvas');
canvas.width = viewport.width;
diff --git a/frontend/src/utils/thumbnailUtils.ts b/frontend/src/utils/thumbnailUtils.ts
index d00b67413..84dc64457 100644
--- a/frontend/src/utils/thumbnailUtils.ts
+++ b/frontend/src/utils/thumbnailUtils.ts
@@ -3,6 +3,7 @@ import { pdfWorkerManager } from '../services/pdfWorkerManager';
export interface ThumbnailWithMetadata {
thumbnail: string; // Always returns a thumbnail (placeholder if needed)
pageCount: number;
+ pageRotations?: number[]; // Rotation for each page (0, 90, 180, 270)
}
interface ColorScheme {
@@ -377,8 +378,10 @@ export async function generateThumbnailForFile(file: File): Promise {
/**
* Generate thumbnail and extract page count for a PDF file - always returns a valid thumbnail
+ * @param applyRotation - If true, render thumbnail with PDF rotation applied (for static display).
+ * If false, render without rotation (for CSS-based rotation in PageEditor)
*/
-export async function generateThumbnailWithMetadata(file: File): Promise {
+export async function generateThumbnailWithMetadata(file: File, applyRotation: boolean = true): Promise {
// Non-PDF files have no page count
if (!file.type.startsWith('application/pdf')) {
const thumbnail = await generateThumbnailForFile(file);
@@ -399,7 +402,13 @@ export async function generateThumbnailWithMetadata(file: File): Promise
Date: Fri, 3 Oct 2025 09:19:44 +0100
Subject: [PATCH 4/4] Bug/v2/mime (#4582)
# Description of Changes
---
## 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.
---
frontend/src/components/fileEditor/AddFileCard.tsx | 2 +-
frontend/src/components/shared/FileUploadButton.tsx | 2 +-
frontend/src/components/shared/LandingPage.tsx | 2 --
3 files changed, 2 insertions(+), 4 deletions(-)
diff --git a/frontend/src/components/fileEditor/AddFileCard.tsx b/frontend/src/components/fileEditor/AddFileCard.tsx
index dbc319c68..a873b7211 100644
--- a/frontend/src/components/fileEditor/AddFileCard.tsx
+++ b/frontend/src/components/fileEditor/AddFileCard.tsx
@@ -15,7 +15,7 @@ interface AddFileCardProps {
const AddFileCard = ({
onFileSelect,
- accept = "*/*",
+ accept,
multiple = true
}: AddFileCardProps) => {
const { t } = useTranslation();
diff --git a/frontend/src/components/shared/FileUploadButton.tsx b/frontend/src/components/shared/FileUploadButton.tsx
index f09cc19d0..85c3d5817 100644
--- a/frontend/src/components/shared/FileUploadButton.tsx
+++ b/frontend/src/components/shared/FileUploadButton.tsx
@@ -15,7 +15,7 @@ interface FileUploadButtonProps {
const FileUploadButton = ({
file,
onChange,
- accept = "*/*",
+ accept,
disabled = false,
placeholder,
variant = "outline",
diff --git a/frontend/src/components/shared/LandingPage.tsx b/frontend/src/components/shared/LandingPage.tsx
index 5f1fe8d8e..293efc1b4 100644
--- a/frontend/src/components/shared/LandingPage.tsx
+++ b/frontend/src/components/shared/LandingPage.tsx
@@ -41,7 +41,6 @@ const LandingPage = () => {
{/* White PDF Page Background */}
{
ref={fileInputRef}
type="file"
multiple
- accept="*/*"
onChange={handleFileSelect}
style={{ display: 'none' }}
/>