diff --git a/frontend/public/locales/en-GB/translation.json b/frontend/public/locales/en-GB/translation.json
index ff2ee732c..6ba7c83ba 100644
--- a/frontend/public/locales/en-GB/translation.json
+++ b/frontend/public/locales/en-GB/translation.json
@@ -1468,7 +1468,6 @@
"submit": "Submit"
},
"scalePages": {
- "tags": "resize,modify,dimension,adapt",
"title": "Adjust page-scale",
"header": "Adjust page-scale",
"pageSize": "Size of a page of the document.",
@@ -1476,6 +1475,44 @@
"scaleFactor": "Zoom level (crop) of a page.",
"submit": "Submit"
},
+ "adjustPageScale": {
+ "tags": "resize,modify,dimension,adapt",
+ "title": "Adjust Page Scale",
+ "header": "Adjust Page Scale",
+ "scaleFactor": {
+ "label": "Scale Factor"
+ },
+ "pageSize": {
+ "label": "Target Page Size",
+ "keep": "Keep Original Size",
+ "letter": "Letter",
+ "legal": "Legal"
+ },
+ "submit": "Adjust Page Scale",
+ "error": {
+ "failed": "An error occurred while adjusting the page scale."
+ },
+ "tooltip": {
+ "header": {
+ "title": "Page Scale Settings Overview"
+ },
+ "description": {
+ "title": "Description",
+ "text": "Adjust the size of PDF content and change the page dimensions."
+ },
+ "scaleFactor": {
+ "title": "Scale Factor",
+ "text": "Controls how large or small the content appears on the page. Content is scaled and centred - if scaled content is larger than the page size, it may be cropped.",
+ "bullet1": "1.0 = Original size",
+ "bullet2": "0.5 = Half size (50% smaller)",
+ "bullet3": "2.0 = Double size (200% larger, may crop)"
+ },
+ "pageSize": {
+ "title": "Target Page Size",
+ "text": "Sets the dimensions of the output PDF pages. 'Keep Original Size' maintains current dimensions, whilst other options resize to standard paper sizes."
+ }
+ }
+ },
"add-page-numbers": {
"tags": "paginate,label,organize,index"
},
diff --git a/frontend/src/components/tools/adjustPageScale/AdjustPageScaleSettings.test.tsx b/frontend/src/components/tools/adjustPageScale/AdjustPageScaleSettings.test.tsx
new file mode 100644
index 000000000..d71655cdc
--- /dev/null
+++ b/frontend/src/components/tools/adjustPageScale/AdjustPageScaleSettings.test.tsx
@@ -0,0 +1,64 @@
+import { describe, expect, test, vi, beforeEach } from 'vitest';
+import { render, screen } from '@testing-library/react';
+import { MantineProvider } from '@mantine/core';
+import AdjustPageScaleSettings from './AdjustPageScaleSettings';
+import { AdjustPageScaleParameters, PageSize } from '../../../hooks/tools/adjustPageScale/useAdjustPageScaleParameters';
+
+// Mock useTranslation with predictable return values
+const mockT = vi.fn((key: string, fallback?: string) => fallback || `mock-${key}`);
+vi.mock('react-i18next', () => ({
+ useTranslation: () => ({ t: mockT })
+}));
+
+// Wrapper component to provide Mantine context
+const TestWrapper = ({ children }: { children: React.ReactNode }) => (
+ {children}
+);
+
+describe('AdjustPageScaleSettings', () => {
+ const defaultParameters: AdjustPageScaleParameters = {
+ scaleFactor: 1.0,
+ pageSize: PageSize.KEEP,
+ };
+
+ const mockOnParameterChange = vi.fn();
+
+ beforeEach(() => {
+ vi.clearAllMocks();
+ });
+
+ test('should render without crashing', () => {
+ render(
+
+
+
+ );
+
+ // Basic render test - component renders without throwing
+ expect(screen.getByText('Scale Factor')).toBeInTheDocument();
+ expect(screen.getByText('Target Page Size')).toBeInTheDocument();
+ });
+
+ test('should render with custom parameters', () => {
+ const customParameters: AdjustPageScaleParameters = {
+ scaleFactor: 2.5,
+ pageSize: PageSize.A4,
+ };
+
+ render(
+
+
+
+ );
+
+ // Component renders successfully with custom parameters
+ expect(screen.getByText('Scale Factor')).toBeInTheDocument();
+ expect(screen.getByText('Target Page Size')).toBeInTheDocument();
+ });
+});
diff --git a/frontend/src/components/tools/adjustPageScale/AdjustPageScaleSettings.tsx b/frontend/src/components/tools/adjustPageScale/AdjustPageScaleSettings.tsx
new file mode 100644
index 000000000..9262bcba4
--- /dev/null
+++ b/frontend/src/components/tools/adjustPageScale/AdjustPageScaleSettings.tsx
@@ -0,0 +1,55 @@
+import { Stack, NumberInput, Select } from "@mantine/core";
+import { useTranslation } from "react-i18next";
+import { AdjustPageScaleParameters, PageSize } from "../../../hooks/tools/adjustPageScale/useAdjustPageScaleParameters";
+
+interface AdjustPageScaleSettingsProps {
+ parameters: AdjustPageScaleParameters;
+ onParameterChange: (key: K, value: AdjustPageScaleParameters[K]) => void;
+ disabled?: boolean;
+}
+
+const AdjustPageScaleSettings = ({ parameters, onParameterChange, disabled = false }: AdjustPageScaleSettingsProps) => {
+ const { t } = useTranslation();
+
+ const pageSizeOptions = [
+ { value: PageSize.KEEP, label: t('adjustPageScale.pageSize.keep', 'Keep Original Size') },
+ { value: PageSize.A0, label: 'A0' },
+ { value: PageSize.A1, label: 'A1' },
+ { value: PageSize.A2, label: 'A2' },
+ { value: PageSize.A3, label: 'A3' },
+ { value: PageSize.A4, label: 'A4' },
+ { value: PageSize.A5, label: 'A5' },
+ { value: PageSize.A6, label: 'A6' },
+ { value: PageSize.LETTER, label: t('adjustPageScale.pageSize.letter', 'Letter') },
+ { value: PageSize.LEGAL, label: t('adjustPageScale.pageSize.legal', 'Legal') },
+ ];
+
+ return (
+
+ onParameterChange('scaleFactor', typeof value === 'number' ? value : 1.0)}
+ min={0.1}
+ max={10.0}
+ step={0.1}
+ decimalScale={2}
+ disabled={disabled}
+ />
+
+
+ );
+};
+
+export default AdjustPageScaleSettings;
diff --git a/frontend/src/components/tooltips/useAdjustPageScaleTips.ts b/frontend/src/components/tooltips/useAdjustPageScaleTips.ts
new file mode 100644
index 000000000..dbd6bd9d2
--- /dev/null
+++ b/frontend/src/components/tooltips/useAdjustPageScaleTips.ts
@@ -0,0 +1,31 @@
+import { useTranslation } from 'react-i18next';
+import { TooltipContent } from '../../types/tips';
+
+export const useAdjustPageScaleTips = (): TooltipContent => {
+ const { t } = useTranslation();
+
+ return {
+ header: {
+ title: t("adjustPageScale.tooltip.header.title", "Page Scale Settings Overview")
+ },
+ tips: [
+ {
+ title: t("adjustPageScale.tooltip.description.title", "Description"),
+ description: t("adjustPageScale.tooltip.description.text", "Adjust the size of PDF content and change the page dimensions.")
+ },
+ {
+ title: t("adjustPageScale.tooltip.scaleFactor.title", "Scale Factor"),
+ description: t("adjustPageScale.tooltip.scaleFactor.text", "Controls how large or small the content appears on the page. Content is scaled and centered - if scaled content is larger than the page size, it may be cropped."),
+ bullets: [
+ t("adjustPageScale.tooltip.scaleFactor.bullet1", "1.0 = Original size"),
+ t("adjustPageScale.tooltip.scaleFactor.bullet2", "0.5 = Half size (50% smaller)"),
+ t("adjustPageScale.tooltip.scaleFactor.bullet3", "2.0 = Double size (200% larger, may crop)")
+ ]
+ },
+ {
+ title: t("adjustPageScale.tooltip.pageSize.title", "Target Page Size"),
+ description: t("adjustPageScale.tooltip.pageSize.text", "Sets the dimensions of the output PDF pages. 'Keep Original Size' maintains current dimensions, while other options resize to standard paper sizes.")
+ }
+ ]
+ };
+};
diff --git a/frontend/src/data/useTranslatedToolRegistry.tsx b/frontend/src/data/useTranslatedToolRegistry.tsx
index c88d46fec..f3050ea01 100644
--- a/frontend/src/data/useTranslatedToolRegistry.tsx
+++ b/frontend/src/data/useTranslatedToolRegistry.tsx
@@ -49,8 +49,11 @@ import ChangePermissionsSettings from "../components/tools/changePermissions/Cha
import FlattenSettings from "../components/tools/flatten/FlattenSettings";
import RedactSingleStepSettings from "../components/tools/redact/RedactSingleStepSettings";
import Redact from "../tools/Redact";
+import AdjustPageScale from "../tools/AdjustPageScale";
import { ToolId } from "../types/toolId";
import MergeSettings from '../components/tools/merge/MergeSettings';
+import { adjustPageScaleOperationConfig } from "../hooks/tools/adjustPageScale/useAdjustPageScaleOperation";
+import AdjustPageScaleSettings from "../components/tools/adjustPageScale/AdjustPageScaleSettings";
const showPlaceholderTools = true; // Show all tools; grey out unavailable ones in UI
@@ -337,11 +340,14 @@ export function useFlatToolRegistry(): ToolRegistry {
"adjust-page-size-scale": {
icon: ,
name: t("home.scalePages.title", "Adjust page size/scale"),
- component: null,
-
+ component: AdjustPageScale,
description: t("home.scalePages.desc", "Change the size/scale of a page and/or its contents."),
categoryId: ToolCategoryId.STANDARD_TOOLS,
subcategoryId: SubcategoryId.PAGE_FORMATTING,
+ maxFiles: -1,
+ endpoints: ["scale-pages"],
+ operationConfig: adjustPageScaleOperationConfig,
+ settingsComponent: AdjustPageScaleSettings,
},
addPageNumbers: {
icon: ,
diff --git a/frontend/src/hooks/tools/adjustPageScale/useAdjustPageScaleOperation.ts b/frontend/src/hooks/tools/adjustPageScale/useAdjustPageScaleOperation.ts
new file mode 100644
index 000000000..1728e5e1d
--- /dev/null
+++ b/frontend/src/hooks/tools/adjustPageScale/useAdjustPageScaleOperation.ts
@@ -0,0 +1,30 @@
+import { useTranslation } from 'react-i18next';
+import { useToolOperation, ToolType } from '../shared/useToolOperation';
+import { createStandardErrorHandler } from '../../../utils/toolErrorHandler';
+import { AdjustPageScaleParameters, defaultParameters } from './useAdjustPageScaleParameters';
+
+export const buildAdjustPageScaleFormData = (parameters: AdjustPageScaleParameters, file: File): FormData => {
+ const formData = new FormData();
+ formData.append("fileInput", file);
+ formData.append("scaleFactor", parameters.scaleFactor.toString());
+ formData.append("pageSize", parameters.pageSize);
+ return formData;
+};
+
+export const adjustPageScaleOperationConfig = {
+ toolType: ToolType.singleFile,
+ buildFormData: buildAdjustPageScaleFormData,
+ operationType: 'adjustPageScale',
+ endpoint: '/api/v1/general/scale-pages',
+ filePrefix: 'scaled_',
+ defaultParameters,
+} as const;
+
+export const useAdjustPageScaleOperation = () => {
+ const { t } = useTranslation();
+
+ return useToolOperation({
+ ...adjustPageScaleOperationConfig,
+ getErrorMessage: createStandardErrorHandler(t('adjustPageScale.error.failed', 'An error occurred while adjusting the page scale.'))
+ });
+};
diff --git a/frontend/src/hooks/tools/adjustPageScale/useAdjustPageScaleParameters.test.ts b/frontend/src/hooks/tools/adjustPageScale/useAdjustPageScaleParameters.test.ts
new file mode 100644
index 000000000..d68cdd861
--- /dev/null
+++ b/frontend/src/hooks/tools/adjustPageScale/useAdjustPageScaleParameters.test.ts
@@ -0,0 +1,142 @@
+import { describe, expect, test } from 'vitest';
+import { renderHook, act } from '@testing-library/react';
+import { useAdjustPageScaleParameters, defaultParameters, PageSize, AdjustPageScaleParametersHook } from './useAdjustPageScaleParameters';
+
+describe('useAdjustPageScaleParameters', () => {
+ test('should initialize with default parameters', () => {
+ const { result } = renderHook(() => useAdjustPageScaleParameters());
+
+ expect(result.current.parameters).toStrictEqual(defaultParameters);
+ expect(result.current.parameters.scaleFactor).toBe(1.0);
+ expect(result.current.parameters.pageSize).toBe(PageSize.KEEP);
+ });
+
+ test.each([
+ { paramName: 'scaleFactor' as const, value: 0.5 },
+ { paramName: 'scaleFactor' as const, value: 2.0 },
+ { paramName: 'scaleFactor' as const, value: 10.0 },
+ { paramName: 'pageSize' as const, value: PageSize.A4 },
+ { paramName: 'pageSize' as const, value: PageSize.LETTER },
+ { paramName: 'pageSize' as const, value: PageSize.LEGAL },
+ ])('should update parameter $paramName to $value', ({ paramName, value }) => {
+ const { result } = renderHook(() => useAdjustPageScaleParameters());
+
+ act(() => {
+ result.current.updateParameter(paramName, value);
+ });
+
+ expect(result.current.parameters[paramName]).toBe(value);
+ });
+
+ test('should reset parameters to defaults', () => {
+ const { result } = renderHook(() => useAdjustPageScaleParameters());
+
+ // First, change some parameters
+ act(() => {
+ result.current.updateParameter('scaleFactor', 2.5);
+ result.current.updateParameter('pageSize', PageSize.A3);
+ });
+
+ expect(result.current.parameters.scaleFactor).toBe(2.5);
+ expect(result.current.parameters.pageSize).toBe(PageSize.A3);
+
+ // Then reset
+ act(() => {
+ result.current.resetParameters();
+ });
+
+ expect(result.current.parameters).toStrictEqual(defaultParameters);
+ });
+
+ test('should return correct endpoint name', () => {
+ const { result } = renderHook(() => useAdjustPageScaleParameters());
+
+ expect(result.current.getEndpointName()).toBe('scale-pages');
+ });
+
+ test.each([
+ {
+ description: 'with default parameters',
+ setup: () => {},
+ expected: true
+ },
+ {
+ description: 'with valid scale factor 0.1',
+ setup: (hook: AdjustPageScaleParametersHook) => {
+ hook.updateParameter('scaleFactor', 0.1);
+ },
+ expected: true
+ },
+ {
+ description: 'with valid scale factor 10.0',
+ setup: (hook: AdjustPageScaleParametersHook) => {
+ hook.updateParameter('scaleFactor', 10.0);
+ },
+ expected: true
+ },
+ {
+ description: 'with A4 page size',
+ setup: (hook: AdjustPageScaleParametersHook) => {
+ hook.updateParameter('pageSize', PageSize.A4);
+ },
+ expected: true
+ },
+ {
+ description: 'with invalid scale factor 0',
+ setup: (hook: AdjustPageScaleParametersHook) => {
+ hook.updateParameter('scaleFactor', 0);
+ },
+ expected: false
+ },
+ {
+ description: 'with negative scale factor',
+ setup: (hook: AdjustPageScaleParametersHook) => {
+ hook.updateParameter('scaleFactor', -0.5);
+ },
+ expected: false
+ }
+ ])('should validate parameters correctly $description', ({ setup, expected }) => {
+ const { result } = renderHook(() => useAdjustPageScaleParameters());
+
+ act(() => {
+ setup(result.current);
+ });
+
+ expect(result.current.validateParameters()).toBe(expected);
+ });
+
+ test('should handle all PageSize enum values', () => {
+ const { result } = renderHook(() => useAdjustPageScaleParameters());
+
+ Object.values(PageSize).forEach(pageSize => {
+ act(() => {
+ result.current.updateParameter('pageSize', pageSize);
+ });
+
+ expect(result.current.parameters.pageSize).toBe(pageSize);
+ expect(result.current.validateParameters()).toBe(true);
+ });
+ });
+
+ test('should handle scale factor edge cases', () => {
+ const { result } = renderHook(() => useAdjustPageScaleParameters());
+
+ // Test very small valid scale factor
+ act(() => {
+ result.current.updateParameter('scaleFactor', 0.01);
+ });
+ expect(result.current.validateParameters()).toBe(true);
+
+ // Test scale factor just above zero
+ act(() => {
+ result.current.updateParameter('scaleFactor', 0.001);
+ });
+ expect(result.current.validateParameters()).toBe(true);
+
+ // Test exactly zero (invalid)
+ act(() => {
+ result.current.updateParameter('scaleFactor', 0);
+ });
+ expect(result.current.validateParameters()).toBe(false);
+ });
+});
diff --git a/frontend/src/hooks/tools/adjustPageScale/useAdjustPageScaleParameters.ts b/frontend/src/hooks/tools/adjustPageScale/useAdjustPageScaleParameters.ts
new file mode 100644
index 000000000..108d7d3ea
--- /dev/null
+++ b/frontend/src/hooks/tools/adjustPageScale/useAdjustPageScaleParameters.ts
@@ -0,0 +1,37 @@
+import { BaseParameters } from '../../../types/parameters';
+import { useBaseParameters, BaseParametersHook } from '../shared/useBaseParameters';
+
+export enum PageSize {
+ KEEP = 'KEEP',
+ A0 = 'A0',
+ A1 = 'A1',
+ A2 = 'A2',
+ A3 = 'A3',
+ A4 = 'A4',
+ A5 = 'A5',
+ A6 = 'A6',
+ LETTER = 'LETTER',
+ LEGAL = 'LEGAL'
+}
+
+export interface AdjustPageScaleParameters extends BaseParameters {
+ scaleFactor: number;
+ pageSize: PageSize;
+}
+
+export const defaultParameters: AdjustPageScaleParameters = {
+ scaleFactor: 1.0,
+ pageSize: PageSize.KEEP,
+};
+
+export type AdjustPageScaleParametersHook = BaseParametersHook;
+
+export const useAdjustPageScaleParameters = (): AdjustPageScaleParametersHook => {
+ return useBaseParameters({
+ defaultParameters,
+ endpointName: 'scale-pages',
+ validateFn: (params) => {
+ return params.scaleFactor > 0;
+ },
+ });
+};
diff --git a/frontend/src/tools/AdjustPageScale.tsx b/frontend/src/tools/AdjustPageScale.tsx
new file mode 100644
index 000000000..1ae862e6a
--- /dev/null
+++ b/frontend/src/tools/AdjustPageScale.tsx
@@ -0,0 +1,58 @@
+import { useTranslation } from "react-i18next";
+import { createToolFlow } from "../components/tools/shared/createToolFlow";
+import AdjustPageScaleSettings from "../components/tools/adjustPageScale/AdjustPageScaleSettings";
+import { useAdjustPageScaleParameters } from "../hooks/tools/adjustPageScale/useAdjustPageScaleParameters";
+import { useAdjustPageScaleOperation } from "../hooks/tools/adjustPageScale/useAdjustPageScaleOperation";
+import { useBaseTool } from "../hooks/tools/shared/useBaseTool";
+import { BaseToolProps, ToolComponent } from "../types/tool";
+import { useAdjustPageScaleTips } from "../components/tooltips/useAdjustPageScaleTips";
+
+const AdjustPageScale = (props: BaseToolProps) => {
+ const { t } = useTranslation();
+ const adjustPageScaleTips = useAdjustPageScaleTips();
+
+ const base = useBaseTool(
+ 'adjustPageScale',
+ useAdjustPageScaleParameters,
+ useAdjustPageScaleOperation,
+ props
+ );
+
+ return createToolFlow({
+ files: {
+ selectedFiles: base.selectedFiles,
+ isCollapsed: base.hasResults,
+ },
+ steps: [
+ {
+ title: "Settings",
+ isCollapsed: base.settingsCollapsed,
+ onCollapsedClick: base.settingsCollapsed ? base.handleSettingsReset : undefined,
+ tooltip: adjustPageScaleTips,
+ content: (
+
+ ),
+ },
+ ],
+ executeButton: {
+ text: t("adjustPageScale.submit", "Adjust Page Scale"),
+ 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("adjustPageScale.title", "Page Scale Results"),
+ onFileClick: base.handleThumbnailClick,
+ onUndo: base.handleUndo,
+ },
+ });
+};
+
+export default AdjustPageScale as ToolComponent;