原文 : 与佳期的个人博客(gonghonglou.com)
提起二维码 QR Code 想必大家都不再陌生,“扫一扫加好友”、“扫一扫付款”等已是我们日常生活中司空见惯的情形。
以这篇博客来讲述在iOS中关于二维码的识别与生成,会尽可能周全的将二维码识别与生成相关的操作讲述清楚。
关于二维码生成的原理,感兴趣的话各位可以移驾 二维码的生成细节和原理 以做参考。
注:文中识别与生成二维码的方法同样适用于条形码。
先上几张二维码压压惊......

识别二维码
摄像头扫描二维码
iOS7之后苹果推出系统原生API来支持通过扫描获取二维码的功能,较其它 ZBar、ZXing 等第三方库有明显的性能优势。
首先,你需要弄清楚要用到的以下对象分别起到什么作用:
1、 AVCaptureDevice
:
捕获数据的物理设备,如:摄像机、麦克风。 开关灯属性torchMode
就是由它管理的。
2、 AVCaptureSession
:
会话,管理输入流、输出流之间的数据传递。
3、 AVCaptureDeviceInput
:
输入流,从物理设备获取数据。
4、 AVCaptureMetadataOutput
:
输出流,需要设置输出流代理及所在线程,由代理对象处理输出流数据。
需要说明的是:
- 需要将输出流添加到会话后,才能指定元数据类型,否则会报错。
- 将输出流设置在主线程中,其代理方法会执行一次。设置在其他线程的话,代理会执行多次且次数不可控制。
- 可以通过设置
AVFoundation
的rectOfInterest
属性来设定扫描区域,该属性默认取值是CGRectMake(0, 0, 1, 1)
即全屏扫描
x
、y
、width
、height
的取值范围都是0~1
,且原点在屏幕右上角,所以和我们正常理解的CGRect
相比x
和y
对调,width
和height
对调。

5、 AVCaptureVideoPreviewLayerCALayer
:
预览图层,显示相机拍摄到的画面。正因为它是CALayer
的子类,为了将它添加到屏幕上,我们需要额外添加一个UIView
,在这个UIView
的layer上
添加AVCaptureVideoPreviewLayerCALayer
,否则它会覆盖住所有的控件(除非你把所有的控件都在添加这个视图之后添加)。

