/* * This file is part of the SDWebImage package. * (c) Olivier Poitrey * * 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 *)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 *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 = MAX(frames.firstObject.image.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 *)framesFromAnimatedImage:(UIImage *)animatedImage { if (!animatedImage) { return nil; } NSMutableArray *frames = [NSMutableArray array]; NSUInteger frameCount = 0; #if SD_UIKIT || SD_WATCH NSArray *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; size_t newWidth; size_t newHeight; switch (orientation) { case kCGImagePropertyOrientationLeft: case kCGImagePropertyOrientationLeftMirrored: case kCGImagePropertyOrientationRight: case kCGImagePropertyOrientationRightMirrored: { // These orientation should swap width & height newWidth = height; newHeight = width; } break; default: { newWidth = width; newHeight = height; } break; } 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, newWidth, newHeight, 8, 0, [self colorSpaceGetDeviceRGB], bitmapInfo); if (!context) { return NULL; } // Apply transform CGAffineTransform transform = SDCGContextTransformFromOrientation(orientation, CGSizeMake(newWidth, newHeight)); CGContextConcatCTM(context, transform); CGContextDrawImage(context, CGRectMake(0, 0, width, height), cgImage); // The rect is bounding box of CGImage, don't swap width & height 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