再见ZXing 使用系统原生代码处理QRCode

前言


二维码每个人都一定不陌生 尤其是在支付宝和微信的努力下 相信每个人都或多或少的使用过"扫一扫"这个功能
而作为开发者 大家都应该有过二维码的开发经验 不管是练习还是研究 或者是公司的项目(尤其是社交类的APP 基本都有扫一扫加好友这个功能吧?) 

介绍


说到二维码 其实我们现在生活中接触到的都是QRCode --由日本人在上个世纪末用于汽车工业的一项发明 如今在移动互联网发光发亮
而实际上 除了QRCode 还有很多二维码的类型 最出名的应该就是DataMatrix了 而在4,5年前 一度还有很多QRCode的竞争对手出现 比如微软推出的Microsoft Tag 还有个人觉得很有意思的SnapTag 可是在这几年的发展过程中 还是QRCode笑到了最后 所以现在我们一般提起二维码 就指的是QRCode 下面我们就用QRCode来代替二维码

对于开发人员来说 熟悉的第三方QRCode库有:

  • ZXing
    Google出品并开源 一直到现在都还有专人维护 是世界上使用最广的二维码库 iOS上比较稳定的移植版是ZXingObj

  • ZBar
    功能上与ZXing不相伯仲 可惜的是项目在2012年之后就不维护了 虽然代码到现在还可以使用

而我从开始到现在一直是ZXing的忠实用户 除了Google的光环之外 那时候的ZXing资料也比ZBar多很多 不过随着iOS7的发布 苹果推出了内建的二维码扫描功能 这让这些第三方库多少显得有点尴尬了 

回到今天的主题上 为什么说要跟ZXing说再见呢? 其实我目前做的项目使用的也是ZXing 不过在开发过程中发现了一些很严重而且绕不过去的问题 比如下面这张图


此处输入图片的描述


如果使用相册读取QRCode的功能 ZXing是无法识别的 不信大家可以去zxing.org 我也去Github提了issue 不过作者也没有给出有效的解决办法 所以只能跟ZXing说88了(不过ZBar是可以扫描出来的哟!)

先分析一下 我们使用QRCode一般都是要做如下几种场景的操作

  • 扫描
    比如 微信的"扫一扫"加好友
  • 读取
    比如 微信中长按图片 会弹出"识别图中二维码"这个功能
  • 生成
    比如 微信的"我的二维码"功能

接下来我们就分三个部分分别来介绍一下如何实现对应的功能(要注意的是 "读取图片"这个功能只有在iOS8以上的系统才能支持 iOS7的话 建议还是使用ZXing或者ZBar)

扫描


扫描主要使用的是AVFoundation 使用起来也非常的简单 正常的初始化流程如下

@interface Example1Controller()
<
AVCaptureMetadataOutputObjectsDelegate,
UIAlertViewDelegate
>

@property (nonatomic, strong) UIView *scanRectView;

@property (strong, nonatomic) AVCaptureDevice            *device;
@property (strong, nonatomic) AVCaptureDeviceInput       *input;
@property (strong, nonatomic) AVCaptureMetadataOutput    *output;
@property (strong, nonatomic) AVCaptureSession           *session;
@property (strong, nonatomic) AVCaptureVideoPreviewLayer *preview;

@end

@implementation Example1Controller

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.

    self.device = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];

    self.input = [AVCaptureDeviceInput deviceInputWithDevice:self.device error:nil];

    self.output = [[AVCaptureMetadataOutput alloc]init];
    [self.output setMetadataObjectsDelegate:self queue:dispatch_get_main_queue()];

    self.session = [[AVCaptureSession alloc]init];
    [self.session setSessionPreset:([UIScreen mainScreen].bounds.size.height<500)?AVCaptureSessionPreset640x480:AVCaptureSessionPresetHigh];
    [self.session addInput:self.input];
    [self.session addOutput:self.output];
    self.output.metadataObjectTypes=@[AVMetadataObjectTypeQRCode];
    self.output.rectOfInterest = scanRect;

    self.preview = [AVCaptureVideoPreviewLayer layerWithSession:self.session];
    self.preview.videoGravity = AVLayerVideoGravityResizeAspectFill;
    self.preview.frame = [UIScreen mainScreen].bounds;
    [self.view.layer insertSublayer:self.preview atIndex:0];

    //开始捕获
    [self.session startRunning];

}

- (void)captureOutput:(AVCaptureOutput *)captureOutput didOutputMetadataObjects:(NSArray *)metadataObjects fromConnection:(AVCaptureConnection *)connection
{
    if ( (metadataObjects.count==0) )
    {
        return;
    }

    if (metadataObjects.count>0) {

        [self.session stopRunning];

        AVMetadataMachineReadableCodeObject *metadataObject = metadataObjects.firstObject;
        //输出扫描字符串

        UIAlertView *alert = [[UIAlertView alloc] initWithTitle:metadataObject.stringValue message:@"" delegate:self cancelButtonTitle:@"ok" otherButtonTitles: nil];

        [alert show];
    }
}

- (void)alertView:(UIAlertView *)alertView willDismissWithButtonIndex:(NSInteger)buttonIndex
{
    [self.session startRunning];
}

AVFoundation的部分我就不多介绍了 只要按照上面的代码初始化 即可实现二维码扫描的功能 是不是很简单?

不过这里还不能完全满足我们的要求 因为如果运行以后你会发现 现在是全屏扫描 而不是像我们印象中的那种在屏幕中间有个框 只有在框中的二维码才会被扫描到 不过其实改起来也很简单 AVCaptureMetadataOutput有个属性rectOfInterest就是做这个事情的

