react-native-rectangle-doc-scanner 3.100.0 → 3.102.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.
@@ -42,6 +42,7 @@ 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 react_native_fast_opencv_1 = require("react-native-fast-opencv");
45
46
  const DocScanner_1 = require("./DocScanner");
46
47
  const stripFileUri = (value) => value.replace(/^file:\/\//, '');
47
48
  const CROPPER_TIMEOUT_MS = 8000;
@@ -343,61 +344,82 @@ const FullDocScanner = ({ onResult, onClose, detectionConfig, overlayColor = '#3
343
344
  const handleFlashToggle = (0, react_1.useCallback)(() => {
344
345
  setFlashEnabled(prev => !prev);
345
346
  }, []);
346
- const handleRotateImage = (0, react_1.useCallback)(async (degrees) => {
347
+ const handleRotateImage = (0, react_1.useCallback)((degrees) => {
348
+ // UI만 회전 (실제 파일 회전은 confirm 시 처리)
349
+ setRotationDegrees(prev => {
350
+ const newRotation = prev + degrees;
351
+ // -360 ~ 360 범위로 정규화
352
+ if (newRotation <= -360)
353
+ return newRotation + 360;
354
+ if (newRotation >= 360)
355
+ return newRotation - 360;
356
+ return newRotation;
357
+ });
358
+ }, []);
359
+ const handleConfirm = (0, react_1.useCallback)(async () => {
347
360
  if (!croppedImageData)
348
361
  return;
349
- console.log('[FullDocScanner] Starting image rotation...', {
350
- path: croppedImageData.path,
351
- hasBase64: !!croppedImageData.base64,
352
- base64Length: croppedImageData.base64?.length,
353
- });
354
- try {
355
- // 원본 파일을 ImageCropPicker로 열어서 회전 적용
356
- // 사용자가 수동으로 회전 버튼을 클릭하고 완료를 눌러야 함
357
- const rotatedImage = await react_native_image_crop_picker_1.default.openCropper({
362
+ // 회전이 없으면 기존 데이터 그대로 전달
363
+ if (rotationDegrees === 0) {
364
+ onResult({
358
365
  path: croppedImageData.path,
359
- mediaType: 'photo',
360
- cropping: true,
361
- freeStyleCropEnabled: true,
362
- includeBase64: true,
363
- compressImageQuality: 0.9,
364
- cropperToolbarTitle: degrees === 90 ? '우측 90° 회전 → 회전 버튼 클릭 → 완료' : '좌측 90° 회전 → 회전 버튼 클릭 → 완료',
365
- cropperChooseText: '완료',
366
- cropperCancelText: '취소',
367
- cropperRotateButtonsHidden: false,
368
- enableRotationGesture: true,
366
+ base64: croppedImageData.base64,
369
367
  });
370
- console.log('[FullDocScanner] Rotated image saved:', {
371
- path: rotatedImage.path,
372
- hasBase64: !!rotatedImage.data,
373
- base64Length: rotatedImage.data?.length,
374
- });
375
- // 회전된 이미지로 교체
376
- setCroppedImageData({
377
- path: rotatedImage.path,
378
- base64: rotatedImage.data ?? undefined,
368
+ return;
369
+ }
370
+ // 회전이 있으면 실제 파일 회전 처리
371
+ try {
372
+ setProcessing(true);
373
+ // 회전 각도 정규화 (0, 90, 180, 270)
374
+ const rotationNormalized = ((rotationDegrees % 360) + 360) % 360;
375
+ // OpenCV 회전 코드 매핑
376
+ let rotateCode;
377
+ if (rotationNormalized === 90) {
378
+ rotateCode = react_native_fast_opencv_1.RotateFlags.ROTATE_90_CLOCKWISE;
379
+ }
380
+ else if (rotationNormalized === 180) {
381
+ rotateCode = react_native_fast_opencv_1.RotateFlags.ROTATE_180;
382
+ }
383
+ else if (rotationNormalized === 270 || rotationNormalized === -90) {
384
+ rotateCode = react_native_fast_opencv_1.RotateFlags.ROTATE_90_COUNTERCLOCKWISE;
385
+ }
386
+ else {
387
+ // 회전 없음
388
+ onResult({
389
+ path: croppedImageData.path,
390
+ base64: croppedImageData.base64,
391
+ });
392
+ setProcessing(false);
393
+ return;
394
+ }
395
+ // 이미지 파일을 Mat 객체로 로드
396
+ const srcMat = await react_native_fast_opencv_1.OpenCV.invoke('imread', croppedImageData.path);
397
+ // 빈 Mat 객체 생성 (회전된 이미지 저장용)
398
+ const dstMat = new react_native_fast_opencv_1.OpenCV.Mat();
399
+ // OpenCV로 이미지 회전
400
+ react_native_fast_opencv_1.OpenCV.invoke('rotate', srcMat, dstMat, rotateCode);
401
+ // 회전된 이미지를 임시 파일로 저장
402
+ const rotatedPath = `${react_native_fs_1.default.CachesDirectoryPath}/rotated_${Date.now()}.jpg`;
403
+ await react_native_fast_opencv_1.OpenCV.invoke('imwrite', rotatedPath, dstMat);
404
+ // Mat 객체 메모리 해제
405
+ srcMat.delete();
406
+ dstMat.delete();
407
+ // 회전된 이미지를 base64로 변환
408
+ const base64Data = await react_native_fs_1.default.readFile(rotatedPath, 'base64');
409
+ setProcessing(false);
410
+ // 회전된 이미지로 결과 전달
411
+ onResult({
412
+ path: rotatedPath,
413
+ base64: base64Data,
379
414
  });
380
- // rotation degrees는 0으로 리셋
381
- setRotationDegrees(0);
382
415
  }
383
416
  catch (error) {
384
- console.error('[FullDocScanner] Image rotation error:', error);
385
- // 사용자가 취소했으면 아무것도 안함
417
+ console.error('[FullDocScanner] Image rotation error on confirm:', error);
418
+ setProcessing(false);
386
419
  const errorMessage = error && typeof error === 'object' && 'message' in error ? error.message : '';
387
- if (!errorMessage.includes('cancel') && !errorMessage.includes('User cancelled')) {
388
- // 에러 메시지 표시
389
- react_native_1.Alert.alert('회전 실패', '이미지 회전 중 오류가 발생했습니다.');
390
- }
420
+ react_native_1.Alert.alert('회전 실패', '이미지 회전 중 오류가 발생했습니다: ' + errorMessage);
391
421
  }
392
- }, [croppedImageData]);
393
- const handleConfirm = (0, react_1.useCallback)(() => {
394
- if (!croppedImageData)
395
- return;
396
- onResult({
397
- path: croppedImageData.path,
398
- base64: croppedImageData.base64,
399
- });
400
- }, [croppedImageData, onResult]);
422
+ }, [croppedImageData, rotationDegrees, onResult]);
401
423
  const handleRetake = (0, react_1.useCallback)(() => {
402
424
  console.log('[FullDocScanner] Retake - clearing cropped image and resetting scanner');
403
425
  setCroppedImageData(null);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-rectangle-doc-scanner",
3
- "version": "3.100.0",
3
+ "version": "3.102.0",
4
4
  "description": "Native-backed document scanner for React Native with customizable overlays.",
5
5
  "license": "MIT",
6
6
  "main": "dist/index.js",
@@ -33,6 +33,7 @@
33
33
  "peerDependencies": {
34
34
  "react": "*",
35
35
  "react-native": "*",
36
+ "react-native-fast-opencv": "*",
36
37
  "react-native-fs": "*",
37
38
  "react-native-image-crop-picker": "*",
38
39
  "react-native-image-picker": "*",
@@ -12,6 +12,7 @@ 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 { OpenCV, RotateFlags } from 'react-native-fast-opencv';
15
16
  import { DocScanner } from './DocScanner';
16
17
  import type { CapturedDocument } from './types';
17
18
  import type {
@@ -455,66 +456,89 @@ export const FullDocScanner: React.FC<FullDocScannerProps> = ({
455
456
  setFlashEnabled(prev => !prev);
456
457
  }, []);
457
458
 
458
- const handleRotateImage = useCallback(async (degrees: -90 | 90) => {
459
- if (!croppedImageData) return;
460
-
461
- console.log('[FullDocScanner] Starting image rotation...', {
462
- path: croppedImageData.path,
463
- hasBase64: !!croppedImageData.base64,
464
- base64Length: croppedImageData.base64?.length,
459
+ const handleRotateImage = useCallback((degrees: -90 | 90) => {
460
+ // UI만 회전 (실제 파일 회전은 confirm 시 처리)
461
+ setRotationDegrees(prev => {
462
+ const newRotation = prev + degrees;
463
+ // -360 ~ 360 범위로 정규화
464
+ if (newRotation <= -360) return newRotation + 360;
465
+ if (newRotation >= 360) return newRotation - 360;
466
+ return newRotation;
465
467
  });
468
+ }, []);
466
469
 
467
- try {
468
- // 원본 파일을 ImageCropPicker로 열어서 회전 적용
469
- // 사용자가 수동으로 회전 버튼을 클릭하고 완료를 눌러야 함
470
- const rotatedImage = await ImageCropPicker.openCropper({
470
+ const handleConfirm = useCallback(async () => {
471
+ if (!croppedImageData) return;
472
+
473
+ // 회전이 없으면 기존 데이터 그대로 전달
474
+ if (rotationDegrees === 0) {
475
+ onResult({
471
476
  path: croppedImageData.path,
472
- mediaType: 'photo',
473
- cropping: true,
474
- freeStyleCropEnabled: true,
475
- includeBase64: true,
476
- compressImageQuality: 0.9,
477
- cropperToolbarTitle: degrees === 90 ? '우측 90° 회전 → 회전 버튼 클릭 → 완료' : '좌측 90° 회전 → 회전 버튼 클릭 → 완료',
478
- cropperChooseText: '완료',
479
- cropperCancelText: '취소',
480
- cropperRotateButtonsHidden: false,
481
- enableRotationGesture: true,
477
+ base64: croppedImageData.base64,
482
478
  });
479
+ return;
480
+ }
483
481
 
484
- console.log('[FullDocScanner] Rotated image saved:', {
485
- path: rotatedImage.path,
486
- hasBase64: !!rotatedImage.data,
487
- base64Length: rotatedImage.data?.length,
488
- });
482
+ // 회전이 있으면 실제 파일 회전 처리
483
+ try {
484
+ setProcessing(true);
485
+
486
+ // 회전 각도 정규화 (0, 90, 180, 270)
487
+ const rotationNormalized = ((rotationDegrees % 360) + 360) % 360;
488
+
489
+ // OpenCV 회전 코드 매핑
490
+ let rotateCode: RotateFlags;
491
+ if (rotationNormalized === 90) {
492
+ rotateCode = RotateFlags.ROTATE_90_CLOCKWISE;
493
+ } else if (rotationNormalized === 180) {
494
+ rotateCode = RotateFlags.ROTATE_180;
495
+ } else if (rotationNormalized === 270 || rotationNormalized === -90) {
496
+ rotateCode = RotateFlags.ROTATE_90_COUNTERCLOCKWISE;
497
+ } else {
498
+ // 회전 없음
499
+ onResult({
500
+ path: croppedImageData.path,
501
+ base64: croppedImageData.base64,
502
+ });
503
+ setProcessing(false);
504
+ return;
505
+ }
489
506
 
490
- // 회전된 이미지로 교체
491
- setCroppedImageData({
492
- path: rotatedImage.path,
493
- base64: rotatedImage.data ?? undefined,
494
- });
507
+ // 이미지 파일을 Mat 객체로 로드
508
+ const srcMat = await OpenCV.invoke('imread', croppedImageData.path);
509
+
510
+ // Mat 객체 생성 (회전된 이미지 저장용)
511
+ const dstMat = new OpenCV.Mat();
512
+
513
+ // OpenCV로 이미지 회전
514
+ OpenCV.invoke('rotate', srcMat, dstMat, rotateCode);
515
+
516
+ // 회전된 이미지를 임시 파일로 저장
517
+ const rotatedPath = `${RNFS.CachesDirectoryPath}/rotated_${Date.now()}.jpg`;
518
+ await OpenCV.invoke('imwrite', rotatedPath, dstMat);
519
+
520
+ // Mat 객체 메모리 해제
521
+ srcMat.delete();
522
+ dstMat.delete();
523
+
524
+ // 회전된 이미지를 base64로 변환
525
+ const base64Data = await RNFS.readFile(rotatedPath, 'base64');
495
526
 
496
- // rotation degrees는 0으로 리셋
497
- setRotationDegrees(0);
527
+ setProcessing(false);
528
+
529
+ // 회전된 이미지로 결과 전달
530
+ onResult({
531
+ path: rotatedPath,
532
+ base64: base64Data,
533
+ });
498
534
  } catch (error) {
499
- console.error('[FullDocScanner] Image rotation error:', error);
535
+ console.error('[FullDocScanner] Image rotation error on confirm:', error);
536
+ setProcessing(false);
500
537
 
501
- // 사용자가 취소했으면 아무것도 안함
502
538
  const errorMessage = error && typeof error === 'object' && 'message' in error ? (error as Error).message : '';
503
- if (!errorMessage.includes('cancel') && !errorMessage.includes('User cancelled')) {
504
- // 에러 메시지 표시
505
- Alert.alert('회전 실패', '이미지 회전 중 오류가 발생했습니다.');
506
- }
539
+ Alert.alert('회전 실패', '이미지 회전 중 오류가 발생했습니다: ' + errorMessage);
507
540
  }
508
- }, [croppedImageData]);
509
-
510
- const handleConfirm = useCallback(() => {
511
- if (!croppedImageData) return;
512
-
513
- onResult({
514
- path: croppedImageData.path,
515
- base64: croppedImageData.base64,
516
- });
517
- }, [croppedImageData, onResult]);
541
+ }, [croppedImageData, rotationDegrees, onResult]);
518
542
 
519
543
  const handleRetake = useCallback(() => {
520
544
  console.log('[FullDocScanner] Retake - clearing cropped image and resetting scanner');
@@ -0,0 +1,20 @@
1
+ declare module 'react-native-fast-opencv' {
2
+ export enum RotateFlags {
3
+ ROTATE_90_CLOCKWISE = 0,
4
+ ROTATE_180 = 1,
5
+ ROTATE_90_COUNTERCLOCKWISE = 2
6
+ }
7
+
8
+ export class Mat {
9
+ delete(): void;
10
+ }
11
+
12
+ export interface OpenCVModel {
13
+ Mat: typeof Mat;
14
+ invoke(name: 'imread', path: string): Promise<Mat>;
15
+ invoke(name: 'imwrite', path: string, mat: Mat): Promise<void>;
16
+ invoke(name: 'rotate', src: Mat, dst: Mat, code: RotateFlags): void;
17
+ }
18
+
19
+ export const OpenCV: OpenCVModel;
20
+ }