Merge pull request #1952 from dreampiggy/master
#1951: Fix animated WebP decoding issue, including canvas size, the support for dispose method and the duration per frame
This commit is contained in:
commit
f6cca301d5
|
@ -8,6 +8,8 @@
|
|||
|
||||
#import "SDWebImageCompat.h"
|
||||
|
||||
#import "objc/runtime.h"
|
||||
|
||||
#if !__has_feature(objc_arc)
|
||||
#error SDWebImage is ARC only. Either turn on ARC for the project or use -fobjc-arc flag
|
||||
#endif
|
||||
|
@ -26,8 +28,19 @@ inline UIImage *SDScaledImageForKey(NSString * _Nullable key, UIImage * _Nullabl
|
|||
for (UIImage *tempImage in image.images) {
|
||||
[scaledImages addObject:SDScaledImageForKey(key, tempImage)];
|
||||
}
|
||||
|
||||
return [UIImage animatedImageWithImages:scaledImages duration:image.duration];
|
||||
|
||||
UIImage *animatedImage = [UIImage animatedImageWithImages:scaledImages duration:image.duration];
|
||||
#ifdef SD_WEBP
|
||||
if (animatedImage) {
|
||||
SEL sd_webpLoopCount = NSSelectorFromString(@"sd_webpLoopCount");
|
||||
NSNumber *value = objc_getAssociatedObject(image, sd_webpLoopCount);
|
||||
NSInteger loopCount = value.integerValue;
|
||||
if (loopCount) {
|
||||
objc_setAssociatedObject(animatedImage, sd_webpLoopCount, @(loopCount), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
return animatedImage;
|
||||
} else {
|
||||
#if SD_WATCH
|
||||
if ([[WKInterfaceDevice currentDevice] respondsToSelector:@selector(screenScale)]) {
|
||||
|
|
|
@ -12,6 +12,16 @@
|
|||
|
||||
@interface UIImage (WebP)
|
||||
|
||||
/**
|
||||
* Get the current WebP image loop count, the default value is 0.
|
||||
* For static WebP image, the value is 0.
|
||||
* For animated WebP image, 0 means repeat the animation indefinitely.
|
||||
* Note that because of the limitations of categories this property can get out of sync
|
||||
* if you create another instance with CGImage or other methods.
|
||||
* @return WebP image loop count
|
||||
*/
|
||||
- (NSInteger)sd_webpLoopCount;
|
||||
|
||||
+ (nullable UIImage *)sd_imageWithWebPData:(nullable NSData *)data;
|
||||
|
||||
@end
|
||||
|
|
|
@ -14,6 +14,8 @@
|
|||
#import "webp/demux.h"
|
||||
#import "NSImage+WebCache.h"
|
||||
|
||||
#import "objc/runtime.h"
|
||||
|
||||
// Callback for CGDataProviderRelease
|
||||
static void FreeImageData(void *info, const void *data, size_t size) {
|
||||
free((void *)data);
|
||||
|
@ -21,6 +23,12 @@ static void FreeImageData(void *info, const void *data, size_t size) {
|
|||
|
||||
@implementation UIImage (WebP)
|
||||
|
||||
- (NSInteger)sd_webpLoopCount
|
||||
{
|
||||
NSNumber *value = objc_getAssociatedObject(self, @selector(sd_webpLoopCount));
|
||||
return value.integerValue;
|
||||
}
|
||||
|
||||
+ (nullable UIImage *)sd_imageWithWebPData:(nullable NSData *)data {
|
||||
if (!data) {
|
||||
return nil;
|
||||
|
@ -38,7 +46,7 @@ static void FreeImageData(void *info, const void *data, size_t size) {
|
|||
uint32_t flags = WebPDemuxGetI(demuxer, WEBP_FF_FORMAT_FLAGS);
|
||||
if (!(flags & ANIMATION_FLAG)) {
|
||||
// for static single webp image
|
||||
UIImage *staticImage = [self sd_rawWepImageWithData:webpData];
|
||||
UIImage *staticImage = [self sd_rawWebpImageWithData:webpData];
|
||||
WebPDemuxDelete(demuxer);
|
||||
return staticImage;
|
||||
}
|
||||
|
@ -50,15 +58,35 @@ static void FreeImageData(void *info, const void *data, size_t size) {
|
|||
return nil;
|
||||
}
|
||||
|
||||
NSMutableArray *images = [NSMutableArray array];
|
||||
NSTimeInterval duration = 0;
|
||||
#if SD_UIKIT || SD_WATCH
|
||||
int loopCount = WebPDemuxGetI(demuxer, WEBP_FF_LOOP_COUNT);
|
||||
#endif
|
||||
int frameCount = WebPDemuxGetI(demuxer, WEBP_FF_FRAME_COUNT);
|
||||
int canvasWidth = WebPDemuxGetI(demuxer, WEBP_FF_CANVAS_WIDTH);
|
||||
int canvasHeight = WebPDemuxGetI(demuxer, WEBP_FF_CANVAS_HEIGHT);
|
||||
CGBitmapInfo bitmapInfo;
|
||||
if (!(flags & ALPHA_FLAG)) {
|
||||
bitmapInfo = kCGBitmapByteOrder32Big | kCGImageAlphaNoneSkipLast;
|
||||
} else {
|
||||
bitmapInfo = kCGBitmapByteOrder32Big | kCGImageAlphaPremultipliedLast;
|
||||
}
|
||||
CGContextRef canvas = CGBitmapContextCreate(NULL, canvasWidth, canvasHeight, 8, 0, SDCGColorSpaceGetDeviceRGB(), bitmapInfo);
|
||||
if (!canvas) {
|
||||
WebPDemuxReleaseIterator(&iter);
|
||||
WebPDemuxDelete(demuxer);
|
||||
return nil;
|
||||
}
|
||||
|
||||
NSMutableArray<UIImage *> *images = [NSMutableArray array];
|
||||
NSTimeInterval totalDuration = 0;
|
||||
int durations[frameCount];
|
||||
|
||||
do {
|
||||
UIImage *image;
|
||||
if (iter.blend_method == WEBP_MUX_BLEND) {
|
||||
image = [self sd_blendWebpImageWithOriginImage:[images lastObject] iterator:iter];
|
||||
image = [self sd_blendWebpImageWithCanvas:canvas iterator:iter];
|
||||
} else {
|
||||
image = [self sd_rawWepImageWithData:iter.fragment];
|
||||
image = [self sd_nonblendWebpImageWithCanvas:canvas iterator:iter];
|
||||
}
|
||||
|
||||
if (!image) {
|
||||
|
@ -66,46 +94,56 @@ static void FreeImageData(void *info, const void *data, size_t size) {
|
|||
}
|
||||
|
||||
[images addObject:image];
|
||||
duration += iter.duration / 1000.0f;
|
||||
|
||||
#if SD_MAC
|
||||
break;
|
||||
#endif
|
||||
|
||||
int duration = iter.duration;
|
||||
if (duration <= 10) {
|
||||
// WebP standard says 0 duration is used for canvas updating but not showing image, but actually Chrome and other implementations set it to 100ms if duration is lower or equal than 10ms
|
||||
// Some animated WebP images also created without duration, we should keep compatibility
|
||||
duration = 100;
|
||||
}
|
||||
totalDuration += duration;
|
||||
size_t count = images.count;
|
||||
durations[count - 1] = duration;
|
||||
|
||||
} while (WebPDemuxNextFrame(&iter));
|
||||
|
||||
WebPDemuxReleaseIterator(&iter);
|
||||
WebPDemuxDelete(demuxer);
|
||||
CGContextRelease(canvas);
|
||||
|
||||
UIImage *finalImage = nil;
|
||||
#if SD_UIKIT || SD_WATCH
|
||||
finalImage = [UIImage animatedImageWithImages:images duration:duration];
|
||||
#elif SD_MAC
|
||||
if ([images count] > 0) {
|
||||
finalImage = images[0];
|
||||
NSArray<UIImage *> *animatedImages = [self sd_animatedImagesWithImages:images durations:durations totalDuration:totalDuration];
|
||||
finalImage = [UIImage animatedImageWithImages:animatedImages duration:totalDuration / 1000.0];
|
||||
if (finalImage) {
|
||||
objc_setAssociatedObject(finalImage, @selector(sd_webpLoopCount), @(loopCount), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
|
||||
}
|
||||
#elif SD_MAC
|
||||
finalImage = images.firstObject;
|
||||
#endif
|
||||
return finalImage;
|
||||
}
|
||||
|
||||
|
||||
+ (nullable UIImage *)sd_blendWebpImageWithOriginImage:(nullable UIImage *)originImage iterator:(WebPIterator)iter {
|
||||
if (!originImage) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
CGSize size = originImage.size;
|
||||
CGFloat tmpX = iter.x_offset;
|
||||
CGFloat tmpY = size.height - iter.height - iter.y_offset;
|
||||
CGRect imageRect = CGRectMake(tmpX, tmpY, iter.width, iter.height);
|
||||
|
||||
UIImage *image = [self sd_rawWepImageWithData:iter.fragment];
|
||||
+ (nullable UIImage *)sd_blendWebpImageWithCanvas:(CGContextRef)canvas iterator:(WebPIterator)iter {
|
||||
UIImage *image = [self sd_rawWebpImageWithData:iter.fragment];
|
||||
if (!image) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
CGColorSpaceRef colorSpaceRef = CGColorSpaceCreateDeviceRGB();
|
||||
uint32_t bitmapInfo = iter.has_alpha ? kCGBitmapByteOrder32Big | kCGImageAlphaPremultipliedLast : 0;
|
||||
CGContextRef blendCanvas = CGBitmapContextCreate(NULL, size.width, size.height, 8, 0, colorSpaceRef, bitmapInfo);
|
||||
CGContextDrawImage(blendCanvas, CGRectMake(0, 0, size.width, size.height), originImage.CGImage);
|
||||
CGContextDrawImage(blendCanvas, imageRect, image.CGImage);
|
||||
CGImageRef newImageRef = CGBitmapContextCreateImage(blendCanvas);
|
||||
size_t canvasWidth = CGBitmapContextGetWidth(canvas);
|
||||
size_t canvasHeight = CGBitmapContextGetHeight(canvas);
|
||||
CGSize size = CGSizeMake(canvasWidth, canvasHeight);
|
||||
CGFloat tmpX = iter.x_offset;
|
||||
CGFloat tmpY = size.height - iter.height - iter.y_offset;
|
||||
CGRect imageRect = CGRectMake(tmpX, tmpY, iter.width, iter.height);
|
||||
|
||||
CGContextDrawImage(canvas, imageRect, image.CGImage);
|
||||
CGImageRef newImageRef = CGBitmapContextCreateImage(canvas);
|
||||
|
||||
#if SD_UIKIT || SD_WATCH
|
||||
image = [UIImage imageWithCGImage:newImageRef];
|
||||
|
@ -114,13 +152,47 @@ static void FreeImageData(void *info, const void *data, size_t size) {
|
|||
#endif
|
||||
|
||||
CGImageRelease(newImageRef);
|
||||
CGContextRelease(blendCanvas);
|
||||
CGColorSpaceRelease(colorSpaceRef);
|
||||
|
||||
if (iter.dispose_method == WEBP_MUX_DISPOSE_BACKGROUND) {
|
||||
CGContextClearRect(canvas, imageRect);
|
||||
}
|
||||
|
||||
return image;
|
||||
}
|
||||
|
||||
+ (nullable UIImage *)sd_rawWepImageWithData:(WebPData)webpData {
|
||||
+ (nullable UIImage *)sd_nonblendWebpImageWithCanvas:(CGContextRef)canvas iterator:(WebPIterator)iter {
|
||||
UIImage *image = [self sd_rawWebpImageWithData:iter.fragment];
|
||||
if (!image) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
size_t canvasWidth = CGBitmapContextGetWidth(canvas);
|
||||
size_t canvasHeight = CGBitmapContextGetHeight(canvas);
|
||||
CGSize size = CGSizeMake(canvasWidth, canvasHeight);
|
||||
CGFloat tmpX = iter.x_offset;
|
||||
CGFloat tmpY = size.height - iter.height - iter.y_offset;
|
||||
CGRect imageRect = CGRectMake(tmpX, tmpY, iter.width, iter.height);
|
||||
|
||||
CGContextClearRect(canvas, imageRect);
|
||||
CGContextDrawImage(canvas, imageRect, image.CGImage);
|
||||
CGImageRef newImageRef = CGBitmapContextCreateImage(canvas);
|
||||
|
||||
#if SD_UIKIT || SD_WATCH
|
||||
image = [UIImage imageWithCGImage:newImageRef];
|
||||
#elif SD_MAC
|
||||
image = [[UIImage alloc] initWithCGImage:newImageRef size:NSZeroSize];
|
||||
#endif
|
||||
|
||||
CGImageRelease(newImageRef);
|
||||
|
||||
if (iter.dispose_method == WEBP_MUX_DISPOSE_BACKGROUND) {
|
||||
CGContextClearRect(canvas, imageRect);
|
||||
}
|
||||
|
||||
return image;
|
||||
}
|
||||
|
||||
+ (nullable UIImage *)sd_rawWebpImageWithData:(WebPData)webpData {
|
||||
WebPDecoderConfig config;
|
||||
if (!WebPInitDecoderConfig(&config)) {
|
||||
return nil;
|
||||
|
@ -133,7 +205,7 @@ static void FreeImageData(void *info, const void *data, size_t size) {
|
|||
config.output.colorspace = config.input.has_alpha ? MODE_rgbA : MODE_RGB;
|
||||
config.options.use_threads = 1;
|
||||
|
||||
// Decode the WebP image data into a RGBA value array.
|
||||
// Decode the WebP image data into a RGBA value array
|
||||
if (WebPDecode(webpData.bytes, webpData.size, &config) != VP8_STATUS_OK) {
|
||||
return nil;
|
||||
}
|
||||
|
@ -145,16 +217,15 @@ static void FreeImageData(void *info, const void *data, size_t size) {
|
|||
height = config.options.scaled_height;
|
||||
}
|
||||
|
||||
// Construct a UIImage from the decoded RGBA value array.
|
||||
// Construct a UIImage from the decoded RGBA value array
|
||||
CGDataProviderRef provider =
|
||||
CGDataProviderCreateWithData(NULL, config.output.u.RGBA.rgba, config.output.u.RGBA.size, FreeImageData);
|
||||
CGColorSpaceRef colorSpaceRef = CGColorSpaceCreateDeviceRGB();
|
||||
CGBitmapInfo bitmapInfo = config.input.has_alpha ? kCGBitmapByteOrder32Big | kCGImageAlphaPremultipliedLast : 0;
|
||||
CGColorSpaceRef colorSpaceRef = SDCGColorSpaceGetDeviceRGB();
|
||||
CGBitmapInfo bitmapInfo = config.input.has_alpha ? kCGBitmapByteOrder32Big | kCGImageAlphaPremultipliedLast : kCGBitmapByteOrder32Big | kCGImageAlphaNoneSkipLast;
|
||||
size_t components = config.input.has_alpha ? 4 : 3;
|
||||
CGColorRenderingIntent renderingIntent = kCGRenderingIntentDefault;
|
||||
CGImageRef imageRef = CGImageCreate(width, height, 8, components * 8, components * width, colorSpaceRef, bitmapInfo, provider, NULL, NO, renderingIntent);
|
||||
|
||||
CGColorSpaceRelease(colorSpaceRef);
|
||||
CGDataProviderRelease(provider);
|
||||
|
||||
#if SD_UIKIT || SD_WATCH
|
||||
|
@ -167,6 +238,63 @@ static void FreeImageData(void *info, const void *data, size_t size) {
|
|||
return image;
|
||||
}
|
||||
|
||||
+ (NSArray<UIImage *> *)sd_animatedImagesWithImages:(NSArray<UIImage *> *)images durations:(int const * const)durations totalDuration:(NSTimeInterval)totalDuration
|
||||
{
|
||||
// [UIImage animatedImageWithImages:duration:] only use the average duration for per frame
|
||||
// divide the total duration to implement per frame duration for animated WebP
|
||||
NSUInteger count = images.count;
|
||||
if (!count) {
|
||||
return nil;
|
||||
}
|
||||
if (count == 1) {
|
||||
return images;
|
||||
}
|
||||
|
||||
int const gcd = gcdArray(count, durations);
|
||||
NSMutableArray<UIImage *> *animatedImages = [NSMutableArray arrayWithCapacity:count];
|
||||
[images enumerateObjectsUsingBlock:^(UIImage * _Nonnull image, NSUInteger idx, BOOL * _Nonnull stop) {
|
||||
int duration = durations[idx];
|
||||
int repeatCount;
|
||||
if (gcd) {
|
||||
repeatCount = duration / gcd;
|
||||
} else {
|
||||
repeatCount = 1;
|
||||
}
|
||||
for (int i = 0; i < repeatCount; ++i) {
|
||||
[animatedImages addObject:image];
|
||||
}
|
||||
}];
|
||||
|
||||
return animatedImages;
|
||||
}
|
||||
|
||||
static CGColorSpaceRef SDCGColorSpaceGetDeviceRGB() {
|
||||
static CGColorSpaceRef space;
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
space = CGColorSpaceCreateDeviceRGB();
|
||||
});
|
||||
return space;
|
||||
}
|
||||
|
||||
static int gcdArray(size_t const count, int const * const values) {
|
||||
int result = values[0];
|
||||
for (size_t i = 1; i < count; ++i) {
|
||||
result = gcd(values[i], result);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
static int gcd(int a,int b) {
|
||||
int c;
|
||||
while (a != 0) {
|
||||
c = a;
|
||||
a = b % a;
|
||||
b = c;
|
||||
}
|
||||
return b;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
#endif
|
||||
|
|
|
@ -22,12 +22,41 @@
|
|||
|
||||
@implementation UIImageMultiFormatTests
|
||||
|
||||
- (void)testImageOrientationFromImageDataWithInvalidData {
|
||||
- (void)test01ImageOrientationFromImageDataWithInvalidData {
|
||||
// sync download image
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wundeclared-selector"
|
||||
SEL selector = @selector(sd_imageOrientationFromImageData:);
|
||||
#pragma clang diagnostic pop
|
||||
|
||||
UIImageOrientation orientation = [[UIImage class] performSelector:selector withObject:nil];
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
|
||||
UIImageOrientation orientation = (UIImageOrientation)[[UIImage class] performSelector:selector withObject:nil];
|
||||
#pragma clang diagnostic pop
|
||||
expect(orientation).to.equal(UIImageOrientationUp);
|
||||
}
|
||||
|
||||
- (void)test02AnimatedWebPImageArrayWithEqualSizeAndScale {
|
||||
NSURL *webpURL = [NSURL URLWithString:@"https://isparta.github.io/compare-webp/image/gif_webp/webp/2.webp"];
|
||||
NSData *data = [NSData dataWithContentsOfURL:webpURL];
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wundeclared-selector"
|
||||
SEL selector = @selector(sd_imageWithWebPData:);
|
||||
#pragma clang diagnostic pop
|
||||
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
|
||||
UIImage *animatedImage = [[UIImage class] performSelector:selector withObject:data];
|
||||
#pragma clang diagnostic pop
|
||||
CGSize imageSize = animatedImage.size;
|
||||
CGFloat imageScale = animatedImage.scale;
|
||||
[animatedImage.images enumerateObjectsUsingBlock:^(UIImage * _Nonnull image, NSUInteger idx, BOOL * _Nonnull stop) {
|
||||
CGSize size = image.size;
|
||||
CGFloat scale = image.scale;
|
||||
expect(imageSize.width).to.equal(size.width);
|
||||
expect(imageSize.height).to.equal(size.height);
|
||||
expect(imageScale).to.equal(scale);
|
||||
}];
|
||||
}
|
||||
|
||||
@end
|
||||
|
|
Loading…
Reference in New Issue