Skip to content

ICC Profile is not embedded when encoding to WebP #119

@KYHyeon

Description

@KYHyeon

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 created

Root Cause Analysis

In SDImageWebPCoder.m, the encoding implementation:

  1. ✅ Extracts the input CGColorSpace (line ~886):

    CGColorSpaceRef colorSpace = CGImageGetColorSpace(imageRef);
  2. ✅ 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);
  3. ❌ Does NOT:

    • Extract ICC profile data using CGColorSpaceCopyICCData()
    • Add ICCP chunk to the WebP container
    • Set ICCP_FLAG in VP8X chunk (when present)

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

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!

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions