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