Skip to content

Avoid force-decode by apply the byte alignment for static WebP images, using runtime detection for bitmap info #77

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Jul 13, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Cartfile
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
github "SDWebImage/SDWebImage" ~> 5.16
github "SDWebImage/SDWebImage" ~> 5.17
github "SDWebImage/libwebp-Xcode" ~> 1.0
2 changes: 1 addition & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ let package = Package(
dependencies: [
// Dependencies declare other packages that this package depends on.
// .package(url: /* package url */, from: "1.0.0"),
.package(url: "https://github.com/SDWebImage/SDWebImage.git", from: "5.16.0"),
.package(url: "https://github.com/SDWebImage/SDWebImage.git", from: "5.17.0"),
.package(url: "https://github.com/SDWebImage/libwebp-Xcode.git", from: "1.1.0")
],
targets: [
Expand Down
2 changes: 1 addition & 1 deletion SDWebImageWebPCoder.podspec
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ This is a SDWebImage coder plugin to support WebP image.
'USER_HEADER_SEARCH_PATHS' => '$(inherited) $(SRCROOT)/libwebp/src'
}
s.framework = 'CoreGraphics'
s.dependency 'SDWebImage/Core', '~> 5.16'
s.dependency 'SDWebImage/Core', '~> 5.17'
s.dependency 'libwebp', '~> 1.0'

end
164 changes: 127 additions & 37 deletions SDWebImageWebPCoder/Classes/SDImageWebPCoder.m
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,8 @@
/// Used for animated WebP, which need a canvas for decoding (rendering), possible apply a scale transform for thumbnail decoding (avoiding post-rescale using vImage)
/// See more in #73
static inline CGContextRef _Nullable CreateWebPCanvas(BOOL hasAlpha, CGSize canvasSize, CGSize thumbnailSize, BOOL preserveAspectRatio) {
CGBitmapInfo bitmapInfo = kCGBitmapByteOrder32Host;
bitmapInfo |= hasAlpha ? kCGImageAlphaPremultipliedFirst : kCGImageAlphaNoneSkipFirst;
// From SDWebImage v5.17.0, use runtime detection of bitmap info instead of hardcode.
CGBitmapInfo bitmapInfo = [SDImageCoderHelper preferredPixelFormat:hasAlpha].bitmapInfo;
// Check whether we need to use thumbnail
CGSize scaledSize = [SDImageCoderHelper scaledSizeWithImageSize:CGSizeMake(canvasSize.width, canvasSize.height) scaleSize:thumbnailSize preserveAspectRatio:preserveAspectRatio shouldScaleUp:NO];
CGContextRef canvas = CGBitmapContextCreate(NULL, scaledSize.width, scaledSize.height, 8, 0, [SDImageCoderHelper colorSpaceGetDeviceRGB], bitmapInfo);
Expand All @@ -88,18 +88,87 @@ static inline CGContextRef _Nullable CreateWebPCanvas(BOOL hasAlpha, CGSize canv
return canvas;
}

// 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`)
static inline CGSize SDCalculateScaleDownPixelSize(NSUInteger limitBytes, CGSize originalSize, NSUInteger frameCount, NSUInteger bytesPerPixel) {
if (CGSizeEqualToSize(originalSize, CGSizeZero)) return CGSizeMake(1, 1);
NSUInteger totalFramePixelSize = limitBytes / bytesPerPixel / (frameCount ?: 1);
CGFloat ratio = originalSize.height / originalSize.width;
CGFloat width = sqrt(totalFramePixelSize / ratio);
CGFloat height = width * ratio;
width = MAX(1, floor(width));
height = MAX(1, floor(height));
CGSize size = CGSizeMake(width, height);

return size;
WEBP_CSP_MODE ConvertCSPMode(CGBitmapInfo bitmapInfo) {
// Get alpha info, byteOrder info
CGImageAlphaInfo alphaInfo = bitmapInfo & kCGBitmapAlphaInfoMask;
CGBitmapInfo byteOrderInfo = bitmapInfo & kCGBitmapByteOrderMask;
BOOL byteOrderNormal = NO;
switch (byteOrderInfo) {
case kCGBitmapByteOrderDefault: {
byteOrderNormal = YES;
} break;
case kCGBitmapByteOrder32Little: {
} break;
case kCGBitmapByteOrder32Big: {
byteOrderNormal = YES;
} break;
default: break;
}
switch (alphaInfo) {
case kCGImageAlphaPremultipliedFirst: {
if (byteOrderNormal) {
// ARGB8888, premultiplied
return MODE_Argb;
} else {
// BGRA8888, premultiplied
return MODE_bgrA;
}
}
break;
case kCGImageAlphaPremultipliedLast: {
if (byteOrderNormal) {
// RGBA8888, premultiplied
return MODE_rgbA;
} else {
// ABGR8888, premultiplied
// Unsupported!
return MODE_LAST;
}
}
break;
case kCGImageAlphaNone: {
if (byteOrderNormal) {
// RGB
return MODE_RGB;
} else {
// BGR
return MODE_BGR;
}
}
break;
case kCGImageAlphaLast:
case kCGImageAlphaNoneSkipLast: {
if (byteOrderNormal) {
// RGBA or RGBX
return MODE_RGBA;
} else {
// ABGR or XBGR
// Unsupported!
return MODE_LAST;
}
}
break;
case kCGImageAlphaFirst:
case kCGImageAlphaNoneSkipFirst: {
if (byteOrderNormal) {
// ARGB or XRGB
return MODE_ARGB;
} else {
// BGRA or BGRX
return MODE_BGRA;
}
}
break;
case kCGImageAlphaOnly: {
// A
// Unsupported
return MODE_LAST;
}
break;
default:
break;
}
return MODE_LAST;
}

@interface SDWebPCoderFrame : NSObject
Expand Down Expand Up @@ -245,7 +314,7 @@ - (UIImage *)decodedImageWithData:(NSData *)data options:(nullable SDImageCoderO
if (limitBytes > 0) {
// Hack 32 BitsPerPixel
CGSize imageSize = CGSizeMake(canvasWidth, canvasHeight);
CGSize framePixelSize = SDCalculateScaleDownPixelSize(limitBytes, imageSize, frameCount, 4);
CGSize framePixelSize = [SDImageCoderHelper scaledSizeWithImageSize:imageSize limitBytes:limitBytes bytesPerPixel:4 frameCount:frameCount];
// Override thumbnail size
thumbnailSize = framePixelSize;
preserveAspectRatio = YES;
Expand Down Expand Up @@ -317,8 +386,8 @@ - (UIImage *)decodedImageWithData:(NSData *)data options:(nullable SDImageCoderO
- (instancetype)initIncrementalWithOptions:(nullable SDImageCoderOptions *)options {
self = [super init];
if (self) {
// Progressive images need transparent, so always use premultiplied BGRA
_idec = WebPINewRGB(MODE_bgrA, NULL, 0, 0);
// Progressive images need transparent, so always use premultiplied RGBA
_idec = WebPINewRGB(MODE_rgbA, NULL, 0, 0);
CGFloat scale = 1;
NSNumber *scaleFactor = options[SDImageCoderDecodeScaleFactor];
if (scaleFactor != nil) {
Expand Down Expand Up @@ -394,7 +463,7 @@ - (void)updateIncrementalData:(NSData *)data finished:(BOOL)finished {
if (_limitBytes > 0) {
// Hack 32 BitsPerPixel
CGSize imageSize = CGSizeMake(_canvasWidth, _canvasHeight);
CGSize framePixelSize = SDCalculateScaleDownPixelSize(_limitBytes, imageSize, _frameCount, 4);
CGSize framePixelSize = [SDImageCoderHelper scaledSizeWithImageSize:imageSize limitBytes:_limitBytes bytesPerPixel:4 frameCount:_frameCount];
// Override thumbnail size
_thumbnailSize = framePixelSize;
_preserveAspectRatio = YES;
Expand Down Expand Up @@ -428,17 +497,18 @@ - (UIImage *)incrementalDecodedImageWithOptions:(SDImageCoderOptions *)options {
CGDataProviderRef provider =
CGDataProviderCreateWithData(NULL, rgba, rgbaSize, NULL);
CGColorSpaceRef colorSpaceRef = [SDImageCoderHelper colorSpaceGetDeviceRGB];

CGBitmapInfo bitmapInfo = kCGBitmapByteOrder32Host | kCGImageAlphaPremultipliedFirst;
// Because _idec use MODE_rgbA
CGBitmapInfo bitmapInfo = kCGBitmapByteOrderDefault | kCGImageAlphaPremultipliedLast;
size_t components = 4;
BOOL shouldInterpolate = YES;
CGColorRenderingIntent renderingIntent = kCGRenderingIntentDefault;
// Why to use last_y for image height is because of libwebp's bug (https://bugs.chromium.org/p/webp/issues/detail?id=362)
// It will not keep memory barrier safe on x86 architechure (macOS & iPhone simulator) but on ARM architecture (iPhone & iPad & tv & watch) it works great
// If different threads use WebPIDecGetRGB to grab rgba bitmap, it will contain the previous decoded bitmap data
// So this will cause our drawed image looks strange(above is the current part but below is the previous part)
// We only grab the last_y height and draw the last_y height instead of total height image
// Besides fix, this can enhance performance since we do not need to create extra bitmap
CGImageRef imageRef = CGImageCreate(width, last_y, 8, components * 8, components * width, colorSpaceRef, bitmapInfo, provider, NULL, NO, renderingIntent);
CGImageRef imageRef = CGImageCreate(width, last_y, 8, components * 8, components * width, colorSpaceRef, bitmapInfo, provider, NULL, shouldInterpolate, renderingIntent);

CGDataProviderRelease(provider);

Expand Down Expand Up @@ -546,20 +616,46 @@ - (nullable CGImageRef)sd_createWebpImageWithData:(WebPData)webpData colorSpace:
}

BOOL hasAlpha = config.input.has_alpha;
// iOS prefer BGRA8888 (premultiplied) or BGRX8888 bitmapInfo for screen rendering, which is same as `UIGraphicsBeginImageContext()` or `- [CALayer drawInContext:]`
// 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)
CGBitmapInfo bitmapInfo = kCGBitmapByteOrder32Host;
bitmapInfo |= hasAlpha ? kCGImageAlphaPremultipliedFirst : kCGImageAlphaNoneSkipFirst;
// From SDWebImage v5.17.0, use runtime detection of bitmap info instead of hardcode.
SDImagePixelFormat pixelFormat = [SDImageCoderHelper preferredPixelFormat:hasAlpha];
CGBitmapInfo bitmapInfo = pixelFormat.bitmapInfo;
WEBP_CSP_MODE mode = ConvertCSPMode(bitmapInfo);
if (mode == MODE_LAST) {
NSAssert(NO, @"Unsupported libwebp preferred CGBitmapInfo: %d", bitmapInfo);
return nil;
}
config.output.colorspace = mode;
config.options.use_threads = 1;
config.output.colorspace = MODE_bgrA;


// Use scaling for thumbnail
size_t width = config.input.width;
size_t height = config.input.height;
if (scaledSize.width != 0 && scaledSize.height != 0) {
config.options.use_scaling = 1;
config.options.scaled_width = scaledSize.width;
config.options.scaled_height = scaledSize.height;
width = scaledSize.width;
height = scaledSize.height;
}

// 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`
size_t bitsPerComponent = 8;
size_t components = (mode == MODE_RGB || mode == MODE_BGR) ? 3 : 4; // Actually always 4
size_t bitsPerPixel = bitsPerComponent * components;
// Read: https://github.com/path/FastImageCache#byte-alignment
// A properly aligned bytes-per-row value must be a multiple of 8 pixels × bytes per pixel
// For a typical ARGB image, the aligned bytes-per-row value is a multiple of 64.
size_t alignment = pixelFormat.alignment;
size_t bytesPerRow = SDByteAlign(width * (bitsPerPixel / 8), alignment);
//size_t bytesPerRow = 6688;

void *rgba = WebPMalloc(bytesPerRow * height);
config.output.is_external_memory = 1;
config.output.u.RGBA.rgba = rgba;
config.output.u.RGBA.stride = (int)bytesPerRow;
config.output.u.RGBA.size = height * bytesPerRow;

// Decode the WebP image data into a RGBA value array
if (WebPDecode(webpData.bytes, webpData.size, &config) != VP8_STATUS_OK) {
return nil;
Expand All @@ -568,13 +664,9 @@ - (nullable CGImageRef)sd_createWebpImageWithData:(WebPData)webpData colorSpace:
// Construct a UIImage from the decoded RGBA value array
CGDataProviderRef provider =
CGDataProviderCreateWithData(NULL, config.output.u.RGBA.rgba, config.output.u.RGBA.size, FreeImageData);
size_t bitsPerComponent = 8;
size_t bitsPerPixel = 32;
size_t bytesPerRow = config.output.u.RGBA.stride;
size_t width = config.output.width;
size_t height = config.output.height;
BOOL shouldInterpolate = YES;
CGColorRenderingIntent renderingIntent = kCGRenderingIntentDefault;
CGImageRef imageRef = CGImageCreate(width, height, bitsPerComponent, bitsPerPixel, bytesPerRow, colorSpaceRef, bitmapInfo, provider, NULL, NO, renderingIntent);
CGImageRef imageRef = CGImageCreate(width, height, bitsPerComponent, bitsPerPixel, bytesPerRow, colorSpaceRef, bitmapInfo, provider, NULL, shouldInterpolate, renderingIntent);

CGDataProviderRelease(provider);

Expand Down Expand Up @@ -756,9 +848,6 @@ - (nullable NSData *)sd_encodedWebpDataWithImage:(nullable CGImageRef)imageRef
}

size_t bytesPerRow = CGImageGetBytesPerRow(imageRef);
size_t bitsPerComponent = CGImageGetBitsPerComponent(imageRef);
size_t bitsPerPixel = CGImageGetBitsPerPixel(imageRef);
size_t components = bitsPerPixel / bitsPerComponent;
CGBitmapInfo bitmapInfo = CGImageGetBitmapInfo(imageRef);
CGImageAlphaInfo alphaInfo = bitmapInfo & kCGBitmapAlphaInfoMask;
CGBitmapInfo byteOrderInfo = bitmapInfo & kCGBitmapByteOrderMask;
Expand Down Expand Up @@ -891,7 +980,7 @@ - (void) updateWebPOptionsToConfig:(WebPConfig * _Nonnull)config
}

static void FreeImageData(void *info, const void *data, size_t size) {
free((void *)data);
WebPFree((void *)data);
}

static int GetIntValueForKey(NSDictionary * _Nonnull dictionary, NSString * _Nonnull key, int defaultValue) {
Expand Down Expand Up @@ -968,7 +1057,7 @@ - (instancetype)initWithAnimatedImageData:(NSData *)data options:(nullable SDIma
if (_limitBytes > 0) {
// Hack 32 BitsPerPixel
CGSize imageSize = CGSizeMake(_canvasWidth, _canvasHeight);
CGSize framePixelSize = SDCalculateScaleDownPixelSize(_limitBytes, imageSize, _frameCount, 4);
CGSize framePixelSize = [SDImageCoderHelper scaledSizeWithImageSize:imageSize limitBytes:_limitBytes bytesPerPixel:4 frameCount:_frameCount];
// Override thumbnail size
_thumbnailSize = framePixelSize;
_preserveAspectRatio = YES;
Expand Down Expand Up @@ -1236,6 +1325,7 @@ - (UIImage *)safeAnimatedImageFrameAtIndex:(NSUInteger)index {
#else
image = [[UIImage alloc] initWithCGImage:imageRef scale:_scale orientation:kCGImagePropertyOrientationUp];
#endif
image.sd_imageFormat = SDImageFormatWebP;
CGImageRelease(imageRef);

WebPDemuxReleaseIterator(&iter);
Expand Down
Binary file added Tests/Images/TestColorspaceStatic.webp
Binary file not shown.
12 changes: 12 additions & 0 deletions Tests/SDWebImageWebPCoderTests.m
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,18 @@ - (void)test45WebPEncodingMaxFileSize {
XCTAssertLessThanOrEqual(dataWithLimit.length, maxFileSize);
}

- (void)testWebPDecodeDoesNotTriggerCACopyImage {
NSURL *staticWebPURL = [[NSBundle bundleForClass:[self class]] URLForResource:@"TestColorspaceStatic" withExtension:@"webp"];
NSData *data = [NSData dataWithContentsOfURL:staticWebPURL];
UIImage *image = [SDImageWebPCoder.sharedCoder decodedImageWithData:data options:@{SDImageCoderDecodeThumbnailPixelSize: @(CGSizeMake(1023, 680))}]; // 1023 * 4 need aligned to 4096
CGImageRef cgImage = [image CGImage];
size_t bytesPerRow = CGImageGetBytesPerRow(cgImage);
XCTAssertEqual(bytesPerRow, 4096);
CGColorSpaceRef colorspace = CGImageGetColorSpace(cgImage);
NSString *colorspaceName = (__bridge_transfer NSString *)CGColorSpaceCopyName(colorspace);
XCTAssertEqual(colorspaceName, (__bridge NSString *)kCGColorSpaceSRGB, @"Color space is not sRGB");
}

- (void)testEncodingSettings {
WebPConfig config;
WebPConfigPreset(&config, WEBP_PRESET_DEFAULT, 0.2);
Expand Down
6 changes: 6 additions & 0 deletions Tests/SDWebImageWebPCoderTests.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
0EF5B6264833B7BC20894578 /* Pods_SDWebImageWebPCoderTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 46F21AD7D1692EBAC4D0FF33 /* Pods_SDWebImageWebPCoderTests.framework */; };
3219F3B2228B0453003822A6 /* TestImageBlendAnimated.webp in Resources */ = {isa = PBXBuildFile; fileRef = 3219F3B1228B0453003822A6 /* TestImageBlendAnimated.webp */; };
325E268E25C82BE1000B807B /* TestImageGrayscale.jpg in Resources */ = {isa = PBXBuildFile; fileRef = 325E268D25C82BE1000B807B /* TestImageGrayscale.jpg */; };
326420312A5D53E300EE3E46 /* TestColorspaceStatic.webp in Resources */ = {isa = PBXBuildFile; fileRef = 326420302A5D53E300EE3E46 /* TestColorspaceStatic.webp */; };
808C918E213FD131004B0F7C /* SDWebImageWebPCoderTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 808C918D213FD131004B0F7C /* SDWebImageWebPCoderTests.m */; };
808C919C213FD2B2004B0F7C /* TestImageStatic.webp in Resources */ = {isa = PBXBuildFile; fileRef = 808C919A213FD2B2004B0F7C /* TestImageStatic.webp */; };
808C919D213FD2B2004B0F7C /* TestImageAnimated.webp in Resources */ = {isa = PBXBuildFile; fileRef = 808C919B213FD2B2004B0F7C /* TestImageAnimated.webp */; };
Expand All @@ -19,6 +20,7 @@
28D8AA3D3015E075692FD3E3 /* Pods-SDWebImageWebPCoderTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SDWebImageWebPCoderTests.debug.xcconfig"; path = "../Pods/Target Support Files/Pods-SDWebImageWebPCoderTests/Pods-SDWebImageWebPCoderTests.debug.xcconfig"; sourceTree = "<group>"; };
3219F3B1228B0453003822A6 /* TestImageBlendAnimated.webp */ = {isa = PBXFileReference; lastKnownFileType = file; path = TestImageBlendAnimated.webp; sourceTree = "<group>"; };
325E268D25C82BE1000B807B /* TestImageGrayscale.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = TestImageGrayscale.jpg; sourceTree = "<group>"; };
326420302A5D53E300EE3E46 /* TestColorspaceStatic.webp */ = {isa = PBXFileReference; lastKnownFileType = file; path = TestColorspaceStatic.webp; sourceTree = "<group>"; };
46F21AD7D1692EBAC4D0FF33 /* Pods_SDWebImageWebPCoderTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_SDWebImageWebPCoderTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
808C918B213FD130004B0F7C /* SDWebImageWebPCoderTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SDWebImageWebPCoderTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
808C918D213FD131004B0F7C /* SDWebImageWebPCoderTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SDWebImageWebPCoderTests.m; sourceTree = "<group>"; };
Expand Down Expand Up @@ -80,6 +82,7 @@
808C9199213FD2B2004B0F7C /* Images */ = {
isa = PBXGroup;
children = (
326420302A5D53E300EE3E46 /* TestColorspaceStatic.webp */,
325E268D25C82BE1000B807B /* TestImageGrayscale.jpg */,
808C919A213FD2B2004B0F7C /* TestImageStatic.webp */,
808C919B213FD2B2004B0F7C /* TestImageAnimated.webp */,
Expand Down Expand Up @@ -157,6 +160,7 @@
3219F3B2228B0453003822A6 /* TestImageBlendAnimated.webp in Resources */,
808C919D213FD2B2004B0F7C /* TestImageAnimated.webp in Resources */,
808C919C213FD2B2004B0F7C /* TestImageStatic.webp in Resources */,
326420312A5D53E300EE3E46 /* TestColorspaceStatic.webp in Resources */,
325E268E25C82BE1000B807B /* TestImageGrayscale.jpg in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
Expand Down Expand Up @@ -377,6 +381,7 @@
"$(inherited)",
);
INFOPLIST_FILE = Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
PRODUCT_BUNDLE_IDENTIFIER = org.SDWebImage.SDWebImageWebPCoderTests;
PRODUCT_NAME = "$(TARGET_NAME)";
};
Expand All @@ -387,6 +392,7 @@
baseConfigurationReference = D92E6791BF088D1A101E670E /* Pods-SDWebImageWebPCoderTests.release.xcconfig */;
buildSettings = {
INFOPLIST_FILE = Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
PRODUCT_BUNDLE_IDENTIFIER = org.SDWebImage.SDWebImageWebPCoderTests;
PRODUCT_NAME = "$(TARGET_NAME)";
};
Expand Down