CGBitmapContextCreate: unsupported parameter combination問題調(diào)查及解決

項(xiàng)目中在進(jìn)行圖片裁剪時(shí)候,為了性能和時(shí)間上的優(yōu)化,使用了Core Graphics中的相關(guān)方法。但在使用CGBitmapContextCreate方法時(shí),卻遇到了一些問題。

在某些iOS11系統(tǒng)上,手機(jī)快捷鍵屏幕截圖生成的方法,在經(jīng)過如下代碼


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時(shí),會(huì)報(bào)錯(cuò)

CGBitmapContextCreate: unsupported parameter combination: set CGBITMAP_CONTEXT_LOG_ERRORS environmental variable to see the details

為什么會(huì)出現(xiàn)這種問題呢?在iOS11之前,屏幕截圖的時(shí)候使用上述代碼都沒有問題。即便是iOS11,如果是系統(tǒng)相機(jī)拍照,也不會(huì)出現(xiàn)這種報(bào)錯(cuò)。那么為什么iOS11系統(tǒng)通過快捷鍵截圖屏幕產(chǎn)生的圖片會(huì)出現(xiàn)這種問題呢?帶著疑問,筆者做了如下調(diào)查,結(jié)果如下:

iOS11.0~iOS11.4為止,這個(gè)區(qū)間的iPhone手機(jī),在進(jìn)行快捷鍵屏幕截圖時(shí)候,發(fā)現(xiàn)生成的圖片的bitmap信息為kCGImageAlphaLast | kCGImageByteOrder16Little

CGBitmapInfo

首先看下CGBitmapInfo的結(jié)構(gòu)


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);

它主要提供了三個(gè)方面的布局信息:
* alpha的信息;
* 顏色分量是否為浮點(diǎn)數(shù);
* 像素格式的字節(jié)順序;

alpha信息

圖像的alpha信息,可以通過bitmap按位與kCGBitmapAlphaInfoMask獲取(bitmapInfo & kCGBitmapAlphaInfoMask),也可以直接通過CGImageGetAlphaInfo函數(shù)獲取。

獲取到的是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,那么每個(gè)顏色分量是否已經(jīng)預(yù)乘alpha的值。這種做法可以加速圖片的渲染時(shí)間,因?yàn)樗苊饬虽秩緯r(shí)的額外乘法運(yùn)算。比如,對(duì)于RGB顏色空間,用已經(jīng)乘以alpha的數(shù)據(jù)來渲染圖片,每個(gè)像素都可以避免3次乘法運(yùn)算,紅色乘以alpha、綠色乘以alpha以及藍(lán)色乘以alpha。

其中的kCGImageAlphaNone、kCGImageAlphaNoneSkipLast、kCGImageAlphaNoneSkipFirst表示該圖片中不包含alpha,kCGImageAlphaPremultipliedLast和kCGImageAlphaPremultipliedFirst表示進(jìn)行alpha的預(yù)乘操作,kCGImageAlphaLast、kCGImageAlphaFirst有alpha信息但是不預(yù)乘,kCGImageAlphaOnly代表沒有顏色數(shù)據(jù),只有alpha。

下面來看一張圖,它非常形象地展示了在使用16或32位像素格式的CMYK和RGB顏色空間下,一個(gè)像素是如何被表示的:

image

我們從圖中可以看出,在 32 位像素格式下,每個(gè)顏色分量使用 8 位;而在 16 位像素格式下,每個(gè)顏色分量則使用 5 位。

這時(shí)候插個(gè)題外話:CMYK和RGB格式都是干嘛的?

顏色和顏色空間

Quartz中的顏色是用一組值來表示。而顏色空間用于解析這些顏色信息。例如,表4-1列出了在全亮度下藍(lán)色值在不同顏色空間下的值。如果不知道顏色空間及顏色空間所能接受的值,我們沒有辦法知道一組值所表示的顏色。

image

如果我們使用了錯(cuò)誤的顏色空間,我們可能會(huì)獲得完全不同的顏色,如圖4-1所示。

image

顏色空間可以有不同數(shù)量的組件。表4-1中的顏色空間中其中三個(gè)只有三個(gè)組件,而CMYK有四個(gè)組件。值的范圍與顏色空間有關(guān)。對(duì)大部分顏色空間來說,顏色值范圍為[0.0, 1.0],1.0表示全亮度。例如,全亮度藍(lán)色值在Quartz的RGB顏色空間中的值是(0, 0, 1.0)。在Quartz中,顏色值同樣有一個(gè)alpha值來表示透明度。在表4-1中沒有列出該值。

Pixel Format

位圖其實(shí)就是一個(gè)像素?cái)?shù)組,而像素格式則是用來描述每個(gè)像素的組成格式,它包括以下信息:

  • Bits per component : 一個(gè)像素中每個(gè)獨(dú)立的顏色分量使用的bit數(shù)

  • Bits per pixel : 一個(gè)像素使用的總bit數(shù)

  • Bytes per row : 位圖中每一行使用的字節(jié)數(shù)

有一點(diǎn)需要注意的是,對(duì)于位圖來說,像素格式并不是隨意組合的,目前支持以下有限的17種特定組合

image

從上圖可知,對(duì)于iOS來說,只支持8種像素格式。其中顏色空間為Null的1種,Gray的2種,RGB的5種,CMYK的0種。換句話說,iOS不支持CMYK的顏色空間。

