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:
commit
ca975a3c46
|
@ -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.
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue