Merge pull request #3469 from dreampiggy/feat/animated_image_encode
Added encodeWithFrames API for animation encoding in custom coder, better for usage
This commit is contained in:
commit
975502b4a6
|
@ -16,6 +16,11 @@
|
||||||
#import "SDImageHEICCoder.h"
|
#import "SDImageHEICCoder.h"
|
||||||
#import "SDImageAWebPCoder.h"
|
#import "SDImageAWebPCoder.h"
|
||||||
|
|
||||||
|
@interface SDAnimatedImageRep ()
|
||||||
|
/// This wrap the animated image frames for legacy animated image coder API (`encodedDataWithImage:`).
|
||||||
|
@property (nonatomic, readwrite, weak) NSArray<SDImageFrame *> *frames;
|
||||||
|
@end
|
||||||
|
|
||||||
@implementation SDAnimatedImageRep {
|
@implementation SDAnimatedImageRep {
|
||||||
CGImageSourceRef _imageSource;
|
CGImageSourceRef _imageSource;
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,6 +9,7 @@
|
||||||
#import <Foundation/Foundation.h>
|
#import <Foundation/Foundation.h>
|
||||||
#import "SDWebImageCompat.h"
|
#import "SDWebImageCompat.h"
|
||||||
#import "NSData+ImageContentType.h"
|
#import "NSData+ImageContentType.h"
|
||||||
|
#import "SDImageFrame.h"
|
||||||
|
|
||||||
typedef NSString * SDImageCoderOption NS_STRING_ENUM;
|
typedef NSString * SDImageCoderOption NS_STRING_ENUM;
|
||||||
typedef NSDictionary<SDImageCoderOption, id> SDImageCoderOptions;
|
typedef NSDictionary<SDImageCoderOption, id> SDImageCoderOptions;
|
||||||
|
@ -171,7 +172,8 @@ FOUNDATION_EXPORT SDImageCoderOption _Nonnull const SDImageCoderWebImageContext
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Encode the image to image data.
|
Encode the image to image data.
|
||||||
@note This protocol may supports encode animated image frames. You can use `+[SDImageCoderHelper framesFromAnimatedImage:]` to assemble an animated image with frames.
|
@note This protocol may supports encode animated image frames. You can use `+[SDImageCoderHelper framesFromAnimatedImage:]` to assemble an animated image with frames. But this consume time is not always reversible. In 5.15.0, we introduce `encodedDataWithFrames` API for better animated image encoding. Use that instead.
|
||||||
|
@note Which means, this just forward to `encodedDataWithFrames([SDImageFrame(image: image, duration: 0], image.sd_imageLoopCount))`
|
||||||
|
|
||||||
@param image The image to be encoded
|
@param image The image to be encoded
|
||||||
@param format The image format to encode, you should note `SDImageFormatUndefined` format is also possible
|
@param format The image format to encode, you should note `SDImageFormatUndefined` format is also possible
|
||||||
|
@ -182,6 +184,21 @@ FOUNDATION_EXPORT SDImageCoderOption _Nonnull const SDImageCoderWebImageContext
|
||||||
format:(SDImageFormat)format
|
format:(SDImageFormat)format
|
||||||
options:(nullable SDImageCoderOptions *)options;
|
options:(nullable SDImageCoderOptions *)options;
|
||||||
|
|
||||||
|
#pragma mark - Animated Encoding
|
||||||
|
@optional
|
||||||
|
/**
|
||||||
|
Encode the animated image frames to image data.
|
||||||
|
|
||||||
|
@param frames The animated image frames to be encoded, should be at least 1 element, or it will fallback to static image encode.
|
||||||
|
@param loopCount The final animated image loop count. 0 means infinity loop. This config ignore each frame's `sd_imageLoopCount`
|
||||||
|
@param format The image format to encode, you should note `SDImageFormatUndefined` format is also possible
|
||||||
|
@param options A dictionary containing any encoding options. Pass @{SDImageCoderEncodeCompressionQuality: @(1)} to specify compression quality.
|
||||||
|
@return The encoded image data
|
||||||
|
*/
|
||||||
|
- (nullable NSData *)encodedDataWithFrames:(nonnull NSArray<SDImageFrame *>*)frames
|
||||||
|
loopCount:(NSUInteger)loopCount
|
||||||
|
format:(SDImageFormat)format
|
||||||
|
options:(nullable SDImageCoderOptions *)options;
|
||||||
@end
|
@end
|
||||||
|
|
||||||
#pragma mark - Progressive Coder
|
#pragma mark - Progressive Coder
|
||||||
|
|
|
@ -94,6 +94,13 @@ static CGFloat kDestImageLimitBytes = 30.f * kBytesPerMB;
|
||||||
|
|
||||||
static const CGFloat kDestSeemOverlap = 2.0f; // the numbers of pixels to overlap the seems where tiles meet.
|
static const CGFloat kDestSeemOverlap = 2.0f; // the numbers of pixels to overlap the seems where tiles meet.
|
||||||
|
|
||||||
|
#if SD_MAC
|
||||||
|
@interface SDAnimatedImageRep (Private)
|
||||||
|
/// This wrap the animated image frames for legacy animated image coder API (`encodedDataWithImage:`).
|
||||||
|
@property (nonatomic, readwrite, weak) NSArray<SDImageFrame *> *frames;
|
||||||
|
@end
|
||||||
|
#endif
|
||||||
|
|
||||||
@implementation SDImageCoderHelper
|
@implementation SDImageCoderHelper
|
||||||
|
|
||||||
+ (UIImage *)animatedImageWithFrames:(NSArray<SDImageFrame *> *)frames {
|
+ (UIImage *)animatedImageWithFrames:(NSArray<SDImageFrame *> *)frames {
|
||||||
|
@ -159,6 +166,7 @@ static const CGFloat kDestSeemOverlap = 2.0f; // the numbers of pixels to over
|
||||||
SDAnimatedImageRep *imageRep = [[SDAnimatedImageRep alloc] initWithData:imageData];
|
SDAnimatedImageRep *imageRep = [[SDAnimatedImageRep alloc] initWithData:imageData];
|
||||||
NSSize size = NSMakeSize(imageRep.pixelsWide / scale, imageRep.pixelsHigh / scale);
|
NSSize size = NSMakeSize(imageRep.pixelsWide / scale, imageRep.pixelsHigh / scale);
|
||||||
imageRep.size = size;
|
imageRep.size = size;
|
||||||
|
imageRep.frames = frames; // Weak assign to avoid effect lazy semantic of NSBitmapImageRep
|
||||||
animatedImage = [[NSImage alloc] initWithSize:size];
|
animatedImage = [[NSImage alloc] initWithSize:size];
|
||||||
[animatedImage addRepresentation:imageRep];
|
[animatedImage addRepresentation:imageRep];
|
||||||
#endif
|
#endif
|
||||||
|
@ -211,6 +219,14 @@ static const CGFloat kDestSeemOverlap = 2.0f; // the numbers of pixels to over
|
||||||
|
|
||||||
NSRect imageRect = NSMakeRect(0, 0, animatedImage.size.width, animatedImage.size.height);
|
NSRect imageRect = NSMakeRect(0, 0, animatedImage.size.width, animatedImage.size.height);
|
||||||
NSImageRep *imageRep = [animatedImage bestRepresentationForRect:imageRect context:nil hints:nil];
|
NSImageRep *imageRep = [animatedImage bestRepresentationForRect:imageRect context:nil hints:nil];
|
||||||
|
// Check weak assigned frames firstly
|
||||||
|
if ([imageRep isKindOfClass:[SDAnimatedImageRep class]]) {
|
||||||
|
SDAnimatedImageRep *animatedImageRep = (SDAnimatedImageRep *)imageRep;
|
||||||
|
if (animatedImageRep.frames) {
|
||||||
|
return animatedImageRep.frames;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
NSBitmapImageRep *bitmapImageRep;
|
NSBitmapImageRep *bitmapImageRep;
|
||||||
if ([imageRep isKindOfClass:[NSBitmapImageRep class]]) {
|
if ([imageRep isKindOfClass:[NSBitmapImageRep class]]) {
|
||||||
bitmapImageRep = (NSBitmapImageRep *)imageRep;
|
bitmapImageRep = (NSBitmapImageRep *)imageRep;
|
||||||
|
@ -235,7 +251,7 @@ static const CGFloat kDestSeemOverlap = 2.0f; // the numbers of pixels to over
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
return frames;
|
return [frames copy];
|
||||||
}
|
}
|
||||||
|
|
||||||
+ (CGColorSpaceRef)colorSpaceGetDeviceRGB {
|
+ (CGColorSpaceRef)colorSpaceGetDeviceRGB {
|
||||||
|
|
|
@ -127,4 +127,19 @@
|
||||||
return nil;
|
return nil;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
- (NSData *)encodedDataWithFrames:(NSArray<SDImageFrame *> *)frames loopCount:(NSUInteger)loopCount format:(SDImageFormat)format options:(SDImageCoderOptions *)options {
|
||||||
|
if (!frames || frames.count < 1) {
|
||||||
|
return nil;
|
||||||
|
}
|
||||||
|
NSArray<id<SDImageCoder>> *coders = self.coders;
|
||||||
|
for (id<SDImageCoder> coder in coders.reverseObjectEnumerator) {
|
||||||
|
if ([coder canEncodeToFormat:format]) {
|
||||||
|
if ([coder respondsToSelector:@selector(encodedDataWithFrames:loopCount:format:options:)]) {
|
||||||
|
return [coder encodedDataWithFrames:frames loopCount:loopCount format:format options:options];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil;
|
||||||
|
}
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|
|
@ -24,6 +24,11 @@
|
||||||
*/
|
*/
|
||||||
@property (nonatomic, readonly, assign) NSTimeInterval duration;
|
@property (nonatomic, readonly, assign) NSTimeInterval duration;
|
||||||
|
|
||||||
|
/// Create a frame instance with specify image and duration
|
||||||
|
/// @param image current frame's image
|
||||||
|
/// @param duration current frame's duration
|
||||||
|
- (nonnull instancetype)initWithImage:(nonnull UIImage *)image duration:(NSTimeInterval)duration;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Create a frame instance with specify image and duration
|
Create a frame instance with specify image and duration
|
||||||
|
|
||||||
|
@ -31,6 +36,9 @@
|
||||||
@param duration current frame's duration
|
@param duration current frame's duration
|
||||||
@return frame instance
|
@return frame instance
|
||||||
*/
|
*/
|
||||||
+ (instancetype _Nonnull)frameWithImage:(UIImage * _Nonnull)image duration:(NSTimeInterval)duration;
|
+ (nonnull instancetype)frameWithImage:(nonnull UIImage *)image duration:(NSTimeInterval)duration;
|
||||||
|
|
||||||
|
- (nonnull instancetype)init NS_UNAVAILABLE;
|
||||||
|
+ (nonnull instancetype)new NS_UNAVAILABLE;
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|
|
@ -17,11 +17,17 @@
|
||||||
|
|
||||||
@implementation SDImageFrame
|
@implementation SDImageFrame
|
||||||
|
|
||||||
|
- (instancetype)initWithImage:(UIImage *)image duration:(NSTimeInterval)duration {
|
||||||
|
self = [super init];
|
||||||
|
if (self) {
|
||||||
|
_image = image;
|
||||||
|
_duration = duration;
|
||||||
|
}
|
||||||
|
return self;
|
||||||
|
}
|
||||||
|
|
||||||
+ (instancetype)frameWithImage:(UIImage *)image duration:(NSTimeInterval)duration {
|
+ (instancetype)frameWithImage:(UIImage *)image duration:(NSTimeInterval)duration {
|
||||||
SDImageFrame *frame = [[SDImageFrame alloc] init];
|
SDImageFrame *frame = [[SDImageFrame alloc] initWithImage:image duration:duration];
|
||||||
frame.image = image;
|
|
||||||
frame.duration = duration;
|
|
||||||
|
|
||||||
return frame;
|
return frame;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -552,6 +552,23 @@ static CGImageRef __nullable SDCGImageCreateCopy(CGImageRef cg_nullable image) {
|
||||||
}
|
}
|
||||||
|
|
||||||
- (NSData *)encodedDataWithImage:(UIImage *)image format:(SDImageFormat)format options:(nullable SDImageCoderOptions *)options {
|
- (NSData *)encodedDataWithImage:(UIImage *)image format:(SDImageFormat)format options:(nullable SDImageCoderOptions *)options {
|
||||||
|
if (!image) {
|
||||||
|
return nil;
|
||||||
|
}
|
||||||
|
if (format != self.class.imageFormat) {
|
||||||
|
return nil;
|
||||||
|
}
|
||||||
|
|
||||||
|
NSArray<SDImageFrame *> *frames = [SDImageCoderHelper framesFromAnimatedImage:image];
|
||||||
|
if (!frames || frames.count == 0) {
|
||||||
|
SDImageFrame *frame = [SDImageFrame frameWithImage:image duration:0];
|
||||||
|
frames = @[frame];
|
||||||
|
}
|
||||||
|
return [self encodedDataWithFrames:frames loopCount:image.sd_imageLoopCount format:format options:options];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (NSData *)encodedDataWithFrames:(NSArray<SDImageFrame *> *)frames loopCount:(NSUInteger)loopCount format:(SDImageFormat)format options:(SDImageCoderOptions *)options {
|
||||||
|
UIImage *image = frames.firstObject.image; // Primary image
|
||||||
if (!image) {
|
if (!image) {
|
||||||
return nil;
|
return nil;
|
||||||
}
|
}
|
||||||
|
@ -561,13 +578,8 @@ static CGImageRef __nullable SDCGImageCreateCopy(CGImageRef cg_nullable image) {
|
||||||
return nil;
|
return nil;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (format != self.class.imageFormat) {
|
|
||||||
return nil;
|
|
||||||
}
|
|
||||||
|
|
||||||
NSMutableData *imageData = [NSMutableData data];
|
NSMutableData *imageData = [NSMutableData data];
|
||||||
CFStringRef imageUTType = [NSData sd_UTTypeFromImageFormat:format];
|
CFStringRef imageUTType = [NSData sd_UTTypeFromImageFormat:format];
|
||||||
NSArray<SDImageFrame *> *frames = [SDImageCoderHelper framesFromAnimatedImage:image];
|
|
||||||
|
|
||||||
// Create an image destination. Animated Image does not support EXIF image orientation TODO
|
// Create an image destination. Animated Image does not support EXIF image orientation TODO
|
||||||
// The `CGImageDestinationCreateWithData` will log a warning when count is 0, use 1 instead.
|
// The `CGImageDestinationCreateWithData` will log a warning when count is 0, use 1 instead.
|
||||||
|
@ -630,12 +642,11 @@ static CGImageRef __nullable SDCGImageCreateCopy(CGImageRef cg_nullable image) {
|
||||||
properties[(__bridge NSString *)kCGImageDestinationEmbedThumbnail] = @(embedThumbnail);
|
properties[(__bridge NSString *)kCGImageDestinationEmbedThumbnail] = @(embedThumbnail);
|
||||||
|
|
||||||
BOOL encodeFirstFrame = [options[SDImageCoderEncodeFirstFrameOnly] boolValue];
|
BOOL encodeFirstFrame = [options[SDImageCoderEncodeFirstFrameOnly] boolValue];
|
||||||
if (encodeFirstFrame || frames.count == 0) {
|
if (encodeFirstFrame || frames.count <= 1) {
|
||||||
// for static single images
|
// for static single images
|
||||||
CGImageDestinationAddImage(imageDestination, imageRef, (__bridge CFDictionaryRef)properties);
|
CGImageDestinationAddImage(imageDestination, imageRef, (__bridge CFDictionaryRef)properties);
|
||||||
} else {
|
} else {
|
||||||
// for animated images
|
// for animated images
|
||||||
NSUInteger loopCount = image.sd_imageLoopCount;
|
|
||||||
NSDictionary *containerProperties = @{
|
NSDictionary *containerProperties = @{
|
||||||
self.class.dictionaryProperty: @{self.class.loopCountProperty : @(loopCount)}
|
self.class.dictionaryProperty: @{self.class.loopCountProperty : @(loopCount)}
|
||||||
};
|
};
|
||||||
|
|
|
@ -140,7 +140,7 @@
|
||||||
expect(maxEncodedData).notTo.beNil();
|
expect(maxEncodedData).notTo.beNil();
|
||||||
expect(maxEncodedData.length).beGreaterThan(limitFileSize);
|
expect(maxEncodedData.length).beGreaterThan(limitFileSize);
|
||||||
// 0 quality (smallest)
|
// 0 quality (smallest)
|
||||||
NSData *minEncodedData = [SDImageCodersManager.sharedManager encodedDataWithImage:image format:SDImageFormatJPEG options:@{SDImageCoderEncodeCompressionQuality : @(0)}];
|
NSData *minEncodedData = [SDImageCodersManager.sharedManager encodedDataWithImage:image format:SDImageFormatJPEG options:@{SDImageCoderEncodeCompressionQuality : @(0.01)}]; // Seems 0 has some bugs in old macOS
|
||||||
expect(minEncodedData).notTo.beNil();
|
expect(minEncodedData).notTo.beNil();
|
||||||
expect(minEncodedData.length).beLessThan(limitFileSize);
|
expect(minEncodedData.length).beLessThan(limitFileSize);
|
||||||
NSData *limitEncodedData = [SDImageCodersManager.sharedManager encodedDataWithImage:image format:SDImageFormatJPEG options:@{SDImageCoderEncodeMaxFileSize : @(limitFileSize)}];
|
NSData *limitEncodedData = [SDImageCodersManager.sharedManager encodedDataWithImage:image format:SDImageFormatJPEG options:@{SDImageCoderEncodeMaxFileSize : @(limitFileSize)}];
|
||||||
|
@ -422,6 +422,32 @@
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
- (void)test27ThatEncodeWithFramesWorks {
|
||||||
|
// Mock
|
||||||
|
NSMutableArray<SDImageFrame *> *frames = [NSMutableArray array];
|
||||||
|
NSUInteger frameCount = 5;
|
||||||
|
for (size_t i = 0; i < frameCount; i++) {
|
||||||
|
CGSize size = CGSizeMake(100, 100);
|
||||||
|
SDGraphicsImageRenderer *renderer = [[SDGraphicsImageRenderer alloc] initWithSize:size];
|
||||||
|
UIImage *image = [renderer imageWithActions:^(CGContextRef _Nonnull context) {
|
||||||
|
CGContextSetRGBFillColor(context, 1.0 / i, 0.0, 0.0, 1.0);
|
||||||
|
CGContextSetRGBStrokeColor(context, 1.0 / i, 0.0, 0.0, 1.0);
|
||||||
|
CGContextFillRect(context, CGRectMake(0, 0, size.width, size.height));
|
||||||
|
}];
|
||||||
|
SDImageFrame *frame = [SDImageFrame frameWithImage:image duration:0.1];
|
||||||
|
[frames addObject:frame];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test old API
|
||||||
|
UIImage *animatedImage = [SDImageCoderHelper animatedImageWithFrames:frames];
|
||||||
|
NSData *data = [SDImageGIFCoder.sharedCoder encodedDataWithImage:animatedImage format:SDImageFormatGIF options:nil];
|
||||||
|
expect(data).notTo.beNil();
|
||||||
|
|
||||||
|
// Test new API
|
||||||
|
NSData *data2 = [SDImageGIFCoder.sharedCoder encodedDataWithFrames:frames loopCount:0 format:SDImageFormatGIF options:nil];
|
||||||
|
expect(data2).notTo.beNil();
|
||||||
|
}
|
||||||
|
|
||||||
#pragma mark - Utils
|
#pragma mark - Utils
|
||||||
|
|
||||||
- (void)verifyCoder:(id<SDImageCoder>)coder
|
- (void)verifyCoder:(id<SDImageCoder>)coder
|
||||||
|
|
Loading…
Reference in New Issue