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