2424
2525#import < Accelerate/Accelerate.h>
2626
27+ // / Calculate the actual thumnail pixel size
28+ static CGSize SDCalculateThumbnailSize (CGSize fullSize, BOOL preserveAspectRatio, CGSize thumbnailSize) {
29+ CGFloat width = fullSize.width ;
30+ CGFloat height = fullSize.height ;
31+ CGFloat resultWidth;
32+ CGFloat resultHeight;
33+
34+ if (width == 0 || height == 0 || thumbnailSize.width == 0 || thumbnailSize.height == 0 || (width <= thumbnailSize.width && height <= thumbnailSize.height )) {
35+ // Full Pixel
36+ resultWidth = width;
37+ resultHeight = height;
38+ } else {
39+ // Thumbnail
40+ if (preserveAspectRatio) {
41+ CGFloat pixelRatio = width / height;
42+ CGFloat thumbnailRatio = thumbnailSize.width / thumbnailSize.height ;
43+ if (pixelRatio > thumbnailRatio) {
44+ resultWidth = thumbnailSize.width ;
45+ resultHeight = ceil (thumbnailSize.width / pixelRatio);
46+ } else {
47+ resultHeight = thumbnailSize.height ;
48+ resultWidth = ceil (thumbnailSize.height * pixelRatio);
49+ }
50+ } else {
51+ resultWidth = thumbnailSize.width ;
52+ resultHeight = thumbnailSize.height ;
53+ }
54+ }
55+
56+ return CGSizeMake (resultWidth, resultHeight);
57+ }
58+
2759#ifndef SD_LOCK
2860#define SD_LOCK (lock ) dispatch_semaphore_wait(lock, DISPATCH_TIME_FOREVER);
2961#endif
@@ -68,6 +100,8 @@ @implementation SDImageWebPCoder {
68100 CGFloat _canvasHeight;
69101 dispatch_semaphore_t _lock;
70102 NSUInteger _currentBlendIndex;
103+ BOOL _preserveAspectRatio;
104+ CGSize _thumbnailSize;
71105}
72106
73107- (void )dealloc {
@@ -133,6 +167,22 @@ - (UIImage *)decodedImageWithData:(NSData *)data options:(nullable SDImageCoderO
133167 }
134168 }
135169
170+ CGSize thumbnailSize = CGSizeZero;
171+ NSValue *thumbnailSizeValue = options[SDImageCoderDecodeThumbnailPixelSize];
172+ if (thumbnailSizeValue != nil ) {
173+ #if SD_MAC
174+ thumbnailSize = thumbnailSizeValue.sizeValue ;
175+ #else
176+ thumbnailSize = thumbnailSizeValue.CGSizeValue ;
177+ #endif
178+ }
179+
180+ BOOL preserveAspectRatio = YES ;
181+ NSNumber *preserveAspectRatioValue = options[SDImageCoderDecodePreserveAspectRatio];
182+ if (preserveAspectRatioValue != nil ) {
183+ preserveAspectRatio = preserveAspectRatioValue.boolValue ;
184+ }
185+
136186 // for animated webp image
137187 WebPIterator iter;
138188 // libwebp's index start with 1
@@ -141,11 +191,15 @@ - (UIImage *)decodedImageWithData:(NSData *)data options:(nullable SDImageCoderO
141191 WebPDemuxDelete (demuxer);
142192 return nil ;
143193 }
144- CGColorSpaceRef colorSpace = [self sd_colorSpaceWithDemuxer: demuxer];
194+ CGColorSpaceRef colorSpace = [self sd_createColorSpaceWithDemuxer: demuxer];
195+ int canvasWidth = WebPDemuxGetI (demuxer, WEBP_FF_CANVAS_WIDTH);
196+ int canvasHeight = WebPDemuxGetI (demuxer, WEBP_FF_CANVAS_HEIGHT);
197+ // Check whether we need to use thumbnail
198+ CGSize scaledSize = SDCalculateThumbnailSize (CGSizeMake (canvasWidth, canvasHeight), preserveAspectRatio, thumbnailSize);
145199
146200 if (!hasAnimation || decodeFirstFrame) {
147201 // first frame for animated webp image
148- CGImageRef imageRef = [self sd_createWebpImageWithData: iter.fragment colorSpace: colorSpace];
202+ CGImageRef imageRef = [self sd_createWebpImageWithData: iter.fragment colorSpace: colorSpace scaledSize: scaledSize ];
149203 CGColorSpaceRelease (colorSpace);
150204#if SD_UIKIT || SD_WATCH
151205 UIImage *firstFrameImage = [[UIImage alloc ] initWithCGImage: imageRef scale: scale orientation: UIImageOrientationUp];
@@ -159,9 +213,6 @@ - (UIImage *)decodedImageWithData:(NSData *)data options:(nullable SDImageCoderO
159213 return firstFrameImage;
160214 }
161215
162- int loopCount = WebPDemuxGetI (demuxer, WEBP_FF_LOOP_COUNT);
163- int canvasWidth = WebPDemuxGetI (demuxer, WEBP_FF_CANVAS_WIDTH);
164- int canvasHeight = WebPDemuxGetI (demuxer, WEBP_FF_CANVAS_HEIGHT);
165216 BOOL hasAlpha = flags & ALPHA_FLAG;
166217 CGBitmapInfo bitmapInfo = kCGBitmapByteOrder32Host ;
167218 bitmapInfo |= hasAlpha ? kCGImageAlphaPremultipliedFirst : kCGImageAlphaNoneSkipFirst ;
@@ -172,14 +223,16 @@ - (UIImage *)decodedImageWithData:(NSData *)data options:(nullable SDImageCoderO
172223 return nil ;
173224 }
174225
226+ int loopCount = WebPDemuxGetI (demuxer, WEBP_FF_LOOP_COUNT);
175227 NSMutableArray <SDImageFrame *> *frames = [NSMutableArray array ];
176228
177229 do {
178230 @autoreleasepool {
179- CGImageRef imageRef = [self sd_drawnWebpImageWithCanvas: canvas iterator: iter colorSpace: colorSpace];
231+ CGImageRef imageRef = [self sd_drawnWebpImageWithCanvas: canvas iterator: iter colorSpace: colorSpace scaledSize: scaledSize ];
180232 if (!imageRef) {
181233 continue ;
182234 }
235+
183236#if SD_UIKIT || SD_WATCH
184237 UIImage *image = [[UIImage alloc ] initWithCGImage: imageRef scale: scale orientation: UIImageOrientationUp];
185238#else
@@ -221,6 +274,22 @@ - (instancetype)initIncrementalWithOptions:(nullable SDImageCoderOptions *)optio
221274 }
222275 }
223276 _scale = scale;
277+ CGSize thumbnailSize = CGSizeZero;
278+ NSValue *thumbnailSizeValue = options[SDImageCoderDecodeThumbnailPixelSize];
279+ if (thumbnailSizeValue != nil ) {
280+ #if SD_MAC
281+ thumbnailSize = thumbnailSizeValue.sizeValue ;
282+ #else
283+ thumbnailSize = thumbnailSizeValue.CGSizeValue ;
284+ #endif
285+ }
286+ _thumbnailSize = thumbnailSize;
287+ BOOL preserveAspectRatio = YES ;
288+ NSNumber *preserveAspectRatioValue = options[SDImageCoderDecodePreserveAspectRatio];
289+ if (preserveAspectRatioValue != nil ) {
290+ preserveAspectRatio = preserveAspectRatioValue.boolValue ;
291+ }
292+ _preserveAspectRatio = preserveAspectRatio;
224293 }
225294 return self;
226295}
@@ -317,7 +386,7 @@ - (void)sd_blendWebpImageWithCanvas:(CGContextRef)canvas iterator:(WebPIterator)
317386 if (iter.dispose_method == WEBP_MUX_DISPOSE_BACKGROUND) {
318387 CGContextClearRect (canvas, imageRect);
319388 } else {
320- CGImageRef imageRef = [self sd_createWebpImageWithData: iter.fragment colorSpace: colorSpaceRef];
389+ CGImageRef imageRef = [self sd_createWebpImageWithData: iter.fragment colorSpace: colorSpaceRef scaledSize: CGSizeZero ];
321390 if (!imageRef) {
322391 return ;
323392 }
@@ -331,16 +400,18 @@ - (void)sd_blendWebpImageWithCanvas:(CGContextRef)canvas iterator:(WebPIterator)
331400 }
332401}
333402
334- - (nullable CGImageRef)sd_drawnWebpImageWithCanvas : (CGContextRef)canvas iterator : (WebPIterator)iter colorSpace : (nonnull CGColorSpaceRef)colorSpaceRef CF_RETURNS_RETAINED {
335- CGImageRef imageRef = [self sd_createWebpImageWithData: iter.fragment colorSpace: colorSpaceRef];
403+ - (nullable CGImageRef)sd_drawnWebpImageWithCanvas : (CGContextRef)canvas iterator : (WebPIterator)iter colorSpace : (nonnull CGColorSpaceRef)colorSpaceRef scaledSize : (CGSize) scaledSize CF_RETURNS_RETAINED {
404+ CGImageRef imageRef = [self sd_createWebpImageWithData: iter.fragment colorSpace: colorSpaceRef scaledSize: CGSizeZero ];
336405 if (!imageRef) {
337406 return nil ;
338407 }
339408
409+ size_t canvasWidth = CGBitmapContextGetWidth (canvas);
340410 size_t canvasHeight = CGBitmapContextGetHeight (canvas);
341411 CGFloat tmpX = iter.x_offset ;
342412 CGFloat tmpY = canvasHeight - iter.height - iter.y_offset ;
343413 CGRect imageRect = CGRectMake (tmpX, tmpY, iter.width , iter.height );
414+
344415 BOOL shouldBlend = iter.blend_method == WEBP_MUX_BLEND;
345416
346417 // If not blend, cover the target image rect. (firstly clear then draw)
@@ -351,15 +422,26 @@ - (nullable CGImageRef)sd_drawnWebpImageWithCanvas:(CGContextRef)canvas iterator
351422 CGImageRef newImageRef = CGBitmapContextCreateImage (canvas);
352423
353424 CGImageRelease (imageRef);
354-
425+
355426 if (iter.dispose_method == WEBP_MUX_DISPOSE_BACKGROUND) {
356427 CGContextClearRect (canvas, imageRect);
357428 }
358429
430+ // Check whether we need to use thumbnail
431+ if (!CGSizeEqualToSize (CGSizeMake (canvasWidth, canvasHeight), scaledSize)) {
432+ // Important: For Animated WebP thumbnail generation, we can not just use a scaled small canvas and draw each thumbnail frame
433+ // 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
434+ // Because of this reason, even each frame contains small zigzag, the final animation contains visible glitch, this is not we want.
435+ // 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
436+ CGImageRef scaledImageRef = [SDImageCoderHelper CGImageCreateScaled: newImageRef size: scaledSize];
437+ CGImageRelease (newImageRef);
438+ newImageRef = scaledImageRef;
439+ }
440+
359441 return newImageRef;
360442}
361443
362- - (nullable CGImageRef)sd_createWebpImageWithData : (WebPData)webpData colorSpace : (nonnull CGColorSpaceRef)colorSpaceRef CF_RETURNS_RETAINED {
444+ - (nullable CGImageRef)sd_createWebpImageWithData : (WebPData)webpData colorSpace : (nonnull CGColorSpaceRef)colorSpaceRef scaledSize : (CGSize) scaledSize CF_RETURNS_RETAINED {
363445 WebPDecoderConfig config;
364446 if (!WebPInitDecoderConfig (&config)) {
365447 return nil ;
@@ -377,24 +459,26 @@ - (nullable CGImageRef)sd_createWebpImageWithData:(WebPData)webpData colorSpace:
377459 config.options .use_threads = 1 ;
378460 config.output .colorspace = MODE_bgrA;
379461
462+ // Use scaling for thumbnail
463+ if (scaledSize.width != 0 && scaledSize.height != 0 ) {
464+ config.options .use_scaling = 1 ;
465+ config.options .scaled_width = scaledSize.width ;
466+ config.options .scaled_height = scaledSize.height ;
467+ }
468+
380469 // Decode the WebP image data into a RGBA value array
381470 if (WebPDecode (webpData.bytes , webpData.size , &config) != VP8_STATUS_OK) {
382471 return nil ;
383472 }
384473
385- int width = config.input .width ;
386- int height = config.input .height ;
387- if (config.options .use_scaling ) {
388- width = config.options .scaled_width ;
389- height = config.options .scaled_height ;
390- }
391-
392474 // Construct a UIImage from the decoded RGBA value array
393475 CGDataProviderRef provider =
394476 CGDataProviderCreateWithData (NULL , config.output .u .RGBA .rgba , config.output .u .RGBA .size , FreeImageData);
395477 size_t bitsPerComponent = 8 ;
396478 size_t bitsPerPixel = 32 ;
397479 size_t bytesPerRow = config.output .u .RGBA .stride ;
480+ size_t width = config.output .width ;
481+ size_t height = config.output .height ;
398482 CGColorRenderingIntent renderingIntent = kCGRenderingIntentDefault ;
399483 CGImageRef imageRef = CGImageCreate (width, height, bitsPerComponent, bitsPerPixel, bytesPerRow, colorSpaceRef, bitmapInfo, provider, NULL , NO , renderingIntent);
400484
@@ -414,7 +498,7 @@ - (NSTimeInterval)sd_frameDurationWithIterator:(WebPIterator)iter {
414498}
415499
416500// Create and return the correct colorspace by checking the ICC Profile
417- - (nonnull CGColorSpaceRef)sd_colorSpaceWithDemuxer : (nonnull WebPDemuxer *)demuxer CF_RETURNS_RETAINED {
501+ - (nonnull CGColorSpaceRef)sd_createColorSpaceWithDemuxer : (nonnull WebPDemuxer *)demuxer CF_RETURNS_RETAINED {
418502 // WebP contains ICC Profile should use the desired colorspace, instead of default device colorspace
419503 // See: https://developers.google.com/speed/webp/docs/riff_container#color_profile
420504
@@ -681,6 +765,22 @@ - (instancetype)initWithAnimatedImageData:(NSData *)data options:(nullable SDIma
681765 scale = 1 ;
682766 }
683767 }
768+ CGSize thumbnailSize = CGSizeZero;
769+ NSValue *thumbnailSizeValue = options[SDImageCoderDecodeThumbnailPixelSize];
770+ if (thumbnailSizeValue != nil ) {
771+ #if SD_MAC
772+ thumbnailSize = thumbnailSizeValue.sizeValue ;
773+ #else
774+ thumbnailSize = thumbnailSizeValue.CGSizeValue ;
775+ #endif
776+ }
777+ _thumbnailSize = thumbnailSize;
778+ BOOL preserveAspectRatio = YES ;
779+ NSNumber *preserveAspectRatioValue = options[SDImageCoderDecodePreserveAspectRatio];
780+ if (preserveAspectRatioValue != nil ) {
781+ preserveAspectRatio = preserveAspectRatioValue.boolValue ;
782+ }
783+ _preserveAspectRatio = preserveAspectRatio;
684784 _scale = scale;
685785 _demux = demuxer;
686786 _imageData = data;
@@ -804,15 +904,17 @@ - (UIImage *)animatedImageFrameAtIndex:(NSUInteger)index {
804904- (UIImage *)safeStaticImageFrame {
805905 UIImage *image;
806906 if (!_colorSpace) {
807- _colorSpace = [self sd_colorSpaceWithDemuxer : _demux];
907+ _colorSpace = [self sd_createColorSpaceWithDemuxer : _demux];
808908 }
809909 // Static WebP image
810910 WebPIterator iter;
811911 if (!WebPDemuxGetFrame (_demux, 1 , &iter)) {
812912 WebPDemuxReleaseIterator (&iter);
813913 return nil ;
814914 }
815- CGImageRef imageRef = [self sd_createWebpImageWithData: iter.fragment colorSpace: _colorSpace];
915+ // Check whether we need to use thumbnail
916+ CGSize scaledSize = SDCalculateThumbnailSize (CGSizeMake (_canvasWidth, _canvasHeight), _preserveAspectRatio, _thumbnailSize);
917+ CGImageRef imageRef = [self sd_createWebpImageWithData: iter.fragment colorSpace: _colorSpace scaledSize: scaledSize];
816918 if (!imageRef) {
817919 return nil ;
818920 }
@@ -837,7 +939,7 @@ - (UIImage *)safeAnimatedImageFrameAtIndex:(NSUInteger)index {
837939 _canvas = canvas;
838940 }
839941 if (!_colorSpace) {
840- _colorSpace = [self sd_colorSpaceWithDemuxer : _demux];
942+ _colorSpace = [self sd_createColorSpaceWithDemuxer : _demux];
841943 }
842944
843945 SDWebPCoderFrame *frame = _frames[index];
@@ -887,7 +989,9 @@ - (UIImage *)safeAnimatedImageFrameAtIndex:(NSUInteger)index {
887989 _currentBlendIndex = index;
888990
889991 // Now the canvas is ready, which respects of dispose method behavior. Just do normal decoding and produce image.
890- CGImageRef imageRef = [self sd_drawnWebpImageWithCanvas: _canvas iterator: iter colorSpace: _colorSpace];
992+ // Check whether we need to use thumbnail
993+ CGSize scaledSize = SDCalculateThumbnailSize (CGSizeMake (_canvasWidth, _canvasHeight), _preserveAspectRatio, _thumbnailSize);
994+ CGImageRef imageRef = [self sd_drawnWebpImageWithCanvas: _canvas iterator: iter colorSpace: _colorSpace scaledSize: scaledSize];
891995 if (!imageRef) {
892996 return nil ;
893997 }
0 commit comments