
If someone on your team has bumped a thumbnail service from quality 80 to 95 because the images look faded, check one thing before touching quality again: does the output file still carry the source image's ICC profile? On the sample I tested, removing the profile made the photo visibly duller while every pixel value stayed exactly the same. Saving that file at quality 100 wouldn't bring the color back, because nothing was lost to compression in the first place.
The sample
A CC0 photo from Wikimedia Commons, shot on an iPhone 6 at Madrid airport, resized to 2048×1536. With Pillow 11.3's ImageCms and the ICC files that ship with macOS, I wrote three JPEGs at quality 92: one tagged sRGB, one converted to Display P3, one converted to Adobe RGB (1998). The photo's colors sit inside sRGB, so this isn't a wide-gamut stress test, and the numbers below describe this one image only.
Two different kinds of worse
Compression damage lives in the details: soft edges, banding across smooth sky, 8×8 blocks when you zoom in. A dropped profile does something else. The RGB numbers don't change, but without a profile the viewer assumes sRGB, and P3 or Adobe RGB values read as sRGB come out less saturated. I measured the difference in CIELAB (ΔE76) between each tagged file and the same pixels read as sRGB:
| Profile dropped | mean ΔE | p95 ΔE | pixels with ΔE > 5 | mean chroma loss |
|---|---|---|---|---|
| Display P3 | 1.83 | 3.9 | 1.7% | 16.6% |
| Adobe RGB | 2.91 | 5.27 | 7.4% | 20.1% |
A mean ΔE under 3 sounds harmless, and the chroma column is where the problem shows. Grays, whites and shadows barely move. The most saturated 5% of pixels shift by ΔE 5.4 with P3 and 7.2 with Adobe RGB, which in this photo is the red and green tail livery and the blue sky going pale. So blurry or blocky points at compression, while uniformly faded with intact detail points at the profile.
Where the profile gets dropped
Input: JPEG tagged Display P3. Sample: Wikimedia Commons CC0 photo (iPhone 6), profile converted locally.
I fed the P3 JPEG through default save paths. Pillow 11.3 drops the profile when saving JPEG or WebP, after resize() or thumbnail() too, and quality 100 changes nothing; saving PNG keeps it. ffmpeg 7.1 keeps it converting to JPEG or PNG and drops it converting to WebP. macOS sips kept it for PNG, -Z resizing and a quality 60 re-save. Pillow resize followed by WebP output, which is probably the most common thumbnail recipe, hits the problem twice. The fix is to pass the profile through explicitly:
from PIL import Image
thumb = Image.open(src_path)
icc = thumb.info.get("icc_profile") # None when the source is untagged
thumb.thumbnail((800, 800))
thumb.save(dst_path, "WEBP", quality=80, icc_profile=icc)
Run against the P3 sample, both the JPEG and WebP outputs read back as Display P3. The other option is converting to sRGB with ImageCms.profileToProfile before saving, which protects you from any later step that strips profiles again. I haven't settled which one I want for thumbnails. On a sample that fits inside sRGB the two look identical, and I have no wide-gamut data to judge what the conversion clips.
One trap while checking: sips -g profile reports sRGB IEC61966-2.1 for a JPEG with no embedded profile, so it can't tell untagged from real sRGB. I trust Image.open(path).info.get("icc_profile") instead.
What about canvas compression in the browser?

Sample: Wikimedia Commons CC0 photo (iPhone 6), converted to Display P3 locally. Firefox is the build bundled with Playwright.
Using Playwright's bundled engines on macOS 26.5, Chromium 149 and WebKit 26.5 honor the profile in <img>, and pixels read back from a canvas are already converted to sRGB (mean difference from the sRGB source: 0.66 and 0.69, which is JPEG noise). toBlob('image/jpeg') output looks right in both. Chromium also embeds an sRGB profile; WebKit's JPEG has none, which is fine because the pixels are already sRGB. The Firefox 151 build bundled with Playwright rendered the tagged and stripped files the same (mean screenshot difference 0.03), read back unconverted P3 values, and exported an untagged JPEG that looks washed out. That's one build with default settings, not a statement about release Firefox. It also means a has-profile check alone can mislead on browser exports, since WebKit's untagged JPEG is correct.
The order I check things in now
- Does the source have a profile, and which one? If it's sRGB or absent, a dropped profile can't explain the fade, so go look at compression.
- Does the output still have it? If the source is P3 and the output has nothing, the pipeline dropped it.
- Only when both are tagged and colors are still off do quality settings and client-side export become suspects.
Apple's developer documentation notes that iPhone 7 and later can capture in P3, so tagged uploads are ordinary input rather than an edge case.
Top comments (0)