@rustors/react 0.0.0-stage → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +85 -2
- package/dist/components/BoundingBox.d.ts +4 -0
- package/dist/components/BoundingBox.d.ts.map +1 -0
- package/dist/components/BoundingBox.js +40 -0
- package/dist/components/BoundingBox.js.map +1 -0
- package/dist/components/DropzonePreview.d.ts +4 -0
- package/dist/components/DropzonePreview.d.ts.map +1 -0
- package/dist/components/DropzonePreview.js +40 -0
- package/dist/components/DropzonePreview.js.map +1 -0
- package/dist/components/DropzonePrompt.d.ts +4 -0
- package/dist/components/DropzonePrompt.d.ts.map +1 -0
- package/dist/components/DropzonePrompt.js +16 -0
- package/dist/components/DropzonePrompt.js.map +1 -0
- package/dist/components/OcrDropzone.d.ts +4 -0
- package/dist/components/OcrDropzone.d.ts.map +1 -0
- package/dist/components/OcrDropzone.js +65 -0
- package/dist/components/OcrDropzone.js.map +1 -0
- package/dist/components/OcrOverlay.d.ts +4 -0
- package/dist/components/OcrOverlay.d.ts.map +1 -0
- package/dist/components/OcrOverlay.js +50 -0
- package/dist/components/OcrOverlay.js.map +1 -0
- package/dist/context/RustoContext.d.ts +3 -0
- package/dist/context/RustoContext.d.ts.map +1 -0
- package/dist/context/RustoContext.js +6 -0
- package/dist/context/RustoContext.js.map +1 -0
- package/dist/context/RustoProvider.d.ts +4 -0
- package/dist/context/RustoProvider.d.ts.map +1 -0
- package/dist/context/RustoProvider.js +41 -0
- package/dist/context/RustoProvider.js.map +1 -0
- package/dist/hooks/useOcr.d.ts +3 -0
- package/dist/hooks/useOcr.d.ts.map +1 -0
- package/dist/hooks/useOcr.js +59 -0
- package/dist/hooks/useOcr.js.map +1 -0
- package/dist/hooks/useRusto.d.ts +3 -0
- package/dist/hooks/useRusto.d.ts.map +1 -0
- package/dist/hooks/useRusto.js +13 -0
- package/dist/hooks/useRusto.js.map +1 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +54 -0
- package/dist/index.js.map +1 -0
- package/dist/services/ocrService.d.ts +12 -0
- package/dist/services/ocrService.d.ts.map +1 -0
- package/dist/services/ocrService.js +30 -0
- package/dist/services/ocrService.js.map +1 -0
- package/dist/types/components.d.ts +44 -0
- package/dist/types/components.d.ts.map +1 -0
- package/dist/types/components.js +3 -0
- package/dist/types/components.js.map +1 -0
- package/dist/types/context.d.ts +16 -0
- package/dist/types/context.d.ts.map +1 -0
- package/dist/types/context.js +3 -0
- package/dist/types/context.js.map +1 -0
- package/dist/types/hooks.d.ts +14 -0
- package/dist/types/hooks.d.ts.map +1 -0
- package/dist/types/hooks.js +3 -0
- package/dist/types/hooks.js.map +1 -0
- package/dist/types/index.d.ts +4 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +20 -0
- package/dist/types/index.js.map +1 -0
- package/dist/utils/formatters.d.ts +14 -0
- package/dist/utils/formatters.d.ts.map +1 -0
- package/dist/utils/formatters.js +29 -0
- package/dist/utils/formatters.js.map +1 -0
- package/dist/utils/geometry.d.ts +9 -0
- package/dist/utils/geometry.d.ts.map +1 -0
- package/dist/utils/geometry.js +31 -0
- package/dist/utils/geometry.js.map +1 -0
- package/package.json +59 -4
- package/src/components/BoundingBox.tsx +63 -0
- package/src/components/DropzonePreview.tsx +61 -0
- package/src/components/DropzonePrompt.tsx +46 -0
- package/src/components/OcrDropzone.tsx +117 -0
- package/src/components/OcrOverlay.tsx +100 -0
- package/src/context/RustoContext.ts +4 -0
- package/src/context/RustoProvider.tsx +62 -0
- package/src/hooks/useOcr.ts +73 -0
- package/src/hooks/useRusto.ts +11 -0
- package/src/index.ts +34 -0
- package/src/services/ocrService.ts +43 -0
- package/src/types/components.ts +48 -0
- package/src/types/context.ts +18 -0
- package/src/types/hooks.ts +22 -0
- package/src/types/index.ts +3 -0
- package/src/utils/formatters.ts +27 -0
- package/src/utils/geometry.ts +36 -0
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import {
|
|
2
|
+
useState,
|
|
3
|
+
useRef,
|
|
4
|
+
useCallback,
|
|
5
|
+
type ReactElement,
|
|
6
|
+
type DragEvent,
|
|
7
|
+
type ChangeEvent,
|
|
8
|
+
} from 'react';
|
|
9
|
+
import type { OcrDropzoneProps } from '../types/index.js';
|
|
10
|
+
import { DropzonePrompt } from './DropzonePrompt.js';
|
|
11
|
+
import { DropzonePreview } from './DropzonePreview.js';
|
|
12
|
+
import { useOcr } from '../hooks/useOcr.js';
|
|
13
|
+
|
|
14
|
+
export function OcrDropzone({
|
|
15
|
+
onFileSelect,
|
|
16
|
+
onDetect,
|
|
17
|
+
autoDetect = false,
|
|
18
|
+
detectOptions,
|
|
19
|
+
accept = 'image/png,image/jpeg,image/webp,image/bmp',
|
|
20
|
+
className,
|
|
21
|
+
style,
|
|
22
|
+
}: OcrDropzoneProps): ReactElement {
|
|
23
|
+
const fileInputRef = useRef<HTMLInputElement | null>(null);
|
|
24
|
+
const [isDragOver, setIsDragOver] = useState(false);
|
|
25
|
+
const [previewUrl, setPreviewUrl] = useState<string | null>(null);
|
|
26
|
+
|
|
27
|
+
const { detect } = useOcr({
|
|
28
|
+
...detectOptions,
|
|
29
|
+
onSuccess: (res) => onDetect?.(res),
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const handleFile = useCallback(
|
|
33
|
+
async (file: File) => {
|
|
34
|
+
onFileSelect?.(file);
|
|
35
|
+
const url = URL.createObjectURL(file);
|
|
36
|
+
setPreviewUrl(url);
|
|
37
|
+
|
|
38
|
+
if (autoDetect) {
|
|
39
|
+
await detect(file, detectOptions);
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
[autoDetect, detect, detectOptions, onFileSelect]
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
const onDragOver = useCallback((e: DragEvent<HTMLDivElement>) => {
|
|
46
|
+
e.preventDefault();
|
|
47
|
+
setIsDragOver(true);
|
|
48
|
+
}, []);
|
|
49
|
+
|
|
50
|
+
const onDragLeave = useCallback(() => {
|
|
51
|
+
setIsDragOver(false);
|
|
52
|
+
}, []);
|
|
53
|
+
|
|
54
|
+
const onDrop = useCallback(
|
|
55
|
+
(e: DragEvent<HTMLDivElement>) => {
|
|
56
|
+
e.preventDefault();
|
|
57
|
+
setIsDragOver(false);
|
|
58
|
+
const files = e.dataTransfer.files;
|
|
59
|
+
if (files && files.length > 0) {
|
|
60
|
+
handleFile(files[0]);
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
[handleFile]
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
const onInputChange = useCallback(
|
|
67
|
+
(e: ChangeEvent<HTMLInputElement>) => {
|
|
68
|
+
const files = e.target.files;
|
|
69
|
+
if (files && files.length > 0) {
|
|
70
|
+
handleFile(files[0]);
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
[handleFile]
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
const clearSelection = useCallback(() => {
|
|
77
|
+
if (previewUrl) {
|
|
78
|
+
URL.revokeObjectURL(previewUrl);
|
|
79
|
+
}
|
|
80
|
+
setPreviewUrl(null);
|
|
81
|
+
if (fileInputRef.current) {
|
|
82
|
+
fileInputRef.current.value = '';
|
|
83
|
+
}
|
|
84
|
+
}, [previewUrl]);
|
|
85
|
+
|
|
86
|
+
return (
|
|
87
|
+
<div
|
|
88
|
+
className={className}
|
|
89
|
+
onClick={() => fileInputRef.current?.click()}
|
|
90
|
+
onDragOver={onDragOver}
|
|
91
|
+
onDragLeave={onDragLeave}
|
|
92
|
+
onDrop={onDrop}
|
|
93
|
+
style={{
|
|
94
|
+
border: `2px dashed ${isDragOver ? '#3b82f6' : '#d1d5db'}`,
|
|
95
|
+
borderRadius: '8px',
|
|
96
|
+
backgroundColor: isDragOver ? '#eff6ff' : '#fafafa',
|
|
97
|
+
cursor: 'pointer',
|
|
98
|
+
transition: 'border-color 0.15s ease, background-color 0.15s ease',
|
|
99
|
+
boxSizing: 'border-box',
|
|
100
|
+
...style,
|
|
101
|
+
}}
|
|
102
|
+
>
|
|
103
|
+
<input
|
|
104
|
+
ref={fileInputRef}
|
|
105
|
+
type="file"
|
|
106
|
+
accept={accept}
|
|
107
|
+
onChange={onInputChange}
|
|
108
|
+
style={{ display: 'none' }}
|
|
109
|
+
/>
|
|
110
|
+
{previewUrl ? (
|
|
111
|
+
<DropzonePreview previewUrl={previewUrl} onClear={clearSelection} />
|
|
112
|
+
) : (
|
|
113
|
+
<DropzonePrompt />
|
|
114
|
+
)}
|
|
115
|
+
</div>
|
|
116
|
+
);
|
|
117
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import {
|
|
2
|
+
useState,
|
|
3
|
+
useRef,
|
|
4
|
+
useCallback,
|
|
5
|
+
type ReactElement,
|
|
6
|
+
type SyntheticEvent,
|
|
7
|
+
} from 'react';
|
|
8
|
+
import type { OcrOverlayProps } from '../types/index.js';
|
|
9
|
+
import { BoundingBox } from './BoundingBox.js';
|
|
10
|
+
import { calculateScale } from '../utils/geometry.js';
|
|
11
|
+
|
|
12
|
+
export function OcrOverlay({
|
|
13
|
+
imageSrc,
|
|
14
|
+
results,
|
|
15
|
+
naturalWidth: propNaturalWidth,
|
|
16
|
+
naturalHeight: propNaturalHeight,
|
|
17
|
+
highlightColor,
|
|
18
|
+
showTooltips = true,
|
|
19
|
+
className,
|
|
20
|
+
style,
|
|
21
|
+
onItemClick,
|
|
22
|
+
}: OcrOverlayProps): ReactElement {
|
|
23
|
+
const imgRef = useRef<HTMLImageElement | null>(null);
|
|
24
|
+
const [renderDims, setRenderDims] = useState<{ width: number; height: number }>({
|
|
25
|
+
width: 0,
|
|
26
|
+
height: 0,
|
|
27
|
+
});
|
|
28
|
+
const [naturalDims, setNaturalDims] = useState<{ width: number; height: number }>({
|
|
29
|
+
width: propNaturalWidth || 0,
|
|
30
|
+
height: propNaturalHeight || 0,
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const onImageLoad = useCallback((e: SyntheticEvent<HTMLImageElement>) => {
|
|
34
|
+
const img = e.currentTarget;
|
|
35
|
+
setNaturalDims({
|
|
36
|
+
width: img.naturalWidth,
|
|
37
|
+
height: img.naturalHeight,
|
|
38
|
+
});
|
|
39
|
+
setRenderDims({
|
|
40
|
+
width: img.clientWidth,
|
|
41
|
+
height: img.clientHeight,
|
|
42
|
+
});
|
|
43
|
+
}, []);
|
|
44
|
+
|
|
45
|
+
const effectiveNaturalWidth = propNaturalWidth || naturalDims.width;
|
|
46
|
+
const effectiveNaturalHeight = propNaturalHeight || naturalDims.height;
|
|
47
|
+
|
|
48
|
+
const { scaleX, scaleY } = calculateScale(
|
|
49
|
+
effectiveNaturalWidth,
|
|
50
|
+
effectiveNaturalHeight,
|
|
51
|
+
renderDims.width || effectiveNaturalWidth,
|
|
52
|
+
renderDims.height || effectiveNaturalHeight
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
<div
|
|
57
|
+
className={className}
|
|
58
|
+
style={{
|
|
59
|
+
position: 'relative',
|
|
60
|
+
display: 'inline-block',
|
|
61
|
+
overflow: 'hidden',
|
|
62
|
+
...style,
|
|
63
|
+
}}
|
|
64
|
+
>
|
|
65
|
+
<img
|
|
66
|
+
ref={imgRef}
|
|
67
|
+
src={imageSrc}
|
|
68
|
+
alt="OCR Target"
|
|
69
|
+
onLoad={onImageLoad}
|
|
70
|
+
style={{
|
|
71
|
+
display: 'block',
|
|
72
|
+
maxWidth: '100%',
|
|
73
|
+
height: 'auto',
|
|
74
|
+
}}
|
|
75
|
+
/>
|
|
76
|
+
<div
|
|
77
|
+
style={{
|
|
78
|
+
position: 'absolute',
|
|
79
|
+
top: 0,
|
|
80
|
+
left: 0,
|
|
81
|
+
right: 0,
|
|
82
|
+
bottom: 0,
|
|
83
|
+
pointerEvents: 'none',
|
|
84
|
+
}}
|
|
85
|
+
>
|
|
86
|
+
{results.map((item, idx) => (
|
|
87
|
+
<BoundingBox
|
|
88
|
+
key={`${item.text}-${idx}`}
|
|
89
|
+
item={item}
|
|
90
|
+
scaleX={scaleX}
|
|
91
|
+
scaleY={scaleY}
|
|
92
|
+
highlightColor={highlightColor}
|
|
93
|
+
showTooltip={showTooltips}
|
|
94
|
+
onClick={onItemClick}
|
|
95
|
+
/>
|
|
96
|
+
))}
|
|
97
|
+
</div>
|
|
98
|
+
</div>
|
|
99
|
+
);
|
|
100
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import {
|
|
2
|
+
useState,
|
|
3
|
+
useEffect,
|
|
4
|
+
useCallback,
|
|
5
|
+
useMemo,
|
|
6
|
+
type ReactElement,
|
|
7
|
+
} from 'react';
|
|
8
|
+
import type { InitializeConfig } from '@rustors/web';
|
|
9
|
+
import { RustoContext } from './RustoContext.js';
|
|
10
|
+
import { defaultOcrService } from '../services/ocrService.js';
|
|
11
|
+
import type {
|
|
12
|
+
RustoContextValue,
|
|
13
|
+
RustoProviderProps,
|
|
14
|
+
RustoStatus,
|
|
15
|
+
} from '../types/index.js';
|
|
16
|
+
|
|
17
|
+
export function RustoProvider({
|
|
18
|
+
children,
|
|
19
|
+
config,
|
|
20
|
+
autoInitialize = true,
|
|
21
|
+
}: RustoProviderProps): ReactElement {
|
|
22
|
+
const [status, setStatus] = useState<RustoStatus>('idle');
|
|
23
|
+
const [error, setError] = useState<Error | null>(null);
|
|
24
|
+
|
|
25
|
+
const initialize = useCallback(
|
|
26
|
+
async (overrideConfig?: InitializeConfig): Promise<void> => {
|
|
27
|
+
try {
|
|
28
|
+
setStatus('initializing');
|
|
29
|
+
setError(null);
|
|
30
|
+
await defaultOcrService.init(overrideConfig || config);
|
|
31
|
+
setStatus('ready');
|
|
32
|
+
} catch (err) {
|
|
33
|
+
const errorObj = err instanceof Error ? err : new Error(String(err));
|
|
34
|
+
setError(errorObj);
|
|
35
|
+
setStatus('error');
|
|
36
|
+
throw errorObj;
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
[config]
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
if (autoInitialize && status === 'idle') {
|
|
44
|
+
initialize().catch(() => {
|
|
45
|
+
// Error state already captured in state
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
}, [autoInitialize, status, initialize]);
|
|
49
|
+
|
|
50
|
+
const value: RustoContextValue = useMemo(
|
|
51
|
+
() => ({
|
|
52
|
+
status,
|
|
53
|
+
isReady: status === 'ready',
|
|
54
|
+
error,
|
|
55
|
+
initialize,
|
|
56
|
+
service: defaultOcrService,
|
|
57
|
+
}),
|
|
58
|
+
[status, error, initialize]
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
return <RustoContext.Provider value={value}>{children}</RustoContext.Provider>;
|
|
62
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { useState, useCallback, useContext } from 'react';
|
|
2
|
+
import type {
|
|
3
|
+
DetectTextOptions,
|
|
4
|
+
ImageSource,
|
|
5
|
+
TextResult,
|
|
6
|
+
} from '@rustors/web';
|
|
7
|
+
import { RustoContext } from '../context/RustoContext.js';
|
|
8
|
+
import { defaultOcrService } from '../services/ocrService.js';
|
|
9
|
+
import type { UseOcrOptions, UseOcrReturn } from '../types/index.js';
|
|
10
|
+
|
|
11
|
+
export function useOcr(options: UseOcrOptions = {}): UseOcrReturn {
|
|
12
|
+
const context = useContext(RustoContext);
|
|
13
|
+
const service = context?.service || defaultOcrService;
|
|
14
|
+
|
|
15
|
+
const [results, setResults] = useState<TextResult[]>([]);
|
|
16
|
+
const [spatialText, setSpatialText] = useState<string>('');
|
|
17
|
+
const [isProcessing, setIsProcessing] = useState<boolean>(false);
|
|
18
|
+
const [error, setError] = useState<Error | null>(null);
|
|
19
|
+
|
|
20
|
+
const reset = useCallback(() => {
|
|
21
|
+
setResults([]);
|
|
22
|
+
setSpatialText('');
|
|
23
|
+
setIsProcessing(false);
|
|
24
|
+
setError(null);
|
|
25
|
+
}, []);
|
|
26
|
+
|
|
27
|
+
const detect = useCallback(
|
|
28
|
+
async (
|
|
29
|
+
source: ImageSource,
|
|
30
|
+
overrideOptions?: DetectTextOptions
|
|
31
|
+
): Promise<TextResult[] | string> => {
|
|
32
|
+
setIsProcessing(true);
|
|
33
|
+
setError(null);
|
|
34
|
+
|
|
35
|
+
const mergedOptions: DetectTextOptions = {
|
|
36
|
+
...options,
|
|
37
|
+
...overrideOptions,
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
const output = await service.detect(source, mergedOptions);
|
|
42
|
+
|
|
43
|
+
if (typeof output === 'string') {
|
|
44
|
+
setSpatialText(output);
|
|
45
|
+
setResults([]);
|
|
46
|
+
} else {
|
|
47
|
+
setResults(output);
|
|
48
|
+
setSpatialText('');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
options.onSuccess?.(output);
|
|
52
|
+
return output;
|
|
53
|
+
} catch (err) {
|
|
54
|
+
const errorObj = err instanceof Error ? err : new Error(String(err));
|
|
55
|
+
setError(errorObj);
|
|
56
|
+
options.onError?.(errorObj);
|
|
57
|
+
throw errorObj;
|
|
58
|
+
} finally {
|
|
59
|
+
setIsProcessing(false);
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
[options, service]
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
return {
|
|
66
|
+
detect,
|
|
67
|
+
results,
|
|
68
|
+
spatialText,
|
|
69
|
+
isProcessing,
|
|
70
|
+
error,
|
|
71
|
+
reset,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { useContext } from 'react';
|
|
2
|
+
import { RustoContext } from '../context/RustoContext.js';
|
|
3
|
+
import type { RustoContextValue } from '../types/index.js';
|
|
4
|
+
|
|
5
|
+
export function useRusto(): RustoContextValue {
|
|
6
|
+
const context = useContext(RustoContext);
|
|
7
|
+
if (!context) {
|
|
8
|
+
throw new Error('useRusto must be used within a RustoProvider.');
|
|
9
|
+
}
|
|
10
|
+
return context;
|
|
11
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// Provider & Context
|
|
2
|
+
export { RustoProvider } from './context/RustoProvider.js';
|
|
3
|
+
export { RustoContext } from './context/RustoContext.js';
|
|
4
|
+
|
|
5
|
+
// Hooks
|
|
6
|
+
export { useRusto } from './hooks/useRusto.js';
|
|
7
|
+
export { useOcr } from './hooks/useOcr.js';
|
|
8
|
+
|
|
9
|
+
// Services
|
|
10
|
+
export { OcrService, defaultOcrService } from './services/ocrService.js';
|
|
11
|
+
|
|
12
|
+
// UI Components
|
|
13
|
+
export { OcrOverlay } from './components/OcrOverlay.js';
|
|
14
|
+
export { BoundingBox } from './components/BoundingBox.js';
|
|
15
|
+
export { OcrDropzone } from './components/OcrDropzone.js';
|
|
16
|
+
export { DropzonePrompt } from './components/DropzonePrompt.js';
|
|
17
|
+
export { DropzonePreview } from './components/DropzonePreview.js';
|
|
18
|
+
|
|
19
|
+
// Geometry & Formatters
|
|
20
|
+
export { scaleFrame, scaleQuad, calculateScale } from './utils/geometry.js';
|
|
21
|
+
export { resultsToText, resultsToCsv, resultsToJson } from './utils/formatters.js';
|
|
22
|
+
|
|
23
|
+
// Types
|
|
24
|
+
export * from './types/index.js';
|
|
25
|
+
|
|
26
|
+
// Re-export common types from @rustors/web
|
|
27
|
+
export type {
|
|
28
|
+
TextResult,
|
|
29
|
+
Frame,
|
|
30
|
+
ImageSource,
|
|
31
|
+
DetectTextOptions,
|
|
32
|
+
InitializeConfig,
|
|
33
|
+
ModelPreset,
|
|
34
|
+
} from '@rustors/web';
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import {
|
|
2
|
+
WebOcrEngine,
|
|
3
|
+
type DetectTextOptions,
|
|
4
|
+
type ImageSource,
|
|
5
|
+
type InitializeConfig,
|
|
6
|
+
type TextResult,
|
|
7
|
+
} from '@rustors/web';
|
|
8
|
+
|
|
9
|
+
export class OcrService {
|
|
10
|
+
private engine: WebOcrEngine;
|
|
11
|
+
|
|
12
|
+
constructor(engineInstance?: WebOcrEngine) {
|
|
13
|
+
this.engine = engineInstance || new WebOcrEngine();
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
public async init(config?: InitializeConfig): Promise<void> {
|
|
17
|
+
await this.engine.initialize(config);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
public isReady(): boolean {
|
|
21
|
+
return this.engine.isInitialized();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
public async detect(
|
|
25
|
+
source: ImageSource,
|
|
26
|
+
options?: DetectTextOptions
|
|
27
|
+
): Promise<TextResult[] | string> {
|
|
28
|
+
if (!this.engine.isInitialized()) {
|
|
29
|
+
throw new Error('OCR Service engine is not ready. Call init() first.');
|
|
30
|
+
}
|
|
31
|
+
return this.engine.detectText(source, options);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
public terminate(): void {
|
|
35
|
+
this.engine.terminate();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
public getEngine(): WebOcrEngine {
|
|
39
|
+
return this.engine;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export const defaultOcrService = new OcrService();
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { CSSProperties, ReactNode } from 'react';
|
|
2
|
+
import type { TextResult, DetectTextOptions } from '@rustors/web';
|
|
3
|
+
|
|
4
|
+
export interface BoundingBoxProps {
|
|
5
|
+
item: TextResult;
|
|
6
|
+
scaleX: number;
|
|
7
|
+
scaleY: number;
|
|
8
|
+
highlightColor?: string;
|
|
9
|
+
showTooltip?: boolean;
|
|
10
|
+
className?: string;
|
|
11
|
+
style?: CSSProperties;
|
|
12
|
+
onClick?: (item: TextResult) => void;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface OcrOverlayProps {
|
|
16
|
+
imageSrc: string;
|
|
17
|
+
results: TextResult[];
|
|
18
|
+
naturalWidth?: number;
|
|
19
|
+
naturalHeight?: number;
|
|
20
|
+
highlightColor?: string;
|
|
21
|
+
showTooltips?: boolean;
|
|
22
|
+
className?: string;
|
|
23
|
+
style?: CSSProperties;
|
|
24
|
+
onItemClick?: (item: TextResult) => void;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface DropzonePromptProps {
|
|
28
|
+
title?: string;
|
|
29
|
+
subtitle?: string;
|
|
30
|
+
icon?: ReactNode;
|
|
31
|
+
className?: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface DropzonePreviewProps {
|
|
35
|
+
previewUrl: string;
|
|
36
|
+
onClear: () => void;
|
|
37
|
+
className?: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface OcrDropzoneProps {
|
|
41
|
+
onFileSelect?: (file: File) => void;
|
|
42
|
+
onDetect?: (results: TextResult[] | string) => void;
|
|
43
|
+
autoDetect?: boolean;
|
|
44
|
+
detectOptions?: DetectTextOptions;
|
|
45
|
+
accept?: string;
|
|
46
|
+
className?: string;
|
|
47
|
+
style?: CSSProperties;
|
|
48
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { InitializeConfig } from '@rustors/web';
|
|
2
|
+
import type { OcrService } from '../services/ocrService.js';
|
|
3
|
+
|
|
4
|
+
export type RustoStatus = 'idle' | 'initializing' | 'ready' | 'error';
|
|
5
|
+
|
|
6
|
+
export interface RustoContextValue {
|
|
7
|
+
status: RustoStatus;
|
|
8
|
+
isReady: boolean;
|
|
9
|
+
error: Error | null;
|
|
10
|
+
initialize: (config?: InitializeConfig) => Promise<void>;
|
|
11
|
+
service: OcrService;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface RustoProviderProps {
|
|
15
|
+
children: React.ReactNode;
|
|
16
|
+
config?: InitializeConfig;
|
|
17
|
+
autoInitialize?: boolean;
|
|
18
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
DetectTextOptions,
|
|
3
|
+
ImageSource,
|
|
4
|
+
TextResult,
|
|
5
|
+
} from '@rustors/web';
|
|
6
|
+
|
|
7
|
+
export interface UseOcrOptions extends DetectTextOptions {
|
|
8
|
+
onSuccess?: (results: TextResult[] | string) => void;
|
|
9
|
+
onError?: (error: Error) => void;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface UseOcrReturn {
|
|
13
|
+
detect: (
|
|
14
|
+
source: ImageSource,
|
|
15
|
+
overrideOptions?: DetectTextOptions
|
|
16
|
+
) => Promise<TextResult[] | string>;
|
|
17
|
+
results: TextResult[];
|
|
18
|
+
spatialText: string;
|
|
19
|
+
isProcessing: boolean;
|
|
20
|
+
error: Error | null;
|
|
21
|
+
reset: () => void;
|
|
22
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { TextResult } from '@rustors/web';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Combines all recognized text tokens into a single text block.
|
|
5
|
+
*/
|
|
6
|
+
export function resultsToText(results: TextResult[]): string {
|
|
7
|
+
return results.map((r) => r.text).join('\n');
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Formats OCR results into standard CSV with bounding box coordinates and scores.
|
|
12
|
+
*/
|
|
13
|
+
export function resultsToCsv(results: TextResult[]): string {
|
|
14
|
+
const header = 'text,score,left,top,width,height';
|
|
15
|
+
const rows = results.map((r) => {
|
|
16
|
+
const escapedText = `"${r.text.replace(/"/g, '""')}"`;
|
|
17
|
+
return `${escapedText},${r.score.toFixed(4)},${r.frame.left},${r.frame.top},${r.frame.width},${r.frame.height}`;
|
|
18
|
+
});
|
|
19
|
+
return [header, ...rows].join('\n');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Pretty formats OCR results as a JSON string.
|
|
24
|
+
*/
|
|
25
|
+
export function resultsToJson(results: TextResult[], indent: number = 2): string {
|
|
26
|
+
return JSON.stringify(results, null, indent);
|
|
27
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { Frame } from '@rustors/web';
|
|
2
|
+
|
|
3
|
+
export type QuadPoints = [[number, number], [number, number], [number, number], [number, number]];
|
|
4
|
+
|
|
5
|
+
export function scaleFrame(frame: Frame, scaleX: number, scaleY: number): Frame {
|
|
6
|
+
return {
|
|
7
|
+
left: Math.round(frame.left * scaleX * 100) / 100,
|
|
8
|
+
top: Math.round(frame.top * scaleY * 100) / 100,
|
|
9
|
+
width: Math.round(frame.width * scaleX * 100) / 100,
|
|
10
|
+
height: Math.round(frame.height * scaleY * 100) / 100,
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function scaleQuad(quad: QuadPoints, scaleX: number, scaleY: number): QuadPoints {
|
|
15
|
+
return [
|
|
16
|
+
[Math.round(quad[0][0] * scaleX * 100) / 100, Math.round(quad[0][1] * scaleY * 100) / 100],
|
|
17
|
+
[Math.round(quad[1][0] * scaleX * 100) / 100, Math.round(quad[1][1] * scaleY * 100) / 100],
|
|
18
|
+
[Math.round(quad[2][0] * scaleX * 100) / 100, Math.round(quad[2][1] * scaleY * 100) / 100],
|
|
19
|
+
[Math.round(quad[3][0] * scaleX * 100) / 100, Math.round(quad[3][1] * scaleY * 100) / 100],
|
|
20
|
+
];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function calculateScale(
|
|
24
|
+
naturalWidth: number,
|
|
25
|
+
naturalHeight: number,
|
|
26
|
+
renderWidth: number,
|
|
27
|
+
renderHeight: number
|
|
28
|
+
): { scaleX: number; scaleY: number } {
|
|
29
|
+
if (naturalWidth <= 0 || naturalHeight <= 0 || renderWidth <= 0 || renderHeight <= 0) {
|
|
30
|
+
return { scaleX: 1, scaleY: 1 };
|
|
31
|
+
}
|
|
32
|
+
return {
|
|
33
|
+
scaleX: renderWidth / naturalWidth,
|
|
34
|
+
scaleY: renderHeight / naturalHeight,
|
|
35
|
+
};
|
|
36
|
+
}
|