react-native-rectangle-doc-scanner 3.106.0 → 3.108.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 CHANGED
@@ -120,7 +120,7 @@ Manual capture exposes an imperative `capture()` method via `ref`. Children rend
120
120
  ## Convenience APIs
121
121
 
122
122
  - `CropEditor` – wraps `react-native-perspective-image-cropper` for manual corner adjustment.
123
- - `FullDocScanner` – puts the scanner and crop editor into a single modal-like flow. If the host app has the `expo-image-manipulator` native module installed, the confirmation screen exposes 90° rotation buttons; otherwise rotation controls remain hidden.
123
+ - `FullDocScanner` – puts the scanner and crop editor into a single modal-like flow. If the host app links either `expo-image-manipulator` or `react-native-image-rotate`, the confirmation screen exposes 90° rotation buttons; otherwise rotation controls remain hidden.
124
124
 
125
125
  ## License
126
126
 
package/SETUP.md CHANGED
@@ -117,7 +117,7 @@ function MyComponent() {
117
117
  }
118
118
  ```
119
119
 
120
- > 참고: 최종 확인 화면의 회전 버튼은 프로젝트에 `expo-image-manipulator` 네이티브 모듈이 설치되어 있을 때만 노출됩니다. 모듈이 없으면 버튼이 자동으로 숨겨지고 원본 각도로 결과가 반환됩니다.
120
+ > 참고: 최종 확인 화면의 회전 버튼은 프로젝트에 `expo-image-manipulator` 또는 `react-native-image-rotate` 네이티브 모듈이 연결되어 있을 때만 노출됩니다. 중 하나라도 없으면 버튼이 자동으로 숨겨지고 원본 각도로 결과가 반환됩니다.
121
121
 
122
122
  ## API 변경사항
123
123
 
@@ -44,14 +44,44 @@ const react_native_image_crop_picker_1 = __importDefault(require("react-native-i
44
44
  const react_native_fs_1 = __importDefault(require("react-native-fs"));
45
45
  const DocScanner_1 = require("./DocScanner");
46
46
  let ImageManipulator = null;
47
+ let ImageRotate = null;
48
+ const ImageStoreManager = react_native_1.NativeModules.ImageStoreManager;
47
49
  try {
48
50
  ImageManipulator = require('expo-image-manipulator');
49
51
  }
50
52
  catch (error) {
51
- console.warn('[FullDocScanner] expo-image-manipulator module unavailable. Image rotation will be disabled.', error);
53
+ console.warn('[FullDocScanner] expo-image-manipulator module unavailable. Checking fallback options.', error);
52
54
  }
53
- const isImageRotationSupported = !!ImageManipulator?.manipulateAsync;
55
+ try {
56
+ const rotateModule = require('react-native-image-rotate');
57
+ ImageRotate = (rotateModule?.default ?? rotateModule);
58
+ }
59
+ catch (error) {
60
+ console.warn('[FullDocScanner] react-native-image-rotate module unavailable. Image rotation fallback disabled.', error);
61
+ }
62
+ const isExpoImageManipulatorAvailable = !!ImageManipulator?.manipulateAsync;
63
+ const isImageRotateAvailable = !!ImageRotate?.rotateImage;
64
+ const isImageRotationSupported = isExpoImageManipulatorAvailable || isImageRotateAvailable;
54
65
  const stripFileUri = (value) => value.replace(/^file:\/\//, '');
66
+ const ensureFileUri = (value) => (value.startsWith('file://') ? value : `file://${value}`);
67
+ const getBase64FromImageStore = async (uri) => {
68
+ const getBase64ForTag = ImageStoreManager?.getBase64ForTag?.bind(ImageStoreManager);
69
+ if (!getBase64ForTag) {
70
+ throw new Error('ImageStoreManager.getBase64ForTag unavailable');
71
+ }
72
+ return new Promise((resolve, reject) => {
73
+ getBase64ForTag(uri, (base64) => resolve(base64), (error) => {
74
+ const message = typeof error === 'string'
75
+ ? error
76
+ : error && typeof error === 'object' && 'message' in error
77
+ ? String(error.message)
78
+ : 'Failed to read from ImageStore';
79
+ reject(new Error(message));
80
+ });
81
+ }).finally(() => {
82
+ ImageStoreManager?.removeImageForTag?.(uri);
83
+ });
84
+ };
55
85
  const CROPPER_TIMEOUT_MS = 8000;
