这篇教程,是系列教程的第二篇,前篇名为《iOS平台图片编解码入门教程(Image/IO篇)》。这篇主要讲第三方解码器如何在iOS平台上处理(和Image/IO的几大要点一一对应),更会介绍一些基本的Bitmap概念,总结通用的处理方法,毕竟授人以鱼不如授人以渔
第三方编解码
对于图片编解码来说,Apple自带的Image/IO确实非常的易用,但是对于Image/IO不支持的图像格式就能无能为力了。截止到iOS 11,Image/IO不支持WebP,BPG,对于一些需要依赖WebP的业务就比较麻烦了(WebP的优点就不再介绍了)。不过我们可以自己集成第三方的图片解码器,去支持这些需要的的格式。
一般来说,我们需要根据自己想要支持的图像格式,选择相对应的编解码器,进行编解码。这里我们以WebP的解码库libwebp为例子,其他解码器需要根据对应解码器的API处理,基本概念类似。
解码
不像Image/IO那样封装了整套流程,第三方解码的关键之处,就是在于获取到图像的Bitmap数据,通常情况就是RGBA的矢量表示。
简单解释一下,Bitmap可以理解为连续排列像素(Pixel)的二维数组,一个像素包括4个通道(Components)的点,每个点的位数叫做色深(常见的32位色,指的就是1个像素有4通道,每个通道8位),而像素的通道排列顺序按照对应的RGBA Mode顺序排列,比如说RGBA8888(大端序),就是这样一串连续的值:
1
| {uint8_t red, uint8_t green, uint8_t blue, uint8_t alpha, uint8_t red, uint8_t green, uint8_t blue, uint8_t alpha...}
|
这样的话,在内存中,一般就可以用uint8_t bitmap[width * components * 8][height]
来表示。
有了这样的知识,对照着就能看懂CGImage的BitmapInfo所表示的信息了。
静态图
只要Bitmap数据到手,后面的过程其实都大同小异。第三方解码器主要处理图像编码数据到原始Bitmap的解码过程,后续就可以通过固定的方式来得到CGImage或者UIImage,用于上层UI组件的渲染。
- 第三方解码器获取图像Bitmap
- 通过Bitmap创建CGImage
- CGImage重绘(可选)
- 生成上层的UIImage,清理
1. 第三方解码器获取图像Bitmap
根据自己的需要,可以选择对应的编码器,来获取图像Bitmap。这里我们以WebP的解码库libwebp为例子,其他解码器需要根据对应解码器的API处理,基本概念类似。
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| NSData *data;
WebPData webpData; WebPDataInit(&webpData); webpData.bytes = data.bytes; webpData.size = data.length;
WebPDecoderConfig config; if (!WebPInitDecoderConfig(&config)) { return nil; }
if (WebPGetFeatures(webpData.bytes, webpData.size, &config.input) != VP8_STATUS_OK) { return nil; }
bool has_alpha = config.input.has_alpha; config.output.colorspace = has_alpha ? MODE_rgbA : MODE_RGB; config.options.use_threads = 1;
if (WebPDecode(webpData.bytes, webpData.size, &config) != VP8_STATUS_OK) { return nil; }
int width = config.input.width; int height = config.input.height; if (config.options.use_scaling) { width = config.options.scaled_width; height = config.options.scaled_height; }
uint8_t *rgba = config.output.u.RGBA.rgb; size_t rgbaSize = config.output.u.RGBA.size;
|
截止到这里,我们基本上调用通过第三方解码库的接口,就完成了获取Bitmap的工作。一般来说,以RGBA来说,最少需要知道以下信息:图像RGBA数组,数组大小,图像宽度、高度、是否含有Alpha通道这几个,以便开始下一步的创建CGImage的过程
2. 通过Bitmap创建CGImage
有了图像的Bitmap数据之后,可以通过CGImageCreate
来生成CGImage。对于RGBA的输入,需要的参数基本比较固定,以下代码基本上可以参考来复用。(需要注意,iOS上只支持premultiplied-alpha,macOS可以支持非premultiplied-alpha)
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| CGDataProviderRef provider = CGDataProviderCreateWithData(NULL, rgba, rgbaSize, FreeImageData);
CGColorSpaceRef colorSpaceRef = CGColorSpaceCreateDeviceRGB();
CGBitmapInfo bitmapInfo = has_alpha ? kCGBitmapByteOrder32Big | kCGImageAlphaPremultipliedLast : kCGBitmapByteOrder32Big | kCGImageAlphaNoneSkipLast;
size_t components = has_alpha ? 4 : 3;
CGColorRenderingIntent renderingIntent = kCGRenderingIntentDefault;
size_t bytesPerRow = components * width;
CGImageRef imageRef = CGImageCreate(width, height, 8, components * 8, bytesPerRow, colorSpaceRef, bitmapInfo, provider, NULL, NO, renderingIntent);
CGDataProviderRelease(provider);
static void FreeImageData(void *info, const void *data, size_t size) { free((void *)data); }
|
3. CGImage重绘
这一步其实是可选的,但是建议都加上这一步骤。虽然我们之前通过RGBA创建了CGImage,但是实际上,CALayer和上层的UIImageView这些渲染的时候,要求的色彩是限定的,不然会有额外的内存和渲染消耗,我们解码出来的rgba的格式可能并不是按照这样的色彩空间排列,因此建议进行一次重绘,即将CGImage重绘到一个CGBitmapContext之上。这个代码比较简单。
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| CGImageRef imageRef; size_t canvasWidth = CGImageGetWidth(imageRef) size_t canvasHeight = CGImageGetHeight(imageRef) CGBitmapInfo bitmapInfo; if (! has_alpha) { bitmapInfo = kCGBitmapByteOrder32Big | kCGImageAlphaNoneSkipLast; } else { bitmapInfo = kCGBitmapByteOrder32Big | kCGImageAlphaPremultipliedLast; }
CGContextRef canvas = CGBitmapContextCreate(NULL, canvasWidth, canvasHeight, 8, 0, CGColorSpaceCreateDeviceRGB(), bitmapInfo); if (!canvas) { return nil; } CGImageRef imageRef = image.CGImage; size_t width = CGImageGetWidth(imageRef); size_t height = CGImageGetHeight(imageRef);
CGContextDrawImage(canvas, CGRectMake(0, 0, width, height), imageRef); CGImageRef newImageRef = CGBitmapContextCreateImage(canvas);
|
4. 生成上层的UIImage,清理
有了最终的用于显示CGImage,那么我们就可以生成一个UIImage来给UI组件显示了。注意如果需要有特殊的scale,orientation处理(比如说图像可能有额外的EXIF Orientation信息),需要在这一步加上。
由于是C接口,需要手动清理内存,除了CGImage相关的,也需要清理第三方库自己的内存分配。对于错误提前返回的清理内存,灵活运用__attribute__((cleanup))
,设置一个返回函数前清理的Block,可以减少犯错的可能性
示例代码:
1 2 3 4 5
| UIImage *image = [UIImage imageWithCGImage:imageRef scale:1 orientation:UIImageOrientationUp];
CGImageRelease(newImageRef); CGContextRelease(canvas);
|
动态图
动态图的解码过程,其实很直观的想,我们目标就是需要对所有动图帧,都拿到Bitmap,解码到CGImage和UIImage就行了。这样想的话,其实步骤就比较明确了。
步骤:
- 第三方解码器生成每帧的Bitmap
- 重复静态图的2-3
- 生成动图UIImage(参考Image/IO)
1. 第三方解码器生成每帧的Bitmap
不同解码器可能对于动图有特殊的解码过程,拿libwebp举例来说,libwebp的动图,需要用到它的demux模块,其他解码器自行参考对应的API。
同时,这里需要额外介绍一些概念。一般来说,动图格式的话不会直接将每帧原始的Bitmap都编码到文件中,这样得到的文件过于庞大(帧数 * 每帧Bitmap)。因此,会有Dispose Method的方式(可以参考WebP规范Disposal method (D): 1 bit,移动端图片格式调研)。简单点来说,对于动图来说,每一帧有一个参考画布,在前一帧画完以后,后一帧可以利用前一帧已画好的图像,仅仅改变前后变化的部分,从而减小整体大小。因此我们创建动图时,需要准备好一个CGBitmapContext当作画布,根据Disposal Method(如果为None,不清空canvas;如果为Background,清空为Background Color,一般就是直接清空成透明)
有了所有帧的Bitmap后,转成CGImage,UIImage,最后生成动图UIImage,这个在系列前篇已经介绍过了,不再赘述
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61
| - (void)decodeWebP { WebPDemuxer *demuxer; WebPIterator iter; if (!WebPDemuxGetFrame(demuxer, 1, &iter)) { return nil; } NSMutableArray<UIImage *> *images = [NSMutableArray array]; double durations[frameCount]; do { @autoreleasepool { UIImage *image = [self drawnWebpImageWithCanvas:canvas iterator:iter]; int duration = iter.duration; [images addObject:image]; durations[frame_num] = duration; } } while (WebPDemuxNextFrame(&iter)); UIImage *animatedImage = [self animatedImageWithImages:images durations:durations]; WebPDemuxReleaseIterator(&iter); }
- (nullable UIImage *)drawnWebpImageWithCanvas:(CGContextRef)canvas iterator:(WebPIterator)iter { UIImage *image = [self rawWebpImageWithData:iter.fragment]; if (!image) { return nil; } size_t canvasWidth = CGBitmapContextGetWidth(canvas); size_t canvasHeight = CGBitmapContextGetHeight(canvas); CGSize size = CGSizeMake(canvasWidth, canvasHeight); CGFloat tmpX = iter.x_offset; CGFloat tmpY = size.height - iter.height - iter.y_offset; CGRect imageRect = CGRectMake(tmpX, tmpY, iter.width, iter.height); BOOL shouldBlend = iter.blend_method == WEBP_MUX_BLEND; if (!shouldBlend) { CGContextClearRect(canvas, imageRect); } CGContextDrawImage(canvas, imageRect, image.CGImage); CGImageRef newImageRef = CGBitmapContextCreateImage(canvas); image = [UIImage imageWithCGImage:newImageRef]; CGImageRelease(newImageRef); if (iter.dispose_method == WEBP_MUX_DISPOSE_BACKGROUND) { CGContextClearRect(canvas, imageRect); } return image; }
|
渐进式解码
渐进式解码的概念,在系列前篇中已经介绍过了,一般来说,第三方解码器支持渐进式解码的接口都比较类似,通过提供二进制流不断进行Update,每次能够得到当前解码的部分的Bitmap,最后可以拿到完整的Bitmap。之后只需要参考静态图对应步骤即可。
这里还是以libwebp的接口为例,libwebp需要使用它的WebPIDecoder接口,来专门进行渐进式解码。注意,libwebp渐进式解码出来的Bitmap不会将未解码的部分自动填空,会保留随机的内存地址置,要么手动清空,要么画的时候仅仅画解码出来的高度部分。
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| NSData *data; UIImage *image; WebPIDecoder *idec = WebPINewRGB(MODE_rgbA, NULL, 0, 0); if (!idec) { return nil; }
VP8StatusCode status = WebPIUpdate(idec, data.bytes, data.length); if (status != VP8_STATUS_OK && status != VP8_STATUS_SUSPENDED) { return nil; } int width = 0; int height = 0; int last_y = 0; int stride = 0;
uint8_t *rgba = WebPIDecGetRGB(_idec, &last_y, &width, &height, &stride);
CGDataProviderRef provider; CGBitmapInfo bitmapInfo = kCGBitmapByteOrder32Big | kCGImageAlphaPremultipliedLast; size_t components = 4;
CGImageRef imageRef = CGImageCreate(width, last_y, 8, components * 8, components * width, CGColorSpaceCreateDeviceRGB(), bitmapInfo, provider, NULL, NO, kCGRenderingIntentDefault);
CGContextRef canvas = CGBitmapContextCreate(NULL, width, height, 8, 0, CGColorSpaceCreateDeviceRGB(), bitmapInfo);
CGContextDrawImage(canvas, CGRectMake(0, height - last_y, width, last_y), imageRef);
CGImageRef newImageRef = CGBitmapContextCreateImage(canvas);
image = [[UIImage alloc] initWithCGImage:newImageRef];
|
编码
编码过程其实比解码过程要简单得多,因为实际上,我们可以通过自带的接口,直接拿到当前UIImage的Bitmap数据,因此只要将Bitmap交给第三方编码库来进行编码,最后输出数据即可。
静态图
静态图的过程其实就可以直接分为两步:
- UIImage获取Bitmap
- 调用编码器进行编码
1. UIImage获取Bitmap
UIImage本身能够直接通过方法拿到对应的CGImage,这样只需要调用CGImageGetDataProvider
就可以拿到对应的Bitmap数据的DataProvider了,直接上代码吧。
示例代码:
1 2 3 4 5 6 7 8 9 10
| UIImage *image; NSData *webpData; CGImageRef imageRef = image.CGImage; size_t width = CGImageGetWidth(imageRef); size_t height = CGImageGetHeight(imageRef); size_t bytesPerRow = CGImageGetBytesPerRow(imageRef); CGDataProviderRef dataProvider = CGImageGetDataProvider(imageRef); CFDataRef dataRef = CGDataProviderCopyData(dataProvider); uint8_t *rgba = (uint8_t *)CFDataGetBytePtr(dataRef);
|
2. 调用编码器进行编码
我们还是以libwebp来对WebP进行编码,libwebp对于静态图片的编码非常简单(动态图片需要调用另一套mux的API,在动图章节讲)
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13
| uint8_t *data = NULL; float quality = 100.0; size_t size = WebPEncodeRGBA(rgba, (int)width, (int)height, (int)bytesPerRow, quality, &data); CFRelease(dataRef); rgba = NULL; if (size) { webpData = [NSData dataWithBytes:data length:size]; } if (data) { WebPFree(data); }
|
动态图
对于动态图来说,也就是将多帧的Bitmap输入到编码器即可。对于libwebp的动态图编码,需要利用到它的mux模块,它能够将多个编码成WebP的二进制流,最后mux合并一次,最终得到了动态WebP。因此我们需要利用之前的静态图编码的步骤,只需要依次遍历取图并编码,最后使用mux处理即可。
步骤:
- 遍历每帧Bitmap,编码
1. 遍历每帧Bitmap,编码
示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| NSData *data; NSArray<UIImgae *> *images; double durations[frameCount]; int loopCount;
WebPMux *mux = WebPMuxNew(); if (!mux) { return nil; }
for (size_t i = 0; i < framesCount; i++) { NSData *webpData = [self encodedWebpDataWithImage:images[i]]; int duration = (int)durations[i] * 1000; WebPMuxFrameInfo frame = {.bitstream.bytes = webpData.bytes if (WebPMuxPushFrame(mux, &frame, 0) != WEBP_MUX_OK) { WebPMuxDelete(mux); return nil; } }
WebPMuxAnimParams params; params.bgcolor = 0, params.loop_count = loopCount; WebPMuxSetAnimationParams(mux, ¶ms);
WebPData outputData;
WebPMuxError error = WebPMuxAssemble(mux, &outputData); WebPMuxDelete(mux); if (error != WEBP_MUX_OK) { return nil; } data = [NSData dataWithBytes:outputData.bytes length:outputData.size]; WebPDataClear(&outputData);
|
总结
第三方编解码其实相对于Image/IO来说,主要难度其实在于需要获取的Bitmap。开发者需要一点基本的图像知识,再者就是要能会用第三方编解码器的接口(一般来说第三方编解码器就是C或者C++写的,与OC和Swift交互也非常方便,至少不用像Java JNI那样调用)。之后只要按照通用的步骤,去编码和解码即可。
到这里的话,一般的大部分格式的编解码就基本没有问题了。当然,关于进阶的方面,比如图像的编解码性能优化,进阶的图像处理(Bitmap的几何变化,Alpha合成,位数转换等等)这就需要用到更低层的库vImage了,会在之后的系列教程中进行介绍。