Skip to content

Commit 9082448

Browse files
authored
Merge pull request #73 from SDWebImage/perf/awebp_thumbnail_allocation
Reduce memory usage peak when using thumbnail animated WebP decoding and encoding
2 parents e6cd1a9 + 9dae8d3 commit 9082448

File tree

1 file changed

+50
-111
lines changed

1 file changed

+50
-111
lines changed

SDWebImageWebPCoder/Classes/SDImageWebPCoder.m

Lines changed: 50 additions & 111 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,26 @@
6868
#endif
6969
#endif
7070

71+
/// Used for animated WebP, which need a canvas for decoding (rendering), possible apply a scale transform for thumbnail decoding (avoiding post-rescale using vImage)
72+
/// See more in #73
73+
static inline CGContextRef _Nullable CreateWebPCanvas(BOOL hasAlpha, CGSize canvasSize, CGSize thumbnailSize, BOOL preserveAspectRatio) {
74+
CGBitmapInfo bitmapInfo = kCGBitmapByteOrder32Host;
75+
bitmapInfo |= hasAlpha ? kCGImageAlphaPremultipliedFirst : kCGImageAlphaNoneSkipFirst;
76+
// Check whether we need to use thumbnail
77+
CGSize scaledSize = [SDImageCoderHelper scaledSizeWithImageSize:CGSizeMake(canvasSize.width, canvasSize.height) scaleSize:thumbnailSize preserveAspectRatio:preserveAspectRatio shouldScaleUp:NO];
78+
CGContextRef canvas = CGBitmapContextCreate(NULL, scaledSize.width, scaledSize.height, 8, 0, [SDImageCoderHelper colorSpaceGetDeviceRGB], bitmapInfo);
79+
if (!canvas) {
80+
return nil;
81+
}
82+
// Check whether we need to use thumbnail
83+
if (!CGSizeEqualToSize(canvasSize, scaledSize)) {
84+
CGFloat sx = scaledSize.width / canvasSize.width;
85+
CGFloat sy = scaledSize.height / canvasSize.height;
86+
CGContextScaleCTM(canvas, sx, sy);
87+
}
88+
return canvas;
89+
}
90+
7191
@interface SDWebPCoderFrame : NSObject
7292

7393
@property (nonatomic, assign) NSUInteger index; // Frame index (zero based)
@@ -218,9 +238,7 @@ - (UIImage *)decodedImageWithData:(NSData *)data options:(nullable SDImageCoderO
218238
}
219239

