Skip to main content

Crate xgpui_color

Crate xgpui_color 

Source
Expand description

CPU-only, unclipped color conversions. Named RGB spaces and XyzD65 use D65. Custom D50 colorants can be explicitly adapted to D65. Matrices are row-major and multiply column vectors. Alpha is not involved.

§xgpui-color

Color science extension for xgpui, also usable independently. Convert RGB spaces, map colors into gamut, sample gradients and reuse the same color math in WGSL. The runtime uses only Rust’s standard library: no GPUI, window system, GPU or shader compiler is required.

§Dependency

[dependencies]
xgpui-color = "0.2.1"

Rust imports use xgpui_color.

§Convert a color

use xgpui_color::{EncodedRgb, RgbSpace};

let encoded = EncodedRgb::new(RgbSpace::Srgb, [0.2, 0.5, 0.8])?;
let p3 = encoded.decode()?.convert(RgbSpace::DisplayP3)?;
let perceptual = p3.to_oklab()?.to_oklch()?;
TypeRepresents
EncodedRgb, LinearRgbEncoded or linear RGB with explicit primaries.
RgbSpacesRGB, Display P3, Rec.2020 or Adobe RGB (1998).
XyzD65, Oklab, OklchIntermediate and perceptual color spaces.
LinearRgbaLinear color with validated straight alpha.

CPU calculations use f64, D65 and finite extended RGB/XYZ values. Conversion does not automatically clip or map gamut. Rec.2020 uses its SDR transfer function. Adobe RGB (1998) uses the exact 563/256 decoding exponent and 256/563 encoding exponent, including sign-preserving extended channels; it does not use the sRGB transfer curve. PQ, HLG and white-point adaptation are outside this API.

§Adobe RGB (1998)

use xgpui_color::{EncodedRgb, GamutMapping, RgbSpace};

let adobe = EncodedRgb::new(RgbSpace::AdobeRgb1998, [0., 1., 1.])?.decode()?;
let extended_p3 = adobe.convert(RgbSpace::DisplayP3)?;
assert!(extended_p3.components()[0] < 0.); // This cyan is outside P3.
let display_p3 = adobe.map_gamut(RgbSpace::DisplayP3, GamutMapping::ReduceChroma)?;

Conversion and gradients retain extended values until explicit mapping. Adobe RGB and P3 do not fully contain one another. Native image/shader producers still use bounded linear P3; this math support does not widen those storage or output contracts. See the media supplied-pixel API for explicit mapping into P3.

§Gamut mapping and gradients

LinearRgb::map_gamut explicitly chooses clipping or chroma reduction. LinearGradient samples two stops in linear RGB, OkLab or OkLCh, with explicit hue interpolation. Map the sampled color before associating alpha for output.

use xgpui_color::{
    ColorInterpolation, GamutMapping, LinearGradient, LinearRgb, LinearRgba, RgbSpace,
};

let red = LinearRgba::new(LinearRgb::new(RgbSpace::DisplayP3, [1., 0., 0.])?, 1.)?;
let blue = LinearRgba::new(LinearRgb::new(RgbSpace::DisplayP3, [0., 0., 1.])?, 0.5)?;
let gradient = LinearGradient::new(
    [0., 0.], [320., 0.], [red, blue], RgbSpace::DisplayP3,
    ColorInterpolation::Oklab,
)?;
let midpoint = gradient.sample([160., 0.])?;
let output = midpoint
    .map_gamut(RgbSpace::DisplayP3, GamutMapping::ReduceChroma)?
    .premultiplied();

This is a deterministic mapping policy, not ICC relative colorimetric or CSS local MINDE. Invalid or nonfinite values return ColorError.

§WGSL helpers

WGSL_COLOR_MATH contains resource-free WGSL helpers with the gamut_ prefix. Prepend it once to your shader source; it declares no entry point or bindings. The GPU contract uses finite f32 inputs and validated space/policy IDs. Space IDs are stable: 0 = sRGB, 1 = Display P3, 2 = Rec.2020, 3 = Adobe RGB (1998). Mapping IDs are 0 = clip and 1 = fixed-lightness/hue chroma reduction.

These functions do not change a renderer’s output color space. Use the explicit linear-P3 contract when integrating wide output with xgpui-shader or xgpui-gamut.

§Documentation and validation

The color guide covers types, dependency boundaries and the color/gamut lab. Refer to the source for the public API and numerical contracts.

