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