-
Notifications
You must be signed in to change notification settings - Fork 92
Description
Issue: ICC Profile is not embedded when encoding to WebP
Description
When encoding images with wide color gamut color spaces (such as Display P3) to WebP format, the ICC color profile is not embedded in the output WebP file. This causes color reproduction issues when the WebP file is viewed on platforms that assume sRGB color space (e.g., Android devices, web browsers).
While SDWebImageWebPCoder correctly handles color space during pixel conversion (using vImage with the input CGColorSpace), it does not preserve the ICC profile metadata in the output WebP file, resulting in incorrect color interpretation on other platforms.
Environment
- SDWebImageWebPCoder version: 0.14.6
- SDWebImage version: 5.21.3
- Platform: macOS / iOS
- Swift version: 6.2
Steps to Reproduce
import SDWebImage
import SDWebImageWebPCoder
// 1. Register WebP coder
SDImageCodersManager.shared.addCoder(SDImageWebPCoder.shared)
// 2. Create an image with Display P3 color space
let colorSpace = CGColorSpace(name: CGColorSpace.displayP3)!
let context = CGContext(
data: nil,
width: 100,
height: 100,
bitsPerComponent: 8,
bytesPerRow: 0,
space: colorSpace,
bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue
)!
context.setFillColor(CGColor(colorSpace: colorSpace, components: [1.0, 0.0, 0.0, 1.0])!)
context.fill(CGRect(x: 0, y: 0, width: 100, height: 100))
let cgImage = context.makeImage()!
let image = UIImage(cgImage: cgImage)
// 3. Encode to WebP
let options: [SDImageCoderOption: Any] = [
.encodeCompressionQuality: 0.9
]
let webpData = SDImageWebPCoder.shared.encodedData(
with: image,
format: .webP,
options: options
)
// 4. Save and analyze
try webpData?.write(to: URL(fileURLWithPath: "output.webp"))Expected Behavior
The output WebP file should contain an ICCP chunk with the Display P3 ICC profile data, regardless of whether the image has an alpha channel or not.
For images without alpha (RGB):
Offset Content
------ -------
0x00 RIFF header
0x08 WEBP magic
0x0C VP8X chunk ✅ (created by WebPMux)
0x14 Flags: 0x00000020 ✅ (ICCP_FLAG set)
0x1E ICCP chunk ✅ (536 bytes Display P3 profile)
VP8/VP8L chunk (image data)
For images with alpha (RGBA):
Offset Content
------ -------
0x00 RIFF header
0x08 WEBP magic
0x0C VP8X chunk ✅ (already exists for alpha)
0x14 Flags: 0x00000030 ✅ (ALPHA_FLAG + ICCP_FLAG)
0x1E ICCP chunk ✅ (536 bytes Display P3 profile)
VP8/VP8L chunk (image data with alpha)
Actual Behavior
Case 1: Images without alpha channel (RGB)
The output uses Simple Format without any metadata:
Offset Content
------ -------
0x00 RIFF header
0x08 WEBP magic
0x0C VP8 chunk ❌ (Simple Format)
(no ICC profile) ❌
Case 2: Images with alpha channel (RGBA)
The output uses Extended Format with VP8X, but still no ICC profile:
Offset Content
------ -------
0x00 RIFF header
0x08 WEBP magic
0x0C VP8X chunk ✅ (created for alpha)
0x14 Flags: 0x00000010 ⚠️ (ALPHA_FLAG only, no ICCP_FLAG)
(no ICCP chunk) ❌
VP8/VP8L chunk (image data with alpha)
Python PIL comparison (correct behavior):
Python PIL correctly embeds ICC profiles in both cases:
from PIL import Image
img = Image.open('displayp3_image.png') # 536 bytes ICC profile
img.save('output.webp', 'WEBP', quality=90,
icc_profile=img.info.get('icc_profile'))
# ✅ VP8X + ICCP chunk createdRoot Cause Analysis
In SDImageWebPCoder.m, the encoding implementation:
-
✅ Extracts the input CGColorSpace (line ~886):
CGColorSpaceRef colorSpace = CGImageGetColorSpace(imageRef);
-
✅ Uses it for vImage pixel conversion (line ~890):
vImage_CGImageFormat destFormat = { .colorSpace = colorSpace, // Display P3 preserved during conversion ... }; vImageBuffer_InitWithCGImage(&dest, &destFormat, NULL, imageRef, kvImageNoFlags); -
❌ Does NOT:
- Extract ICC profile data using
CGColorSpaceCopyICCData() - Add ICCP chunk to the WebP container
- Set ICCP_FLAG in VP8X chunk (when present)
- Extract ICC profile data using
Result: Pixel values are correctly converted to Display P3, but the color space metadata is completely lost.
Impact on Different Scenarios
| Image Type | Current Output | Missing | Impact |
|---|---|---|---|
| RGB (no alpha) | VP8 Simple Format | VP8X + ICCP | Severe color shift |
| RGBA (with alpha) | VP8X + ALPHA_FLAG | ICCP_FLAG + ICCP | Severe color shift |
| sRGB images | Works correctly | N/A | No impact |
Comparison: Decoding vs Encoding
The library successfully handles ICC profiles during decoding but not encoding:
Decoding (✅ Working):
// Line ~704: sd_createColorSpaceWithDemuxer
if (flags & ICCP_FLAG) {
WebPDemuxGetChunk(demuxer, "ICCP", 1, &chunk_iter);
NSData *profileData = [NSData dataWithBytes:chunk_iter.chunk.bytes ...];
colorSpaceRef = CGColorSpaceCreateWithICCData((__bridge CFDataRef)profileData);
}Encoding (❌ Missing):
// No equivalent code to:
// 1. Extract: CFDataRef iccData = CGColorSpaceCopyICCData(colorSpace);
// 2. Add ICCP: WebPMuxSetChunk(mux, "ICCP", &icc_chunk, 0);Related Information
- WebP specification supports ICC profiles: https://developers.google.com/speed/webp/docs/riff_container#color_profile
- Issue Encoding Results in Washed-Out Colors #89 (merged) fixed color space handling in vImage conversion but did not add ICC profile embedding
Proposed Solution
Add ICC profile embedding to sd_encodedWebpDataWithImage method using the WebPMux API:
- (nullable NSData *)sd_encodedWebpDataWithImage:(nullable CGImageRef)imageRef
quality:(double)quality
maxPixelSize:(CGSize)maxPixelSize
maxFileSize:(NSUInteger)maxFileSize
options:(nullable SDImageCoderOptions *)options
{
// ... existing encoding code ...
// After WebPEncode succeeds
result = WebPEncode(&config, &picture);
WebPPictureFree(&picture);
free(dest.data);
if (result) {
NSData *webpData = [NSData dataWithBytes:writer.mem length:writer.size];
// Add ICC profile if present
CFDataRef iccData = NULL;
if (colorSpace) {
iccData = CGColorSpaceCopyICCData(colorSpace);
}
if (iccData && CFDataGetLength(iccData) > 0) {
// Use WebPMux to add ICCP chunk
// This automatically converts Simple Format to Extended Format (VP8X)
WebPMux *mux = WebPMuxNew();
if (mux) {
WebPData webp_input = {
.bytes = webpData.bytes,
.size = webpData.length
};
// Set image (creates VP8X if needed)
if (WebPMuxSetImage(mux, &webp_input, 1) == WEBP_MUX_OK) {
// Add ICC profile chunk
WebPData icc_chunk = {
.bytes = CFDataGetBytePtr(iccData),
.size = CFDataGetLength(iccData)
};
if (WebPMuxSetChunk(mux, "ICCP", &icc_chunk, 1) == WEBP_MUX_OK) {
WebPData output;
if (WebPMuxAssemble(mux, &output) == WEBP_MUX_OK) {
webpData = [NSData dataWithBytes:output.bytes length:output.size];
WebPDataClear(&output);
}
}
}
WebPMuxDelete(mux);
}
CFRelease(iccData);
}
WebPMemoryWriterClear(&writer);
return webpData;
} else {
// failed
WebPMemoryWriterClear(&writer);
return nil;
}
}Request
Would it be possible to add ICC profile embedding support to the WebP encoding path? This would bring feature parity with the decoding path and other WebP implementations.
I'm happy to contribute a PR if that would be helpful. I have:
- Identified the exact code location for the fix
- Tested the proposed solution approach
- Verified the issue with real-world examples
Thank you for maintaining this excellent library!