Objective-C製バーコードライブラリZXingObjCをSwiftで使ってハマった話

2016.05.18

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

はじめに

こんにちは! 最近は湿度が高く蒸し暑いのでげんなりしがちな加藤 潤です。
今日はObjective-Cのバーコードライブラリ ZXingObjCをSwiftで使ってみていくつかハマった所があったので共有したいと思います。

開発環境

  • Xcode 7.3.1 (7D1014)
  • Swift 2.2
  • ZXingObjC 3.1.0

ハマリポイント その1

バーコード読み取りに使用するZXCaptureのデフォルトのsessionPresetがAVCaptureSessionPresetMedium

これはライブラリの仕様なのでSwift限定の話ではありませんがハマリポイントとして挙げておきます。 バーコードの読み取りにZXCaptureを使用していましたが、カメラがぼやけていてバーコードの認識率が悪いことに気づきました。 ZXCaptureの実装を見てみると、以下のようにAVCaptureSessionPresetMediumがデフォルトのsessionPresetとして設定されていました。

@implementation ZXCapture

- (ZXCapture *)init {
  if (self = [super init]) {
    _captureDeviceIndex = -1;
    _captureQueue = dispatch_queue_create("com.zxing.captureQueue", NULL);
    _focusMode = AVCaptureFocusModeContinuousAutoFocus;
    _hardStop = NO;
    _hints = [ZXDecodeHints hints];
    _lastScannedImage = NULL;
    _onScreen = NO;
    _orderInSkip = 0;
    _orderOutSkip = 0;

    if (NSClassFromString(@"ZXMultiFormatReader")) {
      _reader = [NSClassFromString(@"ZXMultiFormatReader") performSelector:@selector(reader)];
    }

    _rotation = 0.0f;
    _running = NO;
    _sessionPreset = AVCaptureSessionPresetMedium;
    _transform = CGAffineTransformIdentity;
    _scanRect = CGRectZero;
  }

  return self;
}

この問題を解決するには、以下のようにsessionPresetAVCaptureSessionPreset1920x1080などの認識率が良い解像度に変更する必要があります。

let capture = ZXCapture()
capture.sessionPreset = AVCaptureSessionPreset1920x1080
capture.camera = capture.back()

ハマリポイント その2

バーコード生成時に指定する幅と高さはピクセルで指定する

これもライブラリの仕様ですが、ハマリポイントとして挙げておきます。
バーコードの生成にはZXWriterプロトコルに定義されている下記メソッドを使用していました。

/**
 * Encode a barcode using the default settings.
 *
 * @param contents The contents to encode in the barcode
 * @param format The barcode format to generate
 * @param width The preferred width in pixels
 * @param height The preferred height in pixels
 */
- (ZXBitMatrix *)encode:(NSString *)contents format:(ZXBarcodeFormat)format width:(int)width height:(int)height error:(NSError **)error;

引数でバーコードの幅と高さを指定するのですが、ポイントではなくピクセルで指定する必要があります。 ちゃんとメソッドのコメントにもwidth The preferred width in pixelsheight The preferred height in pixelsと書いてありますが、 「バーコードを表示するビューのframeの幅と高さを指定すればいいんでしょ?」と勝手に思い込んでるとポイントで指定してしまい、「なんかバーコードが小さい...」みたいなことが起こります。

参考までにピクセル指定のサンプルを載せておきます。 barcodeImageはバーコード表示領域のUIImageViewです。 ピクセルなのでUIScreen.mainScreen().scaleでスケールを取得して幅と高さそれぞれに掛ける必要があります。

guard let writer = ZXMultiFormatWriter.writer() as? ZXWriter else {
    return
}
let scale = UIScreen.mainScreen().scale
writer.encode(
    "9999999999999",
    format: kBarcodeFormatEan13,
    width: Int32(barcodeImage.bounds.width * scale),
    height: Int32(barcodeImage.bounds.height * scale))

ハマリポイント その3

バーコード生成時にNSExceptionが発生する可能性がある

