react-native-rectangle-doc-scanner 3.38.0 → 3.39.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.
@@ -58,17 +58,25 @@ const CropEditor = ({ document, overlayColor = 'rgba(0,0,0,0.5)', overlayStrokeC
58
58
  });
59
59
  const [isImageLoading, setIsImageLoading] = (0, react_1.useState)(true);
60
60
  const [loadError, setLoadError] = (0, react_1.useState)(null);
61
+ const [croppedImageUri, setCroppedImageUri] = (0, react_1.useState)(null);
61
62
  (0, react_1.useEffect)(() => {
62
63
  console.log('[CropEditor] Document path:', document.path);
63
64
  console.log('[CropEditor] Document dimensions:', document.width, 'x', document.height);
64
65
  console.log('[CropEditor] Document quad:', document.quad);
66
+ console.log('[CropEditor] Document rectangle:', document.rectangle);
65
67
  // Load image size using Image.getSize
66
- const imageUri = `file://${document.path}`;
68
+ const imageUri = document.path.startsWith('file://') ? document.path : `file://${document.path}`;
67
69
  react_native_1.Image.getSize(imageUri, (width, height) => {
68
70
  console.log('[CropEditor] Image.getSize success:', { width, height });
69
71
  setImageSize({ width, height });
70
- setIsImageLoading(false);
71
- setLoadError(null);
72
+ // If we have a rectangle (from auto-capture), crop the image
73
+ if (document.rectangle || document.quad) {
74
+ cropImageToRectangle(imageUri, width, height);
75
+ }
76
+ else {
77
+ setIsImageLoading(false);
78
+ setLoadError(null);
79
+ }
72
80
  }, (error) => {
73
81
  console.error('[CropEditor] Image.getSize error:', error);
74
82
  // Fallback to document dimensions
@@ -77,6 +85,48 @@ const CropEditor = ({ document, overlayColor = 'rgba(0,0,0,0.5)', overlayStrokeC
77
85
  setIsImageLoading(false);
78
86
  });
79
87
  }, [document]);
88
+ const cropImageToRectangle = (0, react_1.useCallback)((imageUri, width, height) => {
89
+ const cropManager = react_native_1.NativeModules.CustomCropManager;
90
+ if (!cropManager?.crop) {
91
+ console.warn('[CropEditor] CustomCropManager not available, showing original image');
92
+ setIsImageLoading(false);
93
+ return;
94
+ }
95
+ const baseWidth = document.width > 0 ? document.width : width;
96
+ const baseHeight = document.height > 0 ? document.height : height;
97
+ let rectangle = document.rectangle ?? null;
98
+ if (!rectangle && document.quad && document.quad.length === 4) {
99
+ rectangle = (0, coordinate_1.quadToRectangle)(document.quad);
100
+ }
101
+ if (!rectangle) {
102
+ console.warn('[CropEditor] No rectangle found, showing original image');
103
+ setIsImageLoading(false);
104
+ return;
105
+ }
106
+ // Scale rectangle to actual image size
107
+ const scaledRect = (0, coordinate_1.scaleRectangle)(rectangle, baseWidth, baseHeight, width, height);
108
+ console.log('[CropEditor] Cropping image with rectangle:', scaledRect);
109
+ cropManager.crop({
110
+ topLeft: scaledRect.topLeft,
111
+ topRight: scaledRect.topRight,
112
+ bottomRight: scaledRect.bottomRight,
113
+ bottomLeft: scaledRect.bottomLeft,
114
+ width,
115
+ height,
116
+ }, imageUri, (error, result) => {
117
+ if (error) {
118
+ console.error('[CropEditor] Crop error:', error);
119
+ setIsImageLoading(false);
120
+ return;
121
+ }
122
+ console.log('[CropEditor] Crop success, base64 length:', result.image?.length);
123
+ // Convert base64 to data URI
124
+ const croppedUri = `data:image/jpeg;base64,${result.image}`;
125
+ setCroppedImageUri(croppedUri);
126
+ setIsImageLoading(false);
127
+ setLoadError(null);
128
+ });
129
+ }, [document]);
80
130
  // Get initial rectangle from detected quad or use default
81
131
  const getInitialRectangle = (0, react_1.useCallback)(() => {
82
132
  if (!imageSize) {
@@ -131,7 +181,7 @@ const CropEditor = ({ document, overlayColor = 'rgba(0,0,0,0.5)', overlayStrokeC
131
181
  react_1.default.createElement(react_native_1.Text, { style: styles.errorPath }, imageUri))) : !imageSize || isImageLoading ? (react_1.default.createElement(react_native_1.View, { style: styles.loadingContainer },
132
182
  react_1.default.createElement(react_native_1.ActivityIndicator, { size: "large", color: handlerColor }),
133
183
  react_1.default.createElement(react_native_1.Text, { style: styles.loadingText }, "Loading image..."))) : (react_1.default.createElement(react_1.default.Fragment, null,
134
- react_1.default.createElement(react_native_1.Image, { source: { uri: imageUri }, style: styles.fullImage, resizeMode: "contain", onLoad: () => console.log('[CropEditor] Image loaded successfully'), onError: (e) => console.error('[CropEditor] Image load error:', e.nativeEvent.error) })))));
184
+ react_1.default.createElement(react_native_1.Image, { source: { uri: croppedImageUri || imageUri }, style: styles.fullImage, resizeMode: "contain", onLoad: () => console.log('[CropEditor] Image loaded successfully', croppedImageUri ? 'cropped' : 'original'), onError: (e) => console.error('[CropEditor] Image load error:', e.nativeEvent.error) })))));
135
185
  };
