react-native-rectangle-doc-scanner 0.65.0 → 0.69.0
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/README.md +97 -168
- package/android/build.gradle +55 -0
- package/android/consumer-rules.pro +1 -0
- package/android/proguard-rules.pro +1 -0
- package/android/src/main/AndroidManifest.xml +11 -0
- package/android/src/main/java/com/reactnativerectangledocscanner/RNRDocScannerModule.kt +37 -0
- package/android/src/main/java/com/reactnativerectangledocscanner/RNRDocScannerPackage.kt +16 -0
- package/android/src/main/java/com/reactnativerectangledocscanner/RNRDocScannerView.kt +129 -0
- package/android/src/main/java/com/reactnativerectangledocscanner/RNRDocScannerViewManager.kt +50 -0
- package/dist/DocScanner.d.ts +12 -7
- package/dist/DocScanner.js +97 -42
- package/dist/FullDocScanner.d.ts +3 -0
- package/dist/FullDocScanner.js +3 -2
- package/dist/index.d.ts +1 -1
- package/dist/utils/overlay.js +77 -48
- package/docs/native-module-architecture.md +178 -0
- package/ios/RNRDocScannerModule.swift +49 -0
- package/ios/RNRDocScannerView.swift +228 -0
- package/ios/RNRDocScannerViewManager.m +21 -0
- package/ios/RNRDocScannerViewManager.swift +47 -0
- package/package.json +6 -5
- package/react-native-rectangle-doc-scanner.podspec +22 -0
- package/src/DocScanner.tsx +153 -76
- package/src/FullDocScanner.tsx +10 -0
- package/src/external.d.ts +12 -45
- package/src/index.ts +1 -1
- package/src/utils/overlay.tsx +83 -54
package/src/FullDocScanner.tsx
CHANGED
|
@@ -56,6 +56,9 @@ export interface FullDocScannerProps {
|
|
|
56
56
|
onClose?: () => void;
|
|
57
57
|
detectionConfig?: DetectionConfig;
|
|
58
58
|
overlayColor?: string;
|
|
59
|
+
gridColor?: string;
|
|
60
|
+
gridLineWidth?: number;
|
|
61
|
+
showGrid?: boolean;
|
|
59
62
|
overlayStrokeColor?: string;
|
|
60
63
|
handlerColor?: string;
|
|
61
64
|
strings?: FullDocScannerStrings;
|
|
@@ -71,6 +74,9 @@ export const FullDocScanner: React.FC<FullDocScannerProps> = ({
|
|
|
71
74
|
onClose,
|
|
72
75
|
detectionConfig,
|
|
73
76
|
overlayColor = '#3170f3',
|
|
77
|
+
gridColor,
|
|
78
|
+
gridLineWidth,
|
|
79
|
+
showGrid,
|
|
74
80
|
overlayStrokeColor = '#3170f3',
|
|
75
81
|
handlerColor = '#3170f3',
|
|
76
82
|
strings,
|
|
@@ -83,6 +89,7 @@ export const FullDocScanner: React.FC<FullDocScannerProps> = ({
|
|
|
83
89
|
const [cropRectangle, setCropRectangle] = useState<Rectangle | null>(null);
|
|
84
90
|
const [imageSize, setImageSize] = useState<{ width: number; height: number } | null>(null);
|
|
85
91
|
const [processing, setProcessing] = useState(false);
|
|
92
|
+
const resolvedGridColor = gridColor ?? overlayColor;
|
|
86
93
|
|
|
87
94
|
const mergedStrings = useMemo<Required<FullDocScannerStrings>>(
|
|
88
95
|
() => ({
|
|
@@ -239,6 +246,9 @@ export const FullDocScanner: React.FC<FullDocScannerProps> = ({
|
|
|
239
246
|
<DocScanner
|
|
240
247
|
autoCapture={!manualCapture}
|
|
241
248
|
overlayColor={overlayColor}
|
|
249
|
+
showGrid={showGrid}
|
|
250
|
+
gridColor={resolvedGridColor}
|
|
251
|
+
gridLineWidth={gridLineWidth}
|
|
242
252
|
minStableFrames={minStableFrames ?? 6}
|
|
243
253
|
detectionConfig={detectionConfig}
|
|
244
254
|
onCapture={handleCapture}
|
package/src/external.d.ts
CHANGED
|
@@ -1,48 +1,3 @@
|
|
|
1
|
-
declare module 'react-native-document-scanner-plugin' {
|
|
2
|
-
import type { ComponentType } from 'react';
|
|
3
|
-
import type { ViewStyle } from 'react-native';
|
|
4
|
-
|
|
5
|
-
export type RectangleCoordinates = {
|
|
6
|
-
topLeft: { x: number; y: number };
|
|
7
|
-
topRight: { x: number; y: number };
|
|
8
|
-
bottomRight: { x: number; y: number };
|
|
9
|
-
bottomLeft: { x: number; y: number };
|
|
10
|
-
};
|
|
11
|
-
|
|
12
|
-
export type RectangleEvent = {
|
|
13
|
-
rectangleCoordinates?: RectangleCoordinates;
|
|
14
|
-
stableCounter?: number;
|
|
15
|
-
lastDetectionType?: 'initial' | 'updated' | 'lost';
|
|
16
|
-
};
|
|
17
|
-
|
|
18
|
-
export type CaptureResult = {
|
|
19
|
-
croppedImage?: string;
|
|
20
|
-
initialImage?: string;
|
|
21
|
-
width?: number;
|
|
22
|
-
height?: number;
|
|
23
|
-
};
|
|
24
|
-
|
|
25
|
-
export type DocumentScannerProps = {
|
|
26
|
-
ref?: (value: DocumentScannerHandle | null) => void;
|
|
27
|
-
style?: ViewStyle;
|
|
28
|
-
overlayColor?: string;
|
|
29
|
-
detectionCountBeforeCapture?: number;
|
|
30
|
-
enableTorch?: boolean;
|
|
31
|
-
hideControls?: boolean;
|
|
32
|
-
useBase64?: boolean;
|
|
33
|
-
quality?: number;
|
|
34
|
-
onRectangleDetect?: (event: RectangleEvent) => void;
|
|
35
|
-
onPictureTaken?: (result: CaptureResult) => void;
|
|
36
|
-
};
|
|
37
|
-
|
|
38
|
-
export type DocumentScannerHandle = {
|
|
39
|
-
capture: () => Promise<CaptureResult>;
|
|
40
|
-
};
|
|
41
|
-
|
|
42
|
-
const DocumentScanner: ComponentType<DocumentScannerProps>;
|
|
43
|
-
export default DocumentScanner;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
1
|
declare module '@shopify/react-native-skia' {
|
|
47
2
|
import type { ComponentType, ReactNode } from 'react';
|
|
48
3
|
import type { ViewStyle } from 'react-native';
|
|
@@ -76,6 +31,18 @@ declare module '@shopify/react-native-skia' {
|
|
|
76
31
|
export const Path: ComponentType<PathProps>;
|
|
77
32
|
}
|
|
78
33
|
|
|
34
|
+
declare module 'react-native-rectangle-doc-scanner/RNRDocScannerModule' {
|
|
35
|
+
export type NativeCaptureResult = {
|
|
36
|
+
croppedImage?: string | null;
|
|
37
|
+
initialImage?: string;
|
|
38
|
+
width?: number;
|
|
39
|
+
height?: number;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export function capture(viewTag: number): Promise<NativeCaptureResult>;
|
|
43
|
+
export function reset(viewTag: number): void;
|
|
44
|
+
}
|
|
45
|
+
|
|
79
46
|
declare module 'react-native-perspective-image-cropper' {
|
|
80
47
|
import type { ComponentType } from 'react';
|
|
81
48
|
|
package/src/index.ts
CHANGED
|
@@ -11,7 +11,7 @@ export type {
|
|
|
11
11
|
|
|
12
12
|
// Types
|
|
13
13
|
export type { Point, Quad, Rectangle, CapturedDocument } from './types';
|
|
14
|
-
export type { DetectionConfig } from './DocScanner';
|
|
14
|
+
export type { DetectionConfig, DocScannerHandle } from './DocScanner';
|
|
15
15
|
|
|
16
16
|
// Utilities
|
|
17
17
|
export {
|
package/src/utils/overlay.tsx
CHANGED
|
@@ -8,6 +8,23 @@ const lerp = (start: Point, end: Point, t: number): Point => ({
|
|
|
8
8
|
y: start.y + (end.y - start.y) * t,
|
|
9
9
|
});
|
|
10
10
|
|
|
11
|
+
const withAlpha = (value: string, alpha: number): string => {
|
|
12
|
+
const hexMatch = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i.exec(value.trim());
|
|
13
|
+
if (!hexMatch) {
|
|
14
|
+
return `rgba(231, 166, 73, ${alpha})`;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const hex = hexMatch[1];
|
|
18
|
+
const normalize = hex.length === 3
|
|
19
|
+
? hex.split('').map((ch) => ch + ch).join('')
|
|
20
|
+
: hex;
|
|
21
|
+
|
|
22
|
+
const r = parseInt(normalize.slice(0, 2), 16);
|
|
23
|
+
const g = parseInt(normalize.slice(2, 4), 16);
|
|
24
|
+
const b = parseInt(normalize.slice(4, 6), 16);
|
|
25
|
+
return `rgba(${r}, ${g}, ${b}, ${alpha})`;
|
|
26
|
+
};
|
|
27
|
+
|
|
11
28
|
type OverlayProps = {
|
|
12
29
|
quad: Point[] | null;
|
|
13
30
|
color?: string;
|
|
@@ -22,6 +39,14 @@ type OverlayGeometry = {
|
|
|
22
39
|
gridPaths: ReturnType<typeof Skia.Path.Make>[];
|
|
23
40
|
};
|
|
24
41
|
|
|
42
|
+
const buildPath = (points: Point[]) => {
|
|
43
|
+
const path = Skia.Path.Make();
|
|
44
|
+
path.moveTo(points[0].x, points[0].y);
|
|
45
|
+
points.slice(1).forEach((p) => path.lineTo(p.x, p.y));
|
|
46
|
+
path.close();
|
|
47
|
+
return path;
|
|
48
|
+
};
|
|
49
|
+
|
|
25
50
|
export const Overlay: React.FC<OverlayProps> = ({
|
|
26
51
|
quad,
|
|
27
52
|
color = '#e7a649',
|
|
@@ -31,71 +56,75 @@ export const Overlay: React.FC<OverlayProps> = ({
|
|
|
31
56
|
gridLineWidth = 2,
|
|
32
57
|
}) => {
|
|
33
58
|
const { width: screenWidth, height: screenHeight } = useWindowDimensions();
|
|
59
|
+
const fillColor = useMemo(() => withAlpha(color, 0.2), [color]);
|
|
34
60
|
|
|
35
61
|
const { outlinePath, gridPaths }: OverlayGeometry = useMemo(() => {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
}
|
|
40
|
-
return { outlinePath: null, gridPaths: [] };
|
|
41
|
-
}
|
|
62
|
+
let transformedQuad: Point[] | null = null;
|
|
63
|
+
let sourceQuad: Point[] | null = null;
|
|
64
|
+
let sourceFrameSize = frameSize;
|
|
42
65
|
|
|
43
|
-
if (
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
66
|
+
if (quad && frameSize) {
|
|
67
|
+
sourceQuad = quad;
|
|
68
|
+
} else {
|
|
69
|
+
const marginRatio = 0.12;
|
|
70
|
+
const marginX = screenWidth * marginRatio;
|
|
71
|
+
const marginY = screenHeight * marginRatio;
|
|
72
|
+
const maxWidth = screenWidth - marginX * 2;
|
|
73
|
+
const maxHeight = screenHeight - marginY * 2;
|
|
74
|
+
const a4Ratio = Math.SQRT2; // ~1.414 height / width
|
|
75
|
+
let width = maxWidth;
|
|
76
|
+
let height = width * a4Ratio;
|
|
77
|
+
if (height > maxHeight) {
|
|
78
|
+
height = maxHeight;
|
|
79
|
+
width = height / a4Ratio;
|
|
80
|
+
}
|
|
81
|
+
const left = (screenWidth - width) / 2;
|
|
82
|
+
const top = (screenHeight - height) / 2;
|
|
83
|
+
transformedQuad = [
|
|
84
|
+
{ x: left, y: top },
|
|
85
|
+
{ x: left + width, y: top },
|
|
86
|
+
{ x: left + width, y: top + height },
|
|
87
|
+
{ x: left, y: top + height },
|
|
88
|
+
];
|
|
89
|
+
sourceFrameSize = null;
|
|
57
90
|
}
|
|
58
91
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
if (needsRotation) {
|
|
62
|
-
// Camera is landscape, screen is portrait - need to rotate 90 degrees
|
|
63
|
-
// Transform: rotate 90° clockwise and scale
|
|
64
|
-
// New coordinates: x' = y * (screenWidth / frameHeight), y' = (frameWidth - x) * (screenHeight / frameWidth)
|
|
65
|
-
const scaleX = screenWidth / frameSize.height;
|
|
66
|
-
const scaleY = screenHeight / frameSize.width;
|
|
67
|
-
|
|
92
|
+
if (sourceQuad && sourceFrameSize) {
|
|
68
93
|
if (__DEV__) {
|
|
69
|
-
console.log('[Overlay]
|
|
94
|
+
console.log('[Overlay] drawing quad:', sourceQuad);
|
|
95
|
+
console.log('[Overlay] color:', color);
|
|
96
|
+
console.log('[Overlay] screen dimensions:', screenWidth, 'x', screenHeight);
|
|
97
|
+
console.log('[Overlay] frame dimensions:', sourceFrameSize.width, 'x', sourceFrameSize.height);
|
|
70
98
|
}
|
|
71
99
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
100
|
+
const isFrameLandscape = sourceFrameSize.width > sourceFrameSize.height;
|
|
101
|
+
const isScreenPortrait = screenHeight > screenWidth;
|
|
102
|
+
const needsRotation = isFrameLandscape && isScreenPortrait;
|
|
103
|
+
|
|
104
|
+
if (needsRotation) {
|
|
105
|
+
const scaleX = screenWidth / sourceFrameSize.height;
|
|
106
|
+
const scaleY = screenHeight / sourceFrameSize.width;
|
|
107
|
+
|
|
108
|
+
transformedQuad = sourceQuad.map((p) => ({
|
|
109
|
+
x: p.y * scaleX,
|
|
110
|
+
y: (sourceFrameSize.width - p.x) * scaleY,
|
|
111
|
+
}));
|
|
112
|
+
} else {
|
|
113
|
+
const scaleX = screenWidth / sourceFrameSize.width;
|
|
114
|
+
const scaleY = screenHeight / sourceFrameSize.height;
|
|
115
|
+
|
|
116
|
+
transformedQuad = sourceQuad.map((p) => ({
|
|
117
|
+
x: p.x * scaleX,
|
|
118
|
+
y: p.y * scaleY,
|
|
119
|
+
}));
|
|
83
120
|
}
|
|
84
|
-
|
|
85
|
-
transformedQuad = quad.map((p) => ({
|
|
86
|
-
x: p.x * scaleX,
|
|
87
|
-
y: p.y * scaleY,
|
|
88
|
-
}));
|
|
89
121
|
}
|
|
90
122
|
|
|
91
|
-
if (
|
|
92
|
-
|
|
123
|
+
if (!transformedQuad) {
|
|
124
|
+
return { outlinePath: null, gridPaths: [] };
|
|
93
125
|
}
|
|
94
126
|
|
|
95
|
-
const skPath =
|
|
96
|
-
skPath.moveTo(transformedQuad[0].x, transformedQuad[0].y);
|
|
97
|
-
transformedQuad.slice(1).forEach((p) => skPath.lineTo(p.x, p.y));
|
|
98
|
-
skPath.close();
|
|
127
|
+
const skPath = buildPath(transformedQuad);
|
|
99
128
|
const grid: ReturnType<typeof Skia.Path.Make>[] = [];
|
|
100
129
|
|
|
101
130
|
if (showGrid) {
|
|
@@ -122,7 +151,7 @@ export const Overlay: React.FC<OverlayProps> = ({
|
|
|
122
151
|
}
|
|
123
152
|
|
|
124
153
|
return { outlinePath: skPath, gridPaths: grid };
|
|
125
|
-
}, [quad, screenWidth, screenHeight, frameSize, showGrid]);
|
|
154
|
+
}, [quad, screenWidth, screenHeight, frameSize, showGrid, color]);
|
|
126
155
|
|
|
127
156
|
if (__DEV__) {
|
|
128
157
|
console.log('[Overlay] rendering Canvas with dimensions:', screenWidth, 'x', screenHeight);
|
|
@@ -134,7 +163,7 @@ export const Overlay: React.FC<OverlayProps> = ({
|
|
|
134
163
|
{outlinePath && (
|
|
135
164
|
<>
|
|
136
165
|
<Path path={outlinePath} color={color} style="stroke" strokeWidth={8} />
|
|
137
|
-
<Path path={outlinePath} color=
|
|
166
|
+
<Path path={outlinePath} color={fillColor} style="fill" />
|
|
138
167
|
{gridPaths.map((gridPath, index) => (
|
|
139
168
|
<Path
|
|
140
169
|
// eslint-disable-next-line react/no-array-index-key
|