Create a subclass of NSBitmapImageRep to fix the GIF frame duration issue on macOS

This commit is contained in:
DreamPiggy 2018-02-18 11:34:06 +08:00
parent a54d1d7a2f
commit fc1fd0a74e
9 changed files with 112 additions and 7 deletions

View File

@ -25,6 +25,9 @@
- (void)viewDidLoad { - (void)viewDidLoad {
[super viewDidLoad]; [super viewDidLoad];
//Add GIF coder for better animated image rendering
[[SDWebImageCodersManager sharedInstance] addCoder:[SDWebImageGIFCoder sharedCoder]];
// NOTE: https links or authentication ones do not work (there is a crash) // NOTE: https links or authentication ones do not work (there is a crash)
// Do any additional setup after loading the view. // Do any additional setup after loading the view.

View File

@ -37,6 +37,8 @@
00733A711BC4880E00A5A117 /* UIImageView+WebCache.h in Headers */ = {isa = PBXBuildFile; fileRef = 53922D95148C56230056699D /* UIImageView+WebCache.h */; settings = {ATTRIBUTES = (Public, ); }; }; 00733A711BC4880E00A5A117 /* UIImageView+WebCache.h in Headers */ = {isa = PBXBuildFile; fileRef = 53922D95148C56230056699D /* UIImageView+WebCache.h */; settings = {ATTRIBUTES = (Public, ); }; };
00733A721BC4880E00A5A117 /* UIView+WebCacheOperation.h in Headers */ = {isa = PBXBuildFile; fileRef = AB615301192DA24600A2D8E9 /* UIView+WebCacheOperation.h */; settings = {ATTRIBUTES = (Public, ); }; }; 00733A721BC4880E00A5A117 /* UIView+WebCacheOperation.h in Headers */ = {isa = PBXBuildFile; fileRef = AB615301192DA24600A2D8E9 /* UIView+WebCacheOperation.h */; settings = {ATTRIBUTES = (Public, ); }; };
00733A731BC4880E00A5A117 /* SDWebImage.h in Headers */ = {isa = PBXBuildFile; fileRef = 4A2CAE031AB4BB5400B6BC39 /* SDWebImage.h */; settings = {ATTRIBUTES = (Public, ); }; }; 00733A731BC4880E00A5A117 /* SDWebImage.h in Headers */ = {isa = PBXBuildFile; fileRef = 4A2CAE031AB4BB5400B6BC39 /* SDWebImage.h */; settings = {ATTRIBUTES = (Public, ); }; };
320224BB203979BA00E9F285 /* SDAnimatedImageRep.h in Headers */ = {isa = PBXBuildFile; fileRef = 320224B9203979BA00E9F285 /* SDAnimatedImageRep.h */; settings = {ATTRIBUTES = (Public, ); }; };
320224BC203979BA00E9F285 /* SDAnimatedImageRep.m in Sources */ = {isa = PBXBuildFile; fileRef = 320224BA203979BA00E9F285 /* SDAnimatedImageRep.m */; };
321DB3612011D4D70015D2CB /* NSButton+WebCache.h in Headers */ = {isa = PBXBuildFile; fileRef = 321DB35F2011D4D60015D2CB /* NSButton+WebCache.h */; settings = {ATTRIBUTES = (Public, ); }; }; 321DB3612011D4D70015D2CB /* NSButton+WebCache.h in Headers */ = {isa = PBXBuildFile; fileRef = 321DB35F2011D4D60015D2CB /* NSButton+WebCache.h */; settings = {ATTRIBUTES = (Public, ); }; };
321DB3622011D4D70015D2CB /* NSButton+WebCache.m in Sources */ = {isa = PBXBuildFile; fileRef = 321DB3602011D4D60015D2CB /* NSButton+WebCache.m */; }; 321DB3622011D4D70015D2CB /* NSButton+WebCache.m in Sources */ = {isa = PBXBuildFile; fileRef = 321DB3602011D4D60015D2CB /* NSButton+WebCache.m */; };
321E60861F38E8C800405457 /* SDWebImageCoder.h in Headers */ = {isa = PBXBuildFile; fileRef = 321E60841F38E8C800405457 /* SDWebImageCoder.h */; settings = {ATTRIBUTES = (Public, ); }; }; 321E60861F38E8C800405457 /* SDWebImageCoder.h in Headers */ = {isa = PBXBuildFile; fileRef = 321E60841F38E8C800405457 /* SDWebImageCoder.h */; settings = {ATTRIBUTES = (Public, ); }; };
@ -1287,6 +1289,8 @@
/* Begin PBXFileReference section */ /* Begin PBXFileReference section */
00733A4C1BC487C000A5A117 /* SDWebImage.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = SDWebImage.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 00733A4C1BC487C000A5A117 /* SDWebImage.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = SDWebImage.framework; sourceTree = BUILT_PRODUCTS_DIR; };
320224B9203979BA00E9F285 /* SDAnimatedImageRep.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SDAnimatedImageRep.h; sourceTree = "<group>"; };
320224BA203979BA00E9F285 /* SDAnimatedImageRep.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SDAnimatedImageRep.m; sourceTree = "<group>"; };
321DB35F2011D4D60015D2CB /* NSButton+WebCache.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "NSButton+WebCache.h"; path = "SDWebImage/NSButton+WebCache.h"; sourceTree = "<group>"; }; 321DB35F2011D4D60015D2CB /* NSButton+WebCache.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "NSButton+WebCache.h"; path = "SDWebImage/NSButton+WebCache.h"; sourceTree = "<group>"; };
321DB3602011D4D60015D2CB /* NSButton+WebCache.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = "NSButton+WebCache.m"; path = "SDWebImage/NSButton+WebCache.m"; sourceTree = "<group>"; }; 321DB3602011D4D60015D2CB /* NSButton+WebCache.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = "NSButton+WebCache.m"; path = "SDWebImage/NSButton+WebCache.m"; sourceTree = "<group>"; };
321E60841F38E8C800405457 /* SDWebImageCoder.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SDWebImageCoder.h; sourceTree = "<group>"; }; 321E60841F38E8C800405457 /* SDWebImageCoder.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SDWebImageCoder.h; sourceTree = "<group>"; };
@ -1586,6 +1590,8 @@
3290FA031FA478AF0047D20C /* SDWebImageFrame.m */, 3290FA031FA478AF0047D20C /* SDWebImageFrame.m */,
32CF1C051FA496B000004BD1 /* SDWebImageCoderHelper.h */, 32CF1C051FA496B000004BD1 /* SDWebImageCoderHelper.h */,
32CF1C061FA496B000004BD1 /* SDWebImageCoderHelper.m */, 32CF1C061FA496B000004BD1 /* SDWebImageCoderHelper.m */,
320224B9203979BA00E9F285 /* SDAnimatedImageRep.h */,
320224BA203979BA00E9F285 /* SDAnimatedImageRep.m */,
); );
name = Decoder; name = Decoder;
sourceTree = "<group>"; sourceTree = "<group>";
@ -2263,6 +2269,7 @@
4397D2D11D0DDD8C00BB2784 /* decode.h in Headers */, 4397D2D11D0DDD8C00BB2784 /* decode.h in Headers */,
80377E481F2F66A800F89830 /* dsp.h in Headers */, 80377E481F2F66A800F89830 /* dsp.h in Headers */,
323F8BE91F38EF770092B609 /* vp8li_enc.h in Headers */, 323F8BE91F38EF770092B609 /* vp8li_enc.h in Headers */,
320224BB203979BA00E9F285 /* SDAnimatedImageRep.h in Headers */,
80377E761F2F66A800F89830 /* yuv.h in Headers */, 80377E761F2F66A800F89830 /* yuv.h in Headers */,
80377C7A1F2F666400F89830 /* bit_reader_inl_utils.h in Headers */, 80377C7A1F2F666400F89830 /* bit_reader_inl_utils.h in Headers */,
80377E631F2F66A800F89830 /* lossless.h in Headers */, 80377E631F2F66A800F89830 /* lossless.h in Headers */,
@ -3229,6 +3236,7 @@
4397D2A61D0DDD8C00BB2784 /* SDWebImageCompat.m in Sources */, 4397D2A61D0DDD8C00BB2784 /* SDWebImageCompat.m in Sources */,
80377E6F1F2F66A800F89830 /* upsampling_neon.c in Sources */, 80377E6F1F2F66A800F89830 /* upsampling_neon.c in Sources */,
4397D2A81D0DDD8C00BB2784 /* UIButton+WebCache.m in Sources */, 4397D2A81D0DDD8C00BB2784 /* UIButton+WebCache.m in Sources */,
320224BC203979BA00E9F285 /* SDAnimatedImageRep.m in Sources */,
80377C8E1F2F666400F89830 /* rescaler_utils.c in Sources */, 80377C8E1F2F666400F89830 /* rescaler_utils.c in Sources */,
80377E5C1F2F66A800F89830 /* lossless_enc_sse41.c in Sources */, 80377E5C1F2F66A800F89830 /* lossless_enc_sse41.c in Sources */,
323F8BE31F38EF770092B609 /* vp8l_enc.c in Sources */, 323F8BE31F38EF770092B609 /* vp8l_enc.c in Sources */,

