Introduce the automatically calculation of thumbnail (include animated/static image) using `SDImageCoderDecodeScaleDownLimitBytes`

The exist SDWebImageScaleDownLargeImages translate to new option instead
This commit is contained in:
DreamPiggy 2023-05-15 17:27:29 +08:00
parent d82f5574e4
commit 91ff7189c9
6 changed files with 108 additions and 12 deletions

View File

@ -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) {

View File

@ -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.

View File

@ -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";

View File

@ -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) {

View File

@ -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
/**

View File

@ -137,6 +137,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";