Skip to content

fix(Android): decode wide-gamut sources into sRGB - #407

Open
toyaji wants to merge 1 commit into
fluttercandies:mainfrom
toyaji:fix/android-srgb-color-space
Open

fix(Android): decode wide-gamut sources into sRGB#407
toyaji wants to merge 1 commit into
fluttercandies:mainfrom
toyaji:fix/android-srgb-color-space

Conversation

@toyaji

@toyaji toyaji commented Aug 31, 2026

Copy link
Copy Markdown

Problem

On Android, BitmapFactory decodes a Display P3 / Adobe RGB source keeping its wide color space, and the subsequent Bitmap.compress() does not reliably tag the JPEG/WebP/HEIC output with an ICC profile (it varies by format and API level). A non-color-managed consumer then interprets those wide-gamut pixel values as sRGB, so the result looks oversaturated / hue-shifted compared to the source as shown in the system gallery.

This is most visible with photos from devices that shoot Display P3 by default (widely reported on Samsung). iOS is unaffected because UIImage+scale.m already pins its render range to sRGB (UIGraphicsImageRendererFormat.preferredRange = .standard); Android had no equivalent.

Change

Pin every BitmapFactory decode in the common Android implementation to sRGB via BitmapFactory.Options.inPreferredColorSpace, so wide-gamut sources are color-managed down to sRGB at decode time and the output is self-describing sRGB regardless of the encoder's ICC behaviour. This brings Android in line with the iOS side.

  • New BitmapFactory.Options.preferSrgbColorSpace() extension (ext/BitmapCompressExt.kt)
  • Applied at all 3 decode sites: CommonHandler.compress(byteArray…), CommonHandler.handleFile, HeifHandler.makeOption
  • inPreferredColorSpace exists from API 26; the helper is a no-op below that, where the platform has no color management anyway. No public API change.

Verification

The repo has no Android instrumentation-test harness, so this was verified on a physical device (Galaxy S21, Android 15):

  1. Built a JPEG carrying an Apple "Display P3" ICC profile (solid saturated color).
  2. Ran FlutterImageCompress.compressWithFile(..., format: jpeg) and scanned the output bytes for the ICC Display P3 signature.
Build Result
main (before) output JPEG still carries the Display P3 profile → wide-gamut pixels passed through
this branch Display P3 profile gone, output is sRGB

(Happy to add an integration test under packages/flutter_image_compress/example/integration_test/ if you want one — would need a small P3 fixture asset.)

Open question

Should this be unconditional (matches iOS, "just works" for the common case) or gated behind a new opt-in parameter? I went with unconditional for parity, but can rework it into a keepColorSpace / targetColorSpace option if you'd prefer to preserve the current passthrough behaviour by default.

Display P3 / Adobe RGB files were decoded keeping their wide color space,
and Bitmap.compress() does not reliably tag the JPEG/WebP/HEIC output with
the ICC profile. Non-color-managed viewers then render those pixels as
sRGB and they look oversaturated (commonly reported on Samsung devices,
which shoot P3 by default).

Pin every BitmapFactory decode to sRGB via inPreferredColorSpace (API
26+, no-op on older devices). Matches the iOS side, which already fixes
its render range to sRGB.
@toyaji
toyaji requested a review from CaiJingLong as a code owner August 31, 2026 01:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant