Skip to content

Commit d77570d

Browse files
authored
Merge pull request #31 from SDWebImage/feature_thumbnail_decoding
WebPCoder now supports the thumbnail decoding
2 parents a936bdb + 6726339 commit d77570d

File tree

7 files changed

+187
-43
lines changed

7 files changed

+187
-43
lines changed

Cartfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
github "SDWebImage/SDWebImage" ~> 5.0
1+
github "SDWebImage/SDWebImage" ~> 5.5
22
github "SDWebImage/libwebp-Xcode" ~> 1.0

Cartfile.resolved

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
github "SDWebImage/SDWebImage" "5.0.0"
2-
github "SDWebImage/libwebp-Xcode" "1.0.0"
1+
github "SDWebImage/SDWebImage" "5.5.0"
2+
github "SDWebImage/libwebp-Xcode" "1.1.0"

Example/SDWebImageWebPCoderExample/ViewController.m

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ - (void)viewDidLoad {
3535
NSURL *staticWebPURL = [NSURL URLWithString:@"https://www.gstatic.com/webp/gallery/2.webp"];
3636
NSURL *animatedWebPURL = [NSURL URLWithString:@"http://littlesvr.ca/apng/images/world-cup-2014-42.webp"];
3737

38-
[self.imageView1 sd_setImageWithURL:staticWebPURL completed:^(UIImage * _Nullable image, NSError * _Nullable error, SDImageCacheType cacheType, NSURL * _Nullable imageURL) {
38+
[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) {
3939
if (image) {
4040
NSLog(@"%@", @"Static WebP load success");
4141
}

SDWebImageWebPCoder.podspec

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ This is a SDWebImage coder plugin to support WebP image.
2727
'GCC_PREPROCESSOR_DEFINITIONS' => '$(inherited) SD_WEBP=1 WEBP_USE_INTRINSICS=1',
2828
'USER_HEADER_SEARCH_PATHS' => '$(inherited) $(SRCROOT)/libwebp/src'
2929
}
30-
s.dependency 'SDWebImage/Core', '~> 5.0'
30+
s.dependency 'SDWebImage/Core', '~> 5.5'
3131
s.dependency 'libwebp', '~> 1.0'
3232

3333
end

SDWebImageWebPCoder/Classes/SDImageWebPCoder.m

Lines changed: 127 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,38 @@
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
}

SDWebImageWebPCoderTests/Podfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,6 @@ project '../SDWebImageWebPCoder'
55
workspace '../SDWebImageWebPCoder'
66

77
target 'SDWebImageWebPCoderTests' do
8+
pod 'Expecta'
89
pod 'SDWebImageWebPCoder', :path => '../'
910
end

0 commit comments

Comments
 (0)