Merge pull request #2972 from dreampiggy/feature_encoding_options_thumnail_background

Feature supports encoding options like max file size, max pixel size, as well as background color when using JPEG for alpha image
This commit is contained in:
DreamPiggy 2020-04-04 16:50:45 +08:00 committed by GitHub
commit ca975a3c46
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 172 additions and 4 deletions

View File

@ -37,6 +37,7 @@ FOUNDATION_EXPORT SDImageCoderOption _Nonnull const SDImageCoderDecodePreserveAs
/**
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 Supports for animated image as well.
@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`.
*/
@ -55,6 +56,28 @@ FOUNDATION_EXPORT SDImageCoderOption _Nonnull const SDImageCoderEncodeFirstFrame
*/
FOUNDATION_EXPORT SDImageCoderOption _Nonnull const SDImageCoderEncodeCompressionQuality;
/**
A UIColor(NSColor) value to used for non-alpha image encoding when the input image has alpha channel, the background color will be used to compose the alpha one. If not provide, use white color.
@note works for `SDImageCoder`
*/
FOUNDATION_EXPORT SDImageCoderOption _Nonnull const SDImageCoderEncodeBackgroundColor;
/**
A CGSize value indicating the max image resolution in pixels during encoding. For vector image, this also effect the output vector data information about width and height. The encoder will not generate the encoded image larger than this limit. Note it always use the aspect ratio of input image..
Defaults to CGSizeZero, which means no max size limit at all.
@note Supports for animated image as well.
@note The ouput image's width is limited to pixel size's width, the output 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`
*/
FOUNDATION_EXPORT SDImageCoderOption _Nonnull const SDImageCoderEncodeMaxPixelSize;
/**
A NSUInteger value specify the max ouput data bytes size after encoding. Some lossy format like JPEG/HEIF supports the hint for codec to automatically reduce the quality and match the file size you want. Note this option will override the `SDImageCoderEncodeCompressionQuality`, because now the quality is decided by the encoder. (NSNumber)
@note This is a hint, no gurantee for output size because of compression algorithm limit. And this options does not works for vector images.
@note works for `SDImageCoder`
*/
FOUNDATION_EXPORT SDImageCoderOption _Nonnull const SDImageCoderEncodeMaxFileSize;
/**
A SDWebImageContext object which hold the original context options from top-level API. (SDWebImageContext)
This option is ignored for all built-in coders and take no effect.

View File

@ -15,5 +15,8 @@ SDImageCoderOption const SDImageCoderDecodeThumbnailPixelSize = @"decodeThumbnai
SDImageCoderOption const SDImageCoderEncodeFirstFrameOnly = @"encodeFirstFrameOnly";
SDImageCoderOption const SDImageCoderEncodeCompressionQuality = @"encodeCompressionQuality";
SDImageCoderOption const SDImageCoderEncodeBackgroundColor = @"encodeBackgroundColor";
SDImageCoderOption const SDImageCoderEncodeMaxPixelSize = @"encodeMaxPixelSize";
SDImageCoderOption const SDImageCoderEncodeMaxFileSize = @"encodeMaxFileSize";
SDImageCoderOption const SDImageCoderWebImageContext = @"webImageContext";

View File

@ -16,6 +16,8 @@
// Specify DPI for vector format in CGImageSource, like PDF
static NSString * kSDCGImageSourceRasterizationDPI = @"kCGImageSourceRasterizationDPI";
// Specify File Size for lossy format encoding, like JPEG
static NSString * kSDCGImageDestinationRequestedFileSize = @"kCGImageDestinationRequestedFileSize";
@interface SDImageIOCoderFrame : NSObject
@ -409,6 +411,11 @@ static NSString * kSDCGImageSourceRasterizationDPI = @"kCGImageSourceRasterizati
if (!image) {
return nil;
}
CGImageRef imageRef = image.CGImage;
if (!imageRef) {
// Earily return, supports CGImage only
return nil;
}
if (format != self.class.imageFormat) {
return nil;
@ -426,16 +433,51 @@ static NSString * kSDCGImageSourceRasterizationDPI = @"kCGImageSourceRasterizati
return nil;
}
NSMutableDictionary *properties = [NSMutableDictionary dictionary];
// Encoding Options
double compressionQuality = 1;
if (options[SDImageCoderEncodeCompressionQuality]) {
compressionQuality = [options[SDImageCoderEncodeCompressionQuality] doubleValue];
}
properties[(__bridge NSString *)kCGImageDestinationLossyCompressionQuality] = @(compressionQuality);
CGColorRef backgroundColor = [options[SDImageCoderEncodeBackgroundColor] CGColor];
if (backgroundColor) {
properties[(__bridge NSString *)kCGImageDestinationBackgroundColor] = (__bridge id)(backgroundColor);
}
CGSize maxPixelSize = CGSizeZero;
NSValue *maxPixelSizeValue = options[SDImageCoderEncodeMaxPixelSize];
if (maxPixelSizeValue != nil) {
#if SD_MAC
maxPixelSize = maxPixelSizeValue.sizeValue;
#else
maxPixelSize = maxPixelSizeValue.CGSizeValue;
#endif
}
NSUInteger pixelWidth = CGImageGetWidth(imageRef);
NSUInteger pixelHeight = CGImageGetHeight(imageRef);
CGFloat finalPixelSize = 0;
if (maxPixelSize.width > 0 && maxPixelSize.height > 0 && pixelWidth > 0 && pixelHeight > 0) {
CGFloat pixelRatio = pixelWidth / pixelHeight;
CGFloat maxPixelSizeRatio = maxPixelSize.width / maxPixelSize.height;
if (pixelRatio > maxPixelSizeRatio) {
finalPixelSize = maxPixelSize.width;
} else {
finalPixelSize = maxPixelSize.height;
}
}
NSUInteger maxFileSize = [options[SDImageCoderEncodeMaxFileSize] unsignedIntegerValue];
if (maxFileSize > 0) {
properties[kSDCGImageDestinationRequestedFileSize] = @(maxFileSize);
// Remove the quality if we have file size limit
properties[(__bridge NSString *)kCGImageDestinationLossyCompressionQuality] = nil;
}
BOOL encodeFirstFrame = [options[SDImageCoderEncodeFirstFrameOnly] boolValue];
if (encodeFirstFrame || frames.count == 0) {
// for static single images
CGImageDestinationAddImage(imageDestination, image.CGImage, (__bridge CFDictionaryRef)properties);
if (finalPixelSize > 0) {
properties[(__bridge NSString *)kCGImageDestinationImageMaxPixelSize] = @(finalPixelSize);
}
CGImageDestinationAddImage(imageDestination, imageRef, (__bridge CFDictionaryRef)properties);
} else {
// for animated images
NSUInteger loopCount = image.sd_imageLoopCount;
@ -447,7 +489,11 @@ static NSString * kSDCGImageSourceRasterizationDPI = @"kCGImageSourceRasterizati
SDImageFrame *frame = frames[i];
NSTimeInterval frameDuration = frame.duration;
CGImageRef frameImageRef = frame.image.CGImage;
NSDictionary *frameProperties = @{self.class.dictionaryProperty : @{self.class.delayTimeProperty : @(frameDuration)}};
NSMutableDictionary *frameProperties = [NSMutableDictionary dictionary];
frameProperties[self.class.dictionaryProperty] = @{self.class.delayTimeProperty : @(frameDuration)};
if (finalPixelSize > 0) {
frameProperties[(__bridge NSString *)kCGImageDestinationImageMaxPixelSize] = @(finalPixelSize);
}
CGImageDestinationAddImage(imageDestination, frameImageRef, (__bridge CFDictionaryRef)frameProperties);
}
}

View File

@ -14,6 +14,9 @@
#import "SDImageHEICCoderInternal.h"
#import "SDImageIOAnimatedCoderInternal.h"
// Specify File Size for lossy format encoding, like JPEG
static NSString * kSDCGImageDestinationRequestedFileSize = @"kCGImageDestinationRequestedFileSize";
@implementation SDImageIOCoder {
size_t _width, _height;
CGImagePropertyOrientation _orientation;
@ -221,9 +224,14 @@
if (!image) {
return nil;
}
CGImageRef imageRef = image.CGImage;
if (!imageRef) {
// Earily return, supports CGImage only
return nil;
}
if (format == SDImageFormatUndefined) {
BOOL hasAlpha = [SDImageCoderHelper CGImageContainsAlpha:image.CGImage];
BOOL hasAlpha = [SDImageCoderHelper CGImageContainsAlpha:imageRef];
if (hasAlpha) {
format = SDImageFormatPNG;
} else {
@ -248,14 +256,47 @@
CGImagePropertyOrientation exifOrientation = kCGImagePropertyOrientationUp;
#endif
properties[(__bridge NSString *)kCGImagePropertyOrientation] = @(exifOrientation);
// Encoding Options
double compressionQuality = 1;
if (options[SDImageCoderEncodeCompressionQuality]) {
compressionQuality = [options[SDImageCoderEncodeCompressionQuality] doubleValue];
}
properties[(__bridge NSString *)kCGImageDestinationLossyCompressionQuality] = @(compressionQuality);
CGColorRef backgroundColor = [options[SDImageCoderEncodeBackgroundColor] CGColor];
if (backgroundColor) {
properties[(__bridge NSString *)kCGImageDestinationBackgroundColor] = (__bridge id)(backgroundColor);
}
CGSize maxPixelSize = CGSizeZero;
NSValue *maxPixelSizeValue = options[SDImageCoderEncodeMaxPixelSize];
if (maxPixelSizeValue != nil) {
#if SD_MAC
maxPixelSize = maxPixelSizeValue.sizeValue;
#else
maxPixelSize = maxPixelSizeValue.CGSizeValue;
#endif
}
NSUInteger pixelWidth = CGImageGetWidth(imageRef);
NSUInteger pixelHeight = CGImageGetHeight(imageRef);
if (maxPixelSize.width > 0 && maxPixelSize.height > 0 && pixelWidth > 0 && pixelHeight > 0) {
CGFloat pixelRatio = pixelWidth / pixelHeight;
CGFloat maxPixelSizeRatio = maxPixelSize.width / maxPixelSize.height;
CGFloat finalPixelSize;
if (pixelRatio > maxPixelSizeRatio) {
finalPixelSize = maxPixelSize.width;
} else {
finalPixelSize = maxPixelSize.height;
}
properties[(__bridge NSString *)kCGImageDestinationImageMaxPixelSize] = @(finalPixelSize);
}
NSUInteger maxFileSize = [options[SDImageCoderEncodeMaxFileSize] unsignedIntegerValue];
if (maxFileSize > 0) {
properties[kSDCGImageDestinationRequestedFileSize] = @(maxFileSize);
// Remove the quality if we have file size limit
properties[(__bridge NSString *)kCGImageDestinationLossyCompressionQuality] = nil;
}
// Add your image to the destination.
CGImageDestinationAddImage(imageDestination, image.CGImage, (__bridge CFDictionaryRef)properties);
CGImageDestinationAddImage(imageDestination, imageRef, (__bridge CFDictionaryRef)properties);
// Finalize the destination.
if (CGImageDestinationFinalize(imageDestination) == NO) {

View File

@ -8,6 +8,7 @@
*/
#import "SDTestCase.h"
#import "UIColor+HexString.h"
@interface SDWebImageDecoderTests : SDTestCase
@ -82,6 +83,41 @@
expect(decodedImage.size.height).to.equal(image.size.height);
}
- (void)test08ThatEncodeAlphaImageToJPGWithBackgroundColor {
NSString * testImagePath = [[NSBundle bundleForClass:[self class]] pathForResource:@"TestImage" ofType:@"png"];
UIImage *image = [[UIImage alloc] initWithContentsOfFile:testImagePath];
UIColor *backgroundColor = [UIColor blackColor];
NSData *encodedData = [SDImageCodersManager.sharedManager encodedDataWithImage:image format:SDImageFormatJPEG options:@{SDImageCoderEncodeBackgroundColor : backgroundColor}];
expect(encodedData).notTo.beNil();
UIImage *decodedImage = [SDImageCodersManager.sharedManager decodedImageWithData:encodedData options:nil];
expect(decodedImage).notTo.beNil();
expect(decodedImage.size.width).to.equal(image.size.width);
expect(decodedImage.size.height).to.equal(image.size.height);
// Check background color, should not be white but the black color
UIColor *testColor = [decodedImage sd_colorAtPoint:CGPointMake(1, 1)];
expect(testColor.sd_hexString).equal(backgroundColor.sd_hexString);
}
- (void)test09ThatJPGImageEncodeWithMaxFileSize {
NSString * testImagePath = [[NSBundle bundleForClass:[self class]] pathForResource:@"TestImageLarge" ofType:@"jpg"];
UIImage *image = [[UIImage alloc] initWithContentsOfFile:testImagePath];
// This large JPEG encoding size between (770KB ~ 2.23MB)
NSUInteger limitFileSize = 1 * 1024 * 1024; // 1MB
// 100 quality (biggest)
NSData *maxEncodedData = [SDImageCodersManager.sharedManager encodedDataWithImage:image format:SDImageFormatJPEG options:nil];
expect(maxEncodedData).notTo.beNil();
expect(maxEncodedData.length).beGreaterThan(limitFileSize);
// 0 quality (smallest)
NSData *minEncodedData = [SDImageCodersManager.sharedManager encodedDataWithImage:image format:SDImageFormatJPEG options:@{SDImageCoderEncodeCompressionQuality : @(0)}];
expect(minEncodedData).notTo.beNil();
expect(minEncodedData.length).beLessThan(limitFileSize);
NSData *limitEncodedData = [SDImageCodersManager.sharedManager encodedDataWithImage:image format:SDImageFormatJPEG options:@{SDImageCoderEncodeMaxFileSize : @(limitFileSize)}];
expect(limitEncodedData).notTo.beNil();
// So, if we limit the file size, the output data should in (770KB ~ 2.23MB)
expect(limitEncodedData.length).beLessThan(maxEncodedData.length);
expect(limitEncodedData.length).beGreaterThan(minEncodedData.length);
}
- (void)test11ThatAPNGPCoderWorks {
NSURL *APNGURL = [[NSBundle bundleForClass:[self class]] URLForResource:@"TestImageAnimated" withExtension:@"apng"];
[self verifyCoder:[SDImageAPNGCoder sharedCoder]
@ -271,6 +307,25 @@ withLocalImageURL:(NSURL *)imageUrl
#if SD_UIKIT
expect(outputImage.images.count).to.equal(inputImage.images.count);
#endif
// check max pixel size encoding with scratch
CGFloat maxWidth = 50;
CGFloat maxHeight = 50;
CGFloat maxRatio = maxWidth / maxHeight;
CGSize maxPixelSize;
if (ratio > maxRatio) {
maxPixelSize = CGSizeMake(maxWidth, round(maxWidth / ratio));
} else {
maxPixelSize = CGSizeMake(round(maxHeight * ratio), maxHeight);
}
NSData *outputMaxImageData = [coder encodedDataWithImage:inputImage format:encodingFormat options:@{SDImageCoderEncodeMaxPixelSize : @(CGSizeMake(maxWidth, maxHeight))}];
UIImage *outputMaxImage = [coder decodedImageWithData:outputMaxImageData options:nil];
// Image/IO's thumbnail API does not always use round to preserve precision, we check ABS <= 1
expect(ABS(outputMaxImage.size.width - maxPixelSize.width) <= 1);
expect(ABS(outputMaxImage.size.height - maxPixelSize.height) <= 1);
#if SD_UIKIT
expect(outputMaxImage.images.count).to.equal(inputImage.images.count);
#endif
}
}