在YYKit開源庫(kù)中,有這樣一段代碼:


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函數(shù)傳入的colorSpace為YYCGColorSpaceGetDeviceRGB(),看下YYCGColorSpaceGetDeviceRGB()函數(shù)的定義:


CGColorSpaceRefYYCGColorSpaceGetDeviceRGB() {
    static CGColorSpaceRef space;
    staticdispatch_once_tonceToken;
    dispatch_once(&onceToken, ^{
        space =CGColorSpaceCreateDeviceRGB();
    });
    returnspace;
}

可見是使用了RGB格式,這里就是因?yàn)閕OS顯示的圖片不支持CMYK等一些格式,所以做了轉(zhuǎn)化,轉(zhuǎn)為RGB格式的圖片。而如果不需要顯示圖片,那么沒必要轉(zhuǎn)化,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提供了兩個(gè)方面的字節(jié)順序信息:

  • 小端模式還是大端模式

  • 數(shù)據(jù)以16位還是32位為單位

對(duì)于iPhone 來說,采用的是小端模式,但是為了保證應(yīng)用的向后兼容性,我們可以使用系統(tǒng)提供的宏,來避免Hardcoding


#ifdef __BIG_ENDIAN__
# define kCGBitmapByteOrder16Host kCGBitmapByteOrder16Big
# define kCGBitmapByteOrder32Host kCGBitmapByteOrder32Big
#else    /* Little endian. */
# define kCGBitmapByteOrder16Host kCGBitmapByteOrder16Little
# define kCGBitmapByteOrder32Host kCGBitmapByteOrder32Little
#endif

那么平時(shí)使用時(shí)候,使用Core Graphics創(chuàng)建bitmap應(yīng)該使用哪種CGImageByteOrderInfo呢?在蘋果官方文檔中關(guān)于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).

翻譯:您可以使用此功能配置繪圖環(huán)境以渲染為位圖。 位圖的格式是使用主機(jī)字節(jié)順序的ARGB 32位整數(shù)像素格式。 如果opaque參數(shù)為true,則忽略alpha通道,并將位圖視為完全不透明(CGImageAlphaInfo.noneSkipFirst | kCGBitmapByteOrder32Host)。 否則,每個(gè)像素使用預(yù)乘的ARGB格式(CGImageAlphaInfo.premultipliedFirst | kCGBitmapByteOrder32Host)。

可見蘋果推薦使用kCGBitmapByteOrder32Host。比較了幾個(gè)開源庫(kù),YYKit使用了推薦的kCGBitmapByteOrder32Host,而SDWebImage和FLAnimatedImage都是使用的kCGBitmapByteOrderDefault。

問題解決

好了,以上就是為了解決上面遇到的問題需要了解的bitmap相關(guān)的知識(shí)。現(xiàn)在我們可以接著看這個(gè)問題。

上面說了,iOS11.0~iOS11.4版本的屏幕截圖,CGBitmapInfo信息為kCGImageAlphaLast | kCGImageByteOrder16Little。
而在iOS11之前,甚至在最新的iOS11.4.1上,屏幕截圖的CGBitmapInfo信息為kCGImageAlphaNoneSkipLast | 0 (default byte order)

而如果是系統(tǒng)相機(jī)(或者其他相機(jī))拍攝出來的照片,不管是iOS11.0~iOS11.4,還是其他所有的iOS系統(tǒng),CGBitmapInfo都是kCGImageAlphaNoneSkipLast | 0 (default byte order)

其中0 (default byte order)就是指的kCGBitmapByteOrderDefault。

是因?yàn)閗CGImageByteOrder16Little引起的創(chuàng)建CGContextRef失敗嗎?

我們將CGBitmapInfo創(chuàng)建時(shí)設(shè)置為kCGImageAlphaLast | kCGBitmapByteOrderDefault

發(fā)現(xiàn)依然失敗,那么是kCGImageAlphaLast的問題嗎?是不是因?yàn)闆]有預(yù)乘,所以失敗呢?帶著疑問,我們將CGBitmapInfo創(chuàng)建時(shí)設(shè)置為kCGImageAlphaPremultipliedLast | kCGImageByteOrder16Little

果然成功了!??!

經(jīng)過多方查證發(fā)現(xiàn),自從iOS8之后,蘋果官方不允許使用不經(jīng)過預(yù)乘的alpha,也就是說kCGImageAlphaLast和kCGImageAlphaFirst都不能使用,而是應(yīng)該使用kCGImageAlphaPremultipliedLast和kCGImageAlphaPremultipliedFirst。

好了,找到了問題的原因,那么應(yīng)該怎么修改呢?可以參考以下開源庫(kù)的做法。

SDWebImage在圖片解碼時(shí)固定使用 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);

這幾個(gè)開源庫(kù)是比較熱門的,用戶數(shù)量龐大,相對(duì)來說兼容性會(huì)好得多,所以比較推薦使用開源庫(kù)的做法。尤其是FLAnimatedImage開源庫(kù)中那句注釋:“If the alpha info doesn't match to one of the supported formats (see above), pick a reasonable supported one.”說得很好,這也是為什么開源庫(kù)能夠避開那些坑。

具體使用哪種方式,可自行斟酌。

PS:關(guān)于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版本上,貌似是蘋果意識(shí)到了這個(gè)問題,將屏幕截圖的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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容