Skip to content

Commit 9dae8d3

Browse files
committed
Refactory thumbnail decoding, use a transformed CGContext instead of full context and re-scale after decode finished
This can help to avoid memory allocation For encoding, avoid using vImageConvert_AnyToAny and just use it convenience method instead
1 parent c779312 commit 9dae8d3

File tree

1 file changed

+50
-114
lines changed

1 file changed

+50
-114
lines changed

SDWebImageWebPCoder/Classes/SDImageWebPCoder.m

Lines changed: 50 additions & 114 deletions
Original file line numberDiff line numberDiff line change
@@ -68,12 +68,24 @@
6868
#endif
6969
#endif
7070

71-
static inline CGImageRef __nullable CGBitmapContextCreateScaledImage(cg_nullable CGContextRef canvas, CGSize scaledSize) CF_RETURNS_RETAINED {
72-
if (!canvas) return NULL;
73-
CGContextSaveGState(canvas);
74-
CGContextScaleCTM(canvas, scaledSize.width, scaledSize.height);
75-
CGContextRestoreGState(canvas);
76-
return CGBitmapContextCreateImage(canvas);
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;
7789
}
7890

7991
@interface SDWebPCoderFrame : NSObject
@@ -226,9 +238,7 @@ - (UIImage *)decodedImageWithData:(NSData *)data options:(nullable SDImageCoderO
226238
}
227239

