iOS 原生扫 QR 码的那些事

很多应用都有扫描二维码的功能,在开发这些功能时大家都可能接触过 ZXingZBar 这类第三方扫码库,但从 iOS 7 开始系统原生 API 就支持通过扫描获取二维码的功能。今天就来说说原生扫码的那些事。

0、相机权限

也是从 iOS 7 开始,应用要使用相机功能首先要获得用户的授权,所以要先判断授权情况。

  • 判断授权情况方法:
AVAuthorizationStatus authorizationStatus = [AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeVideo];  


  • AVAuthorizationStatus enum 值有:
typedef NS_ENUM(NSInteger, AVAuthorizationStatus) {  
    AVAuthorizationStatusNotDetermined = 0,
    AVAuthorizationStatusRestricted, // 受限,有可能开启了访问限制
    AVAuthorizationStatusDenied,
    AVAuthorizationStatusAuthorized
} NS_AVAILABLE_IOS(7_0);


  • 请求授权方法:
[AVCaptureDevice requestAccessForMediaType:AVMediaTypeVideo completionHandler: ^(BOOL granted) {
    if (granted) { 
        [self startCapture]; // 获得授权
    } else {    
        NSLog(@"%@", @"访问受限");
    }
}];


  • 完整授权处理逻辑:
AVAuthorizationStatus authorizationStatus = [AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeVideo];  
switch (authorizationStatus) {  
    case AVAuthorizationStatusNotDetermined: {
        [AVCaptureDevice requestAccessForMediaType:AVMediaTypeVideo completionHandler: ^(BOOL granted) {
            if (granted) {
                [self startCapture];
            } else {
                NSLog(@"%@", @"访问受限");
            }
        }];
        break;
    }

    case AVAuthorizationStatusAuthorized: {
        [self startCapture];
        break;
    }

    case AVAuthorizationStatusRestricted:
    case AVAuthorizationStatusDenied: {
        NSLog(@"%@", @"访问受限");
        break;
    }

    default: {
        break;
    }
}


1、扫码

  • AVCaptureSession

扫码是一个从摄像头(input)到 解析出字符串(output) 的过程,用AVCaptureSession 来协调。其中是通过 AVCaptureConnection 来连接各个 input 和 output,还可以用它来控制 input 和 output 的 数据流向。它们的关系如下图:


  • 扫码的代码很简单,如下:
AVCaptureSession *session = [[AVCaptureSession alloc] init];  
AVCaptureDevice *device = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];  
NSError *error;  
AVCaptureDeviceInput *deviceInput = [AVCaptureDeviceInput deviceInputWithDevice:device error:&error];  
if (deviceInput) {  
    [session addInput:deviceInput];

    AVCaptureMetadataOutput *metadataOutput = [[AVCaptureMetadataOutput alloc] init];
    [metadataOutput setMetadataObjectsDelegate:self queue:dispatch_get_main_queue()];
    [session addOutput:metadataOutput]; // 这行代码要在设置 metadataObjectTypes 前
    metadataOutput.metadataObjectTypes = @[AVMetadataObjectTypeQRCode];

    AVCaptureVideoPreviewLayer *previewLayer = [[AVCaptureVideoPreviewLayer alloc] initWithSession:session];
    previewLayer.videoGravity = AVLayerVideoGravityResizeAspectFill;
    previewLayer.frame = self.view.frame;
    [self.view.layer insertSublayer:previewLayer atIndex:0];

    [session startRunning];
} else {
    NSLog(@"%@", error);
}


  • 扫码结果从委托方法返回:
- (void)captureOutput:(AVCaptureOutput *)captureOutput didOutputMetadataObjects:(NSArray *)metadataObjects fromConnection:(AVCaptureConnection *)connection {
    AVMetadataMachineReadableCodeObject *metadataObject = metadataObjects.firstObject;
    if ([metadataObject.type isEqualToString:AVMetadataObjectTypeQRCode] && !self.isQRCodeCaptured) { // 成功后系统不会停止扫描,可以用一个变量来控制。
        self.isQRCodeCaptured = YES;

        NSLog(@"%@", metadataObject.stringValue);
    }
}


2、从图片文件解析(iOS 8 起)

从 iOS 8 开始你也可以从图片文件解析出二维码,用到 Core Image 的 CIDetector。

代码也很简单:

CIDetector *detector = [CIDetector detectorOfType:CIDetectorTypeQRCode context:nil options:@{ CIDetectorAccuracy:CIDetectorAccuracyHigh }];  
CIImage *image = [[CIImage alloc] initWithImage:[UIImage imageNamed:@"foobar.png"]];  
NSArray *features = [detector featuresInImage:image];  
for (CIQRCodeFeature *feature in features) {  
    NSLog(@"%@", feature.messageString);
}

foobar.png

(foobar.png)


3、生成二维码图片

生成二维码用到了 CIQRCodeGenerator 这种 CIFilter。它有两个字段可以设置,inputMessage 和 inputCorrectionLevel。inputMessage 是一个 NSData 对象,可以是字符串也可以是一个 URL。inputCorrectionLevel 是一个单字母(@"L", @"M", @"Q", @"H" 中的一个),表示不同级别的容错率,默认为 @"M"。

