项目中在进行图片裁剪时候,为了性能和时间上的优化,使用了Core Graphics中的相关方法。但在使用CGBitmapContextCreate方法时,却遇到了一些问题。
在某些iOS11系统上,手机快捷键屏幕截图生成的方法,在经过如下代码
CGImageRef imageRef = self.CGImage;
size_t bitsPerComponent = CGImageGetBitsPerComponent(imageRef);
size_t bytesPerRow = CGImageGetBytesPerRow(imageRef);
CGColorSpaceRef colorSpaceRef = CGImageGetColorSpace(imageRef);
CGBitmapInfo bitmapInfo = CGImageGetBitmapInfo(imageRef);
CGContextRefcontext =CGBitmapContextCreate(NULL, width, height, bitsPerComponent, bytesPerRow, colorSpaceRef, bitmapInfo);
if(!context)returnnil;
使用CGBitmapContextCreate生成CGContextRef时,会报错
CGBitmapContextCreate: unsupported parameter combination: set CGBITMAP_CONTEXT_LOG_ERRORS environmental variable to see the details
为什么会出现这种问题呢?在iOS11之前,屏幕截图的时候使用上述代码都没有问题。即便是iOS11,如果是系统相机拍照,也不会出现这种报错。那么为什么iOS11系统通过快捷键截图屏幕产生的图片会出现这种问题呢?带着疑问,笔者做了如下调查,结果如下:
iOS11.0~iOS11.4为止,这个区间的iPhone手机,在进行快捷键屏幕截图时候,发现生成的图片的bitmap信息为kCGImageAlphaLast | kCGImageByteOrder16Little
CGBitmapInfo
首先看下CGBitmapInfo的结构
typedefCF_OPTIONS(uint32_t, CGBitmapInfo) {
kCGBitmapAlphaInfoMask =0x1F,
kCGBitmapFloatInfoMask =0xF00,
kCGBitmapFloatComponents = (1<<8),
kCGBitmapByteOrderMask =kCGImageByteOrderMask,
kCGBitmapByteOrderDefault =kCGImageByteOrderDefault,
kCGBitmapByteOrder16Little =kCGImageByteOrder16Little,
kCGBitmapByteOrder32Little =kCGImageByteOrder32Little,
kCGBitmapByteOrder16Big =kCGImageByteOrder16Big,
kCGBitmapByteOrder32Big =kCGImageByteOrder32Big
} CG_AVAILABLE_STARTING(10.0, 2.0);
它主要提供了三个方面的布局信息:
* alpha的信息;
* 颜色分量是否为浮点数;
* 像素格式的字节顺序;
alpha信息
图像的alpha信息,可以通过bitmap按位与kCGBitmapAlphaInfoMask获取(bitmapInfo & kCGBitmapAlphaInfoMask),也可以直接通过CGImageGetAlphaInfo函数获取。
获取到的是CGImageAlphaInfo类型的信息:
typedefCF_ENUM(uint32_t, CGImageAlphaInfo) {
kCGImageAlphaNone, /* For example, RGB. */
kCGImageAlphaPremultipliedLast, /* For example, premultiplied RGBA */
kCGImageAlphaPremultipliedFirst, /* For example, premultiplied ARGB */
kCGImageAlphaLast, /* For example, non-premultiplied RGBA */
kCGImageAlphaFirst, /* For example, non-premultiplied ARGB */
kCGImageAlphaNoneSkipLast, /* For example, RBGX. */
kCGImageAlphaNoneSkipFirst, /* For example, XRGB. */
kCGImageAlphaOnly /* No color data, alpha data only */
};
同样提供了三方面的alpha信息:
是否包含alpha;
如果包含alpha,那么alpha信息所处的位置,在像素的最低有效位,比如RGBA,还是最高有效位,比如ARGB;
如果包含alpha,那么每个颜色分量是否已经预乘alpha的值。这种做法可以加速图片的渲染时间,因为它避免了渲染时的额外乘法运算。比如,对于RGB颜色空间,用已经乘以alpha的数据来渲染图片,每个像素都可以避免3次乘法运算,红色乘以alpha、绿色乘以alpha以及蓝色乘以alpha。
其中的kCGImageAlphaNone、kCGImageAlphaNoneSkipLast、kCGImageAlphaNoneSkipFirst表示该图片中不包含alpha,kCGImageAlphaPremultipliedLast和kCGImageAlphaPremultipliedFirst表示进行alpha的预乘操作,kCGImageAlphaLast、kCGImageAlphaFirst有alpha信息但是不预乘,kCGImageAlphaOnly代表没有颜色数据,只有alpha。
下面来看一张图,它非常形象地展示了在使用16或32位像素格式的CMYK和RGB颜色空间下,一个像素是如何被表示的:
我们从图中可以看出,在 32 位像素格式下,每个颜色分量使用 8 位;而在 16 位像素格式下,每个颜色分量则使用 5 位。
这时候插个题外话:CMYK和RGB格式都是干嘛的?
颜色和颜色空间
Quartz中的颜色是用一组值来表示。而颜色空间用于解析这些颜色信息。例如,表4-1列出了在全亮度下蓝色值在不同颜色空间下的值。如果不知道颜色空间及颜色空间所能接受的值,我们没有办法知道一组值所表示的颜色。
如果我们使用了错误的颜色空间,我们可能会获得完全不同的颜色,如图4-1所示。
颜色空间可以有不同数量的组件。表4-1中的颜色空间中其中三个只有三个组件,而CMYK有四个组件。值的范围与颜色空间有关。对大部分颜色空间来说,颜色值范围为[0.0, 1.0],1.0表示全亮度。例如,全亮度蓝色值在Quartz的RGB颜色空间中的值是(0, 0, 1.0)。在Quartz中,颜色值同样有一个alpha值来表示透明度。在表4-1中没有列出该值。
Pixel Format
位图其实就是一个像素数组,而像素格式则是用来描述每个像素的组成格式,它包括以下信息:
Bits per component : 一个像素中每个独立的颜色分量使用的bit数
Bits per pixel : 一个像素使用的总bit数
Bytes per row : 位图中每一行使用的字节数
有一点需要注意的是,对于位图来说,像素格式并不是随意组合的,目前支持以下有限的17种特定组合
从上图可知,对于iOS来说,只支持8种像素格式。其中颜色空间为Null的1种,Gray的2种,RGB的5种,CMYK的0种。换句话说,iOS不支持CMYK的颜色空间。
在YYKit开源库中,有这样一段代码:
CGImageRefYYCGImageCreateDecodedCopy(CGImageRefimageRef,BOOLdecodeForDisplay) {
if(!imageRef)returnNULL;
size_twidth =CGImageGetWidth(imageRef);
size_theight =CGImageGetHeight(imageRef);
if(width ==0|| height ==0)returnNULL;
if (decodeForDisplay) { //decode with redraw (may lose some precision)
CGImageAlphaInfo alphaInfo = CGImageGetAlphaInfo(imageRef) & kCGBitmapAlphaInfoMask;
BOOLhasAlpha =NO;
if (alphaInfo == kCGImageAlphaPremultipliedLast ||
alphaInfo ==kCGImageAlphaPremultipliedFirst ||
alphaInfo ==kCGImageAlphaLast||
alphaInfo ==kCGImageAlphaFirst) {
hasAlpha =YES;
}
// BGRA8888 (premultiplied) or BGRX8888
// same as UIGraphicsBeginImageContext() and -[UIView drawRect:]
CGBitmapInfo bitmapInfo = kCGBitmapByteOrder32Host;
bitmapInfo |= hasAlpha ?kCGImageAlphaPremultipliedFirst : kCGImageAlphaNoneSkipFirst;
CGContextRefcontext =CGBitmapContextCreate(NULL, width, height,8,0,YYCGColorSpaceGetDeviceRGB(), bitmapInfo);
if(!context)returnNULL;
CGContextDrawImage(context,CGRectMake(0,0, width, height), imageRef);// decode解码
CGImageRef newImage = CGBitmapContextCreateImage(context);
CFRelease(context);
returnnewImage;
}else{
CGColorSpaceRef space = CGImageGetColorSpace(imageRef);
size_tbitsPerComponent =CGImageGetBitsPerComponent(imageRef);
size_tbitsPerPixel =CGImageGetBitsPerPixel(imageRef);
size_tbytesPerRow =CGImageGetBytesPerRow(imageRef);
CGBitmapInfobitmapInfo =CGImageGetBitmapInfo(imageRef);
if(bytesPerRow ==0|| width ==0|| height ==0)returnNULL;
CGDataProviderRef dataProvider = CGImageGetDataProvider(imageRef);
if(!dataProvider)returnNULL;
CFDataRefdata =CGDataProviderCopyData(dataProvider);// decode解码
if(!data)returnNULL;
CGDataProviderRef newProvider = CGDataProviderCreateWithCFData(data);
CFRelease(data);
if(!newProvider)returnNULL;
CGImageRefnewImage =CGImageCreate(width, height, bitsPerComponent, bitsPerPixel, bytesPerRow, space, bitmapInfo, newProvider,NULL,false,kCGRenderingIntentDefault);
CFRelease(newProvider);
returnnewImage;
}
}
可以看到,如果decodeForDisplay为YES,也就是说需要显示图片,那么CGBitmapContextCreate函数传入的colorSpace为YYCGColorSpaceGetDeviceRGB(),看下YYCGColorSpaceGetDeviceRGB()函数的定义:
CGColorSpaceRefYYCGColorSpaceGetDeviceRGB() {
static CGColorSpaceRef space;
staticdispatch_once_tonceToken;
dispatch_once(&onceToken, ^{
space =CGColorSpaceCreateDeviceRGB();
});
returnspace;
}
可见是使用了RGB格式,这里就是因为iOS显示的图片不支持CMYK等一些格式,所以做了转化,转为RGB格式的图片。而如果不需要显示图片,那么没必要转化,YYKit直接使用了CGImageGetColorSpace获取colorSpace。
上面介绍了bitmap的alpha等信息,那么大端小端又是怎么回事儿呢?
CGImageByteOrderInfo
typedefCF_ENUM(uint32_t, CGImageByteOrderInfo) {
kCGImageByteOrderMask = 0x7000,
kCGImageByteOrderDefault = (0<<12),
kCGImageByteOrder16Little = (1<<12),
kCGImageByteOrder32Little = (2<<12),
kCGImageByteOrder16Big = (3<<12),
kCGImageByteOrder32Big = (4<<12)
} CG_AVAILABLE_STARTING(10.0, 2.0);
CGImageByteOrderInfo提供了两个方面的字节顺序信息:
小端模式还是大端模式
数据以16位还是32位为单位
对于iPhone 来说,采用的是小端模式,但是为了保证应用的向后兼容性,我们可以使用系统提供的宏,来避免Hardcoding
#ifdef __BIG_ENDIAN__
# define kCGBitmapByteOrder16Host kCGBitmapByteOrder16Big
# define kCGBitmapByteOrder32Host kCGBitmapByteOrder32Big
#else /* Little endian. */
# define kCGBitmapByteOrder16Host kCGBitmapByteOrder16Little
# define kCGBitmapByteOrder32Host kCGBitmapByteOrder32Little
#endif
那么平时使用时候,使用Core Graphics创建bitmap应该使用哪种CGImageByteOrderInfo呢?在苹果官方文档中关于UIGraphicsBeginImageContextWithOptions有这样一段话:
Discussion
You use this function to configure the drawing environment for rendering into a bitmap. The format for the bitmap is a ARGB 32-bit integer pixel format using host-byte order. If the opaque parameter is true, the alpha channel is ignored and the bitmap is treated as fully opaque (CGImageAlphaInfo.noneSkipFirst | kCGBitmapByteOrder32Host). Otherwise, each pixel uses a premultipled ARGB format (CGImageAlphaInfo.premultipliedFirst | kCGBitmapByteOrder32Host).
翻译:您可以使用此功能配置绘图环境以渲染为位图。 位图的格式是使用主机字节顺序的ARGB 32位整数像素格式。 如果opaque参数为true,则忽略alpha通道,并将位图视为完全不透明(CGImageAlphaInfo.noneSkipFirst | kCGBitmapByteOrder32Host)。 否则,每个像素使用预乘的ARGB格式(CGImageAlphaInfo.premultipliedFirst | kCGBitmapByteOrder32Host)。
可见苹果推荐使用kCGBitmapByteOrder32Host。比较了几个开源库,YYKit使用了推荐的kCGBitmapByteOrder32Host,而SDWebImage和FLAnimatedImage都是使用的kCGBitmapByteOrderDefault。
问题解决
好了,以上就是为了解决上面遇到的问题需要了解的bitmap相关的知识。现在我们可以接着看这个问题。
上面说了,iOS11.0~iOS11.4版本的屏幕截图,CGBitmapInfo信息为kCGImageAlphaLast | kCGImageByteOrder16Little
。
而在iOS11之前,甚至在最新的iOS11.4.1上,屏幕截图的CGBitmapInfo信息为kCGImageAlphaNoneSkipLast | 0 (default byte order)
而如果是系统相机(或者其他相机)拍摄出来的照片,不管是iOS11.0~iOS11.4,还是其他所有的iOS系统,CGBitmapInfo都是kCGImageAlphaNoneSkipLast | 0 (default byte order)
其中0 (default byte order)就是指的kCGBitmapByteOrderDefault。
是因为kCGImageByteOrder16Little引起的创建CGContextRef失败吗?
我们将CGBitmapInfo创建时设置为kCGImageAlphaLast | kCGBitmapByteOrderDefault
发现依然失败,那么是kCGImageAlphaLast的问题吗?是不是因为没有预乘,所以失败呢?带着疑问,我们将CGBitmapInfo创建时设置为kCGImageAlphaPremultipliedLast | kCGImageByteOrder16Little
果然成功了!!!
经过多方查证发现,自从iOS8之后,苹果官方不允许使用不经过预乘的alpha,也就是说kCGImageAlphaLast和kCGImageAlphaFirst都不能使用,而是应该使用kCGImageAlphaPremultipliedLast和kCGImageAlphaPremultipliedFirst。
好了,找到了问题的原因,那么应该怎么修改呢?可以参考以下开源库的做法。
SDWebImage在图片解码时固定使用 kCGBitmapByteOrderDefault | kCGImageAlphaNoneSkipLast
,其他有些地方使用了kCGBitmapByteOrderDefault | kCGImageAlphaPremultipliedFirst
FLAnimatedImage中使用如下的方法:
CGBitmapInfo bitmapInfo = kCGBitmapByteOrderDefault;
CGImageAlphaInfo alphaInfo = CGImageGetAlphaInfo(imageToPredraw.CGImage);
// If the alpha info doesn't match to one of the supported formats (see above), pick a reasonable supported one.
// "For bitmaps created in iOS 3.2 and later, the drawing environment uses the premultiplied ARGB format to store the bitmap data." (source: docs)
if (alphaInfo ==kCGImageAlphaNone|| alphaInfo ==kCGImageAlphaOnly) {
alphaInfo =kCGImageAlphaNoneSkipFirst;
} else if (alphaInfo ==kCGImageAlphaFirst) {
alphaInfo =kCGImageAlphaPremultipliedFirst;
} else if (alphaInfo ==kCGImageAlphaLast) {
alphaInfo =kCGImageAlphaPremultipliedLast;
}
// "The constants for specifying the alpha channel information are declared with the `CGImageAlphaInfo` type but can be passed to this parameter safely." (source: docs)
bitmapInfo |= alphaInfo;
而YYKit使用如下方法:
CGImageAlphaInfo alphaInfo = CGImageGetAlphaInfo(imageRef) & kCGBitmapAlphaInfoMask;
BOOLhasAlpha =NO;
if (alphaInfo == kCGImageAlphaPremultipliedLast ||
alphaInfo ==kCGImageAlphaPremultipliedFirst ||
alphaInfo ==kCGImageAlphaLast||
alphaInfo ==kCGImageAlphaFirst) {
hasAlpha =YES;
}
// BGRA8888 (premultiplied) or BGRX8888
// same as UIGraphicsBeginImageContext() and -[UIView drawRect:]
CGBitmapInfo bitmapInfo = kCGBitmapByteOrder32Host;
bitmapInfo |= hasAlpha ?kCGImageAlphaPremultipliedFirst : kCGImageAlphaNoneSkipFirst;
CGContextRefcontext =CGBitmapContextCreate(NULL, width, height,8,0,YYCGColorSpaceGetDeviceRGB(), bitmapInfo);
这几个开源库是比较热门的,用户数量庞大,相对来说兼容性会好得多,所以比较推荐使用开源库的做法。尤其是FLAnimatedImage开源库中那句注释:“If the alpha info doesn't match to one of the supported formats (see above), pick a reasonable supported one.”说得很好,这也是为什么开源库能够避开那些坑。
具体使用哪种方式,可自行斟酌。
PS:关于colorSpace的问题,如果遇到了iOS不支持的格式比如CMYK等,可参考SDWebImage的做法:
+ (CGColorSpaceRef)colorSpaceForImageRef:(CGImageRef)imageRef {
// current
CGColorSpaceModel imageColorSpaceModel = CGColorSpaceGetModel(CGImageGetColorSpace(imageRef));
CGColorSpaceRef colorspaceRef = CGImageGetColorSpace(imageRef);
BOOLunsupportedColorSpace = (imageColorSpaceModel ==kCGColorSpaceModelUnknown||
imageColorSpaceModel ==kCGColorSpaceModelMonochrome||
imageColorSpaceModel ==kCGColorSpaceModelCMYK||
imageColorSpaceModel ==kCGColorSpaceModelIndexed);
if(unsupportedColorSpace) {
colorspaceRef =CGColorSpaceCreateDeviceRGB();
CFAutorelease(colorspaceRef);
}
returncolorspaceRef;
}
iOS11.4.1版本上,貌似是苹果意识到了这个问题,将屏幕截图的bitmap信息又修改回了kCGImageAlphaNoneSkipLast | 0 (default byte order)
正确方法:
- (UIImage*)scaleWithSize:(CGSize)size {
if (!self) {
return nil;
}
CGFloat width = self.size.width;
CGFloat height = self.size.height;
if (width * height == 0) {
return self;
}
float verticalRadio = size.height * 1.0 / height;
float horizontalRadio = size.width * 1.0 / width;
float radio =1;
if (verticalRadio <1 || horizontalRadio <1) {
radio = MIN(verticalRadio, horizontalRadio);
}
width = width * radio;
height = height * radio;
CGImageRef imageRef = self.CGImage;
CGImageAlphaInfo alphaInfo = CGImageGetAlphaInfo(imageRef) & kCGBitmapAlphaInfoMask;
BOOL hasAlpha = NO;
if (alphaInfo == kCGImageAlphaPremultipliedLast ||
alphaInfo == kCGImageAlphaPremultipliedFirst ||
alphaInfo == kCGImageAlphaLast ||
alphaInfo == kCGImageAlphaFirst) {
hasAlpha = YES;
}
// BGRA8888 (premultiplied) or BGRX8888
// same as UIGraphicsBeginImageContext() and -[UIView drawRect:]
CGBitmapInfo bitmapInfo = kCGBitmapByteOrder32Host;
bitmapInfo |= hasAlpha ? kCGImageAlphaPremultipliedFirst : kCGImageAlphaNoneSkipFirst;
CGContextRef context = CGBitmapContextCreate(NULL, width, height, 8, 0, ZZCGColorSpaceGetDeviceRGB(), bitmapInfo);
if (!context) return nil;
CGContextDrawImage(context,CGRectMake(0,0, width, height), imageRef);// decode
CGImageRef newImageRef = CGBitmapContextCreateImage(context);
UIImage *newImage = [UIImage imageWithCGImage:newImageRef];
CFRelease(context);
CGImageRelease(newImageRef);
return newImage;
}