From 9db3358eb0a9134ca2482457352f0445ab401565 Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Thu, 16 Jan 2020 22:12:43 +0800 Subject: [PATCH 1/6] WebPCoder now supports the thumbnail decoding --- Cartfile | 2 +- Cartfile.resolved | 4 +- .../ViewController.m | 2 +- SDWebImageWebPCoder.podspec | 2 +- .../Classes/SDImageWebPCoder.m | 95 ++++++++++++++++--- 5 files changed, 88 insertions(+), 17 deletions(-) diff --git a/Cartfile b/Cartfile index 3aec783..156ab0a 100644 --- a/Cartfile +++ b/Cartfile @@ -1,2 +1,2 @@ -github "SDWebImage/SDWebImage" ~> 5.0 +github "SDWebImage/SDWebImage" ~> 5.5 github "SDWebImage/libwebp-Xcode" ~> 1.0 diff --git a/Cartfile.resolved b/Cartfile.resolved index 0956195..76a844b 100644 --- a/Cartfile.resolved +++ b/Cartfile.resolved @@ -1,2 +1,2 @@ -github "SDWebImage/SDWebImage" "5.0.0" -github "SDWebImage/libwebp-Xcode" "1.0.0" +github "SDWebImage/SDWebImage" "5.5.0" +github "SDWebImage/libwebp-Xcode" "1.1.0" diff --git a/Example/SDWebImageWebPCoderExample/ViewController.m b/Example/SDWebImageWebPCoderExample/ViewController.m index e358dd0..0d31b16 100644 --- a/Example/SDWebImageWebPCoderExample/ViewController.m +++ b/Example/SDWebImageWebPCoderExample/ViewController.m @@ -35,7 +35,7 @@ NSURL *staticWebPURL = [NSURL URLWithString:@"https://www.gstatic.com/webp/gallery/2.webp"]; NSURL *animatedWebPURL = [NSURL URLWithString:@"http://littlesvr.ca/apng/images/world-cup-2014-42.webp"]; - [self.imageView1 sd_setImageWithURL:staticWebPURL completed:^(UIImage * _Nullable image, NSError * _Nullable error, SDImageCacheType cacheType, NSURL * _Nullable imageURL) { + [self.imageView1 sd_setImageWithURL:staticWebPURL placeholderImage:nil options:0 context:@{SDWebImageContextImageThumbnailPixelSize : @(CGSizeMake(300, 300))} progress:nil completed:^(UIImage * _Nullable image, NSError * _Nullable error, SDImageCacheType cacheType, NSURL * _Nullable imageURL) { if (image) { NSLog(@"%@", @"Static WebP load success"); } diff --git a/SDWebImageWebPCoder.podspec b/SDWebImageWebPCoder.podspec index da35dcb..e59c138 100644 --- a/SDWebImageWebPCoder.podspec +++ b/SDWebImageWebPCoder.podspec @@ -27,7 +27,7 @@ This is a SDWebImage coder plugin to support WebP image. 'GCC_PREPROCESSOR_DEFINITIONS' => '$(inherited) SD_WEBP=1 WEBP_USE_INTRINSICS=1', 'USER_HEADER_SEARCH_PATHS' => '$(inherited) $(SRCROOT)/libwebp/src' } - s.dependency 'SDWebImage/Core', '~> 5.0' + s.dependency 'SDWebImage/Core', '~> 5.5' s.dependency 'libwebp', '~> 1.0' end diff --git a/SDWebImageWebPCoder/Classes/SDImageWebPCoder.m b/SDWebImageWebPCoder/Classes/SDImageWebPCoder.m index 932eebb..bf6ce09 100644 --- a/SDWebImageWebPCoder/Classes/SDImageWebPCoder.m +++ b/SDWebImageWebPCoder/Classes/SDImageWebPCoder.m @@ -68,6 +68,8 @@ CGFloat _canvasHeight; dispatch_semaphore_t _lock; NSUInteger _currentBlendIndex; + BOOL _preserveAspectRatio; + CGSize _thumbnailSize; } - (void)dealloc { @@ -133,6 +135,22 @@ } } + CGSize thumbnailSize = CGSizeZero; + NSValue *thumbnailSizeValue = options[SDImageCoderDecodeThumbnailPixelSize]; + if (thumbnailSizeValue != nil) { +#if SD_MAC + thumbnailSize = thumbnailSizeValue.sizeValue; +#else + thumbnailSize = thumbnailSizeValue.CGSizeValue; +#endif + } + + BOOL preserveAspectRatio = YES; + NSNumber *preserveAspectRatioValue = options[SDImageCoderDecodePreserveAspectRatio]; + if (preserveAspectRatioValue != nil) { + preserveAspectRatio = preserveAspectRatioValue.boolValue; + } + // for animated webp image WebPIterator iter; // libwebp's index start with 1 @@ -145,7 +163,7 @@ if (!hasAnimation || decodeFirstFrame) { // first frame for animated webp image - CGImageRef imageRef = [self sd_createWebpImageWithData:iter.fragment colorSpace:colorSpace]; + CGImageRef imageRef = [self sd_createWebpImageWithData:iter.fragment colorSpace:colorSpace preserveAspectRatio:preserveAspectRatio thumbnailSize:thumbnailSize]; CGColorSpaceRelease(colorSpace); #if SD_UIKIT || SD_WATCH UIImage *firstFrameImage = [[UIImage alloc] initWithCGImage:imageRef scale:scale orientation:UIImageOrientationUp]; @@ -176,7 +194,7 @@ do { @autoreleasepool { - CGImageRef imageRef = [self sd_drawnWebpImageWithCanvas:canvas iterator:iter colorSpace:colorSpace]; + CGImageRef imageRef = [self sd_drawnWebpImageWithCanvas:canvas iterator:iter colorSpace:colorSpace preserveAspectRatio:preserveAspectRatio thumbnailSize:thumbnailSize]; if (!imageRef) { continue; } @@ -221,6 +239,22 @@ } } _scale = scale; + CGSize thumbnailSize = CGSizeZero; + NSValue *thumbnailSizeValue = options[SDImageCoderDecodeThumbnailPixelSize]; + if (thumbnailSizeValue != nil) { + #if SD_MAC + thumbnailSize = thumbnailSizeValue.sizeValue; + #else + thumbnailSize = thumbnailSizeValue.CGSizeValue; + #endif + } + _thumbnailSize = thumbnailSize; + BOOL preserveAspectRatio = YES; + NSNumber *preserveAspectRatioValue = options[SDImageCoderDecodePreserveAspectRatio]; + if (preserveAspectRatioValue != nil) { + preserveAspectRatio = preserveAspectRatioValue.boolValue; + } + _preserveAspectRatio = preserveAspectRatio; } return self; } @@ -308,7 +342,7 @@ return image; } -- (void)sd_blendWebpImageWithCanvas:(CGContextRef)canvas iterator:(WebPIterator)iter colorSpace:(nonnull CGColorSpaceRef)colorSpaceRef { +- (void)sd_blendWebpImageWithCanvas:(CGContextRef)canvas iterator:(WebPIterator)iter colorSpace:(nonnull CGColorSpaceRef)colorSpaceRef preserveAspectRatio:(BOOL)preserveAspectRatio thumbnailSize:(CGSize)thumbnailSize { size_t canvasHeight = CGBitmapContextGetHeight(canvas); CGFloat tmpX = iter.x_offset; CGFloat tmpY = canvasHeight - iter.height - iter.y_offset; @@ -317,7 +351,7 @@ if (iter.dispose_method == WEBP_MUX_DISPOSE_BACKGROUND) { CGContextClearRect(canvas, imageRect); } else { - CGImageRef imageRef = [self sd_createWebpImageWithData:iter.fragment colorSpace:colorSpaceRef]; + CGImageRef imageRef = [self sd_createWebpImageWithData:iter.fragment colorSpace:colorSpaceRef preserveAspectRatio:preserveAspectRatio thumbnailSize:thumbnailSize]; if (!imageRef) { return; } @@ -331,8 +365,8 @@ } } -- (nullable CGImageRef)sd_drawnWebpImageWithCanvas:(CGContextRef)canvas iterator:(WebPIterator)iter colorSpace:(nonnull CGColorSpaceRef)colorSpaceRef CF_RETURNS_RETAINED { - CGImageRef imageRef = [self sd_createWebpImageWithData:iter.fragment colorSpace:colorSpaceRef]; +- (nullable CGImageRef)sd_drawnWebpImageWithCanvas:(CGContextRef)canvas iterator:(WebPIterator)iter colorSpace:(nonnull CGColorSpaceRef)colorSpaceRef preserveAspectRatio:(BOOL)preserveAspectRatio thumbnailSize:(CGSize)thumbnailSize CF_RETURNS_RETAINED { + CGImageRef imageRef = [self sd_createWebpImageWithData:iter.fragment colorSpace:colorSpaceRef preserveAspectRatio:preserveAspectRatio thumbnailSize:thumbnailSize]; if (!imageRef) { return nil; } @@ -359,7 +393,7 @@ return newImageRef; } -- (nullable CGImageRef)sd_createWebpImageWithData:(WebPData)webpData colorSpace:(nonnull CGColorSpaceRef)colorSpaceRef CF_RETURNS_RETAINED { +- (nullable CGImageRef)sd_createWebpImageWithData:(WebPData)webpData colorSpace:(nonnull CGColorSpaceRef)colorSpaceRef preserveAspectRatio:(BOOL)preserveAspectRatio thumbnailSize:(CGSize)thumbnailSize CF_RETURNS_RETAINED { WebPDecoderConfig config; if (!WebPInitDecoderConfig(&config)) { return nil; @@ -377,13 +411,34 @@ config.options.use_threads = 1; config.output.colorspace = MODE_bgrA; + int width = config.input.width; + int height = config.input.height; + if (width == 0 || height == 0 || thumbnailSize.width == 0 || thumbnailSize.height == 0 || (width <= thumbnailSize.width && height <= thumbnailSize.height)) { + // Full Pixel + } else { + // Thumbnail + config.options.use_scaling = 1; + if (preserveAspectRatio) { + CGFloat pixelRatio = (CGFloat)width / (CGFloat)height; + CGFloat thumbnailRatio = thumbnailSize.width / thumbnailSize.height; + if (pixelRatio > thumbnailRatio) { + config.options.scaled_width = thumbnailSize.width; + config.options.scaled_height = thumbnailSize.width / pixelRatio; + } else { + config.options.scaled_height = thumbnailSize.height; + config.options.scaled_width = thumbnailSize.height * pixelRatio; + } + } else { + config.options.scaled_width = thumbnailSize.width; + config.options.scaled_height = thumbnailSize.height; + } + } + // Decode the WebP image data into a RGBA value array if (WebPDecode(webpData.bytes, webpData.size, &config) != VP8_STATUS_OK) { return nil; } - int width = config.input.width; - int height = config.input.height; if (config.options.use_scaling) { width = config.options.scaled_width; height = config.options.scaled_height; @@ -681,6 +736,22 @@ static void FreeImageData(void *info, const void *data, size_t size) { scale = 1; } } + CGSize thumbnailSize = CGSizeZero; + NSValue *thumbnailSizeValue = options[SDImageCoderDecodeThumbnailPixelSize]; + if (thumbnailSizeValue != nil) { + #if SD_MAC + thumbnailSize = thumbnailSizeValue.sizeValue; + #else + thumbnailSize = thumbnailSizeValue.CGSizeValue; + #endif + } + _thumbnailSize = thumbnailSize; + BOOL preserveAspectRatio = YES; + NSNumber *preserveAspectRatioValue = options[SDImageCoderDecodePreserveAspectRatio]; + if (preserveAspectRatioValue != nil) { + preserveAspectRatio = preserveAspectRatioValue.boolValue; + } + _preserveAspectRatio = preserveAspectRatio; _scale = scale; _demux = demuxer; _imageData = data; @@ -812,7 +883,7 @@ static void FreeImageData(void *info, const void *data, size_t size) { WebPDemuxReleaseIterator(&iter); return nil; } - CGImageRef imageRef = [self sd_createWebpImageWithData:iter.fragment colorSpace:_colorSpace]; + CGImageRef imageRef = [self sd_createWebpImageWithData:iter.fragment colorSpace:_colorSpace preserveAspectRatio:_preserveAspectRatio thumbnailSize:_thumbnailSize]; if (!imageRef) { return nil; } @@ -874,7 +945,7 @@ static void FreeImageData(void *info, const void *data, size_t size) { if (endIndex > startIndex) { do { @autoreleasepool { - [self sd_blendWebpImageWithCanvas:_canvas iterator:iter colorSpace:_colorSpace]; + [self sd_blendWebpImageWithCanvas:_canvas iterator:iter colorSpace:_colorSpace preserveAspectRatio:_preserveAspectRatio thumbnailSize:_thumbnailSize]; } } while ((size_t)iter.frame_num < endIndex && WebPDemuxNextFrame(&iter)); } @@ -887,7 +958,7 @@ static void FreeImageData(void *info, const void *data, size_t size) { _currentBlendIndex = index; // Now the canvas is ready, which respects of dispose method behavior. Just do normal decoding and produce image. - CGImageRef imageRef = [self sd_drawnWebpImageWithCanvas:_canvas iterator:iter colorSpace:_colorSpace]; + CGImageRef imageRef = [self sd_drawnWebpImageWithCanvas:_canvas iterator:iter colorSpace:_colorSpace preserveAspectRatio:_preserveAspectRatio thumbnailSize:_thumbnailSize]; if (!imageRef) { return nil; } From 50136be56b94f2efe85c44820d4d03b0cec922b4 Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Fri, 17 Jan 2020 16:12:27 +0800 Subject: [PATCH 2/6] Fix the issue that animated image (which use a canvas) should also scale the canvas size --- .../Classes/SDImageWebPCoder.m | 104 +++++++++++------- 1 file changed, 63 insertions(+), 41 deletions(-) diff --git a/SDWebImageWebPCoder/Classes/SDImageWebPCoder.m b/SDWebImageWebPCoder/Classes/SDImageWebPCoder.m index bf6ce09..807be53 100644 --- a/SDWebImageWebPCoder/Classes/SDImageWebPCoder.m +++ b/SDWebImageWebPCoder/Classes/SDImageWebPCoder.m @@ -24,6 +24,38 @@ #import +/// Calculate the actual thumnail pixel size +static CGSize SDCalculateThumbnailSize(CGSize fullSize, BOOL preserveAspectRatio, CGSize thumbnailSize) { + CGFloat width = fullSize.width; + CGFloat height = fullSize.height; + CGFloat resultWidth; + CGFloat resultHeight; + + if (width == 0 || height == 0 || thumbnailSize.width == 0 || thumbnailSize.height == 0 || (width <= thumbnailSize.width && height <= thumbnailSize.height)) { + // Full Pixel + resultWidth = width; + resultHeight = height; + } else { + // Thumbnail + if (preserveAspectRatio) { + CGFloat pixelRatio = width / height; + CGFloat thumbnailRatio = thumbnailSize.width / thumbnailSize.height; + if (pixelRatio > thumbnailRatio) { + resultWidth = thumbnailSize.width; + resultHeight = ceil(thumbnailSize.width / pixelRatio); + } else { + resultHeight = thumbnailSize.height; + resultWidth = ceil(thumbnailSize.height * pixelRatio); + } + } else { + resultWidth = thumbnailSize.width; + resultHeight = thumbnailSize.height; + } + } + + return CGSizeMake(resultWidth, resultHeight); +} + #ifndef SD_LOCK #define SD_LOCK(lock) dispatch_semaphore_wait(lock, DISPATCH_TIME_FOREVER); #endif @@ -64,8 +96,6 @@ BOOL _hasAnimation; BOOL _hasAlpha; BOOL _finished; - CGFloat _canvasWidth; - CGFloat _canvasHeight; dispatch_semaphore_t _lock; NSUInteger _currentBlendIndex; BOOL _preserveAspectRatio; @@ -159,7 +189,7 @@ WebPDemuxDelete(demuxer); return nil; } - CGColorSpaceRef colorSpace = [self sd_colorSpaceWithDemuxer:demuxer]; + CGColorSpaceRef colorSpace = [self sd_createColorSpaceWithDemuxer:demuxer]; if (!hasAnimation || decodeFirstFrame) { // first frame for animated webp image @@ -177,19 +207,14 @@ return firstFrameImage; } - int loopCount = WebPDemuxGetI(demuxer, WEBP_FF_LOOP_COUNT); - int canvasWidth = WebPDemuxGetI(demuxer, WEBP_FF_CANVAS_WIDTH); - int canvasHeight = WebPDemuxGetI(demuxer, WEBP_FF_CANVAS_HEIGHT); - BOOL hasAlpha = flags & ALPHA_FLAG; - CGBitmapInfo bitmapInfo = kCGBitmapByteOrder32Host; - bitmapInfo |= hasAlpha ? kCGImageAlphaPremultipliedFirst : kCGImageAlphaNoneSkipFirst; - CGContextRef canvas = CGBitmapContextCreate(NULL, canvasWidth, canvasHeight, 8, 0, [SDImageCoderHelper colorSpaceGetDeviceRGB], bitmapInfo); + CGContextRef canvas = [self sd_createCanvasWithDemuxer:demuxer colorSpace:colorSpace preserveAspectRatio:preserveAspectRatio thumbnailSize:thumbnailSize]; if (!canvas) { WebPDemuxDelete(demuxer); CGColorSpaceRelease(colorSpace); return nil; } + int loopCount = WebPDemuxGetI(demuxer, WEBP_FF_LOOP_COUNT); NSMutableArray *frames = [NSMutableArray array]; do { @@ -305,7 +330,7 @@ return nil; } - CGContextRef canvas = CGBitmapContextCreate(NULL, width, height, 8, 0, [SDImageCoderHelper colorSpaceGetDeviceRGB], bitmapInfo); + CGContextRef canvas = [self sd_createCanvasWithDemuxer:_demux colorSpace:colorSpaceRef preserveAspectRatio:_preserveAspectRatio thumbnailSize:_thumbnailSize]; if (!canvas) { CGImageRelease(imageRef); return nil; @@ -413,25 +438,12 @@ int width = config.input.width; int height = config.input.height; - if (width == 0 || height == 0 || thumbnailSize.width == 0 || thumbnailSize.height == 0 || (width <= thumbnailSize.width && height <= thumbnailSize.height)) { - // Full Pixel - } else { - // Thumbnail + CGSize resultSize = SDCalculateThumbnailSize(CGSizeMake(width, height), preserveAspectRatio, thumbnailSize); + if (resultSize.width != width || resultSize.height != height) { + // Use scaling config.options.use_scaling = 1; - if (preserveAspectRatio) { - CGFloat pixelRatio = (CGFloat)width / (CGFloat)height; - CGFloat thumbnailRatio = thumbnailSize.width / thumbnailSize.height; - if (pixelRatio > thumbnailRatio) { - config.options.scaled_width = thumbnailSize.width; - config.options.scaled_height = thumbnailSize.width / pixelRatio; - } else { - config.options.scaled_height = thumbnailSize.height; - config.options.scaled_width = thumbnailSize.height * pixelRatio; - } - } else { - config.options.scaled_width = thumbnailSize.width; - config.options.scaled_height = thumbnailSize.height; - } + config.options.scaled_width = resultSize.width; + config.options.scaled_height = resultSize.height; } // Decode the WebP image data into a RGBA value array @@ -451,7 +463,7 @@ size_t bitsPerPixel = 32; size_t bytesPerRow = config.output.u.RGBA.stride; CGColorRenderingIntent renderingIntent = kCGRenderingIntentDefault; - CGImageRef imageRef = CGImageCreate(width, height, bitsPerComponent, bitsPerPixel, bytesPerRow, colorSpaceRef, bitmapInfo, provider, NULL, NO, renderingIntent); + CGImageRef imageRef = CGImageCreate(resultSize.width, resultSize.height, bitsPerComponent, bitsPerPixel, bytesPerRow, colorSpaceRef, bitmapInfo, provider, NULL, NO, renderingIntent); CGDataProviderRelease(provider); @@ -469,7 +481,7 @@ } // Create and return the correct colorspace by checking the ICC Profile -- (nonnull CGColorSpaceRef)sd_colorSpaceWithDemuxer:(nonnull WebPDemuxer *)demuxer CF_RETURNS_RETAINED { +- (nonnull CGColorSpaceRef)sd_createColorSpaceWithDemuxer:(nonnull WebPDemuxer *)demuxer CF_RETURNS_RETAINED { // WebP contains ICC Profile should use the desired colorspace, instead of default device colorspace // See: https://developers.google.com/speed/webp/docs/riff_container#color_profile @@ -508,6 +520,20 @@ return colorSpaceRef; } +- (CGContextRef)sd_createCanvasWithDemuxer:(nonnull WebPDemuxer *)demuxer colorSpace:(nonnull CGColorSpaceRef)colorSpace preserveAspectRatio:(BOOL)preserveAspectRatio thumbnailSize:(CGSize)thumbnailSize CF_RETURNS_RETAINED { + uint32_t flags = WebPDemuxGetI(demuxer, WEBP_FF_FORMAT_FLAGS); + int canvasWidth = WebPDemuxGetI(demuxer, WEBP_FF_CANVAS_WIDTH); + int canvasHeight = WebPDemuxGetI(demuxer, WEBP_FF_CANVAS_HEIGHT); + BOOL hasAlpha = flags & ALPHA_FLAG; + CGBitmapInfo bitmapInfo = kCGBitmapByteOrder32Host; + bitmapInfo |= hasAlpha ? kCGImageAlphaPremultipliedFirst : kCGImageAlphaNoneSkipFirst; + + CGSize canvasSize = SDCalculateThumbnailSize(CGSizeMake(canvasWidth, canvasHeight), preserveAspectRatio, thumbnailSize); + CGContextRef canvas = CGBitmapContextCreate(NULL, canvasSize.width, canvasSize.height, 8, 0, colorSpace, bitmapInfo); + + return canvas; +} + #pragma mark - Encode - (BOOL)canEncodeToFormat:(SDImageFormat)format { return (format == SDImageFormatWebP); @@ -784,8 +810,6 @@ static void FreeImageData(void *info, const void *data, size_t size) { _hasAnimation = hasAnimation; _hasAlpha = hasAlpha; - _canvasWidth = canvasWidth; - _canvasHeight = canvasHeight; _frameCount = frameCount; _loopCount = loopCount; @@ -875,7 +899,7 @@ static void FreeImageData(void *info, const void *data, size_t size) { - (UIImage *)safeStaticImageFrame { UIImage *image; if (!_colorSpace) { - _colorSpace = [self sd_colorSpaceWithDemuxer:_demux]; + _colorSpace = [self sd_createColorSpaceWithDemuxer:_demux]; } // Static WebP image WebPIterator iter; @@ -898,18 +922,16 @@ static void FreeImageData(void *info, const void *data, size_t size) { } - (UIImage *)safeAnimatedImageFrameAtIndex:(NSUInteger)index { + if (!_colorSpace) { + _colorSpace = [self sd_createColorSpaceWithDemuxer:_demux]; + } if (!_canvas) { - CGBitmapInfo bitmapInfo = kCGBitmapByteOrder32Host; - bitmapInfo |= _hasAlpha ? kCGImageAlphaPremultipliedFirst : kCGImageAlphaNoneSkipFirst; - CGContextRef canvas = CGBitmapContextCreate(NULL, _canvasWidth, _canvasHeight, 8, 0, [SDImageCoderHelper colorSpaceGetDeviceRGB], bitmapInfo); + CGContextRef canvas = [self sd_createCanvasWithDemuxer:_demux colorSpace:_colorSpace preserveAspectRatio:_preserveAspectRatio thumbnailSize:_thumbnailSize]; if (!canvas) { return nil; } _canvas = canvas; } - if (!_colorSpace) { - _colorSpace = [self sd_colorSpaceWithDemuxer:_demux]; - } SDWebPCoderFrame *frame = _frames[index]; UIImage *image; @@ -929,7 +951,7 @@ static void FreeImageData(void *info, const void *data, size_t size) { } else { // Else, this can happen when one image set to different imageViews or one loop end. So we should clear the canvas. Then draw until the canvas is ready. if (_currentBlendIndex != NSNotFound) { - CGContextClearRect(_canvas, CGRectMake(0, 0, _canvasWidth, _canvasHeight)); + CGContextClearRect(_canvas, CGRectMake(0, 0, CGBitmapContextGetWidth(_canvas), CGBitmapContextGetHeight(_canvas))); } // Then, loop from the blend from index, draw each of previous frames on the canvas. From e3ba3abc3a1be705c193cb465fc40bd3eea92f35 Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Fri, 17 Jan 2020 16:12:47 +0800 Subject: [PATCH 3/6] Update the Test Case to ensure the thumbnail decoding works --- SDWebImageWebPCoderTests/Podfile | 1 + .../SDWebImageWebPCoderTests.m | 69 +++++++++++++++---- 2 files changed, 55 insertions(+), 15 deletions(-) diff --git a/SDWebImageWebPCoderTests/Podfile b/SDWebImageWebPCoderTests/Podfile index b7812b0..9d0b030 100644 --- a/SDWebImageWebPCoderTests/Podfile +++ b/SDWebImageWebPCoderTests/Podfile @@ -5,5 +5,6 @@ project '../SDWebImageWebPCoder' workspace '../SDWebImageWebPCoder' target 'SDWebImageWebPCoderTests' do + pod 'Expecta' pod 'SDWebImageWebPCoder', :path => '../' end diff --git a/SDWebImageWebPCoderTests/SDWebImageWebPCoderTests.m b/SDWebImageWebPCoderTests/SDWebImageWebPCoderTests.m index d046396..97e9b75 100644 --- a/SDWebImageWebPCoderTests/SDWebImageWebPCoderTests.m +++ b/SDWebImageWebPCoderTests/SDWebImageWebPCoderTests.m @@ -10,6 +10,7 @@ @import XCTest; #import #import +#import #import const int64_t kAsyncTestTimeout = 5; @@ -193,45 +194,83 @@ const int64_t kAsyncTestTimeout = 5; withLocalImageURL:(NSURL *)imageUrl supportsEncoding:(BOOL)supportsEncoding isAnimatedImage:(BOOL)isAnimated { + SDImageFormat encodingFormat = SDImageFormatWebP; + NSData *inputImageData = [NSData dataWithContentsOfURL:imageUrl]; - XCTAssertNotNil(inputImageData, @"Input image data should not be nil"); + expect(inputImageData).toNot.beNil(); SDImageFormat inputImageFormat = [NSData sd_imageFormatForImageData:inputImageData]; - XCTAssert(inputImageFormat != SDImageFormatUndefined, @"Input image format should not be undefined"); + expect(inputImageFormat).toNot.equal(SDImageFormatUndefined); // 1 - check if we can decode - should be true - XCTAssertTrue([coder canDecodeFromData:inputImageData]); + expect([coder canDecodeFromData:inputImageData]).to.beTruthy(); // 2 - decode from NSData to UIImage and check it UIImage *inputImage = [coder decodedImageWithData:inputImageData options:nil]; - XCTAssertNotNil(inputImage, @"The decoded image from input data should not be nil"); + expect(inputImage).toNot.beNil(); if (isAnimated) { // 2a - check images count > 0 (only for animated images) - XCTAssertTrue(inputImage.sd_isAnimated, @"The decoded image should be animated"); + expect(inputImage.sd_isAnimated).to.beTruthy(); // 2b - check image size and scale for each frameImage (only for animated images) #if SD_UIKIT CGSize imageSize = inputImage.size; CGFloat imageScale = inputImage.scale; [inputImage.images enumerateObjectsUsingBlock:^(UIImage * frameImage, NSUInteger idx, BOOL * stop) { - XCTAssertTrue(CGSizeEqualToSize(imageSize, frameImage.size), @"Each frame size should match the image size"); - XCTAssertEqual(imageScale, frameImage.scale, @"Each frame scale should match the image scale"); + expect(imageSize).to.equal(frameImage.size); + expect(imageScale).to.equal(frameImage.scale); }]; #endif } + // 3 - check thumbnail decoding + CGFloat pixelWidth = inputImage.size.width; + CGFloat pixelHeight = inputImage.size.height; + expect(pixelWidth).beGreaterThan(0); + expect(pixelHeight).beGreaterThan(0); + // check thumnail with scratch + CGFloat thumbnailWidth = 50; + CGFloat thumbnailHeight = 50; + UIImage *thumbImage = [coder decodedImageWithData:inputImageData options:@{ + SDImageCoderDecodeThumbnailPixelSize : @(CGSizeMake(thumbnailWidth, thumbnailHeight)), + SDImageCoderDecodePreserveAspectRatio : @(NO) + }]; + expect(thumbImage).toNot.beNil(); + expect(thumbImage.size).equal(CGSizeMake(thumbnailWidth, thumbnailHeight)); + // check thumnail with aspect ratio limit + thumbImage = [coder decodedImageWithData:inputImageData options:@{ + SDImageCoderDecodeThumbnailPixelSize : @(CGSizeMake(thumbnailWidth, thumbnailHeight)), + SDImageCoderDecodePreserveAspectRatio : @(YES) + }]; + expect(thumbImage).toNot.beNil(); + CGFloat ratio = pixelWidth / pixelHeight; + CGFloat thumbnailRatio = thumbnailWidth / thumbnailHeight; + CGSize thumbnailPixelSize; + if (ratio > thumbnailRatio) { + thumbnailPixelSize = CGSizeMake(thumbnailWidth, round(thumbnailWidth / ratio)); + } else { + thumbnailPixelSize = CGSizeMake(round(thumbnailHeight * ratio), thumbnailHeight); + } + // Image/IO's thumbnail API does not always use round to preserve precision, we check ABS <= 1 + expect(ABS(thumbImage.size.width - thumbnailPixelSize.width) <= 1); + expect(ABS(thumbImage.size.height - thumbnailPixelSize.height) <= 1); + + if (supportsEncoding) { - // 3 - check if we can encode to the original format - XCTAssertTrue([coder canEncodeToFormat:inputImageFormat], @"Coder should be able to encode"); + // 4 - check if we can encode to the original format + if (encodingFormat == SDImageFormatUndefined) { + encodingFormat = inputImageFormat; + } + expect([coder canEncodeToFormat:encodingFormat]).to.beTruthy(); - // 4 - encode from UIImage to NSData using the inputImageFormat and check it - NSData *outputImageData = [coder encodedDataWithImage:inputImage format:inputImageFormat options:nil]; - XCTAssertNotNil(outputImageData, @"The encoded image data should not be nil"); + // 5 - encode from UIImage to NSData using the inputImageFormat and check it + NSData *outputImageData = [coder encodedDataWithImage:inputImage format:encodingFormat options:nil]; + expect(outputImageData).toNot.beNil(); UIImage *outputImage = [coder decodedImageWithData:outputImageData options:nil]; - XCTAssertTrue(CGSizeEqualToSize(outputImage.size, inputImage.size), @"Output and input image size should match"); - XCTAssertEqual(outputImage.scale, inputImage.scale, @"Output and input image scale should match"); + expect(outputImage.size).to.equal(inputImage.size); + expect(outputImage.scale).to.equal(inputImage.scale); #if SD_UIKIT - XCTAssertEqual(outputImage.images.count, inputImage.images.count, @"Output and input image frame count should match"); + expect(outputImage.images.count).to.equal(inputImage.images.count); #endif } } From 72d1968d42e8f9da3b16dc249e41099d3b8a6d93 Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Fri, 17 Jan 2020 18:11:11 +0800 Subject: [PATCH 4/6] Fix the animated canvas again, this time should work --- .../Classes/SDImageWebPCoder.m | 70 +++++++++++-------- 1 file changed, 42 insertions(+), 28 deletions(-) diff --git a/SDWebImageWebPCoder/Classes/SDImageWebPCoder.m b/SDWebImageWebPCoder/Classes/SDImageWebPCoder.m index 807be53..104ee32 100644 --- a/SDWebImageWebPCoder/Classes/SDImageWebPCoder.m +++ b/SDWebImageWebPCoder/Classes/SDImageWebPCoder.m @@ -96,6 +96,8 @@ static CGSize SDCalculateThumbnailSize(CGSize fullSize, BOOL preserveAspectRatio BOOL _hasAnimation; BOOL _hasAlpha; BOOL _finished; + CGFloat _canvasWidth; // Full Size without thumbnail scale + CGFloat _canvasHeight; // Full Size without thumbnail scale dispatch_semaphore_t _lock; NSUInteger _currentBlendIndex; BOOL _preserveAspectRatio; @@ -207,7 +209,15 @@ static CGSize SDCalculateThumbnailSize(CGSize fullSize, BOOL preserveAspectRatio return firstFrameImage; } - CGContextRef canvas = [self sd_createCanvasWithDemuxer:demuxer colorSpace:colorSpace preserveAspectRatio:preserveAspectRatio thumbnailSize:thumbnailSize]; + int canvasWidth = WebPDemuxGetI(demuxer, WEBP_FF_CANVAS_WIDTH); + int canvasHeight = WebPDemuxGetI(demuxer, WEBP_FF_CANVAS_HEIGHT); + BOOL hasAlpha = flags & ALPHA_FLAG; + CGBitmapInfo bitmapInfo = kCGBitmapByteOrder32Host; + bitmapInfo |= hasAlpha ? kCGImageAlphaPremultipliedFirst : kCGImageAlphaNoneSkipFirst; + + CGSize canvasFullSize = CGSizeMake(canvasWidth, canvasHeight); + CGSize canvasSize = SDCalculateThumbnailSize(canvasFullSize, preserveAspectRatio, thumbnailSize); + CGContextRef canvas = CGBitmapContextCreate(NULL, canvasSize.width, canvasSize.height, 8, 0, [SDImageCoderHelper colorSpaceGetDeviceRGB], bitmapInfo); if (!canvas) { WebPDemuxDelete(demuxer); CGColorSpaceRelease(colorSpace); @@ -219,7 +229,7 @@ static CGSize SDCalculateThumbnailSize(CGSize fullSize, BOOL preserveAspectRatio do { @autoreleasepool { - CGImageRef imageRef = [self sd_drawnWebpImageWithCanvas:canvas iterator:iter colorSpace:colorSpace preserveAspectRatio:preserveAspectRatio thumbnailSize:thumbnailSize]; + CGImageRef imageRef = [self sd_drawnWebpImageWithCanvas:canvas iterator:iter colorSpace:colorSpace preserveAspectRatio:preserveAspectRatio thumbnailSize:thumbnailSize canvasFullSize:canvasFullSize]; if (!imageRef) { continue; } @@ -330,7 +340,7 @@ static CGSize SDCalculateThumbnailSize(CGSize fullSize, BOOL preserveAspectRatio return nil; } - CGContextRef canvas = [self sd_createCanvasWithDemuxer:_demux colorSpace:colorSpaceRef preserveAspectRatio:_preserveAspectRatio thumbnailSize:_thumbnailSize]; + CGContextRef canvas = CGBitmapContextCreate(NULL, width, height, 8, 0, [SDImageCoderHelper colorSpaceGetDeviceRGB], bitmapInfo); if (!canvas) { CGImageRelease(imageRef); return nil; @@ -367,11 +377,17 @@ static CGSize SDCalculateThumbnailSize(CGSize fullSize, BOOL preserveAspectRatio return image; } -- (void)sd_blendWebpImageWithCanvas:(CGContextRef)canvas iterator:(WebPIterator)iter colorSpace:(nonnull CGColorSpaceRef)colorSpaceRef preserveAspectRatio:(BOOL)preserveAspectRatio thumbnailSize:(CGSize)thumbnailSize { +- (void)sd_blendWebpImageWithCanvas:(CGContextRef)canvas iterator:(WebPIterator)iter colorSpace:(nonnull CGColorSpaceRef)colorSpaceRef preserveAspectRatio:(BOOL)preserveAspectRatio thumbnailSize:(CGSize)thumbnailSize canvasFullSize:(CGSize)canvasFullSize { + size_t canvasWidth = CGBitmapContextGetWidth(canvas); size_t canvasHeight = CGBitmapContextGetHeight(canvas); - CGFloat tmpX = iter.x_offset; - CGFloat tmpY = canvasHeight - iter.height - iter.y_offset; - CGRect imageRect = CGRectMake(tmpX, tmpY, iter.width, iter.height); + CGFloat xScale = canvasWidth / canvasFullSize.width; + CGFloat yScale = canvasHeight / canvasFullSize.height; + + CGFloat tmpX = iter.x_offset * xScale; + CGFloat tmpY = (canvasFullSize.height - iter.height - iter.y_offset) * yScale; + CGFloat tmpWidth = iter.width * xScale; + CGFloat tmpHeight = iter.height * yScale; + CGRect imageRect = CGRectMake(tmpX, tmpY, tmpWidth, tmpHeight); if (iter.dispose_method == WEBP_MUX_DISPOSE_BACKGROUND) { CGContextClearRect(canvas, imageRect); @@ -390,16 +406,22 @@ static CGSize SDCalculateThumbnailSize(CGSize fullSize, BOOL preserveAspectRatio } } -- (nullable CGImageRef)sd_drawnWebpImageWithCanvas:(CGContextRef)canvas iterator:(WebPIterator)iter colorSpace:(nonnull CGColorSpaceRef)colorSpaceRef preserveAspectRatio:(BOOL)preserveAspectRatio thumbnailSize:(CGSize)thumbnailSize CF_RETURNS_RETAINED { +- (nullable CGImageRef)sd_drawnWebpImageWithCanvas:(CGContextRef)canvas iterator:(WebPIterator)iter colorSpace:(nonnull CGColorSpaceRef)colorSpaceRef preserveAspectRatio:(BOOL)preserveAspectRatio thumbnailSize:(CGSize)thumbnailSize canvasFullSize:(CGSize)canvasFullSize CF_RETURNS_RETAINED { CGImageRef imageRef = [self sd_createWebpImageWithData:iter.fragment colorSpace:colorSpaceRef preserveAspectRatio:preserveAspectRatio thumbnailSize:thumbnailSize]; if (!imageRef) { return nil; } + size_t canvasWidth = CGBitmapContextGetWidth(canvas); size_t canvasHeight = CGBitmapContextGetHeight(canvas); - CGFloat tmpX = iter.x_offset; - CGFloat tmpY = canvasHeight - iter.height - iter.y_offset; - CGRect imageRect = CGRectMake(tmpX, tmpY, iter.width, iter.height); + CGFloat xScale = canvasWidth / canvasFullSize.width; + CGFloat yScale = canvasHeight / canvasFullSize.height; + + CGFloat tmpX = iter.x_offset * xScale; + CGFloat tmpY = (canvasFullSize.height - iter.height - iter.y_offset) * yScale; + CGFloat tmpWidth = iter.width * xScale; + CGFloat tmpHeight = iter.height * yScale; + CGRect imageRect = CGRectMake(tmpX, tmpY, tmpWidth, tmpHeight); BOOL shouldBlend = iter.blend_method == WEBP_MUX_BLEND; // If not blend, cover the target image rect. (firstly clear then draw) @@ -520,20 +542,6 @@ static CGSize SDCalculateThumbnailSize(CGSize fullSize, BOOL preserveAspectRatio return colorSpaceRef; } -- (CGContextRef)sd_createCanvasWithDemuxer:(nonnull WebPDemuxer *)demuxer colorSpace:(nonnull CGColorSpaceRef)colorSpace preserveAspectRatio:(BOOL)preserveAspectRatio thumbnailSize:(CGSize)thumbnailSize CF_RETURNS_RETAINED { - uint32_t flags = WebPDemuxGetI(demuxer, WEBP_FF_FORMAT_FLAGS); - int canvasWidth = WebPDemuxGetI(demuxer, WEBP_FF_CANVAS_WIDTH); - int canvasHeight = WebPDemuxGetI(demuxer, WEBP_FF_CANVAS_HEIGHT); - BOOL hasAlpha = flags & ALPHA_FLAG; - CGBitmapInfo bitmapInfo = kCGBitmapByteOrder32Host; - bitmapInfo |= hasAlpha ? kCGImageAlphaPremultipliedFirst : kCGImageAlphaNoneSkipFirst; - - CGSize canvasSize = SDCalculateThumbnailSize(CGSizeMake(canvasWidth, canvasHeight), preserveAspectRatio, thumbnailSize); - CGContextRef canvas = CGBitmapContextCreate(NULL, canvasSize.width, canvasSize.height, 8, 0, colorSpace, bitmapInfo); - - return canvas; -} - #pragma mark - Encode - (BOOL)canEncodeToFormat:(SDImageFormat)format { return (format == SDImageFormatWebP); @@ -810,6 +818,8 @@ static void FreeImageData(void *info, const void *data, size_t size) { _hasAnimation = hasAnimation; _hasAlpha = hasAlpha; + _canvasWidth = canvasWidth; + _canvasHeight = canvasHeight; _frameCount = frameCount; _loopCount = loopCount; @@ -925,8 +935,12 @@ static void FreeImageData(void *info, const void *data, size_t size) { if (!_colorSpace) { _colorSpace = [self sd_createColorSpaceWithDemuxer:_demux]; } + CGSize canvasFullSize = CGSizeMake(_canvasWidth, _canvasHeight); if (!_canvas) { - CGContextRef canvas = [self sd_createCanvasWithDemuxer:_demux colorSpace:_colorSpace preserveAspectRatio:_preserveAspectRatio thumbnailSize:_thumbnailSize]; + CGBitmapInfo bitmapInfo = kCGBitmapByteOrder32Host; + bitmapInfo |= _hasAlpha ? kCGImageAlphaPremultipliedFirst : kCGImageAlphaNoneSkipFirst; + CGSize canvasSize = SDCalculateThumbnailSize(canvasFullSize, _preserveAspectRatio, _thumbnailSize); + CGContextRef canvas = CGBitmapContextCreate(NULL, canvasSize.width, canvasSize.height, 8, 0, [SDImageCoderHelper colorSpaceGetDeviceRGB], bitmapInfo); if (!canvas) { return nil; } @@ -967,7 +981,7 @@ static void FreeImageData(void *info, const void *data, size_t size) { if (endIndex > startIndex) { do { @autoreleasepool { - [self sd_blendWebpImageWithCanvas:_canvas iterator:iter colorSpace:_colorSpace preserveAspectRatio:_preserveAspectRatio thumbnailSize:_thumbnailSize]; + [self sd_blendWebpImageWithCanvas:_canvas iterator:iter colorSpace:_colorSpace preserveAspectRatio:_preserveAspectRatio thumbnailSize:_thumbnailSize canvasFullSize:canvasFullSize]; } } while ((size_t)iter.frame_num < endIndex && WebPDemuxNextFrame(&iter)); } @@ -980,7 +994,7 @@ static void FreeImageData(void *info, const void *data, size_t size) { _currentBlendIndex = index; // Now the canvas is ready, which respects of dispose method behavior. Just do normal decoding and produce image. - CGImageRef imageRef = [self sd_drawnWebpImageWithCanvas:_canvas iterator:iter colorSpace:_colorSpace preserveAspectRatio:_preserveAspectRatio thumbnailSize:_thumbnailSize]; + CGImageRef imageRef = [self sd_drawnWebpImageWithCanvas:_canvas iterator:iter colorSpace:_colorSpace preserveAspectRatio:_preserveAspectRatio thumbnailSize:_thumbnailSize canvasFullSize:canvasFullSize]; if (!imageRef) { return nil; } From f6ff82be42cf27349eabce09a339a2fe3dfe12a4 Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Sat, 18 Jan 2020 00:36:47 +0800 Subject: [PATCH 5/6] The Animated WebP should not use the scaled canvas size, which will cause the draw frame contains Jagged and buggy. Instead, use the full pixels canvas to draw, scale down each frame after drawn (sadlly) --- .../Classes/SDImageWebPCoder.m | 85 +++++++------------ 1 file changed, 33 insertions(+), 52 deletions(-) diff --git a/SDWebImageWebPCoder/Classes/SDImageWebPCoder.m b/SDWebImageWebPCoder/Classes/SDImageWebPCoder.m index 104ee32..89094d7 100644 --- a/SDWebImageWebPCoder/Classes/SDImageWebPCoder.m +++ b/SDWebImageWebPCoder/Classes/SDImageWebPCoder.m @@ -96,8 +96,8 @@ static CGSize SDCalculateThumbnailSize(CGSize fullSize, BOOL preserveAspectRatio BOOL _hasAnimation; BOOL _hasAlpha; BOOL _finished; - CGFloat _canvasWidth; // Full Size without thumbnail scale - CGFloat _canvasHeight; // Full Size without thumbnail scale + CGFloat _canvasWidth; + CGFloat _canvasHeight; dispatch_semaphore_t _lock; NSUInteger _currentBlendIndex; BOOL _preserveAspectRatio; @@ -192,10 +192,14 @@ static CGSize SDCalculateThumbnailSize(CGSize fullSize, BOOL preserveAspectRatio return nil; } CGColorSpaceRef colorSpace = [self sd_createColorSpaceWithDemuxer:demuxer]; + int canvasWidth = WebPDemuxGetI(demuxer, WEBP_FF_CANVAS_WIDTH); + int canvasHeight = WebPDemuxGetI(demuxer, WEBP_FF_CANVAS_HEIGHT); if (!hasAnimation || decodeFirstFrame) { // first frame for animated webp image - CGImageRef imageRef = [self sd_createWebpImageWithData:iter.fragment colorSpace:colorSpace preserveAspectRatio:preserveAspectRatio thumbnailSize:thumbnailSize]; + CGSize scaledSize = SDCalculateThumbnailSize(CGSizeMake(canvasWidth, canvasHeight), preserveAspectRatio, thumbnailSize); + // Create thumbnail if need + CGImageRef imageRef = [self sd_createWebpImageWithData:iter.fragment colorSpace:colorSpace scaledSize:scaledSize]; CGColorSpaceRelease(colorSpace); #if SD_UIKIT || SD_WATCH UIImage *firstFrameImage = [[UIImage alloc] initWithCGImage:imageRef scale:scale orientation:UIImageOrientationUp]; @@ -209,15 +213,10 @@ static CGSize SDCalculateThumbnailSize(CGSize fullSize, BOOL preserveAspectRatio return firstFrameImage; } - int canvasWidth = WebPDemuxGetI(demuxer, WEBP_FF_CANVAS_WIDTH); - int canvasHeight = WebPDemuxGetI(demuxer, WEBP_FF_CANVAS_HEIGHT); BOOL hasAlpha = flags & ALPHA_FLAG; CGBitmapInfo bitmapInfo = kCGBitmapByteOrder32Host; bitmapInfo |= hasAlpha ? kCGImageAlphaPremultipliedFirst : kCGImageAlphaNoneSkipFirst; - - CGSize canvasFullSize = CGSizeMake(canvasWidth, canvasHeight); - CGSize canvasSize = SDCalculateThumbnailSize(canvasFullSize, preserveAspectRatio, thumbnailSize); - CGContextRef canvas = CGBitmapContextCreate(NULL, canvasSize.width, canvasSize.height, 8, 0, [SDImageCoderHelper colorSpaceGetDeviceRGB], bitmapInfo); + CGContextRef canvas = CGBitmapContextCreate(NULL, canvasWidth, canvasHeight, 8, 0, [SDImageCoderHelper colorSpaceGetDeviceRGB], bitmapInfo); if (!canvas) { WebPDemuxDelete(demuxer); CGColorSpaceRelease(colorSpace); @@ -229,7 +228,7 @@ static CGSize SDCalculateThumbnailSize(CGSize fullSize, BOOL preserveAspectRatio do { @autoreleasepool { - CGImageRef imageRef = [self sd_drawnWebpImageWithCanvas:canvas iterator:iter colorSpace:colorSpace preserveAspectRatio:preserveAspectRatio thumbnailSize:thumbnailSize canvasFullSize:canvasFullSize]; + CGImageRef imageRef = [self sd_drawnWebpImageWithCanvas:canvas iterator:iter colorSpace:colorSpace]; if (!imageRef) { continue; } @@ -377,22 +376,16 @@ static CGSize SDCalculateThumbnailSize(CGSize fullSize, BOOL preserveAspectRatio return image; } -- (void)sd_blendWebpImageWithCanvas:(CGContextRef)canvas iterator:(WebPIterator)iter colorSpace:(nonnull CGColorSpaceRef)colorSpaceRef preserveAspectRatio:(BOOL)preserveAspectRatio thumbnailSize:(CGSize)thumbnailSize canvasFullSize:(CGSize)canvasFullSize { - size_t canvasWidth = CGBitmapContextGetWidth(canvas); +- (void)sd_blendWebpImageWithCanvas:(CGContextRef)canvas iterator:(WebPIterator)iter colorSpace:(nonnull CGColorSpaceRef)colorSpaceRef { size_t canvasHeight = CGBitmapContextGetHeight(canvas); - CGFloat xScale = canvasWidth / canvasFullSize.width; - CGFloat yScale = canvasHeight / canvasFullSize.height; - - CGFloat tmpX = iter.x_offset * xScale; - CGFloat tmpY = (canvasFullSize.height - iter.height - iter.y_offset) * yScale; - CGFloat tmpWidth = iter.width * xScale; - CGFloat tmpHeight = iter.height * yScale; - CGRect imageRect = CGRectMake(tmpX, tmpY, tmpWidth, tmpHeight); + CGFloat tmpX = iter.x_offset; + CGFloat tmpY = canvasHeight - iter.height - iter.y_offset; + CGRect imageRect = CGRectMake(tmpX, tmpY, iter.width, iter.height); if (iter.dispose_method == WEBP_MUX_DISPOSE_BACKGROUND) { CGContextClearRect(canvas, imageRect); } else { - CGImageRef imageRef = [self sd_createWebpImageWithData:iter.fragment colorSpace:colorSpaceRef preserveAspectRatio:preserveAspectRatio thumbnailSize:thumbnailSize]; + CGImageRef imageRef = [self sd_createWebpImageWithData:iter.fragment colorSpace:colorSpaceRef scaledSize:CGSizeZero]; if (!imageRef) { return; } @@ -406,22 +399,17 @@ static CGSize SDCalculateThumbnailSize(CGSize fullSize, BOOL preserveAspectRatio } } -- (nullable CGImageRef)sd_drawnWebpImageWithCanvas:(CGContextRef)canvas iterator:(WebPIterator)iter colorSpace:(nonnull CGColorSpaceRef)colorSpaceRef preserveAspectRatio:(BOOL)preserveAspectRatio thumbnailSize:(CGSize)thumbnailSize canvasFullSize:(CGSize)canvasFullSize CF_RETURNS_RETAINED { - CGImageRef imageRef = [self sd_createWebpImageWithData:iter.fragment colorSpace:colorSpaceRef preserveAspectRatio:preserveAspectRatio thumbnailSize:thumbnailSize]; +- (nullable CGImageRef)sd_drawnWebpImageWithCanvas:(CGContextRef)canvas iterator:(WebPIterator)iter colorSpace:(nonnull CGColorSpaceRef)colorSpaceRef CF_RETURNS_RETAINED { + CGImageRef imageRef = [self sd_createWebpImageWithData:iter.fragment colorSpace:colorSpaceRef scaledSize:CGSizeZero]; if (!imageRef) { return nil; } - size_t canvasWidth = CGBitmapContextGetWidth(canvas); size_t canvasHeight = CGBitmapContextGetHeight(canvas); - CGFloat xScale = canvasWidth / canvasFullSize.width; - CGFloat yScale = canvasHeight / canvasFullSize.height; + CGFloat tmpX = iter.x_offset; + CGFloat tmpY = canvasHeight - iter.height - iter.y_offset; + CGRect imageRect = CGRectMake(tmpX, tmpY, iter.width, iter.height); - CGFloat tmpX = iter.x_offset * xScale; - CGFloat tmpY = (canvasFullSize.height - iter.height - iter.y_offset) * yScale; - CGFloat tmpWidth = iter.width * xScale; - CGFloat tmpHeight = iter.height * yScale; - CGRect imageRect = CGRectMake(tmpX, tmpY, tmpWidth, tmpHeight); BOOL shouldBlend = iter.blend_method == WEBP_MUX_BLEND; // If not blend, cover the target image rect. (firstly clear then draw) @@ -432,7 +420,7 @@ static CGSize SDCalculateThumbnailSize(CGSize fullSize, BOOL preserveAspectRatio CGImageRef newImageRef = CGBitmapContextCreateImage(canvas); CGImageRelease(imageRef); - + if (iter.dispose_method == WEBP_MUX_DISPOSE_BACKGROUND) { CGContextClearRect(canvas, imageRect); } @@ -440,7 +428,7 @@ static CGSize SDCalculateThumbnailSize(CGSize fullSize, BOOL preserveAspectRatio return newImageRef; } -- (nullable CGImageRef)sd_createWebpImageWithData:(WebPData)webpData colorSpace:(nonnull CGColorSpaceRef)colorSpaceRef preserveAspectRatio:(BOOL)preserveAspectRatio thumbnailSize:(CGSize)thumbnailSize CF_RETURNS_RETAINED { +- (nullable CGImageRef)sd_createWebpImageWithData:(WebPData)webpData colorSpace:(nonnull CGColorSpaceRef)colorSpaceRef scaledSize:(CGSize)scaledSize CF_RETURNS_RETAINED { WebPDecoderConfig config; if (!WebPInitDecoderConfig(&config)) { return nil; @@ -458,14 +446,11 @@ static CGSize SDCalculateThumbnailSize(CGSize fullSize, BOOL preserveAspectRatio config.options.use_threads = 1; config.output.colorspace = MODE_bgrA; - int width = config.input.width; - int height = config.input.height; - CGSize resultSize = SDCalculateThumbnailSize(CGSizeMake(width, height), preserveAspectRatio, thumbnailSize); - if (resultSize.width != width || resultSize.height != height) { - // Use scaling + // Use scaling for thumbnail + if (scaledSize.width != 0 && scaledSize.height != 0) { config.options.use_scaling = 1; - config.options.scaled_width = resultSize.width; - config.options.scaled_height = resultSize.height; + config.options.scaled_width = scaledSize.width; + config.options.scaled_height = scaledSize.height; } // Decode the WebP image data into a RGBA value array @@ -473,19 +458,16 @@ static CGSize SDCalculateThumbnailSize(CGSize fullSize, BOOL preserveAspectRatio return nil; } - if (config.options.use_scaling) { - width = config.options.scaled_width; - height = config.options.scaled_height; - } - // Construct a UIImage from the decoded RGBA value array CGDataProviderRef provider = CGDataProviderCreateWithData(NULL, config.output.u.RGBA.rgba, config.output.u.RGBA.size, FreeImageData); size_t bitsPerComponent = 8; size_t bitsPerPixel = 32; size_t bytesPerRow = config.output.u.RGBA.stride; + size_t width = config.output.width; + size_t height = config.output.height; CGColorRenderingIntent renderingIntent = kCGRenderingIntentDefault; - CGImageRef imageRef = CGImageCreate(resultSize.width, resultSize.height, bitsPerComponent, bitsPerPixel, bytesPerRow, colorSpaceRef, bitmapInfo, provider, NULL, NO, renderingIntent); + CGImageRef imageRef = CGImageCreate(width, height, bitsPerComponent, bitsPerPixel, bytesPerRow, colorSpaceRef, bitmapInfo, provider, NULL, NO, renderingIntent); CGDataProviderRelease(provider); @@ -917,7 +899,8 @@ static void FreeImageData(void *info, const void *data, size_t size) { WebPDemuxReleaseIterator(&iter); return nil; } - CGImageRef imageRef = [self sd_createWebpImageWithData:iter.fragment colorSpace:_colorSpace preserveAspectRatio:_preserveAspectRatio thumbnailSize:_thumbnailSize]; + CGSize scaledSize = SDCalculateThumbnailSize(CGSizeMake(_canvasWidth, _canvasHeight), _preserveAspectRatio, _thumbnailSize); + CGImageRef imageRef = [self sd_createWebpImageWithData:iter.fragment colorSpace:_colorSpace scaledSize:scaledSize]; if (!imageRef) { return nil; } @@ -935,12 +918,10 @@ static void FreeImageData(void *info, const void *data, size_t size) { if (!_colorSpace) { _colorSpace = [self sd_createColorSpaceWithDemuxer:_demux]; } - CGSize canvasFullSize = CGSizeMake(_canvasWidth, _canvasHeight); if (!_canvas) { CGBitmapInfo bitmapInfo = kCGBitmapByteOrder32Host; bitmapInfo |= _hasAlpha ? kCGImageAlphaPremultipliedFirst : kCGImageAlphaNoneSkipFirst; - CGSize canvasSize = SDCalculateThumbnailSize(canvasFullSize, _preserveAspectRatio, _thumbnailSize); - CGContextRef canvas = CGBitmapContextCreate(NULL, canvasSize.width, canvasSize.height, 8, 0, [SDImageCoderHelper colorSpaceGetDeviceRGB], bitmapInfo); + CGContextRef canvas = CGBitmapContextCreate(NULL, _canvasWidth, _canvasHeight, 8, 0, [SDImageCoderHelper colorSpaceGetDeviceRGB], bitmapInfo); if (!canvas) { return nil; } @@ -981,7 +962,7 @@ static void FreeImageData(void *info, const void *data, size_t size) { if (endIndex > startIndex) { do { @autoreleasepool { - [self sd_blendWebpImageWithCanvas:_canvas iterator:iter colorSpace:_colorSpace preserveAspectRatio:_preserveAspectRatio thumbnailSize:_thumbnailSize canvasFullSize:canvasFullSize]; + [self sd_blendWebpImageWithCanvas:_canvas iterator:iter colorSpace:_colorSpace]; } } while ((size_t)iter.frame_num < endIndex && WebPDemuxNextFrame(&iter)); } @@ -994,7 +975,7 @@ static void FreeImageData(void *info, const void *data, size_t size) { _currentBlendIndex = index; // Now the canvas is ready, which respects of dispose method behavior. Just do normal decoding and produce image. - CGImageRef imageRef = [self sd_drawnWebpImageWithCanvas:_canvas iterator:iter colorSpace:_colorSpace preserveAspectRatio:_preserveAspectRatio thumbnailSize:_thumbnailSize canvasFullSize:canvasFullSize]; + CGImageRef imageRef = [self sd_drawnWebpImageWithCanvas:_canvas iterator:iter colorSpace:_colorSpace]; if (!imageRef) { return nil; } From 672633969068b0e700b84e2aa3d66d58638ffa8c Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Sat, 18 Jan 2020 01:01:39 +0800 Subject: [PATCH 6/6] Re-implements the animated webp's thumbnail decoding, now looks better --- .../Classes/SDImageWebPCoder.m | 34 ++++++++++++++----- 1 file changed, 25 insertions(+), 9 deletions(-) diff --git a/SDWebImageWebPCoder/Classes/SDImageWebPCoder.m b/SDWebImageWebPCoder/Classes/SDImageWebPCoder.m index 89094d7..8855bf7 100644 --- a/SDWebImageWebPCoder/Classes/SDImageWebPCoder.m +++ b/SDWebImageWebPCoder/Classes/SDImageWebPCoder.m @@ -194,11 +194,11 @@ static CGSize SDCalculateThumbnailSize(CGSize fullSize, BOOL preserveAspectRatio CGColorSpaceRef colorSpace = [self sd_createColorSpaceWithDemuxer:demuxer]; int canvasWidth = WebPDemuxGetI(demuxer, WEBP_FF_CANVAS_WIDTH); int canvasHeight = WebPDemuxGetI(demuxer, WEBP_FF_CANVAS_HEIGHT); + // Check whether we need to use thumbnail + CGSize scaledSize = SDCalculateThumbnailSize(CGSizeMake(canvasWidth, canvasHeight), preserveAspectRatio, thumbnailSize); if (!hasAnimation || decodeFirstFrame) { // first frame for animated webp image - CGSize scaledSize = SDCalculateThumbnailSize(CGSizeMake(canvasWidth, canvasHeight), preserveAspectRatio, thumbnailSize); - // Create thumbnail if need CGImageRef imageRef = [self sd_createWebpImageWithData:iter.fragment colorSpace:colorSpace scaledSize:scaledSize]; CGColorSpaceRelease(colorSpace); #if SD_UIKIT || SD_WATCH @@ -228,10 +228,11 @@ static CGSize SDCalculateThumbnailSize(CGSize fullSize, BOOL preserveAspectRatio do { @autoreleasepool { - CGImageRef imageRef = [self sd_drawnWebpImageWithCanvas:canvas iterator:iter colorSpace:colorSpace]; + CGImageRef imageRef = [self sd_drawnWebpImageWithCanvas:canvas iterator:iter colorSpace:colorSpace scaledSize:scaledSize]; if (!imageRef) { continue; } + #if SD_UIKIT || SD_WATCH UIImage *image = [[UIImage alloc] initWithCGImage:imageRef scale:scale orientation:UIImageOrientationUp]; #else @@ -399,12 +400,13 @@ static CGSize SDCalculateThumbnailSize(CGSize fullSize, BOOL preserveAspectRatio } } -- (nullable CGImageRef)sd_drawnWebpImageWithCanvas:(CGContextRef)canvas iterator:(WebPIterator)iter colorSpace:(nonnull CGColorSpaceRef)colorSpaceRef CF_RETURNS_RETAINED { +- (nullable CGImageRef)sd_drawnWebpImageWithCanvas:(CGContextRef)canvas iterator:(WebPIterator)iter colorSpace:(nonnull CGColorSpaceRef)colorSpaceRef scaledSize:(CGSize)scaledSize CF_RETURNS_RETAINED { CGImageRef imageRef = [self sd_createWebpImageWithData:iter.fragment colorSpace:colorSpaceRef scaledSize:CGSizeZero]; if (!imageRef) { return nil; } + size_t canvasWidth = CGBitmapContextGetWidth(canvas); size_t canvasHeight = CGBitmapContextGetHeight(canvas); CGFloat tmpX = iter.x_offset; CGFloat tmpY = canvasHeight - iter.height - iter.y_offset; @@ -425,6 +427,17 @@ static CGSize SDCalculateThumbnailSize(CGSize fullSize, BOOL preserveAspectRatio CGContextClearRect(canvas, imageRect); } + // Check whether we need to use thumbnail + if (!CGSizeEqualToSize(CGSizeMake(canvasWidth, canvasHeight), scaledSize)) { + // Important: For Animated WebP thumbnail generation, we can not just use a scaled small canvas and draw each thumbnail frame + // This works **On Theory**. However, image scale down loss details. Animated WebP use the partial pixels with blend mode / dispose method with offset, to cover previous canvas status + // Because of this reason, even each frame contains small zigzag, the final animation contains visible glitch, this is not we want. + // So, always create the full pixels canvas (even though this consume more RAM), after drawn on the canvas, re-scale again with the final size + CGImageRef scaledImageRef = [SDImageCoderHelper CGImageCreateScaled:newImageRef size:scaledSize]; + CGImageRelease(newImageRef); + newImageRef = scaledImageRef; + } + return newImageRef; } @@ -899,6 +912,7 @@ static void FreeImageData(void *info, const void *data, size_t size) { WebPDemuxReleaseIterator(&iter); return nil; } + // Check whether we need to use thumbnail CGSize scaledSize = SDCalculateThumbnailSize(CGSizeMake(_canvasWidth, _canvasHeight), _preserveAspectRatio, _thumbnailSize); CGImageRef imageRef = [self sd_createWebpImageWithData:iter.fragment colorSpace:_colorSpace scaledSize:scaledSize]; if (!imageRef) { @@ -915,9 +929,6 @@ static void FreeImageData(void *info, const void *data, size_t size) { } - (UIImage *)safeAnimatedImageFrameAtIndex:(NSUInteger)index { - if (!_colorSpace) { - _colorSpace = [self sd_createColorSpaceWithDemuxer:_demux]; - } if (!_canvas) { CGBitmapInfo bitmapInfo = kCGBitmapByteOrder32Host; bitmapInfo |= _hasAlpha ? kCGImageAlphaPremultipliedFirst : kCGImageAlphaNoneSkipFirst; @@ -927,6 +938,9 @@ static void FreeImageData(void *info, const void *data, size_t size) { } _canvas = canvas; } + if (!_colorSpace) { + _colorSpace = [self sd_createColorSpaceWithDemuxer:_demux]; + } SDWebPCoderFrame *frame = _frames[index]; UIImage *image; @@ -946,7 +960,7 @@ static void FreeImageData(void *info, const void *data, size_t size) { } else { // Else, this can happen when one image set to different imageViews or one loop end. So we should clear the canvas. Then draw until the canvas is ready. if (_currentBlendIndex != NSNotFound) { - CGContextClearRect(_canvas, CGRectMake(0, 0, CGBitmapContextGetWidth(_canvas), CGBitmapContextGetHeight(_canvas))); + CGContextClearRect(_canvas, CGRectMake(0, 0, _canvasWidth, _canvasHeight)); } // Then, loop from the blend from index, draw each of previous frames on the canvas. @@ -975,7 +989,9 @@ static void FreeImageData(void *info, const void *data, size_t size) { _currentBlendIndex = index; // Now the canvas is ready, which respects of dispose method behavior. Just do normal decoding and produce image. - CGImageRef imageRef = [self sd_drawnWebpImageWithCanvas:_canvas iterator:iter colorSpace:_colorSpace]; + // Check whether we need to use thumbnail + CGSize scaledSize = SDCalculateThumbnailSize(CGSizeMake(_canvasWidth, _canvasHeight), _preserveAspectRatio, _thumbnailSize); + CGImageRef imageRef = [self sd_drawnWebpImageWithCanvas:_canvas iterator:iter colorSpace:_colorSpace scaledSize:scaledSize]; if (!imageRef) { return nil; }