56
86
  const CROPPER_TIMEOUT_CODE = 'cropper_timeout';
57
87
  const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
@@ -351,9 +381,25 @@ const FullDocScanner = ({ onResult, onClose, detectionConfig, overlayColor = '#3
351
381
  const handleFlashToggle = (0, react_1.useCallback)(() => {
352
382
  setFlashEnabled(prev => !prev);
353
383
  }, []);
384
+ const rotateImageWithFallback = (0, react_1.useCallback)((uri, angle) => {
385
+ return new Promise((resolve, reject) => {
386
+ if (!ImageRotate?.rotateImage) {
387
+ reject(new Error('react-native-image-rotate unavailable'));
388
+ return;
389
+ }
390
+ ImageRotate.rotateImage(uri, angle, (rotatedUri) => resolve(rotatedUri), (error) => {
391
+ const message = typeof error === 'string'
392
+ ? error
393
+ : error && typeof error === 'object' && 'message' in error
394
+ ? String(error.message)
395
+ : 'Unknown rotation error';
396
+ reject(new Error(message));
397
+ });
398
+ });
399
+ }, []);
354
400
  const handleRotateImage = (0, react_1.useCallback)((degrees) => {
355
401
  if (!isImageRotationSupported) {
356
- console.warn('[FullDocScanner] Image rotation requested but expo-image-manipulator is unavailable.');
402
+ console.warn('[FullDocScanner] Image rotation requested but no rotation module is available.');
357
403
  return;
358
404
  }
359
405
  // UI만 회전 (실제 파일 회전은 confirm 시 처리)
@@ -372,10 +418,7 @@ const FullDocScanner = ({ onResult, onClose, detectionConfig, overlayColor = '#3
372
418
  return;
373
419
  }
374
420
  // 회전이 없거나 모듈을 사용할 수 없으면 기존 데이터 그대로 전달
375
- if (rotationDegrees === 0 || !isImageRotationSupported || !ImageManipulator) {
376
- if (rotationDegrees !== 0 && !isImageRotationSupported) {
377
- console.warn('[FullDocScanner] Confirm requested with rotation but expo-image-manipulator is unavailable. Returning original image.');
378
- }
421
+ if (rotationDegrees === 0) {
379
422
  onResult({
380
423
  path: croppedImageData.path,
381
424
  base64: croppedImageData.base64,
@@ -386,25 +429,51 @@ const FullDocScanner = ({ onResult, onClose, detectionConfig, overlayColor = '#3
386
429
  setProcessing(true);
387
430
  // 회전 각도 정규화 (0, 90, 180, 270)
388
431
  const rotationNormalized = ((rotationDegrees % 360) + 360) % 360;
389
- const manipulator = ImageManipulator;
390
- if (!manipulator?.manipulateAsync) {
391
- console.warn('[FullDocScanner] expo-image-manipulator is unavailable at runtime. Returning original image.');
432
+ if (rotationNormalized === 0) {
392
433
  onResult({
393
434
  path: croppedImageData.path,
394
435
  base64: croppedImageData.base64,
395
436
  });
396
437
  return;
397
438
  }
398
- // expo-image-manipulator로 이미지 회전
399
- const result = await manipulator.manipulateAsync(croppedImageData.path, [{ rotate: rotationNormalized }], {
400
- compress: 0.9,
401
- format: manipulator.SaveFormat.JPEG,
402
- base64: true,
403
- });
404
- // 회전된 이미지로 결과 전달
439
+ if (isExpoImageManipulatorAvailable && ImageManipulator?.manipulateAsync) {
440
+ // expo-image-manipulator 이미지 회전
441
+ const result = await ImageManipulator.manipulateAsync(croppedImageData.path, [{ rotate: rotationNormalized }], {
442
+ compress: 0.9,
443
+ format: ImageManipulator.SaveFormat.JPEG,
444
+ base64: true,
445
+ });
446
+ onResult({
447
+ path: result.uri,
448
+ base64: result.base64,
449
+ });
450
+ return;
451
+ }
452
+ if (isImageRotateAvailable && ImageRotate?.rotateImage) {
453
+ const sourceUri = ensureFileUri(croppedImageData.path);
454
+ const rotatedUri = await rotateImageWithFallback(sourceUri, rotationNormalized);
455
+ let finalPath = croppedImageData.path;
456
+ let base64Result = croppedImageData.base64;
457
+ try {
458
+ const base64FromStore = await getBase64FromImageStore(rotatedUri);
459
+ const destinationPath = `${react_native_fs_1.default.CachesDirectoryPath}/full-doc-scanner-rotated-${Date.now()}-${Math.floor(Math.random() * 10000)}.jpg`;
460
+ await react_native_fs_1.default.writeFile(destinationPath, base64FromStore, 'base64');
461
+ finalPath = destinationPath;
462
+ base64Result = base64FromStore;
463
+ }
464
+ catch (readError) {
465
+ console.warn('[FullDocScanner] Failed to persist rotated image from ImageStore:', readError);
466
+ }
467
+ onResult({
468
+ path: finalPath,
469
+ base64: base64Result,
470
+ });
471
+ return;
472
+ }
473
+ console.warn('[FullDocScanner] Rotation requested but no supported rotation module is available. Returning original image.');
405
474
  onResult({
406
- path: result.uri,
407
- base64: result.base64,
475
+ path: croppedImageData.path,
476
+ base64: croppedImageData.base64,
408
477
  });
409
478
  }
410
479
  catch (error) {
@@ -415,7 +484,7 @@ const FullDocScanner = ({ onResult, onClose, detectionConfig, overlayColor = '#3
415
484
  finally {
416
485
  setProcessing(false);
417
486
  }
418
- }, [croppedImageData, rotationDegrees, onResult]);
487
+ }, [croppedImageData, rotationDegrees, onResult, rotateImageWithFallback]);
419
488
  const handleRetake = (0, react_1.useCallback)(() => {
420
489
  console.log('[FullDocScanner] Retake - clearing cropped image and resetting scanner');
421
490
  setCroppedImageData(null);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-rectangle-doc-scanner",
3
- "version": "3.106.0",
3
+ "version": "3.108.0",
4
4
  "description": "Native-backed document scanner for React Native with customizable overlays.",
5
5
  "license": "MIT",
6
6
  "main": "dist/index.js",
@@ -8,6 +8,7 @@ import {
8
8
  Text,
9
9
  TouchableOpacity,
10
10
  View,
11
+ NativeModules,
11
12
  } from 'react-native';
12
13
  import { launchImageLibrary } from 'react-native-image-picker';
13
14
  import ImageCropPicker from 'react-native-image-crop-picker';
@@ -22,21 +23,79 @@ import type {
22
23
  } from './DocScanner';
23
24
 
24
25
  type ImageManipulatorModule = typeof import('expo-image-manipulator');
26
+ type ImageRotateModule = {
27
+ rotateImage: (
28
+ uri: string,
29
+ angle: number,
30
+ onSuccess: (uri: string) => void,
31
+ onError: (error: unknown) => void,
32
+ ) => void;
33
+ } | null;
34
+
35
+ type ImageStoreModule = {
36
+ getBase64ForTag?: (
37
+ uri: string,
38
+ success: (base64: string) => void,
39
+ failure: (error: unknown) => void,
40
+ ) => void;
41
+ removeImageForTag?: (uri: string) => void;
42
+ } | undefined;
25
43
 
26
44
  let ImageManipulator: ImageManipulatorModule | null = null;
45
+ let ImageRotate: ImageRotateModule = null;
46
+ const ImageStoreManager: ImageStoreModule = NativeModules.ImageStoreManager;
27
47
 
28
48
  try {
29
49
  ImageManipulator = require('expo-image-manipulator') as ImageManipulatorModule;
30
50
  } catch (error) {
31
51
  console.warn(
32
- '[FullDocScanner] expo-image-manipulator module unavailable. Image rotation will be disabled.',
52
+ '[FullDocScanner] expo-image-manipulator module unavailable. Checking fallback options.',
33
53
  error,
34
54
  );
35
55
  }
36
56
 
37
- const isImageRotationSupported = !!ImageManipulator?.manipulateAsync;
57
+ try {
58
+ const rotateModule = require('react-native-image-rotate');
59
+ ImageRotate = (rotateModule?.default ?? rotateModule) as ImageRotateModule;
60
+ } catch (error) {
61
+ console.warn(
62
+ '[FullDocScanner] react-native-image-rotate module unavailable. Image rotation fallback disabled.',
63
+ error,
64
+ );
65
+ }
66
+
67
+ const isExpoImageManipulatorAvailable = !!ImageManipulator?.manipulateAsync;
68
+ const isImageRotateAvailable = !!ImageRotate?.rotateImage;
69
+ const isImageRotationSupported = isExpoImageManipulatorAvailable || isImageRotateAvailable;
38
70
 
39
71
  const stripFileUri = (value: string) => value.replace(/^file:\/\//, '');
72
+ const ensureFileUri = (value: string) => (value.startsWith('file://') ? value : `file://${value}`);
73
+
74
+ const getBase64FromImageStore = async (uri: string): Promise<string> => {
75
+ const getBase64ForTag = ImageStoreManager?.getBase64ForTag?.bind(ImageStoreManager);
76
+
77
+ if (!getBase64ForTag) {
78
+ throw new Error('ImageStoreManager.getBase64ForTag unavailable');
79
+ }
80
+
81
+ return new Promise<string>((resolve, reject) => {
82
+ getBase64ForTag(
83
+ uri,
84
+ (base64: string) => resolve(base64),
85
+ (error: unknown) => {
86
+ const message =
87
+ typeof error === 'string'
88
+ ? error
89
+ : error && typeof error === 'object' && 'message' in error
90
+ ? String((error as any).message)
91
+ : 'Failed to read from ImageStore';
92
+ reject(new Error(message));
93
+ },
94
+ );
95
+ }).finally(() => {
96
+ ImageStoreManager?.removeImageForTag?.(uri);
97
+ });
98
+ };
40
99
 
41
100
  const CROPPER_TIMEOUT_MS = 8000;
42
101
  const CROPPER_TIMEOUT_CODE = 'cropper_timeout';
@@ -470,37 +529,58 @@ export const FullDocScanner: React.FC<FullDocScannerProps> = ({
470
529
  setFlashEnabled(prev => !prev);
471
530
  }, []);
472
531
 
473
- const handleRotateImage = useCallback((degrees: -90 | 90) => {
474
- if (!isImageRotationSupported) {
475
- console.warn(
476
- '[FullDocScanner] Image rotation requested but expo-image-manipulator is unavailable.',
477
- );
478
- return;
479
- }
532
+ const rotateImageWithFallback = useCallback((uri: string, angle: number) => {
533
+ return new Promise<string>((resolve, reject) => {
534
+ if (!ImageRotate?.rotateImage) {
535
+ reject(new Error('react-native-image-rotate unavailable'));
536
+ return;
537
+ }
480
538
 
481
- // UI만 회전 (실제 파일 회전은 confirm 시 처리)
482
- setRotationDegrees((prev) => {
483
- const newRotation = prev + degrees;
484
- // -360 ~ 360 범위로 정규화
485
- if (newRotation <= -360) return newRotation + 360;
486
- if (newRotation >= 360) return newRotation - 360;
487
- return newRotation;
539
+ ImageRotate.rotateImage(
540
+ uri,
541
+ angle,
542
+ (rotatedUri) => resolve(rotatedUri),
543
+ (error) => {
544
+ const message =
545
+ typeof error === 'string'
546
+ ? error
547
+ : error && typeof error === 'object' && 'message' in error
548
+ ? String((error as any).message)
549
+ : 'Unknown rotation error';
550
+ reject(new Error(message));
551
+ },
552
+ );
488
553
  });
489
554
  }, []);
490
555
 
556
+ const handleRotateImage = useCallback(
557
+ (degrees: -90 | 90) => {
558
+ if (!isImageRotationSupported) {
559
+ console.warn(
560
+ '[FullDocScanner] Image rotation requested but no rotation module is available.',
561
+ );
562
+ return;
563
+ }
564
+
565
+ // UI만 회전 (실제 파일 회전은 confirm 시 처리)
566
+ setRotationDegrees((prev) => {
567
+ const newRotation = prev + degrees;
568
+ // -360 ~ 360 범위로 정규화
569
+ if (newRotation <= -360) return newRotation + 360;
570
+ if (newRotation >= 360) return newRotation - 360;
571
+ return newRotation;
572
+ });
573
+ },
574
+ [],
575
+ );
576
+
491
577
  const handleConfirm = useCallback(async () => {
492
578
  if (!croppedImageData) {
493
579
  return;
494
580
  }
495
581
 
496
582
  // 회전이 없거나 모듈을 사용할 수 없으면 기존 데이터 그대로 전달
497
- if (rotationDegrees === 0 || !isImageRotationSupported || !ImageManipulator) {
498
- if (rotationDegrees !== 0 && !isImageRotationSupported) {
499
- console.warn(
500
- '[FullDocScanner] Confirm requested with rotation but expo-image-manipulator is unavailable. Returning original image.',
501
- );
502
- }
503
-
583
+ if (rotationDegrees === 0) {
504
584
  onResult({
505
585
  path: croppedImageData.path,
506
586
  base64: croppedImageData.base64,
@@ -514,12 +594,7 @@ export const FullDocScanner: React.FC<FullDocScannerProps> = ({
514
594
  // 회전 각도 정규화 (0, 90, 180, 270)
515
595
  const rotationNormalized = ((rotationDegrees % 360) + 360) % 360;
516
596
 
517
- const manipulator = ImageManipulator;
518
-
519
- if (!manipulator?.manipulateAsync) {
520
- console.warn(
521
- '[FullDocScanner] expo-image-manipulator is unavailable at runtime. Returning original image.',
522
- );
597
+ if (rotationNormalized === 0) {
523
598
  onResult({
524
599
  path: croppedImageData.path,
525
600
  base64: croppedImageData.base64,
@@ -527,21 +602,56 @@ export const FullDocScanner: React.FC<FullDocScannerProps> = ({
527
602
  return;
528
603
  }
529
604
 
530
- // expo-image-manipulator로 이미지 회전
531
- const result = await manipulator.manipulateAsync(
532
- croppedImageData.path,
533
- [{ rotate: rotationNormalized }],
534
- {
535
- compress: 0.9,
536
- format: manipulator.SaveFormat.JPEG,
537
- base64: true,
538
- },
539
- );
605
+ if (isExpoImageManipulatorAvailable && ImageManipulator?.manipulateAsync) {
606
+ // expo-image-manipulator로 이미지 회전
607
+ const result = await ImageManipulator.manipulateAsync(
608
+ croppedImageData.path,
609
+ [{ rotate: rotationNormalized }],
610
+ {
611
+ compress: 0.9,
612
+ format: ImageManipulator.SaveFormat.JPEG,
613
+ base64: true,
614
+ },
615
+ );
616
+
617
+ onResult({
618
+ path: result.uri,
619
+ base64: result.base64,
620
+ });
621
+ return;
622
+ }
623
+
624
+ if (isImageRotateAvailable && ImageRotate?.rotateImage) {
625
+ const sourceUri = ensureFileUri(croppedImageData.path);
626
+ const rotatedUri = await rotateImageWithFallback(sourceUri, rotationNormalized);
627
+
628
+ let finalPath = croppedImageData.path;
629
+ let base64Result: string | undefined = croppedImageData.base64;
630
+
631
+ try {
632
+ const base64FromStore = await getBase64FromImageStore(rotatedUri);
633
+ const destinationPath = `${RNFS.CachesDirectoryPath}/full-doc-scanner-rotated-${Date.now()}-${Math.floor(Math.random() * 10000)}.jpg`;
634
+
635
+ await RNFS.writeFile(destinationPath, base64FromStore, 'base64');
636
+ finalPath = destinationPath;
637
+ base64Result = base64FromStore;
638
+ } catch (readError) {
639
+ console.warn('[FullDocScanner] Failed to persist rotated image from ImageStore:', readError);
640
+ }
540
641
 
541
- // 회전된 이미지로 결과 전달
642
+ onResult({
643
+ path: finalPath,
644
+ base64: base64Result,
645
+ });
646
+ return;
647
+ }
648
+
649
+ console.warn(
650
+ '[FullDocScanner] Rotation requested but no supported rotation module is available. Returning original image.',
651
+ );
542
652
  onResult({
543
- path: result.uri,
544
- base64: result.base64,
653
+ path: croppedImageData.path,
654
+ base64: croppedImageData.base64,
545
655
  });
546
656
  } catch (error) {
547
657
  console.error('[FullDocScanner] Image rotation error on confirm:', error);
@@ -552,7 +662,7 @@ export const FullDocScanner: React.FC<FullDocScannerProps> = ({
552
662
  } finally {
553
663
  setProcessing(false);
554
664
  }
555
- }, [croppedImageData, rotationDegrees, onResult]);
665
+ }, [croppedImageData, rotationDegrees, onResult, rotateImageWithFallback]);
556
666
 
557
667
  const handleRetake = useCallback(() => {
558
668
  console.log('[FullDocScanner] Retake - clearing cropped image and resetting scanner');