項(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è)像素是如何被表示的:
我們從圖中可以看出,在 32 位像素格式下,每個(gè)顏色分量使用 8 位;而在 16 位像素格式下,每個(gè)顏色分量則使用 5 位。
這時(shí)候插個(gè)題外話:CMYK和RGB格式都是干嘛的?
顏色和顏色空間
Quartz中的顏色是用一組值來表示。而顏色空間用于解析這些顏色信息。例如,表4-1列出了在全亮度下藍(lán)色值在不同顏色空間下的值。如果不知道顏色空間及顏色空間所能接受的值,我們沒有辦法知道一組值所表示的顏色。
如果我們使用了錯(cuò)誤的顏色空間,我們可能會(huì)獲得完全不同的顏色,如圖4-1所示。
顏色空間可以有不同數(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種特定組合
從上圖可知,對(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;
}