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
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 = (AVMetadataMachineReadableCodeObject *) metadata;
209      for (id barcodeType in _settings[EX_BARCODE_TYPES_KEY]) {
210        // some barcodes aren't handled properly by iOS SDK build-in reader -> zxing handles it in separate flow
211        if ([_zxingBarcodeReaders objectForKey:barcodeType]) {
212          continue;
213        }
214        if (codeMetadata.stringValue && [codeMetadata.type isEqualToString:barcodeType]) {
215
216          NSDictionary *event = @{
217                                  @"type" : codeMetadata.type,
218                                  @"data" : codeMetadata.stringValue
219                                  };
220
221          if (_onBarCodeScanned) {
222            _onBarCodeScanned(event);
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:^(NSString* text, NSString* type, NSError* error){
263        // text contains characteres u'\0' (null character) that malforme resulting string, so we get rid of them
264        NSMutableString* data = [NSMutableString new];
265        for (int i = 0; i < [text length]; i++) {
266          if ([text characterAtIndex:i] != u'\0') {
267            [data appendFormat:@"%c", [text characterAtIndex:i]];
268          }
269        }
270        NSDictionary *event = @{
271          @"type": type,
272          @"data": data,
273        };
274        if (self->_onBarCodeScanned) {
275          self->_onBarCodeScanned(event);
276        }
277      }];
278    }
279  }
280}
281
282- (void)scanBarcodesFromImage:(CGImageRef)image
283               withCompletion:(void(^)(NSString* text, NSString* type, NSError* error))completion
284{
285  ZXCGImageLuminanceSource *source = [[ZXCGImageLuminanceSource alloc] initWithCGImage:image];
286  CGImageRelease(image);
287
288  ZXHybridBinarizer *binarizer = [[ZXHybridBinarizer alloc] initWithSource:source];
289  ZXBinaryBitmap *bitmap = [[ZXBinaryBitmap alloc] initWithBinarizer:binarizer];
290
291  NSError *error = nil;
292  ZXResult *result;
293
294  for (id<ZXReader> reader in [_zxingBarcodeReaders allValues]) {
295    result = [reader decode:bitmap hints:nil error:&error];
296    if (result) {
297      break;
298    }
299  }
300  // rotate bitmap by 90° only, becasue zxing rotates bitmap by 180° internally, so that each possible orientation is covered
301  if (!result && [bitmap rotateSupported]) {
302    ZXBinaryBitmap *rotatedBitmap = [bitmap rotateCounterClockwise];
303    for (id<ZXReader> reader in [_zxingBarcodeReaders allValues]) {
304      result = [reader decode:rotatedBitmap hints:nil error:&error];
305      if (result) {
306        break;
307      }
308    }
309  }
310
311  if (result) {
312    NSString* type = [EXBarCodeScanner zxingFormatToString:result.barcodeFormat];
313    completion(result.text, type, error);
314  }
315}
316
317+ (NSString *)zxingFormatToString:(ZXBarcodeFormat)format
318{
319  switch (format) {
320    case kBarcodeFormatPDF417:
321      return AVMetadataObjectTypePDF417Code;
322    case kBarcodeFormatCode39:
323      return AVMetadataObjectTypeCode39Code;
324    default:
325      return @"unknown";
326  }
327}
328
329# pragma mark - default settings
330
331+ (NSDictionary *)_getDefaultSettings
332{
333  return @{
334           EX_BARCODE_TYPES_KEY: [[EXBarCodeScannerUtils validBarCodeTypes] allValues],
335           };
336}
337
338@end
339