react-native-rectangle-doc-scanner 13.8.0 → 13.11.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/android/src/common/kotlin/com/reactnativerectangledocscanner/DocumentDetector.kt +7 -5
- package/dist/CropEditor.d.ts +2 -0
- package/dist/CropEditor.js +33 -28
- package/dist/FullDocScanner.js +109 -1
- package/package.json +1 -1
- package/src/CropEditor.tsx +50 -44
- package/src/FullDocScanner.tsx +162 -2
|
@@ -262,10 +262,10 @@ class DocumentDetector {
|
|
|
262
262
|
var largestRectangle: Rectangle? = null
|
|
263
263
|
var bestScore = 0.0
|
|
264
264
|
val imageArea = (srcMat.rows() * srcMat.cols()).toDouble()
|
|
265
|
-
// Min:
|
|
266
|
-
// Max:
|
|
267
|
-
val minArea = max(350.0, imageArea * 0.
|
|
268
|
-
val maxArea = imageArea * 0.
|
|
265
|
+
// Min: 5% of image (documents should be reasonably sized)
|
|
266
|
+
// Max: 70% of image (avoid detecting screen borders)
|
|
267
|
+
val minArea = max(350.0, imageArea * 0.05)
|
|
268
|
+
val maxArea = imageArea * 0.70
|
|
269
269
|
|
|
270
270
|
debugStats.contours = contours.size
|
|
271
271
|
|
|
@@ -439,8 +439,10 @@ class DocumentDetector {
|
|
|
439
439
|
if (width < minEdge || height < minEdge) {
|
|
440
440
|
return false
|
|
441
441
|
}
|
|
442
|
+
// Stricter aspect ratio for typical documents (business cards, A4, etc.)
|
|
443
|
+
// Business cards: ~1.6, A4: ~0.7, allow range 0.5-2.2
|
|
442
444
|
val aspect = if (height > 0) width / height else 0.0
|
|
443
|
-
if (aspect < 0.
|
|
445
|
+
if (aspect < 0.5 || aspect > 2.2) {
|
|
444
446
|
return false
|
|
445
447
|
}
|
|
446
448
|
|
package/dist/CropEditor.d.ts
CHANGED
package/dist/CropEditor.js
CHANGED
|
@@ -32,10 +32,14 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
32
32
|
return result;
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
35
38
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
39
|
exports.CropEditor = void 0;
|
|
37
40
|
const react_1 = __importStar(require("react"));
|
|
38
41
|
const react_native_1 = require("react-native");
|
|
42
|
+
const react_native_perspective_image_cropper_1 = __importDefault(require("react-native-perspective-image-cropper"));
|
|
39
43
|
const coordinate_1 = require("./utils/coordinate");
|
|
40
44
|
/**
|
|
41
45
|
* CropEditor Component
|
|
@@ -50,7 +54,7 @@ const coordinate_1 = require("./utils/coordinate");
|
|
|
50
54
|
* @param enablePanStrict - Enable strict panning behavior
|
|
51
55
|
* @param onCropChange - Callback when user adjusts crop corners
|
|
52
56
|
*/
|
|
53
|
-
const CropEditor = ({ document, overlayColor = 'rgba(0,0,0,0.5)', overlayStrokeColor = '#e7a649', handlerColor = '#e7a649', enablePanStrict = false, onCropChange, }) => {
|
|
57
|
+
const CropEditor = ({ document, overlayColor = 'rgba(0,0,0,0.5)', overlayStrokeColor = '#e7a649', handlerColor = '#e7a649', enablePanStrict = false, enableEditor = false, autoCrop = true, onCropChange, }) => {
|
|
54
58
|
const [imageSize, setImageSize] = (0, react_1.useState)(null);
|
|
55
59
|
const [displaySize, setDisplaySize] = (0, react_1.useState)({
|
|
56
60
|
width: react_native_1.Dimensions.get('window').width,
|
|
@@ -59,32 +63,6 @@ const CropEditor = ({ document, overlayColor = 'rgba(0,0,0,0.5)', overlayStrokeC
|
|
|
59
63
|
const [isImageLoading, setIsImageLoading] = (0, react_1.useState)(true);
|
|
60
64
|
const [loadError, setLoadError] = (0, react_1.useState)(null);
|
|
61
65
|
const [croppedImageUri, setCroppedImageUri] = (0, react_1.useState)(null);
|
|
62
|
-
(0, react_1.useEffect)(() => {
|
|
63
|
-
console.log('[CropEditor] Document path:', document.path);
|
|
64
|
-
console.log('[CropEditor] Document dimensions:', document.width, 'x', document.height);
|
|
65
|
-
console.log('[CropEditor] Document quad:', document.quad);
|
|
66
|
-
console.log('[CropEditor] Document rectangle:', document.rectangle);
|
|
67
|
-
// Load image size using Image.getSize
|
|
68
|
-
const imageUri = document.path.startsWith('file://') ? document.path : `file://${document.path}`;
|
|
69
|
-
react_native_1.Image.getSize(imageUri, (width, height) => {
|
|
70
|
-
console.log('[CropEditor] Image.getSize success:', { width, height });
|
|
71
|
-
setImageSize({ width, height });
|
|
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
|
-
}
|
|
80
|
-
}, (error) => {
|
|
81
|
-
console.error('[CropEditor] Image.getSize error:', error);
|
|
82
|
-
// Fallback to document dimensions
|
|
83
|
-
console.log('[CropEditor] Using fallback dimensions:', document.width, 'x', document.height);
|
|
84
|
-
setImageSize({ width: document.width, height: document.height });
|
|
85
|
-
setIsImageLoading(false);
|
|
86
|
-
});
|
|
87
|
-
}, [document]);
|
|
88
66
|
const cropImageToRectangle = (0, react_1.useCallback)((imageUri, width, height) => {
|
|
89
67
|
const cropManager = react_native_1.NativeModules.CustomCropManager;
|
|
90
68
|
if (!cropManager?.crop) {
|
|
@@ -127,6 +105,33 @@ const CropEditor = ({ document, overlayColor = 'rgba(0,0,0,0.5)', overlayStrokeC
|
|
|
127
105
|
setLoadError(null);
|
|
128
106
|
});
|
|
129
107
|
}, [document]);
|
|
108
|
+
(0, react_1.useEffect)(() => {
|
|
109
|
+
console.log('[CropEditor] Document path:', document.path);
|
|
110
|
+
console.log('[CropEditor] Document dimensions:', document.width, 'x', document.height);
|
|
111
|
+
console.log('[CropEditor] Document quad:', document.quad);
|
|
112
|
+
console.log('[CropEditor] Document rectangle:', document.rectangle);
|
|
113
|
+
const shouldAutoCrop = autoCrop && !enableEditor;
|
|
114
|
+
// Load image size using Image.getSize
|
|
115
|
+
const imageUri = document.path.startsWith('file://') ? document.path : `file://${document.path}`;
|
|
116
|
+
react_native_1.Image.getSize(imageUri, (width, height) => {
|
|
117
|
+
console.log('[CropEditor] Image.getSize success:', { width, height });
|
|
118
|
+
setImageSize({ width, height });
|
|
119
|
+
// If we have a rectangle (from auto-capture), crop the image
|
|
120
|
+
if (shouldAutoCrop && (document.rectangle || document.quad)) {
|
|
121
|
+
cropImageToRectangle(imageUri, width, height);
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
setIsImageLoading(false);
|
|
125
|
+
setLoadError(null);
|
|
126
|
+
}
|
|
127
|
+
}, (error) => {
|
|
128
|
+
console.error('[CropEditor] Image.getSize error:', error);
|
|
129
|
+
// Fallback to document dimensions
|
|
130
|
+
console.log('[CropEditor] Using fallback dimensions:', document.width, 'x', document.height);
|
|
131
|
+
setImageSize({ width: document.width, height: document.height });
|
|
132
|
+
setIsImageLoading(false);
|
|
133
|
+
});
|
|
134
|
+
}, [autoCrop, cropImageToRectangle, document, enableEditor]);
|
|
130
135
|
// Get initial rectangle from detected quad or use default
|
|
131
136
|
const getInitialRectangle = (0, react_1.useCallback)(() => {
|
|
132
137
|
if (!imageSize) {
|
|
@@ -180,7 +185,7 @@ const CropEditor = ({ document, overlayColor = 'rgba(0,0,0,0.5)', overlayStrokeC
|
|
|
180
185
|
react_1.default.createElement(react_native_1.Text, { style: styles.errorText }, "Failed to load image"),
|
|
181
186
|
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 },
|
|
182
187
|
react_1.default.createElement(react_native_1.ActivityIndicator, { size: "large", color: handlerColor }),
|
|
183
|
-
react_1.default.createElement(react_native_1.Text, { style: styles.loadingText }, "Loading image..."))) : (react_1.default.createElement(react_1.default.Fragment, null,
|
|
188
|
+
react_1.default.createElement(react_native_1.Text, { style: styles.loadingText }, "Loading image..."))) : enableEditor ? (react_1.default.createElement(react_native_perspective_image_cropper_1.default, { height: displaySize.height, width: displaySize.width, image: imageUri, rectangleCoordinates: initialRect, overlayColor: overlayColor, overlayStrokeColor: overlayStrokeColor, handlerColor: handlerColor, enablePanStrict: enablePanStrict, onDragEnd: handleDragEnd })) : (react_1.default.createElement(react_1.default.Fragment, null,
|
|
184
189
|
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) })))));
|
|
185
190
|
};
|
|
186
191
|
exports.CropEditor = CropEditor;
|
package/dist/FullDocScanner.js
CHANGED
|
@@ -43,6 +43,7 @@ const react_native_image_picker_1 = require("react-native-image-picker");
|
|
|
43
43
|
const react_native_image_crop_picker_1 = __importDefault(require("react-native-image-crop-picker"));
|
|
44
44
|
const react_native_fs_1 = __importDefault(require("react-native-fs"));
|
|
45
45
|
const DocScanner_1 = require("./DocScanner");
|
|
46
|
+
const coordinate_1 = require("./utils/coordinate");
|
|
46
47
|
// 회전은 항상 지원됨 (회전 각도를 반환하고 tdb 앱에서 처리)
|
|
47
48
|
const isImageRotationSupported = () => true;
|
|
48
49
|
const stripFileUri = (value) => value.replace(/^file:\/\//, '');
|
|
@@ -58,6 +59,17 @@ const ensureFileUri = (value) => {
|
|
|
58
59
|
}
|
|
59
60
|
return value;
|
|
60
61
|
};
|
|
62
|
+
const safeRequire = (moduleName) => {
|
|
63
|
+
try {
|
|
64
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
65
|
+
return require(moduleName);
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
const CropEditorModule = safeRequire('./CropEditor');
|
|
72
|
+
const CropEditor = CropEditorModule?.CropEditor;
|
|
61
73
|
const CROPPER_TIMEOUT_MS = 8000;
|
|
62
74
|
const CROPPER_TIMEOUT_CODE = 'cropper_timeout';
|
|
63
75
|
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
@@ -135,6 +147,8 @@ const FullDocScanner = ({ onResult, onClose, detectionConfig, overlayColor = '#3
|
|
|
135
147
|
const [capturedPhotos, setCapturedPhotos] = (0, react_1.useState)([]);
|
|
136
148
|
const [currentPhotoIndex, setCurrentPhotoIndex] = (0, react_1.useState)(0);
|
|
137
149
|
const [scannerSession, setScannerSession] = (0, react_1.useState)(0);
|
|
150
|
+
const [cropEditorDocument, setCropEditorDocument] = (0, react_1.useState)(null);
|
|
151
|
+
const [cropEditorRectangle, setCropEditorRectangle] = (0, react_1.useState)(null);
|
|
138
152
|
const resolvedGridColor = gridColor ?? overlayColor;
|
|
139
153
|
const docScannerRef = (0, react_1.useRef)(null);
|
|
140
154
|
const captureModeRef = (0, react_1.useRef)(null);
|
|
@@ -146,6 +160,8 @@ const FullDocScanner = ({ onResult, onClose, detectionConfig, overlayColor = '#3
|
|
|
146
160
|
const resetScannerView = (0, react_1.useCallback)((options) => {
|
|
147
161
|
setProcessing(false);
|
|
148
162
|
setCroppedImageData(null);
|
|
163
|
+
setCropEditorDocument(null);
|
|
164
|
+
setCropEditorRectangle(null);
|
|
149
165
|
setRotationDegrees(0);
|
|
150
166
|
setRectangleDetected(false);
|
|
151
167
|
setRectangleHint(false);
|
|
@@ -187,6 +203,7 @@ const FullDocScanner = ({ onResult, onClose, detectionConfig, overlayColor = '#3
|
|
|
187
203
|
originalBtn: strings?.originalBtn ?? 'Use Original',
|
|
188
204
|
}), [strings]);
|
|
189
205
|
const pdfScannerManager = react_native_1.NativeModules?.RNPdfScannerManager;
|
|
206
|
+
const isAndroidCropEditorAvailable = react_native_1.Platform.OS === 'android' && Boolean(CropEditor);
|
|
190
207
|
const autoEnhancementEnabled = (0, react_1.useMemo)(() => typeof pdfScannerManager?.applyColorControls === 'function', [pdfScannerManager]);
|
|
191
208
|
const ensureBase64ForImage = (0, react_1.useCallback)(async (image) => {
|
|
192
209
|
if (image.base64) {
|
|
@@ -243,6 +260,17 @@ const FullDocScanner = ({ onResult, onClose, detectionConfig, overlayColor = '#3
|
|
|
243
260
|
react_native_1.Alert.alert('Document Scanner', fallbackMessage);
|
|
244
261
|
}
|
|
245
262
|
}, [onError]);
|
|
263
|
+
const openAndroidCropEditor = (0, react_1.useCallback)((document) => {
|
|
264
|
+
const rectangle = document.rectangle ??
|
|
265
|
+
(document.quad && document.quad.length === 4 ? (0, coordinate_1.quadToRectangle)(document.quad) : null);
|
|
266
|
+
const documentForEditor = rectangle ? { ...document, rectangle } : document;
|
|
267
|
+
setCropEditorDocument(documentForEditor);
|
|
268
|
+
setCropEditorRectangle(rectangle);
|
|
269
|
+
}, []);
|
|
270
|
+
const closeAndroidCropEditor = (0, react_1.useCallback)(() => {
|
|
271
|
+
setCropEditorDocument(null);
|
|
272
|
+
setCropEditorRectangle(null);
|
|
273
|
+
}, []);
|
|
246
274
|
const openCropper = (0, react_1.useCallback)(async (imagePath, options) => {
|
|
247
275
|
try {
|
|
248
276
|
console.log('[FullDocScanner] openCropper called with path:', imagePath);
|
|
@@ -314,6 +342,64 @@ const FullDocScanner = ({ onResult, onClose, detectionConfig, overlayColor = '#3
|
|
|
314
342
|
}
|
|
315
343
|
}
|
|
316
344
|
}, [cropWidth, cropHeight, emitError, preparePreviewImage, resetScannerView]);
|
|
345
|
+
const handleCropEditorConfirm = (0, react_1.useCallback)(async () => {
|
|
346
|
+
if (!cropEditorDocument) {
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
const documentPath = cropEditorDocument.path;
|
|
350
|
+
const rectangle = cropEditorRectangle ??
|
|
351
|
+
cropEditorDocument.rectangle ??
|
|
352
|
+
(cropEditorDocument.quad && cropEditorDocument.quad.length === 4
|
|
353
|
+
? (0, coordinate_1.quadToRectangle)(cropEditorDocument.quad)
|
|
354
|
+
: null);
|
|
355
|
+
if (!rectangle || !pdfScannerManager?.processImage) {
|
|
356
|
+
closeAndroidCropEditor();
|
|
357
|
+
await openCropper(documentPath, { waitForPickerDismissal: false });
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
setProcessing(true);
|
|
361
|
+
try {
|
|
362
|
+
const payload = await pdfScannerManager.processImage({
|
|
363
|
+
imagePath: documentPath,
|
|
364
|
+
rectangleCoordinates: rectangle,
|
|
365
|
+
rectangleWidth: cropEditorDocument.width ?? 0,
|
|
366
|
+
rectangleHeight: cropEditorDocument.height ?? 0,
|
|
367
|
+
useBase64: false,
|
|
368
|
+
quality: 90,
|
|
369
|
+
brightness: 0,
|
|
370
|
+
contrast: 1,
|
|
371
|
+
saturation: 1,
|
|
372
|
+
saveInAppDocument: false,
|
|
373
|
+
});
|
|
374
|
+
const croppedPath = typeof payload?.croppedImage === 'string' ? stripFileUri(payload.croppedImage) : null;
|
|
375
|
+
if (!croppedPath) {
|
|
376
|
+
throw new Error('missing_cropped_image');
|
|
377
|
+
}
|
|
378
|
+
const preview = await preparePreviewImage({ path: croppedPath });
|
|
379
|
+
setCroppedImageData(preview);
|
|
380
|
+
}
|
|
381
|
+
catch (error) {
|
|
382
|
+
console.error('[FullDocScanner] Crop editor processing failed:', error);
|
|
383
|
+
resetScannerView({ remount: true });
|
|
384
|
+
emitError(error instanceof Error ? error : new Error(String(error)), 'Failed to crop image. Please try again.');
|
|
385
|
+
}
|
|
386
|
+
finally {
|
|
387
|
+
setProcessing(false);
|
|
388
|
+
closeAndroidCropEditor();
|
|
389
|
+
}
|
|
390
|
+
}, [
|
|
391
|
+
closeAndroidCropEditor,
|
|
392
|
+
cropEditorDocument,
|
|
393
|
+
cropEditorRectangle,
|
|
394
|
+
emitError,
|
|
395
|
+
openCropper,
|
|
396
|
+
pdfScannerManager,
|
|
397
|
+
preparePreviewImage,
|
|
398
|
+
resetScannerView,
|
|
399
|
+
]);
|
|
400
|
+
const handleCropEditorCancel = (0, react_1.useCallback)(() => {
|
|
401
|
+
resetScannerView({ remount: true });
|
|
402
|
+
}, [resetScannerView]);
|
|
317
403
|
const handleCapture = (0, react_1.useCallback)(async (document) => {
|
|
318
404
|
console.log('[FullDocScanner] handleCapture called:', {
|
|
319
405
|
origin: document.origin,
|
|
@@ -332,6 +418,15 @@ const FullDocScanner = ({ onResult, onClose, detectionConfig, overlayColor = '#3
|
|
|
332
418
|
return;
|
|
333
419
|
}
|
|
334
420
|
const normalizedDoc = normalizeCapturedDocument(document);
|
|
421
|
+
const shouldOpenAndroidCropEditor = isAndroidCropEditorAvailable &&
|
|
422
|
+
captureMode === 'grid' &&
|
|
423
|
+
Boolean(normalizedDoc.rectangle ||
|
|
424
|
+
(normalizedDoc.quad && normalizedDoc.quad.length === 4));
|
|
425
|
+
if (shouldOpenAndroidCropEditor) {
|
|
426
|
+
console.log('[FullDocScanner] Opening Android crop editor with detected rectangle');
|
|
427
|
+
openAndroidCropEditor(normalizedDoc);
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
335
430
|
if (captureMode === 'no-grid') {
|
|
336
431
|
console.log('[FullDocScanner] No grid at capture button press: opening cropper for manual selection');
|
|
337
432
|
await openCropper(normalizedDoc.path, { waitForPickerDismissal: false });
|
|
@@ -358,7 +453,14 @@ const FullDocScanner = ({ onResult, onClose, detectionConfig, overlayColor = '#3
|
|
|
358
453
|
}
|
|
359
454
|
console.log('[FullDocScanner] Fallback to manual crop (no croppedPath available)');
|
|
360
455
|
await openCropper(normalizedDoc.path, { waitForPickerDismissal: false });
|
|
361
|
-
}, [
|
|
456
|
+
}, [
|
|
457
|
+
emitError,
|
|
458
|
+
isAndroidCropEditorAvailable,
|
|
459
|
+
openAndroidCropEditor,
|
|
460
|
+
openCropper,
|
|
461
|
+
preparePreviewImage,
|
|
462
|
+
resetScannerView,
|
|
463
|
+
]);
|
|
362
464
|
const triggerManualCapture = (0, react_1.useCallback)(() => {
|
|
363
465
|
const scannerInstance = docScannerRef.current;
|
|
364
466
|
const hasScanner = !!scannerInstance;
|
|
@@ -649,6 +751,12 @@ const FullDocScanner = ({ onResult, onClose, detectionConfig, overlayColor = '#3
|
|
|
649
751
|
react_1.default.createElement(react_native_1.TouchableOpacity, { style: [styles.confirmButton, styles.retakeButton], onPress: handleRetake, accessibilityLabel: mergedStrings.retake, accessibilityRole: "button" },
|
|
650
752
|
react_1.default.createElement(react_native_1.Text, { style: styles.confirmButtonText }, mergedStrings.retake)),
|
|
651
753
|
react_1.default.createElement(react_native_1.TouchableOpacity, { style: [styles.confirmButton, styles.confirmButtonPrimary], onPress: handleConfirm, accessibilityLabel: mergedStrings.confirm, accessibilityRole: "button" },
|
|
754
|
+
react_1.default.createElement(react_native_1.Text, { style: styles.confirmButtonText }, mergedStrings.confirm))))) : cropEditorDocument && CropEditor ? (react_1.default.createElement(react_native_1.View, { style: styles.flex },
|
|
755
|
+
react_1.default.createElement(CropEditor, { document: cropEditorDocument, enableEditor: true, autoCrop: false, onCropChange: setCropEditorRectangle }),
|
|
756
|
+
react_1.default.createElement(react_native_1.View, { style: styles.confirmationButtons },
|
|
757
|
+
react_1.default.createElement(react_native_1.TouchableOpacity, { style: [styles.confirmButton, styles.retakeButton], onPress: handleCropEditorCancel, accessibilityLabel: mergedStrings.retake, accessibilityRole: "button", disabled: processing },
|
|
758
|
+
react_1.default.createElement(react_native_1.Text, { style: styles.confirmButtonText }, mergedStrings.retake)),
|
|
759
|
+
react_1.default.createElement(react_native_1.TouchableOpacity, { style: [styles.confirmButton, styles.confirmButtonPrimary], onPress: handleCropEditorConfirm, accessibilityLabel: mergedStrings.confirm, accessibilityRole: "button", disabled: processing },
|
|
652
760
|
react_1.default.createElement(react_native_1.Text, { style: styles.confirmButtonText }, mergedStrings.confirm))))) : (react_1.default.createElement(react_native_1.View, { style: styles.flex },
|
|
653
761
|
react_1.default.createElement(DocScanner_1.DocScanner, { key: scannerSession, ref: docScannerRef, autoCapture: false, overlayColor: overlayColor, showGrid: showGrid, gridColor: resolvedGridColor, gridLineWidth: gridLineWidth, minStableFrames: minStableFrames ?? 6, detectionConfig: detectionConfig, onCapture: handleCapture, onRectangleDetect: handleRectangleDetect, showManualCaptureButton: false, enableTorch: flashEnabled },
|
|
654
762
|
react_1.default.createElement(react_native_1.View, { style: styles.overlayTop, pointerEvents: "box-none" },
|
package/package.json
CHANGED
package/src/CropEditor.tsx
CHANGED
|
@@ -26,6 +26,8 @@ interface CropEditorProps {
|
|
|
26
26
|
overlayStrokeColor?: string;
|
|
27
27
|
handlerColor?: string;
|
|
28
28
|
enablePanStrict?: boolean;
|
|
29
|
+
enableEditor?: boolean;
|
|
30
|
+
autoCrop?: boolean;
|
|
29
31
|
onCropChange?: (rectangle: Rectangle) => void;
|
|
30
32
|
}
|
|
31
33
|
|
|
@@ -48,6 +50,8 @@ export const CropEditor: React.FC<CropEditorProps> = ({
|
|
|
48
50
|
overlayStrokeColor = '#e7a649',
|
|
49
51
|
handlerColor = '#e7a649',
|
|
50
52
|
enablePanStrict = false,
|
|
53
|
+
enableEditor = false,
|
|
54
|
+
autoCrop = true,
|
|
51
55
|
onCropChange,
|
|
52
56
|
}) => {
|
|
53
57
|
const [imageSize, setImageSize] = useState<{ width: number; height: number } | null>(null);
|
|
@@ -59,38 +63,6 @@ export const CropEditor: React.FC<CropEditorProps> = ({
|
|
|
59
63
|
const [loadError, setLoadError] = useState<string | null>(null);
|
|
60
64
|
const [croppedImageUri, setCroppedImageUri] = useState<string | null>(null);
|
|
61
65
|
|
|
62
|
-
useEffect(() => {
|
|
63
|
-
console.log('[CropEditor] Document path:', document.path);
|
|
64
|
-
console.log('[CropEditor] Document dimensions:', document.width, 'x', document.height);
|
|
65
|
-
console.log('[CropEditor] Document quad:', document.quad);
|
|
66
|
-
console.log('[CropEditor] Document rectangle:', document.rectangle);
|
|
67
|
-
|
|
68
|
-
// Load image size using Image.getSize
|
|
69
|
-
const imageUri = document.path.startsWith('file://') ? document.path : `file://${document.path}`;
|
|
70
|
-
Image.getSize(
|
|
71
|
-
imageUri,
|
|
72
|
-
(width, height) => {
|
|
73
|
-
console.log('[CropEditor] Image.getSize success:', { width, height });
|
|
74
|
-
setImageSize({ width, height });
|
|
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
|
-
}
|
|
83
|
-
},
|
|
84
|
-
(error) => {
|
|
85
|
-
console.error('[CropEditor] Image.getSize error:', error);
|
|
86
|
-
// Fallback to document dimensions
|
|
87
|
-
console.log('[CropEditor] Using fallback dimensions:', document.width, 'x', document.height);
|
|
88
|
-
setImageSize({ width: document.width, height: document.height });
|
|
89
|
-
setIsImageLoading(false);
|
|
90
|
-
}
|
|
91
|
-
);
|
|
92
|
-
}, [document]);
|
|
93
|
-
|
|
94
66
|
const cropImageToRectangle = useCallback((imageUri: string, width: number, height: number) => {
|
|
95
67
|
const cropManager = NativeModules.CustomCropManager as CustomCropManagerType | undefined;
|
|
96
68
|
|
|
@@ -145,6 +117,40 @@ export const CropEditor: React.FC<CropEditorProps> = ({
|
|
|
145
117
|
);
|
|
146
118
|
}, [document]);
|
|
147
119
|
|
|
120
|
+
useEffect(() => {
|
|
121
|
+
console.log('[CropEditor] Document path:', document.path);
|
|
122
|
+
console.log('[CropEditor] Document dimensions:', document.width, 'x', document.height);
|
|
123
|
+
console.log('[CropEditor] Document quad:', document.quad);
|
|
124
|
+
console.log('[CropEditor] Document rectangle:', document.rectangle);
|
|
125
|
+
|
|
126
|
+
const shouldAutoCrop = autoCrop && !enableEditor;
|
|
127
|
+
|
|
128
|
+
// Load image size using Image.getSize
|
|
129
|
+
const imageUri = document.path.startsWith('file://') ? document.path : `file://${document.path}`;
|
|
130
|
+
Image.getSize(
|
|
131
|
+
imageUri,
|
|
132
|
+
(width, height) => {
|
|
133
|
+
console.log('[CropEditor] Image.getSize success:', { width, height });
|
|
134
|
+
setImageSize({ width, height });
|
|
135
|
+
|
|
136
|
+
// If we have a rectangle (from auto-capture), crop the image
|
|
137
|
+
if (shouldAutoCrop && (document.rectangle || document.quad)) {
|
|
138
|
+
cropImageToRectangle(imageUri, width, height);
|
|
139
|
+
} else {
|
|
140
|
+
setIsImageLoading(false);
|
|
141
|
+
setLoadError(null);
|
|
142
|
+
}
|
|
143
|
+
},
|
|
144
|
+
(error) => {
|
|
145
|
+
console.error('[CropEditor] Image.getSize error:', error);
|
|
146
|
+
// Fallback to document dimensions
|
|
147
|
+
console.log('[CropEditor] Using fallback dimensions:', document.width, 'x', document.height);
|
|
148
|
+
setImageSize({ width: document.width, height: document.height });
|
|
149
|
+
setIsImageLoading(false);
|
|
150
|
+
}
|
|
151
|
+
);
|
|
152
|
+
}, [autoCrop, cropImageToRectangle, document, enableEditor]);
|
|
153
|
+
|
|
148
154
|
// Get initial rectangle from detected quad or use default
|
|
149
155
|
const getInitialRectangle = useCallback((): CropperRectangle | undefined => {
|
|
150
156
|
if (!imageSize) {
|
|
@@ -226,6 +232,18 @@ export const CropEditor: React.FC<CropEditorProps> = ({
|
|
|
226
232
|
<ActivityIndicator size="large" color={handlerColor} />
|
|
227
233
|
<Text style={styles.loadingText}>Loading image...</Text>
|
|
228
234
|
</View>
|
|
235
|
+
) : enableEditor ? (
|
|
236
|
+
<CustomImageCropper
|
|
237
|
+
height={displaySize.height}
|
|
238
|
+
width={displaySize.width}
|
|
239
|
+
image={imageUri}
|
|
240
|
+
rectangleCoordinates={initialRect}
|
|
241
|
+
overlayColor={overlayColor}
|
|
242
|
+
overlayStrokeColor={overlayStrokeColor}
|
|
243
|
+
handlerColor={handlerColor}
|
|
244
|
+
enablePanStrict={enablePanStrict}
|
|
245
|
+
onDragEnd={handleDragEnd}
|
|
246
|
+
/>
|
|
229
247
|
) : (
|
|
230
248
|
<>
|
|
231
249
|
{/* Show cropped image if available, otherwise show original */}
|
|
@@ -236,18 +254,6 @@ export const CropEditor: React.FC<CropEditorProps> = ({
|
|
|
236
254
|
onLoad={() => console.log('[CropEditor] Image loaded successfully', croppedImageUri ? 'cropped' : 'original')}
|
|
237
255
|
onError={(e) => console.error('[CropEditor] Image load error:', e.nativeEvent.error)}
|
|
238
256
|
/>
|
|
239
|
-
{/* Temporarily disabled CustomImageCropper - showing image only */}
|
|
240
|
-
{/* <CustomImageCropper
|
|
241
|
-
height={displaySize.height}
|
|
242
|
-
width={displaySize.width}
|
|
243
|
-
image={imageUri}
|
|
244
|
-
rectangleCoordinates={initialRect}
|
|
245
|
-
overlayColor={overlayColor}
|
|
246
|
-
overlayStrokeColor={overlayStrokeColor}
|
|
247
|
-
handlerColor={handlerColor}
|
|
248
|
-
enablePanStrict={enablePanStrict}
|
|
249
|
-
onDragEnd={handleDragEnd}
|
|
250
|
-
/> */}
|
|
251
257
|
</>
|
|
252
258
|
)}
|
|
253
259
|
</View>
|
package/src/FullDocScanner.tsx
CHANGED
|
@@ -16,7 +16,8 @@ import { launchImageLibrary } from 'react-native-image-picker';
|
|
|
16
16
|
import ImageCropPicker from 'react-native-image-crop-picker';
|
|
17
17
|
import RNFS from 'react-native-fs';
|
|
18
18
|
import { DocScanner } from './DocScanner';
|
|
19
|
-
import type { CapturedDocument } from './types';
|
|
19
|
+
import type { CapturedDocument, Rectangle } from './types';
|
|
20
|
+
import { quadToRectangle } from './utils/coordinate';
|
|
20
21
|
import type {
|
|
21
22
|
DetectionConfig,
|
|
22
23
|
DocScannerHandle,
|
|
@@ -41,6 +42,25 @@ const ensureFileUri = (value?: string | null) => {
|
|
|
41
42
|
return value;
|
|
42
43
|
};
|
|
43
44
|
|
|
45
|
+
const safeRequire = (moduleName: string) => {
|
|
46
|
+
try {
|
|
47
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
48
|
+
return require(moduleName);
|
|
49
|
+
} catch {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const CropEditorModule = safeRequire('./CropEditor');
|
|
55
|
+
const CropEditor = CropEditorModule?.CropEditor as
|
|
56
|
+
| React.ComponentType<{
|
|
57
|
+
document: CapturedDocument;
|
|
58
|
+
enableEditor?: boolean;
|
|
59
|
+
autoCrop?: boolean;
|
|
60
|
+
onCropChange?: (rectangle: Rectangle) => void;
|
|
61
|
+
}>
|
|
62
|
+
| undefined;
|
|
63
|
+
|
|
44
64
|
const CROPPER_TIMEOUT_MS = 8000;
|
|
45
65
|
const CROPPER_TIMEOUT_CODE = 'cropper_timeout';
|
|
46
66
|
|
|
@@ -198,6 +218,8 @@ export const FullDocScanner: React.FC<FullDocScannerProps> = ({
|
|
|
198
218
|
const [capturedPhotos, setCapturedPhotos] = useState<FullDocScannerResult[]>([]);
|
|
199
219
|
const [currentPhotoIndex, setCurrentPhotoIndex] = useState(0);
|
|
200
220
|
const [scannerSession, setScannerSession] = useState(0);
|
|
221
|
+
const [cropEditorDocument, setCropEditorDocument] = useState<CapturedDocument | null>(null);
|
|
222
|
+
const [cropEditorRectangle, setCropEditorRectangle] = useState<Rectangle | null>(null);
|
|
201
223
|
const resolvedGridColor = gridColor ?? overlayColor;
|
|
202
224
|
const docScannerRef = useRef<DocScannerHandle | null>(null);
|
|
203
225
|
const captureModeRef = useRef<'grid' | 'no-grid' | null>(null);
|
|
@@ -212,6 +234,8 @@ export const FullDocScanner: React.FC<FullDocScannerProps> = ({
|
|
|
212
234
|
(options?: { remount?: boolean }) => {
|
|
213
235
|
setProcessing(false);
|
|
214
236
|
setCroppedImageData(null);
|
|
237
|
+
setCropEditorDocument(null);
|
|
238
|
+
setCropEditorRectangle(null);
|
|
215
239
|
setRotationDegrees(0);
|
|
216
240
|
setRectangleDetected(false);
|
|
217
241
|
setRectangleHint(false);
|
|
@@ -264,6 +288,7 @@ export const FullDocScanner: React.FC<FullDocScannerProps> = ({
|
|
|
264
288
|
);
|
|
265
289
|
|
|
266
290
|
const pdfScannerManager = (NativeModules as any)?.RNPdfScannerManager;
|
|
291
|
+
const isAndroidCropEditorAvailable = Platform.OS === 'android' && Boolean(CropEditor);
|
|
267
292
|
|
|
268
293
|
const autoEnhancementEnabled = useMemo(
|
|
269
294
|
() => typeof pdfScannerManager?.applyColorControls === 'function',
|
|
@@ -353,6 +378,20 @@ export const FullDocScanner: React.FC<FullDocScannerProps> = ({
|
|
|
353
378
|
[onError],
|
|
354
379
|
);
|
|
355
380
|
|
|
381
|
+
const openAndroidCropEditor = useCallback((document: CapturedDocument) => {
|
|
382
|
+
const rectangle =
|
|
383
|
+
document.rectangle ??
|
|
384
|
+
(document.quad && document.quad.length === 4 ? quadToRectangle(document.quad) : null);
|
|
385
|
+
const documentForEditor = rectangle ? { ...document, rectangle } : document;
|
|
386
|
+
setCropEditorDocument(documentForEditor);
|
|
387
|
+
setCropEditorRectangle(rectangle);
|
|
388
|
+
}, []);
|
|
389
|
+
|
|
390
|
+
const closeAndroidCropEditor = useCallback(() => {
|
|
391
|
+
setCropEditorDocument(null);
|
|
392
|
+
setCropEditorRectangle(null);
|
|
393
|
+
}, []);
|
|
394
|
+
|
|
356
395
|
const openCropper = useCallback(
|
|
357
396
|
async (imagePath: string, options?: OpenCropperOptions) => {
|
|
358
397
|
try {
|
|
@@ -446,6 +485,76 @@ export const FullDocScanner: React.FC<FullDocScannerProps> = ({
|
|
|
446
485
|
[cropWidth, cropHeight, emitError, preparePreviewImage, resetScannerView],
|
|
447
486
|
);
|
|
448
487
|
|
|
488
|
+
const handleCropEditorConfirm = useCallback(async () => {
|
|
489
|
+
if (!cropEditorDocument) {
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
const documentPath = cropEditorDocument.path;
|
|
494
|
+
|
|
495
|
+
const rectangle =
|
|
496
|
+
cropEditorRectangle ??
|
|
497
|
+
cropEditorDocument.rectangle ??
|
|
498
|
+
(cropEditorDocument.quad && cropEditorDocument.quad.length === 4
|
|
499
|
+
? quadToRectangle(cropEditorDocument.quad)
|
|
500
|
+
: null);
|
|
501
|
+
|
|
502
|
+
if (!rectangle || !pdfScannerManager?.processImage) {
|
|
503
|
+
closeAndroidCropEditor();
|
|
504
|
+
await openCropper(documentPath, { waitForPickerDismissal: false });
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
setProcessing(true);
|
|
509
|
+
try {
|
|
510
|
+
const payload = await pdfScannerManager.processImage({
|
|
511
|
+
imagePath: documentPath,
|
|
512
|
+
rectangleCoordinates: rectangle,
|
|
513
|
+
rectangleWidth: cropEditorDocument.width ?? 0,
|
|
514
|
+
rectangleHeight: cropEditorDocument.height ?? 0,
|
|
515
|
+
useBase64: false,
|
|
516
|
+
quality: 90,
|
|
517
|
+
brightness: 0,
|
|
518
|
+
contrast: 1,
|
|
519
|
+
saturation: 1,
|
|
520
|
+
saveInAppDocument: false,
|
|
521
|
+
});
|
|
522
|
+
|
|
523
|
+
const croppedPath =
|
|
524
|
+
typeof payload?.croppedImage === 'string' ? stripFileUri(payload.croppedImage) : null;
|
|
525
|
+
|
|
526
|
+
if (!croppedPath) {
|
|
527
|
+
throw new Error('missing_cropped_image');
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
const preview = await preparePreviewImage({ path: croppedPath });
|
|
531
|
+
setCroppedImageData(preview);
|
|
532
|
+
} catch (error) {
|
|
533
|
+
console.error('[FullDocScanner] Crop editor processing failed:', error);
|
|
534
|
+
resetScannerView({ remount: true });
|
|
535
|
+
emitError(
|
|
536
|
+
error instanceof Error ? error : new Error(String(error)),
|
|
537
|
+
'Failed to crop image. Please try again.',
|
|
538
|
+
);
|
|
539
|
+
} finally {
|
|
540
|
+
setProcessing(false);
|
|
541
|
+
closeAndroidCropEditor();
|
|
542
|
+
}
|
|
543
|
+
}, [
|
|
544
|
+
closeAndroidCropEditor,
|
|
545
|
+
cropEditorDocument,
|
|
546
|
+
cropEditorRectangle,
|
|
547
|
+
emitError,
|
|
548
|
+
openCropper,
|
|
549
|
+
pdfScannerManager,
|
|
550
|
+
preparePreviewImage,
|
|
551
|
+
resetScannerView,
|
|
552
|
+
]);
|
|
553
|
+
|
|
554
|
+
const handleCropEditorCancel = useCallback(() => {
|
|
555
|
+
resetScannerView({ remount: true });
|
|
556
|
+
}, [resetScannerView]);
|
|
557
|
+
|
|
449
558
|
const handleCapture = useCallback(
|
|
450
559
|
async (document: DocScannerCapture) => {
|
|
451
560
|
console.log('[FullDocScanner] handleCapture called:', {
|
|
@@ -470,6 +579,20 @@ export const FullDocScanner: React.FC<FullDocScannerProps> = ({
|
|
|
470
579
|
|
|
471
580
|
const normalizedDoc = normalizeCapturedDocument(document);
|
|
472
581
|
|
|
582
|
+
const shouldOpenAndroidCropEditor =
|
|
583
|
+
isAndroidCropEditorAvailable &&
|
|
584
|
+
captureMode === 'grid' &&
|
|
585
|
+
Boolean(
|
|
586
|
+
normalizedDoc.rectangle ||
|
|
587
|
+
(normalizedDoc.quad && normalizedDoc.quad.length === 4),
|
|
588
|
+
);
|
|
589
|
+
|
|
590
|
+
if (shouldOpenAndroidCropEditor) {
|
|
591
|
+
console.log('[FullDocScanner] Opening Android crop editor with detected rectangle');
|
|
592
|
+
openAndroidCropEditor(normalizedDoc);
|
|
593
|
+
return;
|
|
594
|
+
}
|
|
595
|
+
|
|
473
596
|
if (captureMode === 'no-grid') {
|
|
474
597
|
console.log('[FullDocScanner] No grid at capture button press: opening cropper for manual selection');
|
|
475
598
|
await openCropper(normalizedDoc.path, { waitForPickerDismissal: false });
|
|
@@ -501,7 +624,14 @@ export const FullDocScanner: React.FC<FullDocScannerProps> = ({
|
|
|
501
624
|
console.log('[FullDocScanner] Fallback to manual crop (no croppedPath available)');
|
|
502
625
|
await openCropper(normalizedDoc.path, { waitForPickerDismissal: false });
|
|
503
626
|
},
|
|
504
|
-
[
|
|
627
|
+
[
|
|
628
|
+
emitError,
|
|
629
|
+
isAndroidCropEditorAvailable,
|
|
630
|
+
openAndroidCropEditor,
|
|
631
|
+
openCropper,
|
|
632
|
+
preparePreviewImage,
|
|
633
|
+
resetScannerView,
|
|
634
|
+
],
|
|
505
635
|
);
|
|
506
636
|
|
|
507
637
|
const triggerManualCapture = useCallback(() => {
|
|
@@ -949,6 +1079,36 @@ export const FullDocScanner: React.FC<FullDocScannerProps> = ({
|
|
|
949
1079
|
</TouchableOpacity>
|
|
950
1080
|
</View>
|
|
951
1081
|
</View>
|
|
1082
|
+
) : cropEditorDocument && CropEditor ? (
|
|
1083
|
+
<View style={styles.flex}>
|
|
1084
|
+
<CropEditor
|
|
1085
|
+
document={cropEditorDocument}
|
|
1086
|
+
enableEditor
|
|
1087
|
+
autoCrop={false}
|
|
1088
|
+
onCropChange={setCropEditorRectangle}
|
|
1089
|
+
/>
|
|
1090
|
+
<View style={styles.confirmationButtons}>
|
|
1091
|
+
<TouchableOpacity
|
|
1092
|
+
style={[styles.confirmButton, styles.retakeButton]}
|
|
1093
|
+
onPress={handleCropEditorCancel}
|
|
1094
|
+
accessibilityLabel={mergedStrings.retake}
|
|
1095
|
+
accessibilityRole="button"
|
|
1096
|
+
disabled={processing}
|
|
1097
|
+
>
|
|
1098
|
+
<Text style={styles.confirmButtonText}>{mergedStrings.retake}</Text>
|
|
1099
|
+
</TouchableOpacity>
|
|
1100
|
+
|
|
1101
|
+
<TouchableOpacity
|
|
1102
|
+
style={[styles.confirmButton, styles.confirmButtonPrimary]}
|
|
1103
|
+
onPress={handleCropEditorConfirm}
|
|
1104
|
+
accessibilityLabel={mergedStrings.confirm}
|
|
1105
|
+
accessibilityRole="button"
|
|
1106
|
+
disabled={processing}
|
|
1107
|
+
>
|
|
1108
|
+
<Text style={styles.confirmButtonText}>{mergedStrings.confirm}</Text>
|
|
1109
|
+
</TouchableOpacity>
|
|
1110
|
+
</View>
|
|
1111
|
+
</View>
|
|
952
1112
|
) : (
|
|
953
1113
|
<View style={styles.flex}>
|
|
954
1114
|
<DocScanner
|