diff --git a/Examples/SDWebImage Demo/MasterViewController.m b/Examples/SDWebImage Demo/MasterViewController.m index 3a6c1780..f131f55f 100644 --- a/Examples/SDWebImage Demo/MasterViewController.m +++ b/Examples/SDWebImage Demo/MasterViewController.m @@ -115,7 +115,8 @@ cell.customTextLabel.text = [NSString stringWithFormat:@"Image #%ld", (long)indexPath.row]; [cell.customImageView sd_setImageWithURL:[NSURL URLWithString:self.objects[indexPath.row]] placeholderImage:placeholderImage - options:indexPath.row == 0 ? SDWebImageRefreshCached : 0]; + options:indexPath.row == 0 ? SDWebImageRefreshCached : 0 + context:@{SDWebImageContextImageThumbnailPixelSize : @(CGSizeMake(180, 120))}]; return cell; } diff --git a/SDWebImage/Core/SDImageCacheDefine.m b/SDWebImage/Core/SDImageCacheDefine.m index 99e57f1a..75dfb4e6 100644 --- a/SDWebImage/Core/SDImageCacheDefine.m +++ b/SDWebImage/Core/SDImageCacheDefine.m @@ -18,12 +18,25 @@ UIImage * _Nullable SDImageCacheDecodeImageData(NSData * _Nonnull imageData, NSS BOOL decodeFirstFrame = SD_OPTIONS_CONTAINS(options, SDWebImageDecodeFirstFrameOnly); NSNumber *scaleValue = context[SDWebImageContextImageScaleFactor]; CGFloat scale = scaleValue.doubleValue >= 1 ? scaleValue.doubleValue : SDImageScaleFactorForKey(cacheKey); - SDImageCoderOptions *coderOptions = @{SDImageCoderDecodeFirstFrameOnly : @(decodeFirstFrame), SDImageCoderDecodeScaleFactor : @(scale)}; - if (context) { - SDImageCoderMutableOptions *mutableCoderOptions = [coderOptions mutableCopy]; - [mutableCoderOptions setValue:context forKey:SDImageCoderWebImageContext]; - coderOptions = [mutableCoderOptions copy]; + NSNumber *preserveAspectRatioValue = context[SDWebImageContextImagePreserveAspectRatio]; + NSValue *thumbnailSizeValue; + BOOL shouldScaleDown = SD_OPTIONS_CONTAINS(options, SDWebImageScaleDownLargeImages); + if (shouldScaleDown) { + CGFloat thumbnailPixels = SDImageCoderHelper.defaultScaleDownLimitBytes / 4; + CGFloat dimension = ceil(sqrt(thumbnailPixels)); + thumbnailSizeValue = @(CGSizeMake(dimension, dimension)); } + if (context[SDWebImageContextImageThumbnailPixelSize]) { + thumbnailSizeValue = context[SDWebImageContextImageThumbnailPixelSize]; + } + + SDImageCoderMutableOptions *mutableCoderOptions = [NSMutableDictionary dictionaryWithCapacity:2]; + mutableCoderOptions[SDImageCoderDecodeFirstFrameOnly] = @(decodeFirstFrame); + mutableCoderOptions[SDImageCoderDecodeScaleFactor] = @(scale); + mutableCoderOptions[SDImageCoderDecodePreserveAspectRatio] = preserveAspectRatioValue; + mutableCoderOptions[SDImageCoderDecodeThumbnailPixelSize] = thumbnailSizeValue; + mutableCoderOptions[SDImageCoderWebImageContext] = context; + SDImageCoderOptions *coderOptions = [mutableCoderOptions copy]; if (!decodeFirstFrame) { Class animatedImageClass = context[SDWebImageContextAnimatedImageClass]; @@ -56,12 +69,7 @@ UIImage * _Nullable SDImageCacheDecodeImageData(NSData * _Nonnull imageData, NSS shouldDecode = NO; } if (shouldDecode) { - BOOL shouldScaleDown = SD_OPTIONS_CONTAINS(options, SDWebImageScaleDownLargeImages); - if (shouldScaleDown) { - image = [SDImageCoderHelper decodedAndScaledDownImageWithImage:image limitBytes:0]; - } else { - image = [SDImageCoderHelper decodedImageWithImage:image]; - } + image = [SDImageCoderHelper decodedImageWithImage:image]; } } diff --git a/SDWebImage/Core/SDImageCoder.h b/SDWebImage/Core/SDImageCoder.h index 3b2049e5..221246ac 100644 --- a/SDWebImage/Core/SDImageCoder.h +++ b/SDWebImage/Core/SDImageCoder.h @@ -27,6 +27,22 @@ FOUNDATION_EXPORT SDImageCoderOption _Nonnull const SDImageCoderDecodeFirstFrame */ FOUNDATION_EXPORT SDImageCoderOption _Nonnull const SDImageCoderDecodeScaleFactor; +/** + A Boolean value indicating whether to keep the original aspect ratio when generating thumbnail images (or bitmap images from vector format). + Defaults to YES. + @note works for `SDImageCoder`, `SDProgressiveImageCoder`, `SDAnimatedImageCoder`. + */ +FOUNDATION_EXPORT SDImageCoderOption _Nonnull const SDImageCoderDecodePreserveAspectRatio; + +/** + A CGSize value indicating whether or not to generate the thumbnail images (or bitmap images from vector format). When this value is provided, the decoder will generate a thumbnail image which pixel size is smaller than or equal to (depends the `.preserveAspectRatio`) the value size. + Defaults to CGSizeZero, which means no thumbnail generation at all. + @note When you pass `.preserveAspectRatio == NO`, the thumbnail image is stretched to match each dimension. When `.preserveAspectRatio == YES`, the thumbnail image's width is limited to pixel size's width, the thumbnail image's height is limited to pixel size's height. For common cases, you can just pass a square size to limit both. + @note works for `SDImageCoder`, `SDProgressiveImageCoder`, `SDAnimatedImageCoder`. + */ +FOUNDATION_EXPORT SDImageCoderOption _Nonnull const SDImageCoderDecodeThumbnailPixelSize; + + // These options are for image encoding /** A Boolean value indicating whether to encode the first frame only for animated image during encoding. (NSNumber). If not provide, encode animated image if need. diff --git a/SDWebImage/Core/SDImageCoder.m b/SDWebImage/Core/SDImageCoder.m index c963376b..df5224ae 100644 --- a/SDWebImage/Core/SDImageCoder.m +++ b/SDWebImage/Core/SDImageCoder.m @@ -10,6 +10,8 @@ SDImageCoderOption const SDImageCoderDecodeFirstFrameOnly = @"decodeFirstFrameOnly"; SDImageCoderOption const SDImageCoderDecodeScaleFactor = @"decodeScaleFactor"; +SDImageCoderOption const SDImageCoderDecodePreserveAspectRatio = @"decodePreserveAspectRatio"; +SDImageCoderOption const SDImageCoderDecodeThumbnailPixelSize = @"decodeThumbnailPixelSize"; SDImageCoderOption const SDImageCoderEncodeFirstFrameOnly = @"encodeFirstFrameOnly"; SDImageCoderOption const SDImageCoderEncodeCompressionQuality = @"encodeCompressionQuality"; diff --git a/SDWebImage/Core/SDImageCoderHelper.h b/SDWebImage/Core/SDImageCoderHelper.h index dcf1da2b..5dbd523c 100644 --- a/SDWebImage/Core/SDImageCoderHelper.h +++ b/SDWebImage/Core/SDImageCoderHelper.h @@ -73,6 +73,16 @@ */ + (CGImageRef _Nullable)CGImageCreateDecoded:(_Nonnull CGImageRef)cgImage orientation:(CGImagePropertyOrientation)orientation CF_RETURNS_RETAINED; +/** + Create a scaled CGImage by the provided CGImage and size. This follows The Create Rule and you are response to call release after usage. + It will detect whether the image size matching the scale size, if not, stretch the image to the target size. + + @param cgImage The CGImage + @param size The scale size in pixel. + @return A new created scaled image + */ ++ (CGImageRef _Nullable)CGImageCreateScaled:(_Nonnull CGImageRef)cgImage size:(CGSize)size CF_RETURNS_RETAINED; + /** Return the decoded image by the provided image. This one unlike `CGImageCreateDecoded:`, will not decode the image which contains alpha channel or animated image @param image The image to be decoded @@ -89,6 +99,12 @@ */ + (UIImage * _Nullable)decodedAndScaledDownImageWithImage:(UIImage * _Nullable)image limitBytes:(NSUInteger)bytes; +/** + Control the default limit bytes to scale down larget images. + This value must be larger than or equal to 1MB. Defaults to 60MB on iOS/tvOS, 90MB on macOS, 30MB on watchOS. + */ +@property (class, readwrite) NSUInteger defaultScaleDownLimitBytes; + #if SD_UIKIT || SD_WATCH /** Convert an EXIF image orientation to an iOS one. diff --git a/SDWebImage/Core/SDImageCoderHelper.m b/SDWebImage/Core/SDImageCoderHelper.m index 3cc0c7ea..de3d0cfc 100644 --- a/SDWebImage/Core/SDImageCoderHelper.m +++ b/SDWebImage/Core/SDImageCoderHelper.m @@ -13,34 +13,34 @@ #import "SDAnimatedImageRep.h" #import "UIImage+ForceDecode.h" #import "SDAssociatedObject.h" +#import "UIImage+Metadata.h" +#import "SDInternalMacros.h" +#import + +static inline size_t SDByteAlign(size_t size, size_t alignment) { + return ((size + (alignment - 1)) / alignment) * alignment; +} -#if SD_UIKIT || SD_WATCH static const size_t kBytesPerPixel = 4; static const size_t kBitsPerComponent = 8; +static const CGFloat kBytesPerMB = 1024.0f * 1024.0f; +static const CGFloat kPixelsPerMB = kBytesPerMB / kBytesPerPixel; /* * 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; +#if SD_MAC +static CGFloat kDestImageLimitBytes = 90.f * kBytesPerMB; +#elif SD_UIKIT +static CGFloat kDestImageLimitBytes = 60.f * kBytesPerMB; +#elif SD_WATCH +static CGFloat kDestImageLimitBytes = 30.f * kBytesPerMB; +#endif static const CGFloat kDestSeemOverlap = 2.0f; // the numbers of pixels to overlap the seems where tiles meet. -#endif @implementation SDImageCoderHelper @@ -277,10 +277,54 @@ static const CGFloat kDestSeemOverlap = 2.0f; // the numbers of pixels to over return newImageRef; } ++ (CGImageRef)CGImageCreateScaled:(CGImageRef)cgImage size:(CGSize)size { + if (!cgImage) { + return NULL; + } + size_t width = CGImageGetWidth(cgImage); + size_t height = CGImageGetHeight(cgImage); + if (width == size.width && height == size.height) { + CGImageRetain(cgImage); + return cgImage; + } + + __block vImage_Buffer input_buffer = {}, output_buffer = {}; + @onExit { + if (input_buffer.data) free(input_buffer.data); + if (output_buffer.data) free(output_buffer.data); + }; + + vImage_CGImageFormat format = (vImage_CGImageFormat) { + .bitsPerComponent = 8, + .bitsPerPixel = 32, + .colorSpace = NULL, + .bitmapInfo = kCGImageAlphaFirst | kCGBitmapByteOrderDefault, + .version = 0, + .decode = NULL, + .renderingIntent = kCGRenderingIntentDefault, + }; + + vImage_Error a_ret = vImageBuffer_InitWithCGImage(&input_buffer, &format, NULL, cgImage, kvImageNoFlags); + if (a_ret != kvImageNoError) return NULL; + output_buffer.width = MAX(size.width, 0); + output_buffer.height = MAX(size.height, 0); + output_buffer.rowBytes = SDByteAlign(output_buffer.width * 4, 64); + output_buffer.data = malloc(output_buffer.rowBytes * output_buffer.height); + if (!output_buffer.data) return NULL; + + vImage_Error ret = vImageScale_ARGB8888(&input_buffer, &output_buffer, NULL, kvImageHighQualityResampling); + if (ret != kvImageNoError) return NULL; + + CGImageRef outputImage = vImageCreateCGImageFromBuffer(&output_buffer, &format, NULL, NULL, kvImageNoFlags, &ret); + if (ret != kvImageNoError) { + CGImageRelease(outputImage); + return NULL; + } + + return outputImage; +} + + (UIImage *)decodedImageWithImage:(UIImage *)image { -#if SD_MAC - return image; -#else if (![self shouldDecodeImage:image]) { return image; } @@ -289,18 +333,18 @@ static const CGFloat kDestSeemOverlap = 2.0f; // the numbers of pixels to over if (!imageRef) { return image; } +#if SD_MAC + UIImage *decodedImage = [[UIImage alloc] initWithCGImage:imageRef scale:image.scale orientation:kCGImagePropertyOrientationUp]; +#else UIImage *decodedImage = [[UIImage alloc] initWithCGImage:imageRef scale:image.scale orientation:image.imageOrientation]; +#endif CGImageRelease(imageRef); SDImageCopyAssociatedObject(image, decodedImage); decodedImage.sd_isDecoded = YES; return decodedImage; -#endif } + (UIImage *)decodedAndScaledDownImageWithImage:(UIImage *)image limitBytes:(NSUInteger)bytes { -#if SD_MAC - return image; -#else if (![self shouldDecodeImage:image]) { return image; } @@ -311,13 +355,11 @@ static const CGFloat kDestSeemOverlap = 2.0f; // the numbers of pixels to over CGFloat destTotalPixels; CGFloat tileTotalPixels; - if (bytes > 0) { - destTotalPixels = bytes / kBytesPerPixel; - tileTotalPixels = destTotalPixels / 3; - } else { - destTotalPixels = kDestTotalPixels; - tileTotalPixels = kTileTotalPixels; + if (bytes == 0) { + bytes = kDestImageLimitBytes; } + destTotalPixels = bytes / kBytesPerPixel; + tileTotalPixels = destTotalPixels / 3; CGContextRef destContext; // autorelease the bitmap context and all vars to help system to free memory when there are memory warning. @@ -420,7 +462,11 @@ static const CGFloat kDestSeemOverlap = 2.0f; // the numbers of pixels to over if (destImageRef == NULL) { return image; } +#if SD_MAC + UIImage *destImage = [[UIImage alloc] initWithCGImage:destImageRef scale:image.scale orientation:kCGImagePropertyOrientationUp]; +#else UIImage *destImage = [[UIImage alloc] initWithCGImage:destImageRef scale:image.scale orientation:image.imageOrientation]; +#endif CGImageRelease(destImageRef); if (destImage == nil) { return image; @@ -429,7 +475,17 @@ static const CGFloat kDestSeemOverlap = 2.0f; // the numbers of pixels to over destImage.sd_isDecoded = YES; return destImage; } -#endif +} + ++ (NSUInteger)defaultScaleDownLimitBytes { + return kDestImageLimitBytes; +} + ++ (void)setDefaultScaleDownLimitBytes:(NSUInteger)defaultScaleDownLimitBytes { + if (defaultScaleDownLimitBytes < kBytesPerMB) { + return; + } + kDestImageLimitBytes = defaultScaleDownLimitBytes; } #if SD_UIKIT || SD_WATCH @@ -503,7 +559,6 @@ static const CGFloat kDestSeemOverlap = 2.0f; // the numbers of pixels to over #endif #pragma mark - Helper Fuction -#if SD_UIKIT || SD_WATCH + (BOOL)shouldDecodeImage:(nullable UIImage *)image { // Avoid extra decode if (image.sd_isDecoded) { @@ -514,7 +569,7 @@ static const CGFloat kDestSeemOverlap = 2.0f; // the numbers of pixels to over return NO; } // do not decode animated images - if (image.images != nil) { + if (image.sd_isAnimated) { return NO; } @@ -533,11 +588,10 @@ static const CGFloat kDestSeemOverlap = 2.0f; // the numbers of pixels to over return NO; } CGFloat destTotalPixels; - if (bytes > 0) { - destTotalPixels = bytes / kBytesPerPixel; - } else { - destTotalPixels = kDestTotalPixels; + if (bytes == 0) { + bytes = kDestImageLimitBytes; } + destTotalPixels = bytes / kBytesPerPixel; if (destTotalPixels <= kPixelsPerMB) { // Too small to scale down return NO; @@ -551,7 +605,6 @@ static const CGFloat kDestSeemOverlap = 2.0f; // the numbers of pixels to over return shouldScaleDown; } -#endif static inline CGAffineTransform SDCGContextTransformFromOrientation(CGImagePropertyOrientation orientation, CGSize size) { // Inspiration from @libfeihu diff --git a/SDWebImage/Core/SDImageIOAnimatedCoder.m b/SDWebImage/Core/SDImageIOAnimatedCoder.m index b554fceb..b72dc4e0 100644 --- a/SDWebImage/Core/SDImageIOAnimatedCoder.m +++ b/SDWebImage/Core/SDImageIOAnimatedCoder.m @@ -12,6 +12,7 @@ #import "NSData+ImageContentType.h" #import "SDImageCoderHelper.h" #import "SDAnimatedImageRep.h" +#import "UIImage+ForceDecode.h" @interface SDImageIOCoderFrame : NSObject @@ -32,6 +33,8 @@ NSUInteger _frameCount; NSArray *_frames; BOOL _finished; + BOOL _preserveAspectRatio; + CGSize _thumbnailSize; } - (void)dealloc @@ -145,6 +148,63 @@ return frameDuration; } ++ (UIImage *)createFrameAtIndex:(NSUInteger)index source:(CGImageSourceRef)source scale:(CGFloat)scale preserveAspectRatio:(BOOL)preserveAspectRatio thumbnailSize:(CGSize)thumbnailSize { + // Parse the image properties + NSDictionary *properties = (__bridge_transfer NSDictionary *)CGImageSourceCopyPropertiesAtIndex(source, index, NULL); + NSUInteger pixelWidth = [properties[(__bridge NSString *)kCGImagePropertyPixelWidth] unsignedIntegerValue]; + NSUInteger pixelHeight = [properties[(__bridge NSString *)kCGImagePropertyPixelHeight] unsignedIntegerValue]; + CGImagePropertyOrientation exifOrientation = (CGImagePropertyOrientation)[properties[(__bridge NSString *)kCGImagePropertyOrientation] unsignedIntegerValue]; + if (!exifOrientation) { + exifOrientation = kCGImagePropertyOrientationUp; + } + + CGImageRef imageRef; + if (thumbnailSize.width == 0 || thumbnailSize.height == 0 || (pixelWidth <= thumbnailSize.width && pixelHeight <= thumbnailSize.height)) { + imageRef = CGImageSourceCreateImageAtIndex(source, index, NULL); + } else { + NSMutableDictionary *thumbnailOptions = [NSMutableDictionary dictionary]; + thumbnailOptions[(__bridge NSString *)kCGImageSourceCreateThumbnailWithTransform] = @(preserveAspectRatio); + CGFloat maxPixelSize; + if (preserveAspectRatio) { + CGFloat pixelRatio = pixelWidth / pixelHeight; + CGFloat thumbnailRatio = thumbnailSize.width / thumbnailSize.height; + if (pixelRatio > thumbnailRatio) { + maxPixelSize = thumbnailSize.width; + } else { + maxPixelSize = thumbnailSize.height; + } + } else { + maxPixelSize = MAX(thumbnailSize.width, thumbnailSize.height); + } + thumbnailOptions[(__bridge NSString *)kCGImageSourceThumbnailMaxPixelSize] = @(maxPixelSize); + thumbnailOptions[(__bridge NSString *)kCGImageSourceCreateThumbnailFromImageIfAbsent] = @(YES); + imageRef = CGImageSourceCreateThumbnailAtIndex(source, index, (__bridge CFDictionaryRef)thumbnailOptions); + if (preserveAspectRatio) { + // kCGImageSourceCreateThumbnailWithTransform will apply EXIF transform as well, we should not apply twice + exifOrientation = kCGImagePropertyOrientationUp; + } else { + // `CGImageSourceCreateThumbnailAtIndex` take only pixel dimension, if not `preserveAspectRatio`, we should manual scale to the target size + if (imageRef) { + CGImageRef scaledImageRef = [SDImageCoderHelper CGImageCreateScaled:imageRef size:thumbnailSize]; + CGImageRelease(imageRef); + imageRef = scaledImageRef; + } + } + } + if (!imageRef) { + return nil; + } + +#if SD_UIKIT || SD_WATCH + UIImageOrientation imageOrientation = [SDImageCoderHelper imageOrientationFromEXIFOrientation:exifOrientation]; + UIImage *image = [[UIImage alloc] initWithCGImage:imageRef scale:scale orientation:imageOrientation]; +#else + UIImage *image = [[UIImage alloc] initWithCGImage:imageRef scale:scale orientation:exifOrientation]; +#endif + CGImageRelease(imageRef); + return image; +} + #pragma mark - Decode - (BOOL)canDecodeFromData:(nullable NSData *)data { return ([NSData sd_imageFormatForImageData:data] == self.class.imageFormat); @@ -160,14 +220,34 @@ scale = MAX([scaleFactor doubleValue], 1); } + CGSize thumbnailSize = CGSizeZero; + NSValue *thumbnailSizeValue = options[SDImageCoderDecodeThumbnailPixelSize]; + if (thumbnailSizeValue != nil) { #if SD_MAC - SDAnimatedImageRep *imageRep = [[SDAnimatedImageRep alloc] initWithData:data]; - NSSize size = NSMakeSize(imageRep.pixelsWide / scale, imageRep.pixelsHigh / scale); - imageRep.size = size; - NSImage *animatedImage = [[NSImage alloc] initWithSize:size]; - [animatedImage addRepresentation:imageRep]; - return animatedImage; + thumbnailSize = thumbnailSizeValue.sizeValue; #else + thumbnailSize = thumbnailSizeValue.CGSizeValue; +#endif + } + + BOOL preserveAspectRatio = YES; + NSNumber *preserveAspectRatioValue = options[SDImageCoderDecodePreserveAspectRatio]; + if (preserveAspectRatioValue != nil) { + preserveAspectRatio = preserveAspectRatioValue.boolValue; + } + +#if SD_MAC + // If don't use thumbnail, prefers the built-in generation of frames (GIF/APNG) + // Which decode frames in time and reduce memory usage + if (thumbnailSize.width == 0 || thumbnailSize.height == 0) { + SDAnimatedImageRep *imageRep = [[SDAnimatedImageRep alloc] initWithData:data]; + NSSize size = NSMakeSize(imageRep.pixelsWide / scale, imageRep.pixelsHigh / scale); + imageRep.size = size; + NSImage *animatedImage = [[NSImage alloc] initWithSize:size]; + [animatedImage addRepresentation:imageRep]; + return animatedImage; + } +#endif CGImageSourceRef source = CGImageSourceCreateWithData((__bridge CFDataRef)data, NULL); if (!source) { @@ -178,19 +258,17 @@ BOOL decodeFirstFrame = [options[SDImageCoderDecodeFirstFrameOnly] boolValue]; if (decodeFirstFrame || count <= 1) { - animatedImage = [[UIImage alloc] initWithData:data scale:scale]; + animatedImage = [self.class createFrameAtIndex:0 source:source scale:scale preserveAspectRatio:preserveAspectRatio thumbnailSize:thumbnailSize]; } else { NSMutableArray *frames = [NSMutableArray array]; for (size_t i = 0; i < count; i++) { - CGImageRef imageRef = CGImageSourceCreateImageAtIndex(source, i, NULL); - if (!imageRef) { + UIImage *image = [self.class createFrameAtIndex:i source:source scale:scale preserveAspectRatio:preserveAspectRatio thumbnailSize:thumbnailSize]; + if (!image) { continue; } NSTimeInterval duration = [self.class frameDurationAtIndex:i source:source]; - UIImage *image = [[UIImage alloc] initWithCGImage:imageRef scale:scale orientation:UIImageOrientationUp]; - CGImageRelease(imageRef); SDImageFrame *frame = [SDImageFrame frameWithImage:image duration:duration]; [frames addObject:frame]; @@ -205,7 +283,6 @@ CFRelease(source); return animatedImage; -#endif } #pragma mark - Progressive Decode @@ -225,6 +302,22 @@ scale = MAX([scaleFactor doubleValue], 1); } _scale = scale; + CGSize thumbnailSize = CGSizeZero; + NSValue *thumbnailSizeValue = options[SDImageCoderDecodeThumbnailPixelSize]; + if (thumbnailSizeValue != nil) { + #if SD_MAC + thumbnailSize = thumbnailSizeValue.sizeValue; + #else + thumbnailSize = thumbnailSizeValue.CGSizeValue; + #endif + } + _thumbnailSize = thumbnailSize; + BOOL preserveAspectRatio = YES; + NSNumber *preserveAspectRatioValue = options[SDImageCoderDecodePreserveAspectRatio]; + if (preserveAspectRatioValue != nil) { + preserveAspectRatio = preserveAspectRatioValue.boolValue; + } + _preserveAspectRatio = preserveAspectRatio; #if SD_UIKIT [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didReceiveMemoryWarning:) name:UIApplicationDidReceiveMemoryWarningNotification object:nil]; #endif @@ -265,20 +358,13 @@ if (_width + _height > 0) { // Create the image - CGImageRef partialImageRef = CGImageSourceCreateImageAtIndex(_imageSource, 0, NULL); - - if (partialImageRef) { - CGFloat scale = _scale; - NSNumber *scaleFactor = options[SDImageCoderDecodeScaleFactor]; - if (scaleFactor != nil) { - scale = MAX([scaleFactor doubleValue], 1); - } -#if SD_UIKIT || SD_WATCH - image = [[UIImage alloc] initWithCGImage:partialImageRef scale:scale orientation:UIImageOrientationUp]; -#else - image = [[UIImage alloc] initWithCGImage:partialImageRef scale:scale orientation:kCGImagePropertyOrientationUp]; -#endif - CGImageRelease(partialImageRef); + CGFloat scale = _scale; + NSNumber *scaleFactor = options[SDImageCoderDecodeScaleFactor]; + if (scaleFactor != nil) { + scale = MAX([scaleFactor doubleValue], 1); + } + image = [SDImageIOAnimatedCoder createFrameAtIndex:0 source:_imageSource scale:scale preserveAspectRatio:_preserveAspectRatio thumbnailSize:_thumbnailSize]; + if (image) { image.sd_imageFormat = self.class.imageFormat; } } @@ -370,6 +456,22 @@ scale = MAX([scaleFactor doubleValue], 1); } _scale = scale; + CGSize thumbnailSize = CGSizeZero; + NSValue *thumbnailSizeValue = options[SDImageCoderDecodeThumbnailPixelSize]; + if (thumbnailSizeValue != nil) { + #if SD_MAC + thumbnailSize = thumbnailSizeValue.sizeValue; + #else + thumbnailSize = thumbnailSizeValue.CGSizeValue; + #endif + } + _thumbnailSize = thumbnailSize; + BOOL preserveAspectRatio = YES; + NSNumber *preserveAspectRatioValue = options[SDImageCoderDecodePreserveAspectRatio]; + if (preserveAspectRatioValue != nil) { + preserveAspectRatio = preserveAspectRatioValue.boolValue; + } + _preserveAspectRatio = preserveAspectRatio; _imageSource = imageSource; _imageData = data; #if SD_UIKIT @@ -421,23 +523,24 @@ } - (UIImage *)animatedImageFrameAtIndex:(NSUInteger)index { - CGImageRef imageRef = CGImageSourceCreateImageAtIndex(_imageSource, index, NULL); - if (!imageRef) { + UIImage *image = [self.class createFrameAtIndex:index source:_imageSource scale:_scale preserveAspectRatio:_preserveAspectRatio thumbnailSize:_thumbnailSize]; + if (!image) { return nil; } + image.sd_imageFormat = self.class.imageFormat; // Image/IO create CGImage does not decode, so we do this because this is called background queue, this can avoid main queue block when rendering(especially when one more imageViews use the same image instance) - CGImageRef newImageRef = [SDImageCoderHelper CGImageCreateDecoded:imageRef]; - if (!newImageRef) { - newImageRef = imageRef; - } else { - CGImageRelease(imageRef); + CGImageRef imageRef = [SDImageCoderHelper CGImageCreateDecoded:image.CGImage]; + if (!imageRef) { + return image; } #if SD_MAC - UIImage *image = [[UIImage alloc] initWithCGImage:newImageRef scale:_scale orientation:kCGImagePropertyOrientationUp]; + image = [[UIImage alloc] initWithCGImage:imageRef scale:_scale orientation:kCGImagePropertyOrientationUp]; #else - UIImage *image = [[UIImage alloc] initWithCGImage:newImageRef scale:_scale orientation:UIImageOrientationUp]; + image = [[UIImage alloc] initWithCGImage:imageRef scale:_scale orientation:image.imageOrientation]; #endif - CGImageRelease(newImageRef); + CGImageRelease(imageRef); + image.sd_isDecoded = YES; + image.sd_imageFormat = self.class.imageFormat; return image; } diff --git a/SDWebImage/Core/SDImageIOCoder.m b/SDWebImage/Core/SDImageIOCoder.m index a6fa10af..f617f437 100644 --- a/SDWebImage/Core/SDImageIOCoder.m +++ b/SDWebImage/Core/SDImageIOCoder.m @@ -12,6 +12,7 @@ #import #import "UIImage+Metadata.h" #import "SDImageHEICCoderInternal.h" +#import "SDImageIOAnimatedCoderInternal.h" @implementation SDImageIOCoder { size_t _width, _height; @@ -19,6 +20,8 @@ CGImageSourceRef _imageSource; CGFloat _scale; BOOL _finished; + BOOL _preserveAspectRatio; + CGSize _thumbnailSize; } - (void)dealloc { @@ -74,7 +77,33 @@ scale = MAX([scaleFactor doubleValue], 1) ; } - UIImage *image = [[UIImage alloc] initWithData:data scale:scale]; + CGSize thumbnailSize = CGSizeZero; + NSValue *thumbnailSizeValue = options[SDImageCoderDecodeThumbnailPixelSize]; + if (thumbnailSizeValue != nil) { +#if SD_MAC + thumbnailSize = thumbnailSizeValue.sizeValue; +#else + thumbnailSize = thumbnailSizeValue.CGSizeValue; +#endif + } + + BOOL preserveAspectRatio = YES; + NSNumber *preserveAspectRatioValue = options[SDImageCoderDecodePreserveAspectRatio]; + if (preserveAspectRatioValue != nil) { + preserveAspectRatio = preserveAspectRatioValue.boolValue; + } + + CGImageSourceRef source = CGImageSourceCreateWithData((__bridge CFDataRef)data, NULL); + if (!source) { + return nil; + } + + UIImage *image = [SDImageIOAnimatedCoder createFrameAtIndex:0 source:source scale:scale preserveAspectRatio:preserveAspectRatio thumbnailSize:thumbnailSize]; + CFRelease(source); + if (!image) { + return nil; + } + image.sd_imageFormat = [NSData sd_imageFormatForImageData:data]; return image; } @@ -95,6 +124,22 @@ scale = MAX([scaleFactor doubleValue], 1); } _scale = scale; + CGSize thumbnailSize = CGSizeZero; + NSValue *thumbnailSizeValue = options[SDImageCoderDecodeThumbnailPixelSize]; + if (thumbnailSizeValue != nil) { + #if SD_MAC + thumbnailSize = thumbnailSizeValue.sizeValue; + #else + thumbnailSize = thumbnailSizeValue.CGSizeValue; + #endif + } + _thumbnailSize = thumbnailSize; + BOOL preserveAspectRatio = YES; + NSNumber *preserveAspectRatioValue = options[SDImageCoderDecodePreserveAspectRatio]; + if (preserveAspectRatioValue != nil) { + preserveAspectRatio = preserveAspectRatioValue.boolValue; + } + _preserveAspectRatio = preserveAspectRatio; #if SD_UIKIT [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didReceiveMemoryWarning:) name:UIApplicationDidReceiveMemoryWarningNotification object:nil]; #endif @@ -140,21 +185,13 @@ if (_width + _height > 0) { // Create the image - CGImageRef partialImageRef = CGImageSourceCreateImageAtIndex(_imageSource, 0, NULL); - - if (partialImageRef) { - CGFloat scale = _scale; - NSNumber *scaleFactor = options[SDImageCoderDecodeScaleFactor]; - if (scaleFactor != nil) { - scale = MAX([scaleFactor doubleValue], 1); - } -#if SD_UIKIT || SD_WATCH - UIImageOrientation imageOrientation = [SDImageCoderHelper imageOrientationFromEXIFOrientation:_orientation]; - image = [[UIImage alloc] initWithCGImage:partialImageRef scale:scale orientation:imageOrientation]; -#else - image = [[UIImage alloc] initWithCGImage:partialImageRef scale:scale orientation:_orientation]; -#endif - CGImageRelease(partialImageRef); + CGFloat scale = _scale; + NSNumber *scaleFactor = options[SDImageCoderDecodeScaleFactor]; + if (scaleFactor != nil) { + scale = MAX([scaleFactor doubleValue], 1); + } + image = [SDImageIOAnimatedCoder createFrameAtIndex:0 source:_imageSource scale:scale preserveAspectRatio:_preserveAspectRatio thumbnailSize:_thumbnailSize]; + if (image) { CFStringRef uttype = CGImageSourceGetType(_imageSource); image.sd_imageFormat = [NSData sd_imageFormatFromUTType:uttype]; } diff --git a/SDWebImage/Core/SDImageLoader.m b/SDWebImage/Core/SDImageLoader.m index 8cbbe4e0..4c831c59 100644 --- a/SDWebImage/Core/SDImageLoader.m +++ b/SDWebImage/Core/SDImageLoader.m @@ -32,12 +32,25 @@ UIImage * _Nullable SDImageLoaderDecodeImageData(NSData * _Nonnull imageData, NS BOOL decodeFirstFrame = SD_OPTIONS_CONTAINS(options, SDWebImageDecodeFirstFrameOnly); NSNumber *scaleValue = context[SDWebImageContextImageScaleFactor]; CGFloat scale = scaleValue.doubleValue >= 1 ? scaleValue.doubleValue : SDImageScaleFactorForKey(cacheKey); - SDImageCoderOptions *coderOptions = @{SDImageCoderDecodeFirstFrameOnly : @(decodeFirstFrame), SDImageCoderDecodeScaleFactor : @(scale)}; - if (context) { - SDImageCoderMutableOptions *mutableCoderOptions = [coderOptions mutableCopy]; - [mutableCoderOptions setValue:context forKey:SDImageCoderWebImageContext]; - coderOptions = [mutableCoderOptions copy]; + NSNumber *preserveAspectRatioValue = context[SDWebImageContextImagePreserveAspectRatio]; + NSValue *thumbnailSizeValue; + BOOL shouldScaleDown = SD_OPTIONS_CONTAINS(options, SDWebImageScaleDownLargeImages); + if (shouldScaleDown) { + CGFloat thumbnailPixels = SDImageCoderHelper.defaultScaleDownLimitBytes / 4; + CGFloat dimension = ceil(sqrt(thumbnailPixels)); + thumbnailSizeValue = @(CGSizeMake(dimension, dimension)); } + if (context[SDWebImageContextImageThumbnailPixelSize]) { + thumbnailSizeValue = context[SDWebImageContextImageThumbnailPixelSize]; + } + + SDImageCoderMutableOptions *mutableCoderOptions = [NSMutableDictionary dictionaryWithCapacity:2]; + mutableCoderOptions[SDImageCoderDecodeFirstFrameOnly] = @(decodeFirstFrame); + mutableCoderOptions[SDImageCoderDecodeScaleFactor] = @(scale); + mutableCoderOptions[SDImageCoderDecodePreserveAspectRatio] = preserveAspectRatioValue; + mutableCoderOptions[SDImageCoderDecodeThumbnailPixelSize] = thumbnailSizeValue; + mutableCoderOptions[SDImageCoderWebImageContext] = context; + SDImageCoderOptions *coderOptions = [mutableCoderOptions copy]; if (!decodeFirstFrame) { // check whether we should use `SDAnimatedImage` @@ -71,12 +84,7 @@ UIImage * _Nullable SDImageLoaderDecodeImageData(NSData * _Nonnull imageData, NS } if (shouldDecode) { - BOOL shouldScaleDown = SD_OPTIONS_CONTAINS(options, SDWebImageScaleDownLargeImages); - if (shouldScaleDown) { - image = [SDImageCoderHelper decodedAndScaledDownImageWithImage:image limitBytes:0]; - } else { - image = [SDImageCoderHelper decodedImageWithImage:image]; - } + image = [SDImageCoderHelper decodedImageWithImage:image]; } } @@ -99,12 +107,25 @@ UIImage * _Nullable SDImageLoaderDecodeProgressiveImageData(NSData * _Nonnull im BOOL decodeFirstFrame = SD_OPTIONS_CONTAINS(options, SDWebImageDecodeFirstFrameOnly); NSNumber *scaleValue = context[SDWebImageContextImageScaleFactor]; CGFloat scale = scaleValue.doubleValue >= 1 ? scaleValue.doubleValue : SDImageScaleFactorForKey(cacheKey); - SDImageCoderOptions *coderOptions = @{SDImageCoderDecodeFirstFrameOnly : @(decodeFirstFrame), SDImageCoderDecodeScaleFactor : @(scale)}; - if (context) { - SDImageCoderMutableOptions *mutableCoderOptions = [coderOptions mutableCopy]; - [mutableCoderOptions setValue:context forKey:SDImageCoderWebImageContext]; - coderOptions = [mutableCoderOptions copy]; + NSNumber *preserveAspectRatioValue = context[SDWebImageContextImagePreserveAspectRatio]; + NSValue *thumbnailSizeValue; + BOOL shouldScaleDown = SD_OPTIONS_CONTAINS(options, SDWebImageScaleDownLargeImages); + if (shouldScaleDown) { + CGFloat thumbnailPixels = SDImageCoderHelper.defaultScaleDownLimitBytes / 4; + CGFloat dimension = ceil(sqrt(thumbnailPixels)); + thumbnailSizeValue = @(CGSizeMake(dimension, dimension)); } + if (context[SDWebImageContextImageThumbnailPixelSize]) { + thumbnailSizeValue = context[SDWebImageContextImageThumbnailPixelSize]; + } + + SDImageCoderMutableOptions *mutableCoderOptions = [NSMutableDictionary dictionaryWithCapacity:2]; + mutableCoderOptions[SDImageCoderDecodeFirstFrameOnly] = @(decodeFirstFrame); + mutableCoderOptions[SDImageCoderDecodeScaleFactor] = @(scale); + mutableCoderOptions[SDImageCoderDecodePreserveAspectRatio] = preserveAspectRatioValue; + mutableCoderOptions[SDImageCoderDecodeThumbnailPixelSize] = thumbnailSizeValue; + mutableCoderOptions[SDImageCoderWebImageContext] = context; + SDImageCoderOptions *coderOptions = [mutableCoderOptions copy]; id progressiveCoder = objc_getAssociatedObject(operation, SDImageLoaderProgressiveCoderKey); if (!progressiveCoder) { diff --git a/SDWebImage/Core/SDWebImageDefine.h b/SDWebImage/Core/SDWebImageDefine.h index a9a34367..bd4d4e68 100644 --- a/SDWebImage/Core/SDWebImageDefine.h +++ b/SDWebImage/Core/SDWebImageDefine.h @@ -122,9 +122,12 @@ typedef NS_OPTIONS(NSUInteger, SDWebImageOptions) { SDWebImageAvoidAutoSetImage = 1 << 10, /** - * By default, images are decoded respecting their original size. On iOS, this flag will scale down the - * images to a size compatible with the constrained memory of devices. - * This flag take no effect if `SDWebImageAvoidDecodeImage` is set. And it will be ignored if `SDWebImageProgressiveLoad` is set. + * By default, images are decoded respecting their original size. + * This flag will scale down the images to a size compatible with the constrained memory of devices. + * To control the limit memory bytes, check `SDImageCoderHelper.defaultScaleDownLimitBytes` (Defaults to 60MB on iOS) + * This will actually translate to use context option `.imageThumbnailPixelSize` from v5.5.0 (Defaults to (3966, 3966) on iOS). Previously does not. + * This flags effect the progressive and animated images as well from v5.5.0. Previously does not. + * @note If you need detail controls, it's better to use context option `imageThumbnailPixelSize` and `imagePreserveAspectRatio` instead. */ SDWebImageScaleDownLargeImages = 1 << 11, @@ -217,6 +220,19 @@ FOUNDATION_EXPORT SDWebImageContextOption _Nonnull const SDWebImageContextImageT */ FOUNDATION_EXPORT SDWebImageContextOption _Nonnull const SDWebImageContextImageScaleFactor; +/** + A Boolean value indicating whether to keep the original aspect ratio when generating thumbnail images (or bitmap images from vector format). + Defaults to YES. (NSNumber) + */ +FOUNDATION_EXPORT SDWebImageContextOption _Nonnull const SDWebImageContextImagePreserveAspectRatio; + +/** + A CGSize raw value indicating whether or not to generate the thumbnail images (or bitmap images from vector format). When this value is provided, the decoder will generate a thumbnail image which pixel size is smaller than or equal to (depends the `.imagePreserveAspectRatio`) the value size. + @note When you pass `.preserveAspectRatio == NO`, the thumbnail image is stretched to match each dimension. When `.preserveAspectRatio == YES`, the thumbnail image's width is limited to pixel size's width, the thumbnail image's height is limited to pixel size's height. For common cases, you can just pass a square size to limit both. + Defaults to CGSizeZero, which means no thumbnail generation at all. (NSValue) + */ +FOUNDATION_EXPORT SDWebImageContextOption _Nonnull const SDWebImageContextImageThumbnailPixelSize; + /** A SDImageCacheType raw value which specify the store cache type when the image has just been downloaded and will be stored to the cache. Specify `SDImageCacheTypeNone` to disable cache storage; `SDImageCacheTypeDisk` to store in disk cache only; `SDImageCacheTypeMemory` to store in memory only. And `SDImageCacheTypeAll` to store in both memory cache and disk cache. If you use image transformer feature, this actually apply for the transformed image, but not the original image itself. Use `SDWebImageContextOriginalStoreCacheType` if you want to control the original image's store cache type at the same time. diff --git a/SDWebImage/Core/SDWebImageDefine.m b/SDWebImage/Core/SDWebImageDefine.m index 496392c4..921e878a 100644 --- a/SDWebImage/Core/SDWebImageDefine.m +++ b/SDWebImage/Core/SDWebImageDefine.m @@ -122,6 +122,8 @@ SDWebImageContextOption const SDWebImageContextSetImageOperationKey = @"setImage SDWebImageContextOption const SDWebImageContextCustomManager = @"customManager"; SDWebImageContextOption const SDWebImageContextImageTransformer = @"imageTransformer"; SDWebImageContextOption const SDWebImageContextImageScaleFactor = @"imageScaleFactor"; +SDWebImageContextOption const SDWebImageContextImagePreserveAspectRatio = @"imagePreserveAspectRatio"; +SDWebImageContextOption const SDWebImageContextImageThumbnailPixelSize = @"imageThumbnailPixelSize"; SDWebImageContextOption const SDWebImageContextStoreCacheType = @"storeCacheType"; SDWebImageContextOption const SDWebImageContextOriginalStoreCacheType = @"originalStoreCacheType"; SDWebImageContextOption const SDWebImageContextAnimatedImageClass = @"animatedImageClass"; diff --git a/SDWebImage/Core/SDWebImageManager.m b/SDWebImage/Core/SDWebImageManager.m index 61c0a36c..97ffb488 100644 --- a/SDWebImage/Core/SDWebImageManager.m +++ b/SDWebImage/Core/SDWebImageManager.m @@ -95,19 +95,45 @@ static id _defaultImageLoader; } - (nullable NSString *)cacheKeyForURL:(nullable NSURL *)url { - return [self cacheKeyForURL:url cacheKeyFilter:self.cacheKeyFilter]; + return [self cacheKeyForURL:url context:nil]; } -- (nullable NSString *)cacheKeyForURL:(nullable NSURL *)url cacheKeyFilter:(id)cacheKeyFilter { +- (nullable NSString *)cacheKeyForURL:(nullable NSURL *)url context:(nullable SDWebImageContext *)context { if (!url) { return @""; } - - if (cacheKeyFilter) { - return [cacheKeyFilter cacheKeyForURL:url]; - } else { - return url.absoluteString; + + NSString *key; + // Cache Key Filter + id cacheKeyFilter = self.cacheKeyFilter; + if (context[SDWebImageContextCacheKeyFilter]) { + cacheKeyFilter = context[SDWebImageContextCacheKeyFilter]; } + if (cacheKeyFilter) { + key = [cacheKeyFilter cacheKeyForURL:url]; + } else { + key = url.absoluteString; + } + // Thumbnail Key Appending + NSValue *thumbnailSizeValue = context[SDWebImageContextImageThumbnailPixelSize]; + if (thumbnailSizeValue != nil) { + CGSize thumbnailSize = CGSizeZero; +#if SD_MAC + thumbnailSize = thumbnailSizeValue.sizeValue; +#else + thumbnailSize = thumbnailSizeValue.CGSizeValue; +#endif + + BOOL preserveAspectRatio = YES; + NSNumber *preserveAspectRatioValue = context[SDWebImageContextImagePreserveAspectRatio]; + if (preserveAspectRatioValue != nil) { + preserveAspectRatio = preserveAspectRatioValue.boolValue; + } + NSString *transformerKey = [NSString stringWithFormat:@"Thumbnail({%f,%f},%d)", thumbnailSize.width, thumbnailSize.height, preserveAspectRatio]; + key = SDTransformedKeyForKey(key, transformerKey); + } + + return key; } - (SDWebImageCombinedOperation *)loadImageWithURL:(NSURL *)url options:(SDWebImageOptions)options progress:(SDImageLoaderProgressBlock)progressBlock completed:(SDInternalCompletionBlock)completedBlock { @@ -188,8 +214,7 @@ static id _defaultImageLoader; // Check whether we should query cache BOOL shouldQueryCache = !SD_OPTIONS_CONTAINS(options, SDWebImageFromLoaderOnly); if (shouldQueryCache) { - id cacheKeyFilter = context[SDWebImageContextCacheKeyFilter]; - NSString *key = [self cacheKeyForURL:url cacheKeyFilter:cacheKeyFilter]; + NSString *key = [self cacheKeyForURL:url context:context]; @weakify(operation); operation.cacheOperation = [self.imageCache queryImageForKey:key options:options context:context completion:^(UIImage * _Nullable cachedImage, NSData * _Nullable cachedData, SDImageCacheType cacheType) { @strongify(operation); @@ -303,8 +328,7 @@ static id _defaultImageLoader; if (context[SDWebImageContextOriginalStoreCacheType]) { originalStoreCacheType = [context[SDWebImageContextOriginalStoreCacheType] integerValue]; } - id cacheKeyFilter = context[SDWebImageContextCacheKeyFilter]; - NSString *key = [self cacheKeyForURL:url cacheKeyFilter:cacheKeyFilter]; + NSString *key = [self cacheKeyForURL:url context:context]; id transformer = context[SDWebImageContextImageTransformer]; id cacheSerializer = context[SDWebImageContextCacheSerializer]; @@ -353,8 +377,7 @@ static id _defaultImageLoader; if (context[SDWebImageContextStoreCacheType]) { storeCacheType = [context[SDWebImageContextStoreCacheType] integerValue]; } - id cacheKeyFilter = context[SDWebImageContextCacheKeyFilter]; - NSString *key = [self cacheKeyForURL:url cacheKeyFilter:cacheKeyFilter]; + NSString *key = [self cacheKeyForURL:url context:context]; id transformer = context[SDWebImageContextImageTransformer]; id cacheSerializer = context[SDWebImageContextCacheSerializer]; BOOL shouldTransformImage = originalImage && (!originalImage.sd_isAnimated || (options & SDWebImageTransformAnimatedImage)) && transformer; diff --git a/SDWebImage/Private/SDImageIOAnimatedCoderInternal.h b/SDWebImage/Private/SDImageIOAnimatedCoderInternal.h index 4b500131..f2976ea8 100644 --- a/SDWebImage/Private/SDImageIOAnimatedCoderInternal.h +++ b/SDWebImage/Private/SDImageIOAnimatedCoderInternal.h @@ -13,5 +13,6 @@ + (NSTimeInterval)frameDurationAtIndex:(NSUInteger)index source:(nonnull CGImageSourceRef)source; + (NSUInteger)imageLoopCountWithSource:(nonnull CGImageSourceRef)source; ++ (nullable UIImage *)createFrameAtIndex:(NSUInteger)index source:(nonnull CGImageSourceRef)source scale:(CGFloat)scale preserveAspectRatio:(BOOL)preserveAspectRatio thumbnailSize:(CGSize)thumbnailSize; @end diff --git a/Tests/Tests/SDImageCoderTests.m b/Tests/Tests/SDImageCoderTests.m index 0320c2c2..23dc6d56 100644 --- a/Tests/Tests/SDImageCoderTests.m +++ b/Tests/Tests/SDImageCoderTests.m @@ -20,7 +20,6 @@ expect([UIImage sd_decodedAndScaledDownImageWithImage:nil]).to.beNil(); } -#if SD_UIKIT - (void)test02ThatDecodedImageWithImageWorksWithARegularJPGImage { NSString * testImagePath = [[NSBundle bundleForClass:[self class]] pathForResource:@"TestImage" ofType:@"jpg"]; UIImage *image = [[UIImage alloc] initWithContentsOfFile:testImagePath]; @@ -34,7 +33,11 @@ - (void)test03ThatDecodedImageWithImageDoesNotDecodeAnimatedImages { NSString * testImagePath = [[NSBundle bundleForClass:[self class]] pathForResource:@"TestImage" ofType:@"gif"]; UIImage *image = [[UIImage alloc] initWithContentsOfFile:testImagePath]; +#if SD_MAC + UIImage *animatedImage = image; +#else UIImage *animatedImage = [UIImage animatedImageWithImages:@[image] duration:0]; +#endif UIImage *decodedImage = [UIImage sd_decodedImageWithImage:animatedImage]; expect(decodedImage).toNot.beNil(); expect(decodedImage).to.equal(animatedImage); @@ -61,7 +64,7 @@ - (void)test06ThatDecodeAndScaleDownImageWorks { NSString * testImagePath = [[NSBundle bundleForClass:[self class]] pathForResource:@"TestImageLarge" ofType:@"jpg"]; UIImage *image = [[UIImage alloc] initWithContentsOfFile:testImagePath]; - UIImage *decodedImage = [UIImage sd_decodedAndScaledDownImageWithImage:image]; + UIImage *decodedImage = [UIImage sd_decodedAndScaledDownImageWithImage:image limitBytes:(60 * 1024 * 1024)]; expect(decodedImage).toNot.beNil(); expect(decodedImage).toNot.equal(image); expect(decodedImage.size.width).toNot.equal(image.size.width); @@ -78,7 +81,6 @@ expect(decodedImage.size.width).to.equal(image.size.width); expect(decodedImage.size.height).to.equal(image.size.height); } -#endif - (void)test11ThatAPNGPCoderWorks { NSURL *APNGURL = [[NSBundle bundleForClass:[self class]] URLForResource:@"TestImageAnimated" withExtension:@"apng"]; @@ -197,14 +199,47 @@ withLocalImageURL:(NSURL *)imageUrl #endif } + // 3 - check thumbnail decoding + CGFloat pixelWidth = inputImage.size.width; + CGFloat pixelHeight = inputImage.size.height; + expect(pixelWidth).beGreaterThan(0); + expect(pixelHeight).beGreaterThan(0); + // check thumnail with scratch + CGFloat thumbnailWidth = 50; + CGFloat thumbnailHeight = 50; + UIImage *thumbImage = [coder decodedImageWithData:inputImageData options:@{ + SDImageCoderDecodeThumbnailPixelSize : @(CGSizeMake(thumbnailWidth, thumbnailHeight)), + SDImageCoderDecodePreserveAspectRatio : @(NO) + }]; + expect(thumbImage).toNot.beNil(); + expect(thumbImage.size).equal(CGSizeMake(thumbnailWidth, thumbnailHeight)); + // check thumnail with aspect ratio limit + thumbImage = [coder decodedImageWithData:inputImageData options:@{ + SDImageCoderDecodeThumbnailPixelSize : @(CGSizeMake(thumbnailWidth, thumbnailHeight)), + SDImageCoderDecodePreserveAspectRatio : @(YES) + }]; + expect(thumbImage).toNot.beNil(); + CGFloat ratio = pixelWidth / pixelHeight; + CGFloat thumbnailRatio = thumbnailWidth / thumbnailHeight; + CGSize thumbnailPixelSize; + if (ratio > thumbnailRatio) { + thumbnailPixelSize = CGSizeMake(thumbnailWidth, round(thumbnailWidth / ratio)); + } else { + thumbnailPixelSize = CGSizeMake(round(thumbnailHeight * ratio), thumbnailHeight); + } + // Image/IO's thumbnail API does not always use round to preserve precision, we check ABS <= 1 + expect(ABS(thumbImage.size.width - thumbnailPixelSize.width) <= 1); + expect(ABS(thumbImage.size.height - thumbnailPixelSize.height) <= 1); + + if (supportsEncoding) { - // 3 - check if we can encode to the original format + // 4 - check if we can encode to the original format if (encodingFormat == SDImageFormatUndefined) { encodingFormat = inputImageFormat; } expect([coder canEncodeToFormat:encodingFormat]).to.beTruthy(); - // 4 - encode from UIImage to NSData using the inputImageFormat and check it + // 5 - encode from UIImage to NSData using the inputImageFormat and check it NSData *outputImageData = [coder encodedDataWithImage:inputImage format:encodingFormat options:nil]; expect(outputImageData).toNot.beNil(); UIImage *outputImage = [coder decodedImageWithData:outputImageData options:nil]; diff --git a/Tests/Tests/SDWebImageManagerTests.m b/Tests/Tests/SDWebImageManagerTests.m index 143dc504..c75fc950 100644 --- a/Tests/Tests/SDWebImageManagerTests.m +++ b/Tests/Tests/SDWebImageManagerTests.m @@ -247,6 +247,27 @@ [self waitForExpectationsWithCommonTimeout]; } +- (void)test13ThatScaleDownLargeImageUseThumbnailDecoding { + XCTestExpectation *expectation = [self expectationWithDescription:@"SDWebImageScaleDownLargeImages should translate to thumbnail decoding"]; + NSURL *originalImageURL = [NSURL URLWithString:@"http://via.placeholder.com/3999x3999.png"]; // Max size for this API + NSUInteger defaultLimitBytes = SDImageCoderHelper.defaultScaleDownLimitBytes; + SDImageCoderHelper.defaultScaleDownLimitBytes = 1000 * 1000 * 4; // Limit 1000x1000 pixel + // From v5.5.0, the `SDWebImageScaleDownLargeImages` translate to `SDWebImageContextImageThumbnailPixelSize`, and works for progressive loading + [SDWebImageManager.sharedManager loadImageWithURL:originalImageURL options:SDWebImageScaleDownLargeImages | SDWebImageProgressiveLoad progress:nil completed:^(UIImage * _Nullable image, NSData * _Nullable data, NSError * _Nullable error, SDImageCacheType cacheType, BOOL finished, NSURL * _Nullable imageURL) { + expect(image).notTo.beNil(); + expect(image.size).equal(CGSizeMake(1000, 1000)); + if (finished) { + [expectation fulfill]; + } else { + expect(image.sd_isIncremental).beTruthy(); + } + }]; + + [self waitForExpectationsWithCommonTimeoutUsingHandler:^(NSError * _Nullable error) { + SDImageCoderHelper.defaultScaleDownLimitBytes = defaultLimitBytes; + }]; +} + - (NSString *)testJPEGPath { NSBundle *testBundle = [NSBundle bundleForClass:[self class]]; return [testBundle pathForResource:@"TestImage" ofType:@"jpg"];