Skip to content

Commit 975502b

Browse files
authored
Merge pull request SDWebImage#3469 from dreampiggy/feat/animated_image_encode
Added encodeWithFrames API for animation encoding in custom coder, better for usage
2 parents a01715e + 9dd8b6c commit 975502b

File tree

8 files changed

+119
-15
lines changed

8 files changed

+119
-15
lines changed

SDWebImage/Core/SDAnimatedImageRep.m

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,11 @@
1616
#import "SDImageHEICCoder.h"
1717
#import "SDImageAWebPCoder.h"
1818

19+
@interface SDAnimatedImageRep ()
20+
/// This wrap the animated image frames for legacy animated image coder API (`encodedDataWithImage:`).
21+
@property (nonatomic, readwrite, weak) NSArray<SDImageFrame *> *frames;
22+
@end
23+
1924
@implementation SDAnimatedImageRep {
2025
CGImageSourceRef _imageSource;
2126
}

SDWebImage/Core/SDImageCoder.h

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
#import <Foundation/Foundation.h>
1010
#import "SDWebImageCompat.h"
1111
#import "NSData+ImageContentType.h"
12+
#import "SDImageFrame.h"
1213

1314
typedef NSString * SDImageCoderOption NS_STRING_ENUM;
1415
typedef NSDictionary<SDImageCoderOption, id> SDImageCoderOptions;
@@ -171,7 +172,8 @@ FOUNDATION_EXPORT SDImageCoderOption _Nonnull const SDImageCoderWebImageContext
171172

172173
/**
173174
Encode the image to image data.
174-
@note This protocol may supports encode animated image frames. You can use `+[SDImageCoderHelper framesFromAnimatedImage:]` to assemble an animated image with frames.
175+
@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.
176+
@note Which means, this just forward to `encodedDataWithFrames([SDImageFrame(image: image, duration: 0], image.sd_imageLoopCount))`
175177
176178
@param image The image to be encoded
177179
@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
182184
format:(SDImageFormat)format
183185
options:(nullable SDImageCoderOptions *)options;
184186

187+
#pragma mark - Animated Encoding
188+
@optional
189+
/**
190+
Encode the animated image frames to image data.
191+
192+
@param frames The animated image frames to be encoded, should be at least 1 element, or it will fallback to static image encode.
193+
@param loopCount The final animated image loop count. 0 means infinity loop. This config ignore each frame's `sd_imageLoopCount`
194+
@param format The image format to encode, you should note `SDImageFormatUndefined` format is also possible
195+
@param options A dictionary containing any encoding options. Pass @{SDImageCoderEncodeCompressionQuality: @(1)} to specify compression quality.
196+
@return The encoded image data
197+
*/
198+
- (nullable NSData *)encodedDataWithFrames:(nonnull NSArray<SDImageFrame *>*)frames
199+
loopCount:(NSUInteger)loopCount
200+
format:(SDImageFormat)format
201+
options:(nullable SDImageCoderOptions *)options;
185202
@end
186203

187204
#pragma mark - Progressive Coder

