diff --git a/frontend/src/core/components/pageEditor/bulkSelectionPanel/AdvancedSelectionPanel.tsx b/frontend/src/core/components/pageEditor/bulkSelectionPanel/AdvancedSelectionPanel.tsx index c8d170dde..365b8b0df 100644 --- a/frontend/src/core/components/pageEditor/bulkSelectionPanel/AdvancedSelectionPanel.tsx +++ b/frontend/src/core/components/pageEditor/bulkSelectionPanel/AdvancedSelectionPanel.tsx @@ -10,7 +10,7 @@ import { everyNthExpression, rangeExpression, LogicalOperator, -} from '@app/components/pageEditor/bulkSelectionPanel/BulkSelection'; +} from '@app/utils/bulkselection/selectionBuilders'; import SelectPages from '@app/components/pageEditor/bulkSelectionPanel/SelectPages'; import OperatorsSection from '@app/components/pageEditor/bulkSelectionPanel/OperatorsSection'; diff --git a/frontend/src/core/components/pageEditor/bulkSelectionPanel/OperatorsSection.tsx b/frontend/src/core/components/pageEditor/bulkSelectionPanel/OperatorsSection.tsx index 49812da61..eaaa2af08 100644 --- a/frontend/src/core/components/pageEditor/bulkSelectionPanel/OperatorsSection.tsx +++ b/frontend/src/core/components/pageEditor/bulkSelectionPanel/OperatorsSection.tsx @@ -1,7 +1,7 @@ import { Button, Text, Group, Divider } from '@mantine/core'; import { useTranslation } from 'react-i18next'; import classes from '@app/components/pageEditor/bulkSelectionPanel/BulkSelectionPanel.module.css'; -import { LogicalOperator } from '@app/components/pageEditor/bulkSelectionPanel/BulkSelection'; +import { LogicalOperator } from '@app/utils/bulkselection/selectionBuilders'; interface OperatorsSectionProps { csvInput: string; diff --git a/frontend/src/core/components/tools/extractPages/ExtractPagesSettings.tsx b/frontend/src/core/components/tools/extractPages/ExtractPagesSettings.tsx new file mode 100644 index 000000000..672c6b175 --- /dev/null +++ b/frontend/src/core/components/tools/extractPages/ExtractPagesSettings.tsx @@ -0,0 +1,34 @@ +import { Stack, TextInput } from "@mantine/core"; +import { useTranslation } from "react-i18next"; +import { ExtractPagesParameters } from "@app/hooks/tools/extractPages/useExtractPagesParameters"; + +interface ExtractPagesSettingsProps { + parameters: ExtractPagesParameters; + onParameterChange: (key: K, value: ExtractPagesParameters[K]) => void; + disabled?: boolean; +} + +const ExtractPagesSettings = ({ parameters, onParameterChange, disabled = false }: ExtractPagesSettingsProps) => { + const { t } = useTranslation(); + + const handleChange = (value: string) => { + onParameterChange('pageNumbers', value); + }; + + return ( + + handleChange(event.currentTarget.value)} + placeholder={t('extractPages.pageNumbers.placeholder', 'e.g., 1,3,5-8 or odd & 1-10')} + disabled={disabled} + required + /> + + ); +}; + +export default ExtractPagesSettings; + + diff --git a/frontend/src/core/data/useTranslatedToolRegistry.tsx b/frontend/src/core/data/useTranslatedToolRegistry.tsx index 0860b5d96..a5c681cd6 100644 --- a/frontend/src/core/data/useTranslatedToolRegistry.tsx +++ b/frontend/src/core/data/useTranslatedToolRegistry.tsx @@ -105,7 +105,10 @@ import AddPageNumbers from "@app/tools/AddPageNumbers"; import RemoveAnnotations from "@app/tools/RemoveAnnotations"; import PageLayoutSettings from "@app/components/tools/pageLayout/PageLayoutSettings"; import ExtractImages from "@app/tools/ExtractImages"; +import ExtractPages from "@app/tools/ExtractPages"; import ExtractImagesSettings from "@app/components/tools/extractImages/ExtractImagesSettings"; +import ExtractPagesSettings from "@app/components/tools/extractPages/ExtractPagesSettings"; +import { extractPagesOperationConfig } from "@app/hooks/tools/extractPages/useExtractPagesOperation"; import ReplaceColorSettings from "@app/components/tools/replaceColor/ReplaceColorSettings"; import AddStampAutomationSettings from "@app/components/tools/addStamp/AddStampAutomationSettings"; import CertSignAutomationSettings from "@app/components/tools/certSign/CertSignAutomationSettings"; @@ -474,12 +477,14 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog { extractPages: { icon: , name: t("home.extractPages.title", "Extract Pages"), - component: null, + component: ExtractPages, description: t("home.extractPages.desc", "Extract specific pages from a PDF document"), categoryId: ToolCategoryId.STANDARD_TOOLS, subcategoryId: SubcategoryId.EXTRACTION, synonyms: getSynonyms(t, "extractPages"), - automationSettings: null, + automationSettings: ExtractPagesSettings, + operationConfig: extractPagesOperationConfig, + endpoints: ["rearrange-pages"], }, extractImages: { icon: , diff --git a/frontend/src/core/hooks/tools/extractPages/useExtractPagesOperation.ts b/frontend/src/core/hooks/tools/extractPages/useExtractPagesOperation.ts new file mode 100644 index 000000000..634215260 --- /dev/null +++ b/frontend/src/core/hooks/tools/extractPages/useExtractPagesOperation.ts @@ -0,0 +1,59 @@ +import apiClient from '@app/services/apiClient'; +import { useTranslation } from 'react-i18next'; +import { ToolType, useToolOperation } from '@app/hooks/tools/shared/useToolOperation'; +import { createStandardErrorHandler } from '@app/utils/toolErrorHandler'; +import { ExtractPagesParameters, defaultParameters } from '@app/hooks/tools/extractPages/useExtractPagesParameters'; +import { pdfWorkerManager } from '@app/services/pdfWorkerManager'; +import { parseSelection } from '@app/utils/bulkselection/parseSelection'; + +// Convert advanced page selection expression into CSV of explicit one-based page numbers +async function resolveSelectionToCsv(expression: string, file: File): Promise { + // Load PDF to determine max pages + const arrayBuffer = await file.arrayBuffer(); + const pdf = await pdfWorkerManager.createDocument(arrayBuffer, { disableAutoFetch: true, disableStream: true }); + try { + const maxPages = (pdf as any).numPages as number; + const pages = parseSelection(expression || '', maxPages); + return pages.join(','); + } finally { + pdfWorkerManager.destroyDocument(pdf); + } +} + +export const extractPagesOperationConfig = { + toolType: ToolType.custom, + operationType: 'extractPages', + customProcessor: async (parameters: ExtractPagesParameters, files: File[]): Promise => { + const outputs: File[] = []; + + for (const file of files) { + // Resolve selection into CSV acceptable by backend + const csv = await resolveSelectionToCsv(parameters.pageNumbers, file); + + const formData = new FormData(); + formData.append('fileInput', file); + formData.append('pageNumbers', csv); + + const response = await apiClient.post('/api/v1/general/rearrange-pages', formData, { responseType: 'blob' }); + + // Name output file with suffix + const base = (file.name || 'document.pdf').replace(/\.[^.]+$/, ''); + const outName = `${base}_extracted_pages.pdf`; + const outFile = new File([response.data], outName, { type: 'application/pdf' }); + outputs.push(outFile); + } + + return outputs; + }, + defaultParameters, +} as const; + +export const useExtractPagesOperation = () => { + const { t } = useTranslation(); + return useToolOperation({ + ...extractPagesOperationConfig, + getErrorMessage: createStandardErrorHandler(t('extractPages.error.failed', 'Failed to extract pages')) + }); +}; + + diff --git a/frontend/src/core/hooks/tools/extractPages/useExtractPagesParameters.ts b/frontend/src/core/hooks/tools/extractPages/useExtractPagesParameters.ts new file mode 100644 index 000000000..8ff220166 --- /dev/null +++ b/frontend/src/core/hooks/tools/extractPages/useExtractPagesParameters.ts @@ -0,0 +1,22 @@ +import { BaseParameters } from '@app/types/parameters'; +import { useBaseParameters, BaseParametersHook } from '@app/hooks/tools/shared/useBaseParameters'; + +export interface ExtractPagesParameters extends BaseParameters { + pageNumbers: string; +} + +export const defaultParameters: ExtractPagesParameters = { + pageNumbers: '', +}; + +export type ExtractPagesParametersHook = BaseParametersHook; + +export const useExtractPagesParameters = (): ExtractPagesParametersHook => { + return useBaseParameters({ + defaultParameters, + endpointName: 'rearrange-pages', + validateFn: (p) => (p.pageNumbers || '').trim().length > 0, + }); +}; + + diff --git a/frontend/src/core/tools/ExtractPages.tsx b/frontend/src/core/tools/ExtractPages.tsx new file mode 100644 index 000000000..81b9d628c --- /dev/null +++ b/frontend/src/core/tools/ExtractPages.tsx @@ -0,0 +1,59 @@ +import { useTranslation } from "react-i18next"; +import { createToolFlow } from "@app/components/tools/shared/createToolFlow"; +import { BaseToolProps, ToolComponent } from "@app/types/tool"; +import { useBaseTool } from "@app/hooks/tools/shared/useBaseTool"; +import { useExtractPagesParameters } from "@app/hooks/tools/extractPages/useExtractPagesParameters"; +import { useExtractPagesOperation } from "@app/hooks/tools/extractPages/useExtractPagesOperation"; +import ExtractPagesSettings from "@app/components/tools/extractPages/ExtractPagesSettings"; + +const ExtractPages = (props: BaseToolProps) => { + const { t } = useTranslation(); + + const base = useBaseTool( + 'extract-pages', + useExtractPagesParameters, + useExtractPagesOperation, + props + ); + + const settingsContent = ( + + ); + + return createToolFlow({ + files: { + selectedFiles: base.selectedFiles, + isCollapsed: base.hasResults, + }, + steps: [ + { + title: t("extractPages.settings.title", "Settings"), + isCollapsed: base.settingsCollapsed, + onCollapsedClick: base.settingsCollapsed ? base.handleSettingsReset : undefined, + content: settingsContent, + }, + ], + executeButton: { + text: t("extractPages.submit", "Extract 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("extractPages.results.title", "Pages Extracted"), + onFileClick: base.handleThumbnailClick, + onUndo: base.handleUndo, + }, + }); +}; + +export default ExtractPages as ToolComponent; + + diff --git a/frontend/src/core/components/pageEditor/bulkSelectionPanel/BulkSelection.ts b/frontend/src/core/utils/bulkselection/selectionBuilders.ts similarity index 98% rename from frontend/src/core/components/pageEditor/bulkSelectionPanel/BulkSelection.ts rename to frontend/src/core/utils/bulkselection/selectionBuilders.ts index d99b03a51..7d8146089 100644 --- a/frontend/src/core/components/pageEditor/bulkSelectionPanel/BulkSelection.ts +++ b/frontend/src/core/utils/bulkselection/selectionBuilders.ts @@ -1,4 +1,4 @@ -// Pure helper utilities for the BulkSelectionPanel UI +// Pure helper utilities for building and manipulating bulk page selection expressions export type LogicalOperator = 'and' | 'or' | 'not' | 'even' | 'odd'; @@ -33,7 +33,7 @@ export function insertOperatorSmart(currentInput: string, op: LogicalOperator): } return `${text} or ${op} `; } - + if (text.length === 0) return `${op} `; // Extract up to the last two operator tokens (words or symbols) from the end