136
186
  exports.CropEditor = CropEditor;
137
187
  const styles = react_native_1.StyleSheet.create({
@@ -224,25 +224,28 @@ const FullDocScanner = ({ onResult, onClose, detectionConfig, overlayColor = '#3
224
224
  console.log('[FullDocScanner] Already processing, skipping manual capture');
225
225
  return;
226
226
  }
227
- // Reset DocScanner state before capturing
228
- docScannerRef.current?.reset();
227
+ if (manualCapturePending.current) {
228
+ console.log('[FullDocScanner] Manual capture already pending, skipping');
229
+ return;
230
+ }
229
231
  console.log('[FullDocScanner] Setting manualCapturePending to true');
230
232
  manualCapturePending.current = true;
231
- // Small delay to ensure reset completes
232
- setTimeout(() => {
233
- const capturePromise = docScannerRef.current?.capture();
234
- console.log('[FullDocScanner] capturePromise:', !!capturePromise);
235
- if (capturePromise && typeof capturePromise.catch === 'function') {
236
- capturePromise.catch((error) => {
237
- manualCapturePending.current = false;
238
- console.warn('[FullDocScanner] manual capture failed', error);
239
- });
240
- }
241
- else if (!capturePromise) {
242
- console.warn('[FullDocScanner] No capture promise returned');
233
+ const capturePromise = docScannerRef.current?.capture();
234
+ console.log('[FullDocScanner] capturePromise:', !!capturePromise);
235
+ if (capturePromise && typeof capturePromise.then === 'function') {
236
+ capturePromise
237
+ .then(() => {
238
+ console.log('[FullDocScanner] Capture success');
239
+ })
240
+ .catch((error) => {
243
241
  manualCapturePending.current = false;
244
- }
245
- }, 100);
242
+ console.warn('[FullDocScanner] manual capture failed', error);
243
+ });
244
+ }
245
+ else if (!capturePromise) {
246
+ console.warn('[FullDocScanner] No capture promise returned');
247
+ manualCapturePending.current = false;
248
+ }
246
249
  }, []);
247
250
  const performCrop = (0, react_1.useCallback)(async () => {
248
251
  if (!capturedDoc) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-rectangle-doc-scanner",
3
- "version": "3.38.0",
3
+ "version": "3.39.0",
4
4
  "description": "Native-backed document scanner for React Native with customizable overlays.",
5
5
  "license": "MIT",
6
6
  "main": "dist/index.js",
@@ -1,10 +1,25 @@
1
1
  import React, { useState, useCallback, useEffect } from 'react';
2
- import { View, StyleSheet, Image, Dimensions, ActivityIndicator, Text } from 'react-native';
2
+ import { View, StyleSheet, Image, Dimensions, ActivityIndicator, Text, NativeModules } from 'react-native';
3
3
  import CustomImageCropper from 'react-native-perspective-image-cropper';
4
4
  import type { Rectangle as CropperRectangle } from 'react-native-perspective-image-cropper';
5
5
  import type { Point, Rectangle, CapturedDocument } from './types';
6
6
  import { createFullImageRectangle, quadToRectangle, scaleRectangle } from './utils/coordinate';
7
7
 
8
+ type CustomCropManagerType = {
9
+ crop: (
10
+ points: {
11
+ topLeft: Point;
12
+ topRight: Point;
13
+ bottomRight: Point;
14
+ bottomLeft: Point;
15
+ width: number;
16
+ height: number;
17
+ },
18
+ imageUri: string,
19
+ callback: (error: unknown, result: { image: string }) => void,
20
+ ) => void;
21
+ };
22
+
8
23
  interface CropEditorProps {
9
24
  document: CapturedDocument;
10
25
  overlayColor?: string;
@@ -42,21 +57,29 @@ export const CropEditor: React.FC<CropEditorProps> = ({
42
57
  });
43
58
  const [isImageLoading, setIsImageLoading] = useState(true);
44
59
  const [loadError, setLoadError] = useState<string | null>(null);
60
+ const [croppedImageUri, setCroppedImageUri] = useState<string | null>(null);
45
61
 
46
62
  useEffect(() => {
47
63
  console.log('[CropEditor] Document path:', document.path);
48
64
  console.log('[CropEditor] Document dimensions:', document.width, 'x', document.height);
49
65
  console.log('[CropEditor] Document quad:', document.quad);
66
+ console.log('[CropEditor] Document rectangle:', document.rectangle);
50
67
 
51
68
  // Load image size using Image.getSize
52
- const imageUri = `file://${document.path}`;
69
+ const imageUri = document.path.startsWith('file://') ? document.path : `file://${document.path}`;
53
70
  Image.getSize(
54
71
  imageUri,
55
72
  (width, height) => {
56
73
  console.log('[CropEditor] Image.getSize success:', { width, height });
57
74
  setImageSize({ width, height });
58
- setIsImageLoading(false);
59
- setLoadError(null);
75
+
76
+ // If we have a rectangle (from auto-capture), crop the image
77
+ if (document.rectangle || document.quad) {
78
+ cropImageToRectangle(imageUri, width, height);
79
+ } else {
80
+ setIsImageLoading(false);
81
+ setLoadError(null);
82
+ }
60
83
  },
61
84
  (error) => {
62
85
  console.error('[CropEditor] Image.getSize error:', error);
@@ -68,6 +91,60 @@ export const CropEditor: React.FC<CropEditorProps> = ({
68
91
  );
69
92
  }, [document]);
70
93
 
94
+ const cropImageToRectangle = useCallback((imageUri: string, width: number, height: number) => {
95
+ const cropManager = NativeModules.CustomCropManager as CustomCropManagerType | undefined;
96
+
97
+ if (!cropManager?.crop) {
98
+ console.warn('[CropEditor] CustomCropManager not available, showing original image');
99
+ setIsImageLoading(false);
100
+ return;
101
+ }
102
+
103
+ const baseWidth = document.width > 0 ? document.width : width;
104
+ const baseHeight = document.height > 0 ? document.height : height;
105
+
106
+ let rectangle: Rectangle | null = document.rectangle ?? null;
107
+ if (!rectangle && document.quad && document.quad.length === 4) {
108
+ rectangle = quadToRectangle(document.quad);
109
+ }
110
+
111
+ if (!rectangle) {
112
+ console.warn('[CropEditor] No rectangle found, showing original image');
113
+ setIsImageLoading(false);
114
+ return;
115
+ }
116
+
117
+ // Scale rectangle to actual image size
118
+ const scaledRect = scaleRectangle(rectangle, baseWidth, baseHeight, width, height);
119
+
120
+ console.log('[CropEditor] Cropping image with rectangle:', scaledRect);
121
+
122
+ cropManager.crop(
123
+ {
124
+ topLeft: scaledRect.topLeft,
125
+ topRight: scaledRect.topRight,
126
+ bottomRight: scaledRect.bottomRight,
127
+ bottomLeft: scaledRect.bottomLeft,
128
+ width,
129
+ height,
130
+ },
131
+ imageUri,
132
+ (error: unknown, result: { image: string }) => {
133
+ if (error) {
134
+ console.error('[CropEditor] Crop error:', error);
135
+ setIsImageLoading(false);
136
+ return;
137
+ }
138
+ console.log('[CropEditor] Crop success, base64 length:', result.image?.length);
139
+ // Convert base64 to data URI
140
+ const croppedUri = `data:image/jpeg;base64,${result.image}`;
141
+ setCroppedImageUri(croppedUri);
142
+ setIsImageLoading(false);
143
+ setLoadError(null);
144
+ }
145
+ );
146
+ }, [document]);
147
+
71
148
  // Get initial rectangle from detected quad or use default
72
149
  const getInitialRectangle = useCallback((): CropperRectangle | undefined => {
73
150
  if (!imageSize) {
@@ -151,12 +228,12 @@ export const CropEditor: React.FC<CropEditorProps> = ({
151
228
  </View>
152
229
  ) : (
153
230
  <>
154
- {/* Full screen image */}
231
+ {/* Show cropped image if available, otherwise show original */}
155
232
  <Image
156
- source={{ uri: imageUri }}
233
+ source={{ uri: croppedImageUri || imageUri }}
157
234
  style={styles.fullImage}
158
235
  resizeMode="contain"
159
- onLoad={() => console.log('[CropEditor] Image loaded successfully')}
236
+ onLoad={() => console.log('[CropEditor] Image loaded successfully', croppedImageUri ? 'cropped' : 'original')}
160
237
  onError={(e) => console.error('[CropEditor] Image load error:', e.nativeEvent.error)}
161
238
  />
162
239
  {/* Temporarily disabled CustomImageCropper - showing image only */}
@@ -347,27 +347,29 @@ export const FullDocScanner: React.FC<FullDocScannerProps> = ({
347
347
  console.log('[FullDocScanner] Already processing, skipping manual capture');
348
348
  return;
349
349
  }
350
-
351
- // Reset DocScanner state before capturing
352
- docScannerRef.current?.reset();
350
+ if (manualCapturePending.current) {
351
+ console.log('[FullDocScanner] Manual capture already pending, skipping');
352
+ return;
353
+ }
353
354
 
354
355
  console.log('[FullDocScanner] Setting manualCapturePending to true');
355
356
  manualCapturePending.current = true;
356
357
 
357
- // Small delay to ensure reset completes
358
- setTimeout(() => {
359
- const capturePromise = docScannerRef.current?.capture();
360
- console.log('[FullDocScanner] capturePromise:', !!capturePromise);
361
- if (capturePromise && typeof capturePromise.catch === 'function') {
362
- capturePromise.catch((error: unknown) => {
358
+ const capturePromise = docScannerRef.current?.capture();
359
+ console.log('[FullDocScanner] capturePromise:', !!capturePromise);
360
+ if (capturePromise && typeof capturePromise.then === 'function') {
361
+ capturePromise
362
+ .then(() => {
363
+ console.log('[FullDocScanner] Capture success');
364
+ })
365
+ .catch((error: unknown) => {
363
366
  manualCapturePending.current = false;
364
367
  console.warn('[FullDocScanner] manual capture failed', error);
365
368
  });
366
- } else if (!capturePromise) {
367
- console.warn('[FullDocScanner] No capture promise returned');
368
- manualCapturePending.current = false;
369
- }
370
- }, 100);
369
+ } else if (!capturePromise) {
370
+ console.warn('[FullDocScanner] No capture promise returned');
371
+ manualCapturePending.current = false;
372
+ }
371
373
  }, []);
372
374
 
373
375
  const performCrop = useCallback(async (): Promise<{ base64: string; rectangle: Rectangle }> => {