先ほどのencodeメソッドですが、ZXMultiFormatWriterの実装を見ると、実際はパラメーターで指定したバーコード形式に応じてwriterオブジェクトを生成し、そのオブジェクトのencodeメソッドを呼んでいることがわかります。

@implementation ZXMultiFormatWriter

+ (id)writer {
  return [[ZXMultiFormatWriter alloc] init];
}

- (ZXBitMatrix *)encode:(NSString *)contents format:(ZXBarcodeFormat)format width:(int)width height:(int)height error:(NSError **)error {
  return [self encode:contents format:format width:width height:height hints:nil error:error];
}

- (ZXBitMatrix *)encode:(NSString *)contents format:(ZXBarcodeFormat)format width:(int)width height:(int)height hints:(ZXEncodeHints *)hints error:(NSError **)error {
  id<ZXWriter> writer;
  switch (format) {
    case kBarcodeFormatEan8:
      writer = [[ZXEAN8Writer alloc] init];
      break;

    case kBarcodeFormatEan13:
      writer = [[ZXEAN13Writer alloc] init];
      break;

    case kBarcodeFormatUPCA:
      writer = [[ZXUPCAWriter alloc] init];
      break;

    case kBarcodeFormatQRCode:
      writer = [[ZXQRCodeWriter alloc] init];
      break;

    case kBarcodeFormatCode39:
      writer = [[ZXCode39Writer alloc] init];
      break;

    case kBarcodeFormatCode128:
      writer = [[ZXCode128Writer alloc] init];
      break;

    case kBarcodeFormatITF:
      writer = [[ZXITFWriter alloc] init];
      break;

    case kBarcodeFormatPDF417:
      writer = [[ZXPDF417Writer alloc] init];
      break;

    case kBarcodeFormatCodabar:
      writer = [[ZXCodaBarWriter alloc] init];
      break;

    case kBarcodeFormatDataMatrix:
      writer = [[ZXDataMatrixWriter alloc] init];
      break;

    case kBarcodeFormatAztec:
      writer = [[ZXAztecWriter alloc] init];
      break;

    default:
      if (error) *error = [NSError errorWithDomain:ZXErrorDomain code:ZXWriterError userInfo:@{NSLocalizedDescriptionKey: @"No encoder available for format"}];
      return nil;
  }
  return [writer encode:contents format:format width:width height:height hints:hints error:error];
}

@end

例えば、バーコード形式がkBarcodeFormatEan13ならZXEAN13Writerのオジェクトが生成されてそのencodeが呼ばれる事になります。 ZXEAN13Writerの実装は下記のようになっています。

@implementation ZXEAN13Writer

- (ZXBitMatrix *)encode:(NSString *)contents format:(ZXBarcodeFormat)format width:(int)width height:(int)height hints:(ZXEncodeHints *)hints error:(NSError **)error {
  if (format != kBarcodeFormatEan13) {
    @throw [NSException exceptionWithName:NSInvalidArgumentException
                                   reason:[NSString stringWithFormat:@"Can only encode EAN_13, but got %d", format]
                                 userInfo:nil];
  }

  return [super encode:contents format:format width:width height:height hints:hints error:error];
}

