objc-code

面向数据编程

使用 Core Image 制作海报

19 November ’17

之前做了个以图片的形式分享菜谱作品等到微信的需求,使用 Core Image 完成,效果图如下:

Core Image 除了用来做人脸识别之外,最主要的用途就是做图片处理了,Core Image 提供了 CIImage 和 CIFilter 来描述这个过程。整个过程很直观,CIImage 是代表 image 的 data model,CIFilter 即图片处理方法(滤镜)。苹果在 Core Image 框架里面内置了二百多种 Filter,足以满足大多数需求,如果需要自定义 Filter,Core Image 插件化的架构也可以很方便的把我们自己实现的 Filter 集成进来。如何自定义 Filter 可以看这里Creating Custom Filters

对图片做 Transform、Crop、rotate 等操作也是 Filter 里面的一类,CIImage 提供了一些列 imageBy 方法对这些常用的操作进行了封装,方便调用。在做分享海报的过程中,大量用到了这些操作。

设置画布

需要先确定最后生成的图片的宽度,以像素为单位,这里先定为 1080 px,排版需要用。然后先生成一张白色的画布 renderImage:

CGFloat renderWidth = 1080.0f;
CIImage *renderImage = [CIImage imageWithColor:[[CIColor alloc] initWithColor:[UIColor whiteColor]]];

此时 renderImage 的尺寸是无限大,以它 {0,0} 的位置作为海报左下角布局。

绘制切了圆角的正方形头像

UIImage *avatarImage = /* 事先准备好的头像图片 */;
CIImage *avatar = [CIImage imageWithCGImage:avatarImage.CGImage]; // 生成 CIImage

// 画圆
CGFloat radius = avatar.extent.size.width / 2;
NSDictionary *maskParas = @{@"inputCenter"  : [CIVector vectorWithX:radius Y:radius],
                            @"inputRadius0" : @(radius),
                            @"inputRadius1" : @(radius),
                            @"inputColor0" : [CIColor colorWithRed:1 green:1 blue:1 alpha:1],
                            @"inputColor1" : [CIColor colorWithRed:0 green:0 blue:0 alpha:1]};
CIImage *circle = [CIFilter filterWithName:@"CIRadialGradient"
                       withInputParameters:maskParas].outputImage;

// 生成圆形 mask
CIImage *mask = [CIFilter filterWithName:@"CIMaskToAlpha"
                     withInputParameters:@{kCIInputImageKey : circle}].outputImage;

// 生成新的切了圆角的头像
avatar = [CIFilter filterWithName:@"CIBlendWithAlphaMask"
              withInputParameters:@{kCIInputMaskImageKey : mask,
                                        kCIInputImageKey : avatar}].outputImage;

// 移动头像到画布上设计好的位置,假设左下角坐标是 {x,y}
avatar = [avatar imageByApplyingTransform:CGAffineTransformMakeTranslation(x, y)];

// 最后画到画布上,得到新的画布
renderImage = [avatar imageByCompositingOverImage:renderImage];

- imageByCompositingOverImage: 实际上是使用了名为 CISourceOverCompositing 的 Filter,将两张图片合成一张。 后面的其他部分都是这样,先生成一个单独的 CIImage,再移动到画布上相应的位置,最后使用 - imageByCompositingOverImage: 合成新的 renderImage。从下往上画,只要从最后添加的 CIImage 的 extent 用 CGRectGetMaxY 就可以知道现在画了多高了。(这里的坐标系和 UIKit 的相反,Y坐标的方向是由下向上的)

绘制文本

绘制文本需要先把文本转成 image,剩下的和其他部分没有区别。转换代码:

/*
 * text  text to be drawn
 * width max width in px
 * scale iphone screen scale factor
 */
static UIImage* h_createImageFromAttributedString(NSAttributedString *text,CGFloat width,CGFloat scale) {
    CGRect drawRect = [text boundingRectWithSize:CGSizeMake(width / scale, CGFLOAT_MAX)
                                         options:NSStringDrawingUsesLineFragmentOrigin|NSStringDrawingUsesFontLeading
                                         context:nil];
    drawRect.origin = CGPointZero;
    UIGraphicsBeginImageContextWithOptions(drawRect.size, NO, scale);
    [text drawInRect:drawRect];
    UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();
    
    return image;
}

其中 scale 使用 [UIScreen mainScreen].scale 即可。

绘制二维码

Core Image 内置了用于生成二维码的 Filter CIQRCodeGenerator。

NSString *link = @"https://www.google.com";
NSData *codeData = [link dataUsingEncoding:NSUTF8StringEncoding];
CIFilter *codeFilter = [CIFilter filterWithName:@"CIQRCodeGenerator"
                            withInputParameters:@{@"inputMessage" : codeData,
                                                @"inputCorrectionLevel" : @"Q"}];
CIImage *image = codeFilter.outputImage;

参数中的 inputCorrectionLevel 指生成的二维码的纠错级别,LMQH 四个级别可选,级别越高纠错能力越强。注意最后生成的 CIImage 还需要使用 imageByApplyingTransform 缩放到计算好的尺寸。

