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