react-native-rectangle-doc-scanner 3.104.0 → 3.106.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.
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.
124
124
 
125
125
  ## License
126
126
 
package/SETUP.md CHANGED
@@ -117,6 +117,8 @@ function MyComponent() {
117
117
  }
118
118
  ```
119
119
 
120
+ > 참고: 최종 확인 화면의 회전 버튼은 프로젝트에 `expo-image-manipulator` 네이티브 모듈이 설치되어 있을 때만 노출됩니다. 모듈이 없으면 버튼이 자동으로 숨겨지고 원본 각도로 결과가 반환됩니다.
121
+
120
122
  ## API 변경사항
121
123
 
122
124
  ### FullDocScannerResult (변경됨)
@@ -42,8 +42,15 @@ const react_native_1 = require("react-native");
42
42
  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
- const ImageManipulator = __importStar(require("expo-image-manipulator"));
46
45
  const DocScanner_1 = require("./DocScanner");
46
+ let ImageManipulator = null;
47
+ try {
48
+ ImageManipulator = require('expo-image-manipulator');
49
+ }
50
+ catch (error) {
51
+ console.warn('[FullDocScanner] expo-image-manipulator module unavailable. Image rotation will be disabled.', error);
52
+ }
53
+ const isImageRotationSupported = !!ImageManipulator?.manipulateAsync;
47
54
  const stripFileUri = (value) => value.replace(/^file:\/\//, '');
48
55
  const CROPPER_TIMEOUT_MS = 8000;
49
56
  const CROPPER_TIMEOUT_CODE = 'cropper_timeout';
@@ -345,8 +352,12 @@ const FullDocScanner = ({ onResult, onClose, detectionConfig, overlayColor = '#3
345
352
  setFlashEnabled(prev => !prev);
346
353
  }, []);
347
354
  const handleRotateImage = (0, react_1.useCallback)((degrees) => {
355
+ if (!isImageRotationSupported) {
356
+ console.warn('[FullDocScanner] Image rotation requested but expo-image-manipulator is unavailable.');
357
+ return;
358
+ }
348
359
  // UI만 회전 (실제 파일 회전은 confirm 시 처리)
349
- setRotationDegrees(prev => {
360
+ setRotationDegrees((prev) => {
350
361
  const newRotation = prev + degrees;
351
362
  // -360 ~ 360 범위로 정규화
352
363
  if (newRotation <= -360)
@@ -357,28 +368,39 @@ const FullDocScanner = ({ onResult, onClose, detectionConfig, overlayColor = '#3
357
368
  });
358
369
  }, []);
359
370
  const handleConfirm = (0, react_1.useCallback)(async () => {
360
- if (!croppedImageData)
371
+ if (!croppedImageData) {
361
372
  return;
362
- // 회전이 없으면 기존 데이터 그대로 전달
363
- if (rotationDegrees === 0) {
373
+ }
374
+ // 회전이 없거나 모듈을 사용할 수 없으면 기존 데이터 그대로 전달
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
+ }
364
379
  onResult({
365
380
  path: croppedImageData.path,
366
381
  base64: croppedImageData.base64,
367
382
  });
368
383
  return;
369
384
  }
370
- // 회전이 있으면 실제 파일 회전 처리
371
385
  try {
372
386
  setProcessing(true);
373
387
  // 회전 각도 정규화 (0, 90, 180, 270)
374
388
  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.');
392
+ onResult({
393
+ path: croppedImageData.path,
394
+ base64: croppedImageData.base64,
395
+ });
396
+ return;
397
+ }
375
398
  // expo-image-manipulator로 이미지 회전
376
- const result = await ImageManipulator.manipulateAsync(croppedImageData.path, [{ rotate: rotationNormalized }], {
399
+ const result = await manipulator.manipulateAsync(croppedImageData.path, [{ rotate: rotationNormalized }], {
377
400
  compress: 0.9,
378
- format: ImageManipulator.SaveFormat.JPEG,
401
+ format: manipulator.SaveFormat.JPEG,
379
402
  base64: true,
380
403
  });
381
- setProcessing(false);
382
404
  // 회전된 이미지로 결과 전달
383
405
  onResult({
384
406
  path: result.uri,
@@ -387,10 +409,12 @@ const FullDocScanner = ({ onResult, onClose, detectionConfig, overlayColor = '#3
387
409
  }
388
410
  catch (error) {
389
411
  console.error('[FullDocScanner] Image rotation error on confirm:', error);
390
- setProcessing(false);
391
412
  const errorMessage = error && typeof error === 'object' && 'message' in error ? error.message : '';
392
413
  react_native_1.Alert.alert('회전 실패', '이미지 회전 중 오류가 발생했습니다: ' + errorMessage);
393
414
  }
415
+ finally {
416
+ setProcessing(false);
417
+ }
394
418
  }, [croppedImageData, rotationDegrees, onResult]);
395
419
  const handleRetake = (0, react_1.useCallback)(() => {
396
420
  console.log('[FullDocScanner] Retake - clearing cropped image and resetting scanner');
@@ -466,13 +490,13 @@ const FullDocScanner = ({ onResult, onClose, detectionConfig, overlayColor = '#3
466
490
  croppedImageData ? (
467
491
  // check_DP: Show confirmation screen
468
492
  react_1.default.createElement(react_native_1.View, { style: styles.confirmationContainer },
469
- react_1.default.createElement(react_native_1.View, { style: styles.rotateButtonsCenter },
493
+ isImageRotationSupported ? (react_1.default.createElement(react_native_1.View, { style: styles.rotateButtonsCenter },
470
494
  react_1.default.createElement(react_native_1.TouchableOpacity, { style: styles.rotateButtonTop, onPress: () => handleRotateImage(-90), accessibilityLabel: "\uC67C\uCABD\uC73C\uB85C 90\uB3C4 \uD68C\uC804", accessibilityRole: "button" },
471
495
  react_1.default.createElement(react_native_1.Text, { style: styles.rotateIconText }, "\u21BA"),
472
496
  react_1.default.createElement(react_native_1.Text, { style: styles.rotateButtonLabel }, "\uC88C\uB85C 90\u00B0")),
473
497
  react_1.default.createElement(react_native_1.TouchableOpacity, { style: styles.rotateButtonTop, onPress: () => handleRotateImage(90), accessibilityLabel: "\uC624\uB978\uCABD\uC73C\uB85C 90\uB3C4 \uD68C\uC804", accessibilityRole: "button" },
474
498
  react_1.default.createElement(react_native_1.Text, { style: styles.rotateIconText }, "\u21BB"),
475
- react_1.default.createElement(react_native_1.Text, { style: styles.rotateButtonLabel }, "\uC6B0\uB85C 90\u00B0"))),
499
+ react_1.default.createElement(react_native_1.Text, { style: styles.rotateButtonLabel }, "\uC6B0\uB85C 90\u00B0")))) : null,
476
500
  react_1.default.createElement(react_native_1.Image, { source: { uri: croppedImageData.path }, style: [
477
501
  styles.previewImage,
478
502
  { transform: [{ rotate: `${rotationDegrees}deg` }] }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-rectangle-doc-scanner",
3
- "version": "3.104.0",
3
+ "version": "3.106.0",
4
4
  "description": "Native-backed document scanner for React Native with customizable overlays.",
5
5
  "license": "MIT",
6
6
  "main": "dist/index.js",
@@ -36,7 +36,8 @@
36
36
  "react-native-fs": "*",
37
37
  "react-native-image-crop-picker": "*",
38
38
  "react-native-image-picker": "*",
39
- "react-native-svg": "*"
39
+ "react-native-svg": "*",
40
+ "expo-modules-core": "*"
40
41
  },
41
42
  "devDependencies": {
42
43
  "@types/react": "^18.2.41",
@@ -47,7 +48,6 @@
47
48
  "typescript": "^5.3.3"
48
49
  },
49
50
  "dependencies": {
50
- "expo-image-manipulator": "^12.0.5",
51
- "expo-modules-core": "^2.1.0"
51
+ "expo-image-manipulator": "^12.0.5"
52
52
  }
53
53
  }
@@ -12,7 +12,6 @@ import {
12
12
  import { launchImageLibrary } from 'react-native-image-picker';
13
13
  import ImageCropPicker from 'react-native-image-crop-picker';
14
14
  import RNFS from 'react-native-fs';
15
- import * as ImageManipulator from 'expo-image-manipulator';
16
15
  import { DocScanner } from './DocScanner';
17
16
  import type { CapturedDocument } from './types';
18
17
  import type {
@@ -22,6 +21,21 @@ import type {
22
21
  RectangleDetectEvent,
23
22
  } from './DocScanner';
24
23
 
24
+ type ImageManipulatorModule = typeof import('expo-image-manipulator');
25
+
26
+ let ImageManipulator: ImageManipulatorModule | null = null;
27
+
28
+ try {
29
+ ImageManipulator = require('expo-image-manipulator') as ImageManipulatorModule;
30
+ } catch (error) {
31
+ console.warn(
32
+ '[FullDocScanner] expo-image-manipulator module unavailable. Image rotation will be disabled.',
33
+ error,
34
+ );
35
+ }
36
+
37
+ const isImageRotationSupported = !!ImageManipulator?.manipulateAsync;
38
+
25
39
  const stripFileUri = (value: string) => value.replace(/^file:\/\//, '');
26
40
 
27
41
  const CROPPER_TIMEOUT_MS = 8000;
@@ -457,8 +471,15 @@ export const FullDocScanner: React.FC<FullDocScannerProps> = ({
457
471
  }, []);
458
472
 
459
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
+ }
480
+
460
481
  // UI만 회전 (실제 파일 회전은 confirm 시 처리)
461
- setRotationDegrees(prev => {
482
+ setRotationDegrees((prev) => {
462
483
  const newRotation = prev + degrees;
463
484
  // -360 ~ 360 범위로 정규화
464
485
  if (newRotation <= -360) return newRotation + 360;
@@ -468,10 +489,18 @@ export const FullDocScanner: React.FC<FullDocScannerProps> = ({
468
489
  }, []);
469
490
 
470
491
  const handleConfirm = useCallback(async () => {
471
- if (!croppedImageData) return;
492
+ if (!croppedImageData) {
493
+ return;
494
+ }
495
+
496
+ // 회전이 없거나 모듈을 사용할 수 없으면 기존 데이터 그대로 전달
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
+ }
472
503
 
473
- // 회전이 없으면 기존 데이터 그대로 전달
474
- if (rotationDegrees === 0) {
475
504
  onResult({
476
505
  path: croppedImageData.path,
477
506
  base64: croppedImageData.base64,
@@ -479,26 +508,36 @@ export const FullDocScanner: React.FC<FullDocScannerProps> = ({
479
508
  return;
480
509
  }
481
510
 
482
- // 회전이 있으면 실제 파일 회전 처리
483
511
  try {
484
512
  setProcessing(true);
485
513
 
486
514
  // 회전 각도 정규화 (0, 90, 180, 270)
487
515
  const rotationNormalized = ((rotationDegrees % 360) + 360) % 360;
488
516
 
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
+ );
523
+ onResult({
524
+ path: croppedImageData.path,
525
+ base64: croppedImageData.base64,
526
+ });
527
+ return;
528
+ }
529
+
489
530
  // expo-image-manipulator로 이미지 회전
490
- const result = await ImageManipulator.manipulateAsync(
531
+ const result = await manipulator.manipulateAsync(
491
532
  croppedImageData.path,
492
533
  [{ rotate: rotationNormalized }],
493
534
  {
494
535
  compress: 0.9,
495
- format: ImageManipulator.SaveFormat.JPEG,
536
+ format: manipulator.SaveFormat.JPEG,
496
537
  base64: true,
497
- }
538
+ },
498
539
  );
499
540
 
500
- setProcessing(false);
501
-
502
541
  // 회전된 이미지로 결과 전달
503
542
  onResult({
504
543
  path: result.uri,
@@ -506,10 +545,12 @@ export const FullDocScanner: React.FC<FullDocScannerProps> = ({
506
545
  });
507
546
  } catch (error) {
508
547
  console.error('[FullDocScanner] Image rotation error on confirm:', error);
509
- setProcessing(false);
510
548
 
511
- const errorMessage = error && typeof error === 'object' && 'message' in error ? (error as Error).message : '';
549
+ const errorMessage =
550
+ error && typeof error === 'object' && 'message' in error ? (error as Error).message : '';
512
551
  Alert.alert('회전 실패', '이미지 회전 중 오류가 발생했습니다: ' + errorMessage);
552
+ } finally {
553
+ setProcessing(false);
513
554
  }
514
555
  }, [croppedImageData, rotationDegrees, onResult]);
515
556
 
@@ -599,26 +640,28 @@ export const FullDocScanner: React.FC<FullDocScannerProps> = ({
599
640
  // check_DP: Show confirmation screen
600
641
  <View style={styles.confirmationContainer}>
601
642
  {/* 회전 버튼들 - 가운데 정렬 */}
602
- <View style={styles.rotateButtonsCenter}>
603
- <TouchableOpacity
604
- style={styles.rotateButtonTop}
605
- onPress={() => handleRotateImage(-90)}
606
- accessibilityLabel="왼쪽으로 90도 회전"
607
- accessibilityRole="button"
608
- >
609
- <Text style={styles.rotateIconText}>↺</Text>
610
- <Text style={styles.rotateButtonLabel}>좌로 90°</Text>
611
- </TouchableOpacity>
612
- <TouchableOpacity
613
- style={styles.rotateButtonTop}
614
- onPress={() => handleRotateImage(90)}
615
- accessibilityLabel="오른쪽으로 90도 회전"
616
- accessibilityRole="button"
617
- >
618
- <Text style={styles.rotateIconText}>↻</Text>
619
- <Text style={styles.rotateButtonLabel}>우로 90°</Text>
620
- </TouchableOpacity>
621
- </View>
643
+ {isImageRotationSupported ? (
644
+ <View style={styles.rotateButtonsCenter}>
645
+ <TouchableOpacity
646
+ style={styles.rotateButtonTop}
647
+ onPress={() => handleRotateImage(-90)}
648
+ accessibilityLabel="왼쪽으로 90도 회전"
649
+ accessibilityRole="button"
650
+ >
651
+ <Text style={styles.rotateIconText}>↺</Text>
652
+ <Text style={styles.rotateButtonLabel}>좌로 90°</Text>
653
+ </TouchableOpacity>
654
+ <TouchableOpacity
655
+ style={styles.rotateButtonTop}
656
+ onPress={() => handleRotateImage(90)}
657
+ accessibilityLabel="오른쪽으로 90도 회전"
658
+ accessibilityRole="button"
659
+ >
660
+ <Text style={styles.rotateIconText}>↻</Text>
661
+ <Text style={styles.rotateButtonLabel}>우로 90°</Text>
662
+ </TouchableOpacity>
663
+ </View>
664
+ ) : null}
622
665
  <Image
623
666
  source={{ uri: croppedImageData.path }}
624
667
  style={[