Skip to content

Commit 8196947

Browse files
committed
Avoid force-decode by apply the byte alignment for static WebP images
See: SDWebImage/SDWebImage#3559
1 parent 76565bc commit 8196947

File tree

1 file changed

+126
-22
lines changed

1 file changed

+126
-22
lines changed

SDWebImageWebPCoder/Classes/SDImageWebPCoder.m

Lines changed: 126 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -71,8 +71,8 @@
7171
/// Used for animated WebP, which need a canvas for decoding (rendering), possible apply a scale transform for thumbnail decoding (avoiding post-rescale using vImage)
7272
/// See more in #73
7373
static inline CGContextRef _Nullable CreateWebPCanvas(BOOL hasAlpha, CGSize canvasSize, CGSize thumbnailSize, BOOL preserveAspectRatio) {
74-
CGBitmapInfo bitmapInfo = kCGBitmapByteOrder32Host;
75-
bitmapInfo |= hasAlpha ? kCGImageAlphaPremultipliedFirst : kCGImageAlphaNoneSkipFirst;
74+
// From SDWebImage v5.17.0, use runtime detection of bitmap info instead of hardcode.
75+
CGBitmapInfo bitmapInfo = [SDImageCoderHelper preferredBitmapInfo:hasAlpha];
7676
// Check whether we need to use thumbnail
7777
CGSize scaledSize = [SDImageCoderHelper scaledSizeWithImageSize:CGSizeMake(canvasSize.width, canvasSize.height) scaleSize:thumbnailSize preserveAspectRatio:preserveAspectRatio shouldScaleUp:NO];
7878
CGContextRef canvas = CGBitmapContextCreate(NULL, scaledSize.width, scaledSize.height, 8, 0, [SDImageCoderHelper colorSpaceGetDeviceRGB], bitmapInfo);
@@ -88,6 +88,89 @@ static inline CGContextRef _Nullable CreateWebPCanvas(BOOL hasAlpha, CGSize canv
8888
return canvas;
8989
}
9090

91+
WEBP_CSP_MODE ConvertCSPMode(CGBitmapInfo bitmapInfo) {
92+
// Get alpha info, byteOrder info
93+
CGImageAlphaInfo alphaInfo = bitmapInfo & kCGBitmapAlphaInfoMask;
94+
CGBitmapInfo byteOrderInfo = bitmapInfo & kCGBitmapByteOrderMask;
95+
BOOL byteOrderNormal = NO;
96+
switch (byteOrderInfo) {
97+
case kCGBitmapByteOrderDefault: {
98+
byteOrderNormal = YES;
99+
} break;
100+
case kCGBitmapByteOrder32Little: {
101+
} break;
102+
case kCGBitmapByteOrder32Big: {
103+
byteOrderNormal = YES;
104+
} break;
105+
default: break;
106+
}
107+
switch (alphaInfo) {
108+
case kCGImageAlphaPremultipliedFirst: {
109+
if (byteOrderNormal) {
110+
// ARGB8888, premultiplied
111+
return MODE_Argb;
112+
} else {
113+
// BGRA8888, premultiplied
114+
return MODE_bgrA;
115+
}
116+
}
117+
break;
118+
case kCGImageAlphaPremultipliedLast: {
119+
if (byteOrderNormal) {
120+
// RGBA8888, premultiplied
121+
return MODE_rgbA;
122+
} else {
123+
// ABGR8888, premultiplied
124+
// Unsupported!
125+
return MODE_LAST;
126+
}
127+
}
128+
break;
129+
case kCGImageAlphaNone: {
130+
if (byteOrderNormal) {
131+
// RGB
132+
return MODE_RGB;
133+
} else {
134+
// BGR
135+
return MODE_BGR;
136+
}
137+
}
138+
break;
139+
case kCGImageAlphaLast:
140+
case kCGImageAlphaNoneSkipLast: {
141+
if (byteOrderNormal) {
142+
// RGBA or RGBX
143+
return MODE_RGBA;
144+
} else {
145+
// ABGR or XBGR
146+
// Unsupported!
147+
return MODE_LAST;
148+
}
149+
}
150+
break;
151+
case kCGImageAlphaFirst:
152+
case kCGImageAlphaNoneSkipFirst: {
153+
if (byteOrderNormal) {
154+
// ARGB or XRGB
155+
return MODE_ARGB;
156+
} else {
157+
// BGRA or BGRX
158+
return MODE_BGRA;
159+
}
160+
}
161+
break;
162+
case kCGImageAlphaOnly: {
163+
// A
164+
// Unsupported
165+
return MODE_LAST;
166+
}
167+
break;
168+
default:
169+
break;
170+
}
171+
return MODE_LAST;
172+
}
173+
91174
// TODO, share this logic for multiple coders, or do refactory in v6.0 (The coder plugin should provide image information back to Core, like `CGImageSourceCopyPropertiesAtIndex`)
92175
static inline CGSize SDCalculateScaleDownPixelSize(NSUInteger limitBytes, CGSize originalSize, NSUInteger frameCount, NSUInteger bytesPerPixel) {
93176
if (CGSizeEqualToSize(originalSize, CGSizeZero)) return CGSizeMake(1, 1);
@@ -264,6 +347,7 @@ - (UIImage *)decodedImageWithData:(NSData *)data options:(nullable SDImageCoderO
264347
UIImage *firstFrameImage = [[UIImage alloc] initWithCGImage:imageRef scale:scale orientation:kCGImagePropertyOrientationUp];
265348
#endif
266349
firstFrameImage.sd_imageFormat = SDImageFormatWebP;
350+
firstFrameImage.sd_isDecoded = YES; // We handle byte alignment and alloc bitmap buffer
267351
CGImageRelease(imageRef);
268352
WebPDemuxReleaseIterator(&iter);
269353
WebPDemuxDelete(demuxer);
@@ -317,8 +401,8 @@ - (UIImage *)decodedImageWithData:(NSData *)data options:(nullable SDImageCoderO
317401
- (instancetype)initIncrementalWithOptions:(nullable SDImageCoderOptions *)options {
318402
self = [super init];
319403
if (self) {
320-
// Progressive images need transparent, so always use premultiplied BGRA
321-
_idec = WebPINewRGB(MODE_bgrA, NULL, 0, 0);
404+
// Progressive images need transparent, so always use premultiplied RGBA
405+
_idec = WebPINewRGB(MODE_rgbA, NULL, 0, 0);
322406
CGFloat scale = 1;
323407
NSNumber *scaleFactor = options[SDImageCoderDecodeScaleFactor];
324408
if (scaleFactor != nil) {
@@ -428,17 +512,18 @@ - (UIImage *)incrementalDecodedImageWithOptions:(SDImageCoderOptions *)options {
428512
CGDataProviderRef provider =
429513
CGDataProviderCreateWithData(NULL, rgba, rgbaSize, NULL);
430514
CGColorSpaceRef colorSpaceRef = [SDImageCoderHelper colorSpaceGetDeviceRGB];
431-
432-
CGBitmapInfo bitmapInfo = kCGBitmapByteOrder32Host | kCGImageAlphaPremultipliedFirst;
515+
// Because _idec use MODE_rgbA
516+
CGBitmapInfo bitmapInfo = kCGBitmapByteOrderDefault | kCGImageAlphaPremultipliedLast;
433517
size_t components = 4;
518+
BOOL shouldInterpolate = YES;
434519
CGColorRenderingIntent renderingIntent = kCGRenderingIntentDefault;
435520
// Why to use last_y for image height is because of libwebp's bug (https://bugs.chromium.org/p/webp/issues/detail?id=362)
436521
// It will not keep memory barrier safe on x86 architechure (macOS & iPhone simulator) but on ARM architecture (iPhone & iPad & tv & watch) it works great
437522
// If different threads use WebPIDecGetRGB to grab rgba bitmap, it will contain the previous decoded bitmap data
438523
// So this will cause our drawed image looks strange(above is the current part but below is the previous part)
439524
// We only grab the last_y height and draw the last_y height instead of total height image
440525
// Besides fix, this can enhance performance since we do not need to create extra bitmap
441-
CGImageRef imageRef = CGImageCreate(width, last_y, 8, components * 8, components * width, colorSpaceRef, bitmapInfo, provider, NULL, NO, renderingIntent);
526+
CGImageRef imageRef = CGImageCreate(width, last_y, 8, components * 8, components * width, colorSpaceRef, bitmapInfo, provider, NULL, shouldInterpolate, renderingIntent);
442527

443528
CGDataProviderRelease(provider);
444529

@@ -546,20 +631,44 @@ - (nullable CGImageRef)sd_createWebpImageWithData:(WebPData)webpData colorSpace:
546631
}
547632

548633
BOOL hasAlpha = config.input.has_alpha;
549-
// iOS prefer BGRA8888 (premultiplied) or BGRX8888 bitmapInfo for screen rendering, which is same as `UIGraphicsBeginImageContext()` or `- [CALayer drawInContext:]`
550-
// use this bitmapInfo, combined with right colorspace, even without decode, can still avoid extra CA::Render::copy_image(which marked `Color Copied Images` from Instruments)
551-
CGBitmapInfo bitmapInfo = kCGBitmapByteOrder32Host;
552-
bitmapInfo |= hasAlpha ? kCGImageAlphaPremultipliedFirst : kCGImageAlphaNoneSkipFirst;
634+
// From SDWebImage v5.17.0, use runtime detection of bitmap info instead of hardcode.
635+
CGBitmapInfo bitmapInfo = [SDImageCoderHelper preferredBitmapInfo:hasAlpha];
636+
WEBP_CSP_MODE mode = ConvertCSPMode(bitmapInfo);
637+
if (mode == MODE_LAST) {
638+
NSAssert(NO, @"Unsupported libwebp preferred CGBitmapInfo: %d", bitmapInfo);
639+
return nil;
640+
}
641+
config.output.colorspace = mode;
553642
config.options.use_threads = 1;
554-
config.output.colorspace = MODE_bgrA;
643+
555644

556645
// Use scaling for thumbnail
646+
size_t width = config.input.width;
647+
size_t height = config.input.height;
557648
if (scaledSize.width != 0 && scaledSize.height != 0) {
558649
config.options.use_scaling = 1;
559650
config.options.scaled_width = scaledSize.width;
560651
config.options.scaled_height = scaledSize.height;
652+
width = scaledSize.width;
653+
height = scaledSize.height;
561654
}
562655

656+
// We alloc the buffer and do byte alignment by ourself. libwebp defaults does not byte alignment to `bitsPerPixel`, which cause the CoreAnimation unhappy and always trigger the `CA::Render::copy_image`
657+
size_t bitsPerComponent = 8;
658+
size_t components = (mode == MODE_RGB || mode == MODE_BGR) ? 3 : 4; // Actually always 4
659+
size_t bitsPerPixel = bitsPerComponent * components;
660+
// Read: https://github.com/path/FastImageCache#byte-alignment
661+
// A properly aligned bytes-per-row value must be a multiple of 8 pixels × bytes per pixel
662+
// For a typical ARGB image, the aligned bytes-per-row value is a multiple of 64.
663+
size_t alignment = [SDImageCoderHelper preferredByteAlignment];
664+
size_t bytesPerRow = SDByteAlign(width * (bitsPerPixel / 8), alignment);
665+
666+
void *rgba = WebPMalloc(bytesPerRow * height);
667+
config.output.is_external_memory = 1;
668+
config.output.u.RGBA.rgba = rgba;
669+
config.output.u.RGBA.stride = (int)bytesPerRow;
670+
config.output.u.RGBA.size = height * bytesPerRow;
671+
563672
// Decode the WebP image data into a RGBA value array
564673
if (WebPDecode(webpData.bytes, webpData.size, &config) != VP8_STATUS_OK) {
565674
return nil;
@@ -568,13 +677,9 @@ - (nullable CGImageRef)sd_createWebpImageWithData:(WebPData)webpData colorSpace:
568677
// Construct a UIImage from the decoded RGBA value array
569678
CGDataProviderRef provider =
570679
CGDataProviderCreateWithData(NULL, config.output.u.RGBA.rgba, config.output.u.RGBA.size, FreeImageData);
571-
size_t bitsPerComponent = 8;
572-
size_t bitsPerPixel = 32;
573-
size_t bytesPerRow = config.output.u.RGBA.stride;
574-
size_t width = config.output.width;
575-
size_t height = config.output.height;
680+
BOOL shouldInterpolate = YES;
576681
CGColorRenderingIntent renderingIntent = kCGRenderingIntentDefault;
577-
CGImageRef imageRef = CGImageCreate(width, height, bitsPerComponent, bitsPerPixel, bytesPerRow, colorSpaceRef, bitmapInfo, provider, NULL, NO, renderingIntent);
682+
CGImageRef imageRef = CGImageCreate(width, height, bitsPerComponent, bitsPerPixel, bytesPerRow, colorSpaceRef, bitmapInfo, provider, NULL, shouldInterpolate, renderingIntent);
578683

579684
CGDataProviderRelease(provider);
580685

@@ -756,9 +861,6 @@ - (nullable NSData *)sd_encodedWebpDataWithImage:(nullable CGImageRef)imageRef
756861
}
757862

758863
size_t bytesPerRow = CGImageGetBytesPerRow(imageRef);
759-
size_t bitsPerComponent = CGImageGetBitsPerComponent(imageRef);
760-
size_t bitsPerPixel = CGImageGetBitsPerPixel(imageRef);
761-
size_t components = bitsPerPixel / bitsPerComponent;
762864
CGBitmapInfo bitmapInfo = CGImageGetBitmapInfo(imageRef);
763865
CGImageAlphaInfo alphaInfo = bitmapInfo & kCGBitmapAlphaInfoMask;
764866
CGBitmapInfo byteOrderInfo = bitmapInfo & kCGBitmapByteOrderMask;
@@ -891,7 +993,7 @@ - (void) updateWebPOptionsToConfig:(WebPConfig * _Nonnull)config
891993
}
892994

893995
static void FreeImageData(void *info, const void *data, size_t size) {
894-
free((void *)data);
996+
WebPFree((void *)data);
895997
}
896998

897999
static int GetIntValueForKey(NSDictionary * _Nonnull dictionary, NSString * _Nonnull key, int defaultValue) {
@@ -1236,6 +1338,8 @@ - (UIImage *)safeAnimatedImageFrameAtIndex:(NSUInteger)index {
12361338
#else
12371339
image = [[UIImage alloc] initWithCGImage:imageRef scale:_scale orientation:kCGImagePropertyOrientationUp];
12381340
#endif
1341+
image.sd_imageFormat = SDImageFormatWebP;
1342+
image.sd_isDecoded = YES; // We handle byte alignment and alloc bitmap buffer
12391343
CGImageRelease(imageRef);
12401344

12411345
WebPDemuxReleaseIterator(&iter);

0 commit comments

Comments
 (0)