生成海报

一步步把海报各个部分挨个“画”上去,最后计算出 renderHeight,使用 CIContext 生成海报。

CGRect renderRect = CGRectMake(0, 0, canvasWidth, renderHeight);
// 切成最后计算好的尺寸    
renderImage = [renderImage imageByCroppingToRect:renderRect];
    
// draw image
CIContext *context = [CIContext contextWithOptions:@{kCIContextUseSoftwareRenderer : @NO}];
CGFloat scale = [UIScreen mainScreen].scale;
CGImageRef imageRef = [context createCGImage:renderImage fromRect:renderImage.extent];
UIImage *finalImage = [UIImage imageWithCGImage:imageRef scale:scale orientation:UIImageOrientationUp];
CGImageRelease(imageRef);

得到最终的海报图片。

关于 CIContext

  1. kCIContextUseSoftwareRenderer 为 NO 的时候会使用 GPU 来处理,只能在主线程运行。
  2. CIContext 的创建开销比较大,尽量重用。
  3. 在用 CIFilter 处理 CIImage 的时候,并没有真正对图片数据进行处理,只是描述了整个过程;等到 CIContext 来生成的图片的时候,才真正处理了图片。并且 Core Image 会对一些过程做合并优化,提高效率。

参考资料:Core Image Getting the Best Performance

Fast Enumeration in Objc

4 May ’16

遍历一个容器内所有的元素,是在开发过程中很常见的操作。在 Objc 中常用的有 C style loop(据说在 swift 3 中已不再支持)、Block style enumerate、NSEnumerator 和 for in style。前两种比较简单这里不再赘述,先简单谈一下 NSEnumerator。

NSEnumerator 是一个抽象类,只有两个方法,-(NSArray*)allobjects-(id)nexObject。你必须继承 NSEnumerator 然后使用子类的实例,在内部实现中使用自定义的状态值来记住当前遍历的状态,以便实现 -(id)nextObject 方法,如果你得容器类存放的数据是有序的,这个状态值可以是当前遍历到的元素的序号;当-(id)nexObject 返回 nil的时候遍历结束。-(NSArray*)allObjects不是一定要实现的,NSEnumerator 提供了一个默认的实现,既循环执行 -(id)nexObject 把返回的 object 塞到一个 NSMutableArray 里面,为了效率考虑可以根据情况自己提供更快的实现。

如果不关心 index 的话,for in style 的遍历是一种更方便的写法,也是遍历速度“最快”的,毕竟只有遵循 NSFastEnumeration 协议的才能这样遍历。NSArray , NSDictionary , NSSet 都实现了这个协议。

for (NSNumber *number in array) {
  //do something
}

NSFastEnmeration协议只有一个方法,- (NSUInteger)countByEnumeratingWithState:(NSFastEnumerationState *)state objects:(id *)stackbuf count:(NSUInteger)len;,当进行 for in 循环的时候,这个方法会被多次调用,分批遍历,每次遍历的元素个数既这个方法的返回值,当这个方法返回 0 的时候,既表示没有更多元素可供遍历了,遍历随即结束。接下来看看 NSFastEnumerationState

typedef struct {
		unsigned long state;
		id *itemsPtr;
		unsigned long *mutationsPtr;
		unsigned long extra[5];
} NSFastEnumerationState;

NSFastEnumerationState 这个结构体的作用是用来储存遍历的状态信息的,遍历开始的时候 Cocoa 会初始化一个 NSFastEnumerationState ,即 {state = 0},通过上述方法的第一个参数传入,传入之后便交给协议实现方来控制,根据具体情况来设置相应的状态。对于一个有序的容器来说,state可能是当前遍历到的元素的序号;对于一个链表类型的容器来说,state可能是当前遍历到个元素的指针。itemPtr是一个 C 数组的指针,指向的就是这批遍历所有的元素,其大小由方法的返回值指定。mutationsPtr 用于表明在遍历过程中容器是否发生了变化,Cocoa 在遍历过程中会多次检查这个值,如果前后不匹配,则会抛出异常;mutationsPtr 不可以为 null ,也不要指向 self;对于不可变的容器(如 NSArray),将其指向一个不会变的地址即可,对于可变的容器(如 NSMutaleArray),就需要用一个内部变量来表示这个状态了。extra是额外用来辅助保存遍历状态的空间位,用不用自便。方法的后面两个参数,也是可用可不用,stackbuf是 Cocoa 提供的 C 数组方便方法实现方来存放该批将要被遍历的元素的,如果不用的话就需要自己去申请内存空间来生成 C 数组,用的话那么还需要看 len 的值,它是 stackbuf 的长度,既最多可存放的元素的个数,一般是 2^4 ,取出该批遍历的元素塞进 stackbuf, 然后把 itemsPtr 指向 stackbuf,既 state.itemsPtr = stackbuf 即可。

参考文献:Friday Q&A 2010-04-16: Implementing Fast Enumeration