SDWebImage/Core/SDImageCoderHelper.m

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,13 @@ static inline BOOL SDImageSupportsHardwareHEVCDecoder(void) {
9494

9595
static const CGFloat kDestSeemOverlap = 2.0f; // the numbers of pixels to overlap the seems where tiles meet.
9696

97+
#if SD_MAC
98+
@interface SDAnimatedImageRep (Private)
99+
/// This wrap the animated image frames for legacy animated image coder API (`encodedDataWithImage:`).
100+
@property (nonatomic, readwrite, weak) NSArray<SDImageFrame *> *frames;
101+
@end
102+
#endif
103+
97104
@implementation SDImageCoderHelper
98105

99106
+ (UIImage *)animatedImageWithFrames:(NSArray<SDImageFrame *> *)frames {
@@ -159,6 +166,7 @@ + (UIImage *)animatedImageWithFrames:(NSArray<SDImageFrame *> *)frames {
159166
SDAnimatedImageRep *imageRep = [[SDAnimatedImageRep alloc] initWithData:imageData];
160167
NSSize size = NSMakeSize(imageRep.pixelsWide / scale, imageRep.pixelsHigh / scale);
161168
imageRep.size = size;
169+
imageRep.frames = frames; // Weak assign to avoid effect lazy semantic of NSBitmapImageRep
162170
animatedImage = [[NSImage alloc] initWithSize:size];
163171
[animatedImage addRepresentation:imageRep];
164172
#endif
@@ -211,6 +219,14 @@ + (UIImage *)animatedImageWithFrames:(NSArray<SDImageFrame *> *)frames {
211219

212220
NSRect imageRect = NSMakeRect(0, 0, animatedImage.size.width, animatedImage.size.height);
213221
NSImageRep *imageRep = [animatedImage bestRepresentationForRect:imageRect context:nil hints:nil];
222+
// Check weak assigned frames firstly
223+
if ([imageRep isKindOfClass:[SDAnimatedImageRep class]]) {
224+
SDAnimatedImageRep *animatedImageRep = (SDAnimatedImageRep *)imageRep;
225+
if (animatedImageRep.frames) {
226+
return animatedImageRep.frames;
227+
}
228+
}
229+
214230
NSBitmapImageRep *bitmapImageRep;
215231
if ([imageRep isKindOfClass:[NSBitmapImageRep class]]) {
216232
bitmapImageRep = (NSBitmapImageRep *)imageRep;
@@ -235,7 +251,7 @@ + (UIImage *)animatedImageWithFrames:(NSArray<SDImageFrame *> *)frames {
235251
}
236252
#endif
237253

238-
return frames;
254+
return [frames copy];
239255
}
240256

241257
+ (CGColorSpaceRef)colorSpaceGetDeviceRGB {

SDWebImage/Core/SDImageCodersManager.m

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,4 +127,19 @@ - (NSData *)encodedDataWithImage:(UIImage *)image format:(SDImageFormat)format o
127127
return nil;
128128
}
129129

130+
- (NSData *)encodedDataWithFrames:(NSArray<SDImageFrame *> *)frames loopCount:(NSUInteger)loopCount format:(SDImageFormat)format options:(SDImageCoderOptions *)options {
131+
if (!frames || frames.count < 1) {
132+
return nil;
133+
}
134+
NSArray<id<SDImageCoder>> *coders = self.coders;
135+
for (id<SDImageCoder> coder in coders.reverseObjectEnumerator) {
136+
if ([coder canEncodeToFormat:format]) {
137+
if ([coder respondsToSelector:@selector(encodedDataWithFrames:loopCount:format:options:)]) {
138+
return [coder encodedDataWithFrames:frames loopCount:loopCount format:format options:options];
139+
}
140+
}
141+
}
142+
return nil;
143+
}
144+
130145
@end

SDWebImage/Core/SDImageFrame.h

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,21 @@
2424
*/
2525
@property (nonatomic, readonly, assign) NSTimeInterval duration;
2626

27+
/// Create a frame instance with specify image and duration
28+
/// @param image current frame's image
29+
/// @param duration current frame's duration
30+
- (nonnull instancetype)initWithImage:(nonnull UIImage *)image duration:(NSTimeInterval)duration;
31+
2732
/**
2833
Create a frame instance with specify image and duration
2934
3035
@param image current frame's image
3136
@param duration current frame's duration
3237
@return frame instance
3338
*/
34-
+ (instancetype _Nonnull)frameWithImage:(UIImage * _Nonnull)image duration:(NSTimeInterval)duration;
39+
+ (nonnull instancetype)frameWithImage:(nonnull UIImage *)image duration:(NSTimeInterval)duration;
40+
41+
- (nonnull instancetype)init NS_UNAVAILABLE;
42+
+ (nonnull instancetype)new NS_UNAVAILABLE;
3543

3644
@end

SDWebImage/Core/SDImageFrame.m

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,17 @@ @interface SDImageFrame ()
1717

1818
@implementation SDImageFrame
1919

20+
- (instancetype)initWithImage:(UIImage *)image duration:(NSTimeInterval)duration {
21+
self = [super init];
22+
if (self) {
23+
_image = image;
24+
_duration = duration;
25+
}
26+
return self;
27+
}
28+
2029
+ (instancetype)frameWithImage:(UIImage *)image duration:(NSTimeInterval)duration {
21-
SDImageFrame *frame = [[SDImageFrame alloc] init];
22-
frame.image = image;
23-
frame.duration = duration;
24-
30+
SDImageFrame *frame = [[SDImageFrame alloc] initWithImage:image duration:duration];
2531
return frame;
2632
}
2733

SDWebImage/Core/SDImageIOAnimatedCoder.m

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -555,19 +555,31 @@ - (NSData *)encodedDataWithImage:(UIImage *)image format:(SDImageFormat)format o
555555
if (!image) {
556556
return nil;
557557
}
558-
CGImageRef imageRef = image.CGImage;
559-
if (!imageRef) {
560-
// Earily return, supports CGImage only
558+
if (format != self.class.imageFormat) {
561559
return nil;
562560
}
563561

564-
if (format != self.class.imageFormat) {
562+
NSArray<SDImageFrame *> *frames = [SDImageCoderHelper framesFromAnimatedImage:image];
563+
if (!frames || frames.count == 0) {
564+
SDImageFrame *frame = [SDImageFrame frameWithImage:image duration:0];
565+
frames = @[frame];
566+
}
567+
return [self encodedDataWithFrames:frames loopCount:image.sd_imageLoopCount format:format options:options];
568+
}
569+
570+
- (NSData *)encodedDataWithFrames:(NSArray<SDImageFrame *> *)frames loopCount:(NSUInteger)loopCount format:(SDImageFormat)format options:(SDImageCoderOptions *)options {
571+
UIImage *image = frames.firstObject.image; // Primary image
572+
if (!image) {
573+
return nil;
574+
}
575+
CGImageRef imageRef = image.CGImage;
576+
if (!imageRef) {
577+
// Earily return, supports CGImage only
565578
return nil;
566579
}
567580

568581
NSMutableData *imageData = [NSMutableData data];
569582
CFStringRef imageUTType = [NSData sd_UTTypeFromImageFormat:format];
570-
NSArray<SDImageFrame *> *frames = [SDImageCoderHelper framesFromAnimatedImage:image];
571583

572584
// Create an image destination. Animated Image does not support EXIF image orientation TODO
573585
// The `CGImageDestinationCreateWithData` will log a warning when count is 0, use 1 instead.
@@ -630,12 +642,11 @@ - (NSData *)encodedDataWithImage:(UIImage *)image format:(SDImageFormat)format o
630642
properties[(__bridge NSString *)kCGImageDestinationEmbedThumbnail] = @(embedThumbnail);
631643

632644
BOOL encodeFirstFrame = [options[SDImageCoderEncodeFirstFrameOnly] boolValue];
633-
if (encodeFirstFrame || frames.count == 0) {
645+
if (encodeFirstFrame || frames.count <= 1) {
634646
// for static single images
635647
CGImageDestinationAddImage(imageDestination, imageRef, (__bridge CFDictionaryRef)properties);
636648
} else {
637649
// for animated images
638-
NSUInteger loopCount = image.sd_imageLoopCount;
639650
NSDictionary *containerProperties = @{
640651
self.class.dictionaryProperty: @{self.class.loopCountProperty : @(loopCount)}
641652
};

Tests/Tests/SDImageCoderTests.m

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,7 @@ - (void)test09ThatJPGImageEncodeWithMaxFileSize {
140140
expect(maxEncodedData).notTo.beNil();
141141
expect(maxEncodedData.length).beGreaterThan(limitFileSize);
142142
// 0 quality (smallest)
143-
NSData *minEncodedData = [SDImageCodersManager.sharedManager encodedDataWithImage:image format:SDImageFormatJPEG options:@{SDImageCoderEncodeCompressionQuality : @(0)}];
143+
NSData *minEncodedData = [SDImageCodersManager.sharedManager encodedDataWithImage:image format:SDImageFormatJPEG options:@{SDImageCoderEncodeCompressionQuality : @(0.01)}]; // Seems 0 has some bugs in old macOS
144144
expect(minEncodedData).notTo.beNil();
145145
expect(minEncodedData.length).beLessThan(limitFileSize);
146146
NSData *limitEncodedData = [SDImageCodersManager.sharedManager encodedDataWithImage:image format:SDImageFormatJPEG options:@{SDImageCoderEncodeMaxFileSize : @(limitFileSize)}];
@@ -422,6 +422,32 @@ - (void)test26ThatRawImageTypeHintWorks {
422422
#endif
423423
}
424424

425+
- (void)test27ThatEncodeWithFramesWorks {
426+
// Mock
427+
NSMutableArray<SDImageFrame *> *frames = [NSMutableArray array];
428+
NSUInteger frameCount = 5;
429+
for (size_t i = 0; i < frameCount; i++) {
430+
CGSize size = CGSizeMake(100, 100);
431+
SDGraphicsImageRenderer *renderer = [[SDGraphicsImageRenderer alloc] initWithSize:size];
432+
UIImage *image = [renderer imageWithActions:^(CGContextRef _Nonnull context) {
433+
CGContextSetRGBFillColor(context, 1.0 / i, 0.0, 0.0, 1.0);
434+
CGContextSetRGBStrokeColor(context, 1.0 / i, 0.0, 0.0, 1.0);
435+
CGContextFillRect(context, CGRectMake(0, 0, size.width, size.height));
436+
}];
437+
SDImageFrame *frame = [SDImageFrame frameWithImage:image duration:0.1];
438+
[frames addObject:frame];
439+
}
440+
441+
// Test old API
442+
UIImage *animatedImage = [SDImageCoderHelper animatedImageWithFrames:frames];
443+
NSData *data = [SDImageGIFCoder.sharedCoder encodedDataWithImage:animatedImage format:SDImageFormatGIF options:nil];
444+
expect(data).notTo.beNil();
445+
446+
// Test new API
447+
NSData *data2 = [SDImageGIFCoder.sharedCoder encodedDataWithFrames:frames loopCount:0 format:SDImageFormatGIF options:nil];
448+
expect(data2).notTo.beNil();
449+
}
450+
425451
#pragma mark - Utils
426452

427453
- (void)verifyCoder:(id<SDImageCoder>)coder

0 commit comments

Comments
 (0)