mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2025-09-18 01:19:24 +00:00
Addition of the Remove Pages tool, note: this is different to the Remove Blank Pages tool
This commit is contained in:
parent
fd0aeaaeb8
commit
18b67479a7
@ -0,0 +1,39 @@
|
||||
import { Stack, Text, TextInput } from "@mantine/core";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { RemovePagesParameters } from "../../../hooks/tools/removePages/useRemovePagesParameters";
|
||||
|
||||
interface RemovePagesSettingsProps {
|
||||
parameters: RemovePagesParameters;
|
||||
onParameterChange: <K extends keyof RemovePagesParameters>(key: K, value: RemovePagesParameters[K]) => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
const RemovePagesSettings = ({ parameters, onParameterChange, disabled = false }: RemovePagesSettingsProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const handlePageNumbersChange = (value: string) => {
|
||||
// Remove spaces and normalize input
|
||||
const normalized = value.replace(/\s+/g, '');
|
||||
onParameterChange('pageNumbers', normalized);
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack gap="md">
|
||||
<Stack gap="xs">
|
||||
<TextInput
|
||||
label={t('removePages.pageNumbers.label', 'Pages to Remove')}
|
||||
value={parameters.pageNumbers}
|
||||
onChange={(event) => handlePageNumbersChange(event.currentTarget.value)}
|
||||
placeholder={t('removePages.pageNumbers.placeholder', 'e.g., 1,3,5-8,10')}
|
||||
disabled={disabled}
|
||||
required
|
||||
/>
|
||||
<Text size="xs" c="dimmed">
|
||||
{t('removePages.pageNumbers.desc', 'Enter page numbers or ranges separated by commas. Examples: 1,3,5 or 1-5,10-15')}
|
||||
</Text>
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default RemovePagesSettings;
|
@ -9,6 +9,7 @@ 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";
|
||||
@ -415,10 +416,12 @@ export function useFlatToolRegistry(): ToolRegistry {
|
||||
removePages: {
|
||||
icon: <LocalIcon icon="delete-outline-rounded" width="1.5rem" height="1.5rem" />,
|
||||
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: <LocalIcon icon="scan-delete-rounded" width="1.5rem" height="1.5rem" />,
|
||||
|
@ -0,0 +1,65 @@
|
||||
import { useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ToolType, useToolOperation, ToolOperationConfig } from '../shared/useToolOperation';
|
||||
import { createStandardErrorHandler } from '../../../utils/toolErrorHandler';
|
||||
import { RemovePagesParameters, defaultParameters } from './useRemovePagesParameters';
|
||||
|
||||
export const buildRemovePagesFormData = (parameters: RemovePagesParameters, file: File): FormData => {
|
||||
const formData = new FormData();
|
||||
formData.append('fileInput', file);
|
||||
formData.append('pageNumbers', parameters.pageNumbers);
|
||||
return formData;
|
||||
};
|
||||
|
||||
export const removePagesOperationConfig = {
|
||||
toolType: ToolType.singleFile,
|
||||
buildFormData: buildRemovePagesFormData,
|
||||
operationType: 'remove-pages',
|
||||
endpoint: '/api/v1/general/remove-pages',
|
||||
filePrefix: 'removed_pages_',
|
||||
defaultParameters,
|
||||
} as const satisfies ToolOperationConfig<RemovePagesParameters>;
|
||||
|
||||
export const useRemovePagesOperation = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const responseHandler = useCallback(async (blob: Blob, originalFiles: File[]): Promise<File[]> => {
|
||||
// Try to detect zip vs pdf
|
||||
const headBuf = await blob.slice(0, 4).arrayBuffer();
|
||||
const head = new TextDecoder().decode(new Uint8Array(headBuf));
|
||||
|
||||
// PDF response: return as single file
|
||||
if (head.startsWith('%PDF')) {
|
||||
const base = originalFiles[0]?.name?.replace(/\.[^.]+$/, '') || 'document';
|
||||
return [new File([blob], `removed_pages_${base}.pdf`, { type: 'application/pdf' })];
|
||||
}
|
||||
|
||||
// ZIP: extract PDFs inside
|
||||
if (head.startsWith('PK')) {
|
||||
const { extractZipFiles } = await import('../shared/useToolResources');
|
||||
const files = await extractZipFiles(blob);
|
||||
if (files.length > 0) return files;
|
||||
}
|
||||
|
||||
// Unknown blob type
|
||||
const textBuf = await blob.slice(0, 1024).arrayBuffer();
|
||||
const text = new TextDecoder().decode(new Uint8Array(textBuf));
|
||||
if (/error|exception|html/i.test(text)) {
|
||||
const title =
|
||||
text.match(/<title[^>]*>([^<]+)<\/title>/i)?.[1] ||
|
||||
text.match(/<h1[^>]*>([^<]+)<\/h1>/i)?.[1] ||
|
||||
'Unknown error';
|
||||
throw new Error(`Remove pages service error: ${title}`);
|
||||
}
|
||||
throw new Error('Unexpected response format from remove pages service');
|
||||
}, []);
|
||||
|
||||
return useToolOperation<RemovePagesParameters>({
|
||||
...removePagesOperationConfig,
|
||||
responseHandler,
|
||||
filePrefix: t('removePages.filenamePrefix', 'removed_pages') + '_',
|
||||
getErrorMessage: createStandardErrorHandler(
|
||||
t('removePages.error.failed', 'Failed to remove pages')
|
||||
)
|
||||
});
|
||||
};
|
@ -0,0 +1,20 @@
|
||||
import { BaseParameters } from '../../../types/parameters';
|
||||
import { useBaseParameters, BaseParametersHook } from '../shared/useBaseParameters';
|
||||
|
||||
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<RemovePagesParameters>;
|
||||
|
||||
export const useRemovePagesParameters = (): RemovePagesParametersHook => {
|
||||
return useBaseParameters({
|
||||
defaultParameters,
|
||||
endpointName: 'remove-pages',
|
||||
validateFn: (p) => p.pageNumbers.trim().length > 0,
|
||||
});
|
||||
};
|
61
frontend/src/tools/RemovePages.tsx
Normal file
61
frontend/src/tools/RemovePages.tsx
Normal file
@ -0,0 +1,61 @@
|
||||
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";
|
||||
|
||||
const RemovePages = (props: BaseToolProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const base = useBaseTool(
|
||||
'remove-pages',
|
||||
useRemovePagesParameters,
|
||||
useRemovePagesOperation,
|
||||
props
|
||||
);
|
||||
|
||||
|
||||
const settingsContent = (
|
||||
<RemovePagesSettings
|
||||
parameters={base.params.parameters}
|
||||
onParameterChange={base.params.updateParameter}
|
||||
disabled={base.endpointLoading}
|
||||
/>
|
||||
);
|
||||
|
||||
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,
|
||||
},
|
||||
],
|
||||
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;
|
Loading…
x
Reference in New Issue
Block a user