Conversions follow CSS Color 4 reference conversions and Björn Ottosson’s OkLab definition. Tests cover CPU round trips, Naga WGSL validation/translation and native Metal comparisons. Shader translation alone does not verify native D3D11 execution. Metal and Naga are development dependencies only.

§Adobe RGB numerical regression criteria

Committed oracles use 120-digit Python Decimal arithmetic on exact binary inputs. Primary matrices are independently solved from xy chromaticities and D65 using pivoted Gaussian elimination. Transfer oracles use exact rational exponents; they are independent of the Rust and WGSL implementation constants.

  • CPU tests cover all 65,536 unsigned 16-bit transfer codes, signed zero, subnormals, underflow, overflow rejection, adjacent floats at gamut boundaries, and 21,296 scaled primary-conversion vectors.
  • Scalar error budgets account for binary64 rounding of the reciprocal exponent, powf evaluation and underflow. Matrix budgets scale with the matrix norm and input magnitude, including cancellation.
  • Native Metal checks 1,060 independent transfer cases per compiler mode. Acceptance is relative: eight binary32 epsilons (0.96 ppm) with fast math off, 64 epsilons (7.63 ppm) with fast math on, plus an eight-subnormal rounding floor. Transfer endpoints must be exact. All nonzero GPU reference results are normal binary32 values; GPU subnormal behavior is outside these vectors.
  • Media tests check exact binary16 patterns near black and at midtones, including intentional storage underflow and fractional alpha. Adobe primary/mip checks permit at most one binary16 representable step.

Reproduce both CPU/GPU and media FP16 oracle fixtures from the repository root:

python3 scripts/generate_adobe_rgb_references.py --check

The generator requires Python 3.11+ and rustfmt. Runtime evidence is tracked per backend; numerical test thresholds are acceptance criteria for the covered domains rather than a cross-device bit-identity promise.

§Project

Repository · Release preparation · Website. Licensed under Apache-2.0.

This extension uses its own 0.2.0 version, separate from xgpui framework 1.0.0 and the upstream GPUI baseline.

§Custom RGB metadata

RgbDefinition::new_d65(matrix, curves) validates a row-major linear RGB → relative XYZ/D65 matrix and three independent RgbCurve values (Linear, Srgb, or positive Gamma). decode_to_xyz_d65 retains finite extended RGB and rejects conversion overflow. Matrices must be nonsingular, reasonably conditioned and normalized to D65 white (Y=1); white admission permits at most 3/65536 absolute error per XYZ channel for quantized colorants. This metadata admission bound is not the numerical accuracy promised by conversions.

new_d65 accepts already-adapted colorants. Alternatively, RgbDefinition::new_with_white_point(matrix, curves, WhitePoint::D50) explicitly Bradford-adapts D50 colorants to D65 once. The matrix must sum to the declared white within the colorant quantization bound. That bound is propagated through the adaptation matrix; colorants are never silently renormalized.

BradfordAdaptation::new(source, destination) also adapts relative XYZ or colorant matrices directly between WhitePoint::D50 and WhitePoint::D65. Same-white adaptation preserves finite input exactly, including signed zero. Other routes retain finite extended coordinates and reject overflow. The white chromaticities and Bradford convention follow CSS Color 4: D50 xy=(0.3457,0.3585), D65 xy=(0.3127,0.3290), both normalized to Y=1.

This is not ICC rendering-intent handling. A future adapter must resolve profile PCS normalization, encoded illuminant values and existing adaptation tags before using this API; a profile’s media-white label alone is insufficient. These two reference whites do not represent every custom or encoded ICC white. Sampled and parametric curves and optional ICC parsing are described below. Alpha and file decoding stay outside this color API.

§Sampled and parametric decoding

SampledCurve::new(Vec<u16>) accepts 2–65,536 nondecreasing samples. Decoding interpolates continuously in f64 between normalized entries; it does not round input to an integer lookup index. Tables are immutable shared allocations.

ParametricCurve::new([g,a,b,c,d,e,f]) evaluates (a*x+b)^g+e for x>=d, or c*x+f below it. It validates finite coefficients, positive exponent/slope, nonnegative lower slope, power domain and finite endpoints. Quantized branch locations and discontinuities are retained. Output is bounded to 0–1; nonfinite arithmetic is rejected before clipping. It never substitutes an approximate named curve. These two curve types require bounded 0–1 inputs; existing named linear/sRGB/gamma curves retain their signed-extension behavior.

RgbCurve and RgbDefinition are now Clone, with borrowed evaluation/accessors. Sample tables remain shared when metadata is cloned.

§Optional ICC parsing adapter