AVCaptureSession
设置开始与结束扫描
// 开始会话
[self.captureSession startRunning];
// 停止会话
[self.captureSession stopRunning];
self.captureSession = nil;
- 获取AVCaptureDevice实例
- 初始化输入流
- 初始化输出流
3.1.设置代理及所在线程
3.2设置扫描区域 - 创建会话
4.1添加输入流
4.2添加输出流
*3.3指定元数据类型 - 创建预览图层
// 1.获取AVCaptureDevice实例
self.captureDevice = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];
// 2.初始化输入流
NSError * error;
AVCaptureDeviceInput *input = [AVCaptureDeviceInput deviceInputWithDevice:self.captureDevice error:&error];
if (!input) {
NSLog(@"%@", [error localizedDescription]);
return;
}
// 3.初始化输出流
AVCaptureMetadataOutput *captureMetadataOutput = [AVCaptureMetadataOutput new];
// 3.1设置代理及所在线程
[captureMetadataOutput setMetadataObjectsDelegate:self queue:dispatch_get_main_queue()];
// 3.2设置扫描区域
[captureMetadataOutput setRectOfInterest:[self scanPlace]];
// 4.创建会话
self.captureSession = [AVCaptureSession new];
// 4.1添加输入流
[self.captureSession addInput:input];
if ([self.captureSession canAddInput:input]) {
[self.captureSession addInput:input];
}
// 4.2添加输出流
if ([self.captureSession canAddOutput:captureMetadataOutput]) {
[self.captureSession addOutput:captureMetadataOutput];
}
// 3.3指定元数据类型
captureMetadataOutput.metadataObjectTypes = @[AVMetadataObjectTypeUPCECode,
AVMetadataObjectTypeCode39Code,
AVMetadataObjectTypeCode39Mod43Code,
AVMetadataObjectTypeEAN13Code,
AVMetadataObjectTypeEAN8Code,
AVMetadataObjectTypeCode93Code,
AVMetadataObjectTypeCode128Code,
AVMetadataObjectTypePDF417Code,
AVMetadataObjectTypeQRCode, // 二维码
AVMetadataObjectTypeAztecCode];
// 5创建预览图层
self.captureVideoPreviewLayer = [[AVCaptureVideoPreviewLayer alloc] initWithSession:self.captureSession];
[self.captureVideoPreviewLayer setVideoGravity:AVLayerVideoGravityResizeAspectFill];
[self.captureVideoPreviewLayer setFrame:self.scanView.layer.bounds];
[self.scanView.layer addSublayer:self.captureVideoPreviewLayer];
AVCaptureMetadataOutput
的代理方法处理输出流,返回扫描结果
#pragma mark - 代理方法处理输出流
- (void)captureOutput:(AVCaptureOutput *)captureOutput didOutputMetadataObjects:(NSArray *)metadataObjects fromConnection:(AVCaptureConnection *)connection {
if (metadataObjects != nil && [metadataObjects count] > 0) {
AVMetadataMachineReadableCodeObject *metadataObj = [metadataObjects objectAtIndex:0];
NSString *result = metadataObj.stringValue;
NSLog(@"result:%@", result);
if ([[metadataObj type] isEqualToString:AVMetadataObjectTypeQRCode]) {
NSLog(@"二维码");
} // ...
[self closeScanQRCode];
}
}
开灯
给开关灯按钮调用以下
AVCaptureDevice
方法,轻松实现开灯效果
- (void)turnLight {
// 判断允许设置
if ([self.device hasTorch]) {
[self.device lockForConfiguration:nil];
if (self.device.torchMode == AVCaptureTorchModeOff) {
[self.device setTorchMode:AVCaptureTorchModeOn]; // 开灯
} else if (self.device.torchMode == AVCaptureTorchModeOn) {
[self.device setTorchMode:AVCaptureTorchModeOff]; // 关灯
}
[self.device unlockForConfiguration];
}
}
总结下来就是:
AVCaptureSession
管理从物理设备AVCaptureDevice
那里获取的输入流AVCaptureDeviceInput
数据
通过输出流AVCaptureMetadataOutput
显示到预览图层AVCaptureVideoPreviewLayerCALayer
上
并且由代理方法-(void)captureOutput:(AVCaptureOutput*)captureOutput didOutputMetadataObjects:(NSArray*)metadataObjects fromConnection:(AVCaptureConnection*)connection;
处理捕获到的数据。
相册识别二维码
iOS8之后系统提供的识别二维码图片的方法相当简单
// 读取二维码
UIImage *sourceImage = ...;
CIContext *context = [CIContext contextWithOptions:nil];
CIDetector *detector = [CIDetector detectorOfType:CIDetectorTypeQRCode context:context options:@{CIDetectorAccuracy:CIDetectorAccuracyHigh}];
CIImage *image = [CIImage imageWithCGImage:sourceImage.CGImage];
NSArray *array = [detector featuresInImage:image];
CIQRCodeFeature *feature = [array firstObject];
NSString *result = feature.messageString;
NSLog(@"result:%@", result);
UIImagePickerController
打开系统相册,选择图片识别:
- (void)openPhotoLibrary {
UIImagePickerController *photoPicker = [UIImagePickerController new];
photoPicker.delegate = self;
// 打开的相册类型
photoPicker.sourceType = UIImagePickerControllerSourceTypePhotoLibrary;
[self presentViewController:photoPicker animated:YES completion:NULL];
}
UIImagePickerController
的代理方法识别相册中选中的二维码:
#pragma mark - UIImagePickerControllerDelegate
- (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary *)info {
[self dismissViewControllerAnimated:YES completion:^{
// 读取二维码
UIImage *sourceImage = [info objectForKey:UIImagePickerControllerOriginalImage];
CIContext *context = [CIContext contextWithOptions:nil];
CIDetector *detector = [CIDetector detectorOfType:CIDetectorTypeQRCode context:context options:@{CIDetectorAccuracy:CIDetectorAccuracyHigh}];
CIImage *image = [CIImage imageWithCGImage:sourceImage.CGImage];
NSArray *array = [detector featuresInImage:image];
CIQRCodeFeature *feature = [array firstObject];
NSString *result = feature.messageString;
NSLog(@"result:%@", result);
}];
}
生成二维码
使用系统提供的
CIFilter
可以方便简单的生成二维码
生成二维码方法
- (UIImage *)generateQRCode:(NSString *)code width:(CGFloat)width height:(CGFloat)height {
CIImage *qrcodeImage;
NSData *data = [code dataUsingEncoding:NSUTF8StringEncoding allowLossyConversion:false];
CIFilter *filter = [CIFilter filterWithName:@"CIQRCodeGenerator"];
[filter setValue:data forKey:@"inputMessage"];
[filter setValue:@"H" forKey:@"inputCorrectionLevel"];
qrcodeImage = [filter outputImage];
// 消除模糊
CGFloat scaleX = width / qrcodeImage.extent.size.width; // extent 返回图片的frame
CGFloat scaleY = height / qrcodeImage.extent.size.height;
CIImage *transformedImage = [qrcodeImage imageByApplyingTransform:CGAffineTransformScale(CGAffineTransformIdentity, scaleX, scaleY)];
return [UIImage imageWithCIImage:transformedImage];
}
使用
CIFilter
生成带背景色的二维码
- (UIImage *)gaveColor:(NSString *)code width:(CGFloat)width height:(CGFloat)height {
NSData *data = [code dataUsingEncoding:NSUTF8StringEncoding allowLossyConversion:false];
CIFilter *filter = [CIFilter filterWithName:@"CIQRCodeGenerator"];
[filter setValue:data forKey:@"inputMessage"];
[filter setValue:@"H" forKey:@"inputCorrectionLevel"];
//上色
CIFilter *colorFilter = [CIFilter filterWithName:@"CIFalseColor"
keysAndValues:
@"inputImage",filter.outputImage,
@"inputColor0",[CIColor colorWithCGColor:[UIColor purpleColor].CGColor], // 前景色
@"inputColor1",[CIColor colorWithCGColor:[UIColor cyanColor].CGColor], // 背景色
nil];
CIImage *qrcodeImage = colorFilter.outputImage;
// 消除模糊
CGFloat scaleX = width / qrcodeImage.extent.size.width; // extent 返回图片的frame
CGFloat scaleY = height / qrcodeImage.extent.size.height;
CIImage *transformedImage = [qrcodeImage imageByApplyingTransform:CGAffineTransformScale(CGAffineTransformIdentity, scaleX, scaleY)];
return [UIImage imageWithCIImage:transformedImage];
}
通过遍历图片的像素给二维码个性化上色
这里需要指出的是,如果你在下边方法中传入的
image
是通过CIFilter
方法直接生成的,那么该方法是没法工作的。同理,下一节中保存图片时仍然不能用CIFilter
方法直接生成的image
,而采用CGContextRef
获取图片。(参见下一节:保存二维码)
// 颜色变化
void ProviderReleaseData (void *info, const void *data, size_t size) {
free((void *)data);
}
- (UIImage *)imageBlackToTransparent:(UIImage *)image {
// 分配内存
const int imageWidth = image.size.width;
const int imageHeight = image.size.height;
size_t bytesPerRow = imageWidth * 4;
uint32_t *rgbImageBuf = (uint32_t *)malloc(bytesPerRow * imageHeight);
// 创建context
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
CGContextRef context = CGBitmapContextCreate(rgbImageBuf, imageWidth, imageHeight, 8, bytesPerRow, colorSpace,kCGBitmapByteOrder32Little | kCGImageAlphaNoneSkipLast);
CGContextDrawImage(context, CGRectMake(0, 0, imageWidth, imageHeight), image.CGImage);
// 遍历像素
int pixelNum = imageWidth * imageHeight;
uint32_t *pCurPtr = rgbImageBuf;
for (int i = 0; i < pixelNum; i++, pCurPtr++) {
if ((*pCurPtr & 0xFFFFFF00) < 0x99999900) {
// 改变下面的代码,将图片转成想要的颜色
uint8_t *ptr = (uint8_t *)pCurPtr;
if (i<pixelNum/2) {
ptr[3] = 1; //0~255
ptr[2] = 200;
ptr[1] = 200;
} else {
ptr[3] = 200; //0~255
ptr[2] = 1;
ptr[1] = 200;
}
} else { // 白色 255,255,255
uint8_t *ptr = (uint8_t *)pCurPtr;
ptr[0] = 0;
}
}
// 将内存转成image
CGDataProviderRef dataProvider = CGDataProviderCreateWithData(NULL, rgbImageBuf, bytesPerRow * imageHeight,ProviderReleaseData);
CGImageRef imageRef = CGImageCreate(imageWidth, imageHeight, 8, 32, bytesPerRow, colorSpace,kCGImageAlphaLast | kCGBitmapByteOrder32Little, dataProvider, NULL, true, kCGRenderingIntentDefault);
CGDataProviderRelease(dataProvider);
UIImage *resultUIImage = [UIImage imageWithCGImage:imageRef];
// 释放
CGImageRelease(imageRef);
CGContextRelease(context);
CGColorSpaceRelease(colorSpace);
return resultUIImage;
}
生成条码图片
- (UIImage *)generateBarCode:(NSString *)code width:(CGFloat)width height:(CGFloat)height {
CIImage *barcodeImage;
NSData *data = [code dataUsingEncoding:NSISOLatin1StringEncoding allowLossyConversion:false];
CIFilter *filter = [CIFilter filterWithName:@"CICode128BarcodeGenerator"];
[filter setValue:data forKey:@"inputMessage"];
barcodeImage = [filter outputImage];
// 消除模糊
CGFloat scaleX = width / barcodeImage.extent.size.width; // extent 返回图片的frame
CGFloat scaleY = height / barcodeImage.extent.size.height;
CIImage *transformedImage = [barcodeImage imageByApplyingTransform:CGAffineTransformScale(CGAffineTransformIdentity, scaleX, scaleY)];
return [UIImage imageWithCIImage:transformedImage];
}
保存二维码
// 开启位图上下文
UIGraphicsBeginImageContextWithOptions(self.imageView.bounds.size, NO, 0);
// 获取绘图上下文
CGContextRef context = UIGraphicsGetCurrentContext();
// 将图片渲染的上下文中
[self.imageView.layer renderInContext:context];
// 获取上下文中的图片
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
// 关闭位图上下文
UIGraphicsEndImageContext();
// 保存图片
UIImageWriteToSavedPhotosAlbum(image, self, @selector(image:didFinishSavingWithError:contextInfo:), nil);
UIImageWriteToSavedPhotosAlbum
方法的指定回调,监测保存是否成功
#pragma mark - 指定回调方法
- (void)image:(UIImage *)image didFinishSavingWithError:(NSError *)error contextInfo:(void *)contextInfo {
NSString *string;
if(!error){
NSLog(@"save success");
}else{
NSLog(@"save failed");
}
}
看一哈效果图

ok,就这些吧。
Demo地址:https://github.com/gonghonglou/QRCodeDemo
写到这里估计涉及了二维码操作的大部分内容,期望对诸君有所帮助。
祝大家敲码愉快!
后记
-
小白出手,请多指教。如言有误,还望斧正!
网友评论