220240
BOOL hasAlpha = flags & ALPHA_FLAG;
221-
CGBitmapInfo bitmapInfo = kCGBitmapByteOrder32Host;
222-
bitmapInfo |= hasAlpha ? kCGImageAlphaPremultipliedFirst : kCGImageAlphaNoneSkipFirst;
223-
CGContextRef canvas = CGBitmapContextCreate(NULL, canvasWidth, canvasHeight, 8, 0, [SDImageCoderHelper colorSpaceGetDeviceRGB], bitmapInfo);
241+
CGContextRef canvas = CreateWebPCanvas(hasAlpha, CGSizeMake(canvasWidth, canvasHeight), thumbnailSize, preserveAspectRatio);
224242
if (!canvas) {
225243
WebPDemuxDelete(demuxer);
226244
CGColorSpaceRelease(colorSpace);
@@ -232,7 +250,7 @@ - (UIImage *)decodedImageWithData:(NSData *)data options:(nullable SDImageCoderO
232250

233251
do {
234252
@autoreleasepool {
235-
CGImageRef imageRef = [self sd_drawnWebpImageWithCanvas:canvas iterator:iter colorSpace:colorSpace scaledSize:scaledSize];
253+
CGImageRef imageRef = [self sd_drawnWebpImageWithCanvas:canvas demuxer:demuxer iterator:iter colorSpace:colorSpace];
236254
if (!imageRef) {
237255
continue;
238256
}
@@ -381,7 +399,7 @@ - (UIImage *)incrementalDecodedImageWithOptions:(SDImageCoderOptions *)options {
381399
return nil;
382400
}
383401

384-
CGContextRef canvas = CGBitmapContextCreate(NULL, width, height, 8, 0, [SDImageCoderHelper colorSpaceGetDeviceRGB], bitmapInfo);
402+
CGContextRef canvas = CreateWebPCanvas(YES, CGSizeMake(width, height), _thumbnailSize, _preserveAspectRatio);
385403
if (!canvas) {
386404
CGImageRelease(imageRef);
387405
return nil;
@@ -403,13 +421,6 @@ - (UIImage *)incrementalDecodedImageWithOptions:(SDImageCoderOptions *)options {
403421
scale = 1;
404422
}
405423
}
406-
CGSize scaledSize = [SDImageCoderHelper scaledSizeWithImageSize:CGSizeMake(width, height) scaleSize:_thumbnailSize preserveAspectRatio:_preserveAspectRatio shouldScaleUp:NO];
407-
// Check whether we need to use thumbnail
408-
if (!CGSizeEqualToSize(CGSizeMake(width, height), scaledSize)) {
409-
CGImageRef scaledImageRef = [SDImageCoderHelper CGImageCreateScaled:newImageRef size:scaledSize];
410-
CGImageRelease(newImageRef);
411-
newImageRef = scaledImageRef;
412-
}
413424

414425
#if SD_UIKIT || SD_WATCH
415426
image = [[UIImage alloc] initWithCGImage:newImageRef scale:scale orientation:UIImageOrientationUp];
@@ -425,8 +436,8 @@ - (UIImage *)incrementalDecodedImageWithOptions:(SDImageCoderOptions *)options {
425436
return image;
426437
}
427438

428-
- (void)sd_blendWebpImageWithCanvas:(CGContextRef)canvas iterator:(WebPIterator)iter colorSpace:(nonnull CGColorSpaceRef)colorSpaceRef {
429-
size_t canvasHeight = CGBitmapContextGetHeight(canvas);
439+
- (void)sd_blendWebpImageWithCanvas:(CGContextRef)canvas demuxer:(nonnull WebPDemuxer *)demuxer iterator:(WebPIterator)iter colorSpace:(nonnull CGColorSpaceRef)colorSpaceRef {
440+
int canvasHeight = WebPDemuxGetI(demuxer, WEBP_FF_CANVAS_HEIGHT);
430441
CGFloat tmpX = iter.x_offset;
431442
CGFloat tmpY = canvasHeight - iter.height - iter.y_offset;
432443
CGRect imageRect = CGRectMake(tmpX, tmpY, iter.width, iter.height);
@@ -448,14 +459,13 @@ - (void)sd_blendWebpImageWithCanvas:(CGContextRef)canvas iterator:(WebPIterator)
448459
}
449460
}
450461

451-
- (nullable CGImageRef)sd_drawnWebpImageWithCanvas:(CGContextRef)canvas iterator:(WebPIterator)iter colorSpace:(nonnull CGColorSpaceRef)colorSpaceRef scaledSize:(CGSize)scaledSize CF_RETURNS_RETAINED {
462+
- (nullable CGImageRef)sd_drawnWebpImageWithCanvas:(CGContextRef)canvas demuxer:(nonnull WebPDemuxer *)demuxer iterator:(WebPIterator)iter colorSpace:(nonnull CGColorSpaceRef)colorSpaceRef CF_RETURNS_RETAINED {
452463
CGImageRef imageRef = [self sd_createWebpImageWithData:iter.fragment colorSpace:colorSpaceRef scaledSize:CGSizeZero];
453464
if (!imageRef) {
454465
return nil;
455466
}
456467

457-
size_t canvasWidth = CGBitmapContextGetWidth(canvas);
458-
size_t canvasHeight = CGBitmapContextGetHeight(canvas);
468+
int canvasHeight = WebPDemuxGetI(demuxer, WEBP_FF_CANVAS_HEIGHT);
459469
CGFloat tmpX = iter.x_offset;
460470
CGFloat tmpY = canvasHeight - iter.height - iter.y_offset;
461471
CGRect imageRect = CGRectMake(tmpX, tmpY, iter.width, iter.height);
@@ -466,26 +476,15 @@ - (nullable CGImageRef)sd_drawnWebpImageWithCanvas:(CGContextRef)canvas iterator
466476
if (!shouldBlend) {
467477
CGContextClearRect(canvas, imageRect);
468478
}
479+
469480
CGContextDrawImage(canvas, imageRect, imageRef);
470481
CGImageRef newImageRef = CGBitmapContextCreateImage(canvas);
471-
472482
CGImageRelease(imageRef);
473483

474484
if (iter.dispose_method == WEBP_MUX_DISPOSE_BACKGROUND) {
475485
CGContextClearRect(canvas, imageRect);
476486
}
477487

478-
// Check whether we need to use thumbnail
479-
if (!CGSizeEqualToSize(CGSizeMake(canvasWidth, canvasHeight), scaledSize)) {
480-
// Important: For Animated WebP thumbnail generation, we can not just use a scaled small canvas and draw each thumbnail frame
481-
// 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
482-
// Because of this reason, even each frame contains small zigzag, the final animation contains visible glitch, this is not we want.
483-
// 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
484-
CGImageRef scaledImageRef = [SDImageCoderHelper CGImageCreateScaled:newImageRef size:scaledSize];
485-
CGImageRelease(newImageRef);
486-
newImageRef = scaledImageRef;
487-
}
488-
489488
return newImageRef;
490489
}
491490

@@ -736,76 +735,22 @@ - (nullable NSData *)sd_encodedWebpDataWithImage:(nullable CGImageRef)imageRef
736735
if (!dataProvider) {
737736
return nil;
738737
}
739-
// Check colorSpace is RGB/RGBA
740-
CGColorSpaceRef colorSpace = CGImageGetColorSpace(imageRef);
741-
BOOL isRGB = CGColorSpaceGetModel(colorSpace) == kCGColorSpaceModelRGB;
742738

743-
CFDataRef dataRef;
744739
uint8_t *rgba = NULL; // RGBA Buffer managed by CFData, don't call `free` on it, instead call `CFRelease` on `dataRef`
745740
// We could not assume that input CGImage's color mode is always RGB888/RGBA8888. Convert all other cases to target color mode using vImage
746-
BOOL isRGB888 = isRGB && byteOrderNormal && alphaInfo == kCGImageAlphaNone && components == 3;
747-
BOOL isRGBA8888 = isRGB && byteOrderNormal && alphaInfo == kCGImageAlphaLast && components == 4;
748-
if (isRGB888 || isRGBA8888) {
749-
// If the input CGImage is already RGB888/RGBA8888
750-
dataRef = CGDataProviderCopyData(dataProvider);
751-
if (!dataRef) {
752-
return nil;
753-
}
754-
rgba = (uint8_t *)CFDataGetBytePtr(dataRef);
755-
} else {
756-
// Convert all other cases to target color mode using vImage
757-
vImageConverterRef convertor = NULL;
758-
vImage_Error error = kvImageNoError;
759-
760-
vImage_CGImageFormat srcFormat = {
761-
.bitsPerComponent = (uint32_t)bitsPerComponent,
762-
.bitsPerPixel = (uint32_t)bitsPerPixel,
763-
.colorSpace = colorSpace,
764-
.bitmapInfo = bitmapInfo,
765-
.renderingIntent = CGImageGetRenderingIntent(imageRef)
766-
};
767-
vImage_CGImageFormat destFormat = {
768-
.bitsPerComponent = 8,
769-
.bitsPerPixel = hasAlpha ? 32 : 24,
770-
.colorSpace = [SDImageCoderHelper colorSpaceGetDeviceRGB],
771-
.bitmapInfo = hasAlpha ? kCGImageAlphaLast | kCGBitmapByteOrderDefault : kCGImageAlphaNone | kCGBitmapByteOrderDefault // RGB888/RGBA8888 (Non-premultiplied to works for libwebp)
772-
};
773-
774-
convertor = vImageConverter_CreateWithCGImageFormat(&srcFormat, &destFormat, NULL, kvImageNoFlags, &error);
775-
if (error != kvImageNoError) {
776-
return nil;
777-
}
778-
779-
vImage_Buffer src;
780-
error = vImageBuffer_InitWithCGImage(&src, &srcFormat, nil, imageRef, kvImageNoFlags);
781-
if (error != kvImageNoError) {
782-
vImageConverter_Release(convertor);
783-
return nil;
784-
}
785-
786-
vImage_Buffer dest;
787-
error = vImageBuffer_Init(&dest, height, width, destFormat.bitsPerPixel, kvImageNoFlags);
788-
if (error != kvImageNoError) {
789-
vImageConverter_Release(convertor);
790-
free(src.data);
791-
return nil;
792-
}
793-
794-
// Convert input color mode to RGB888/RGBA8888
795-
error = vImageConvert_AnyToAny(convertor, &src, &dest, NULL, kvImageNoFlags);
796-
797-
// Free the buffer
798-
free(src.data);
799-
vImageConverter_Release(convertor);
800-
if (error != kvImageNoError) {
801-
free(dest.data);
802-
return nil;
803-
}
804-
805-
rgba = dest.data; // Converted buffer
806-
bytesPerRow = dest.rowBytes; // Converted bytePerRow
807-
dataRef = CFDataCreateWithBytesNoCopy(kCFAllocatorDefault, rgba, bytesPerRow * height, kCFAllocatorDefault);
741+
vImage_CGImageFormat destFormat = {
742+
.bitsPerComponent = 8,
743+
.bitsPerPixel = hasAlpha ? 32 : 24,
744+
.colorSpace = [SDImageCoderHelper colorSpaceGetDeviceRGB],
745+
.bitmapInfo = hasAlpha ? kCGImageAlphaLast | kCGBitmapByteOrderDefault : kCGImageAlphaNone | kCGBitmapByteOrderDefault // RGB888/RGBA8888 (Non-premultiplied to works for libwebp)
746+
};
747+
vImage_Buffer dest;
748+
vImage_Error error = vImageBuffer_InitWithCGImage(&dest, &destFormat, NULL, imageRef, kvImageNoFlags);
749+
if (error != kvImageNoError) {
750+
return nil;
808751
}
752+
rgba = dest.data;
753+
bytesPerRow = dest.rowBytes;
809754

810755
float qualityFactor = quality * 100; // WebP quality is 0-100
811756
// Encode RGB888/RGBA8888 buffer to WebP data
@@ -817,7 +762,8 @@ - (nullable NSData *)sd_encodedWebpDataWithImage:(nullable CGImageRef)imageRef
817762
if (!WebPConfigPreset(&config, WEBP_PRESET_DEFAULT, qualityFactor) ||
818763
!WebPPictureInit(&picture)) {
819764
// shouldn't happen, except if system installation is broken
820-
CFRelease(dataRef);
765+
free(dest.data);
766+
// CFRelease(dataRef);
821767
return nil;
822768
}
823769

@@ -837,7 +783,7 @@ - (nullable NSData *)sd_encodedWebpDataWithImage:(nullable CGImageRef)imageRef
837783
}
838784
if (!result) {
839785
WebPMemoryWriterClear(&writer);
840-
CFRelease(dataRef);
786+
free(dest.data);
841787
return nil;
842788
}
843789

@@ -848,14 +794,14 @@ - (nullable NSData *)sd_encodedWebpDataWithImage:(nullable CGImageRef)imageRef
848794
if (!result) {
849795
WebPMemoryWriterClear(&writer);
850796
WebPPictureFree(&picture);
851-
CFRelease(dataRef);
797+
free(dest.data);
852798
return nil;
853799
}
854800
}
855801

856802
result = WebPEncode(&config, &picture);
857803
WebPPictureFree(&picture);
858-
CFRelease(dataRef); // Free bitmap buffer
804+
free(dest.data);
859805

860806
if (result) {
861807
// success
@@ -1137,16 +1083,13 @@ - (UIImage *)safeStaticImageFrame {
11371083
if (_hasAnimation) {
11381084
// If have animation, we still need to allocate a CGContext, because the poster frame may be smaller than canvas
11391085
if (!_canvas) {
1140-
CGBitmapInfo bitmapInfo = kCGBitmapByteOrder32Host;
1141-
bitmapInfo |= _hasAlpha ? kCGImageAlphaPremultipliedFirst : kCGImageAlphaNoneSkipFirst;
1142-
CGContextRef canvas = CGBitmapContextCreate(NULL, _canvasWidth, _canvasHeight, 8, 0, [SDImageCoderHelper colorSpaceGetDeviceRGB], bitmapInfo);
1086+
CGContextRef canvas = CreateWebPCanvas(_hasAlpha, CGSizeMake(_canvasWidth, _canvasHeight), _thumbnailSize, _preserveAspectRatio);
11431087
if (!canvas) {
11441088
return nil;
11451089
}
11461090
_canvas = canvas;
11471091
}
1148-
CGSize scaledSize = [SDImageCoderHelper scaledSizeWithImageSize:CGSizeMake(_canvasWidth, _canvasHeight) scaleSize:_thumbnailSize preserveAspectRatio:_preserveAspectRatio shouldScaleUp:NO];
1149-
imageRef = [self sd_drawnWebpImageWithCanvas:_canvas iterator:iter colorSpace:_colorSpace scaledSize:scaledSize];
1092+
imageRef = [self sd_drawnWebpImageWithCanvas:_canvas demuxer:_demux iterator:iter colorSpace:_colorSpace];
11501093
} else {
11511094
CGSize scaledSize = [SDImageCoderHelper scaledSizeWithImageSize:CGSizeMake(iter.width, iter.height) scaleSize:_thumbnailSize preserveAspectRatio:_preserveAspectRatio shouldScaleUp:NO];
11521095
imageRef = [self sd_createWebpImageWithData:iter.fragment colorSpace:_colorSpace scaledSize:scaledSize];
@@ -1166,9 +1109,7 @@ - (UIImage *)safeStaticImageFrame {
11661109

11671110
- (UIImage *)safeAnimatedImageFrameAtIndex:(NSUInteger)index {
11681111
if (!_canvas) {
1169-
CGBitmapInfo bitmapInfo = kCGBitmapByteOrder32Host;
1170-
bitmapInfo |= _hasAlpha ? kCGImageAlphaPremultipliedFirst : kCGImageAlphaNoneSkipFirst;
1171-
CGContextRef canvas = CGBitmapContextCreate(NULL, _canvasWidth, _canvasHeight, 8, 0, [SDImageCoderHelper colorSpaceGetDeviceRGB], bitmapInfo);
1112+
CGContextRef canvas = CreateWebPCanvas(_hasAlpha, CGSizeMake(_canvasWidth, _canvasHeight), _thumbnailSize, _preserveAspectRatio);
11721113
if (!canvas) {
11731114
return nil;
11741115
}
@@ -1212,7 +1153,7 @@ - (UIImage *)safeAnimatedImageFrameAtIndex:(NSUInteger)index {
12121153
if (endIndex > startIndex) {
12131154
do {
12141155
@autoreleasepool {
1215-
[self sd_blendWebpImageWithCanvas:_canvas iterator:iter colorSpace:_colorSpace];
1156+
[self sd_blendWebpImageWithCanvas:_canvas demuxer:_demux iterator:iter colorSpace:_colorSpace];
12161157
}
12171158
} while ((size_t)iter.frame_num < endIndex && WebPDemuxNextFrame(&iter));
12181159
}
@@ -1225,9 +1166,7 @@ - (UIImage *)safeAnimatedImageFrameAtIndex:(NSUInteger)index {
12251166
_currentBlendIndex = index;
12261167

12271168
// Now the canvas is ready, which respects of dispose method behavior. Just do normal decoding and produce image.
1228-
// Check whether we need to use thumbnail
1229-
CGSize scaledSize = [SDImageCoderHelper scaledSizeWithImageSize:CGSizeMake(_canvasWidth, _canvasHeight) scaleSize:_thumbnailSize preserveAspectRatio:_preserveAspectRatio shouldScaleUp:NO];
1230-
CGImageRef imageRef = [self sd_drawnWebpImageWithCanvas:_canvas iterator:iter colorSpace:_colorSpace scaledSize:scaledSize];
1169+
CGImageRef imageRef = [self sd_drawnWebpImageWithCanvas:_canvas demuxer:_demux iterator:iter colorSpace:_colorSpace];
12311170
if (!imageRef) {
12321171
return nil;
12331172
}

0 commit comments

Comments
 (0)