TwitterImagePipeline/TIPImageUtils.m (453 lines of code) (raw):
//
// TIPImageUtils.m
// TwitterImagePipeline
//
// Created on 2/18/15.
// Copyright (c) 2015 Twitter, Inc. All rights reserved.
//
#import <CoreGraphics/CoreGraphics.h>
#import <ImageIO/ImageIO.h>
#import <UIKit/UIKit.h>
#import "TIP_Project.h"
#import "TIPError.h"
#import "TIPGlobalConfiguration+Project.h"
#import "TIPImageUtils.h"
#import "TIPTiming.h"
#import "UIImage+TIPAdditions.h"
NS_ASSUME_NONNULL_BEGIN
static CGSize TIPSizeAlignToPixelEx(CGSize size, CGFloat scale);
static CGSize TIPDetectImageDataProviderDimensions(CGDataProviderRef dataProviderRef);
#pragma mark - Render Format
@interface TIPRenderImageFormatInternal : NSObject <TIPRenderImageFormat>
@end
@implementation TIPRenderImageFormatInternal
@synthesize prefersExtendedRange = _prefersExtendedRange;
@synthesize opaque = _opaque;
@synthesize scale = _scale;
@synthesize renderSize = _renderSize;
- (instancetype)initWithRendererFormat:(UIGraphicsImageRendererFormat *)format
{
if (self = [self init]) {
if (tip_available_ios_12) {
_prefersExtendedRange = (format.preferredRange == UIGraphicsImageRendererFormatRangeExtended);
} else {
#if TARGET_OS_MACCATALYST
TIPAssertNever();
#else
_prefersExtendedRange = format.prefersExtendedRange;
#endif
}
_opaque = format.opaque;
_scale = format.scale;
}
return self;
}
@end
#pragma mark - Functions
BOOL TIPSizeMatchesTargetSizing(const CGSize size,
CGSize targetSize,
const UIViewContentMode targetContentMode,
const CGFloat scale)
{
if (!TIPSizeGreaterThanZero(targetSize)) {
return NO;
}
switch (targetContentMode) {
case UIViewContentModeScaleAspectFit:
{
targetSize = TIPScaleToFitKeepingAspectRatio(size, targetSize, scale);
break;
}
case UIViewContentModeScaleAspectFill:
{
targetSize = TIPScaleToFillKeepingAspectRatio(size, targetSize, scale);
break;
}
case UIViewContentModeScaleToFill:
default:
{
break;
}
}
// Keep the target dimensions pixel aligned by rounding up partial pixels
targetSize = TIPSizeAlignToPixelEx(targetSize, scale);
return CGSizeEqualToSize(targetSize, size);
}
CGSize TIPDimensionsScaledToTargetSizing(CGSize dimensionsToScale,
CGSize targetDimensionsOrZero,
UIViewContentMode targetContentMode)
{
return TIPSizeScaledToTargetSizing(dimensionsToScale, targetDimensionsOrZero, targetContentMode, 1);
}
CGSize TIPSizeScaledToTargetSizing(CGSize sizeToScale,
CGSize targetSizeOrZero,
UIViewContentMode targetContentMode,
CGFloat scale)
{
if (!TIPSizeGreaterThanZero(targetSizeOrZero)) {
// no target dimensions, use the source dimensions
targetSizeOrZero = sizeToScale;
} else {
switch (targetContentMode) {
case UIViewContentModeScaleToFill:
// leave target size
break;
case UIViewContentModeScaleAspectFit:
targetSizeOrZero = TIPScaleToFitKeepingAspectRatio(sizeToScale, targetSizeOrZero, scale);
break;
case UIViewContentModeScaleAspectFill:
targetSizeOrZero = TIPScaleToFillKeepingAspectRatio(sizeToScale, targetSizeOrZero, scale);
break;
default:
targetSizeOrZero = sizeToScale;
break;
}
}
return targetSizeOrZero;
}
CGImagePropertyOrientation TIPCGImageOrientationFromUIImageOrientation(UIImageOrientation orientation)
{
switch (orientation) {
case UIImageOrientationUp:
return kCGImagePropertyOrientationUp;
case UIImageOrientationUpMirrored:
return kCGImagePropertyOrientationUpMirrored;
case UIImageOrientationDown:
return kCGImagePropertyOrientationDown;
case UIImageOrientationDownMirrored:
return kCGImagePropertyOrientationDownMirrored;
case UIImageOrientationLeftMirrored:
return kCGImagePropertyOrientationLeftMirrored;
case UIImageOrientationRight:
return kCGImagePropertyOrientationRight;
case UIImageOrientationRightMirrored:
return kCGImagePropertyOrientationRightMirrored;
case UIImageOrientationLeft:
return kCGImagePropertyOrientationLeft;
}
return kCGImagePropertyOrientationUp;
}
UIImageOrientation TIPUIImageOrientationFromCGImageOrientation(CGImagePropertyOrientation cgOrientation)
{
switch (cgOrientation) {
case kCGImagePropertyOrientationUp:
return UIImageOrientationUp;
case kCGImagePropertyOrientationUpMirrored:
return UIImageOrientationUpMirrored;
case kCGImagePropertyOrientationDown:
return UIImageOrientationDown;
case kCGImagePropertyOrientationDownMirrored:
return UIImageOrientationDownMirrored;
case kCGImagePropertyOrientationLeftMirrored:
return UIImageOrientationLeftMirrored;
case kCGImagePropertyOrientationRight:
return UIImageOrientationRight;
case kCGImagePropertyOrientationRightMirrored:
return UIImageOrientationRightMirrored;
case kCGImagePropertyOrientationLeft:
return UIImageOrientationLeft;
}
return UIImageOrientationUp;
}
NSUInteger TIPEstimateMemorySizeOfImageWithSettings(CGSize size,
CGFloat scale,
NSUInteger componentsPerPixel,
NSUInteger frameCount)
{
const NSUInteger pixels = (NSUInteger)(size.width * scale * size.height * scale);
return pixels * componentsPerPixel * MAX((NSUInteger)1, frameCount);
}
static int _TIPImageByteIndexOfAlphaComponent(CGBitmapInfo bitmapInfo,
size_t numberOfComponents,
BOOL isLeadingByteAlpha);
static int _TIPImageByteIndexOfAlphaComponent(CGBitmapInfo bitmapInfo,
size_t numberOfComponents,
BOOL isLeadingByteAlpha)
{
int alphaByteIndex = -1;
const uint32_t byteOrderInfo = bitmapInfo & kCGBitmapByteOrderMask;
switch (byteOrderInfo) {
case kCGBitmapByteOrder16Little:
if (/* DISABLES CODE */ (YES)) {
break; // bail: this code path has not been tested
}
// A R G B -> R A B G
// R G B A -> G R A B
if (isLeadingByteAlpha) {
alphaByteIndex = (numberOfComponents % 2);
} else {
alphaByteIndex = (int)(((numberOfComponents / 2) * 2) + ((2 - 1) - (numberOfComponents % 2)));
}
break;
case kCGBitmapByteOrder32Little:
if (isLeadingByteAlpha) {
alphaByteIndex = (numberOfComponents % 4);
} else {
alphaByteIndex = (int)(((numberOfComponents / 4) * 4) + ((4 - 1) - (numberOfComponents % 4)));
}
break;
case kCGBitmapByteOrder16Big:
case kCGBitmapByteOrder32Big:
default:
alphaByteIndex = isLeadingByteAlpha ? 0 : (int)numberOfComponents;
break;
}
return alphaByteIndex;
}
BOOL TIPCGImageHasAlpha(CGImageRef imageRef, BOOL inspectPixels)
{
BOOL isLeadingByteAlpha = YES;
const CGBitmapInfo bmpInfo = CGImageGetBitmapInfo(imageRef);
const CGImageAlphaInfo alphaInfo = bmpInfo & kCGBitmapAlphaInfoMask;
switch (alphaInfo) {
case kCGImageAlphaNone:
if (CGImageIsMask(imageRef)) {
if (inspectPixels) {
break;
}
return YES; // alpha mask
}
case kCGImageAlphaNoneSkipLast:
case kCGImageAlphaNoneSkipFirst:
return NO;
case kCGImageAlphaPremultipliedLast:
case kCGImageAlphaLast:
isLeadingByteAlpha = NO;
case kCGImageAlphaPremultipliedFirst:
case kCGImageAlphaFirst:
if (inspectPixels) {
break;
}
case kCGImageAlphaOnly:
return YES;
}
if (TIP_BITMASK_HAS_SUBSET_FLAGS(bmpInfo, kCGBitmapFloatComponents)) {
return YES; // bail, only tested with 8-bit components
}
const size_t bitsPerComponent = CGImageGetBitsPerComponent(imageRef);
if (bitsPerComponent != 8) {
return YES; // bail
}
CGColorSpaceRef const colorSpace = CGImageGetColorSpace(imageRef);
const size_t numberOfComponents = colorSpace ? CGColorSpaceGetNumberOfComponents(colorSpace) : 0;
const CGSize size = CGSizeMake(CGImageGetWidth(imageRef), CGImageGetHeight(imageRef));
const size_t expectedBytesPerRow = (numberOfComponents + 1) * (size_t)size.width * (bitsPerComponent / 8);
const size_t byteSluffPerRow = CGImageGetBytesPerRow(imageRef) - expectedBytesPerRow;
if (byteSluffPerRow > CGImageGetBytesPerRow(imageRef)) {
return YES; // bail - underflow
}
const int alphaByteIndex = _TIPImageByteIndexOfAlphaComponent(bmpInfo, numberOfComponents, isLeadingByteAlpha);
if (alphaByteIndex < 0) {
return YES; // bail
}
CGDataProviderRef const dataProvider = CGImageGetDataProvider(imageRef);
CFRetain(dataProvider);
TIPDeferRelease(dataProvider);
CFDataRef const data = CGDataProviderCopyData(dataProvider);
TIPDeferRelease(data);
const UInt8 *byteComponent = CFDataGetBytePtr(data);
for (size_t iRow = 0; iRow < (size_t)size.height; iRow++) {
const UInt8 * const endRowByte = byteComponent + expectedBytesPerRow;
while (byteComponent < endRowByte) {
if (0xFF != byteComponent[alphaByteIndex]) {
return YES;
}
byteComponent += numberOfComponents + 1;
}
byteComponent += byteSluffPerRow;
}
return NO;
}
BOOL TIPCIImageHasAlpha(CIImage *image, BOOL inspectPixels)
{
// TODO: implement this function
return YES;
}
BOOL TIPMainScreenSupportsWideColorGamut()
{
static BOOL sScreenIsWideGamut = NO;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
sScreenIsWideGamut = TIPScreenSupportsWideColorGamut([UIScreen mainScreen]);
});
return sScreenIsWideGamut;
}
BOOL TIPScreenSupportsWideColorGamut(UIScreen *screen)
{
UITraitCollection *traits = [screen traitCollection];
if (![traits respondsToSelector:@selector(displayGamut)]) {
return NO;
}
switch ([traits displayGamut]) {
case UIDisplayGamutP3:
return YES;
default:
break;
}
return NO;
}
void TIPExecuteCGContextBlock(dispatch_block_t __attribute__((noescape)) block)
{
/*
There are not-infrequent crashes when there are multiple accesses to CGContext based functions
at the same time from different threads. We cannot determine exactly what is happening, but
have a few theories:
1. it appears that we can crash within a CGSImageDataLock and suspect another thread has
simultaneous access to the same image data without holding the lock. We also see
cases where the context based image generations are yielding `nil` images, which could
be the same issue but with the fortune of not crashing.
2. we also see an elevated level of FOOMs (Foreground Out Of Memory crashes) which could
indicate we do not have a race condition with code/memory access, but concurrent access
to the CGContext increases memory pressure beyond a device's limits. Additionally,
we do see that the addition of this serialization does not eliminate the issue, just
dramatically curb it - which seems to align with memory constraints that get
exacerbated by have multiple accesses to CGContext that lead to expensive uses of
memory.
To ameliorate these race conditions (either of code/memory access or sheer memory consumed),
we will guard CGContext based operations on a serial queue (when opted into with
`serializeCGContextAccess`).
*/
static dispatch_queue_t sContextQueue;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
sContextQueue = dispatch_queue_create("tip.CGContext.queue", DISPATCH_QUEUE_SERIAL);
});
@autoreleasepool {
TIPGlobalConfiguration *config = [TIPGlobalConfiguration sharedInstance];
const uint64_t startTime = mach_absolute_time();
const BOOL serialize = config.serializeCGContextAccess;
if (serialize && (dispatch_queue_get_label(sContextQueue) != dispatch_queue_get_label(DISPATCH_CURRENT_QUEUE_LABEL))) {
dispatch_sync(sContextQueue, block);
} else {
block();
}
const NSTimeInterval elapsedTime = TIPComputeDuration(startTime, mach_absolute_time());
[config accessedCGContext:serialize duration:elapsedTime isMainThread:[NSThread isMainThread]];
}
}
static UIImage * __nullable _TIPRenderImageLegacy(UIImage * __nullable sourceImage,
TIPImageRenderFormattingBlock __nullable __attribute__((noescape)) formatBlock,
TIPImageRenderBlock __attribute__((noescape)) renderBlock)
{
__block UIImage *outImage = nil;
TIPExecuteCGContextBlock(^{
TIPRenderImageFormatInternal *format = [[TIPRenderImageFormatInternal alloc] init];
if (sourceImage) {
format.renderSize = sourceImage.size;
format.scale = sourceImage.scale;
format.opaque = ![sourceImage tip_hasAlpha:NO];
} else {
format.renderSize = CGSizeMake(1, 1);
format.scale = [UIScreen mainScreen].scale;
format.opaque = NO;
}
format.prefersExtendedRange = NO;
if (formatBlock) {
formatBlock(format);
}
UIGraphicsBeginImageContextWithOptions(format.renderSize, format.opaque, format.scale);
CGContextRef ctx = UIGraphicsGetCurrentContext();
renderBlock(sourceImage, ctx);
outImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
});
return outImage;
}
static UIImage * __nullable _TIPRenderImageModern(UIImage * __nullable sourceImage,
TIPImageRenderFormattingBlock __nullable __attribute__((noescape)) formatBlock,
TIPImageRenderBlock __attribute__((noescape)) renderBlock)
{
if (Nil == [UIGraphicsImageRenderer class]) {
return nil;
}
__block UIImage *outImage = nil;
TIPExecuteCGContextBlock(^{
// Get the renderer format (and size)
CGSize size = CGSizeMake(1, 1);
UIGraphicsImageRendererFormat *format;
if (sourceImage) {
format = sourceImage.imageRendererFormat;
size = sourceImage.size;
} else if (tip_available_ios_11) {
// iOS 11.0.0 GM does have `preferredFormat`, but iOS 11 betas did not (argh!)
if ([UIGraphicsImageRenderer respondsToSelector:@selector(preferredFormat)]) {
format = [UIGraphicsImageRendererFormat preferredFormat];
} else {
format = [UIGraphicsImageRendererFormat defaultFormat];
}
} else {
format = [UIGraphicsImageRendererFormat defaultFormat];
}
// Customize format if desired
if (formatBlock) {
// Prep the format mutable object
TIPRenderImageFormatInternal *formatInternal = [[TIPRenderImageFormatInternal alloc] initWithRendererFormat:format];
formatInternal.renderSize = size;
if (tip_available_ios_12) {
if (sourceImage) {
formatInternal.prefersExtendedRange = sourceImage.tip_usesWideGamutColorSpace;
}
}
// Format the format object
formatBlock(formatInternal);
// Only update the renderer format where there's a difference
if (format.opaque != formatInternal.opaque) {
format.opaque = formatInternal.opaque;
}
if (tip_available_ios_12) {
format.preferredRange = (formatInternal.prefersExtendedRange) ? UIGraphicsImageRendererFormatRangeExtended : UIGraphicsImageRendererFormatRangeStandard;
#if !TARGET_OS_MACCATALYST
if (tip_available_ios_13) {
} else {
format.prefersExtendedRange = formatInternal.prefersExtendedRange;
}
} else {
format.prefersExtendedRange = formatInternal.prefersExtendedRange;
#endif
}
if (format.scale != formatInternal.scale) {
format.scale = (formatInternal.scale == 0.0) ? [UIScreen mainScreen].scale : formatInternal.scale;
}
size = formatInternal.renderSize;
}
// Render!
UIGraphicsImageRenderer *renderer = [[UIGraphicsImageRenderer alloc] initWithSize:size format:format];
outImage = [renderer imageWithActions:^(UIGraphicsImageRendererContext * _Nonnull rendererContext) {
renderBlock(sourceImage, rendererContext.CGContext);
}];
});
return outImage;
}
UIImage * __nullable TIPRenderImage(UIImage * __nullable sourceImage,
TIPImageRenderFormattingBlock __nullable __attribute__((noescape)) formatBlock,
TIPImageRenderBlock __attribute__((noescape)) renderBlock)
{
if (sourceImage.images.count > 1) {
return nil;
}
UIImage *outImage = _TIPRenderImageModern(sourceImage, formatBlock, renderBlock);
if (!outImage) {
outImage = _TIPRenderImageLegacy(sourceImage, formatBlock, renderBlock);
}
return outImage;
}
CGSize TIPDetectImageDataDimensions(NSData * __nullable data)
{
if (data) {
__block CGDataProviderRef dataProvider = NULL;
[data enumerateByteRangesUsingBlock:^(const void * _Nonnull bytes, NSRange byteRange, BOOL * _Nonnull stop) {
*stop = YES;
dataProvider = CGDataProviderCreateWithData(NULL, bytes, byteRange.length, NULL);
}];
TIPDeferRelease(dataProvider);
if (dataProvider) {
return TIPDetectImageDataProviderDimensions(dataProvider);
}
}
return CGSizeZero;
}
CGSize TIPDetectImageFileDimensions(NSString * __nullable filePath)
{
if (filePath) {
NSURL *filePathURL = [NSURL fileURLWithPath:filePath];
CGDataProviderRef dataProvider = CGDataProviderCreateWithURL((CFURLRef)filePathURL);
TIPDeferRelease(dataProvider);
if (dataProvider) {
return TIPDetectImageDataProviderDimensions(dataProvider);
}
}
return CGSizeZero;
}
CGSize TIPDetectImageSourceDimensionsAtIndex(CGImageSourceRef __nullable imageSource, size_t index)
{
if (imageSource) {
CFDictionaryRef properties = CGImageSourceCopyPropertiesAtIndex(imageSource, index, NULL);
TIPDeferRelease(properties);
if (properties) {
CFNumberRef heightNum = CFDictionaryGetValue(properties, kCGImagePropertyPixelHeight);
CFNumberRef widthNum = CFDictionaryGetValue(properties, kCGImagePropertyPixelWidth);
if (heightNum && widthNum) {
return CGSizeMake([(__bridge NSNumber *)widthNum integerValue],
[(__bridge NSNumber *)heightNum integerValue]);
}
}
}
return CGSizeZero;
}
#pragma mark - Statics
static CGSize TIPSizeAlignToPixelEx(CGSize size, CGFloat scale)
{
return CGSizeMake(__tg_ceil(size.width * scale) / scale, __tg_ceil(size.height * scale) / scale);
}
static CGSize TIPDetectImageDataProviderDimensions(CGDataProviderRef dataProviderRef)
{
NSDictionary *options = @{ (NSString *)kCGImageSourceShouldCache : @NO };
CGImageSourceRef imageSourceRef = CGImageSourceCreateIncremental((__bridge CFDictionaryRef)options);
TIPDeferRelease(imageSourceRef);
if (imageSourceRef) {
CGImageSourceUpdateDataProvider(imageSourceRef, dataProviderRef, false);
}
return TIPDetectImageSourceDimensionsAtIndex(imageSourceRef, 0);
}
NS_ASSUME_NONNULL_END