diff --git a/SDWebImage/Core/SDImageCoderHelper.h b/SDWebImage/Core/SDImageCoderHelper.h index e1074c7d..d0b51152 100644 --- a/SDWebImage/Core/SDImageCoderHelper.h +++ b/SDWebImage/Core/SDImageCoderHelper.h @@ -131,6 +131,8 @@ typedef struct SDImagePixelFormat { Create a scaled CGImage by the provided CGImage and size. This follows The Create Rule and you are response to call release after usage. It will detect whether the image size matching the scale size, if not, stretch the image to the target size. @note If you need to keep aspect ratio, you can calculate the scale size by using `scaledSizeWithImageSize` first. + @note This scale does not change bits per components (which means RGB888 in, RGB888 out), supports 8/16/32(float) bpc. But the method in UIImage+Transform does not gurantee this. + @note All supported CGImage pixel format: https://developer.apple.com/library/archive/documentation/GraphicsImaging/Conceptual/drawingwithquartz2d/dq_context/dq_context.html#//apple_ref/doc/uid/TP30001066-CH203-BCIBHHBB @param cgImage The CGImage @param size The scale size in pixel. diff --git a/SDWebImage/Core/SDImageCoderHelper.m b/SDWebImage/Core/SDImageCoderHelper.m index 0d5fe45d..a2b81587 100644 --- a/SDWebImage/Core/SDImageCoderHelper.m +++ b/SDWebImage/Core/SDImageCoderHelper.m @@ -434,45 +434,110 @@ static const CGFloat kDestSeemOverlap = 2.0f; // the numbers of pixels to over if (!cgImage) { return NULL; } + if (size.width == 0 || size.height == 0) { + return NULL; + } size_t width = CGImageGetWidth(cgImage); size_t height = CGImageGetHeight(cgImage); if (width == size.width && height == size.height) { + // Already same size CGImageRetain(cgImage); return cgImage; } + size_t bitsPerComponent = CGImageGetBitsPerComponent(cgImage); + if (bitsPerComponent != 8 && bitsPerComponent != 16 && bitsPerComponent != 32) { + // Unsupported + return NULL; + } + size_t bitsPerPixel = CGImageGetBitsPerPixel(cgImage); + CGColorSpaceRef colorSpace = CGImageGetColorSpace(cgImage); + CGColorRenderingIntent renderingIntent = CGImageGetRenderingIntent(cgImage); + CGBitmapInfo bitmapInfo = CGImageGetBitmapInfo(cgImage); + CGImageAlphaInfo alphaInfo = bitmapInfo & kCGBitmapAlphaInfoMask; + CGImageByteOrderInfo byteOrderInfo = bitmapInfo & kCGBitmapByteOrderMask; + CGBitmapInfo alphaBitmapInfo = (uint32_t)byteOrderInfo; + // Input need to convert with alpha + if (alphaInfo == kCGImageAlphaNone) { + // Convert RGB8/16/F -> ARGB8/16/F + alphaBitmapInfo |= kCGImageAlphaFirst; + } else { + alphaBitmapInfo |= alphaInfo; + } + uint32_t components; + if (alphaInfo == kCGImageAlphaOnly) { + // Alpha only, simple to 1 channel + components = 1; + } else { + components = 4; + } + if (SD_OPTIONS_CONTAINS(bitmapInfo, kCGBitmapFloatComponents)) { + // Keep float components + alphaBitmapInfo |= kCGBitmapFloatComponents; + } __block vImage_Buffer input_buffer = {}, output_buffer = {}; @onExit { if (input_buffer.data) free(input_buffer.data); if (output_buffer.data) free(output_buffer.data); }; - BOOL hasAlpha = [self CGImageContainsAlpha:cgImage]; - // kCGImageAlphaNone is not supported in CGBitmapContextCreate. - // Check #3330 for more detail about why this bitmap is choosen. - // From v5.17.0, use runtime detection of bitmap info instead of hardcode. - CGBitmapInfo bitmapInfo = [SDImageCoderHelper preferredPixelFormat:hasAlpha].bitmapInfo; + // Always provide alpha channel vImage_CGImageFormat format = (vImage_CGImageFormat) { - .bitsPerComponent = 8, - .bitsPerPixel = 32, - .colorSpace = NULL, + .bitsPerComponent = (uint32_t)bitsPerComponent, + .bitsPerPixel = (uint32_t)bitsPerComponent * components, + .colorSpace = colorSpace, + .bitmapInfo = alphaBitmapInfo, + .version = 0, + .decode = NULL, + .renderingIntent = renderingIntent + }; + // input + vImage_Error ret = vImageBuffer_InitWithCGImage(&input_buffer, &format, NULL, cgImage, kvImageNoFlags); + if (ret != kvImageNoError) return NULL; + // output + vImageBuffer_Init(&output_buffer, size.height, size.width, (uint32_t)bitsPerComponent * components, kvImageNoFlags); + if (!output_buffer.data) return NULL; + + if (components == 4) { + if (bitsPerComponent == 32) { + ret = vImageScale_ARGBFFFF(&input_buffer, &output_buffer, NULL, kvImageHighQualityResampling); + } else if (bitsPerComponent == 16) { + ret = vImageScale_ARGB16U(&input_buffer, &output_buffer, NULL, kvImageHighQualityResampling); + } else if (bitsPerComponent == 8) { + ret = vImageScale_ARGB8888(&input_buffer, &output_buffer, NULL, kvImageHighQualityResampling); + } + } else { + if (bitsPerComponent == 32) { + ret = vImageScale_PlanarF(&input_buffer, &output_buffer, NULL, kvImageHighQualityResampling); + } else if (bitsPerComponent == 16) { + ret = vImageScale_Planar16U(&input_buffer, &output_buffer, NULL, kvImageHighQualityResampling); + } else if (bitsPerComponent == 8) { + ret = vImageScale_Planar8(&input_buffer, &output_buffer, NULL, kvImageHighQualityResampling); + } + } + if (ret != kvImageNoError) return NULL; + + // Convert back to non-alpha for RGB input to preserve pixel format + if (alphaInfo == kCGImageAlphaNone) { + // in-place, no extra allocation + if (bitsPerComponent == 32) { + ret = vImageConvert_ARGBFFFFtoRGBFFF(&output_buffer, &output_buffer, kvImageNoFlags); + } else if (bitsPerComponent == 16) { + ret = vImageConvert_ARGB16UtoRGB16U(&output_buffer, &output_buffer, kvImageNoFlags); + } else if (bitsPerComponent == 8) { + ret = vImageConvert_ARGB8888toRGB888(&output_buffer, &output_buffer, kvImageNoFlags); + } + if (ret != kvImageNoError) return NULL; + } + vImage_CGImageFormat output_format = (vImage_CGImageFormat) { + .bitsPerComponent = (uint32_t)bitsPerComponent, + .bitsPerPixel = (uint32_t)bitsPerPixel, + .colorSpace = colorSpace, .bitmapInfo = bitmapInfo, .version = 0, .decode = NULL, - .renderingIntent = CGImageGetRenderingIntent(cgImage) + .renderingIntent = renderingIntent }; - - vImage_Error a_ret = vImageBuffer_InitWithCGImage(&input_buffer, &format, NULL, cgImage, kvImageNoFlags); - if (a_ret != kvImageNoError) return NULL; - output_buffer.width = MAX(size.width, 0); - output_buffer.height = MAX(size.height, 0); - output_buffer.rowBytes = SDByteAlign(output_buffer.width * 4, 64); - output_buffer.data = malloc(output_buffer.rowBytes * output_buffer.height); - if (!output_buffer.data) return NULL; - - vImage_Error ret = vImageScale_ARGB8888(&input_buffer, &output_buffer, NULL, kvImageHighQualityResampling); - if (ret != kvImageNoError) return NULL; - - CGImageRef outputImage = vImageCreateCGImageFromBuffer(&output_buffer, &format, NULL, NULL, kvImageNoFlags, &ret); + CGImageRef outputImage = vImageCreateCGImageFromBuffer(&output_buffer, &output_format, NULL, NULL, kvImageNoFlags, &ret); if (ret != kvImageNoError) { CGImageRelease(outputImage); return NULL; diff --git a/Tests/Tests/SDImageTransformerTests.m b/Tests/Tests/SDImageTransformerTests.m index 07c730cf..7fca61af 100644 --- a/Tests/Tests/SDImageTransformerTests.m +++ b/Tests/Tests/SDImageTransformerTests.m @@ -11,6 +11,75 @@ #import "UIColor+SDHexString.h" #import +static void SDAssertCGImagePixelFormatEqual(CGImageRef image1, CGImageRef image2) { + CGBitmapInfo bitmapInfo1 = CGImageGetBitmapInfo(image1); + CGBitmapInfo bitmapInfo2 = CGImageGetBitmapInfo(image2); + XCTAssertEqual(bitmapInfo1, bitmapInfo2); + // alphaInfo && byteOrderInfo && pixelFomat are just calculation of bitmapInfo + XCTAssertEqual(CGImageGetColorSpace(image1), CGImageGetColorSpace(image2)); + XCTAssertEqual(CGImageGetBitsPerPixel(image1), CGImageGetBitsPerPixel(image2)); + XCTAssertEqual(CGImageGetBitsPerComponent(image1), CGImageGetBitsPerComponent(image2)); + XCTAssertEqual(CGImageGetRenderingIntent(image1), CGImageGetRenderingIntent(image2)); + XCTAssertEqual(CGImageGetShouldInterpolate(image1), CGImageGetShouldInterpolate(image2)); +} + +// TODO: Current sd_colorAtPoint: support 8-bits only, 16bits and float color will fail... +// So I write this `SDAssertCGImageFirstComponentWhite` :( +static void SDAssertCGImageFirstComponentWhite(CGImageRef image, OSType pixelType) { + CGDataProviderRef provider = CGImageGetDataProvider(image); + CFDataRef data = CGDataProviderCopyData(provider); + if (pixelType == kCVPixelFormatType_128RGBAFloat) { + float *buffer = (float *)CFDataGetBytePtr(data); + float r = buffer[0]; + float g = buffer[1]; + float b = buffer[2]; + float a = buffer[3]; + XCTAssertEqual(r, 1.0); + XCTAssertEqual(g, 1.0); + XCTAssertEqual(b, 1.0); + XCTAssertEqual(a, 1.0); + } else if (pixelType == kCVPixelFormatType_64RGBALE) { + uint16_t *buffer = (uint16_t *)CFDataGetBytePtr(data); + uint16_t r = buffer[0]; + uint16_t g = buffer[1]; + uint16_t b = buffer[2]; + uint16_t a = buffer[3]; + XCTAssertEqual(r, UINT16_MAX); + XCTAssertEqual(g, UINT16_MAX); + XCTAssertEqual(b, UINT16_MAX); + XCTAssertEqual(a, UINT16_MAX); + } else if (pixelType == kCVPixelFormatType_32ARGB) { + uint8_t *buffer = (uint8_t *)CFDataGetBytePtr(data); + uint8_t a = buffer[0]; + uint8_t r = buffer[1]; + uint8_t g = buffer[2]; + uint8_t b = buffer[3]; + XCTAssertEqual(a, UINT8_MAX); + XCTAssertEqual(r, UINT8_MAX); + XCTAssertEqual(g, UINT8_MAX); + XCTAssertEqual(b, UINT8_MAX); + } else if (pixelType == kCVPixelFormatType_24RGB) { + uint8_t *buffer = (uint8_t *)CFDataGetBytePtr(data); + uint8_t r = buffer[0]; + uint8_t g = buffer[1]; + uint8_t b = buffer[2]; + XCTAssertEqual(r, UINT8_MAX); + XCTAssertEqual(g, UINT8_MAX); + XCTAssertEqual(b, UINT8_MAX); + } else if (pixelType == kCVPixelFormatType_48RGB) { + uint16_t *buffer = (uint16_t *)CFDataGetBytePtr(data); + uint16_t r = buffer[0]; + uint16_t g = buffer[1]; + uint16_t b = buffer[2]; + XCTAssertEqual(r, UINT16_MAX); + XCTAssertEqual(g, UINT16_MAX); + XCTAssertEqual(b, UINT16_MAX); + } else { + XCTFail(@"Should not hit here"); + } + CFRelease(data); +} + @interface SDImageTransformerTests : SDTestCase @property (nonatomic, strong) UIImage *testImageCG; @@ -417,6 +486,169 @@ expect([[testColor sd_hexString] isEqualToString:UIColor.blackColor.sd_hexString]).beFalsy(); } +- (void)test22CGImageCreateScaledWithSize { + size_t width = 100; + size_t height = 100; + size_t scaledWidth = 50; + size_t scaledHeight = 50; + // RGB888 + CGImageRef RGB888Image = ^(){ + size_t bitsPerComponent = 8; + size_t components = 3; + size_t bitsPerPixel = bitsPerComponent * components; + size_t bytesPerRow = bitsPerPixel / 8 * width; + size_t size = bytesPerRow * height; + size_t count = width * height * components; + uint8_t bitmap[count]; + for (size_t i = 0; i < count; i++) { + bitmap[i] = UINT8_MAX; + } + CGColorSpaceRef colorspace = [SDImageCoderHelper colorSpaceGetDeviceRGB]; + CGBitmapInfo bitmapInfo = kCGImageAlphaNone | kCGBitmapByteOrderDefault; + CFDataRef data = CFDataCreate(NULL, (UInt8 *)bitmap, size); + CGDataProviderRef provider = CGDataProviderCreateWithCFData(data); + CFRelease(data); + BOOL shouldInterpolate = YES; + CGColorRenderingIntent intent = kCGRenderingIntentDefault; + CGImageRef cgImage = CGImageCreate(width, height, bitsPerComponent, bitsPerPixel, bytesPerRow, colorspace, bitmapInfo, provider, NULL, shouldInterpolate, intent); + CGDataProviderRelease(provider); + return cgImage; + }(); + CGImageRef RGB888Scaled = [SDImageCoderHelper CGImageCreateScaled:RGB888Image size:CGSizeMake(scaledWidth, scaledHeight)]; + XCTAssertEqual(CGImageGetWidth(RGB888Scaled), scaledWidth); + XCTAssertEqual(CGImageGetHeight(RGB888Scaled), scaledHeight); + SDAssertCGImagePixelFormatEqual(RGB888Scaled, RGB888Image); + SDAssertCGImageFirstComponentWhite(RGB888Scaled, kCVPixelFormatType_24RGB); + + // RGB16161616 + CGImageRef RGB161616Image = ^(){ + size_t bitsPerComponent = 16; + size_t components = 3; + size_t bitsPerPixel = bitsPerComponent * components; + size_t bytesPerRow = bitsPerPixel / 8 * width; + size_t size = bytesPerRow * height; + size_t count = width * height * components; + uint16_t bitmap[count]; + for (size_t i = 0; i < count; i++) { + bitmap[i] = UINT16_MAX; + } + CGColorSpaceRef colorspace = [SDImageCoderHelper colorSpaceGetDeviceRGB]; + CGBitmapInfo bitmapInfo = kCGImageAlphaNone | kCGBitmapByteOrder16Host; + CFDataRef data = CFDataCreate(NULL, (UInt8 *)bitmap, size); + CGDataProviderRef provider = CGDataProviderCreateWithCFData(data); + CFRelease(data); + BOOL shouldInterpolate = YES; + CGColorRenderingIntent intent = kCGRenderingIntentDefault; + CGImageRef cgImage = CGImageCreate(width, height, bitsPerComponent, bitsPerPixel, bytesPerRow, colorspace, bitmapInfo, provider, NULL, shouldInterpolate, intent); + CGDataProviderRelease(provider); + return cgImage; + }(); + CGImageRef RGB161616Scaled = [SDImageCoderHelper CGImageCreateScaled:RGB161616Image size:CGSizeMake(scaledWidth, scaledHeight)]; + XCTAssertEqual(CGImageGetWidth(RGB161616Scaled), scaledWidth); + XCTAssertEqual(CGImageGetHeight(RGB161616Scaled), scaledHeight); + SDAssertCGImagePixelFormatEqual(RGB161616Scaled, RGB161616Image); + SDAssertCGImageFirstComponentWhite(RGB161616Scaled, kCVPixelFormatType_48RGB); + + // ARGB8888 + CGImageRef ARGB8888Image = ^(){ + size_t bitsPerComponent = 8; + size_t components = 4; + size_t bitsPerPixel = bitsPerComponent * components; + size_t bytesPerRow = bitsPerPixel / 8 * width; + size_t size = bytesPerRow * height; + size_t count = width * height * components; + uint8_t bitmap[count]; + for (size_t i = 0; i < count; i++) { + bitmap[i] = UINT8_MAX; + } + CGColorSpaceRef colorspace = [SDImageCoderHelper colorSpaceGetDeviceRGB]; + CGBitmapInfo bitmapInfo = kCGImageAlphaPremultipliedLast | kCGBitmapByteOrderDefault; + CFDataRef data = CFDataCreate(NULL, (UInt8 *)bitmap, size); + CGDataProviderRef provider = CGDataProviderCreateWithCFData(data); + CFRelease(data); + BOOL shouldInterpolate = YES; + CGColorRenderingIntent intent = kCGRenderingIntentDefault; + CGImageRef cgImage = CGImageCreate(width, height, bitsPerComponent, bitsPerPixel, bytesPerRow, colorspace, bitmapInfo, provider, NULL, shouldInterpolate, intent); + CGDataProviderRelease(provider); + return cgImage; + }(); + CGImageRef ARGB8888Scaled = [SDImageCoderHelper CGImageCreateScaled:ARGB8888Image size:CGSizeMake(scaledWidth, scaledHeight)]; + XCTAssertEqual(CGImageGetWidth(ARGB8888Scaled), scaledWidth); + XCTAssertEqual(CGImageGetHeight(ARGB8888Scaled), scaledHeight); + SDAssertCGImagePixelFormatEqual(ARGB8888Scaled, ARGB8888Image); + SDAssertCGImageFirstComponentWhite(ARGB8888Scaled, kCVPixelFormatType_32ARGB); + + // RGBA16161616 + CGImageRef RGBA16161616Image = ^(){ + size_t bitsPerComponent = 16; + size_t components = 4; + size_t bitsPerPixel = bitsPerComponent * components; + size_t bytesPerRow = bitsPerPixel / 8 * width; + size_t size = bytesPerRow * height; + size_t count = width * height * components; + uint16_t bitmap[count]; + for (size_t i = 0; i < count; i++) { + bitmap[i] = UINT16_MAX; + } + CGColorSpaceRef colorspace = [SDImageCoderHelper colorSpaceGetDeviceRGB]; + CGBitmapInfo bitmapInfo = kCGImageAlphaPremultipliedLast | kCGBitmapByteOrder16Host; + CFDataRef data = CFDataCreate(NULL, (UInt8 *)bitmap, size); + CGDataProviderRef provider = CGDataProviderCreateWithCFData(data); + CFRelease(data); + BOOL shouldInterpolate = YES; + CGColorRenderingIntent intent = kCGRenderingIntentDefault; + CGImageRef cgImage = CGImageCreate(width, height, bitsPerComponent, bitsPerPixel, bytesPerRow, colorspace, bitmapInfo, provider, NULL, shouldInterpolate, intent); + CGDataProviderRelease(provider); + return cgImage; + }(); + CGImageRef RGBA16161616Scaled = [SDImageCoderHelper CGImageCreateScaled:RGBA16161616Image size:CGSizeMake(scaledWidth, scaledHeight)]; + XCTAssertEqual(CGImageGetWidth(RGBA16161616Scaled), scaledWidth); + XCTAssertEqual(CGImageGetHeight(RGBA16161616Scaled), scaledHeight); + SDAssertCGImagePixelFormatEqual(RGBA16161616Scaled, RGBA16161616Image); + SDAssertCGImageFirstComponentWhite(RGBA16161616Scaled, kCVPixelFormatType_64RGBALE); + + // RGBAFFFF + CGImageRef RGBAFFFFImage = ^(){ + size_t bitsPerComponent = 32; + size_t components = 4; + size_t bitsPerPixel = bitsPerComponent * components; + size_t bytesPerRow = bitsPerPixel / 8 * width; + size_t size = bytesPerRow * height; + size_t count = width * height * components; + float bitmap[count]; + for (size_t i = 0; i < count; i++) { + bitmap[i] = 1.0; + } + CGColorSpaceRef colorspace = [SDImageCoderHelper colorSpaceGetDeviceRGB]; + CGBitmapInfo bitmapInfo = kCGImageAlphaPremultipliedLast | kCGBitmapByteOrder32Host | kCGBitmapFloatComponents; + CFDataRef data = CFDataCreate(NULL, (UInt8 *)bitmap, size); + CGDataProviderRef provider = CGDataProviderCreateWithCFData(data); + CFRelease(data); + BOOL shouldInterpolate = YES; + CGColorRenderingIntent intent = kCGRenderingIntentDefault; + CGImageRef cgImage = CGImageCreate(width, height, bitsPerComponent, bitsPerPixel, bytesPerRow, colorspace, bitmapInfo, provider, NULL, shouldInterpolate, intent); + CGDataProviderRelease(provider); + return cgImage; + }(); + CGImageRef RGBAFFFFScaled = [SDImageCoderHelper CGImageCreateScaled:RGBAFFFFImage size:CGSizeMake(scaledWidth, scaledHeight)]; + XCTAssertEqual(CGImageGetWidth(RGBAFFFFScaled), scaledWidth); + XCTAssertEqual(CGImageGetHeight(RGBAFFFFScaled), scaledHeight); + SDAssertCGImagePixelFormatEqual(RGBAFFFFScaled, RGBAFFFFImage); + SDAssertCGImageFirstComponentWhite(RGBAFFFFScaled, kCVPixelFormatType_128RGBAFloat); + + // Cleanup and check by human eyes using preview, all should be white image + CGImageRelease(RGB888Image); + CGImageRelease(RGB888Scaled); + CGImageRelease(RGB161616Image); + CGImageRelease(RGB161616Scaled); + CGImageRelease(ARGB8888Image); + CGImageRelease(ARGB8888Scaled); + CGImageRelease(RGBA16161616Image); + CGImageRelease(RGBA16161616Scaled); + CGImageRelease(RGBAFFFFImage); + CGImageRelease(RGBAFFFFScaled); +} + #pragma mark - Helper - (UIImage *)testImageCG {