1// Copyright 2016-present 650 Industries. All rights reserved. 2 3#import <EXBarCodeScanner/EXBarCodeScanner.h> 4#import <EXBarCodeScanner/EXBarCodeScannerUtils.h> 5#import <UMBarCodeScannerInterface/UMBarCodeScannerInterface.h> 6#import <UMCore/UMDefines.h> 7#import <ZXingObjC/ZXingObjCCore.h> 8#import <ZXingObjC/ZXingObjCPDF417.h> 9#import <ZXingObjC/ZXingObjCOneD.h> 10 11@interface EXBarCodeScanner() <AVCaptureMetadataOutputObjectsDelegate, AVCaptureVideoDataOutputSampleBufferDelegate> 12 13@property (nonatomic, strong) AVCaptureMetadataOutput *metadataOutput; 14@property (nonatomic, weak) AVCaptureSession *session; 15@property (nonatomic, weak) dispatch_queue_t sessionQueue; 16@property (nonatomic, copy, nullable) void (^onBarCodeScanned)(NSDictionary*); 17@property (nonatomic, assign, getter=isScanningBarCodes) BOOL barCodesScanning; 18@property (nonatomic, strong) NSDictionary<NSString *, id> *settings; 19@property (nonatomic, weak) AVCaptureVideoPreviewLayer *previewLayer; 20 21@property (nonatomic, strong) NSDictionary<NSString *, id<ZXReader>> *zxingBarcodeReaders; 22@property (nonatomic, assign) CGFloat zxingFPSProcessed; 23@property (nonatomic, strong) AVCaptureVideoDataOutput* videoDataOutput; 24@property (nonatomic, strong) dispatch_queue_t zxingCaptureQueue; 25@property (nonatomic, assign) BOOL zxingEnabled; 26 27@end 28 29NSString *const EX_BARCODE_TYPES_KEY = @"barCodeTypes"; 30 31@implementation EXBarCodeScanner 32 33- (instancetype)init 34{ 35 if (self = [super init]) { 36 _settings = [[NSMutableDictionary alloc] initWithDictionary:[[self class] _getDefaultSettings]]; 37 38 // zxing handles barcodes reading of following types: 39 _zxingBarcodeReaders = @{ 40 // PDF417 - built-in PDF417 reader doesn't handle u'\0' (null) character - https://github.com/expo/expo/issues/4817 41 AVMetadataObjectTypePDF417Code: [ZXPDF417Reader new], 42 // Code39 - built-in Code39 reader doesn't read non-ideal (slightly rotated) images like this - https://github.com/expo/expo/pull/5976#issuecomment-545001008 43 AVMetadataObjectTypeCode39Code: [ZXCode39Reader new], 44 }; 45 _zxingFPSProcessed = 6; 46 _zxingCaptureQueue = dispatch_queue_create("com.zxing.captureQueue", NULL); 47 _zxingEnabled = YES; 48 } 49 return self; 50} 51 52# pragma mark - JS properties setters 53 54- (void)setSettings:(NSDictionary<NSString *, id> *)settings 55{ 56 for (NSString *key in settings) { 57 if ([key isEqualToString:EX_BARCODE_TYPES_KEY]) { 58 NSArray<NSString *> *value = settings[key]; 59 NSSet *previousTypes = [NSSet setWithArray:_settings[EX_BARCODE_TYPES_KEY]]; 60 NSSet *newTypes = [NSSet setWithArray:value]; 61 if (![previousTypes isEqualToSet:newTypes]) { 62 NSMutableDictionary<NSString *, id> *nextSettings = [[NSMutableDictionary alloc] initWithDictionary:_settings]; 63 nextSettings[EX_BARCODE_TYPES_KEY] = value; 64 _settings = nextSettings; 65 NSSet *zxingCoveredTypes = [NSSet setWithArray:[_zxingBarcodeReaders allKeys]]; 66 _zxingEnabled = [zxingCoveredTypes intersectsSet:newTypes]; 67 UM_WEAKIFY(self); 68 [self _runBlockIfQueueIsPresent:^{ 69 UM_ENSURE_STRONGIFY(self); 70 [self maybeStartBarCodeScanning]; 71 }]; 72 } 73 } 74 } 75} 76 77- (void)setIsEnabled:(BOOL)newBarCodeScanning 78{ 79 if ([self isScanningBarCodes] == newBarCodeScanning) { 80 return; 81 } 82 _barCodesScanning = newBarCodeScanning; 83 UM_WEAKIFY(self); 84 [self _runBlockIfQueueIsPresent:^{ 85 UM_ENSURE_STRONGIFY(self); 86 if ([self isScanningBarCodes]) { 87 if (self.metadataOutput) { 88 [self _setConnectionsEnabled:true]; 89 } else { 90 [self maybeStartBarCodeScanning]; 91 } 92 } else { 93 [self _setConnectionsEnabled:false]; 94 } 95 }]; 96} 97 98# pragma mark - Public API 99 100- (void)maybeStartBarCodeScanning 101{ 102 if (!_session || !_sessionQueue || ![self isScanningBarCodes]) { 103 return; 104 } 105 106 if (!_metadataOutput || !_videoDataOutput) { 107 [_session beginConfiguration]; 108 109 if (!_metadataOutput) { 110 AVCaptureMetadataOutput *metadataOutput = [[AVCaptureMetadataOutput alloc] init]; 111 [metadataOutput setMetadataObjectsDelegate:self queue:_sessionQueue]; 112 if ([_session canAddOutput:metadataOutput]) { 113 [_session addOutput:metadataOutput]; 114 _metadataOutput = metadataOutput; 115 } 116 } 117 118 if (!_videoDataOutput) { 119 AVCaptureVideoDataOutput *videoDataOutput = [AVCaptureVideoDataOutput new]; 120 [videoDataOutput setVideoSettings:@{ 121 (NSString *)kCVPixelBufferPixelFormatTypeKey: [NSNumber numberWithUnsignedInt:kCVPixelFormatType_32BGRA], 122 }]; 123 [videoDataOutput setAlwaysDiscardsLateVideoFrames:YES]; 124 [videoDataOutput setSampleBufferDelegate:self queue:_zxingCaptureQueue]; 125 if ([_session canAddOutput:videoDataOutput]) { 126 [_session addOutput:videoDataOutput]; 127 _videoDataOutput = videoDataOutput; 128 } 129 } 130 131 [_session commitConfiguration]; 132 133 if (!_metadataOutput) { 134 return; 135 } 136 } 137 138 NSArray<AVMetadataObjectType> *availableRequestedObjectTypes = @[]; 139 NSArray<AVMetadataObjectType> *requestedObjectTypes = @[]; 140 NSArray<AVMetadataObjectType> *availableObjectTypes = _metadataOutput.availableMetadataObjectTypes; 141 if (_settings && _settings[EX_BARCODE_TYPES_KEY]) { 142 requestedObjectTypes = [[NSArray alloc] initWithArray:_settings[EX_BARCODE_TYPES_KEY]]; 143 } 144 145 for(AVMetadataObjectType objectType in requestedObjectTypes) { 146 if ([availableObjectTypes containsObject:objectType]) { 147 availableRequestedObjectTypes = [availableRequestedObjectTypes arrayByAddingObject:objectType]; 148 } 149 } 150 151 [_metadataOutput setMetadataObjectTypes:availableRequestedObjectTypes]; 152} 153 154- (void)stopBarCodeScanning 155{ 156 if (!_session) { 157 return; 158 } 159 160 [_session beginConfiguration]; 161 162 if ([_session.outputs containsObject:_metadataOutput]) { 163 [_session removeOutput:_metadataOutput]; 164 _metadataOutput = nil; 165 } 166 167 if ([_session.outputs containsObject:_videoDataOutput]) { 168 [_session removeOutput:_videoDataOutput]; 169 _videoDataOutput = nil; 170 } 171 172 [_session commitConfiguration]; 173 174 if ([self isScanningBarCodes] && _onBarCodeScanned) { 175 _onBarCodeScanned(nil); 176 } 177} 178 179# pragma mark - Private API 180 181- (void)_setConnectionsEnabled:(BOOL)enabled 182{ 183 if (!_metadataOutput) { 184 return; 185 } 186 for (AVCaptureConnection *connection in _metadataOutput.connections) { 187 connection.enabled = enabled; 188 } 189} 190 191- (void)_runBlockIfQueueIsPresent:(void (^)(void))block 192{ 193 if (_sessionQueue) { 194 dispatch_async(_sessionQueue, block); 195 } 196} 197 198# pragma mark - AVCaptureMetadataOutputObjectsDelegate 199 200- (void)captureOutput:(AVCaptureOutput *)captureOutput didOutputMetadataObjects:(NSArray *)metadataObjects 201 fromConnection:(AVCaptureConnection *)connection 202{ 203 if (!_settings || !_settings[EX_BARCODE_TYPES_KEY] || !_metadataOutput) { 204 return; 205 } 206 207 for (AVMetadataObject *metadata in metadataObjects) { 208 if ([metadata isKindOfClass:[AVMetadataMachineReadableCodeObject class]]) { 209 AVMetadataMachineReadableCodeObject *codeMetadata; 210 if (_previewLayer) { 211 codeMetadata = (AVMetadataMachineReadableCodeObject *)[_previewLayer transformedMetadataObjectForMetadataObject:metadata]; 212 } else { 213 codeMetadata = (AVMetadataMachineReadableCodeObject *)metadata; 214 } 215 216 for (id barcodeType in _settings[EX_BARCODE_TYPES_KEY]) { 217 // some barcodes aren't handled properly by iOS SDK build-in reader -> zxing handles it in separate flow 218 if ([_zxingBarcodeReaders objectForKey:barcodeType]) { 219 continue; 220 } 221 if (codeMetadata.stringValue && [codeMetadata.type isEqualToString:barcodeType]) { 222 if (_onBarCodeScanned) { 223 _onBarCodeScanned([EXBarCodeScannerUtils avMetadataCodeObjectToDicitionary:codeMetadata]); 224 } 225 return; 226 } 227 } 228 } 229 } 230} 231 232# pragma mark - AVCaptureVideoDataOutputSampleBufferDelegate for ZXing 233 234- (void)captureOutput:(AVCaptureVideoDataOutput *)output 235didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer 236 fromConnection:(AVCaptureConnection *)connection 237{ 238 if (!_settings || !_settings[EX_BARCODE_TYPES_KEY] || !_metadataOutput) { 239 return; 240 } 241 // do not use ZXing library if not scanning for predefined barcodes 242 if (!_zxingEnabled) { 243 return; 244 } 245 246 // below code is mostly taken from ZXing library itself 247 float kMinMargin = 1.0 / _zxingFPSProcessed; 248 249 // Gets the timestamp for each frame. 250 CMTime presentTimeStamp = CMSampleBufferGetPresentationTimeStamp(sampleBuffer); 251 252 @autoreleasepool { 253 static double curFrameTimeStamp = 0; 254 static double lastFrameTimeStamp = 0; 255 256 curFrameTimeStamp = (double)presentTimeStamp.value / presentTimeStamp.timescale; 257 258 if (curFrameTimeStamp - lastFrameTimeStamp > kMinMargin) { 259 lastFrameTimeStamp = curFrameTimeStamp; 260 261 CVImageBufferRef videoFrame = CMSampleBufferGetImageBuffer(sampleBuffer); 262 CGImageRef videoFrameImage = [ZXCGImageLuminanceSource createImageFromBuffer:videoFrame]; 263 [self scanBarcodesFromImage:videoFrameImage withCompletion:^(ZXResult *barCodeScannerResult, NSError *error) { 264 if (self->_onBarCodeScanned) { 265 self->_onBarCodeScanned([EXBarCodeScannerUtils zxResultToDicitionary:barCodeScannerResult]); 266 } 267 }]; 268 } 269 } 270} 271 272- (void)scanBarcodesFromImage:(CGImageRef)image 273 withCompletion:(void(^)(ZXResult *barCodeResult, NSError *error))completion 274{ 275 ZXCGImageLuminanceSource *source = [[ZXCGImageLuminanceSource alloc] initWithCGImage:image]; 276 CGImageRelease(image); 277 278 ZXHybridBinarizer *binarizer = [[ZXHybridBinarizer alloc] initWithSource:source]; 279 ZXBinaryBitmap *bitmap = [[ZXBinaryBitmap alloc] initWithBinarizer:binarizer]; 280 281 NSError *error = nil; 282 ZXResult *result; 283 284 for (id<ZXReader> reader in [_zxingBarcodeReaders allValues]) { 285 result = [reader decode:bitmap hints:nil error:&error]; 286 if (result) { 287 break; 288 } 289 } 290 // rotate bitmap by 90° only, becasue zxing rotates bitmap by 180° internally, so that each possible orientation is covered 291 if (!result && [bitmap rotateSupported]) { 292 ZXBinaryBitmap *rotatedBitmap = [bitmap rotateCounterClockwise]; 293 for (id<ZXReader> reader in [_zxingBarcodeReaders allValues]) { 294 result = [reader decode:rotatedBitmap hints:nil error:&error]; 295 if (result) { 296 break; 297 } 298 } 299 } 300 301 if (result) { 302 completion(result, error); 303 } 304} 305 306+ (NSString *)zxingFormatToString:(ZXBarcodeFormat)format 307{ 308 switch (format) { 309 case kBarcodeFormatPDF417: 310 return AVMetadataObjectTypePDF417Code; 311 case kBarcodeFormatCode39: 312 return AVMetadataObjectTypeCode39Code; 313 default: 314 return @"unknown"; 315 } 316} 317 318# pragma mark - default settings 319 320+ (NSDictionary *)_getDefaultSettings 321{ 322 return @{ 323 EX_BARCODE_TYPES_KEY: [[EXBarCodeScannerUtils validBarCodeTypes] allValues], 324 }; 325} 326 327@end 328