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