123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536 |
- /*
- * 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"
- #import "UIImage+MultiFormat.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];
- case SDImageFormatHEIF:
- // Check HEIF decoding compatibility
- return [[self class] canDecodeFromHEIFFormat];
- 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];
- case SDImageFormatHEIF:
- // Check HEIF decoding compatibility
- return [[self class] canDecodeFromHEIFFormat];
- default:
- return YES;
- }
- }
- - (UIImage *)decodedImageWithData:(NSData *)data {
- if (!data) {
- return nil;
- }
-
- UIImage *image = [[UIImage alloc] initWithData:data];
- image.sd_imageFormat = [NSData sd_imageFormatForImageData: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);
- image.sd_imageFormat = [NSData sd_imageFormatForImageData:data];
- }
- }
-
- 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);
- 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(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];
- case SDImageFormatHEIF:
- // Check HEIF encoding compatibility
- return [[self class] canEncodeToHEIFFormat];
- 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 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, ^{
- CFStringRef imageUTType = [NSData sd_UTTypeFromSDImageFormat:SDImageFormatHEIC];
- NSArray *imageUTTypes = (__bridge_transfer NSArray *)CGImageSourceCopyTypeIdentifiers();
- if ([imageUTTypes containsObject:(__bridge NSString *)(imageUTType)]) {
- canDecode = YES;
- }
- });
- return canDecode;
- }
- + (BOOL)canDecodeFromHEIFFormat {
- static BOOL canDecode = NO;
- static dispatch_once_t onceToken;
- dispatch_once(&onceToken, ^{
- CFStringRef imageUTType = [NSData sd_UTTypeFromSDImageFormat:SDImageFormatHEIF];
- NSArray *imageUTTypes = (__bridge_transfer NSArray *)CGImageSourceCopyTypeIdentifiers();
- if ([imageUTTypes containsObject:(__bridge NSString *)(imageUTType)]) {
- canDecode = YES;
- }
- });
- 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;
- }
- + (BOOL)canEncodeToHEIFFormat {
- static BOOL canEncode = NO;
- static dispatch_once_t onceToken;
- dispatch_once(&onceToken, ^{
- NSMutableData *imageData = [NSMutableData data];
- CFStringRef imageUTType = [NSData sd_UTTypeFromSDImageFormat:SDImageFormatHEIF];
-
- // Create an image destination.
- CGImageDestinationRef imageDestination = CGImageDestinationCreateWithData((__bridge CFMutableDataRef)imageData, imageUTType, 1, NULL);
- if (!imageDestination) {
- // Can't encode to HEIF
- canEncode = NO;
- } else {
- // Can encode to HEIF
- 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
|