標籤:
很多應用都有掃描二維碼的功能,在開發這些功能時大家都可能接觸過 ZXing 或 ZBar 這類第三方掃碼庫,但從 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、掃碼
掃碼是一個從網路攝影機(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)
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 formatNSData * 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);
(產生的二維碼圖片)
4、掃描框
掃碼時 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)
- 2) rectOfInterest 不可以直接在設定 metadataOutput 時接著設定,而需要在這個 AVCaptureInputPortFormatDescriptionDidChangeNotification 通知裡設定,否則 metadataOutputRectOfInterestForRect: 轉換方法會返回 (0, 0, 0, 0)。
[[NSNotificationCenter defaultCenter] addObserverForName:AVCaptureInputPortFormatDescriptionDidChangeNotification object:nil queue:[NSOperationQueue currentQueue] usingBlock: ^(NSNotification *_Nonnull note) { metadataOutput.rectOfInterest = [previewLayer metadataOutputRectOfInterestForRect:CGRectMake(80, 80, 160, 160)];}];
掃碼框四周一般都是半透明的黑色,而它裡面是沒有顏色的。
(掃碼框)
你可以在掃碼框四周各添加視圖,但更簡單的方法是自訂一個視圖,在 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);
ios7原生掃碼