228240
BOOL hasAlpha = flags & ALPHA_FLAG;
229-
CGBitmapInfo bitmapInfo = kCGBitmapByteOrder32Host;
230-
bitmapInfo |= hasAlpha ? kCGImageAlphaPremultipliedFirst : kCGImageAlphaNoneSkipFirst;
231-
CGContextRef canvas = CGBitmapContextCreate(NULL, canvasWidth, canvasHeight, 8, 0, [SDImageCoderHelper colorSpaceGetDeviceRGB], bitmapInfo);
241+
CGContextRef canvas = CreateWebPCanvas(hasAlpha, CGSizeMake(canvasWidth, canvasHeight), thumbnailSize, preserveAspectRatio);
232242
if (!canvas) {
233243
WebPDemuxDelete(demuxer);
234244
CGColorSpaceRelease(colorSpace);
@@ -240,7 +250,7 @@ - (UIImage *)decodedImageWithData:(NSData *)data options:(nullable SDImageCoderO
240250

241251
do {
242252
@autoreleasepool {
243-
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];
244254
if (!imageRef) {
245255
continue;
246256
}
@@ -389,22 +399,15 @@ - (UIImage *)incrementalDecodedImageWithOptions:(SDImageCoderOptions *)options {
389399
return nil;
390400
}
391401

392-
CGContextRef canvas = CGBitmapContextCreate(NULL, width, height, 8, 0, [SDImageCoderHelper colorSpaceGetDeviceRGB], bitmapInfo);
402+
CGContextRef canvas = CreateWebPCanvas(YES, CGSizeMake(width, height), _thumbnailSize, _preserveAspectRatio);
393403
if (!canvas) {
394404
CGImageRelease(imageRef);
395405
return nil;
396406
}
397407

398408
// Only draw the last_y image height, keep remains transparent, in Core Graphics coordinate system
399409
CGContextDrawImage(canvas, CGRectMake(0, height - last_y, width, last_y), imageRef);
400-
// Check whether we need to use thumbnail
401-
CGImageRef newImageRef;
402-
CGSize scaledSize = [SDImageCoderHelper scaledSizeWithImageSize:CGSizeMake(width, height) scaleSize:_thumbnailSize preserveAspectRatio:_preserveAspectRatio shouldScaleUp:NO];
403-
if (!CGSizeEqualToSize(CGSizeMake(width, height), scaledSize)) {
404-
newImageRef = CGBitmapContextCreateScaledImage(canvas, scaledSize);
405-
} else {
406-
newImageRef = CGBitmapContextCreateImage(canvas);
407-
}
410+
CGImageRef newImageRef = CGBitmapContextCreateImage(canvas);
408411
CGImageRelease(imageRef);
409412
if (!newImageRef) {
410413
CGContextRelease(canvas);
@@ -433,8 +436,8 @@ - (UIImage *)incrementalDecodedImageWithOptions:(SDImageCoderOptions *)options {
433436
return image;
434437
}
435438

436-
- (void)sd_blendWebpImageWithCanvas:(CGContextRef)canvas iterator:(WebPIterator)iter colorSpace:(nonnull CGColorSpaceRef)colorSpaceRef {
437-
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);
438441
CGFloat tmpX = iter.x_offset;
439442
CGFloat tmpY = canvasHeight - iter.height - iter.y_offset;
440443
CGRect imageRect = CGRectMake(tmpX, tmpY, iter.width, iter.height);
@@ -456,14 +459,13 @@ - (void)sd_blendWebpImageWithCanvas:(CGContextRef)canvas iterator:(WebPIterator)
456459
}
457460
}
458461

459-
- (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 {
460463
CGImageRef imageRef = [self sd_createWebpImageWithData:iter.fragment colorSpace:colorSpaceRef scaledSize:CGSizeZero];
461464
if (!imageRef) {
462465
return nil;
463466
}
464467

465-
size_t canvasWidth = CGBitmapContextGetWidth(canvas);
466-
size_t canvasHeight = CGBitmapContextGetHeight(canvas);
468+
int canvasHeight = WebPDemuxGetI(demuxer, WEBP_FF_CANVAS_HEIGHT);
467469
CGFloat tmpX = iter.x_offset;
468470
CGFloat tmpY = canvasHeight - iter.height - iter.y_offset;
469471
CGRect imageRect = CGRectMake(tmpX, tmpY, iter.width, iter.height);
@@ -474,17 +476,9 @@ - (nullable CGImageRef)sd_drawnWebpImageWithCanvas:(CGContextRef)canvas iterator
474476
if (!shouldBlend) {
475477
CGContextClearRect(canvas, imageRect);
476478
}
477-
CGContextDrawImage(canvas, imageRect, imageRef);
478-
479-
CGImageRef newImageRef;
480-
// Check whether we need to use thumbnail
481-
if (!CGSizeEqualToSize(CGSizeMake(canvasWidth, canvasHeight), scaledSize)) {
482-
// Use CoreGraphics canvas to scale down, no need extra allocation
483-
newImageRef = CGBitmapContextCreateScaledImage(canvas, scaledSize);
484-
} else {
485-
newImageRef = CGBitmapContextCreateImage(canvas);
486-
}
487479

480+
CGContextDrawImage(canvas, imageRect, imageRef);
481+
CGImageRef newImageRef = CGBitmapContextCreateImage(canvas);
488482
CGImageRelease(imageRef);
489483

490484
if (iter.dispose_method == WEBP_MUX_DISPOSE_BACKGROUND) {
@@ -741,74 +735,22 @@ - (nullable NSData *)sd_encodedWebpDataWithImage:(nullable CGImageRef)imageRef
741735
if (!dataProvider) {
742736
return nil;
743737
}
744-
// Check colorSpace is RGB/RGBA
745-
CGColorSpaceRef colorSpace = CGImageGetColorSpace(imageRef);
746-
BOOL isRGB = CGColorSpaceGetModel(colorSpace) == kCGColorSpaceModelRGB;
747738

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

813755
float qualityFactor = quality * 100; // WebP quality is 0-100
814756
// Encode RGB888/RGBA8888 buffer to WebP data
@@ -820,7 +762,8 @@ - (nullable NSData *)sd_encodedWebpDataWithImage:(nullable CGImageRef)imageRef
820762
if (!WebPConfigPreset(&config, WEBP_PRESET_DEFAULT, qualityFactor) ||
821763
!WebPPictureInit(&picture)) {
822764
// shouldn't happen, except if system installation is broken
823-
CFRelease(dataRef);
765+
free(dest.data);
766+
// CFRelease(dataRef);
824767
return nil;
825768
}
826769

@@ -840,7 +783,7 @@ - (nullable NSData *)sd_encodedWebpDataWithImage:(nullable CGImageRef)imageRef
840783
}
841784
if (!result) {
842785
WebPMemoryWriterClear(&writer);
843-
CFRelease(dataRef);
786+
free(dest.data);
844787
return nil;
845788
}
846789

@@ -851,14 +794,14 @@ - (nullable NSData *)sd_encodedWebpDataWithImage:(nullable CGImageRef)imageRef
851794
if (!result) {
852795
WebPMemoryWriterClear(&writer);
853796
WebPPictureFree(&picture);
854-
CFRelease(dataRef);
797+
free(dest.data);
855798
return nil;
856799
}
857800
}
858801

859802
result = WebPEncode(&config, &picture);
860803
WebPPictureFree(&picture);
861-
CFRelease(dataRef); // Free bitmap buffer
804+
free(dest.data);
862805

863806
if (result) {
864807
// success
@@ -1140,16 +1083,13 @@ - (UIImage *)safeStaticImageFrame {
11401083
if (_hasAnimation) {
11411084
// If have animation, we still need to allocate a CGContext, because the poster frame may be smaller than canvas
11421085
if (!_canvas) {
1143-
CGBitmapInfo bitmapInfo = kCGBitmapByteOrder32Host;
1144-
bitmapInfo |= _hasAlpha ? kCGImageAlphaPremultipliedFirst : kCGImageAlphaNoneSkipFirst;
1145-
CGContextRef canvas = CGBitmapContextCreate(NULL, _canvasWidth, _canvasHeight, 8, 0, [SDImageCoderHelper colorSpaceGetDeviceRGB], bitmapInfo);
1086+
CGContextRef canvas = CreateWebPCanvas(_hasAlpha, CGSizeMake(_canvasWidth, _canvasHeight), _thumbnailSize, _preserveAspectRatio);
11461087
if (!canvas) {
11471088
return nil;
11481089
}
11491090
_canvas = canvas;
11501091
}
1151-
CGSize scaledSize = [SDImageCoderHelper scaledSizeWithImageSize:CGSizeMake(_canvasWidth, _canvasHeight) scaleSize:_thumbnailSize preserveAspectRatio:_preserveAspectRatio shouldScaleUp:NO];
1152-
imageRef = [self sd_drawnWebpImageWithCanvas:_canvas iterator:iter colorSpace:_colorSpace scaledSize:scaledSize];
1092+
imageRef = [self sd_drawnWebpImageWithCanvas:_canvas demuxer:_demux iterator:iter colorSpace:_colorSpace];
11531093
} else {
11541094
CGSize scaledSize = [SDImageCoderHelper scaledSizeWithImageSize:CGSizeMake(iter.width, iter.height) scaleSize:_thumbnailSize preserveAspectRatio:_preserveAspectRatio shouldScaleUp:NO];
11551095
imageRef = [self sd_createWebpImageWithData:iter.fragment colorSpace:_colorSpace scaledSize:scaledSize];
@@ -1169,9 +1109,7 @@ - (UIImage *)safeStaticImageFrame {
11691109

11701110
- (UIImage *)safeAnimatedImageFrameAtIndex:(NSUInteger)index {
11711111
if (!_canvas) {
1172-
CGBitmapInfo bitmapInfo = kCGBitmapByteOrder32Host;
1173-
bitmapInfo |= _hasAlpha ? kCGImageAlphaPremultipliedFirst : kCGImageAlphaNoneSkipFirst;
1174-
CGContextRef canvas = CGBitmapContextCreate(NULL, _canvasWidth, _canvasHeight, 8, 0, [SDImageCoderHelper colorSpaceGetDeviceRGB], bitmapInfo);
1112+
CGContextRef canvas = CreateWebPCanvas(_hasAlpha, CGSizeMake(_canvasWidth, _canvasHeight), _thumbnailSize, _preserveAspectRatio);
11751113
if (!canvas) {
11761114
return nil;
11771115
}
@@ -1215,7 +1153,7 @@ - (UIImage *)safeAnimatedImageFrameAtIndex:(NSUInteger)index {
12151153
if (endIndex > startIndex) {
12161154
do {
12171155
@autoreleasepool {
1218-
[self sd_blendWebpImageWithCanvas:_canvas iterator:iter colorSpace:_colorSpace];
1156+
[self sd_blendWebpImageWithCanvas:_canvas demuxer:_demux iterator:iter colorSpace:_colorSpace];
12191157
}
12201158
} while ((size_t)iter.frame_num < endIndex && WebPDemuxNextFrame(&iter));
12211159
}
@@ -1228,9 +1166,7 @@ - (UIImage *)safeAnimatedImageFrameAtIndex:(NSUInteger)index {
12281166
_currentBlendIndex = index;
12291167

12301168
// Now the canvas is ready, which respects of dispose method behavior. Just do normal decoding and produce image.
1231-
// Check whether we need to use thumbnail
1232-
CGSize scaledSize = [SDImageCoderHelper scaledSizeWithImageSize:CGSizeMake(_canvasWidth, _canvasHeight) scaleSize:_thumbnailSize preserveAspectRatio:_preserveAspectRatio shouldScaleUp:NO];
1233-
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];
12341170
if (!imageRef) {
12351171
return nil;
12361172
}

0 commit comments

Comments
 (0)