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