Enable xgpui-color’s icc feature to use icc::IccRgbProfile. Default builds still have no runtime dependencies. The optional moxcms dependency only parses; all curve evaluation, adaptation and conversion use this crate’s f64 core.

ⓘ
use xgpui_color::icc::IccRgbProfile;

// Profile bytes are extracted by the application; parse on its admitted worker.
let profile = IccRgbProfile::parse(&profile_bytes)?;
let rgb = profile.into_rgb_definition();
// Supply rgb as custom metadata alongside decoded pixels to xgpui-media.

The initial subset is deliberately strict: ICC v2/v4 RGB input/display profiles, XYZ PCS with the standard encoded D50 illuminant, matrix colorants, monotone u16 curves (including linear/gamma encodings), and parametric function types 0, 3 and 4. Parametric coefficients must have magnitude below 128 so their s15Fixed16 values remain exact through the parser’s f32 representation. The adapter rejects LUTs, CMYK/gray, device links/output profiles, CICP, unrecognized tags and unsupported curve semantics. Informational description/copyright/device text and chromaticity tags are permitted. This is not general ICC support.

Admission caps: 512 KiB profile bytes, 32 tags, 65,536 entries per curve and 4 KiB per informational tag. Preflight checks header size, tag bounds, duplicates, partial overlap and curve counts before calling the parser. Exact shared tag ranges are permitted. The dependency’s own parser limits are also configured. Parsing is synchronous and bounded, with no implicit thread creation; applications own worker admission and missing-profile policy. This is not a fuzzing/security certification.

Matrix tags already describe PCS colorants. The adapter uses the encoded header PCS white, adapts once to D65 and does not reapply chad or substitute media white. It provides no rendering-intent selection, black-point compensation, profile-name inference or file decoding. decode_to_pcs exposes the original relative XYZ PCS for validation/integration; rgb_definition supplies the adapted custom definition used by media ingestion.

Structs§

BradfordAdaptation
Linear chromatic adaptation only. No clipping, transfer curves or alpha. D50/D65 agree with CSS Color 4’s Bradford conversion convention.
ColorError
Invalid input or a conversion whose result cannot be represented as finite f64.
EncodedRgb
Finite encoded RGB, possibly outside [0,1]; no implicit gamut mapping.
LinearGradient
Two-stop gradient reference. Coordinates are in caller-defined common units. Stops are fixed at 0 and 1; values beyond them extend the endpoint colors. Coincident endpoints select the second stop. No gamut mapping is implicit.
LinearRgb
Finite linear RGB, possibly outside [0,1]; no implicit gamut mapping.
LinearRgba
Straight-alpha CPU color; RGB may be extended, alpha must be in [0,1].
Oklab
OkLab [L, a, b], with nominal SDR L in 0..1; extended coordinates are retained.
Oklch
OkLCh: lightness, nonnegative chroma, hue in degrees normalized to [0,360). Exact zero chroma has canonical hue zero. Near-neutral hues are retained.
ParametricCurve
Bounded piecewise curve: (ax+b)^g+e for x>=d, cx+f otherwise. Coefficients are [g,a,b,c,d,e,f]. Serialized discontinuities are retained; no substitution of ideal named curves. Finite output is clipped to 0..1.
RgbDefinition
Validated custom RGB definition with relative XYZ/D65 colorants. Matrix rows multiply column RGB vectors. The XYZ reference white has Y=1. A profile matrix may encode a neutral ramp that differs from that reference. Use new_with_white_point to explicitly adapt D50 colorants; new_d65 accepts already-adapted colorants. Adaptation must not be applied twice. This is neither an ICC profile nor a display calibration descriptor.
SampledCurve
Owned monotone normalized u16 samples, interpolated continuously in f64.
XyzD65
Relative XYZ with D65 white; components are unclipped.

Enums§

ColorInterpolation
GamutMapping
Explicit policies for producing bounded SDR RGB. Neither is an ICC intent.
HueInterpolation
RgbCurve
Independent channel decoding curve. Named curves preserve signed extensions; sampled and parametric curves explicitly use bounded input/output 0..1.
RgbSpace
SDR RGB primaries and transfer function. Display P3 uses the sRGB transfer.
WhitePoint
White of the supplied XYZ/colorant matrix, not a monitor mode or ICC label. D50 uses xy=(0.3457,0.3585); D65 uses xy=(0.3127,0.3290).

Constants§

WGSL_COLOR_MATH
Resource-free WGSL helpers; see the crate guide for IDs and input contracts.