- (ZXBoolArray *)encode:(NSString *)contents {
  if ([contents length] != 13) {
    [NSException raise:NSInvalidArgumentException
                format:@"Requested contents should be 13 digits long, but got %d", (int)[contents length]];
  }

  if (![ZXUPCEANReader checkStandardUPCEANChecksum:contents]) {
    [NSException raise:NSInvalidArgumentException
                format:@"Contents do not pass checksum"];
  }

  int firstDigit = [[contents substringToIndex:1] intValue];
  int parities = ZX_EAN13_FIRST_DIGIT_ENCODINGS[firstDigit];
  ZXBoolArray *result = [[ZXBoolArray alloc] initWithLength:ZX_EAN13_CODE_WIDTH];
  int pos = 0;

  pos += [self appendPattern:result pos:pos pattern:ZX_UPC_EAN_START_END_PATTERN patternLen:ZX_UPC_EAN_START_END_PATTERN_LEN startColor:YES];

  for (int i = 1; i <= 6; i++) {
    int digit = [[contents substringWithRange:NSMakeRange(i, 1)] intValue];
    if ((parities >> (6 - i) & 1) == 1) {
      digit += 10;
    }
    pos += [self appendPattern:result pos:pos pattern:ZX_UPC_EAN_L_AND_G_PATTERNS[digit] patternLen:ZX_UPC_EAN_L_PATTERNS_SUB_LEN startColor:FALSE];
  }

  pos += [self appendPattern:result pos:pos pattern:ZX_UPC_EAN_MIDDLE_PATTERN patternLen:ZX_UPC_EAN_MIDDLE_PATTERN_LEN startColor:FALSE];

  for (int i = 7; i <= 12; i++) {
    int digit = [[contents substringWithRange:NSMakeRange(i, 1)] intValue];
    pos += [self appendPattern:result pos:pos pattern:ZX_UPC_EAN_L_PATTERNS[digit] patternLen:ZX_UPC_EAN_L_PATTERNS_SUB_LEN startColor:YES];
  }
  [self appendPattern:result pos:pos pattern:ZX_UPC_EAN_START_END_PATTERN patternLen:ZX_UPC_EAN_START_END_PATTERN_LEN startColor:YES];

  return result;
}

@end

処理を辿ってみると、- (ZXBoolArray *)encode:(NSString *)contentsが呼ばれることがわかったのですが、ここではバーコード形式がkBarcodeFormatEan13なのに桁数が13ではない場合やチェックサムでエラーの場合にNSExceptionを発生させています。 APIなど外部から取得したバーコード文字列やバーコード形式をそのままこのライブラリに渡しているとNSExceptionが発生する可能性があるということになります。

SwiftでNSExceptionに対処する

Apple Developer Forumsでも議論されていますが、SwiftでNSExceptionはハンドリングできません。 対処法としては以下のようにObjective-Cのラッパーを作り、NSExceptionをキャッチしてNSErrorに変換してやることでSwiftでもハンドリング可能となります。

  • ObjC.h
#import <Foundation/Foundation.h>

@interface ObjC : NSObject

/**
 *  例外をキャッチする
 *
 *  @param tryBlock 例外が発生する可能性のある処理
 *  @param error    エラー
 *
 *  @return 処理が成功したかどうか
 */
+ (BOOL)catchException:(void(^)())tryBlock error:(__autoreleasing NSError **)error;

@end
  • ObjC.m
#import "ObjC.h"

@implementation ObjC

+ (BOOL)catchException:(void(^)())tryBlock error:(__autoreleasing NSError **)error {
    @try {
        tryBlock();
        return YES;
    }
    @catch (NSException *exception) {
        *error = [[NSError alloc] initWithDomain:exception.name code:0 userInfo:exception.userInfo];
    }
}

@end

Swiftでのハンドリング例は下記のようになります。 NSExceptionが発生してもNSErrorに変換されるのでcatchブロックでハンドリングできるようになります。

do {
    let scale = UIScreen.mainScreen().scale
    try ObjC.catchException {
        if let result = try? writer.encode(
            "123456789012345",
            format: kBarcodeFormatEan13,
            width: Int32(self.barcodeImage.bounds.width * scale),
            height: Int32(self.barcodeImage.bounds.height * scale)) {
            // 正常処理
        } else {
            // エラー処理
        }
    }
} catch {
    // エラー処理(NSExceptionが発生してもNSErrorに変換されるのでここでハンドリングできる!)
}

まとめ

今回はZXingObjCをSwiftで使ってみてハマった所をまとめてみました。 Swiftでアプリを作る場合でも、要件によってはObjective-CのOSSを使う場合も多々あるかと思います。 導入する際には中の実装がどうなっているか、Swiftから使った場合に問題ないかを十分に検討することをオススメします。(自戒)

参考