SDWebImage/SDWebImage/SDImageCoderHelper.m

629 lines
24 KiB
Objective-C

/*
* This file is part of the SDWebImage package.
* (c) Olivier Poitrey <rs@dailymotion.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
#import "SDImageCoderHelper.h"
#import "SDImageFrame.h"
#import "NSImage+Compatibility.h"
#import "NSData+ImageContentType.h"
#import "SDAnimatedImageRep.h"
#import "UIImage+ForceDecode.h"
#import "UIImage+Metadata.h"
#if SD_UIKIT || SD_WATCH
static const size_t kBytesPerPixel = 4;
static const size_t kBitsPerComponent = 8;
/*
* Defines the maximum size in MB of the decoded image when the flag `SDWebImageScaleDownLargeImages` is set
* Suggested value for iPad1 and iPhone 3GS: 60.
* Suggested value for iPad2 and iPhone 4: 120.
* Suggested value for iPhone 3G and iPod 2 and earlier devices: 30.
*/
static const CGFloat kDestImageSizeMB = 60.f;
/*
* Defines the maximum size in MB of a tile used to decode image when the flag `SDWebImageScaleDownLargeImages` is set
* Suggested value for iPad1 and iPhone 3GS: 20.
* Suggested value for iPad2 and iPhone 4: 40.
* Suggested value for iPhone 3G and iPod 2 and earlier devices: 10.
*/
static const CGFloat kSourceImageTileSizeMB = 20.f;
static const CGFloat kBytesPerMB = 1024.0f * 1024.0f;
static const CGFloat kPixelsPerMB = kBytesPerMB / kBytesPerPixel;
static const CGFloat kDestTotalPixels = kDestImageSizeMB * kPixelsPerMB;
static const CGFloat kTileTotalPixels = kSourceImageTileSizeMB * kPixelsPerMB;
static const CGFloat kDestSeemOverlap = 2.0f; // the numbers of pixels to overlap the seems where tiles meet.
#endif
@implementation SDImageCoderHelper
+ (UIImage *)animatedImageWithFrames:(NSArray<SDImageFrame *> *)frames {
NSUInteger frameCount = frames.count;
if (frameCount == 0) {
return nil;
}
UIImage *animatedImage;
#if SD_UIKIT || SD_WATCH
NSUInteger durations[frameCount];
for (size_t i = 0; i < frameCount; i++) {
durations[i] = frames[i].duration * 1000;
}
NSUInteger const gcd = gcdArray(frameCount, durations);
__block NSUInteger totalDuration = 0;
NSMutableArray<UIImage *> *animatedImages = [NSMutableArray arrayWithCapacity:frameCount];
[frames enumerateObjectsUsingBlock:^(SDImageFrame * _Nonnull frame, NSUInteger idx, BOOL * _Nonnull stop) {
UIImage *image = frame.image;
NSUInteger duration = frame.duration * 1000;
totalDuration += duration;
NSUInteger repeatCount;
if (gcd) {
repeatCount = duration / gcd;
} else {
repeatCount = 1;
}
for (size_t i = 0; i < repeatCount; ++i) {
[animatedImages addObject:image];
}
}];
animatedImage = [UIImage animatedImageWithImages:animatedImages duration:totalDuration / 1000.f];
#else
NSMutableData *imageData = [NSMutableData data];
CFStringRef imageUTType = [NSData sd_UTTypeFromImageFormat:SDImageFormatGIF];
// Create an image destination. GIF does not support EXIF image orientation
CGImageDestinationRef imageDestination = CGImageDestinationCreateWithData((__bridge CFMutableDataRef)imageData, imageUTType, frameCount, NULL);
if (!imageDestination) {
// Handle failure.
return nil;
}
for (size_t i = 0; i < frameCount; i++) {
@autoreleasepool {
SDImageFrame *frame = frames[i];
float frameDuration = frame.duration;
CGImageRef frameImageRef = frame.image.CGImage;
NSDictionary *frameProperties = @{(__bridge NSString *)kCGImagePropertyGIFDictionary : @{(__bridge NSString *)kCGImagePropertyGIFDelayTime : @(frameDuration)}};
CGImageDestinationAddImage(imageDestination, frameImageRef, (__bridge CFDictionaryRef)frameProperties);
}
}
// Finalize the destination.
if (CGImageDestinationFinalize(imageDestination) == NO) {
// Handle failure.
CFRelease(imageDestination);
return nil;
}
CFRelease(imageDestination);
CGFloat scale = frames.firstObject.image.scale;
if (scale < 1) {
scale = 1;
}
SDAnimatedImageRep *imageRep = [[SDAnimatedImageRep alloc] initWithData:imageData];
NSSize size = NSMakeSize(imageRep.pixelsWide / scale, imageRep.pixelsHigh / scale);
imageRep.size = size;
animatedImage = [[NSImage alloc] initWithSize:size];
[animatedImage addRepresentation:imageRep];
#endif
return animatedImage;
}
+ (NSArray<SDImageFrame *> *)framesFromAnimatedImage:(UIImage *)animatedImage {
if (!animatedImage) {
return nil;
}
NSMutableArray<SDImageFrame *> *frames = [NSMutableArray array];
NSUInteger frameCount = 0;
#if SD_UIKIT || SD_WATCH
NSArray<UIImage *> *animatedImages = animatedImage.images;
frameCount = animatedImages.count;
if (frameCount == 0) {
return nil;
}
NSTimeInterval avgDuration = animatedImage.duration / frameCount;
if (avgDuration == 0) {
avgDuration = 0.1; // if it's a animated image but no duration, set it to default 100ms (this do not have that 10ms limit like GIF or WebP to allow custom coder provide the limit)
}
__block NSUInteger index = 0;
__block NSUInteger repeatCount = 1;
__block UIImage *previousImage = animatedImages.firstObject;
[animatedImages enumerateObjectsUsingBlock:^(UIImage * _Nonnull image, NSUInteger idx, BOOL * _Nonnull stop) {
// ignore first
if (idx == 0) {
return;
}
if ([image isEqual:previousImage]) {
repeatCount++;
} else {
SDImageFrame *frame = [SDImageFrame frameWithImage:previousImage duration:avgDuration * repeatCount];
[frames addObject:frame];
repeatCount = 1;
index++;
}
previousImage = image;
// last one
if (idx == frameCount - 1) {
SDImageFrame *frame = [SDImageFrame frameWithImage:previousImage duration:avgDuration * repeatCount];
[frames addObject:frame];
}
}];
#else
NSRect imageRect = NSMakeRect(0, 0, animatedImage.size.width, animatedImage.size.height);
NSImageRep *imageRep = [animatedImage bestRepresentationForRect:imageRect context:nil hints:nil];
NSBitmapImageRep *bitmapImageRep;
if ([imageRep isKindOfClass:[NSBitmapImageRep class]]) {
bitmapImageRep = (NSBitmapImageRep *)imageRep;
}
if (!bitmapImageRep) {
return nil;
}
frameCount = [[bitmapImageRep valueForProperty:NSImageFrameCount] unsignedIntegerValue];
if (frameCount == 0) {
return nil;
}
CGFloat scale = animatedImage.scale;
for (size_t i = 0; i < frameCount; i++) {
@autoreleasepool {
// NSBitmapImageRep need to manually change frame. "Good taste" API
[bitmapImageRep setProperty:NSImageCurrentFrame withValue:@(i)];
float frameDuration = [[bitmapImageRep valueForProperty:NSImageCurrentFrameDuration] floatValue];
NSImage *frameImage = [[NSImage alloc] initWithCGImage:bitmapImageRep.CGImage scale:scale orientation:kCGImagePropertyOrientationUp];
SDImageFrame *frame = [SDImageFrame frameWithImage:frameImage duration:frameDuration];
[frames addObject:frame];
}
}
#endif
return frames;
}
+ (CGColorSpaceRef)colorSpaceGetDeviceRGB {
#if SD_MAC
CGColorSpaceRef screenColorSpace = NSScreen.mainScreen.colorSpace.CGColorSpace;
if (screenColorSpace) {
return screenColorSpace;
}
#endif
static CGColorSpaceRef colorSpace;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
#if SD_UIKIT
if (@available(iOS 9.0, tvOS 9.0, *)) {
colorSpace = CGColorSpaceCreateWithName(kCGColorSpaceSRGB);
} else {
colorSpace = CGColorSpaceCreateDeviceRGB();
}
#else
colorSpace = CGColorSpaceCreateDeviceRGB();
#endif
});
return colorSpace;
}
+ (BOOL)CGImageContainsAlpha:(CGImageRef)cgImage {
if (!cgImage) {
return NO;
}
CGImageAlphaInfo alphaInfo = CGImageGetAlphaInfo(cgImage);
BOOL hasAlpha = !(alphaInfo == kCGImageAlphaNone ||
alphaInfo == kCGImageAlphaNoneSkipFirst ||
alphaInfo == kCGImageAlphaNoneSkipLast);
return hasAlpha;
}
+ (CGImageRef)CGImageCreateDecoded:(CGImageRef)cgImage {
return [self CGImageCreateDecoded:cgImage orientation:kCGImagePropertyOrientationUp];
}
+ (CGImageRef)CGImageCreateDecoded:(CGImageRef)cgImage orientation:(CGImagePropertyOrientation)orientation {
if (!cgImage) {
return NULL;
}
size_t width = CGImageGetWidth(cgImage);
size_t height = CGImageGetHeight(cgImage);
if (width == 0 || height == 0) return NULL;
BOOL hasAlpha = [self CGImageContainsAlpha:cgImage];
// iOS prefer BGRA8888 (premultiplied) or BGRX8888 bitmapInfo for screen rendering, which is same as `UIGraphicsBeginImageContext()` or `- [CALayer drawInContext:]`
// Though you can use any supported bitmapInfo (see: https://developer.apple.com/library/content/documentation/GraphicsImaging/Conceptual/drawingwithquartz2d/dq_context/dq_context.html#//apple_ref/doc/uid/TP30001066-CH203-BCIBHHBB ) and let Core Graphics reorder it when you call `CGContextDrawImage`
// But since our build-in coders use this bitmapInfo, this can have a little performance benefit
CGBitmapInfo bitmapInfo = kCGBitmapByteOrder32Host;
bitmapInfo |= hasAlpha ? kCGImageAlphaPremultipliedFirst : kCGImageAlphaNoneSkipFirst;
CGContextRef context = CGBitmapContextCreate(NULL, width, height, 8, 0, [self colorSpaceGetDeviceRGB], bitmapInfo);
if (!context) {
return NULL;
}
// Apply transform
CGAffineTransform transform = SDCGContextTransformFromOrientation(orientation, CGSizeMake(width, height));
CGRect rect;
switch (orientation) {
case kCGImagePropertyOrientationLeft:
case kCGImagePropertyOrientationLeftMirrored:
case kCGImagePropertyOrientationRight:
case kCGImagePropertyOrientationRightMirrored: {
// These orientation should swap width & height
rect = CGRectMake(0, 0, height, width);
}
break;
default: {
rect = CGRectMake(0, 0, width, height);
}
break;
}
CGContextConcatCTM(context, transform);
CGContextDrawImage(context, rect, cgImage);
CGImageRef newImageRef = CGBitmapContextCreateImage(context);
CGContextRelease(context);
return newImageRef;
}
+ (UIImage *)decodedImageWithImage:(UIImage *)image {
#if SD_MAC
return image;
#else
if (![self shouldDecodeImage:image]) {
return image;
}
CGImageRef imageRef = [self CGImageCreateDecoded:image.CGImage];
if (!imageRef) {
return image;
}
UIImage *decodedImage = [[UIImage alloc] initWithCGImage:imageRef scale:image.scale orientation:image.imageOrientation];
CGImageRelease(imageRef);
decodedImage.sd_isDecoded = YES;
decodedImage.sd_imageFormat = image.sd_imageFormat;
return decodedImage;
#endif
}
+ (UIImage *)decodedAndScaledDownImageWithImage:(UIImage *)image limitBytes:(NSUInteger)bytes {
#if SD_MAC
return image;
#else
if (![self shouldDecodeImage:image]) {
return image;
}
if (![self shouldScaleDownImage:image limitBytes:bytes]) {
return [self decodedImageWithImage:image];
}
CGFloat destTotalPixels;
CGFloat tileTotalPixels;
if (bytes > 0) {
destTotalPixels = bytes / kBytesPerPixel;
tileTotalPixels = destTotalPixels / 3;
} else {
destTotalPixels = kDestTotalPixels;
tileTotalPixels = kTileTotalPixels;
}
CGContextRef destContext;
// autorelease the bitmap context and all vars to help system to free memory when there are memory warning.
// on iOS7, do not forget to call [[SDImageCache sharedImageCache] clearMemory];
@autoreleasepool {
CGImageRef sourceImageRef = image.CGImage;
CGSize sourceResolution = CGSizeZero;
sourceResolution.width = CGImageGetWidth(sourceImageRef);
sourceResolution.height = CGImageGetHeight(sourceImageRef);
CGFloat sourceTotalPixels = sourceResolution.width * sourceResolution.height;
// Determine the scale ratio to apply to the input image
// that results in an output image of the defined size.
// see kDestImageSizeMB, and how it relates to destTotalPixels.
CGFloat imageScale = sqrt(destTotalPixels / sourceTotalPixels);
CGSize destResolution = CGSizeZero;
destResolution.width = (int)(sourceResolution.width * imageScale);
destResolution.height = (int)(sourceResolution.height * imageScale);
// device color space
CGColorSpaceRef colorspaceRef = [self colorSpaceGetDeviceRGB];
BOOL hasAlpha = [self CGImageContainsAlpha:sourceImageRef];
// iOS display alpha info (BGRA8888/BGRX8888)
CGBitmapInfo bitmapInfo = kCGBitmapByteOrder32Host;
bitmapInfo |= hasAlpha ? kCGImageAlphaPremultipliedFirst : kCGImageAlphaNoneSkipFirst;
// kCGImageAlphaNone is not supported in CGBitmapContextCreate.
// Since the original image here has no alpha info, use kCGImageAlphaNoneSkipFirst
// to create bitmap graphics contexts without alpha info.
destContext = CGBitmapContextCreate(NULL,
destResolution.width,
destResolution.height,
kBitsPerComponent,
0,
colorspaceRef,
bitmapInfo);
if (destContext == NULL) {
return image;
}
CGContextSetInterpolationQuality(destContext, kCGInterpolationHigh);
// Now define the size of the rectangle to be used for the
// incremental blits from the input image to the output image.
// we use a source tile width equal to the width of the source
// image due to the way that iOS retrieves image data from disk.
// iOS must decode an image from disk in full width 'bands', even
// if current graphics context is clipped to a subrect within that
// band. Therefore we fully utilize all of the pixel data that results
// from a decoding opertion by achnoring our tile size to the full
// width of the input image.
CGRect sourceTile = CGRectZero;
sourceTile.size.width = sourceResolution.width;
// The source tile height is dynamic. Since we specified the size
// of the source tile in MB, see how many rows of pixels high it
// can be given the input image width.
sourceTile.size.height = (int)(tileTotalPixels / sourceTile.size.width );
sourceTile.origin.x = 0.0f;
// The output tile is the same proportions as the input tile, but
// scaled to image scale.
CGRect destTile;
destTile.size.width = destResolution.width;
destTile.size.height = sourceTile.size.height * imageScale;
destTile.origin.x = 0.0f;
// The source seem overlap is proportionate to the destination seem overlap.
// this is the amount of pixels to overlap each tile as we assemble the ouput image.
float sourceSeemOverlap = (int)((kDestSeemOverlap/destResolution.height)*sourceResolution.height);
CGImageRef sourceTileImageRef;
// calculate the number of read/write operations required to assemble the
// output image.
int iterations = (int)( sourceResolution.height / sourceTile.size.height );
// If tile height doesn't divide the image height evenly, add another iteration
// to account for the remaining pixels.
int remainder = (int)sourceResolution.height % (int)sourceTile.size.height;
if(remainder) {
iterations++;
}
// Add seem overlaps to the tiles, but save the original tile height for y coordinate calculations.
float sourceTileHeightMinusOverlap = sourceTile.size.height;
sourceTile.size.height += sourceSeemOverlap;
destTile.size.height += kDestSeemOverlap;
for( int y = 0; y < iterations; ++y ) {
@autoreleasepool {
sourceTile.origin.y = y * sourceTileHeightMinusOverlap + sourceSeemOverlap;
destTile.origin.y = destResolution.height - (( y + 1 ) * sourceTileHeightMinusOverlap * imageScale + kDestSeemOverlap);
sourceTileImageRef = CGImageCreateWithImageInRect( sourceImageRef, sourceTile );
if( y == iterations - 1 && remainder ) {
float dify = destTile.size.height;
destTile.size.height = CGImageGetHeight( sourceTileImageRef ) * imageScale;
dify -= destTile.size.height;
destTile.origin.y += dify;
}
CGContextDrawImage( destContext, destTile, sourceTileImageRef );
CGImageRelease( sourceTileImageRef );
}
}
CGImageRef destImageRef = CGBitmapContextCreateImage(destContext);
CGContextRelease(destContext);
if (destImageRef == NULL) {
return image;
}
UIImage *destImage = [[UIImage alloc] initWithCGImage:destImageRef scale:image.scale orientation:image.imageOrientation];
CGImageRelease(destImageRef);
if (destImage == nil) {
return image;
}
destImage.sd_isDecoded = YES;
destImage.sd_imageFormat = image.sd_imageFormat;
return destImage;
}
#endif
}
#if SD_UIKIT || SD_WATCH
// Convert an EXIF image orientation to an iOS one.
+ (UIImageOrientation)imageOrientationFromEXIFOrientation:(CGImagePropertyOrientation)exifOrientation {
UIImageOrientation imageOrientation = UIImageOrientationUp;
switch (exifOrientation) {
case kCGImagePropertyOrientationUp:
imageOrientation = UIImageOrientationUp;
break;
case kCGImagePropertyOrientationDown:
imageOrientation = UIImageOrientationDown;
break;
case kCGImagePropertyOrientationLeft:
imageOrientation = UIImageOrientationLeft;
break;
case kCGImagePropertyOrientationRight:
imageOrientation = UIImageOrientationRight;
break;
case kCGImagePropertyOrientationUpMirrored:
imageOrientation = UIImageOrientationUpMirrored;
break;
case kCGImagePropertyOrientationDownMirrored:
imageOrientation = UIImageOrientationDownMirrored;
break;
case kCGImagePropertyOrientationLeftMirrored:
imageOrientation = UIImageOrientationLeftMirrored;
break;
case kCGImagePropertyOrientationRightMirrored:
imageOrientation = UIImageOrientationRightMirrored;
break;
default:
break;
}
return imageOrientation;
}
// Convert an iOS orientation to an EXIF image orientation.
+ (CGImagePropertyOrientation)exifOrientationFromImageOrientation:(UIImageOrientation)imageOrientation {
CGImagePropertyOrientation exifOrientation = kCGImagePropertyOrientationUp;
switch (imageOrientation) {
case UIImageOrientationUp:
exifOrientation = kCGImagePropertyOrientationUp;
break;
case UIImageOrientationDown:
exifOrientation = kCGImagePropertyOrientationDown;
break;
case UIImageOrientationLeft:
exifOrientation = kCGImagePropertyOrientationLeft;
break;
case UIImageOrientationRight:
exifOrientation = kCGImagePropertyOrientationRight;
break;
case UIImageOrientationUpMirrored:
exifOrientation = kCGImagePropertyOrientationUpMirrored;
break;
case UIImageOrientationDownMirrored:
exifOrientation = kCGImagePropertyOrientationDownMirrored;
break;
case UIImageOrientationLeftMirrored:
exifOrientation = kCGImagePropertyOrientationLeftMirrored;
break;
case UIImageOrientationRightMirrored:
exifOrientation = kCGImagePropertyOrientationRightMirrored;
break;
default:
break;
}
return exifOrientation;
}
#endif
#pragma mark - Helper Fuction
#if SD_UIKIT || SD_WATCH
+ (BOOL)shouldDecodeImage:(nullable UIImage *)image {
// Avoid extra decode
if (image.sd_isDecoded) {
return NO;
}
// Prevent "CGBitmapContextCreateImage: invalid context 0x0" error
if (image == nil) {
return NO;
}
// do not decode animated images
if (image.images != nil) {
return NO;
}
return YES;
}
+ (BOOL)shouldScaleDownImage:(nonnull UIImage *)image limitBytes:(NSUInteger)bytes {
BOOL shouldScaleDown = YES;
CGImageRef sourceImageRef = image.CGImage;
CGSize sourceResolution = CGSizeZero;
sourceResolution.width = CGImageGetWidth(sourceImageRef);
sourceResolution.height = CGImageGetHeight(sourceImageRef);
float sourceTotalPixels = sourceResolution.width * sourceResolution.height;
if (sourceTotalPixels <= 0) {
return NO;
}
CGFloat destTotalPixels;
if (bytes > 0) {
destTotalPixels = bytes / kBytesPerPixel;
} else {
destTotalPixels = kDestTotalPixels;
}
if (destTotalPixels <= kPixelsPerMB) {
// Too small to scale down
return NO;
}
float imageScale = destTotalPixels / sourceTotalPixels;
if (imageScale < 1) {
shouldScaleDown = YES;
} else {
shouldScaleDown = NO;
}
return shouldScaleDown;
}
#endif
static inline CGAffineTransform SDCGContextTransformFromOrientation(CGImagePropertyOrientation orientation, CGSize size) {
// Inspiration from @libfeihu
// We need to calculate the proper transformation to make the image upright.
// We do it in 2 steps: Rotate if Left/Right/Down, and then flip if Mirrored.
CGAffineTransform transform = CGAffineTransformIdentity;
switch (orientation) {
case kCGImagePropertyOrientationDown:
case kCGImagePropertyOrientationDownMirrored:
transform = CGAffineTransformTranslate(transform, size.width, size.height);
transform = CGAffineTransformRotate(transform, M_PI);
break;
case kCGImagePropertyOrientationLeft:
case kCGImagePropertyOrientationLeftMirrored:
transform = CGAffineTransformTranslate(transform, size.width, 0);
transform = CGAffineTransformRotate(transform, M_PI_2);
break;
case kCGImagePropertyOrientationRight:
case kCGImagePropertyOrientationRightMirrored:
transform = CGAffineTransformTranslate(transform, 0, size.height);
transform = CGAffineTransformRotate(transform, -M_PI_2);
break;
case kCGImagePropertyOrientationUp:
case kCGImagePropertyOrientationUpMirrored:
break;
}
switch (orientation) {
case kCGImagePropertyOrientationUpMirrored:
case kCGImagePropertyOrientationDownMirrored:
transform = CGAffineTransformTranslate(transform, size.width, 0);
transform = CGAffineTransformScale(transform, -1, 1);
break;
case kCGImagePropertyOrientationLeftMirrored:
case kCGImagePropertyOrientationRightMirrored:
transform = CGAffineTransformTranslate(transform, size.height, 0);
transform = CGAffineTransformScale(transform, -1, 1);
break;
case kCGImagePropertyOrientationUp:
case kCGImagePropertyOrientationDown:
case kCGImagePropertyOrientationLeft:
case kCGImagePropertyOrientationRight:
break;
}
return transform;
}
#if SD_UIKIT || SD_WATCH
static NSUInteger gcd(NSUInteger a, NSUInteger b) {
NSUInteger c;
while (a != 0) {
c = a;
a = b % a;
b = c;
}
return b;
}
static NSUInteger gcdArray(size_t const count, NSUInteger const * const values) {
if (count == 0) {
return 0;
}
NSUInteger result = values[0];
for (size_t i = 1; i < count; ++i) {
result = gcd(values[i], result);
}
return result;
}
#endif
@end