From 756cbc4780319100dba7ea3d213b9fe6fc93c8ff Mon Sep 17 00:00:00 2001 From: EthanHealy01 <80844253+EthanHealy01@users.noreply.github.com> Date: Thu, 18 Sep 2025 09:51:55 +0100 Subject: [PATCH] Feature/v2/remove pages (#4445) # Description of Changes - Addition of the remove pages tool - Addition of the remove blank pages 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 | 84 +++++++++++++++++-- .../public/locales/en-US/translation.json | 84 +++++++++++++++++-- .../removeBlanks/RemoveBlanksSettings.tsx | 75 +++++++++++++++++ .../tools/removePages/RemovePagesSettings.tsx | 39 +++++++++ .../tooltips/useRemoveBlanksTips.ts | 41 +++++++++ .../components/tooltips/useRemovePagesTips.ts | 34 ++++++++ .../src/data/useTranslatedToolRegistry.tsx | 10 ++- .../removeBlanks/useRemoveBlanksOperation.ts | 43 ++++++++++ .../removeBlanks/useRemoveBlanksParameters.ts | 26 ++++++ .../removePages/useRemovePagesOperation.ts | 32 +++++++ .../removePages/useRemovePagesParameters.ts | 21 +++++ frontend/src/tools/RemoveBlanks.tsx | 70 ++++++++++++++++ frontend/src/tools/RemovePages.tsx | 64 ++++++++++++++ frontend/src/utils/pageSelection.ts | 23 +++++ 14 files changed, 626 insertions(+), 20 deletions(-) create mode 100644 frontend/src/components/tools/removeBlanks/RemoveBlanksSettings.tsx create mode 100644 frontend/src/components/tools/removePages/RemovePagesSettings.tsx create mode 100644 frontend/src/components/tooltips/useRemoveBlanksTips.ts create mode 100644 frontend/src/components/tooltips/useRemovePagesTips.ts create mode 100644 frontend/src/hooks/tools/removeBlanks/useRemoveBlanksOperation.ts create mode 100644 frontend/src/hooks/tools/removeBlanks/useRemoveBlanksParameters.ts create mode 100644 frontend/src/hooks/tools/removePages/useRemovePagesOperation.ts create mode 100644 frontend/src/hooks/tools/removePages/useRemovePagesParameters.ts create mode 100644 frontend/src/tools/RemoveBlanks.tsx create mode 100644 frontend/src/tools/RemovePages.tsx create mode 100644 frontend/src/utils/pageSelection.ts diff --git a/frontend/public/locales/en-GB/translation.json b/frontend/public/locales/en-GB/translation.json index 49afabb8c..ccc811781 100644 --- a/frontend/public/locales/en-GB/translation.json +++ b/frontend/public/locales/en-GB/translation.json @@ -1125,15 +1125,46 @@ "removePages": { "tags": "Remove pages,delete pages", "title": "Remove Pages", - "pageNumbers": "Pages to Remove", - "pageNumbersPlaceholder": "e.g. 1,3,5-7", - "pageNumbersHelp": "Enter page numbers separated by commas, or ranges like 1-5. Example: 1,3,5-7", + "pageNumbers": { + "label": "Pages to Remove", + "placeholder": "e.g., 1,3,5-8,10", + "error": "Invalid page number format. Use numbers, ranges (1-5), or mathematical expressions (2n+1)" + }, "filenamePrefix": "pages_removed", "files": { "placeholder": "Select a PDF file in the main view to get started" }, "settings": { - "title": "Page Selection" + "title": "Settings" + }, + "tooltip": { + "header": { + "title": "Remove Pages Settings" + }, + "pageNumbers": { + "title": "Page Selection", + "text": "Specify which pages to remove from your PDF. You can select individual pages, ranges, or use mathematical expressions.", + "bullet1": "Individual pages: 1,3,5 (removes pages 1, 3, and 5)", + "bullet2": "Page ranges: 1-5,10-15 (removes pages 1-5 and 10-15)", + "bullet3": "Mathematical: 2n+1 (removes odd pages)", + "bullet4": "Open ranges: 5- (removes from page 5 to end)" + }, + "examples": { + "title": "Common Examples", + "text": "Here are some common page selection patterns:", + "bullet1": "Remove first page: 1", + "bullet2": "Remove last 3 pages: -3", + "bullet3": "Remove every other page: 2n", + "bullet4": "Remove specific scattered pages: 1,5,10,15" + }, + "safety": { + "title": "Safety Tips", + "text": "Important considerations when removing pages:", + "bullet1": "Always preview your selection before processing", + "bullet2": "Keep a backup of your original file", + "bullet3": "Page numbers start from 1, not 0", + "bullet4": "Invalid page numbers will be ignored" + } }, "error": { "failed": "An error occurred whilst removing pages." @@ -1492,11 +1523,46 @@ "tags": "cleanup,streamline,non-content,organize", "title": "Remove Blanks", "header": "Remove Blank Pages", - "threshold": "Pixel Whiteness Threshold:", - "thresholdDesc": "Threshold for determining how white a white pixel must be to be classed as 'White'. 0 = Black, 255 pure white.", - "whitePercent": "White Percent (%):", - "whitePercentDesc": "Percent of page that must be 'white' pixels to be removed", - "submit": "Remove Blanks" + "settings": { + "title": "Settings" + }, + "threshold": { + "label": "Pixel Whiteness Threshold" + }, + "whitePercent": { + "label": "White Percentage Threshold", + "unit": "%" + }, + "includeBlankPages": { + "label": "Include detected blank pages" + }, + "tooltip": { + "header": { + "title": "Remove Blank Pages Settings" + }, + "threshold": { + "title": "Pixel Whiteness Threshold", + "text": "Controls how white a pixel must be to be considered 'white'. This helps determine what counts as a blank area on the page.", + "bullet1": "0 = Pure black (most restrictive)", + "bullet2": "128 = Medium grey", + "bullet3": "255 = Pure white (least restrictive)" + }, + "whitePercent": { + "title": "White Percentage Threshold", + "text": "Sets the minimum percentage of white pixels required for a page to be considered blank and removed.", + "bullet1": "Lower values (e.g., 80%) = More pages removed", + "bullet2": "Higher values (e.g., 95%) = Only very blank pages removed", + "bullet3": "Use higher values for documents with light backgrounds" + }, + "includeBlankPages": { + "title": "Include Detected Blank Pages", + "text": "When enabled, creates a separate PDF containing all the blank pages that were detected and removed from the original document.", + "bullet1": "Useful for reviewing what was removed", + "bullet2": "Helps verify the detection accuracy", + "bullet3": "Can be disabled to reduce output file size" + } + }, + "submit": "Remove blank pages" }, "removeAnnotations": { "tags": "comments,highlight,notes,markup,remove", diff --git a/frontend/public/locales/en-US/translation.json b/frontend/public/locales/en-US/translation.json index 4b93181b8..359b27160 100644 --- a/frontend/public/locales/en-US/translation.json +++ b/frontend/public/locales/en-US/translation.json @@ -745,15 +745,46 @@ "removePages": { "tags": "Remove pages,delete pages", "title": "Remove Pages", - "pageNumbers": "Pages to Remove", - "pageNumbersPlaceholder": "e.g. 1,3,5-7", - "pageNumbersHelp": "Enter page numbers separated by commas, or ranges like 1-5. Example: 1,3,5-7", + "pageNumbers": { + "label": "Pages to Remove", + "placeholder": "e.g., 1,3,5-8,10", + "error": "Invalid page number format. Use numbers, ranges (1-5), or mathematical expressions (2n+1)" + }, "filenamePrefix": "pages_removed", "files": { "placeholder": "Select a PDF file in the main view to get started" }, "settings": { - "title": "Page Selection" + "title": "Settings" + }, + "tooltip": { + "header": { + "title": "Remove Pages Settings" + }, + "pageNumbers": { + "title": "Page Selection", + "text": "Specify which pages to remove from your PDF. You can select individual pages, ranges, or use mathematical expressions.", + "bullet1": "Individual pages: 1,3,5 (removes pages 1, 3, and 5)", + "bullet2": "Page ranges: 1-5,10-15 (removes pages 1-5 and 10-15)", + "bullet3": "Mathematical: 2n+1 (removes odd pages)", + "bullet4": "Open ranges: 5- (removes from page 5 to end)" + }, + "examples": { + "title": "Common Examples", + "text": "Here are some common page selection patterns:", + "bullet1": "Remove first page: 1", + "bullet2": "Remove last 3 pages: -3", + "bullet3": "Remove every other page: 2n", + "bullet4": "Remove specific scattered pages: 1,5,10,15" + }, + "safety": { + "title": "Safety Tips", + "text": "Important considerations when removing pages:", + "bullet1": "Always preview your selection before processing", + "bullet2": "Keep a backup of your original file", + "bullet3": "Page numbers start from 1, not 0", + "bullet4": "Invalid page numbers will be ignored" + } }, "error": { "failed": "An error occurred while removing pages." @@ -1013,11 +1044,46 @@ "tags": "cleanup,streamline,non-content,organize", "title": "Remove Blanks", "header": "Remove Blank Pages", - "threshold": "Pixel Whiteness Threshold:", - "thresholdDesc": "Threshold for determining how white a white pixel must be to be classed as 'White'. 0 = Black, 255 pure white.", - "whitePercent": "White Percent (%):", - "whitePercentDesc": "Percent of page that must be 'white' pixels to be removed", - "submit": "Remove Blanks" + "settings": { + "title": "Settings" + }, + "threshold": { + "label": "Pixel Whiteness Threshold" + }, + "whitePercent": { + "label": "White Percentage Threshold", + "unit": "%" + }, + "includeBlankPages": { + "label": "Include detected blank pages" + }, + "tooltip": { + "header": { + "title": "Remove Blank Pages Settings" + }, + "threshold": { + "title": "Pixel Whiteness Threshold", + "text": "Controls how white a pixel must be to be considered 'white'. This helps determine what counts as a blank area on the page.", + "bullet1": "0 = Pure black (most restrictive)", + "bullet2": "128 = Medium gray", + "bullet3": "255 = Pure white (least restrictive)" + }, + "whitePercent": { + "title": "White Percentage Threshold", + "text": "Sets the minimum percentage of white pixels required for a page to be considered blank and removed.", + "bullet1": "Lower values (e.g., 80%) = More pages removed", + "bullet2": "Higher values (e.g., 95%) = Only very blank pages removed", + "bullet3": "Use higher values for documents with light backgrounds" + }, + "includeBlankPages": { + "title": "Include Detected Blank Pages", + "text": "When enabled, creates a separate PDF containing all the blank pages that were detected and removed from the original document.", + "bullet1": "Useful for reviewing what was removed", + "bullet2": "Helps verify the detection accuracy", + "bullet3": "Can be disabled to reduce output file size" + } + }, + "submit": "Remove blank pages" }, "removeAnnotations": { "tags": "comments,highlight,notes,markup,remove", diff --git a/frontend/src/components/tools/removeBlanks/RemoveBlanksSettings.tsx b/frontend/src/components/tools/removeBlanks/RemoveBlanksSettings.tsx new file mode 100644 index 000000000..fa4ec6e55 --- /dev/null +++ b/frontend/src/components/tools/removeBlanks/RemoveBlanksSettings.tsx @@ -0,0 +1,75 @@ +import { Stack, Text, Checkbox, Slider, NumberInput, Group } from "@mantine/core"; +import { useTranslation } from "react-i18next"; +import NumberInputWithUnit from "../shared/NumberInputWithUnit"; +import { RemoveBlanksParameters } from "../../../hooks/tools/removeBlanks/useRemoveBlanksParameters"; + +interface RemoveBlanksSettingsProps { + parameters: RemoveBlanksParameters; + onParameterChange: (key: K, value: RemoveBlanksParameters[K]) => void; + disabled?: boolean; +} + +const RemoveBlanksSettings = ({ parameters, onParameterChange, disabled = false }: RemoveBlanksSettingsProps) => { + const { t } = useTranslation(); + + return ( + + + onParameterChange('threshold', typeof v === 'string' ? Number(v) : v)} + unit='' + min={0} + max={255} + disabled={disabled} + /> + + + + + {t('removeBlanks.whitePercent.label', 'White Percent')} + + + onParameterChange('whitePercent', typeof v === 'number' ? v : 0.1)} + min={0.1} + max={100} + step={0.1} + size="sm" + rightSection="%" + style={{ width: '80px' }} + disabled={disabled} + /> + onParameterChange('whitePercent', value)} + min={0.1} + max={100} + step={0.1} + style={{ flex: 1 }} + disabled={disabled} + /> + + + + + onParameterChange('includeBlankPages', event.currentTarget.checked)} + disabled={disabled} + label={ +
+ {t('removeBlanks.includeBlankPages.label', 'Include detected blank pages')} +
+ } + /> +
+
+ ); +}; + +export default RemoveBlanksSettings; + + diff --git a/frontend/src/components/tools/removePages/RemovePagesSettings.tsx b/frontend/src/components/tools/removePages/RemovePagesSettings.tsx new file mode 100644 index 000000000..99856e29c --- /dev/null +++ b/frontend/src/components/tools/removePages/RemovePagesSettings.tsx @@ -0,0 +1,39 @@ +import { Stack, TextInput } from "@mantine/core"; +import { useTranslation } from "react-i18next"; +import { RemovePagesParameters } from "../../../hooks/tools/removePages/useRemovePagesParameters"; +import { validatePageNumbers } from "../../../utils/pageSelection"; + +interface RemovePagesSettingsProps { + parameters: RemovePagesParameters; + onParameterChange: (key: K, value: RemovePagesParameters[K]) => void; + disabled?: boolean; +} + +const RemovePagesSettings = ({ parameters, onParameterChange, disabled = false }: RemovePagesSettingsProps) => { + const { t } = useTranslation(); + + const handlePageNumbersChange = (value: string) => { + // Allow user to type naturally - don't normalize input in real-time + onParameterChange('pageNumbers', value); + }; + + // Check if current input is valid + const isValid = validatePageNumbers(parameters.pageNumbers); + const hasValue = parameters.pageNumbers.trim().length > 0; + + return ( + + handlePageNumbersChange(event.currentTarget.value)} + placeholder={t('removePages.pageNumbers.placeholder', 'e.g., 1,3,5-8,10')} + disabled={disabled} + required + error={hasValue && !isValid ? t('removePages.pageNumbers.error', 'Invalid page number format. Use numbers, ranges (1-5), or mathematical expressions (2n+1)') : undefined} + /> + + ); +}; + +export default RemovePagesSettings; diff --git a/frontend/src/components/tooltips/useRemoveBlanksTips.ts b/frontend/src/components/tooltips/useRemoveBlanksTips.ts new file mode 100644 index 000000000..a011bd4ee --- /dev/null +++ b/frontend/src/components/tooltips/useRemoveBlanksTips.ts @@ -0,0 +1,41 @@ +import { useTranslation } from 'react-i18next'; +import { TooltipContent } from '../../types/tips'; + +export const useRemoveBlanksTips = (): TooltipContent => { + const { t } = useTranslation(); + + return { + header: { + title: t("removeBlanks.tooltip.header.title", "Remove Blank Pages Settings"), + }, + tips: [ + { + title: t("removeBlanks.tooltip.threshold.title", "Pixel Whiteness Threshold"), + description: t("removeBlanks.tooltip.threshold.text", "Controls how white a pixel must be to be considered 'white'. This helps determine what counts as a blank area on the page."), + bullets: [ + t("removeBlanks.tooltip.threshold.bullet1", "0 = Pure black (most restrictive)"), + t("removeBlanks.tooltip.threshold.bullet2", "128 = Medium gray"), + t("removeBlanks.tooltip.threshold.bullet3", "255 = Pure white (least restrictive)") + ] + }, + { + title: t("removeBlanks.tooltip.whitePercent.title", "White Percentage Threshold"), + description: t("removeBlanks.tooltip.whitePercent.text", "Sets the minimum percentage of white pixels required for a page to be considered blank and removed."), + bullets: [ + t("removeBlanks.tooltip.whitePercent.bullet1", "Lower values (e.g., 80%) = More pages removed"), + t("removeBlanks.tooltip.whitePercent.bullet2", "Higher values (e.g., 95%) = Only very blank pages removed"), + t("removeBlanks.tooltip.whitePercent.bullet3", "Use higher values for documents with light backgrounds") + ] + }, + { + title: t("removeBlanks.tooltip.includeBlankPages.title", "Include Detected Blank Pages"), + description: t("removeBlanks.tooltip.includeBlankPages.text", "When enabled, creates a separate PDF containing all the blank pages that were detected and removed from the original document."), + bullets: [ + t("removeBlanks.tooltip.includeBlankPages.bullet1", "Useful for reviewing what was removed"), + t("removeBlanks.tooltip.includeBlankPages.bullet2", "Helps verify the detection accuracy"), + t("removeBlanks.tooltip.includeBlankPages.bullet3", "Can be disabled to reduce output file size") + ] + } + ] + }; +}; diff --git a/frontend/src/components/tooltips/useRemovePagesTips.ts b/frontend/src/components/tooltips/useRemovePagesTips.ts new file mode 100644 index 000000000..1da2a1ad1 --- /dev/null +++ b/frontend/src/components/tooltips/useRemovePagesTips.ts @@ -0,0 +1,34 @@ +import { useTranslation } from 'react-i18next'; +import { TooltipContent } from '../../types/tips'; + +export const useRemovePagesTips = (): TooltipContent => { + const { t } = useTranslation(); + + return { + header: { + title: t("removePages.tooltip.header.title", "Remove Pages Settings"), + }, + tips: [ + { + title: t("removePages.tooltip.pageNumbers.title", "Page Selection"), + description: t("removePages.tooltip.pageNumbers.text", "Specify which pages to remove from your PDF. You can select individual pages, ranges, or use mathematical expressions."), + bullets: [ + t("removePages.tooltip.pageNumbers.bullet1", "Individual pages: 1,3,5 (removes pages 1, 3, and 5)"), + t("removePages.tooltip.pageNumbers.bullet2", "Page ranges: 1-5,10-15 (removes pages 1-5 and 10-15)"), + t("removePages.tooltip.pageNumbers.bullet3", "Mathematical: 2n+1 (removes odd pages)"), + t("removePages.tooltip.pageNumbers.bullet4", "Open ranges: 5- (removes from page 5 to end)") + ] + }, + { + title: t("removePages.tooltip.examples.title", "Common Examples"), + description: t("removePages.tooltip.examples.text", "Here are some common page selection patterns:"), + bullets: [ + t("removePages.tooltip.examples.bullet1", "Remove first page: 1"), + t("removePages.tooltip.examples.bullet2", "Remove last 3 pages: -3"), + t("removePages.tooltip.examples.bullet3", "Remove every other page: 2n"), + t("removePages.tooltip.examples.bullet4", "Remove specific scattered pages: 1,5,10,15") + ] + } + ] + }; +}; diff --git a/frontend/src/data/useTranslatedToolRegistry.tsx b/frontend/src/data/useTranslatedToolRegistry.tsx index f3050ea01..3e5612eb0 100644 --- a/frontend/src/data/useTranslatedToolRegistry.tsx +++ b/frontend/src/data/useTranslatedToolRegistry.tsx @@ -8,6 +8,8 @@ import ConvertPanel from "../tools/Convert"; import Sanitize from "../tools/Sanitize"; import AddPassword from "../tools/AddPassword"; import ChangePermissions from "../tools/ChangePermissions"; +import RemoveBlanks from "../tools/RemoveBlanks"; +import RemovePages from "../tools/RemovePages"; import RemovePassword from "../tools/RemovePassword"; import { SubcategoryId, ToolCategoryId, ToolRegistry } from "./toolsTaxonomy"; import AddWatermark from "../tools/AddWatermark"; @@ -414,18 +416,22 @@ export function useFlatToolRegistry(): ToolRegistry { removePages: { icon: , name: t("home.removePages.title", "Remove Pages"), - component: null, + component: RemovePages, description: t("home.removePages.desc", "Remove specific pages from a PDF document"), categoryId: ToolCategoryId.STANDARD_TOOLS, subcategoryId: SubcategoryId.REMOVAL, + maxFiles: 1, + endpoints: ["remove-pages"], }, "remove-blank-pages": { icon: , name: t("home.removeBlanks.title", "Remove Blank Pages"), - component: null, + component: RemoveBlanks, description: t("home.removeBlanks.desc", "Remove blank pages from PDF documents"), categoryId: ToolCategoryId.STANDARD_TOOLS, subcategoryId: SubcategoryId.REMOVAL, + maxFiles: 1, + endpoints: ["remove-blanks"], }, "remove-annotations": { icon: , diff --git a/frontend/src/hooks/tools/removeBlanks/useRemoveBlanksOperation.ts b/frontend/src/hooks/tools/removeBlanks/useRemoveBlanksOperation.ts new file mode 100644 index 000000000..479132d6b --- /dev/null +++ b/frontend/src/hooks/tools/removeBlanks/useRemoveBlanksOperation.ts @@ -0,0 +1,43 @@ +import { useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; +import { ToolType, useToolOperation, ToolOperationConfig } from '../shared/useToolOperation'; +import { createStandardErrorHandler } from '../../../utils/toolErrorHandler'; +import { RemoveBlanksParameters, defaultParameters } from './useRemoveBlanksParameters'; +import { useToolResources } from '../shared/useToolResources'; + +export const buildRemoveBlanksFormData = (parameters: RemoveBlanksParameters, file: File): FormData => { + const formData = new FormData(); + formData.append('fileInput', file); + formData.append('threshold', String(parameters.threshold)); + formData.append('whitePercent', String(parameters.whitePercent)); + // Note: includeBlankPages is not sent to backend as it always returns both files in a ZIP + return formData; +}; + +export const removeBlanksOperationConfig = { + toolType: ToolType.singleFile, + buildFormData: buildRemoveBlanksFormData, + operationType: 'remove-blanks', + endpoint: '/api/v1/misc/remove-blanks', + defaultParameters, +} as const satisfies ToolOperationConfig; + +export const useRemoveBlanksOperation = () => { + const { t } = useTranslation(); + const { extractZipFiles } = useToolResources(); + + const responseHandler = useCallback(async (blob: Blob): Promise => { + // Backend always returns a ZIP file containing the processed PDFs + return await extractZipFiles(blob); + }, [extractZipFiles]); + + return useToolOperation({ + ...removeBlanksOperationConfig, + responseHandler, + getErrorMessage: createStandardErrorHandler( + t('removeBlanks.error.failed', 'Failed to remove blank pages') + ) + }); +}; + + diff --git a/frontend/src/hooks/tools/removeBlanks/useRemoveBlanksParameters.ts b/frontend/src/hooks/tools/removeBlanks/useRemoveBlanksParameters.ts new file mode 100644 index 000000000..b6716c681 --- /dev/null +++ b/frontend/src/hooks/tools/removeBlanks/useRemoveBlanksParameters.ts @@ -0,0 +1,26 @@ +import { BaseParameters } from '../../../types/parameters'; +import { useBaseParameters, BaseParametersHook } from '../shared/useBaseParameters'; + +export interface RemoveBlanksParameters extends BaseParameters { + threshold: number; // 0-255 + whitePercent: number; // 0.1-100 + includeBlankPages: boolean; // whether to include detected blank pages in output +} + +export const defaultParameters: RemoveBlanksParameters = { + threshold: 10, + whitePercent: 99.9, + includeBlankPages: false, +}; + +export type RemoveBlanksParametersHook = BaseParametersHook; + +export const useRemoveBlanksParameters = (): RemoveBlanksParametersHook => { + return useBaseParameters({ + defaultParameters, + endpointName: 'remove-blanks', + validateFn: (p) => p.threshold >= 0 && p.threshold <= 255 && p.whitePercent > 0 && p.whitePercent <= 100, + }); +}; + + diff --git a/frontend/src/hooks/tools/removePages/useRemovePagesOperation.ts b/frontend/src/hooks/tools/removePages/useRemovePagesOperation.ts new file mode 100644 index 000000000..95296fd07 --- /dev/null +++ b/frontend/src/hooks/tools/removePages/useRemovePagesOperation.ts @@ -0,0 +1,32 @@ +import { useTranslation } from 'react-i18next'; +import { ToolType, useToolOperation, ToolOperationConfig } from '../shared/useToolOperation'; +import { createStandardErrorHandler } from '../../../utils/toolErrorHandler'; +import { RemovePagesParameters, defaultParameters } from './useRemovePagesParameters'; +// import { useToolResources } from '../shared/useToolResources'; + +export const buildRemovePagesFormData = (parameters: RemovePagesParameters, file: File): FormData => { + const formData = new FormData(); + formData.append('fileInput', file); + const cleaned = parameters.pageNumbers.replace(/\s+/g, ''); + formData.append('pageNumbers', cleaned); + return formData; +}; + +export const removePagesOperationConfig = { + toolType: ToolType.singleFile, + buildFormData: buildRemovePagesFormData, + operationType: 'remove-pages', + endpoint: '/api/v1/general/remove-pages', + defaultParameters, +} as const satisfies ToolOperationConfig; + +export const useRemovePagesOperation = () => { + const { t } = useTranslation(); + + return useToolOperation({ + ...removePagesOperationConfig, + getErrorMessage: createStandardErrorHandler( + t('removePages.error.failed', 'Failed to remove pages') + ) + }); +}; diff --git a/frontend/src/hooks/tools/removePages/useRemovePagesParameters.ts b/frontend/src/hooks/tools/removePages/useRemovePagesParameters.ts new file mode 100644 index 000000000..31484f54e --- /dev/null +++ b/frontend/src/hooks/tools/removePages/useRemovePagesParameters.ts @@ -0,0 +1,21 @@ +import { BaseParameters } from '../../../types/parameters'; +import { useBaseParameters, BaseParametersHook } from '../shared/useBaseParameters'; +import { validatePageNumbers } from '../../../utils/pageSelection'; + +export interface RemovePagesParameters extends BaseParameters { + pageNumbers: string; // comma-separated page numbers or ranges (e.g., "1,3,5-8") +} + +export const defaultParameters: RemovePagesParameters = { + pageNumbers: '', +}; + +export type RemovePagesParametersHook = BaseParametersHook; + +export const useRemovePagesParameters = (): RemovePagesParametersHook => { + return useBaseParameters({ + defaultParameters, + endpointName: 'remove-pages', + validateFn: (p) => validatePageNumbers(p.pageNumbers), + }); +}; diff --git a/frontend/src/tools/RemoveBlanks.tsx b/frontend/src/tools/RemoveBlanks.tsx new file mode 100644 index 000000000..d7478bdac --- /dev/null +++ b/frontend/src/tools/RemoveBlanks.tsx @@ -0,0 +1,70 @@ +import { useTranslation } from "react-i18next"; +import { createToolFlow } from "../components/tools/shared/createToolFlow"; +import { BaseToolProps, ToolComponent } from "../types/tool"; +import { useBaseTool } from "../hooks/tools/shared/useBaseTool"; +import { useRemoveBlanksParameters } from "../hooks/tools/removeBlanks/useRemoveBlanksParameters"; +import { useRemoveBlanksOperation } from "../hooks/tools/removeBlanks/useRemoveBlanksOperation"; +import RemoveBlanksSettings from "../components/tools/removeBlanks/RemoveBlanksSettings"; +import { useRemoveBlanksTips } from "../components/tooltips/useRemoveBlanksTips"; + +const RemoveBlanks = (props: BaseToolProps) => { + const { t } = useTranslation(); + const tooltipContent = useRemoveBlanksTips(); + + const base = useBaseTool( + 'remove-blanks', + useRemoveBlanksParameters, + useRemoveBlanksOperation, + props + ); + + const settingsContent = ( + + ); + + const handleSettingsClick = () => { + if (base.hasResults) { + base.handleSettingsReset(); + } + }; + + return createToolFlow({ + files: { + selectedFiles: base.selectedFiles, + isCollapsed: base.hasResults, + }, + steps: [ + { + title: t("removeBlanks.settings.title", "Settings"), + isCollapsed: base.settingsCollapsed, + onCollapsedClick: handleSettingsClick, + content: settingsContent, + tooltip: tooltipContent, + }, + ], + executeButton: { + text: t("removeBlanks.submit", "Remove blank pages"), + loadingText: t("loading"), + onClick: base.handleExecute, + isVisible: !base.hasResults, + disabled: !base.params.validateParameters() || !base.hasFiles || !base.endpointEnabled, + }, + review: { + isVisible: base.hasResults, + operation: base.operation, + title: t("removeBlanks.results.title", "Removed Blank Pages"), + onFileClick: base.handleThumbnailClick, + onUndo: base.handleUndo, + }, + }); +}; + +RemoveBlanks.tool = () => useRemoveBlanksOperation; + +export default RemoveBlanks as ToolComponent; + + diff --git a/frontend/src/tools/RemovePages.tsx b/frontend/src/tools/RemovePages.tsx new file mode 100644 index 000000000..5ae0f6934 --- /dev/null +++ b/frontend/src/tools/RemovePages.tsx @@ -0,0 +1,64 @@ +import React from "react"; +import { useTranslation } from "react-i18next"; +import { createToolFlow } from "../components/tools/shared/createToolFlow"; +import { BaseToolProps, ToolComponent } from "../types/tool"; +import { useBaseTool } from "../hooks/tools/shared/useBaseTool"; +import { useRemovePagesParameters } from "../hooks/tools/removePages/useRemovePagesParameters"; +import { useRemovePagesOperation } from "../hooks/tools/removePages/useRemovePagesOperation"; +import RemovePagesSettings from "../components/tools/removePages/RemovePagesSettings"; +import { useRemovePagesTips } from "../components/tooltips/useRemovePagesTips"; + +const RemovePages = (props: BaseToolProps) => { + const { t } = useTranslation(); + const tooltipContent = useRemovePagesTips(); + + const base = useBaseTool( + 'remove-pages', + useRemovePagesParameters, + useRemovePagesOperation, + props + ); + + + const settingsContent = ( + + ); + + return createToolFlow({ + files: { + selectedFiles: base.selectedFiles, + isCollapsed: base.hasResults, + }, + steps: [ + { + title: t("removePages.settings.title", "Settings"), + isCollapsed: base.settingsCollapsed, + onCollapsedClick: base.settingsCollapsed ? base.handleSettingsReset : undefined, + content: settingsContent, + tooltip: tooltipContent, + }, + ], + executeButton: { + text: t("removePages.submit", "Remove Pages"), + loadingText: t("loading"), + onClick: base.handleExecute, + isVisible: !base.hasResults, + disabled: !base.params.validateParameters() || !base.hasFiles || !base.endpointEnabled, + }, + review: { + isVisible: base.hasResults, + operation: base.operation, + title: t("removePages.results.title", "Pages Removed"), + onFileClick: base.handleThumbnailClick, + onUndo: base.handleUndo, + }, + }); +}; + +RemovePages.tool = () => useRemovePagesOperation; + +export default RemovePages as ToolComponent; diff --git a/frontend/src/utils/pageSelection.ts b/frontend/src/utils/pageSelection.ts new file mode 100644 index 000000000..4909581bb --- /dev/null +++ b/frontend/src/utils/pageSelection.ts @@ -0,0 +1,23 @@ +export const validatePageNumbers = (pageNumbers: string): boolean => { + if (!pageNumbers.trim()) return false; + + // Normalize input for validation: remove spaces around commas and other spaces + const normalized = pageNumbers.replace(/\s*,\s*/g, ',').replace(/\s+/g, ''); + const parts = normalized.split(','); + + // Regular expressions for different page number formats + const allToken = /^all$/i; // Select all pages + const singlePageRegex = /^[1-9]\d*$/; // Single page: positive integers only (no 0) + const rangeRegex = /^[1-9]\d*-(?:[1-9]\d*)?$/; // Range: 1-5 or open range 10- + const mathRegex = /^(?=.*n)[0-9n+\-*/() ]+$/; // Mathematical expressions with n and allowed chars + + return parts.every(part => { + if (!part) return false; + return ( + allToken.test(part) || + singlePageRegex.test(part) || + rangeRegex.test(part) || + mathRegex.test(part) + ); + }); +};