Merge pull request #3537 from dreampiggy/feature/coder_scale_down_limit_bytes_auto
Introduce the automatically calculation of thumbnail (include animated/static image) using `SDImageCoderDecodeScaleDownLimitBytes`
This commit is contained in:
commit
b27c579f47
|
@ -23,7 +23,7 @@
|
|||
}
|
||||
[self.imageView sd_setImageWithURL:self.imageURL
|
||||
placeholderImage:nil
|
||||
options:SDWebImageProgressiveLoad];
|
||||
options:SDWebImageProgressiveLoad | SDWebImageScaleDownLargeImages];
|
||||
self.imageView.shouldCustomLoopCount = YES;
|
||||
self.imageView.animationRepeatCount = 0;
|
||||
}
|
||||
|
|
|
@ -18,14 +18,14 @@
|
|||
SDImageCoderOptions * _Nonnull SDGetDecodeOptionsFromContext(SDWebImageContext * _Nullable context, SDWebImageOptions options, NSString * _Nonnull cacheKey) {
|
||||
BOOL decodeFirstFrame = SD_OPTIONS_CONTAINS(options, SDWebImageDecodeFirstFrameOnly);
|
||||
NSNumber *scaleValue = context[SDWebImageContextImageScaleFactor];
|
||||
CGFloat scale = scaleValue.doubleValue >= 1 ? scaleValue.doubleValue : SDImageScaleFactorForKey(cacheKey);
|
||||
CGFloat scale = scaleValue.doubleValue >= 1 ? scaleValue.doubleValue : SDImageScaleFactorForKey(cacheKey); // Use cache key to detect scale
|
||||
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));
|
||||
NSNumber *scaleDownLimitBytesValue = context[SDWebImageContextImageScaleDownLimitBytes];
|
||||
if (!scaleDownLimitBytesValue && shouldScaleDown) {
|
||||
// Use the default limit bytes
|
||||
scaleDownLimitBytesValue = @(SDImageCoderHelper.defaultScaleDownLimitBytes);
|
||||
}
|
||||
if (context[SDWebImageContextImageThumbnailPixelSize]) {
|
||||
thumbnailSizeValue = context[SDWebImageContextImageThumbnailPixelSize];
|
||||
|
@ -56,6 +56,7 @@ SDImageCoderOptions * _Nonnull SDGetDecodeOptionsFromContext(SDWebImageContext *
|
|||
mutableCoderOptions[SDImageCoderDecodeThumbnailPixelSize] = thumbnailSizeValue;
|
||||
mutableCoderOptions[SDImageCoderDecodeTypeIdentifierHint] = typeIdentifierHint;
|
||||
mutableCoderOptions[SDImageCoderDecodeFileExtensionHint] = fileExtensionHint;
|
||||
mutableCoderOptions[SDImageCoderDecodeScaleDownLimitBytes] = scaleDownLimitBytesValue;
|
||||
|
||||
return [mutableCoderOptions copy];
|
||||
}
|
||||
|
@ -70,6 +71,7 @@ void SDSetDecodeOptionsToContext(SDWebImageMutableContext * _Nonnull mutableCont
|
|||
mutableContext[SDWebImageContextImageScaleFactor] = decodeOptions[SDImageCoderDecodeScaleFactor];
|
||||
mutableContext[SDWebImageContextImagePreserveAspectRatio] = decodeOptions[SDImageCoderDecodePreserveAspectRatio];
|
||||
mutableContext[SDWebImageContextImageThumbnailPixelSize] = decodeOptions[SDImageCoderDecodeThumbnailPixelSize];
|
||||
mutableContext[SDWebImageContextImageScaleDownLimitBytes] = decodeOptions[SDImageCoderDecodeScaleDownLimitBytes];
|
||||
|
||||
NSString *typeIdentifierHint = decodeOptions[SDImageCoderDecodeTypeIdentifierHint];
|
||||
if (!typeIdentifierHint) {
|
||||
|
|
|
@ -75,6 +75,18 @@ FOUNDATION_EXPORT SDImageCoderOption _Nonnull const SDImageCoderDecodeTypeIdenti
|
|||
*/
|
||||
FOUNDATION_EXPORT SDImageCoderOption _Nonnull const SDImageCoderDecodeUseLazyDecoding;
|
||||
|
||||
/**
|
||||
A NSUInteger value to provide the limit bytes during decoding. This can help to avoid OOM on large frame count animated image or large pixel static image when you don't know how much RAM it occupied before decoding
|
||||
The decoder will do these logic based on limit bytes:
|
||||
1. Get the total frame count (static image means 1)
|
||||
2. Calculate the `framePixelSize` width/height to `sqrt(limitBytes / frameCount / bytesPerPixel)`, keeping aspect ratio (at least 1x1)
|
||||
3. If the `framePixelSize < originalImagePixelSize`, then do thumbnail decoding (see `SDImageCoderDecodeThumbnailPixelSize`) use the `framePixelSize` and `preseveAspectRatio = YES`
|
||||
4. Else, use the full pixel decoding (small than limit bytes)
|
||||
5. Whatever result, this does not effect the animated/static behavior of image. So even if you set `limitBytes = 1 && frameCount = 100`, we will stll create animated image with each frame `1x1` pixel size.
|
||||
@note This option has higher priority than `.decodeThumbnailPixelSize`
|
||||
*/
|
||||
FOUNDATION_EXPORT SDImageCoderOption _Nonnull const SDImageCoderDecodeScaleDownLimitBytes;
|
||||
|
||||
// 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.
|
||||
|
|
|
@ -15,6 +15,7 @@ SDImageCoderOption const SDImageCoderDecodeThumbnailPixelSize = @"decodeThumbnai
|
|||
SDImageCoderOption const SDImageCoderDecodeFileExtensionHint = @"decodeFileExtensionHint";
|
||||
SDImageCoderOption const SDImageCoderDecodeTypeIdentifierHint = @"decodeTypeIdentifierHint";
|
||||
SDImageCoderOption const SDImageCoderDecodeUseLazyDecoding = @"decodeUseLazyDecoding";
|
||||
SDImageCoderOption const SDImageCoderDecodeScaleDownLimitBytes = @"decodeScaleDownLimitBytes";
|
||||
|
||||
SDImageCoderOption const SDImageCoderEncodeFirstFrameOnly = @"encodeFirstFrameOnly";
|
||||
SDImageCoderOption const SDImageCoderEncodeCompressionQuality = @"encodeCompressionQuality";
|
||||
|
|
|
@ -47,6 +47,19 @@ static CGImageRef __nullable SDCGImageCreateCopy(CGImageRef cg_nullable image) {
|
|||
return newImage;
|
||||
}
|
||||
|
||||
static inline CGSize SDCalculateScaleDownPixelSize(NSUInteger limitBytes, CGSize originalSize, NSUInteger frameCount, NSUInteger bytesPerPixel) {
|
||||
if (CGSizeEqualToSize(originalSize, CGSizeZero)) return CGSizeMake(1, 1);
|
||||
NSUInteger totalFramePixelSize = limitBytes / bytesPerPixel / (frameCount ?: 1);
|
||||
CGFloat ratio = originalSize.height / originalSize.width;
|
||||
CGFloat width = sqrt(totalFramePixelSize / ratio);
|
||||
CGFloat height = width * ratio;
|
||||
width = MAX(1, floor(width));
|
||||
height = MAX(1, floor(height));
|
||||
CGSize size = CGSizeMake(width, height);
|
||||
|
||||
return size;
|
||||
}
|
||||
|
||||
@interface SDImageIOCoderFrame : NSObject
|
||||
|
||||
@property (nonatomic, assign) NSUInteger index; // Frame index (zero based)
|
||||
|
@ -70,6 +83,7 @@ static CGImageRef __nullable SDCGImageCreateCopy(CGImageRef cg_nullable image) {
|
|||
BOOL _finished;
|
||||
BOOL _preserveAspectRatio;
|
||||
CGSize _thumbnailSize;
|
||||
NSUInteger _limitBytes;
|
||||
BOOL _lazyDecode;
|
||||
}
|
||||
|
||||
|
@ -415,16 +429,35 @@ static CGImageRef __nullable SDCGImageCreateCopy(CGImageRef cg_nullable image) {
|
|||
return nil;
|
||||
}
|
||||
|
||||
size_t count = CGImageSourceGetCount(source);
|
||||
size_t frameCount = CGImageSourceGetCount(source);
|
||||
UIImage *animatedImage;
|
||||
|
||||
NSUInteger limitBytes = 0;
|
||||
NSNumber *limitBytesValue = options[SDImageCoderDecodeScaleDownLimitBytes];
|
||||
if (limitBytesValue != nil) {
|
||||
limitBytes = limitBytesValue.unsignedIntegerValue;
|
||||
}
|
||||
// Parse the image properties
|
||||
NSDictionary *properties = (__bridge_transfer NSDictionary *)CGImageSourceCopyPropertiesAtIndex(source, 0, NULL);
|
||||
size_t width = [properties[(__bridge NSString *)kCGImagePropertyPixelWidth] doubleValue];
|
||||
size_t height = [properties[(__bridge NSString *)kCGImagePropertyPixelHeight] doubleValue];
|
||||
// Scale down to limit bytes if need
|
||||
if (limitBytes > 0) {
|
||||
// Hack since ImageIO public API (not CGImageDecompressor/CMPhoto) always return back RGBA8888 CGImage
|
||||
CGSize imageSize = CGSizeMake(width, height);
|
||||
CGSize framePixelSize = SDCalculateScaleDownPixelSize(limitBytes, imageSize, frameCount, 4);
|
||||
// Override thumbnail size
|
||||
thumbnailSize = framePixelSize;
|
||||
preserveAspectRatio = YES;
|
||||
}
|
||||
|
||||
BOOL decodeFirstFrame = [options[SDImageCoderDecodeFirstFrameOnly] boolValue];
|
||||
if (decodeFirstFrame || count <= 1) {
|
||||
if (decodeFirstFrame || frameCount <= 1) {
|
||||
animatedImage = [self.class createFrameAtIndex:0 source:source scale:scale preserveAspectRatio:preserveAspectRatio thumbnailSize:thumbnailSize lazyDecode:lazyDecode animatedImage:NO];
|
||||
} else {
|
||||
NSMutableArray<SDImageFrame *> *frames = [NSMutableArray arrayWithCapacity:count];
|
||||
NSMutableArray<SDImageFrame *> *frames = [NSMutableArray arrayWithCapacity:frameCount];
|
||||
|
||||
for (size_t i = 0; i < count; i++) {
|
||||
for (size_t i = 0; i < frameCount; i++) {
|
||||
UIImage *image = [self.class createFrameAtIndex:i source:source scale:scale preserveAspectRatio:preserveAspectRatio thumbnailSize:thumbnailSize lazyDecode:lazyDecode animatedImage:NO];
|
||||
if (!image) {
|
||||
continue;
|
||||
|
@ -481,6 +514,12 @@ static CGImageRef __nullable SDCGImageCreateCopy(CGImageRef cg_nullable image) {
|
|||
preserveAspectRatio = preserveAspectRatioValue.boolValue;
|
||||
}
|
||||
_preserveAspectRatio = preserveAspectRatio;
|
||||
NSUInteger limitBytes = 0;
|
||||
NSNumber *limitBytesValue = options[SDImageCoderDecodeScaleDownLimitBytes];
|
||||
if (limitBytesValue != nil) {
|
||||
limitBytes = limitBytesValue.unsignedIntegerValue;
|
||||
}
|
||||
_limitBytes = limitBytes;
|
||||
BOOL lazyDecode = NO; // Defaults NO for animated image coder
|
||||
NSNumber *lazyDecodeValue = options[SDImageCoderDecodeUseLazyDecoding];
|
||||
if (lazyDecodeValue != nil) {
|
||||
|
@ -524,6 +563,16 @@ static CGImageRef __nullable SDCGImageCreateCopy(CGImageRef cg_nullable image) {
|
|||
// For animated image progressive decoding because the frame count and duration may be changed.
|
||||
[self scanAndCheckFramesValidWithImageSource:_imageSource];
|
||||
SD_UNLOCK(_lock);
|
||||
|
||||
// Scale down to limit bytes if need
|
||||
if (_limitBytes > 0) {
|
||||
// Hack since ImageIO public API (not CGImageDecompressor/CMPhoto) always return back RGBA8888 CGImage
|
||||
CGSize imageSize = CGSizeMake(_width, _height);
|
||||
CGSize framePixelSize = SDCalculateScaleDownPixelSize(_limitBytes, imageSize, _frameCount, 4);
|
||||
// Override thumbnail size
|
||||
_thumbnailSize = framePixelSize;
|
||||
_preserveAspectRatio = YES;
|
||||
}
|
||||
}
|
||||
|
||||
- (UIImage *)incrementalDecodedImageWithOptions:(SDImageCoderOptions *)options {
|
||||
|
@ -710,6 +759,25 @@ static CGImageRef __nullable SDCGImageCreateCopy(CGImageRef cg_nullable image) {
|
|||
preserveAspectRatio = preserveAspectRatioValue.boolValue;
|
||||
}
|
||||
_preserveAspectRatio = preserveAspectRatio;
|
||||
NSUInteger limitBytes = 0;
|
||||
NSNumber *limitBytesValue = options[SDImageCoderDecodeScaleDownLimitBytes];
|
||||
if (limitBytesValue != nil) {
|
||||
limitBytes = limitBytesValue.unsignedIntegerValue;
|
||||
}
|
||||
_limitBytes = limitBytes;
|
||||
// Parse the image properties
|
||||
NSDictionary *properties = (__bridge_transfer NSDictionary *)CGImageSourceCopyPropertiesAtIndex(imageSource, 0, NULL);
|
||||
_width = [properties[(__bridge NSString *)kCGImagePropertyPixelWidth] doubleValue];
|
||||
_height = [properties[(__bridge NSString *)kCGImagePropertyPixelHeight] doubleValue];
|
||||
// Scale down to limit bytes if need
|
||||
if (_limitBytes > 0) {
|
||||
// Hack since ImageIO public API (not CGImageDecompressor/CMPhoto) always return back RGBA8888 CGImage
|
||||
CGSize imageSize = CGSizeMake(_width, _height);
|
||||
CGSize framePixelSize = SDCalculateScaleDownPixelSize(_limitBytes, imageSize, _frameCount, 4);
|
||||
// Override thumbnail size
|
||||
_thumbnailSize = framePixelSize;
|
||||
_preserveAspectRatio = YES;
|
||||
}
|
||||
BOOL lazyDecode = NO; // Defaults NO for animated image coder
|
||||
NSNumber *lazyDecodeValue = options[SDImageCoderDecodeUseLazyDecoding];
|
||||
if (lazyDecodeValue != nil) {
|
||||
|
|
|
@ -127,9 +127,9 @@ typedef NS_OPTIONS(NSUInteger, SDWebImageOptions) {
|
|||
* 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.
|
||||
* (from 5.16.0) This will actually translate to use context option `SDWebImageContextImageScaleDownLimitBytes`, which check and calculate the thumbnail pixel size occupied small than limit bytes (including animated image)
|
||||
* (from 5.5.0) This flags effect the progressive and animated images as well
|
||||
* @note If you need detail controls, it's better to use context option `imageScaleDownBytes` instead.
|
||||
*/
|
||||
SDWebImageScaleDownLargeImages = 1 << 11,
|
||||
|
||||
|
@ -293,6 +293,18 @@ FOUNDATION_EXPORT SDWebImageContextOption _Nonnull const SDWebImageContextImageT
|
|||
*/
|
||||
FOUNDATION_EXPORT SDWebImageContextOption _Nonnull const SDWebImageContextImageTypeIdentifierHint;
|
||||
|
||||
/**
|
||||
A NSUInteger value to provide the limit bytes during decoding. This can help to avoid OOM on large frame count animated image or large pixel static image when you don't know how much RAM it occupied before decoding
|
||||
The decoder will do these logic based on limit bytes:
|
||||
1. Get the total frame count (static image means 1)
|
||||
2. Calculate the `framePixelSize` width/height to `sqrt(limitBytes / frameCount / bytesPerPixel)`, keeping aspect ratio (at least 1x1)
|
||||
3. If the `framePixelSize < originalImagePixelSize`, then do thumbnail decoding (see `SDImageCoderDecodeThumbnailPixelSize`) use the `framePixelSize` and `preseveAspectRatio = YES`
|
||||
4. Else, use the full pixel decoding (small than limit bytes)
|
||||
5. Whatever result, this does not effect the animated/static behavior of image. So even if you set `limitBytes = 1 && frameCount = 100`, we will stll create animated image with each frame `1x1` pixel size.
|
||||
@note This option has higher priority than `.imageThumbnailPixelSize`
|
||||
*/
|
||||
FOUNDATION_EXPORT SDWebImageContextOption _Nonnull const SDWebImageContextImageScaleDownLimitBytes;
|
||||
|
||||
#pragma mark - Cache Context Options
|
||||
|
||||
/**
|
||||
|
|
|
@ -156,6 +156,7 @@ SDWebImageContextOption const SDWebImageContextImageScaleFactor = @"imageScaleFa
|
|||
SDWebImageContextOption const SDWebImageContextImagePreserveAspectRatio = @"imagePreserveAspectRatio";
|
||||
SDWebImageContextOption const SDWebImageContextImageThumbnailPixelSize = @"imageThumbnailPixelSize";
|
||||
SDWebImageContextOption const SDWebImageContextImageTypeIdentifierHint = @"imageTypeIdentifierHint";
|
||||
SDWebImageContextOption const SDWebImageContextImageScaleDownLimitBytes = @"imageScaleDownLimitBytes";
|
||||
SDWebImageContextOption const SDWebImageContextImageEncodeOptions = @"imageEncodeOptions";
|
||||
SDWebImageContextOption const SDWebImageContextQueryCacheType = @"queryCacheType";
|
||||
SDWebImageContextOption const SDWebImageContextStoreCacheType = @"storeCacheType";
|
||||
|
|
Loading…
Reference in New Issue