react-native-rectangle-doc-scanner 3.106.0 → 3.107.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,25 @@ 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;
47
48
  try {
48
49
  ImageManipulator = require('expo-image-manipulator');
49
50
  }
50
51
  catch (error) {
51
- console.warn('[FullDocScanner] expo-image-manipulator module unavailable. Image rotation will be disabled.', error);
52
+ console.warn('[FullDocScanner] expo-image-manipulator module unavailable. Checking fallback options.', error);
52
53
  }
53
- const isImageRotationSupported = !!ImageManipulator?.manipulateAsync;
54
+ try {
55
+ const rotateModule = require('react-native-image-rotate');
56
+ ImageRotate = (rotateModule?.default ?? rotateModule);
57
+ }
58
+ catch (error) {
59
+ console.warn('[FullDocScanner] react-native-image-rotate module unavailable. Image rotation fallback disabled.', error);
60
+ }
61
+ const isExpoImageManipulatorAvailable = !!ImageManipulator?.manipulateAsync;
62
+ const isImageRotateAvailable = !!ImageRotate?.rotateImage;
63
+ const isImageRotationSupported = isExpoImageManipulatorAvailable || isImageRotateAvailable;
54
64
  const stripFileUri = (value) => value.replace(/^file:\/\//, '');
65
+ const ensureFileUri = (value) => (value.startsWith('file://') ? value : `file://${value}`);
55
66
  const CROPPER_TIMEOUT_MS = 8000;
56
67
  const CROPPER_TIMEOUT_CODE = 'cropper_timeout';
57
68
  const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
@@ -351,9 +362,25 @@ const FullDocScanner = ({ onResult, onClose, detectionConfig, overlayColor = '#3
351
362
  const handleFlashToggle = (0, react_1.useCallback)(() => {
352
363
  setFlashEnabled(prev => !prev);
353
364
  }, []);
365
+ const rotateImageWithFallback = (0, react_1.useCallback)((uri, angle) => {
366
+ return new Promise((resolve, reject) => {
367
+ if (!ImageRotate?.rotateImage) {
368
+ reject(new Error('react-native-image-rotate unavailable'));
369
+ return;
370
+ }
371
+ ImageRotate.rotateImage(uri, angle, (rotatedUri) => resolve(rotatedUri), (error) => {
372
+ const message = typeof error === 'string'
373
+ ? error
374
+ : error && typeof error === 'object' && 'message' in error
375
+ ? String(error.message)
376
+ : 'Unknown rotation error';
377
+ reject(new Error(message));
378
+ });
379
+ });
380
+ }, []);
354
381
  const handleRotateImage = (0, react_1.useCallback)((degrees) => {
355
382
  if (!isImageRotationSupported) {
356
- console.warn('[FullDocScanner] Image rotation requested but expo-image-manipulator is unavailable.');
383
+ console.warn('[FullDocScanner] Image rotation requested but no rotation module is available.');
357
384
  return;
358
385
  }
359
386
  // UI만 회전 (실제 파일 회전은 confirm 시 처리)
@@ -372,10 +399,7 @@ const FullDocScanner = ({ onResult, onClose, detectionConfig, overlayColor = '#3
372
399
  return;
373
400
  }
374
401
  // 회전이 없거나 모듈을 사용할 수 없으면 기존 데이터 그대로 전달
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
- }
402
+ if (rotationDegrees === 0) {
379
403
  onResult({
380
404
  path: croppedImageData.path,
381
405
  base64: croppedImageData.base64,
@@ -386,25 +410,47 @@ const FullDocScanner = ({ onResult, onClose, detectionConfig, overlayColor = '#3
386
410
  setProcessing(true);
387
411
  // 회전 각도 정규화 (0, 90, 180, 270)
388
412
  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.');
413
+ if (rotationNormalized === 0) {
392
414
  onResult({
393
415
  path: croppedImageData.path,
394
416
  base64: croppedImageData.base64,
395
417
  });
396
418
  return;
397
419
  }
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
- // 회전된 이미지로 결과 전달
420
+ if (isExpoImageManipulatorAvailable && ImageManipulator?.manipulateAsync) {
421
+ // expo-image-manipulator 이미지 회전
422
+ const result = await ImageManipulator.manipulateAsync(croppedImageData.path, [{ rotate: rotationNormalized }], {
423
+ compress: 0.9,
424
+ format: ImageManipulator.SaveFormat.JPEG,
425
+ base64: true,
426
+ });
427
+ onResult({
428
+ path: result.uri,
429
+ base64: result.base64,
430
+ });
431
+ return;
432
+ }
433
+ if (isImageRotateAvailable && ImageRotate?.rotateImage) {
434
+ const sourceUri = ensureFileUri(croppedImageData.path);
435
+ const rotatedUri = await rotateImageWithFallback(sourceUri, rotationNormalized);
436
+ let base64Result = croppedImageData.base64;
437
+ try {
438
+ base64Result = await react_native_fs_1.default.readFile(stripFileUri(rotatedUri), 'base64');
439
+ }
440
+ catch (readError) {
441
+ console.warn('[FullDocScanner] Failed to generate base64 for rotated image:', readError);
442
+ base64Result = undefined;
443
+ }
444
+ onResult({
445
+ path: rotatedUri,
446
+ base64: base64Result,
447
+ });
448
+ return;
449
+ }
450
+ console.warn('[FullDocScanner] Rotation requested but no supported rotation module is available. Returning original image.');
405
451
  onResult({
406
- path: result.uri,
407
- base64: result.base64,
452
+ path: croppedImageData.path,
453
+ base64: croppedImageData.base64,
408
454
  });
409
455
  }
410
456
  catch (error) {
@@ -415,7 +461,7 @@ const FullDocScanner = ({ onResult, onClose, detectionConfig, overlayColor = '#3
415
461
  finally {
416
462
  setProcessing(false);
417
463
  }
418
- }, [croppedImageData, rotationDegrees, onResult]);
464
+ }, [croppedImageData, rotationDegrees, onResult, rotateImageWithFallback]);
419
465
  const handleRetake = (0, react_1.useCallback)(() => {
420
466
  console.log('[FullDocScanner] Retake - clearing cropped image and resetting scanner');
421
467
  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.107.0",
4
4
  "description": "Native-backed document scanner for React Native with customizable overlays.",
5
5
  "license": "MIT",
6
6
  "main": "dist/index.js",
@@ -22,21 +22,43 @@ import type {
22
22
  } from './DocScanner';
23
23
 
24
24
  type ImageManipulatorModule = typeof import('expo-image-manipulator');
25
+ type ImageRotateModule = {
26
+ rotateImage: (
27
+ uri: string,
28
+ angle: number,
29
+ onSuccess: (uri: string) => void,
30
+ onError: (error: unknown) => void,
31
+ ) => void;
32
+ } | null;
25
33
 
26
34
  let ImageManipulator: ImageManipulatorModule | null = null;
35
+ let ImageRotate: ImageRotateModule = null;
27
36
 
28
37
  try {
29
38
  ImageManipulator = require('expo-image-manipulator') as ImageManipulatorModule;
30
39
  } catch (error) {
31
40
  console.warn(
32
- '[FullDocScanner] expo-image-manipulator module unavailable. Image rotation will be disabled.',
41
+ '[FullDocScanner] expo-image-manipulator module unavailable. Checking fallback options.',
33
42
  error,
34
43
  );
35
44
  }
36
45
 
37
- const isImageRotationSupported = !!ImageManipulator?.manipulateAsync;
46
+ try {
47
+ const rotateModule = require('react-native-image-rotate');
48
+ ImageRotate = (rotateModule?.default ?? rotateModule) as ImageRotateModule;
49
+ } catch (error) {
50
+ console.warn(
51
+ '[FullDocScanner] react-native-image-rotate module unavailable. Image rotation fallback disabled.',
52
+ error,
53
+ );
54
+ }
55
+
56
+ const isExpoImageManipulatorAvailable = !!ImageManipulator?.manipulateAsync;
57
+ const isImageRotateAvailable = !!ImageRotate?.rotateImage;
58
+ const isImageRotationSupported = isExpoImageManipulatorAvailable || isImageRotateAvailable;
38
59
 
39
60
  const stripFileUri = (value: string) => value.replace(/^file:\/\//, '');
61
+ const ensureFileUri = (value: string) => (value.startsWith('file://') ? value : `file://${value}`);
40
62
 
41
63
  const CROPPER_TIMEOUT_MS = 8000;
42
64
  const CROPPER_TIMEOUT_CODE = 'cropper_timeout';
@@ -470,37 +492,58 @@ export const FullDocScanner: React.FC<FullDocScannerProps> = ({
470
492
  setFlashEnabled(prev => !prev);
471
493
  }, []);
472
494
 
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
- }
495
+ const rotateImageWithFallback = useCallback((uri: string, angle: number) => {
496
+ return new Promise<string>((resolve, reject) => {
497
+ if (!ImageRotate?.rotateImage) {
498
+ reject(new Error('react-native-image-rotate unavailable'));
499
+ return;
500
+ }
480
501
 
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;
502
+ ImageRotate.rotateImage(
503
+ uri,
504
+ angle,
505
+ (rotatedUri) => resolve(rotatedUri),
506
+ (error) => {
507
+ const message =
508
+ typeof error === 'string'
509
+ ? error
510
+ : error && typeof error === 'object' && 'message' in error
511
+ ? String((error as any).message)
512
+ : 'Unknown rotation error';
513
+ reject(new Error(message));
514
+ },
515
+ );
488
516
  });
489
517
  }, []);
490
518
 
519
+ const handleRotateImage = useCallback(
520
+ (degrees: -90 | 90) => {
521
+ if (!isImageRotationSupported) {
522
+ console.warn(
523
+ '[FullDocScanner] Image rotation requested but no rotation module is available.',
524
+ );
525
+ return;
526
+ }
527
+
528
+ // UI만 회전 (실제 파일 회전은 confirm 시 처리)
529
+ setRotationDegrees((prev) => {
530
+ const newRotation = prev + degrees;
531
+ // -360 ~ 360 범위로 정규화
532
+ if (newRotation <= -360) return newRotation + 360;
533
+ if (newRotation >= 360) return newRotation - 360;
534
+ return newRotation;
535
+ });
536
+ },
537
+ [],
538
+ );
539
+
491
540
  const handleConfirm = useCallback(async () => {
492
541
  if (!croppedImageData) {
493
542
  return;
494
543
  }
495
544
 
496
545
  // 회전이 없거나 모듈을 사용할 수 없으면 기존 데이터 그대로 전달
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
-
546
+ if (rotationDegrees === 0) {
504
547
  onResult({
505
548
  path: croppedImageData.path,
506
549
  base64: croppedImageData.base64,
@@ -514,12 +557,7 @@ export const FullDocScanner: React.FC<FullDocScannerProps> = ({
514
557
  // 회전 각도 정규화 (0, 90, 180, 270)
515
558
  const rotationNormalized = ((rotationDegrees % 360) + 360) % 360;
516
559
 
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
- );
560
+ if (rotationNormalized === 0) {
523
561
  onResult({
524
562
  path: croppedImageData.path,
525
563
  base64: croppedImageData.base64,
@@ -527,21 +565,50 @@ export const FullDocScanner: React.FC<FullDocScannerProps> = ({
527
565
  return;
528
566
  }
529
567
 
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
- );
568
+ if (isExpoImageManipulatorAvailable && ImageManipulator?.manipulateAsync) {
569
+ // expo-image-manipulator로 이미지 회전
570
+ const result = await ImageManipulator.manipulateAsync(
571
+ croppedImageData.path,
572
+ [{ rotate: rotationNormalized }],
573
+ {
574
+ compress: 0.9,
575
+ format: ImageManipulator.SaveFormat.JPEG,
576
+ base64: true,
577
+ },
578
+ );
579
+
580
+ onResult({
581
+ path: result.uri,
582
+ base64: result.base64,
583
+ });
584
+ return;
585
+ }
540
586
 
541
- // 회전된 이미지로 결과 전달
587
+ if (isImageRotateAvailable && ImageRotate?.rotateImage) {
588
+ const sourceUri = ensureFileUri(croppedImageData.path);
589
+ const rotatedUri = await rotateImageWithFallback(sourceUri, rotationNormalized);
590
+ let base64Result = croppedImageData.base64;
591
+
592
+ try {
593
+ base64Result = await RNFS.readFile(stripFileUri(rotatedUri), 'base64');
594
+ } catch (readError) {
595
+ console.warn('[FullDocScanner] Failed to generate base64 for rotated image:', readError);
596
+ base64Result = undefined;
597
+ }
598
+
599
+ onResult({
600
+ path: rotatedUri,
601
+ base64: base64Result,
602
+ });
603
+ return;
604
+ }
605
+
606
+ console.warn(
607
+ '[FullDocScanner] Rotation requested but no supported rotation module is available. Returning original image.',
608
+ );
542
609
  onResult({
543
- path: result.uri,
544
- base64: result.base64,
610
+ path: croppedImageData.path,
611
+ base64: croppedImageData.base64,
545
612
  });
546
613
  } catch (error) {
547
614
  console.error('[FullDocScanner] Image rotation error on confirm:', error);
@@ -552,7 +619,7 @@ export const FullDocScanner: React.FC<FullDocScannerProps> = ({
552
619
  } finally {
553
620
  setProcessing(false);
554
621
  }
555
- }, [croppedImageData, rotationDegrees, onResult]);
622
+ }, [croppedImageData, rotationDegrees, onResult, rotateImageWithFallback]);
556
623
 
557
624
  const handleRetake = useCallback(() => {
558
625
  console.log('[FullDocScanner] Retake - clearing cropped image and resetting scanner');