CGBitmapContextCreate: unsupported parameter combination问题调查及解决

项目中在进行图片裁剪时候,为了性能和时间上的优化,使用了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颜色空间下,一个像素是如何被表示的:

image

我们从图中可以看出,在 32 位像素格式下,每个颜色分量使用 8 位;而在 16 位像素格式下,每个颜色分量则使用 5 位。

这时候插个题外话:CMYK和RGB格式都是干嘛的?

颜色和颜色空间

Quartz中的颜色是用一组值来表示。而颜色空间用于解析这些颜色信息。例如,表4-1列出了在全亮度下蓝色值在不同颜色空间下的值。如果不知道颜色空间及颜色空间所能接受的值,我们没有办法知道一组值所表示的颜色。

image

如果我们使用了错误的颜色空间,我们可能会获得完全不同的颜色,如图4-1所示。

image

颜色空间可以有不同数量的组件。表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种特定组合

image

从上图可知,对于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;
}

参考链接:
https://stackoverflow.com/questions/5545600/iphone-cgcontextref-cgbitmapcontextcreate-unsupported-parameter-combination/7868973

http://www.cocoachina.com/ios/20170227/18784.html

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,525评论 6 507
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,203评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,862评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,728评论 1 294
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,743评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,590评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,330评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,244评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,693评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,885评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,001评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,723评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,343评论 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,919评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,042评论 1 270
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,191评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,955评论 2 355

推荐阅读更多精彩内容