mirror of
https://github.com/Frooodle/Stirling-PDF.git
synced 2025-09-26 17:52:59 +02:00
feat: Add React-based extract-images tool (#4501)
This commit is contained in:
parent
18fa16f08e
commit
9758e871d4
@ -1595,7 +1595,13 @@
|
|||||||
"header": "Extract Images",
|
"header": "Extract Images",
|
||||||
"selectText": "Select image format to convert extracted images to",
|
"selectText": "Select image format to convert extracted images to",
|
||||||
"allowDuplicates": "Save duplicate images",
|
"allowDuplicates": "Save duplicate images",
|
||||||
"submit": "Extract"
|
"submit": "Extract",
|
||||||
|
"settings": {
|
||||||
|
"title": "Settings"
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"failed": "An error occurred while extracting images from the PDF."
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"pdfToPDFA": {
|
"pdfToPDFA": {
|
||||||
"tags": "archive,long-term,standard,conversion,storage,preservation",
|
"tags": "archive,long-term,standard,conversion,storage,preservation",
|
||||||
|
@ -1056,7 +1056,13 @@
|
|||||||
"header": "Extract Images",
|
"header": "Extract Images",
|
||||||
"selectText": "Select image format to convert extracted images to",
|
"selectText": "Select image format to convert extracted images to",
|
||||||
"allowDuplicates": "Save duplicate images",
|
"allowDuplicates": "Save duplicate images",
|
||||||
"submit": "Extract"
|
"submit": "Extract",
|
||||||
|
"settings": {
|
||||||
|
"title": "Settings"
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"failed": "An error occurred while extracting images from the PDF."
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"pdfToPDFA": {
|
"pdfToPDFA": {
|
||||||
"tags": "archive,long-term,standard,conversion,storage,preservation",
|
"tags": "archive,long-term,standard,conversion,storage,preservation",
|
||||||
|
@ -0,0 +1,46 @@
|
|||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { Stack, Select, Checkbox } from '@mantine/core';
|
||||||
|
import { ExtractImagesParameters } from '../../../hooks/tools/extractImages/useExtractImagesParameters';
|
||||||
|
|
||||||
|
interface ExtractImagesSettingsProps {
|
||||||
|
parameters: ExtractImagesParameters;
|
||||||
|
onParameterChange: <K extends keyof ExtractImagesParameters>(key: K, value: ExtractImagesParameters[K]) => void;
|
||||||
|
disabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ExtractImagesSettings = ({
|
||||||
|
parameters,
|
||||||
|
onParameterChange,
|
||||||
|
disabled = false
|
||||||
|
}: ExtractImagesSettingsProps) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack gap="md">
|
||||||
|
<Select
|
||||||
|
label={t('extractImages.selectText', 'Output Format')}
|
||||||
|
value={parameters.format}
|
||||||
|
onChange={(value) => {
|
||||||
|
const allowedFormats = ['png', 'jpg', 'gif'] as const;
|
||||||
|
const format = allowedFormats.includes(value as any) ? (value as typeof allowedFormats[number]) : 'png';
|
||||||
|
onParameterChange('format', format);
|
||||||
|
}}
|
||||||
|
data={[
|
||||||
|
{ value: 'png', label: 'PNG' },
|
||||||
|
{ value: 'jpg', label: 'JPG' },
|
||||||
|
{ value: 'gif', label: 'GIF' },
|
||||||
|
]}
|
||||||
|
disabled={disabled}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Checkbox
|
||||||
|
label={t('extractImages.allowDuplicates', 'Allow Duplicate Images')}
|
||||||
|
checked={parameters.allowDuplicates}
|
||||||
|
onChange={(event) => onParameterChange('allowDuplicates', event.currentTarget.checked)}
|
||||||
|
disabled={disabled}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ExtractImagesSettings;
|
@ -50,6 +50,7 @@ import { redactOperationConfig } from "../hooks/tools/redact/useRedactOperation"
|
|||||||
import { rotateOperationConfig } from "../hooks/tools/rotate/useRotateOperation";
|
import { rotateOperationConfig } from "../hooks/tools/rotate/useRotateOperation";
|
||||||
import { changeMetadataOperationConfig } from "../hooks/tools/changeMetadata/useChangeMetadataOperation";
|
import { changeMetadataOperationConfig } from "../hooks/tools/changeMetadata/useChangeMetadataOperation";
|
||||||
import { cropOperationConfig } from "../hooks/tools/crop/useCropOperation";
|
import { cropOperationConfig } from "../hooks/tools/crop/useCropOperation";
|
||||||
|
import { extractImagesOperationConfig } from "../hooks/tools/extractImages/useExtractImagesOperation";
|
||||||
import { replaceColorOperationConfig } from "../hooks/tools/replaceColor/useReplaceColorOperation";
|
import { replaceColorOperationConfig } from "../hooks/tools/replaceColor/useReplaceColorOperation";
|
||||||
import CompressSettings from "../components/tools/compress/CompressSettings";
|
import CompressSettings from "../components/tools/compress/CompressSettings";
|
||||||
import SplitSettings from "../components/tools/split/SplitSettings";
|
import SplitSettings from "../components/tools/split/SplitSettings";
|
||||||
@ -79,6 +80,8 @@ import AdjustPageScaleSettings from "../components/tools/adjustPageScale/AdjustP
|
|||||||
import ScannerImageSplitSettings from "../components/tools/scannerImageSplit/ScannerImageSplitSettings";
|
import ScannerImageSplitSettings from "../components/tools/scannerImageSplit/ScannerImageSplitSettings";
|
||||||
import ChangeMetadataSingleStep from "../components/tools/changeMetadata/ChangeMetadataSingleStep";
|
import ChangeMetadataSingleStep from "../components/tools/changeMetadata/ChangeMetadataSingleStep";
|
||||||
import CropSettings from "../components/tools/crop/CropSettings";
|
import CropSettings from "../components/tools/crop/CropSettings";
|
||||||
|
import ExtractImages from "../tools/ExtractImages";
|
||||||
|
import ExtractImagesSettings from "../components/tools/extractImages/ExtractImagesSettings";
|
||||||
import ReplaceColorSettings from "../components/tools/replaceColor/ReplaceColorSettings";
|
import ReplaceColorSettings from "../components/tools/replaceColor/ReplaceColorSettings";
|
||||||
|
|
||||||
const showPlaceholderTools = true; // Show all tools; grey out unavailable ones in UI
|
const showPlaceholderTools = true; // Show all tools; grey out unavailable ones in UI
|
||||||
@ -478,12 +481,16 @@ export function useFlatToolRegistry(): ToolRegistry {
|
|||||||
synonyms: getSynonyms(t, "extractPages")
|
synonyms: getSynonyms(t, "extractPages")
|
||||||
},
|
},
|
||||||
extractImages: {
|
extractImages: {
|
||||||
icon: <LocalIcon icon="filter-alt" width="1.5rem" height="1.5rem" />,
|
icon: <LocalIcon icon="photo-library-rounded" width="1.5rem" height="1.5rem" />,
|
||||||
name: t("home.extractImages.title", "Extract Images"),
|
name: t("home.extractImages.title", "Extract Images"),
|
||||||
component: null,
|
component: ExtractImages,
|
||||||
description: t("home.extractImages.desc", "Extract images from PDF documents"),
|
description: t("home.extractImages.desc", "Extract images from PDF documents"),
|
||||||
categoryId: ToolCategoryId.STANDARD_TOOLS,
|
categoryId: ToolCategoryId.STANDARD_TOOLS,
|
||||||
subcategoryId: SubcategoryId.EXTRACTION,
|
subcategoryId: SubcategoryId.EXTRACTION,
|
||||||
|
maxFiles: -1,
|
||||||
|
endpoints: ["extract-images"],
|
||||||
|
operationConfig: extractImagesOperationConfig,
|
||||||
|
settingsComponent: ExtractImagesSettings,
|
||||||
synonyms: getSynonyms(t, "extractImages")
|
synonyms: getSynonyms(t, "extractImages")
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -0,0 +1,51 @@
|
|||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { useToolOperation, ToolType } from '../shared/useToolOperation';
|
||||||
|
import { createStandardErrorHandler } from '../../../utils/toolErrorHandler';
|
||||||
|
import { ExtractImagesParameters, defaultParameters } from './useExtractImagesParameters';
|
||||||
|
import JSZip from 'jszip';
|
||||||
|
|
||||||
|
// Static configuration that can be used by both the hook and automation executor
|
||||||
|
export const buildExtractImagesFormData = (parameters: ExtractImagesParameters, file: File): FormData => {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append("fileInput", file);
|
||||||
|
formData.append("format", parameters.format);
|
||||||
|
formData.append("allowDuplicates", parameters.allowDuplicates.toString());
|
||||||
|
return formData;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Response handler for extract-images which returns a ZIP file
|
||||||
|
const extractImagesResponseHandler = async (responseData: Blob, _originalFiles: File[]): Promise<File[]> => {
|
||||||
|
const zip = new JSZip();
|
||||||
|
const zipContent = await zip.loadAsync(responseData);
|
||||||
|
const extractedFiles: File[] = [];
|
||||||
|
|
||||||
|
for (const [filename, file] of Object.entries(zipContent.files)) {
|
||||||
|
if (!file.dir) {
|
||||||
|
const blob = await file.async('blob');
|
||||||
|
const extractedFile = new File([blob], filename, { type: blob.type });
|
||||||
|
extractedFiles.push(extractedFile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return extractedFiles;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Static configuration object
|
||||||
|
export const extractImagesOperationConfig = {
|
||||||
|
toolType: ToolType.singleFile,
|
||||||
|
buildFormData: buildExtractImagesFormData,
|
||||||
|
operationType: 'extractImages',
|
||||||
|
endpoint: '/api/v1/misc/extract-images',
|
||||||
|
defaultParameters,
|
||||||
|
// Extract-images returns a ZIP file containing multiple image files
|
||||||
|
responseHandler: extractImagesResponseHandler,
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const useExtractImagesOperation = () => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
return useToolOperation<ExtractImagesParameters>({
|
||||||
|
...extractImagesOperationConfig,
|
||||||
|
getErrorMessage: createStandardErrorHandler(t('extractImages.error.failed', 'An error occurred while extracting images from the PDF.'))
|
||||||
|
});
|
||||||
|
};
|
@ -0,0 +1,19 @@
|
|||||||
|
import { useBaseParameters } from '../shared/useBaseParameters';
|
||||||
|
|
||||||
|
export interface ExtractImagesParameters {
|
||||||
|
format: 'png' | 'jpg' | 'gif';
|
||||||
|
allowDuplicates: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const defaultParameters: ExtractImagesParameters = {
|
||||||
|
format: 'png',
|
||||||
|
allowDuplicates: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useExtractImagesParameters = () => {
|
||||||
|
return useBaseParameters<ExtractImagesParameters>({
|
||||||
|
defaultParameters,
|
||||||
|
endpointName: 'extract-images',
|
||||||
|
validateFn: () => true, // All parameters have valid defaults
|
||||||
|
});
|
||||||
|
};
|
55
frontend/src/tools/ExtractImages.tsx
Normal file
55
frontend/src/tools/ExtractImages.tsx
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { createToolFlow } from "../components/tools/shared/createToolFlow";
|
||||||
|
import ExtractImagesSettings from "../components/tools/extractImages/ExtractImagesSettings";
|
||||||
|
import { useExtractImagesParameters } from "../hooks/tools/extractImages/useExtractImagesParameters";
|
||||||
|
import { useExtractImagesOperation } from "../hooks/tools/extractImages/useExtractImagesOperation";
|
||||||
|
import { useBaseTool } from "../hooks/tools/shared/useBaseTool";
|
||||||
|
import { BaseToolProps, ToolComponent } from "../types/tool";
|
||||||
|
|
||||||
|
const ExtractImages = (props: BaseToolProps) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const base = useBaseTool(
|
||||||
|
'extractImages',
|
||||||
|
useExtractImagesParameters,
|
||||||
|
useExtractImagesOperation,
|
||||||
|
props
|
||||||
|
);
|
||||||
|
|
||||||
|
return createToolFlow({
|
||||||
|
files: {
|
||||||
|
selectedFiles: base.selectedFiles,
|
||||||
|
isCollapsed: base.hasResults,
|
||||||
|
},
|
||||||
|
steps: [
|
||||||
|
{
|
||||||
|
title: t("extractImages.settings.title", "Settings"),
|
||||||
|
isCollapsed: base.settingsCollapsed,
|
||||||
|
onCollapsedClick: base.settingsCollapsed ? base.handleSettingsReset : undefined,
|
||||||
|
content: (
|
||||||
|
<ExtractImagesSettings
|
||||||
|
parameters={base.params.parameters}
|
||||||
|
onParameterChange={base.params.updateParameter}
|
||||||
|
disabled={base.endpointLoading}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
executeButton: {
|
||||||
|
text: t("extractImages.submit", "Extract Images"),
|
||||||
|
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("extractImages.title", "Extracted Images"),
|
||||||
|
onFileClick: base.handleThumbnailClick,
|
||||||
|
onUndo: base.handleUndo,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ExtractImages as ToolComponent;
|
Loading…
Reference in New Issue
Block a user