@interface AVCaptureMetadataOutput : AVCaptureOutput 

/*!
 @property rectOfInterest
 @abstract
    Specifies a rectangle of interest for limiting the search area for visual metadata.

 @discussion
    The value of this property is a CGRect that determines the receiver's rectangle of interest for each frame of video.  
    The rectangle's origin is top left and is relative to the coordinate space of the device providing the metadata.  Specifying 
    a rectOfInterest may improve detection performance for certain types of metadata. The default value of this property is the 
    value CGRectMake(0, 0, 1, 1).  Metadata objects whose bounds do not intersect with the rectOfInterest will not be returned.
 */
@property(nonatomic) CGRect rectOfInterest NS_AVAILABLE_IOS(7_0);

@end

可以看到 rectOfInterest的值的范围都是0-1 是按比例取值而不是实际尺寸 不过其实也很简单 只要换算一下就好了 接下来我们添加取景框

CGSize windowSize = [UIScreen mainScreen].bounds.size;

CGSize scanSize = CGSizeMake(windowSize.width*3/4, windowSize.width*3/4);
CGRect scanRect = CGRectMake((windowSize.width-scanSize.width)/2, (windowSize.height-scanSize.height)/2, scanSize.width, scanSize.height);

//计算rectOfInterest 注意x,y交换位置
scanRect = CGRectMake(scanRect.origin.y/windowSize.height, scanRect.origin.x/windowSize.width, scanRect.size.height/windowSize.height,scanRect.size.width/windowSize.width);
self.output.rectOfInterest = scanRect;

self.scanRectView = [UIView new];
[self.view addSubview:self.scanRectView];
self.scanRectView.frame = CGRectMake(0, 0, scanSize.width, scanSize.height);
self.scanRectView.center = CGPointMake(CGRectGetMidX([UIScreen mainScreen].bounds), CGRectGetMidY([UIScreen mainScreen].bounds));
self.scanRectView.layer.borderColor = [UIColor redColor].CGColor;
self.scanRectView.layer.borderWidth = 1;

这里唯一要注意的一点是 rectOfInterest 都是按照横屏来计算的 所以当竖屏的情况下 x轴和y轴要交换一下 

看看实际效果


读取


读取主要用到CoreImage 不过要强调的是读取二维码的功能只有在iOS8之后才支持
读取的代码实现就更简单了

UIImage * srcImage = qrcodeImage;

CIContext *context = [CIContext contextWithOptions:nil];
CIDetector *detector = [CIDetector detectorOfType:CIDetectorTypeQRCode context:context options:@{CIDetectorAccuracy:CIDetectorAccuracyHigh}];
CIImage *image = [CIImage imageWithCGImage:srcImage.CGImage];
NSArray *features = [detector featuresInImage:image];
CIQRCodeFeature *feature = [features firstObject];

NSString *result = feature.messageString;

看看实际效果


生成


生成也是用到CoreImage 其步骤稍微多一点 代码如下

    NSString *text = self.tfCode.text;

    NSData *stringData = [text dataUsingEncoding: NSUTF8StringEncoding];

    //生成
    CIFilter *qrFilter = [CIFilter filterWithName:@"CIQRCodeGenerator"];
    [qrFilter setValue:stringData forKey:@"inputMessage"];
    [qrFilter setValue:@"M" forKey:@"inputCorrectionLevel"];

    UIColor *onColor = [UIColor redColor];
    UIColor *offColor = [UIColor blueColor];

    //上色
    CIFilter *colorFilter = [CIFilter filterWithName:@"CIFalseColor"
                                       keysAndValues:
                             @"inputImage",qrFilter.outputImage,
                             @"inputColor0",[CIColor colorWithCGColor:onColor.CGColor],
                             @"inputColor1",[CIColor colorWithCGColor:offColor.CGColor],
                             nil];

    CIImage *qrImage = colorFilter.outputImage;

    //绘制
    CGSize size = CGSizeMake(300, 300);
    CGImageRef cgImage = [[CIContext contextWithOptions:nil] createCGImage:qrImage fromRect:qrImage.extent];
    UIGraphicsBeginImageContext(size);
    CGContextRef context = UIGraphicsGetCurrentContext();
    CGContextSetInterpolationQuality(context, kCGInterpolationNone);
    CGContextScaleCTM(context, 1.0, -1.0);
    CGContextDrawImage(context, CGContextGetClipBoundingBox(context), cgImage);
    UIImage *codeImage = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();

    CGImageRelease(cgImage);
  • 首先通过[CIFilter filterWithName:@"CIQRCodeGenerator"]生成QRCode
  • 然后通过[CIFilter filterWithName:@"CIFalseColor"]上色(当然这一步不是必须的 如果仅仅需要白底黑块的QRCode 可以跳过这一步)
  • 最后无损放大并绘制QRCode (上面两步生成的QRCode很小 大概是31*31 如果不放大 就会很模糊)

这里要注意的是 在最后一步一定要使用CGContextScaleCTM(context, 1.0, -1.0)来翻转一下图片 不然生成的QRCode就是上下颠倒的哦

看看实际效果


小结


源码和Demo请点这里

如果不需要对QRCode进行深入的探索的话 相信各位看完本文后 绝大多数的关于QRCode的使用问题 应该都可以迎刃而解了 如果需要进一步了解QRCode的原理 我推荐陈皓的这篇二维码的生成细节和原理

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值