[iOS]艺术二维码之路

稀土 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。

相关推荐