/*
|
* 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 "SDWebImageImageIOCoder.h"
|
#import "SDWebImageCoderHelper.h"
|
#import "NSImage+WebCache.h"
|
#import <ImageIO/ImageIO.h>
|
#import "NSData+ImageContentType.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.0f;
|
|
/*
|
* 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.0f;
|
|
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 SDWebImageImageIOCoder {
|
size_t _width, _height;
|
#if SD_UIKIT || SD_WATCH
|
UIImageOrientation _orientation;
|
#endif
|
CGImageSourceRef _imageSource;
|
}
|
|
- (void)dealloc {
|
if (_imageSource) {
|
CFRelease(_imageSource);
|
_imageSource = NULL;
|
}
|
}
|
|
+ (instancetype)sharedCoder {
|
static SDWebImageImageIOCoder *coder;
|
static dispatch_once_t onceToken;
|
dispatch_once(&onceToken, ^{
|
coder = [[SDWebImageImageIOCoder alloc] init];
|
});
|
return coder;
|
}
|
|
#pragma mark - Decode
|
- (BOOL)canDecodeFromData:(nullable NSData *)data {
|
switch ([NSData sd_imageFormatForImageData:data]) {
|
case SDImageFormatWebP:
|
// Do not support WebP decoding
|
return NO;
|
case SDImageFormatHEIC:
|
// Check HEIC decoding compatibility
|
return [[self class] canDecodeFromHEICFormat];
|
default:
|
return YES;
|
}
|
}
|
|
- (BOOL)canIncrementallyDecodeFromData:(NSData *)data {
|
switch ([NSData sd_imageFormatForImageData:data]) {
|
case SDImageFormatWebP:
|
// Do not support WebP progressive decoding
|
return NO;
|
case SDImageFormatHEIC:
|
// Check HEIC decoding compatibility
|
return [[self class] canDecodeFromHEICFormat];
|
default:
|
return YES;
|
}
|
}
|
|
- (UIImage *)decodedImageWithData:(NSData *)data {
|
if (!data) {
|
return nil;
|
}
|
|
UIImage *image = [[UIImage alloc] initWithData:data];
|
|
return image;
|
}
|
|
- (UIImage *)incrementallyDecodedImageWithData:(NSData *)data finished:(BOOL)finished {
|
if (!_imageSource) {
|
_imageSource = CGImageSourceCreateIncremental(NULL);
|
}
|
UIImage *image;
|
|
// The following code is from http://www.cocoaintheshell.com/2011/05/progressive-images-download-imageio/
|
// Thanks to the author @Nyx0uf
|
|
// Update the data source, we must pass ALL the data, not just the new bytes
|
CGImageSourceUpdateData(_imageSource, (__bridge CFDataRef)data, finished);
|
|
if (_width + _height == 0) {
|
CFDictionaryRef properties = CGImageSourceCopyPropertiesAtIndex(_imageSource, 0, NULL);
|
if (properties) {
|
NSInteger orientationValue = 1;
|
CFTypeRef val = CFDictionaryGetValue(properties, kCGImagePropertyPixelHeight);
|
if (val) CFNumberGetValue(val, kCFNumberLongType, &_height);
|
val = CFDictionaryGetValue(properties, kCGImagePropertyPixelWidth);
|
if (val) CFNumberGetValue(val, kCFNumberLongType, &_width);
|
val = CFDictionaryGetValue(properties, kCGImagePropertyOrientation);
|
if (val) CFNumberGetValue(val, kCFNumberNSIntegerType, &orientationValue);
|
CFRelease(properties);
|
|
// When we draw to Core Graphics, we lose orientation information,
|
// which means the image below born of initWithCGIImage will be
|
// oriented incorrectly sometimes. (Unlike the image born of initWithData
|
// in didCompleteWithError.) So save it here and pass it on later.
|
#if SD_UIKIT || SD_WATCH
|
_orientation = [SDWebImageCoderHelper imageOrientationFromEXIFOrientation:orientationValue];
|
#endif
|
}
|
}
|
|
if (_width + _height > 0) {
|
// Create the image
|
CGImageRef partialImageRef = CGImageSourceCreateImageAtIndex(_imageSource, 0, NULL);
|
|
if (partialImageRef) {
|
#if SD_UIKIT || SD_WATCH
|
image = [[UIImage alloc] initWithCGImage:partialImageRef scale:1 orientation:_orientation];
|
#elif SD_MAC
|
image = [[UIImage alloc] initWithCGImage:partialImageRef size:NSZeroSize];
|
#endif
|
CGImageRelease(partialImageRef);
|
}
|
}
|
|
if (finished) {
|
if (_imageSource) {
|
CFRelease(_imageSource);
|
_imageSource = NULL;
|
}
|
}
|
|
return image;
|
}
|
|
- (UIImage *)decompressedImageWithImage:(UIImage *)image
|
data:(NSData *__autoreleasing _Nullable *)data
|
options:(nullable NSDictionary<NSString*, NSObject*>*)optionsDict {
|
#if SD_MAC
|
return image;
|
#endif
|
#if SD_UIKIT || SD_WATCH
|
BOOL shouldScaleDown = NO;
|
if (optionsDict != nil) {
|
NSNumber *scaleDownLargeImagesOption = nil;
|
if ([optionsDict[SDWebImageCoderScaleDownLargeImagesKey] isKindOfClass:[NSNumber class]]) {
|
scaleDownLargeImagesOption = (NSNumber *)optionsDict[SDWebImageCoderScaleDownLargeImagesKey];
|
}
|
if (scaleDownLargeImagesOption != nil) {
|
shouldScaleDown = [scaleDownLargeImagesOption boolValue];
|
}
|
}
|
if (!shouldScaleDown) {
|
return [self sd_decompressedImageWithImage:image];
|
} else {
|
UIImage *scaledDownImage = [self sd_decompressedAndScaledDownImageWithImage:image];
|
if (scaledDownImage && !CGSizeEqualToSize(scaledDownImage.size, image.size)) {
|
// if the image is scaled down, need to modify the data pointer as well
|
SDImageFormat format = [NSData sd_imageFormatForImageData:*data];
|
NSData *imageData = [self encodedDataWithImage:scaledDownImage format:format];
|
if (imageData) {
|
*data = imageData;
|
}
|
}
|
return scaledDownImage;
|
}
|
#endif
|
}
|
|
#if SD_UIKIT || SD_WATCH
|
- (nullable UIImage *)sd_decompressedImageWithImage:(nullable UIImage *)image {
|
if (![[self class] shouldDecodeImage:image]) {
|
return image;
|
}
|
|
// 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 imageRef = image.CGImage;
|
// device color space
|
CGColorSpaceRef colorspaceRef = SDCGColorSpaceGetDeviceRGB();
|
BOOL hasAlpha = SDCGImageRefContainsAlpha(imageRef);
|
// iOS display alpha info (BRGA8888/BGRX8888)
|
CGBitmapInfo bitmapInfo = kCGBitmapByteOrder32Host;
|
bitmapInfo |= hasAlpha ? kCGImageAlphaPremultipliedFirst : kCGImageAlphaNoneSkipFirst;
|
|
size_t width = CGImageGetWidth(imageRef);
|
size_t height = CGImageGetHeight(imageRef);
|
|
// kCGImageAlphaNone is not supported in CGBitmapContextCreate.
|
// Since the original image here has no alpha info, use kCGImageAlphaNoneSkipLast
|
// to create bitmap graphics contexts without alpha info.
|
CGContextRef context = CGBitmapContextCreate(NULL,
|
width,
|
height,
|
kBitsPerComponent,
|
0,
|
colorspaceRef,
|
bitmapInfo);
|
if (context == NULL) {
|
return image;
|
}
|
|
// Draw the image into the context and retrieve the new bitmap image without alpha
|
CGContextDrawImage(context, CGRectMake(0, 0, width, height), imageRef);
|
CGImageRef imageRefWithoutAlpha = CGBitmapContextCreateImage(context);
|
UIImage *imageWithoutAlpha = [[UIImage alloc] initWithCGImage:imageRefWithoutAlpha scale:image.scale orientation:image.imageOrientation];
|
CGContextRelease(context);
|
CGImageRelease(imageRefWithoutAlpha);
|
|
return imageWithoutAlpha;
|
}
|
}
|
|
- (nullable UIImage *)sd_decompressedAndScaledDownImageWithImage:(nullable UIImage *)image {
|
if (![[self class] shouldDecodeImage:image]) {
|
return image;
|
}
|
|
if (![[self class] shouldScaleDownImage:image]) {
|
return [self sd_decompressedImageWithImage:image];
|
}
|
|
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);
|
float 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.
|
float imageScale = kDestTotalPixels / sourceTotalPixels;
|
CGSize destResolution = CGSizeZero;
|
destResolution.width = (int)(sourceResolution.width*imageScale);
|
destResolution.height = (int)(sourceResolution.height*imageScale);
|
|
// device color space
|
CGColorSpaceRef colorspaceRef = SDCGColorSpaceGetDeviceRGB();
|
BOOL hasAlpha = SDCGImageRefContainsAlpha(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 kCGImageAlphaNoneSkipLast
|
// 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)(kTileTotalPixels / 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;
|
}
|
return destImage;
|
}
|
}
|
#endif
|
|
#pragma mark - Encode
|
- (BOOL)canEncodeToFormat:(SDImageFormat)format {
|
switch (format) {
|
case SDImageFormatWebP:
|
// Do not support WebP encoding
|
return NO;
|
case SDImageFormatHEIC:
|
// Check HEIC encoding compatibility
|
return [[self class] canEncodeToHEICFormat];
|
default:
|
return YES;
|
}
|
}
|
|
- (NSData *)encodedDataWithImage:(UIImage *)image format:(SDImageFormat)format {
|
if (!image) {
|
return nil;
|
}
|
|
if (format == SDImageFormatUndefined) {
|
BOOL hasAlpha = SDCGImageRefContainsAlpha(image.CGImage);
|
if (hasAlpha) {
|
format = SDImageFormatPNG;
|
} else {
|
format = SDImageFormatJPEG;
|
}
|
}
|
|
NSMutableData *imageData = [NSMutableData data];
|
CFStringRef imageUTType = [NSData sd_UTTypeFromSDImageFormat:format];
|
|
// Create an image destination.
|
CGImageDestinationRef imageDestination = CGImageDestinationCreateWithData((__bridge CFMutableDataRef)imageData, imageUTType, 1, NULL);
|
if (!imageDestination) {
|
// Handle failure.
|
return nil;
|
}
|
|
NSMutableDictionary *properties = [NSMutableDictionary dictionary];
|
#if SD_UIKIT || SD_WATCH
|
NSInteger exifOrientation = [SDWebImageCoderHelper exifOrientationFromImageOrientation:image.imageOrientation];
|
[properties setValue:@(exifOrientation) forKey:(__bridge_transfer NSString *)kCGImagePropertyOrientation];
|
#endif
|
|
// Add your image to the destination.
|
CGImageDestinationAddImage(imageDestination, image.CGImage, (__bridge CFDictionaryRef)properties);
|
|
// Finalize the destination.
|
if (CGImageDestinationFinalize(imageDestination) == NO) {
|
// Handle failure.
|
imageData = nil;
|
}
|
|
CFRelease(imageDestination);
|
|
return [imageData copy];
|
}
|
|
#pragma mark - Helper
|
+ (BOOL)shouldDecodeImage:(nullable UIImage *)image {
|
// 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)canDecodeFromHEICFormat {
|
static BOOL canDecode = NO;
|
static dispatch_once_t onceToken;
|
dispatch_once(&onceToken, ^{
|
#pragma clang diagnostic push
|
#pragma clang diagnostic ignored "-Wunguarded-availability"
|
#if TARGET_OS_SIMULATOR || SD_WATCH
|
canDecode = NO;
|
#elif SD_MAC
|
NSProcessInfo *processInfo = [NSProcessInfo processInfo];
|
if ([processInfo respondsToSelector:@selector(operatingSystemVersion)]) {
|
// macOS 10.13+
|
canDecode = processInfo.operatingSystemVersion.minorVersion >= 13;
|
} else {
|
canDecode = NO;
|
}
|
#elif SD_UIKIT
|
NSProcessInfo *processInfo = [NSProcessInfo processInfo];
|
if ([processInfo respondsToSelector:@selector(operatingSystemVersion)]) {
|
// iOS 11+ && tvOS 11+
|
canDecode = processInfo.operatingSystemVersion.majorVersion >= 11;
|
} else {
|
canDecode = NO;
|
}
|
#endif
|
#pragma clang diagnostic pop
|
});
|
return canDecode;
|
}
|
|
+ (BOOL)canEncodeToHEICFormat {
|
static BOOL canEncode = NO;
|
static dispatch_once_t onceToken;
|
dispatch_once(&onceToken, ^{
|
NSMutableData *imageData = [NSMutableData data];
|
CFStringRef imageUTType = [NSData sd_UTTypeFromSDImageFormat:SDImageFormatHEIC];
|
|
// Create an image destination.
|
CGImageDestinationRef imageDestination = CGImageDestinationCreateWithData((__bridge CFMutableDataRef)imageData, imageUTType, 1, NULL);
|
if (!imageDestination) {
|
// Can't encode to HEIC
|
canEncode = NO;
|
} else {
|
// Can encode to HEIC
|
CFRelease(imageDestination);
|
canEncode = YES;
|
}
|
});
|
return canEncode;
|
}
|
|
#if SD_UIKIT || SD_WATCH
|
+ (BOOL)shouldScaleDownImage:(nonnull UIImage *)image {
|
BOOL shouldScaleDown = YES;
|
|
CGImageRef sourceImageRef = image.CGImage;
|
CGSize sourceResolution = CGSizeZero;
|
sourceResolution.width = CGImageGetWidth(sourceImageRef);
|
sourceResolution.height = CGImageGetHeight(sourceImageRef);
|
float sourceTotalPixels = sourceResolution.width * sourceResolution.height;
|
float imageScale = kDestTotalPixels / sourceTotalPixels;
|
if (imageScale < 1) {
|
shouldScaleDown = YES;
|
} else {
|
shouldScaleDown = NO;
|
}
|
|
return shouldScaleDown;
|
}
|
#endif
|
|
@end
|