diff --git a/README.md b/README.md index 8801869..25cc39e 100644 --- a/README.md +++ b/README.md @@ -101,6 +101,7 @@ Above is a very barebones version of the scanner. Check out a full example in [e | Prop | Default | Type | Description | | :-------------------------- | :-----: | :-------: | :--------------------------------------------------------- | | filterId | `none` | `integer` | The id of the filter to use. [See More](#filters) | +| cameraId | `none` | `integer` | The id of the camera to use. [See More](#cameras) | | enableTorch | `false` | `bool` | If the flashlight should be turned on | | capturedQuality | `0.5` | `float` | The jpeg quality of the output images | | onTorchChanged | `null` | `func` | Called when the system changes the flash state | @@ -146,7 +147,19 @@ NOTE: There is no UI changes when you capture an image. No screen flash, only a **NOTE**: captured images are stored in the app's cache directory under the `CACHE_FOLDER_NAME`. This allows you to clear the cached images when you are done. (This is advised although these may get deleted by the system.) -**NOTE**: on iOS, it will try to correct the rotation of the image. If you are in portrait mode, but the phone is rotated to landscape, it will rotate the captured image automatically. +**NOTE**: on iOS, it will try to correct the rotation of the image. If you are in portrait mode, but the phone is rotated to landscape, it will rotate the captured image automatically. + +### Cameras +This packages supports using different camera types. Currently there is support for at least 2 camera types (Front and Back) with the potential to support additional (Telephoto and UltraWide). The default is the generic back camera. The supported camera types will be returned in the `onDeviceSetup({ supportedCameras })` callback. **NOTE**: On iOS you can switch the camera without unmounting the component, on Android, you need to unmount and remount the component(I'm hoping to find a fix for this). + +Camera devices. You can import this list via `import { POSSIBLE_CAMERA_TYPES } from 'react-native-rectangle-scanner'` or individually by the names below. + +| ID | Name | Import Name | +| -- | ---------- | -------------------------- | +| 1 | Back | `BACK_FACING_CAMERA_TYPE` | +| 2 | Front | `FRONT_FACING_CAMERA_TYPE` | +| 3 | Telephoto | `TELEPHOTO_CAMERA_TYPE` | +| 4 | UltraWide | `ULTRA_WIDE_CAMERA_TYPE` | ### Filters Instead of allowing you to customize the contrast, saturation, etc of the image, I prebuilt the filters. This is because the filter controls are massively different between platforms and changing those values results in much different image outputs. Below are the avilable filters. Honestly, the color controls where pretty bad on android, so the best ones for android are probably just using the Color and Black & White instead of showing all 4 (they are only slightly better than Greyscale and the original photo). diff --git a/android/src/main/java/com/rectanglescanner/RNRectangleScannerManager.java b/android/src/main/java/com/rectanglescanner/RNRectangleScannerManager.java index 4537573..16d4d9b 100755 --- a/android/src/main/java/com/rectanglescanner/RNRectangleScannerManager.java +++ b/android/src/main/java/com/rectanglescanner/RNRectangleScannerManager.java @@ -48,6 +48,11 @@ public void setFilterId(MainView view, int filterId) { view.setFilterId(filterId); } + @ReactProp(name = "cameraId", defaultInt = 1) + public void setCameraId(MainView view, int cameraId) { + view.setCameraId(cameraId); + } + // Life cycle Events @Override public @Nullable Map getExportedCustomDirectEventTypeConstants() { diff --git a/android/src/main/java/com/rectanglescanner/views/CameraDeviceController.java b/android/src/main/java/com/rectanglescanner/views/CameraDeviceController.java index 43d98c9..72dcf27 100644 --- a/android/src/main/java/com/rectanglescanner/views/CameraDeviceController.java +++ b/android/src/main/java/com/rectanglescanner/views/CameraDeviceController.java @@ -20,6 +20,9 @@ import android.content.res.Configuration; import android.widget.FrameLayout; +import com.facebook.react.bridge.Arguments; +import com.facebook.react.bridge.ReadableArray; +import com.facebook.react.bridge.WritableArray; import com.rectanglescanner.R; import com.facebook.react.bridge.WritableMap; import com.facebook.react.bridge.WritableNativeMap; @@ -57,10 +60,17 @@ public class CameraDeviceController extends JavaCameraView implements PictureCal protected boolean isStopped = true; private WritableMap deviceConfiguration = new WritableNativeMap(); private int captureDevice = -1; + private int cameraId = 1; private boolean imageProcessorBusy = true; private static CameraDeviceController mThis; + public int getCameraId() { + return this.cameraId; + } + + + public CameraDeviceController(Context context, AttributeSet attrs) { super(context, attrs); } @@ -79,6 +89,13 @@ public CameraDeviceController(Context context, Integer numCam, Activity activity // Setters //================================================================================ + /** + Sets the currently active camera + */ + public void setCameraId(int cameraId) { + this.cameraId = cameraId; + } + public boolean isFocused() { return this.mFocused; } @@ -126,8 +143,8 @@ private void refreshCamera() { Starts the capture session */ public void startCamera() { - Log.d(TAG, "Starting preview"); if (this.isStopped) { + Log.d(TAG, "Starting preview"); try { if (!this.cameraIsSetup) { setupCameraView(); @@ -190,7 +207,15 @@ public void setDeviceConfigurationPreviewPercentSize(double heightPercent, doubl } /** - Sets the inital device configuration + * Sets the supported camera type ids + * @param supportedCameras + */ + public void setDeviceConfigurationSupportedCameras(ReadableArray supportedCameras) { + this.deviceConfiguration.putArray("supportedCameras", supportedCameras); + } + + /** + Sets the initial device configuration */ public void resetDeviceConfiguration() { @@ -199,6 +224,7 @@ public void resetDeviceConfiguration() setDeviceConfigurationPermissionToUseCamera(false); setDeviceConfigurationHasCamera(false); setDeviceConfigurationPreviewPercentSize(1.0, 1.0); + setDeviceConfigurationSupportedCameras(Arguments.createArray()); } /** @@ -214,8 +240,40 @@ protected void deviceWasSetup(WritableMap config) {} // Getters //================================================================================ + private void searchForAvailableCameraTypes() { + WritableArray availableCameras = Arguments.createArray(); + boolean foundBackFacing = false; + boolean foundFrontFacing = false; + + // Search for the back facing camera + // get the number of cameras + int numberOfCameras = Camera.getNumberOfCameras(); + // for every camera check + for (int i = 0; i < numberOfCameras; i++) { + Camera.CameraInfo info = new Camera.CameraInfo(); + Camera.getCameraInfo(i, info); + + if (info.facing == Camera.CameraInfo.CAMERA_FACING_BACK && !foundBackFacing) { + availableCameras.pushInt(1); + foundBackFacing = true; + } + + if (info.facing == Camera.CameraInfo.CAMERA_FACING_FRONT && !foundFrontFacing) { + availableCameras.pushInt(2); + foundFrontFacing = true; + } + + if (foundBackFacing && foundFrontFacing) { + break; + } + } + setDeviceConfigurationSupportedCameras(availableCameras); + } + private int getCameraDevice() { int cameraId = -1; + + // Search for the back facing camera // get the number of cameras int numberOfCameras = Camera.getNumberOfCameras(); @@ -223,11 +281,16 @@ private int getCameraDevice() { for (int i = 0; i < numberOfCameras; i++) { Camera.CameraInfo info = new Camera.CameraInfo(); Camera.getCameraInfo(i, info); - if (info.facing == Camera.CameraInfo.CAMERA_FACING_BACK) { + + if (info.facing == Camera.CameraInfo.CAMERA_FACING_BACK && this.getCameraId() == 1) { + cameraId = i; + break; + } + + if (info.facing == Camera.CameraInfo.CAMERA_FACING_FRONT && this.getCameraId() == 2) { cameraId = i; break; } - cameraId = i; } return cameraId; } @@ -418,13 +481,15 @@ public boolean setupCaptureDevice() { this.captureDevice = getCameraDevice(); try { - int cameraId = getCameraDevice(); + int cameraId = this.captureDevice; + if (cameraId < 0) return false; mCamera = Camera.open(cameraId); } catch (RuntimeException e) { System.err.println(e); return false; } setDeviceConfigurationHasCamera(true); + searchForAvailableCameraTypes(); PackageManager pm = mActivity.getPackageManager(); if (pm.hasSystemFeature(PackageManager.FEATURE_CAMERA_FLASH)) { diff --git a/android/src/main/java/com/rectanglescanner/views/MainView.java b/android/src/main/java/com/rectanglescanner/views/MainView.java index 864d5bc..195c07f 100755 --- a/android/src/main/java/com/rectanglescanner/views/MainView.java +++ b/android/src/main/java/com/rectanglescanner/views/MainView.java @@ -54,6 +54,9 @@ public void setCapturedQuality(double quality) { public void setFilterId(int filterId) { view.setFilterId(filterId); } + public void setCameraId(int cameraId) { + view.setCameraId(cameraId); + } public void startCamera() { view.startCamera(); diff --git a/ios/CameraDeviceController.h b/ios/CameraDeviceController.h index 001bca3..e712903 100644 --- a/ios/CameraDeviceController.h +++ b/ios/CameraDeviceController.h @@ -38,6 +38,7 @@ @property (nonatomic, assign) UIInterfaceOrientation lastInterfaceOrientation; @property (nonatomic, assign) int filterId; +@property (nonatomic, assign) int cameraId; @property (nonatomic,strong) EAGLContext *context; @property (nonatomic, strong) CIContext *_coreImageContext; diff --git a/ios/CameraDeviceController.m b/ios/CameraDeviceController.m index 90b6047..441fcb5 100644 --- a/ios/CameraDeviceController.m +++ b/ios/CameraDeviceController.m @@ -168,6 +168,20 @@ - (void)setFilterId:(int)filterId _filterId = filterId; } +/*! + Sets the currently active camera + */ +- (void)setCameraId:(int)cameraId +{ + _cameraId = cameraId; + if(self._cameraIsSetup) { + self._cameraIsSetup = FALSE; + [self stop]; + [self setupCameraView]; + [self start]; + } +} + /*! Sets the device configuration flash setting */ @@ -189,6 +203,18 @@ - (void)_setDeviceConfigurationHasCamera: (BOOL) isAvailable{ [_deviceConfiguration setValue:isAvailable ? @TRUE : @FALSE forKey:@"hasCamera"]; } +/*! + Sets the device configuration supported cameras + */ +- (void)_setDeviceConfigurationSupportedCameras { + NSMutableArray *supportedCameras = [NSMutableArray array]; + if ([self getDefaultBackFacingCamera]) [supportedCameras addObject:@(1)]; + if ([self getFrontFacingCamera]) [supportedCameras addObject:@(2)]; + if ([self getTelephotoBackFacingCamera]) [supportedCameras addObject:@(3)]; + if ([self getUltraWideAngleBackFacingCamera]) [supportedCameras addObject:@(4)]; + [_deviceConfiguration setValue: supportedCameras forKey: @"supportedCameras"]; +} + /*! Sets the inital device configuration */ @@ -200,6 +226,7 @@ - (void)_resetDeviceConfiguration [self _setDeviceConfigurationHasCamera:NO]; [_deviceConfiguration setValue: @1.0 forKey: @"previewHeightPercent"]; [_deviceConfiguration setValue: @1.0 forKey: @"previewWidthPercent"]; + [_deviceConfiguration setValue: @[] forKey: @"supportedCameras"]; } /*! @@ -295,6 +322,49 @@ - (int)getCGImageOrientationForCaptureImage } } +/*! + Searches for a generic wide angle camera on the back of the phone + */ +- (AVCaptureDevice *)getDefaultBackFacingCamera{ + AVCaptureDevice* possibleDevice = [AVCaptureDevice defaultDeviceWithDeviceType:AVCaptureDeviceTypeBuiltInWideAngleCamera mediaType:AVMediaTypeVideo position:AVCaptureDevicePositionBack]; + + if (possibleDevice) return possibleDevice; + return nil; +} + +/*! +Searches for a generic wide angle camera on the front of the phone +*/ +- (AVCaptureDevice *)getFrontFacingCamera{ + AVCaptureDevice* possibleDevice = [AVCaptureDevice defaultDeviceWithDeviceType:AVCaptureDeviceTypeBuiltInWideAngleCamera mediaType:AVMediaTypeVideo position:AVCaptureDevicePositionFront]; + + if (possibleDevice) return possibleDevice; + return nil; +} + +/*! + Searches for the ultra wide angle camera on the back of the phone + @note This may need to be refactored to actually find the desired camera +*/ +- (AVCaptureDevice *)getUltraWideAngleBackFacingCamera{ + if (@available(iOS 13.0, *)) { + AVCaptureDevice* possibleDevice = [AVCaptureDevice defaultDeviceWithDeviceType:AVCaptureDeviceTypeBuiltInUltraWideCamera mediaType:AVMediaTypeVideo position:AVCaptureDevicePositionBack]; + + if (possibleDevice) return possibleDevice; + } + return nil; +} + +/*! + Searches for the telephoto camera on the back of the phone + @note This may need to be refactored to actually find the desired camera +*/ +- (AVCaptureDevice *)getTelephotoBackFacingCamera{ + AVCaptureDevice* possibleDevice = [AVCaptureDevice defaultDeviceWithDeviceType:AVCaptureDeviceTypeBuiltInTelephotoCamera mediaType:AVMediaTypeVideo position:AVCaptureDevicePositionBack]; + + if (possibleDevice) return possibleDevice; + return nil; +} /*! Gets a hardware camera device. @@ -302,7 +372,23 @@ - (int)getCGImageOrientationForCaptureImage */ - (AVCaptureDevice *)getCameraDevice{ AVCaptureDevice* possibleDevice; - possibleDevice = [AVCaptureDevice defaultDeviceWithDeviceType:AVCaptureDeviceTypeBuiltInWideAngleCamera mediaType:AVMediaTypeVideo position:AVCaptureDevicePositionBack]; + + switch (self.cameraId) { + case 1: + possibleDevice = [self getDefaultBackFacingCamera]; + break; + case 2: + possibleDevice = [self getFrontFacingCamera]; + break; + case 3: + possibleDevice = [self getTelephotoBackFacingCamera]; + break; + case 4: + possibleDevice = [self getUltraWideAngleBackFacingCamera]; + break; + default: + possibleDevice = [self getDefaultBackFacingCamera]; + } if (possibleDevice) return possibleDevice; return nil; @@ -389,6 +475,7 @@ - (AVCaptureDevice *)setupCaptureDevice{ if (!self.captureDevice) return nil; [self _setDeviceConfigurationHasCamera:YES]; [self _setDeviceConfigurationFlashAvailable:([self.captureDevice hasTorch] && [self.captureDevice hasFlash])]; + [self _setDeviceConfigurationSupportedCameras]; // Setup camera focus mode if ([self.captureDevice isFocusModeSupported:AVCaptureFocusModeContinuousAutoFocus]) diff --git a/ios/RNRectangleScannerManager.m b/ios/RNRectangleScannerManager.m index 45d8f37..596a874 100644 --- a/ios/RNRectangleScannerManager.m +++ b/ios/RNRectangleScannerManager.m @@ -40,6 +40,11 @@ Determines what filter id to use (1, 2, 3, or 4) */ RCT_EXPORT_VIEW_PROPERTY(filterId, int) +/*! + Determines what camera id to use. Depends on the phone type + */ +RCT_EXPORT_VIEW_PROPERTY(cameraId, int) + // MARK: Life cycle Actions /*! diff --git a/src/Scanner.js b/src/Scanner.js index 34d9eff..ec2b473 100644 --- a/src/Scanner.js +++ b/src/Scanner.js @@ -10,6 +10,34 @@ import { const RNRectangleScanner = requireNativeComponent('RNRectangleScanner'); const CameraManager = NativeModules.RNRectangleScannerManager || {}; +export const BACK_FACING_CAMERA_TYPE = { + id: 1, + name: 'Back', +}; + +export const FRONT_FACING_CAMERA_TYPE = { + id: 2, + name: 'Front', +}; + +export const TELEPHOTO_CAMERA_TYPE = { + id: 3, + name: 'Telephoto', +}; + +export const ULTRA_WIDE_CAMERA_TYPE = { + id: 4, + name: 'UltraWide', +}; + +export const DEFAULT_CAMERA_TYPE = BACK_FACING_CAMERA_TYPE; +export const POSSIBLE_CAMERA_TYPES = [ + BACK_FACING_CAMERA_TYPE, + FRONT_FACING_CAMERA_TYPE, + TELEPHOTO_CAMERA_TYPE, + ULTRA_WIDE_CAMERA_TYPE, +]; + class Scanner extends React.Component { static propTypes = { onPictureTaken: PropTypes.func, @@ -31,10 +59,6 @@ class Scanner extends React.Component { capturedQuality: 0.5, } - constructor(props) { - super(props); - } - componentDidMount() { if (Platform.OS === 'android') { this.askForAndroidCameraForPermission(this.start);