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