View File

@ -0,0 +1,20 @@
/*
* This file is part of the SDWebImage package.
* (c) Olivier Poitrey <rs@dailymotion.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
#import "SDWebImageCompat.h"
#if SD_MAC
// A subclass of `NSBitmapImageRep` to fix that GIF loop count issue because `NSBitmapImageRep` will reset `NSImageCurrentFrameDuration` by using `kCGImagePropertyGIFDelayTime` but not `kCGImagePropertyGIFUnclampedDelayTime`.
// Built in GIF coder use this instead of `NSBitmapImageRep` for better GIF rendering. If you do not want this, only enable `SDWebImageImageIOCoder`, which just call `NSImage` API and actually use `NSBitmapImageRep` for GIF image.
@interface SDAnimatedImageRep : NSBitmapImageRep
@end
#endif

View File

@ -0,0 +1,66 @@
/*
* This file is part of the SDWebImage package.
* (c) Olivier Poitrey <rs@dailymotion.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
#import "SDAnimatedImageRep.h"
#if SD_MAC
#import "SDWebImageGIFCoder.h"
@interface SDWebImageGIFCoder ()
- (float)sd_frameDurationAtIndex:(NSUInteger)index source:(CGImageSourceRef)source;
@end
@interface SDAnimatedImageRep ()
@property (nonatomic, assign, readonly, nullable) CGImageSourceRef imageSource;
@end
@implementation SDAnimatedImageRep
// `NSBitmapImageRep` will use `kCGImagePropertyGIFDelayTime` whenever you call `setProperty:withValue:` with `NSImageCurrentFrame` to change the current frame. We override it and use the actual `kCGImagePropertyGIFUnclampedDelayTime` if need.
- (void)setProperty:(NSBitmapImageRepPropertyKey)property withValue:(id)value {
[super setProperty:property withValue:value];
if ([property isEqualToString:NSImageCurrentFrame]) {
// Access the image source
CGImageSourceRef imageSource = self.imageSource;
if (!imageSource) {
return;
}
// Check format type
CFStringRef type = CGImageSourceGetType(imageSource);
if (!type) {
return;
}
NSUInteger index = [value unsignedIntegerValue];
float frameDuration = 0;
// Through we currently process GIF only, in the 5.x we support APNG so we keep the extensibility
if (CFStringCompare(type, kUTTypeGIF, 0) == kCFCompareEqualTo) {
frameDuration = [[SDWebImageGIFCoder sharedCoder] sd_frameDurationAtIndex:index source:imageSource];
}
if (!frameDuration) {
return;
}
// Reset super frame duration with the actual frame duration
[super setProperty:NSImageCurrentFrameDuration withValue:@(frameDuration)];
}
}
- (CGImageSourceRef)imageSource {
if (_tiffData) {
return (__bridge CGImageSourceRef)(_tiffData);
}
return NULL;
}
@end
#endif

View File

@ -14,8 +14,8 @@
/** /**
Return an animated image with frames array. Return an animated image with frames array.
For UIKit, this will apply the patch and then create animated UIImage. The patch is because that `+[UIImage animatedImageWithImages:duration:]` just use the averate of duration for each image. So it will not work if different frame has different duration. Therefore we repeat the specify frame for specify times to let it work. For UIKit, this will apply the patch and then create animated UIImage. The patch is because that `+[UIImage animatedImageWithImages:duration:]` just use the average of duration for each image. So it will not work if different frame has different duration. Therefore we repeat the specify frame for specify times to let it work.
For AppKit, NSImage does not support animates other than GIF. This will try to encode the frames to GIF format and then create an animated NSImage for rendering. For AppKit, NSImage does not support animates other than GIF. This will try to encode the frames to GIF format and then create an animated NSImage for rendering. Attention the animated image may loss some detail if the input frames contain full alpha channel because GIF only supports 1 bit alpha channel. (For 1 pixel, either transparent or not)
@param frames The frames array. If no frames or frames is empty, return nil @param frames The frames array. If no frames or frames is empty, return nil
@return A animated image for rendering on UIImageView(UIKit) or NSImageView(AppKit) @return A animated image for rendering on UIImageView(UIKit) or NSImageView(AppKit)

View File

@ -11,6 +11,7 @@
#import "UIImage+MultiFormat.h" #import "UIImage+MultiFormat.h"
#import "NSImage+WebCache.h" #import "NSImage+WebCache.h"
#import <ImageIO/ImageIO.h> #import <ImageIO/ImageIO.h>
#import "SDAnimatedImageRep.h"
@implementation SDWebImageCoderHelper @implementation SDWebImageCoderHelper
@ -63,7 +64,7 @@
SDWebImageFrame *frame = frames[i]; SDWebImageFrame *frame = frames[i];
float frameDuration = frame.duration; float frameDuration = frame.duration;
CGImageRef frameImageRef = frame.image.CGImage; CGImageRef frameImageRef = frame.image.CGImage;
NSDictionary *frameProperties = @{(__bridge_transfer NSString *)kCGImagePropertyGIFDictionary : @{(__bridge_transfer NSString *)kCGImagePropertyGIFUnclampedDelayTime : @(frameDuration)}}; NSDictionary *frameProperties = @{(__bridge_transfer NSString *)kCGImagePropertyGIFDictionary : @{(__bridge_transfer NSString *)kCGImagePropertyGIFDelayTime : @(frameDuration)}};
CGImageDestinationAddImage(imageDestination, frameImageRef, (__bridge CFDictionaryRef)frameProperties); CGImageDestinationAddImage(imageDestination, frameImageRef, (__bridge CFDictionaryRef)frameProperties);
} }
} }
@ -74,7 +75,9 @@
return nil; return nil;
} }
CFRelease(imageDestination); CFRelease(imageDestination);
animatedImage = [[NSImage alloc] initWithData:imageData]; SDAnimatedImageRep *imageRep = [[SDAnimatedImageRep alloc] initWithData:imageData];
animatedImage = [[NSImage alloc] initWithSize:imageRep.size];
[animatedImage addRepresentation:imageRep];
#endif #endif
return animatedImage; return animatedImage;

View File

@ -11,7 +11,7 @@
@interface SDWebImageFrame : NSObject @interface SDWebImageFrame : NSObject
// This class is used for creating animated images via `animatedImageWithFrames` in `SDWebImageCoderHelper`. Attension if you need animated images loop count, use `sd_imageLoopCount` property in `UIImage+MultiFormat` // This class is used for creating animated images via `animatedImageWithFrames` in `SDWebImageCoderHelper`. Attention if you need to specify animated images loop count, use `sd_imageLoopCount` property in `UIImage+MultiFormat`.
/** /**
The image of current frame. You should not set an animated image. The image of current frame. You should not set an animated image.

View File

@ -12,6 +12,7 @@
#import "NSData+ImageContentType.h" #import "NSData+ImageContentType.h"
#import "UIImage+MultiFormat.h" #import "UIImage+MultiFormat.h"
#import "SDWebImageCoderHelper.h" #import "SDWebImageCoderHelper.h"
#import "SDAnimatedImageRep.h"
@implementation SDWebImageGIFCoder @implementation SDWebImageGIFCoder
@ -35,7 +36,10 @@
} }
#if SD_MAC #if SD_MAC
return [[UIImage alloc] initWithData:data]; SDAnimatedImageRep *imageRep = [[SDAnimatedImageRep alloc] initWithData:data];
NSImage *animatedImage = [[NSImage alloc] initWithSize:imageRep.size];
[animatedImage addRepresentation:imageRep];
return animatedImage;
#else #else
CGImageSourceRef source = CGImageSourceCreateWithData((__bridge CFDataRef)data, NULL); CGImageSourceRef source = CGImageSourceCreateWithData((__bridge CFDataRef)data, NULL);
@ -161,7 +165,7 @@
SDWebImageFrame *frame = frames[i]; SDWebImageFrame *frame = frames[i];
float frameDuration = frame.duration; float frameDuration = frame.duration;
CGImageRef frameImageRef = frame.image.CGImage; CGImageRef frameImageRef = frame.image.CGImage;
NSDictionary *frameProperties = @{(__bridge_transfer NSString *)kCGImagePropertyGIFDictionary : @{(__bridge_transfer NSString *)kCGImagePropertyGIFUnclampedDelayTime : @(frameDuration)}}; NSDictionary *frameProperties = @{(__bridge_transfer NSString *)kCGImagePropertyGIFDictionary : @{(__bridge_transfer NSString *)kCGImagePropertyGIFDelayTime : @(frameDuration)}};
CGImageDestinationAddImage(imageDestination, frameImageRef, (__bridge CFDictionaryRef)frameProperties); CGImageDestinationAddImage(imageDestination, frameImageRef, (__bridge CFDictionaryRef)frameProperties);
} }
} }

View File

@ -55,6 +55,7 @@ FOUNDATION_EXPORT const unsigned char WebImageVersionString[];
#if SD_MAC #if SD_MAC
#import <SDWebImage/NSImage+WebCache.h> #import <SDWebImage/NSImage+WebCache.h>
#import <SDWebImage/NSButton+WebCache.h> #import <SDWebImage/NSButton+WebCache.h>
#import <SDWebImage/SDAnimatedImageRep.h>
#endif #endif
#if SD_UIKIT #if SD_UIKIT