IEC 61966-2-1 vs C0-Continuous Constants
March 5, 2026 · View on GitHub
The problem
The sRGB transfer function is piecewise: a linear segment near black, a power curve everywhere else. The two pieces must meet at a threshold. IEC 61966-2-1 chose constants that almost meet — but not quite.
The IEC spec uses threshold 0.04045 with offset 0.055 and scale 1.055.
At the junction point, the linear segment evaluates to 0.003130804953560371
and the power segment evaluates to 0.003130807283067685. The gap is
~2.3e-9 in the linearization direction and ~3.0e-8 in the encoding
direction.
This is invisible at 8-bit precision (the gap is ~600x smaller than one u8 level), but it creates a genuine discontinuity in the transfer function. For high-precision workflows (16-bit, f32, or f64), the piecewise segments don't connect.
C0-continuous constants
The moxcms project derived alternative
constants that maintain the same 12.92 linear scale and 2.4 gamma exponent
but adjust the offset to make the piecewise function exactly C0-continuous:
| Constant | IEC 61966-2-1 | C0 (moxcms) |
|---|---|---|
| Offset (A) | 0.055 | 0.055010718947586600 |
| Scale (1+A) | 1.055 | 1.055010718947586600 |
| Gamma threshold | 0.04045 | 0.039293370676848 |
| Linear threshold | 0.003130804953560371 | 0.003041282560127521 |
With these constants, the gap at the junction drops from ~2.3e-9 to ~1.3e-18 (floating-point epsilon — effectively zero).
When does it matter?
At u8 precision: never. Both constant sets produce identical 8-bit LUT values for all 256 entries. The gap is invisible.
At u16 precision: barely. Near the threshold (around sRGB values 3200-3400 out of 65535), you might see a 1-level difference. In practice this is below the noise floor of any real sensor or display.
At f32/f64 precision: the discontinuity shows up in derivatives and in iterative algorithms that converge on the threshold region. If you're writing a color management engine, ICC profile processor, or gamut mapping algorithm that does repeated forward/inverse conversions near the junction, the C0 constants give cleaner behavior.
The gap across the threshold region
Both constant sets were evaluated at points near their respective thresholds.
The "gap" is power_segment(x) - linear_segment(x) — the signed difference
between what the power curve gives and what the linear segment gives at the
same input.
sRGB value IEC gap (power-linear) C0 gap (power-linear)
───────── ────────────────────── ─────────────────────
0.03800 +2.17e-07 +9.59e-07
0.03929 -7.55e-07 +1.30e-18 <── C0 threshold (zero gap)
0.04000 -4.76e-07 +2.87e-07
0.04045 +2.33e-09 +7.70e-07 <── IEC threshold
0.04080 +5.36e-07 +1.31e-06
0.04200 +3.44e-06 +4.22e-06
The IEC constants cross zero at 0.04045 (by construction), but the
transition isn't smooth — the gap changes sign abruptly. The C0 constants
cross zero at 0.03929, and the transition is smooth because the function
is actually continuous there.
What this crate does
By default, linear-srgb uses C0-continuous constants everywhere: the
precise powf-based functions, the default rational polynomial, the SIMD
paths, and all compile-time LUTs.
If you need exact IEC 61966-2-1 behavior (e.g., conformance testing against
the spec, or interop with software that implements the original constants),
enable the iec feature:
linear-srgb = { version = "0.6", features = ["iec"] }
Then use linear_srgb::iec::srgb_to_linear and linear_srgb::iec::linear_to_srgb.
References
- IEC 61966-2-1:1999, "Multimedia systems and equipment — Colour measurement and management"
- moxcms C0-continuous constants — derivation of the continuous constants
- CSS Color Level 4, Section 10.2 — also acknowledges the discontinuity