[iOS]藝術二維碼之路

NewPan發表於2018-01-05

這次和大家分享的是如何畫一枚有趣的二維碼。具體實現效果如下,GitHub 連結在 這裡

[iOS]藝術二維碼之路

01.二維碼常識掃盲

二維碼就是一個矩陣,只不過對於不同的糾錯率,生成的矩陣的大小會有不同。

如下圖所示,當生成二維碼的時候,會根據不同的糾錯率生成一個對應大小的矩陣,比方說生成下圖左側 3 × 3 大小的空矩陣,然後根據生成二維碼的字串進行編碼,把編碼資料以 1 和 0 的形式插入到矩陣當中。如下圖右側圖所示,第一個方塊有資料為 1,就繪製一個圓形標記,其他方塊沒有資料為 0,不用繪製標記。按照這樣的規則進行繪製,就會得到一枚二維碼,只不過以上描述的只是規則的一個簡單版本。

[iOS]藝術二維碼之路

除了知道以上簡單的原理,下圖有一個更加詳細的,如果你只是想實現這篇文章中的功能,你知道這麼多已經夠了。但是如果你覺得不夠,這裡有一篇文章詳細介紹了二維碼的原理,感興趣請 點選 前往。

[iOS]藝術二維碼之路

02.大致實現原理

按照慣例,我們先來分析要畫這麼一枚二維碼大致需要哪些步驟。

01.首先,我們肯定需要依靠系統生成一枚二維碼。

02.拿到系統的二維碼以後我們需要將這張系統生成的二維碼轉成矩陣,並以二維陣列的形式儲存起來。

03.有了這個矩陣以後,我們就可以自己建立一張畫布,按照矩陣的資料進行二維碼的繪製。此時,我們可以選擇繪製圓,也可以繪製正方形等等。

04.我們在繪製的同時可以進行著色的操作。

03.生成二維碼

在 iOS 中建立二維碼依賴 CIFilter 類,傳進字串的二進位制流和糾錯型別就能生成一張對應的二維碼。

+(CIImage *)createOriginalCIImageWithString:(NSString *)str withCorrectionLevel:(kQRCodeCorrectionLevel)corLevel{
    CIFilter *filter = [CIFilter filterWithName:@"CIQRCodeGenerator"];
    [filter setDefaults];
    NSData *data = [str dataUsingEncoding:NSUTF8StringEncoding];
    [filter setValue:data forKeyPath:@"inputMessage"];
    
    NSString *corLevelStr = nil;
    switch (corLevel) {
        case kQRCodeCorrectionLevelLow:
                corLevelStr = @"L";
                break;
            case kQRCodeCorrectionLevelNormal:
                corLevelStr = @"M";
                break;
            case kQRCodeCorrectionLevelSuperior:
                corLevelStr = @"Q";
                break;
            case kQRCodeCorrectionLevelHight:
                corLevelStr = @"H";
                break;
    }
    [filter setValue:corLevelStr forKey:@"inputCorrectionLevel"];
    
    CIImage *outputImage = [filter outputImage];
    return outputImage;
}
複製程式碼

04.生成矩陣陣列

在生成矩陣陣列之前,我們先要將系統生成的二維碼從 CIImage 轉成 CGImageRef 備用。

+(CGImageRef)convertCIImage2CGImageForCIImage:(CIImage *)image{
    CGRect extent = CGRectIntegral(image.extent);

    size_t width = CGRectGetWidth(extent);
    size_t height = CGRectGetHeight(extent);
    CGColorSpaceRef cs = CGColorSpaceCreateDeviceGray();
    CGContextRef bitmapRef = CGBitmapContextCreate(nil, width, height, 8, 0, cs, (CGBitmapInfo)kCGImageAlphaNone);
    CIContext *context = [CIContext contextWithOptions:nil];
    CGImageRef bitmapImage = [context createCGImage:image fromRect:extent];
    CGContextSetInterpolationQuality(bitmapRef, kCGInterpolationNone);
    CGContextScaleCTM(bitmapRef, 1, 1);
    CGContextDrawImage(bitmapRef, extent, bitmapImage);
    
    CGImageRef scaledImage = CGBitmapContextCreateImage(bitmapRef);
    CGContextRelease(bitmapRef);
    CGImageRelease(bitmapImage);
    
    return scaledImage;
}
複製程式碼

接下來就是核心程式碼。利用 CoreGraphics 取出一張圖片指定 pixel 的 RGBA 值,然後將這個值存在二維陣列中。具體看原始碼。

