Bitmap Info引起的CGImage渲染失败

670 阅读2分钟

问题描述

在使用CALayer渲染CGImage时,发现图片并没有被正确渲染。但使用UIImageView却能正确渲染出图片。

观察差异

正常渲染的图片

<<CGColorSpace 0x282837f60> (kCGColorSpaceICCBased; kCGColorSpaceModelRGB; Display P3)>

width = 1284, height = 1284, bpc = 8, bpp = 32, row bytes = 5136 

kCGImageAlphaNoneSkipLast | 0 (default byte order)  | kCGImagePixelFormatPacked 

is mask? No, has masking color? No, has soft mask? No, has matte? No, should interpolate? Yes

无法渲染的图片

<<CGColorSpace 0x282837f60> (kCGColorSpaceICCBased; kCGColorSpaceModelRGB; Display P3)>

width = 1284, height = 2778, bpc = 16, bpp = 64, row bytes = 10272 

kCGImageAlphaLast | kCGImageByteOrder16Little  | kCGImagePixelFormatPacked 

is mask? No, has masking color? No, has soft mask? No, has matte? No, should interpolate? Yes) 		

从属性来比较,两张图的差异主要集中在Pixel FormatBitmap Layout

无法正常渲染的图片正常渲染的图片
Pixel Formatbpc = 16, bpp = 64bpc = 8, bpp = 32
Bitmap LayoutkCGImageAlphaLast、kCGImageByteOrder16LittlekCGImageAlphaNoneSkipLast、0 (default byte order)

无法渲染的图为kCGBitmapByteOrder16Little模式,16位小端模式

查询文档

Pixel Format

位图其实就是一个像素数组,而像素格式则是用来描述每个像素的组成格式,它包括以下信息:

Bits per component (bpc): 一个像素中每个独立的颜色分量使用的bit数
Bits per pixel (bpp): 一个像素使用的总bit数
Bytes per row : 位图中每一行使用的字节数

CGBitmapInfo

typedef CF_OPTIONS(uint32_t, CGBitmapInfo) {
    kCGBitmapAlphaInfoMask = 0x1F,

    kCGBitmapFloatInfoMask = 0xF00, 
    kCGBitmapFloatComponents = (1 << 8), // 浮点型表示

    kCGBitmapByteOrderMask     = kCGImageByteOrderMask,
    kCGBitmapByteOrderDefault  = kCGImageByteOrderDefault,  // 默认
    kCGBitmapByteOrder16Little = kCGImageByteOrder16Little, // 16 位小端
    kCGBitmapByteOrder32Little = kCGImageByteOrder32Little, // 32 位小端
    kCGBitmapByteOrder16Big    = kCGImageByteOrder16Big, // 16 位大端
    kCGBitmapByteOrder32Big    = kCGImageByteOrder32Big // 32 位大端
} CG_AVAILABLE_STARTING(10.0, 2.0);

位图布局信息,是为了让Quartz正确的解释每个像素的信息。 其中主要包含了三点内容:

  • Alpha通道的信息。
  • 像素格式的字节顺序。
  • 颜色分量的数据格式 - 整数或浮点值。

CGImageAlphaInfo(Alpha信息)

typedef CF_ENUM(uint32_t, CGImageAlphaInfo) {
    kCGImageAlphaNone,               /* 等价于 kCGImageAlphaNoneSkipLast; 例如: RGB. */
    kCGImageAlphaPremultipliedLast,  /* alpha 存储在低位, 且 alpha 已于每个颜色分量进行相乘; 例如: RGBA */
    kCGImageAlphaPremultipliedFirst, /* alpha 存储在高位, 且 alpha 已于每个颜色分量进行相乘; 例如: ARGB */
    kCGImageAlphaLast,               /* alpha 存储在低位; 例如 RGBA */
    kCGImageAlphaFirst,              /* alpha 存储在高位; 例如 ARGB */
    kCGImageAlphaNoneSkipLast,       /* 如果色值位数大于所需空间,则低位忽略; 例如 RBGX. */
    kCGImageAlphaNoneSkipFirst,      /* 如果色值位数大于所需空间,则高位忽略; 例如, XRGB. */
    kCGImageAlphaOnly                /* No color data, alpha data only */
};

