mirror of
https://github.com/Frooodle/Stirling-PDF.git
synced 2025-09-26 17:52:59 +02:00
Convert extract-image-scans to React component (#4505)
Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
parent
d613a4659e
commit
233b710b78
@ -229,7 +229,7 @@ public class MergeController {
|
|||||||
if (!invalidIndexes.isEmpty()) {
|
if (!invalidIndexes.isEmpty()) {
|
||||||
// Parse client file IDs (always present from frontend)
|
// Parse client file IDs (always present from frontend)
|
||||||
String[] clientIds = parseClientFileIds(request.getClientFileIds());
|
String[] clientIds = parseClientFileIds(request.getClientFileIds());
|
||||||
|
|
||||||
// Map invalid indexes to client IDs
|
// Map invalid indexes to client IDs
|
||||||
List<String> errorFileIds = new ArrayList<>();
|
List<String> errorFileIds = new ArrayList<>();
|
||||||
for (Integer index : invalidIndexes) {
|
for (Integer index : invalidIndexes) {
|
||||||
@ -237,12 +237,12 @@ public class MergeController {
|
|||||||
errorFileIds.add(clientIds[index]);
|
errorFileIds.add(clientIds[index]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
String payload = String.format(
|
String payload =
|
||||||
"{\"errorFileIds\":%s,\"message\":\"Some of the selected files can't be merged\"}",
|
String.format(
|
||||||
errorFileIds.toString()
|
"{\"errorFileIds\":%s,\"message\":\"Some of the selected files can't be merged\"}",
|
||||||
);
|
errorFileIds.toString());
|
||||||
|
|
||||||
return ResponseEntity.status(HttpStatus.UNPROCESSABLE_ENTITY)
|
return ResponseEntity.status(HttpStatus.UNPROCESSABLE_ENTITY)
|
||||||
.header("Content-Type", MediaType.APPLICATION_JSON_VALUE)
|
.header("Content-Type", MediaType.APPLICATION_JSON_VALUE)
|
||||||
.body(payload.getBytes(StandardCharsets.UTF_8));
|
.body(payload.getBytes(StandardCharsets.UTF_8));
|
||||||
|
@ -436,8 +436,8 @@
|
|||||||
},
|
},
|
||||||
"scannerImageSplit": {
|
"scannerImageSplit": {
|
||||||
"tags": "detect,split,photos",
|
"tags": "detect,split,photos",
|
||||||
"title": "Detect/Split Scanned photos",
|
"title": "Detect & Split Scanned Photos",
|
||||||
"desc": "Splits multiple photos from within a photo/PDF"
|
"desc": "Detect and split scanned photos into separate pages"
|
||||||
},
|
},
|
||||||
"sign": {
|
"sign": {
|
||||||
"tags": "signature,autograph",
|
"tags": "signature,autograph",
|
||||||
@ -1656,18 +1656,48 @@
|
|||||||
"tags": "separate,auto-detect,scans,multi-photo,organize",
|
"tags": "separate,auto-detect,scans,multi-photo,organize",
|
||||||
"selectText": {
|
"selectText": {
|
||||||
"1": "Angle Threshold:",
|
"1": "Angle Threshold:",
|
||||||
"2": "Sets the minimum absolute angle required for the image to be rotated (default: 10).",
|
"2": "Tilt (in degrees) needed before we auto-straighten a photo.",
|
||||||
"3": "Tolerance:",
|
"3": "Tolerance:",
|
||||||
"4": "Determines the range of colour variation around the estimated background colour (default: 30).",
|
"4": "How closely a colour must match the page background to count as background. Higher = looser, lower = stricter.",
|
||||||
"5": "Minimum Area:",
|
"5": "Minimum Area:",
|
||||||
"6": "Sets the minimum area threshold for a photo (default: 10000).",
|
"6": "Smallest photo size (in pixels²) we'll keep to avoid tiny fragments.",
|
||||||
"7": "Minimum Contour Area:",
|
"7": "Minimum Contour Area:",
|
||||||
"8": "Sets the minimum contour area threshold for a photo",
|
"8": "Smallest edge/shape we consider when finding photos (filters dust and specks).",
|
||||||
"9": "Border Size:",
|
"9": "Border Size:",
|
||||||
"10": "Sets the size of the border added and removed to prevent white borders in the output (default: 1)."
|
"10": "Extra padding (in pixels) around each saved photo so edges aren't cut."
|
||||||
},
|
},
|
||||||
"info": "Python is not installed. It is required to run."
|
"info": "Python is not installed. It is required to run."
|
||||||
},
|
},
|
||||||
|
"scannerImageSplit": {
|
||||||
|
"title": "Extracted Images",
|
||||||
|
"submit": "Extract Image Scans",
|
||||||
|
"error": {
|
||||||
|
"failed": "An error occurred while extracting image scans."
|
||||||
|
},
|
||||||
|
"tooltip": {
|
||||||
|
"title": "Photo Splitter",
|
||||||
|
"whatThisDoes": "What this does",
|
||||||
|
"whatThisDoesDesc": "Automatically finds and extracts each photo from a scanned page or composite image—no manual cropping.",
|
||||||
|
"whenToUse": "When to use",
|
||||||
|
"useCase1": "Scan whole album pages in one go",
|
||||||
|
"useCase2": "Split flatbed batches into separate files",
|
||||||
|
"useCase3": "Break collages into individual photos",
|
||||||
|
"useCase4": "Pull photos from documents",
|
||||||
|
"quickFixes": "Quick fixes",
|
||||||
|
"problem1": "Photos not detected → increase Tolerance to 30-50",
|
||||||
|
"problem2": "Too many false detections → increase Minimum Area to 15,000-20,000",
|
||||||
|
"problem3": "Crops are too tight → increase Border Size to 5-10",
|
||||||
|
"problem4": "Tilted photos not straightened → lower Angle Threshold to ~5°",
|
||||||
|
"problem5": "Dust/noise boxes → increase Minimum Contour Area to 1000-2000",
|
||||||
|
"setupTips": "Setup tips",
|
||||||
|
"tip1": "Use a plain, light background",
|
||||||
|
"tip2": "Leave a small gap (≈1 cm) between photos",
|
||||||
|
"tip3": "Scan at 300-600 DPI",
|
||||||
|
"tip4": "Clean the scanner glass",
|
||||||
|
"headsUp": "Heads-up",
|
||||||
|
"headsUpDesc": "Overlapping photos or backgrounds very close in colour to the photos can reduce accuracy-try a lighter or darker background and leave more space."
|
||||||
|
}
|
||||||
|
},
|
||||||
"sign": {
|
"sign": {
|
||||||
"title": "Sign",
|
"title": "Sign",
|
||||||
"header": "Sign PDFs",
|
"header": "Sign PDFs",
|
||||||
|
@ -0,0 +1,74 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { NumberInput, Stack } from '@mantine/core';
|
||||||
|
import { ScannerImageSplitParameters } from '../../../hooks/tools/scannerImageSplit/useScannerImageSplitParameters';
|
||||||
|
|
||||||
|
interface ScannerImageSplitSettingsProps {
|
||||||
|
parameters: ScannerImageSplitParameters;
|
||||||
|
onParameterChange: <K extends keyof ScannerImageSplitParameters>(key: K, value: ScannerImageSplitParameters[K]) => void;
|
||||||
|
disabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ScannerImageSplitSettings: React.FC<ScannerImageSplitSettingsProps> = ({
|
||||||
|
parameters,
|
||||||
|
onParameterChange,
|
||||||
|
disabled = false
|
||||||
|
}) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack gap="md">
|
||||||
|
<NumberInput
|
||||||
|
label={t('ScannerImageSplit.selectText.1', 'Angle Threshold:')}
|
||||||
|
description={t('ScannerImageSplit.selectText.2', 'Sets the minimum absolute angle required for the image to be rotated (default: 10).')}
|
||||||
|
value={parameters.angle_threshold}
|
||||||
|
onChange={(value) => onParameterChange('angle_threshold', Number(value) || 10)}
|
||||||
|
min={0}
|
||||||
|
step={1}
|
||||||
|
disabled={disabled}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<NumberInput
|
||||||
|
label={t('ScannerImageSplit.selectText.3', 'Tolerance:')}
|
||||||
|
description={t('ScannerImageSplit.selectText.4', 'Determines the range of colour variation around the estimated background colour (default: 30).')}
|
||||||
|
value={parameters.tolerance}
|
||||||
|
onChange={(value) => onParameterChange('tolerance', Number(value) || 30)}
|
||||||
|
min={0}
|
||||||
|
step={1}
|
||||||
|
disabled={disabled}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<NumberInput
|
||||||
|
label={t('ScannerImageSplit.selectText.5', 'Minimum Area:')}
|
||||||
|
description={t('ScannerImageSplit.selectText.6', 'Sets the minimum area threshold for a photo (default: 10000).')}
|
||||||
|
value={parameters.min_area}
|
||||||
|
onChange={(value) => onParameterChange('min_area', Number(value) || 10000)}
|
||||||
|
min={0}
|
||||||
|
step={100}
|
||||||
|
disabled={disabled}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<NumberInput
|
||||||
|
label={t('ScannerImageSplit.selectText.7', 'Minimum Contour Area:')}
|
||||||
|
description={t('ScannerImageSplit.selectText.8', 'Sets the minimum contour area threshold for a photo.')}
|
||||||
|
value={parameters.min_contour_area}
|
||||||
|
onChange={(value) => onParameterChange('min_contour_area', Number(value) || 500)}
|
||||||
|
min={0}
|
||||||
|
step={10}
|
||||||
|
disabled={disabled}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<NumberInput
|
||||||
|
label={t('ScannerImageSplit.selectText.9', 'Border Size:')}
|
||||||
|
description={t('ScannerImageSplit.selectText.10', 'Sets the size of the border added and removed to prevent white borders in the output (default: 1).')}
|
||||||
|
value={parameters.border_size}
|
||||||
|
onChange={(value) => onParameterChange('border_size', Number(value) || 1)}
|
||||||
|
min={0}
|
||||||
|
step={1}
|
||||||
|
disabled={disabled}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ScannerImageSplitSettings;
|
54
frontend/src/components/tooltips/useScannerImageSplitTips.ts
Normal file
54
frontend/src/components/tooltips/useScannerImageSplitTips.ts
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { TooltipContent } from '../../types/tips';
|
||||||
|
|
||||||
|
export const useScannerImageSplitTips = (): TooltipContent => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
return {
|
||||||
|
header: {
|
||||||
|
title: t('scannerImageSplit.tooltip.title', 'Photo Splitter')
|
||||||
|
},
|
||||||
|
tips: [
|
||||||
|
{
|
||||||
|
title: t('scannerImageSplit.tooltip.whatThisDoes', 'What this does'),
|
||||||
|
description: t('scannerImageSplit.tooltip.whatThisDoesDesc',
|
||||||
|
'Automatically finds and extracts each photo from a scanned page or composite image—no manual cropping.'
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t('scannerImageSplit.tooltip.whenToUse', 'When to use'),
|
||||||
|
bullets: [
|
||||||
|
t('scannerImageSplit.tooltip.useCase1', 'Scan whole album pages in one go'),
|
||||||
|
t('scannerImageSplit.tooltip.useCase2', 'Split flatbed batches into separate files'),
|
||||||
|
t('scannerImageSplit.tooltip.useCase3', 'Break collages into individual photos'),
|
||||||
|
t('scannerImageSplit.tooltip.useCase4', 'Pull photos from documents')
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t('scannerImageSplit.tooltip.quickFixes', 'Quick fixes'),
|
||||||
|
bullets: [
|
||||||
|
t('scannerImageSplit.tooltip.problem1', 'Photos not detected → increase Tolerance to 30–50'),
|
||||||
|
t('scannerImageSplit.tooltip.problem2', 'Too many false detections → increase Minimum Area to 15,000–20,000'),
|
||||||
|
t('scannerImageSplit.tooltip.problem3', 'Crops are too tight → increase Border Size to 5–10'),
|
||||||
|
t('scannerImageSplit.tooltip.problem4', 'Tilted photos not straightened → lower Angle Threshold to ~5°'),
|
||||||
|
t('scannerImageSplit.tooltip.problem5', 'Dust/noise boxes → increase Minimum Contour Area to 1000–2000')
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t('scannerImageSplit.tooltip.setupTips', 'Setup tips'),
|
||||||
|
bullets: [
|
||||||
|
t('scannerImageSplit.tooltip.tip1', 'Use a plain, light background'),
|
||||||
|
t('scannerImageSplit.tooltip.tip2', 'Leave a small gap (≈1 cm) between photos'),
|
||||||
|
t('scannerImageSplit.tooltip.tip3', 'Scan at 300–600 DPI'),
|
||||||
|
t('scannerImageSplit.tooltip.tip4', 'Clean the scanner glass')
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t('scannerImageSplit.tooltip.headsUp', 'Heads-up'),
|
||||||
|
description: t('scannerImageSplit.tooltip.headsUpDesc',
|
||||||
|
'Overlapping photos or backgrounds very close in colour to the photos can reduce accuracy—try a lighter or darker background and leave more space.'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
};
|
@ -68,10 +68,13 @@ import RedactSingleStepSettings from "../components/tools/redact/RedactSingleSte
|
|||||||
import RotateSettings from "../components/tools/rotate/RotateSettings";
|
import RotateSettings from "../components/tools/rotate/RotateSettings";
|
||||||
import Redact from "../tools/Redact";
|
import Redact from "../tools/Redact";
|
||||||
import AdjustPageScale from "../tools/AdjustPageScale";
|
import AdjustPageScale from "../tools/AdjustPageScale";
|
||||||
|
import ScannerImageSplit from "../tools/ScannerImageSplit";
|
||||||
import { ToolId } from "../types/toolId";
|
import { ToolId } from "../types/toolId";
|
||||||
import MergeSettings from '../components/tools/merge/MergeSettings';
|
import MergeSettings from '../components/tools/merge/MergeSettings';
|
||||||
import { adjustPageScaleOperationConfig } from "../hooks/tools/adjustPageScale/useAdjustPageScaleOperation";
|
import { adjustPageScaleOperationConfig } from "../hooks/tools/adjustPageScale/useAdjustPageScaleOperation";
|
||||||
|
import { scannerImageSplitOperationConfig } from "../hooks/tools/scannerImageSplit/useScannerImageSplitOperation";
|
||||||
import AdjustPageScaleSettings from "../components/tools/adjustPageScale/AdjustPageScaleSettings";
|
import AdjustPageScaleSettings from "../components/tools/adjustPageScale/AdjustPageScaleSettings";
|
||||||
|
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";
|
||||||
|
|
||||||
@ -624,10 +627,14 @@ export function useFlatToolRegistry(): ToolRegistry {
|
|||||||
scannerImageSplit: {
|
scannerImageSplit: {
|
||||||
icon: <LocalIcon icon="scanner-rounded" width="1.5rem" height="1.5rem" />,
|
icon: <LocalIcon icon="scanner-rounded" width="1.5rem" height="1.5rem" />,
|
||||||
name: t("home.scannerImageSplit.title", "Detect & Split Scanned Photos"),
|
name: t("home.scannerImageSplit.title", "Detect & Split Scanned Photos"),
|
||||||
component: null,
|
component: ScannerImageSplit,
|
||||||
description: t("home.scannerImageSplit.desc", "Detect and split scanned photos into separate pages"),
|
description: t("home.scannerImageSplit.desc", "Detect and split scanned photos into separate pages"),
|
||||||
categoryId: ToolCategoryId.ADVANCED_TOOLS,
|
categoryId: ToolCategoryId.ADVANCED_TOOLS,
|
||||||
subcategoryId: SubcategoryId.ADVANCED_FORMATTING,
|
subcategoryId: SubcategoryId.ADVANCED_FORMATTING,
|
||||||
|
maxFiles: -1,
|
||||||
|
endpoints: ["extract-image-scans"],
|
||||||
|
operationConfig: scannerImageSplitOperationConfig,
|
||||||
|
settingsComponent: ScannerImageSplitSettings,
|
||||||
synonyms: getSynonyms(t, "ScannerImageSplit"),
|
synonyms: getSynonyms(t, "ScannerImageSplit"),
|
||||||
},
|
},
|
||||||
overlayPdfs: {
|
overlayPdfs: {
|
||||||
|
@ -0,0 +1,54 @@
|
|||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { ToolType, useToolOperation } from '../shared/useToolOperation';
|
||||||
|
import { createStandardErrorHandler } from '../../../utils/toolErrorHandler';
|
||||||
|
import { ScannerImageSplitParameters, defaultParameters } from './useScannerImageSplitParameters';
|
||||||
|
import { zipFileService } from '../../../services/zipFileService';
|
||||||
|
|
||||||
|
export const buildScannerImageSplitFormData = (parameters: ScannerImageSplitParameters, file: File): FormData => {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('fileInput', file);
|
||||||
|
formData.append('angle_threshold', parameters.angle_threshold.toString());
|
||||||
|
formData.append('tolerance', parameters.tolerance.toString());
|
||||||
|
formData.append('min_area', parameters.min_area.toString());
|
||||||
|
formData.append('min_contour_area', parameters.min_contour_area.toString());
|
||||||
|
formData.append('border_size', parameters.border_size.toString());
|
||||||
|
return formData;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Custom response handler to handle ZIP files that might be misidentified
|
||||||
|
const scannerImageSplitResponseHandler = async (responseData: Blob, inputFiles: File[]): Promise<File[]> => {
|
||||||
|
try {
|
||||||
|
// Always try to extract as ZIP first, regardless of content-type
|
||||||
|
const extractionResult = await zipFileService.extractAllFiles(responseData);
|
||||||
|
if (extractionResult.success && extractionResult.extractedFiles.length > 0) {
|
||||||
|
return extractionResult.extractedFiles;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Failed to extract as ZIP, treating as single file:', error);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: treat as single file (PNG image)
|
||||||
|
const inputFileName = inputFiles[0]?.name || 'document';
|
||||||
|
const baseFileName = inputFileName.replace(/\.[^.]+$/, '');
|
||||||
|
const singleFile = new File([responseData], `${baseFileName}.png`, { type: 'image/png' });
|
||||||
|
return [singleFile];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const scannerImageSplitOperationConfig = {
|
||||||
|
toolType: ToolType.singleFile,
|
||||||
|
buildFormData: buildScannerImageSplitFormData,
|
||||||
|
operationType: 'scannerImageSplit',
|
||||||
|
endpoint: '/api/v1/misc/extract-image-scans',
|
||||||
|
multiFileEndpoint: false,
|
||||||
|
responseHandler: scannerImageSplitResponseHandler,
|
||||||
|
defaultParameters,
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const useScannerImageSplitOperation = () => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
return useToolOperation<ScannerImageSplitParameters>({
|
||||||
|
...scannerImageSplitOperationConfig,
|
||||||
|
getErrorMessage: createStandardErrorHandler(t('scannerImageSplit.error.failed', 'An error occurred while extracting image scans.'))
|
||||||
|
});
|
||||||
|
};
|
@ -0,0 +1,31 @@
|
|||||||
|
import { BaseParameters } from '../../../types/parameters';
|
||||||
|
import { useBaseParameters, BaseParametersHook } from '../shared/useBaseParameters';
|
||||||
|
|
||||||
|
export interface ScannerImageSplitParameters extends BaseParameters {
|
||||||
|
angle_threshold: number;
|
||||||
|
tolerance: number;
|
||||||
|
min_area: number;
|
||||||
|
min_contour_area: number;
|
||||||
|
border_size: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const defaultParameters: ScannerImageSplitParameters = {
|
||||||
|
angle_threshold: 10,
|
||||||
|
tolerance: 30,
|
||||||
|
min_area: 10000,
|
||||||
|
min_contour_area: 500,
|
||||||
|
border_size: 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ScannerImageSplitParametersHook = BaseParametersHook<ScannerImageSplitParameters>;
|
||||||
|
|
||||||
|
export const useScannerImageSplitParameters = (): ScannerImageSplitParametersHook => {
|
||||||
|
return useBaseParameters({
|
||||||
|
defaultParameters,
|
||||||
|
endpointName: 'extract-image-scans',
|
||||||
|
validateFn: () => {
|
||||||
|
// All parameters are numeric with defaults, validation handled by form
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
@ -338,6 +338,125 @@ export class ZipFileService {
|
|||||||
return errorMessage.includes('password') || errorMessage.includes('encrypted');
|
return errorMessage.includes('password') || errorMessage.includes('encrypted');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract all files from a ZIP archive (not limited to PDFs)
|
||||||
|
*/
|
||||||
|
async extractAllFiles(
|
||||||
|
file: File | Blob,
|
||||||
|
onProgress?: (progress: ZipExtractionProgress) => void
|
||||||
|
): Promise<ZipExtractionResult> {
|
||||||
|
const result: ZipExtractionResult = {
|
||||||
|
success: false,
|
||||||
|
extractedFiles: [],
|
||||||
|
errors: [],
|
||||||
|
totalFiles: 0,
|
||||||
|
extractedCount: 0
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Load ZIP contents
|
||||||
|
const zip = new JSZip();
|
||||||
|
const zipContents = await zip.loadAsync(file);
|
||||||
|
|
||||||
|
// Get all files (not directories)
|
||||||
|
const allFiles = Object.entries(zipContents.files).filter(([, zipEntry]) =>
|
||||||
|
!zipEntry.dir
|
||||||
|
);
|
||||||
|
|
||||||
|
result.totalFiles = allFiles.length;
|
||||||
|
|
||||||
|
// Extract each file
|
||||||
|
for (let i = 0; i < allFiles.length; i++) {
|
||||||
|
const [filename, zipEntry] = allFiles[i];
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Report progress
|
||||||
|
if (onProgress) {
|
||||||
|
onProgress({
|
||||||
|
currentFile: filename,
|
||||||
|
extractedCount: i,
|
||||||
|
totalFiles: allFiles.length,
|
||||||
|
progress: (i / allFiles.length) * 100
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract file content
|
||||||
|
const content = await zipEntry.async('blob');
|
||||||
|
|
||||||
|
// Create File object with appropriate MIME type
|
||||||
|
const mimeType = this.getMimeTypeFromExtension(filename);
|
||||||
|
const extractedFile = new File([content], filename, { type: mimeType });
|
||||||
|
|
||||||
|
result.extractedFiles.push(extractedFile);
|
||||||
|
result.extractedCount++;
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
||||||
|
result.errors.push(`Failed to extract "${filename}": ${errorMessage}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Final progress report
|
||||||
|
if (onProgress) {
|
||||||
|
onProgress({
|
||||||
|
currentFile: '',
|
||||||
|
extractedCount: result.extractedCount,
|
||||||
|
totalFiles: result.totalFiles,
|
||||||
|
progress: 100
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
result.success = result.extractedFiles.length > 0;
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
||||||
|
result.errors.push(`Failed to process ZIP file: ${errorMessage}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get MIME type based on file extension
|
||||||
|
*/
|
||||||
|
private getMimeTypeFromExtension(fileName: string): string {
|
||||||
|
const ext = fileName.toLowerCase().split('.').pop();
|
||||||
|
|
||||||
|
const mimeTypes: Record<string, string> = {
|
||||||
|
// Images
|
||||||
|
'png': 'image/png',
|
||||||
|
'jpg': 'image/jpeg',
|
||||||
|
'jpeg': 'image/jpeg',
|
||||||
|
'gif': 'image/gif',
|
||||||
|
'webp': 'image/webp',
|
||||||
|
'bmp': 'image/bmp',
|
||||||
|
'svg': 'image/svg+xml',
|
||||||
|
'tiff': 'image/tiff',
|
||||||
|
'tif': 'image/tiff',
|
||||||
|
|
||||||
|
// Documents
|
||||||
|
'pdf': 'application/pdf',
|
||||||
|
'txt': 'text/plain',
|
||||||
|
'html': 'text/html',
|
||||||
|
'css': 'text/css',
|
||||||
|
'js': 'application/javascript',
|
||||||
|
'json': 'application/json',
|
||||||
|
'xml': 'application/xml',
|
||||||
|
|
||||||
|
// Office documents
|
||||||
|
'doc': 'application/msword',
|
||||||
|
'docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||||||
|
'xls': 'application/vnd.ms-excel',
|
||||||
|
'xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||||
|
|
||||||
|
// Archives
|
||||||
|
'zip': 'application/zip',
|
||||||
|
'rar': 'application/x-rar-compressed',
|
||||||
|
};
|
||||||
|
|
||||||
|
return mimeTypes[ext || ''] || 'application/octet-stream';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Export singleton instance
|
// Export singleton instance
|
||||||
|
58
frontend/src/tools/ScannerImageSplit.tsx
Normal file
58
frontend/src/tools/ScannerImageSplit.tsx
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { createToolFlow } from "../components/tools/shared/createToolFlow";
|
||||||
|
import ScannerImageSplitSettings from "../components/tools/scannerImageSplit/ScannerImageSplitSettings";
|
||||||
|
import { useScannerImageSplitParameters } from "../hooks/tools/scannerImageSplit/useScannerImageSplitParameters";
|
||||||
|
import { useScannerImageSplitOperation } from "../hooks/tools/scannerImageSplit/useScannerImageSplitOperation";
|
||||||
|
import { useBaseTool } from "../hooks/tools/shared/useBaseTool";
|
||||||
|
import { BaseToolProps, ToolComponent } from "../types/tool";
|
||||||
|
import { useScannerImageSplitTips } from "../components/tooltips/useScannerImageSplitTips";
|
||||||
|
|
||||||
|
const ScannerImageSplit = (props: BaseToolProps) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const scannerImageSplitTips = useScannerImageSplitTips();
|
||||||
|
|
||||||
|
const base = useBaseTool(
|
||||||
|
'scannerImageSplit',
|
||||||
|
useScannerImageSplitParameters,
|
||||||
|
useScannerImageSplitOperation,
|
||||||
|
props
|
||||||
|
);
|
||||||
|
|
||||||
|
return createToolFlow({
|
||||||
|
files: {
|
||||||
|
selectedFiles: base.selectedFiles,
|
||||||
|
isCollapsed: base.hasResults,
|
||||||
|
},
|
||||||
|
steps: [
|
||||||
|
{
|
||||||
|
title: "Settings",
|
||||||
|
isCollapsed: base.settingsCollapsed,
|
||||||
|
onCollapsedClick: base.settingsCollapsed ? base.handleSettingsReset : undefined,
|
||||||
|
tooltip: scannerImageSplitTips,
|
||||||
|
content: (
|
||||||
|
<ScannerImageSplitSettings
|
||||||
|
parameters={base.params.parameters}
|
||||||
|
onParameterChange={base.params.updateParameter}
|
||||||
|
disabled={base.endpointLoading}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
executeButton: {
|
||||||
|
text: t("scannerImageSplit.submit", "Extract Image Scans"),
|
||||||
|
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("scannerImageSplit.title", "Extracted Images"),
|
||||||
|
onFileClick: base.handleThumbnailClick,
|
||||||
|
onUndo: base.handleUndo,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ScannerImageSplit as ToolComponent;
|
Loading…
Reference in New Issue
Block a user