Skip to content

fix(j2k): write sRGB-equivalent ICC samples to RGB8/RGBA8 region output - #322

Merged
jcwal1516 merged 1 commit into
frames-sg:mainfrom
jcoyne:fix/srgb-icc-region-rgb8
Nov 6, 2026
Merged

jcwal1516 merged 1 commit into
frames-sg:mainfrom
jcoyne:fix/srgb-icc-region-rgb8

Conversation

@jcoyne

@jcoyne jcoyne commented Nov 5, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #321.

Problem

A three-component JP2 tagged with an embedded IEC 61966-2-1 sRGB ICC profile failed decode_region_into and decode_region_scaled_pow2_into to Rgb8/Rgba8:

unsupported: backend color space cannot be mapped to requested 8-bit pixel format

The only working route was decode_srgb8, which decodes the whole image. That isn't practical for large IIIF sources.

Change

The 8-bit output paths now treat a three-channel ICC profile as plain RGB for Rgb8/Rgba8 output when the profile is equivalent to sRGB. Samples are written without a colour transform. A profile qualifies when:

  • it is a restricted matrix/TRC RGB profile, the same kind decode_srgb8 accepts;
  • its linear RGB-to-sRGB matrix (moxcms transform_matrix) is within 2.5e-4 (off-diagonal row sums) and 1e-3 (diagonal) of identity. Near zero, one 8-bit sRGB code value is about 3.0e-4 of linear light, so the off-diagonal limit keeps crosstalk under one code value;
  • its 8-bit transform to sRGB moves no red, green, blue or neutral ramp sample by more than one code value.

The ramps alone are not enough. A negative off-diagonal term clips to zero on a single-channel ramp and cancels on neutrals, but it still shifts mixed colours: a test profile with matrix [[1.01, -0.01, 0], [0, 1, 0], [0, 0, 1]] moved (30, 255, 0) to a red of 10. That is why the matrix is checked directly.

This applies to decode_region_into, decode_region_scaled_pow2_into, the prepared region path, the whole-image decode_into fallback, and the streamed stripes of decode_rows_u8_bounded. The colour space is resolved once when each row sink is built, so the profile isn't re-checked per band. Other ICC profiles and 16-bit output stay unsupported, with the same error.

Tests

New file crates/j2k/tests/srgb_icc_region.rs:

  • With an sRGB ICC profile, output is byte-identical to the same image tagged with the enumerated sRGB colour space. Checked for region Rgb8/Rgba8, scaled regions at 1 and 2 levels, and whole-image decode_into, for both moxcms's parametric sRGB profile and a 1024-entry sampled-curve sRGB profile like the common HP one. Whole-image output is also within one code value of decode_srgb8.
  • Display P3, Adobe RGB and ProPhoto profiles are still rejected.
  • decode_rows_u8_bounded at 1, 5 and 16 rows per stripe matches the enumerated-sRGB image. This test fails without the stripe-sink change.
  • The crosstalk profile above is rejected. This test fails without the matrix check.

The three sRGB tests fail without the source change. cargo test -p j2k passes. A manual check against the macOS system profiles accepted sRGB Profile.icc and rejected Display P3, ITU-709, Generic RGB and AdobeRGB1998.

Notes

  • The profile is parsed and checked on every 8-bit decode call of an ICC-tagged image; the result isn't cached on the decoder.
  • The matrix and ramp limits are each bounded separately. A profile right at both limits could in theory be off by a little more than one code value; real sRGB profiles are far inside them.

A JP2 tagged with an IEC 61966-2-1 sRGB ICC profile failed region and
pow2-scaled region decodes to Rgb8/Rgba8 with "backend color space cannot
be mapped to requested 8-bit pixel format", leaving decode_srgb8, a
whole-image decode, as the only route.

Treat a three-channel ICC profile as RGB for 8-bit RGB output when it is a
restricted matrix/TRC RGB profile whose linear matrix to sRGB is within
2.5e-4 (off-diagonal row sums) and 1e-3 (diagonal) of identity, and whose
8-bit transform to sRGB moves no channel or neutral ramp sample by more
than one code value. The matrix is checked directly because the ramps
alone miss negative crosstalk, which clips on single-channel ramps and
cancels on neutrals but shifts mixed colours by tens of code values.

This applies to decode_region_into, decode_region_scaled_pow2_into, the
prepared region path, the whole-image decode_into fallback, and the
streamed stripes of decode_rows_u8_bounded. Other ICC
profiles and 16-bit outputs remain unsupported.

Fixes frames-sg#321
@jcoyne
jcoyne force-pushed the fix/srgb-icc-region-rgb8 branch from bfbee0d to 7d91a73 Compare November 5, 2026 18:27
@jcwal1516

Copy link
Copy Markdown
Member

lgtm, thanks for this!

@jcwal1516
jcwal1516 merged commit 17efbb5 into frames-sg:main Nov 6, 2026
30 of 32 checks passed
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.

Allow IEC 61966-2-1 sRGB profiles in RGB8 region decoding

2 participants