QR码有容错能力,QR码图形如果有破损,仍然可以被机器读取内容,最高可以到7%~30%面积破损仍可被读取。所以QR码可以被广泛使用在运输外箱上。

相对而言,容错率愈高,QR码图形面积愈大。所以一般折衷使用15%容错能力。

错误修正容量 L水平 7%的字码可被修正
M水平 15%的字码可被修正
Q水平 25%的字码可被修正
H水平 30%的字码可被修正

所以很多二维码的中间都有头像之类的图片但仍然可以识别出来就是这个原因。

代码如下,应该注意的是:

  • 1)官方建议使用 NSISOLatin1StringEncoding 来编码,但经测试这种编码对中文或表情无法生成,改用 NSUTF8StringEncoding 就可以了。
  • 2)生成的图片尺寸(outputImage.extent.size)会比较小,需要对它进行缩放。
  • 3)生成的 CIImage 需要先转成 CGImage 才可以保存,因为 UIImagePNGRepresentation 接受的 UIImage 要有 CGImage,如果没有或者位图格式不对都会返回 nil。
// return image as PNG. May return nil if image has no CGImageRef or invalid bitmap format
NSData * UIImagePNGRepresentation(UIImage *image);  
NSString *urlString = @"http://weibo.com/u/2255024877";  
NSData *data = [urlString dataUsingEncoding:NSISOLatin1StringEncoding]; // NSISOLatin1StringEncoding 编码

CIFilter *filter = [CIFilter filterWithName:@"CIQRCodeGenerator"];  
[filter setValue:data forKey:@"inputMessage"];

CIImage *outputImage = filter.outputImage;  
NSLog(@"%@", NSStringFromCGSize(outputImage.extent.size));

CGAffineTransform transform = CGAffineTransformMakeScale(scale, scale); // scale 为放大倍数  
CIImage *transformImage = [outputImage imageByApplyingTransform:transform];

// 保存
CIContext *context = [CIContext contextWithOptions:nil];  
CGImageRef imageRef = [context createCGImage:transformImage fromRect:transformImage.extent];

UIImage *qrCodeImage = [UIImage imageWithCGImage:imageRef];  
[UIImagePNGRepresentation(qrCodeImage) writeToFile:path atomically:NO];

CGImageRelease(imageRef);  

c0ming1986

(生成的二维码图片)

4、扫描框

  • rectOfInterest

扫码时 previewLayer 的扫描范围是整个可视范围的,但有些需求可能需要指定扫描的区域,虽然我觉得这样很没有必要,因为整个屏幕都可以扫又何必指定到某个框呢?但如果真的需要这么做可以设定 metadataOutput 的 rectOfInterest。

需要注意的是:

  • 1)rectOfInterest 的值比较特别,需要进行转化。它的默认值是 (0.0, 0.0, 1.0, 1.0)。
metadataOutput.rectOfInterest = [previewLayer metadataOutputRectOfInterestForRect:CGRectMake(80, 80, 160, 160)]; // 假设扫码框的 Rect 是 (80, 80, 160, 160)  


[[NSNotificationCenter defaultCenter] addObserverForName:AVCaptureInputPortFormatDescriptionDidChangeNotification
                                                        object:nil
                                                         queue:[NSOperationQueue currentQueue]
                                                    usingBlock: ^(NSNotification *_Nonnull note) {
      metadataOutput.rectOfInterest = [previewLayer metadataOutputRectOfInterestForRect:CGRectMake(80, 80, 160, 160)];
}];


  • 扫码框的外观

扫码框四周一般都是半透明的黑色,而它里面是没有颜色的。 wechat

(扫码框)

你可以在扫码框四周各添加视图,但更简单的方法是自定义一个视图,在 drawRect: 画扫码框的 path。代码如下:

CGContextRef ctx = UIGraphicsGetCurrentContext();

CGFloat width = CGRectGetWidth([UIScreen mainScreen].bounds);  
[[[UIColor blackColor] colorWithAlphaComponent:0.5] setFill];

CGMutablePathRef screenPath = CGPathCreateMutable();  
CGPathAddRect(screenPath, NULL, self.bounds);

CGMutablePathRef scanPath = CGPathCreateMutable();  
CGPathAddRect(scanPath, NULL, CGRectMake(80, 80, 160, 160);

CGMutablePathRef path = CGPathCreateMutable();  
CGPathAddPath(path, NULL, screenPath);  
CGPathAddPath(path, NULL, scanPath);

CGContextAddPath(ctx, path);  
CGContextDrawPath(ctx, kCGPathEOFill); // kCGPathEOFill 方式

CGPathRelease(screenPath);  
CGPathRelease(scanPath);  
CGPathRelease(path);  


Demo:

QRCodeDemo

总结:

从 iOS 7 开始系统就有很好的原生 API 来开发二维码相关的功能,它们不但识别速度快,而且还可以免除集成第三方扫码库的烦恼,不过如果需要用原生 API 来从图片获取二维码就要从 iOS 8 开始。

参考:

1、QR码 via wikipedia
2、AVFoundation Programming Guide
3、Core Image Filter Reference
4、Core Image Programming Guide
5、rectOfInterest via stackoverflow