Merge pull request #3575 from dreampiggy/bugfix/CGImageCreateScale

Fix the CGImageCreateScaled to support 16/32 bit depth CGImage (RGB161616) and always preserve pixel format info
This commit is contained in:
DreamPiggy 2023-07-29 21:59:26 +08:00 committed by GitHub
commit 7e78633845
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 321 additions and 22 deletions

View File

@ -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. 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. 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 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 cgImage The CGImage
@param size The scale size in pixel. @param size The scale size in pixel.

View File

@ -434,45 +434,110 @@ static const CGFloat kDestSeemOverlap = 2.0f; // the numbers of pixels to over
if (!cgImage) { if (!cgImage) {
return NULL; return NULL;
} }
if (size.width == 0 || size.height == 0) {
return NULL;
}
size_t width = CGImageGetWidth(cgImage); size_t width = CGImageGetWidth(cgImage);
size_t height = CGImageGetHeight(cgImage); size_t height = CGImageGetHeight(cgImage);
if (width == size.width && height == size.height) { if (width == size.width && height == size.height) {
// Already same size
CGImageRetain(cgImage); CGImageRetain(cgImage);
return 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 = {}; __block vImage_Buffer input_buffer = {}, output_buffer = {};
@onExit { @onExit {
if (input_buffer.data) free(input_buffer.data); if (input_buffer.data) free(input_buffer.data);
if (output_buffer.data) free(output_buffer.data); if (output_buffer.data) free(output_buffer.data);
}; };
BOOL hasAlpha = [self CGImageContainsAlpha:cgImage]; // Always provide alpha channel
// 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;
vImage_CGImageFormat format = (vImage_CGImageFormat) { vImage_CGImageFormat format = (vImage_CGImageFormat) {
.bitsPerComponent = 8, .bitsPerComponent = (uint32_t)bitsPerComponent,
.bitsPerPixel = 32, .bitsPerPixel = (uint32_t)bitsPerComponent * components,
.colorSpace = NULL, .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, .bitmapInfo = bitmapInfo,
.version = 0, .version = 0,
.decode = NULL, .decode = NULL,
.renderingIntent = CGImageGetRenderingIntent(cgImage) .renderingIntent = renderingIntent
}; };
CGImageRef outputImage = vImageCreateCGImageFromBuffer(&output_buffer, &output_format, NULL, NULL, kvImageNoFlags, &ret);
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);
if (ret != kvImageNoError) { if (ret != kvImageNoError) {
CGImageRelease(outputImage); CGImageRelease(outputImage);
return NULL; return NULL;

View File

@ -11,6 +11,75 @@
#import "UIColor+SDHexString.h" #import "UIColor+SDHexString.h"
#import <CoreImage/CoreImage.h> #import <CoreImage/CoreImage.h>
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 @interface SDImageTransformerTests : SDTestCase
@property (nonatomic, strong) UIImage *testImageCG; @property (nonatomic, strong) UIImage *testImageCG;
@ -417,6 +486,169 @@
expect([[testColor sd_hexString] isEqualToString:UIColor.blackColor.sd_hexString]).beFalsy(); 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 #pragma mark - Helper
- (UIImage *)testImageCG { - (UIImage *)testImageCG {