alpha信息包括:

  1. 是否包含 alpha 。
  2. 如果包含 alpha,alpha 信息所处的位置。在像素的最低有效位,如RGBA ,还是最高有效位如ARGB。
  3. 如果包含 alpha,每个颜色分量是否已经乘以 alpha 的值,这种做法可以加速图片的渲染时间,因为它避免了渲染时的额外乘法运算。比如,对于 RGB 颜色空间,用已经乘以 alpha 的数据来渲染图片,每个像素都可以避免 3 次乘法运算,红色乘以 alpha ,绿色乘以 alpha 和蓝色乘以 alpha 。

分析问题

可以看出问题是由于位图信息的存储方式未被支持。我们重新设置位图信息的存储方式就可以解决这个问题

下图可以看出不同平台对存储方式的支持。

img

自从iOS8之后,苹果官方不允许使用不经过预乘的alpha,也就是说kCGImageAlphaLast和kCGImageAlphaFirst都不能使用,而是应该使用kCGImageAlphaPremultipliedLast和kCGImageAlphaPremultipliedFirst。

如SDWebImage,在图片解码时固定使用 kCGBitmapByteOrderDefault | kCGImageAlphaNoneSkipLast,其他有些地方使用了kCGBitmapByteOrderDefault | kCGImageAlphaPremultipliedFirst

/// 重新生成Image的BitFormat layout
/// - Parameters:
///   - size: 当size 为 zero时,使用图片的大小
///   - scale: 默认1倍,可以搭配UIScreen.main.scale使用
func format(size: CGSize = .zero, scale: CGFloat = 1) -> UIImage? {
        // Create a vImage_Buffer from the CGImage
        guard let sourceRef = img.cgImage else { return nil }

        var srcBuffer = vImage_Buffer()
        var format = vImage_CGImageFormat(bitsPerComponent: 8,
                                          bitsPerPixel: 32,
                                          colorSpace: nil,
                                          bitmapInfo: CGBitmapInfo(rawValue: CGImageAlphaInfo.noneSkipLast.rawValue),
                                          version: 0,
                                          decode: nil,
                                          renderingIntent: .defaultIntent)
        var ret = vImageBuffer_InitWithCGImage(&srcBuffer, &format, nil, sourceRef, vImage_Flags(kvImageNoFlags))

        guard ret == kvImageNoError else {
            free(srcBuffer.data)
            return nil
        }

        var fitSize = size
        if fitSize == .zero {
            fitSize = img.size
        }
        // Create dest buffer
        let dstWidth = Int(fitSize.width * scale)
        let dstHeight = Int(fitSize.height * scale)
        let bytesPerPixel: Int = 4
        let dstBytesPerRow: Int = bytesPerPixel * dstWidth
        let dstData = UnsafeMutablePointer<UInt8>.allocate(capacity: dstHeight * dstWidth * bytesPerPixel)

        var dstBuffer = vImage_Buffer(data: dstData,
                                      height: vImagePixelCount(dstHeight),
                                      width: vImagePixelCount(dstWidth),
                                      rowBytes: dstBytesPerRow)

        // Scale
        ret = vImageScale_ARGB8888(&srcBuffer, &dstBuffer, nil, vImage_Flags(kvImageHighQualityResampling))
        free(srcBuffer.data)

        guard ret == kvImageNoError else { return nil }

        // Create CGImage from vImage_Buffer
        ret = kvImageNoError
        guard let destRef = vImageCreateCGImageFromBuffer(&dstBuffer, &format, nil, nil, vImage_Flags(kvImageNoAllocate), &ret)?.takeRetainedValue() else {
            return nil
        }

        guard ret == kvImageNoError else { return nil }

        // Create UIImage
        let destImage = UIImage(cgImage: destRef, scale: 0.0, orientation: img.imageOrientation)
        return destImage
    }

参考资料

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