Skip to content

Fix conversion issues and enhance alpha preservation in color models - #77

Merged
merlotqi merged 5 commits into
mainfrom
fix/core-conversion-issues
Nov 5, 2026
Merged

merlotqi merged 5 commits into
mainfrom
fix/core-conversion-issues

Conversation

@merlotqi

@merlotqi merlotqi commented Nov 5, 2026

Copy link
Copy Markdown
Owner

No description provided.

merlotqi and others added 5 commits November 2, 2026 10:13
hsv_to_rgb and hwb_to_rgb were normalized through wrap_unit_hue in #66,
but hsl_to_rgb still fed the raw unit hue into hue2rgb, whose single ±1
adjustment cannot recover hues needing two wraps: hsl(780°) rendered
black instead of yellow, hsl(700°) rendered a wrong sector. Negative
and multi-wrap inputs now land on the [0,1) ring exactly like the other
two polar spaces, symmetric with #66.

Co-Authored-By: Claude Code <noreply@anthropic.com>
cone_scale_adapt hopped color -> xyz_t -> color_cast back; xyz_t has no
alpha, so the cast defaulted it to 1.0 and silently discarded the
caller's value (bradford_adapt(rgbaf(1,0,0,0.5)) returned alpha 1.0).
The transform now captures alpha via get_src_alpha before the hop and
restores it on the result for alpha-carrying models, covering all six
transforms that share the construction (Bradford, Von Kries, CAT02,
CAT16, CMCCAT2000, XYZ scaling). 3-channel inputs are unaffected.

Co-Authored-By: Claude Code <noreply@anthropic.com>
…#73)

The wide-gamut forward matrices were built from chromaticity-rounded
D65/D50 (column sums 0.950456/1.089058) while Lab's Xn/Yn/Zn are the
CIE tabulated whites (0.95047/1.08883), so white (1,1,1) in Display P3,
Rec.2020, ProPhoto and Adobe RGB landed at a*≈-0.002…-0.012,
b*≈0.008…0.014 instead of the achromatic origin.

Row-normalize each forward matrix so column sums equal the tabulated
white (D65 for P3/Rec2020/Adobe, D50 for ProPhoto), and re-derive every
inverse via Gauss-Jordan on the exact normalized matrix (same discipline
as #68) so pairs stay mutually inverse. Sync all copies: functions/{xyz,
rec2020,adobe_rgb,prophoto_rgb}.hpp, gamut/details.hpp inverses,
io/css/{color_function,relative_color}.hpp.

ProPhoto's D50 primaries now sum to WHITEPOINT_D50 exactly, so the
Bradford D50→D65 step maps white onto D65 and OkLab L = 1.0 (the old
1.0000031 above-white symptom is gone; above-one clipping stays covered
by SuperWhiteXyzLightnessAboveOne).

New ConversionWhitePointTest suite: white→Lab a*=b*=0 for all four
spaces plus Lab round-trip. sRGB matrices untouched (already exact).

ctest 1415/1415.

Co-Authored-By: Claude Code <noreply@anthropic.com>
#75)

std::pow/std::cbrt are not constexpr in C++17/20 (P0533R3 = C++26).
GCC folds them as builtins during constant evaluation; Clang/MSVC do not,
so constexpr color_cast through the Adobe gamma and CIELAB/CIELUV/OKLab
cbrt paths failed to compile there while CI stayed green.

- Add conversion/math.hpp: pure double-precision Newton shims (cbrt,
  Adobe linearize/encode) plus portable_* wrappers that branch on
  is_constant_evaluated() — runtime keeps std::pow/std::cbrt (unchanged),
  constant evaluation uses the shims (float-accurate via double internals).
- Wire call sites: adobe_rgb (linearize/encode), lab (2x cbrt),
  luv (cbrt), oklab (safe_cbrt).
- New test_constexpr.cpp: libm-equivalence sweeps (mutation anchors) plus
  unguarded static_asserts proving constant evaluation on any compiler
  with __builtin_is_constant_evaluated (GCC 9+/Clang 8+/MSVC 16.4+).
- README: document the constexpr portability guarantee.

ctest 1420/1420.

Co-Authored-By: Claude Code <noreply@anthropic.com>
 #76)

from_unit/from_value clamped every conversion hop to [0,1]/[min,max], so
rgbf(-0.1, 0.5, 1.2) lost its over/under range at the first hop to linear
RGB — scRGB/HDR intermediates were impossible and Rec.2020's sign-preserving
transfer curve never saw its input.

- Add traits::model_extended_range_v: per-model opt-in member
  (static constexpr bool extended_range = true) on model_traits, detected
  via void_t (same shape as model_has_alpha_v), default false.
- Opt in the linear RGB float families (sRGB/Adobe/P3/Rec.2020/ProPhoto,
  3- and 4-channel) and XYZ.
- from_unit/from_value skip the clamp only for float channels of
  extended-range models; integral channels and display-encoded models keep
  clamping at the encode boundary (issue expectation: "keep display-encoded
  spaces clamping as today").
- Nominal channel ranges stay [0,1]: widening them would re-anchor the
  to_unit/from_unit affine map away from the transfer functions'
  0=black/1=white contract (white would store as 2.0 under a [-1,2] range),
  breaking every raw-value consumer (blend, luminance, IO). Storage still
  accepts out-of-range floats; clamped()/make_checked() remain the
  opt-in boundary factories.
- graph DI comment updated for accuracy; README documents the story.

New tests/operations/test_extended_range.cpp: 9 tests — trait opt-in
asserts, the issue's rgb→linear→xyz→linear→rgb chain, Rec.2020 signed
curve, XYZ beyond [-1,2] round trip, display/integral clamp guards,
alpha policy. RED observed at both layers (missing trait, then clamped
hops); mutation: from_unit gate removed → 6 tests fail. ctest 1429/1429.

Co-Authored-By: Claude Code <noreply@anthropic.com>
@merlotqi
merlotqi merged commit f98103d into main Nov 5, 2026
14 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.

1 participant