mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2025-08-02 10:35:22 +00:00
remove OCR language picker from settings code to make it a little easier to read. Added the link to docs on using different languages with OCR
This commit is contained in:
parent
710b4837a0
commit
a0e57655db
94
frontend/src/components/tools/ocr/LanguagePicker.module.css
Normal file
94
frontend/src/components/tools/ocr/LanguagePicker.module.css
Normal file
@ -0,0 +1,94 @@
|
||||
/* Language Picker Component */
|
||||
.languagePicker {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center; /* Center align items vertically */
|
||||
height: 32px;
|
||||
border: 1px solid var(--border-default);
|
||||
background-color: white; /* Default white background for light mode */
|
||||
color: var(--text-secondary);
|
||||
border-radius: var(--radius-sm);
|
||||
padding: 4px 8px;
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
/* Dark mode background */
|
||||
[data-mantine-color-scheme="dark"] .languagePicker {
|
||||
background-color: #2A2F36; /* Specific dark background color */
|
||||
}
|
||||
|
||||
.languagePicker:hover {
|
||||
border-color: var(--border-strong);
|
||||
background-color: var(--mantine-color-gray-0); /* Light gray on hover for light mode */
|
||||
}
|
||||
|
||||
/* Dark mode hover */
|
||||
[data-mantine-color-scheme="dark"] .languagePicker:hover {
|
||||
background-color: #3A3F46; /* Slightly lighter hover color */
|
||||
}
|
||||
|
||||
.languagePicker:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.languagePickerIcon {
|
||||
font-size: 16px;
|
||||
color: var(--text-muted);
|
||||
margin-left: auto;
|
||||
display: flex;
|
||||
align-items: center; /* Center the icon vertically */
|
||||
}
|
||||
|
||||
.languagePickerDropdown {
|
||||
background-color: white; /* Default white background for light mode */
|
||||
border: 1px solid var(--border-default);
|
||||
border-radius: var(--radius-sm);
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
/* Dark mode dropdown background */
|
||||
[data-mantine-color-scheme="dark"] .languagePickerDropdown {
|
||||
background-color: #2A2F36;
|
||||
}
|
||||
|
||||
.languagePickerOption {
|
||||
padding: 6px 10px;
|
||||
cursor: pointer;
|
||||
border-radius: var(--radius-xs);
|
||||
font-size: 13px;
|
||||
color: var(--text-primary);
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.languagePickerOption:hover {
|
||||
background-color: var(--mantine-color-gray-0); /* Light gray on hover for light mode */
|
||||
}
|
||||
|
||||
/* Dark mode option hover */
|
||||
[data-mantine-color-scheme="dark"] .languagePickerOption:hover {
|
||||
background-color: #3A3F46;
|
||||
}
|
||||
|
||||
.languagePickerOption.selected {
|
||||
background-color: var(--mantine-color-blue-1); /* Light blue for light mode */
|
||||
color: var(--mantine-color-blue-9); /* Dark blue text for light mode */
|
||||
}
|
||||
|
||||
/* Dark mode selected option */
|
||||
[data-mantine-color-scheme="dark"] .languagePickerOption.selected {
|
||||
background-color: var(--mantine-color-blue-8); /* Darker blue for better contrast */
|
||||
color: white; /* White text for dark mode selected items */
|
||||
}
|
||||
|
||||
.languagePickerOption.selected:hover {
|
||||
background-color: var(--mantine-color-blue-2); /* Slightly darker on hover for light mode */
|
||||
}
|
||||
|
||||
/* Dark mode selected option hover */
|
||||
[data-mantine-color-scheme="dark"] .languagePickerOption.selected:hover {
|
||||
background-color: var(--mantine-color-blue-7); /* Slightly lighter on hover */
|
||||
}
|
221
frontend/src/components/tools/ocr/LanguagePicker.tsx
Normal file
221
frontend/src/components/tools/ocr/LanguagePicker.tsx
Normal file
@ -0,0 +1,221 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Stack, Text, Loader, Popover, useMantineTheme, useMantineColorScheme, Box } from '@mantine/core';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { tempOcrLanguages } from '../../../utils/tempOcrLanguages';
|
||||
import UnfoldMoreIcon from '@mui/icons-material/UnfoldMore';
|
||||
|
||||
export interface LanguageOption {
|
||||
value: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
export interface LanguagePickerProps {
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
placeholder?: string;
|
||||
disabled?: boolean;
|
||||
label?: string;
|
||||
languagesEndpoint?: string;
|
||||
}
|
||||
|
||||
const LanguagePicker: React.FC<LanguagePickerProps> = ({
|
||||
value,
|
||||
onChange,
|
||||
placeholder = 'Select language',
|
||||
disabled = false,
|
||||
label,
|
||||
languagesEndpoint = '/api/v1/ui-data/ocr-pdf'
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const theme = useMantineTheme();
|
||||
const { colorScheme } = useMantineColorScheme();
|
||||
const [availableLanguages, setAvailableLanguages] = useState<LanguageOption[]>([]);
|
||||
const [isLoadingLanguages, setIsLoadingLanguages] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
// Fetch available languages from backend
|
||||
const fetchLanguages = async () => {
|
||||
console.log('[LanguagePicker] Starting language fetch...');
|
||||
console.log('[LanguagePicker] Fetching from URL:', languagesEndpoint);
|
||||
|
||||
try {
|
||||
const response = await fetch(languagesEndpoint);
|
||||
console.log('[LanguagePicker] Response received:', {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
ok: response.ok,
|
||||
headers: Object.fromEntries(response.headers.entries())
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const data: { languages: string[] } = await response.json();
|
||||
const languages = data.languages;
|
||||
console.log('[LanguagePicker] Raw response data:', languages);
|
||||
console.log('[LanguagePicker] Response type:', typeof languages, 'Array?', Array.isArray(languages));
|
||||
|
||||
const languageOptions = languages.map(lang => {
|
||||
// TODO: Use actual language translations when they become available
|
||||
// For now, use temporary English translations
|
||||
const translatedName = tempOcrLanguages.lang[lang as keyof typeof tempOcrLanguages.lang] || lang;
|
||||
const displayName = translatedName;
|
||||
|
||||
console.log(`[LanguagePicker] Language mapping: ${lang} -> ${displayName} (translated: ${!!translatedName})`);
|
||||
|
||||
return {
|
||||
value: lang,
|
||||
label: displayName
|
||||
};
|
||||
});
|
||||
console.log('[LanguagePicker] Transformed language options:', languageOptions);
|
||||
|
||||
setAvailableLanguages(languageOptions);
|
||||
console.log('[LanguagePicker] Successfully set', languageOptions.length, 'languages');
|
||||
} else {
|
||||
console.error('[LanguagePicker] Response not OK:', response.status, response.statusText);
|
||||
const errorText = await response.text();
|
||||
console.error('[LanguagePicker] Error response body:', errorText);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[LanguagePicker] Fetch failed with error:', error);
|
||||
console.error('[LanguagePicker] Error details:', {
|
||||
name: error instanceof Error ? error.name : 'Unknown',
|
||||
message: error instanceof Error ? error.message : String(error),
|
||||
stack: error instanceof Error ? error.stack : undefined
|
||||
});
|
||||
} finally {
|
||||
setIsLoadingLanguages(false);
|
||||
console.log('[LanguagePicker] Language loading completed');
|
||||
}
|
||||
};
|
||||
|
||||
fetchLanguages();
|
||||
}, [languagesEndpoint]);
|
||||
|
||||
if (isLoadingLanguages) {
|
||||
return (
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
||||
<Loader size="xs" />
|
||||
<Text size="sm">Loading available languages...</Text>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const selectedLanguage = availableLanguages.find(lang => lang.value === value);
|
||||
|
||||
// Get appropriate background colors based on color scheme
|
||||
const getBackgroundColor = () => {
|
||||
if (colorScheme === 'dark') {
|
||||
return '#2A2F36'; // Specific dark background color
|
||||
}
|
||||
return 'white'; // White background for light mode
|
||||
};
|
||||
|
||||
const getSelectedItemBackgroundColor = () => {
|
||||
if (colorScheme === 'dark') {
|
||||
return 'var(--mantine-color-blue-8)'; // Darker blue for better contrast
|
||||
}
|
||||
return 'var(--mantine-color-blue-1)'; // Light blue for light mode
|
||||
};
|
||||
|
||||
const getSelectedItemTextColor = () => {
|
||||
if (colorScheme === 'dark') {
|
||||
return 'white'; // White text for dark mode selected items
|
||||
}
|
||||
return 'var(--mantine-color-blue-9)'; // Dark blue text for light mode
|
||||
};
|
||||
|
||||
return (
|
||||
<Box>
|
||||
{label && (
|
||||
<Text size="sm" fw={500} mb={4}>
|
||||
{label}
|
||||
</Text>
|
||||
)}
|
||||
<Popover width="target" position="bottom" withArrow={false} shadow="md">
|
||||
<Popover.Target>
|
||||
<Box
|
||||
style={{
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center', // Center align items vertically
|
||||
height: '32px',
|
||||
border: `1px solid var(--border-default)`,
|
||||
backgroundColor: getBackgroundColor(),
|
||||
color: 'var(--text-secondary)',
|
||||
borderRadius: 'var(--radius-sm)',
|
||||
padding: '4px 8px',
|
||||
fontSize: '13px',
|
||||
cursor: disabled ? 'not-allowed' : 'pointer',
|
||||
opacity: disabled ? 0.6 : 1,
|
||||
transition: 'all 0.2s ease'
|
||||
}}
|
||||
>
|
||||
<Text size="sm" style={{ flex: 1 }}>
|
||||
{selectedLanguage?.label || placeholder}
|
||||
</Text>
|
||||
<UnfoldMoreIcon style={{
|
||||
fontSize: '16px',
|
||||
color: 'var(--text-muted)',
|
||||
marginLeft: 'auto',
|
||||
display: 'flex',
|
||||
alignItems: 'center'
|
||||
}} />
|
||||
</Box>
|
||||
</Popover.Target>
|
||||
<Popover.Dropdown style={{
|
||||
backgroundColor: getBackgroundColor(),
|
||||
border: '1px solid var(--border-default)',
|
||||
borderRadius: 'var(--radius-sm)',
|
||||
padding: '4px'
|
||||
}}>
|
||||
<Stack gap="xs">
|
||||
<Box style={{
|
||||
maxHeight: '180px',
|
||||
overflowY: 'auto',
|
||||
borderBottom: '1px solid var(--border-default)',
|
||||
paddingBottom: '8px'
|
||||
}}>
|
||||
{availableLanguages.map((lang) => (
|
||||
<Box
|
||||
key={lang.value}
|
||||
style={{
|
||||
padding: '6px 10px',
|
||||
cursor: 'pointer',
|
||||
borderRadius: 'var(--radius-xs)',
|
||||
fontSize: '13px',
|
||||
color: value === lang.value ? getSelectedItemTextColor() : 'var(--text-primary)',
|
||||
backgroundColor: value === lang.value ? getSelectedItemBackgroundColor() : 'transparent',
|
||||
transition: 'background-color 0.2s ease'
|
||||
}}
|
||||
onClick={() => onChange(lang.value)}
|
||||
>
|
||||
<Text size="sm">{lang.label}</Text>
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
<Box style={{
|
||||
padding: '8px',
|
||||
textAlign: 'center',
|
||||
fontSize: '12px'
|
||||
}}>
|
||||
<Text size="xs" c="dimmed" mb={4}>
|
||||
Looking for additional languages?
|
||||
</Text>
|
||||
<Text
|
||||
size="xs"
|
||||
c="blue"
|
||||
style={{ textDecoration: 'underline', cursor: 'pointer' }}
|
||||
onClick={() => window.open('https://docs.stirlingpdf.com/Advanced%20Configuration/OCR', '_blank')}
|
||||
>
|
||||
View setup guide →
|
||||
</Text>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Popover.Dropdown>
|
||||
</Popover>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default LanguagePicker;
|
@ -1,7 +1,7 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Stack, Select, MultiSelect, Text, Loader } from '@mantine/core';
|
||||
import React from 'react';
|
||||
import { Stack, Select, MultiSelect, Text, Divider } from '@mantine/core';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { tempOcrLanguages } from '../../../utils/tempOcrLanguages';
|
||||
import LanguagePicker from './LanguagePicker';
|
||||
|
||||
export interface OCRParameters {
|
||||
languages: string[];
|
||||
@ -22,8 +22,6 @@ const OCRSettings: React.FC<OCRSettingsProps> = ({
|
||||
disabled = false
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const [availableLanguages, setAvailableLanguages] = useState<{value: string, label: string}[]>([]);
|
||||
const [isLoadingLanguages, setIsLoadingLanguages] = useState(true);
|
||||
|
||||
// Define the additional options available
|
||||
const additionalOptionsData = [
|
||||
@ -31,89 +29,11 @@ const OCRSettings: React.FC<OCRSettingsProps> = ({
|
||||
{ value: 'deskew', label: 'Deskew pages' },
|
||||
{ value: 'clean', label: 'Clean input file' },
|
||||
{ value: 'cleanFinal', label: 'Clean final output' },
|
||||
{ value: 'removeImagesAfter', label: 'Remove images after OCR' },
|
||||
];
|
||||
|
||||
useEffect(() => {
|
||||
// Fetch available languages from backend
|
||||
const fetchLanguages = async () => {
|
||||
console.log('[OCR Languages] Starting language fetch...');
|
||||
const url = '/api/v1/ui-data/ocr-pdf';
|
||||
console.log('[OCR Languages] Fetching from URL:', url);
|
||||
|
||||
try {
|
||||
const response = await fetch(url);
|
||||
console.log('[OCR Languages] Response received:', {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
ok: response.ok,
|
||||
headers: Object.fromEntries(response.headers.entries())
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const data: { languages: string[] } = await response.json();
|
||||
const languages = data.languages;
|
||||
console.log('[OCR Languages] Raw response data:', languages);
|
||||
console.log('[OCR Languages] Response type:', typeof languages, 'Array?', Array.isArray(languages));
|
||||
|
||||
const languageOptions = languages.map(lang => {
|
||||
// TODO: Use actual language translations when they become available
|
||||
// For now, use temporary English translations
|
||||
const translatedName = tempOcrLanguages.lang[lang as keyof typeof tempOcrLanguages.lang] || lang;
|
||||
const displayName = translatedName;
|
||||
|
||||
console.log(`[OCR Languages] Language mapping: ${lang} -> ${displayName} (translated: ${!!translatedName})`);
|
||||
|
||||
return {
|
||||
value: lang,
|
||||
label: displayName
|
||||
};
|
||||
});
|
||||
console.log('[OCR Languages] Transformed language options:', languageOptions);
|
||||
|
||||
setAvailableLanguages(languageOptions);
|
||||
console.log('[OCR Languages] Successfully set', languageOptions.length, 'languages');
|
||||
} else {
|
||||
console.error('[OCR Languages] Response not OK:', response.status, response.statusText);
|
||||
const errorText = await response.text();
|
||||
console.error('[OCR Languages] Error response body:', errorText);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[OCR Languages] Fetch failed with error:', error);
|
||||
console.error('[OCR Languages] Error details:', {
|
||||
name: error instanceof Error ? error.name : 'Unknown',
|
||||
message: error instanceof Error ? error.message : String(error),
|
||||
stack: error instanceof Error ? error.stack : undefined
|
||||
});
|
||||
} finally {
|
||||
setIsLoadingLanguages(false);
|
||||
console.log('[OCR Languages] Language loading completed');
|
||||
}
|
||||
};
|
||||
|
||||
fetchLanguages();
|
||||
}, [t]); // Add t to dependencies since we're using it in the effect
|
||||
|
||||
return (
|
||||
<Stack gap="md">
|
||||
<Text size="sm" fw={500}>OCR Configuration</Text>
|
||||
|
||||
{isLoadingLanguages ? (
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
||||
<Loader size="xs" />
|
||||
<Text size="sm">Loading available languages...</Text>
|
||||
</div>
|
||||
) : (
|
||||
<Select
|
||||
label="Languages"
|
||||
placeholder="Select primary language for OCR"
|
||||
value={parameters.languages[0] || ''}
|
||||
onChange={(value) => onParameterChange('languages', value ? [value] : [])}
|
||||
data={availableLanguages}
|
||||
disabled={disabled}
|
||||
clearable
|
||||
/>
|
||||
)}
|
||||
|
||||
<Select
|
||||
label="OCR Mode"
|
||||
@ -121,12 +41,24 @@ const OCRSettings: React.FC<OCRSettingsProps> = ({
|
||||
onChange={(value) => onParameterChange('ocrType', value || 'skip-text')}
|
||||
data={[
|
||||
{ value: 'skip-text', label: 'Auto (skip text layers)' },
|
||||
{ value: 'force-ocr', label: 'Force OCR - Process all pages' },
|
||||
{ value: 'Normal', label: 'Normal - Error if text exists' },
|
||||
{ value: 'force-ocr', label: 'Force (re-OCR all, replace text)' },
|
||||
{ value: 'Normal', label: 'Strict (abort if text found)' },
|
||||
]}
|
||||
disabled={disabled}
|
||||
/>
|
||||
|
||||
<Divider />
|
||||
|
||||
<LanguagePicker
|
||||
value={parameters.languages[0] || ''}
|
||||
onChange={(value) => onParameterChange('languages', [value])}
|
||||
placeholder="Select primary language for OCR"
|
||||
disabled={disabled}
|
||||
label="Languages"
|
||||
/>
|
||||
|
||||
<Divider />
|
||||
|
||||
<Select
|
||||
label="Output"
|
||||
value={parameters.ocrRenderType}
|
||||
@ -138,23 +70,18 @@ const OCRSettings: React.FC<OCRSettingsProps> = ({
|
||||
disabled={disabled}
|
||||
/>
|
||||
|
||||
<Divider />
|
||||
|
||||
<MultiSelect
|
||||
label="Additional Options"
|
||||
placeholder="Select additional options"
|
||||
placeholder="Select Options"
|
||||
value={parameters.additionalOptions}
|
||||
onChange={(value) => onParameterChange('additionalOptions', value)}
|
||||
data={additionalOptionsData}
|
||||
disabled={disabled}
|
||||
clearable
|
||||
styles={{
|
||||
input: {
|
||||
backgroundColor: 'var(--mantine-color-gray-1)',
|
||||
borderColor: 'var(--mantine-color-gray-3)',
|
||||
},
|
||||
dropdown: {
|
||||
backgroundColor: 'var(--mantine-color-gray-1)',
|
||||
}
|
||||
}}
|
||||
comboboxProps={{ position: 'top', middlewares: { flip: false, shift: false } }}
|
||||
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
|
@ -33,12 +33,13 @@ const ToolStep = ({
|
||||
}: ToolStepProps) => {
|
||||
if (!isVisible) return null;
|
||||
|
||||
const parent = useContext(ToolStepContext);
|
||||
|
||||
// Auto-detect if we should show numbers based on sibling count
|
||||
const shouldShowNumber = useMemo(() => {
|
||||
if (showNumber !== undefined) return showNumber;
|
||||
const parent = useContext(ToolStepContext);
|
||||
return parent ? parent.visibleStepCount >= 3 : false;
|
||||
}, [showNumber]);
|
||||
}, [showNumber, parent]);
|
||||
|
||||
const stepNumber = useContext(ToolStepContext)?.getStepNumber?.() || 1;
|
||||
|
||||
@ -96,7 +97,7 @@ export const ToolStepContainer = ({ children }: ToolStepContainerProps) => {
|
||||
let count = 0;
|
||||
React.Children.forEach(children, (child) => {
|
||||
if (React.isValidElement(child) && child.type === ToolStep) {
|
||||
const isVisible = child.props.isVisible !== false;
|
||||
const isVisible = (child.props as ToolStepProps).isVisible !== false;
|
||||
if (isVisible) count++;
|
||||
}
|
||||
});
|
||||
|
@ -5,6 +5,42 @@ import { useFileContext } from '../../../contexts/FileContext';
|
||||
import { FileOperation } from '../../../types/fileContext';
|
||||
import { OCRParameters } from '../../../components/tools/ocr/OCRSettings';
|
||||
|
||||
//Extract files from a ZIP blob
|
||||
async function extractZipFile(zipBlob: Blob): Promise<File[]> {
|
||||
const JSZip = await import('jszip');
|
||||
const zip = new JSZip.default();
|
||||
|
||||
const arrayBuffer = await zipBlob.arrayBuffer();
|
||||
const zipContent = await zip.loadAsync(arrayBuffer);
|
||||
|
||||
const extractedFiles: File[] = [];
|
||||
|
||||
for (const [filename, file] of Object.entries(zipContent.files)) {
|
||||
if (!file.dir) {
|
||||
const content = await file.async('blob');
|
||||
const extractedFile = new File([content], filename, { type: getMimeType(filename) });
|
||||
extractedFiles.push(extractedFile);
|
||||
}
|
||||
}
|
||||
|
||||
return extractedFiles;
|
||||
}
|
||||
|
||||
//Get MIME type based on file extension
|
||||
function getMimeType(filename: string): string {
|
||||
const ext = filename.toLowerCase().split('.').pop();
|
||||
switch (ext) {
|
||||
case 'pdf':
|
||||
return 'application/pdf';
|
||||
case 'txt':
|
||||
return 'text/plain';
|
||||
case 'zip':
|
||||
return 'application/zip';
|
||||
default:
|
||||
return 'application/octet-stream';
|
||||
}
|
||||
}
|
||||
|
||||
export interface OCROperationHook {
|
||||
files: File[];
|
||||
thumbnails: string[];
|
||||
@ -155,16 +191,79 @@ export const useOCROperation = (): OCROperationHook => {
|
||||
|
||||
try {
|
||||
const { formData, endpoint } = buildFormData(parameters, file);
|
||||
const response = await axios.post(endpoint, formData, { responseType: "blob" });
|
||||
const response = await axios.post(endpoint, formData, {
|
||||
responseType: "blob",
|
||||
timeout: 300000 // 5 minute timeout for OCR
|
||||
});
|
||||
|
||||
// Check for HTTP errors
|
||||
if (response.status >= 400) {
|
||||
// Try to read error response as text
|
||||
const errorText = await response.data.text();
|
||||
throw new Error(`OCR service HTTP error ${response.status}: ${errorText.substring(0, 300)}`);
|
||||
}
|
||||
|
||||
// Validate response
|
||||
if (!response.data || response.data.size === 0) {
|
||||
throw new Error('Empty response from OCR service');
|
||||
}
|
||||
|
||||
const contentType = response.headers['content-type'] || 'application/pdf';
|
||||
|
||||
// Check if response is actually a PDF by examining the first few bytes
|
||||
const arrayBuffer = await response.data.arrayBuffer();
|
||||
const uint8Array = new Uint8Array(arrayBuffer);
|
||||
const header = new TextDecoder().decode(uint8Array.slice(0, 4));
|
||||
|
||||
// Check if it's a ZIP file (OCR service returns ZIP when sidecar is enabled or for multi-file results)
|
||||
if (header.startsWith('PK')) {
|
||||
try {
|
||||
// Extract ZIP file contents
|
||||
const zipFiles = await extractZipFile(response.data);
|
||||
|
||||
// Add extracted files to processed files
|
||||
processedFiles.push(...zipFiles);
|
||||
} catch (extractError) {
|
||||
// Fallback to treating as single ZIP file
|
||||
const blob = new Blob([response.data], { type: 'application/zip' });
|
||||
const processedFile = new File([blob], `ocr_${file.name}.zip`, { type: 'application/zip' });
|
||||
processedFiles.push(processedFile);
|
||||
}
|
||||
continue; // Skip the PDF validation for ZIP files
|
||||
}
|
||||
|
||||
if (!header.startsWith('%PDF')) {
|
||||
// Check if it's an error response
|
||||
const text = new TextDecoder().decode(uint8Array.slice(0, 500));
|
||||
|
||||
if (text.includes('error') || text.includes('Error') || text.includes('exception') || text.includes('html')) {
|
||||
// Check for specific OCR tool unavailable error
|
||||
if (text.includes('OCR tools') && text.includes('not installed')) {
|
||||
throw new Error('OCR tools (OCRmyPDF or Tesseract) are not installed on the server. Use the standard or fat Docker image instead of ultra-lite, or install OCR tools manually.');
|
||||
}
|
||||
throw new Error(`OCR service error: ${text.substring(0, 300)}`);
|
||||
}
|
||||
|
||||
// Check if it's an HTML error page
|
||||
if (text.includes('<html') || text.includes('<!DOCTYPE')) {
|
||||
// Try to extract error message from HTML
|
||||
const errorMatch = text.match(/<title[^>]*>([^<]+)<\/title>/i) ||
|
||||
text.match(/<h1[^>]*>([^<]+)<\/h1>/i) ||
|
||||
text.match(/<body[^>]*>([^<]+)<\/body>/i);
|
||||
const errorMessage = errorMatch ? errorMatch[1].trim() : 'Unknown error';
|
||||
throw new Error(`OCR service error: ${errorMessage}`);
|
||||
}
|
||||
|
||||
throw new Error(`Response is not a valid PDF file. Header: "${header}"`);
|
||||
}
|
||||
|
||||
const blob = new Blob([response.data], { type: contentType });
|
||||
const processedFile = new File([blob], `ocr_${file.name}`, { type: contentType });
|
||||
|
||||
processedFiles.push(processedFile);
|
||||
} catch (fileError) {
|
||||
console.error(`Failed to process OCR for ${file.name}:`, fileError);
|
||||
failedFiles.push(file.name);
|
||||
const errorMessage = fileError instanceof Error ? fileError.message : 'Unknown error';
|
||||
failedFiles.push(`${file.name} (${errorMessage})`);
|
||||
}
|
||||
}
|
||||
|
||||
@ -175,7 +274,19 @@ export const useOCROperation = (): OCROperationHook => {
|
||||
if (failedFiles.length > 0) {
|
||||
setStatus(`Processed ${processedFiles.length}/${validFiles.length} files. Failed: ${failedFiles.join(', ')}`);
|
||||
} else {
|
||||
setStatus(`OCR completed successfully for ${processedFiles.length} file(s)`);
|
||||
const hasPdfFiles = processedFiles.some(file => file.name.endsWith('.pdf'));
|
||||
const hasTxtFiles = processedFiles.some(file => file.name.endsWith('.txt'));
|
||||
let statusMessage = `OCR completed successfully for ${processedFiles.length} file(s)`;
|
||||
|
||||
if (hasPdfFiles && hasTxtFiles) {
|
||||
statusMessage += ' (Extracted PDF and text files)';
|
||||
} else if (hasPdfFiles) {
|
||||
statusMessage += ' (Extracted PDF files)';
|
||||
} else if (hasTxtFiles) {
|
||||
statusMessage += ' (Extracted text files)';
|
||||
}
|
||||
|
||||
setStatus(statusMessage);
|
||||
}
|
||||
|
||||
setFiles(processedFiles);
|
||||
@ -186,18 +297,34 @@ export const useOCROperation = (): OCROperationHook => {
|
||||
// Cleanup old blob URLs
|
||||
cleanupBlobUrls();
|
||||
|
||||
// Create download URL
|
||||
// Create download URL - for multiple files, we'll create a new ZIP
|
||||
if (processedFiles.length === 1) {
|
||||
const url = window.URL.createObjectURL(processedFiles[0]);
|
||||
setDownloadUrl(url);
|
||||
setBlobUrls([url]);
|
||||
setDownloadFilename(`ocr_${selectedFiles[0].name}`);
|
||||
setDownloadFilename(processedFiles[0].name);
|
||||
} else {
|
||||
// For multiple files, we could create a zip, but for now just handle the first file
|
||||
const url = window.URL.createObjectURL(processedFiles[0]);
|
||||
setDownloadUrl(url);
|
||||
setBlobUrls([url]);
|
||||
setDownloadFilename(`ocr_${validFiles.length}_files.pdf`);
|
||||
// For multiple files, create a new ZIP containing all extracted files
|
||||
try {
|
||||
const JSZip = await import('jszip');
|
||||
const zip = new JSZip.default();
|
||||
|
||||
for (const file of processedFiles) {
|
||||
zip.file(file.name, file);
|
||||
}
|
||||
|
||||
const zipBlob = await zip.generateAsync({ type: 'blob' });
|
||||
const url = window.URL.createObjectURL(zipBlob);
|
||||
setDownloadUrl(url);
|
||||
setBlobUrls([url]);
|
||||
setDownloadFilename(`ocr_extracted_files.zip`);
|
||||
} catch (zipError) {
|
||||
// Fallback to first file
|
||||
const url = window.URL.createObjectURL(processedFiles[0]);
|
||||
setDownloadUrl(url);
|
||||
setBlobUrls([url]);
|
||||
setDownloadFilename(processedFiles[0].name);
|
||||
}
|
||||
}
|
||||
|
||||
markOperationApplied(fileId, operationId);
|
||||
|
@ -167,6 +167,39 @@ export const mantineTheme = createTheme({
|
||||
},
|
||||
},
|
||||
|
||||
MultiSelect: {
|
||||
styles: {
|
||||
input: {
|
||||
backgroundColor: 'var(--bg-surface)',
|
||||
borderColor: 'var(--border-default)',
|
||||
color: 'var(--text-primary)',
|
||||
'&:focus': {
|
||||
borderColor: 'var(--color-primary-500)',
|
||||
boxShadow: '0 0 0 1px var(--color-primary-500)',
|
||||
},
|
||||
},
|
||||
label: {
|
||||
color: 'var(--text-secondary)',
|
||||
fontWeight: 'var(--font-weight-medium)',
|
||||
},
|
||||
dropdown: {
|
||||
backgroundColor: 'var(--bg-surface)',
|
||||
borderColor: 'var(--border-subtle)',
|
||||
boxShadow: 'var(--shadow-lg)',
|
||||
},
|
||||
option: {
|
||||
color: 'var(--text-primary)',
|
||||
'&[data-hovered]': {
|
||||
backgroundColor: 'var(--hover-bg)',
|
||||
},
|
||||
'&[data-selected]': {
|
||||
backgroundColor: 'var(--color-primary-100)',
|
||||
color: 'var(--color-primary-900)',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
Checkbox: {
|
||||
styles: {
|
||||
input: {
|
||||
|
Loading…
x
Reference in New Issue
Block a user