+(NSArray<NSArray *>*)getPixelsWithCIImage:(CIImage *)ciimg{
    NSMutableArray *pixels = [NSMutableArray array];
    
    // 將系統生成的二維碼從 `CIImage` 轉成 `CGImageRef`.
    CGImageRef imageRef = [self convertCIImage2CGImageForCIImage:ciimg];
    CGFloat width = CGImageGetWidth(imageRef);
    CGFloat height = CGImageGetHeight(imageRef);
    
    // 建立一個顏色空間.
    CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
    
    // 開闢一段 unsigned char 的儲存空間,用 rawData 指向這段記憶體.
    // 每個 RGBA 色值的範圍是 0-255,所以剛好是一個 unsigned char 的儲存大小.
    // 每張圖片有 height * width 個點,每個點有 RGBA 4個色值,所以剛好是 height * width * 4.
    // 這段程式碼的意思是開闢了 height * width * 4 個 unsigned char 的儲存大小.
    unsigned char *rawData = (unsigned char *)calloc(height * width * 4, sizeof(unsigned char));
    
    // 每個畫素的大小是 4 位元組.
    NSUInteger bytesPerPixel = 4;
    // 每行位元組數.
    NSUInteger bytesPerRow = width * bytesPerPixel;
    // 一個位元組8位元
    NSUInteger bitsPerComponent = 8;
    
    // 將系統的二維碼圖片和我們建立的 rawData 關聯起來,這樣我們就可以通過 rawData 拿到指定 pixel 的記憶體地址.
    CGContextRef context = CGBitmapContextCreate(rawData, width, height, bitsPerComponent, bytesPerRow, colorSpace, kCGImageAlphaPremultipliedLast | kCGBitmapByteOrder32Big);
    CGColorSpaceRelease(colorSpace);
    CGContextDrawImage(context, CGRectMake(0, 0, width, height), imageRef);
    CGContextRelease(context);
    for (int indexY = 0; indexY < height; indexY++) {
          NSMutableArray *tepArrM = [NSMutableArray array];
          for (int indexX = 0; indexX < width; indexX++) {
              // 取出每個 pixel 的 RGBA 值,儲存到矩陣中.
              @autoreleasepool {
                  NSUInteger byteIndex = bytesPerRow * indexY + indexX * bytesPerPixel;
                  CGFloat red = (CGFloat)rawData[byteIndex];
                  CGFloat green = (CGFloat)rawData[byteIndex + 1];
                  CGFloat blue = (CGFloat)rawData[byteIndex + 2];
                
                  BOOL shouldDisplay = red == 0 && green == 0 && blue == 0;
                  [tepArrM addObject:@(shouldDisplay)];
                  byteIndex += bytesPerPixel;
              }
          }
          [pixels addObject:[tepArrM copy]];
    }
    free(rawData);
    return [pixels copy];
}
複製程式碼

05.自定義繪製二維碼

我們有了二維碼矩陣以後,只要開啟一張畫布,將這個矩陣的資料對應的繪製到畫布上,就能獲得一張二維碼。此時,因為是自己在畫布中繪製,我們可以自定義繪製的形狀,可以是圓形,也可以是矩形,還可以是其他形狀,只要你能想到。

06.漸變繪製

繪製不是難點,但是計算漸變顏色要求有一點初中三角函式的基礎才行。

6.1、水平漸變

由於每一個顏色都是由 RGB 組成的,所以我們可以將顏色分解成為 Red、Green、Blue,分別進行漸變運算。

如下圖,漸變的顏色區間為 Red1 到 Red2,對應的座標為(x1, y1) 和 (x2, y2)。要求的點的座標為(x, y),顯然 Red = Red1 + (Red2 - Red1) × (x - x1) / (x2 - x1)。然後我們再將分解求得的值進行合成 UIColor,然後就得到了水平漸變的顏色的漸變顏色區間色值。

[iOS]藝術二維碼之路

6.2、對角漸變

這篇文章的開始那張二維碼就是用的對角漸變。

如下圖所示,要實現對角漸變就需要計算出 targetValue 的值。我們可以通過 Red 點的座標值計算出角度 α 的值,由於 α + β = 90°,因此我們可以計算出 β 的值,然後計算出 targetValue 的值,這樣一來就回到上面的水平漸變的計算了。

[iOS]藝術二維碼之路

是不是很簡單?具體實現請檢視 原始碼

NewPan 的文章集合

下面這個連結是我所有文章的一個集合目錄。這些文章凡是涉及實現的,每篇文章中都有 Github 地址,Github 上都有原始碼。

NewPan 的文章集合索引

如果你有問題,除了在文章最後留言,還可以在微博 @盼盼_HKbuy 上給我留言,以及訪問我的 Github

相關文章