linear-srgb
Fast linear↔sRGB color space conversion with runtime CPU dispatch.
Quick Start
use *;
// Single values (rational polynomial — fast, <8 ULP typical)
let linear = srgb_to_linear;
let srgb = linear_to_srgb;
// Slices (SIMD-accelerated)
let mut values = vec!;
srgb_to_linear_slice;
linear_to_srgb_slice;
// u8 ↔ f32 (image processing)
let linear = srgb_u8_to_linear;
let srgb_byte = linear_to_srgb_u8;
Which Function Should I Use?
| Your situation | Use this |
|---|---|
| One f32 value (fast) | default::srgb_to_linear(x) / default::linear_to_srgb(x) |
| One f32 value (exact) | precise::srgb_to_linear(x) / precise::linear_to_srgb(x) |
| One u8 value | default::srgb_u8_to_linear(x) (LUT, fastest) |
&mut [f32] slice |
default::srgb_to_linear_slice() / default::linear_to_srgb_slice() |
RGBA &mut [f32] (keep alpha) |
default::srgb_to_linear_rgba_slice() / default::linear_to_srgb_rgba_slice() |
&[u8] → &mut [f32] |
default::srgb_u8_to_linear_slice() |
RGBA &[u8] → &mut [f32] |
default::srgb_u8_to_linear_rgba_slice() / linear_to_srgb_u8_rgba_slice() |
&[u16] ↔ &mut [f32] |
default::srgb_u16_to_linear_slice() / default::linear_to_srgb_u16_slice() |
&[f32] → &mut [u8] |
default::linear_to_srgb_u8_slice() |
Inside #[arcane] fn |
tokens::x8::srgb_to_linear_v3() (inlines, no dispatch) |
API Reference
Single Values
use *;
// f32 conversions — rational polynomial (~110 ULP max near threshold, <8 ULP elsewhere)
let linear = srgb_to_linear;
let srgb = linear_to_srgb;
// u8 conversions (LUT-based, zero math)
let linear = srgb_u8_to_linear;
let srgb_byte = linear_to_srgb_u8;
// u16 conversions (LUT-based)
let linear = srgb_u16_to_linear;
let srgb_u16 = linear_to_srgb_u16;
Precise (powf) Conversions
Uses C0-continuous constants that eliminate the IEC spec's piecewise discontinuity. See the Accuracy section for details on how these differ from the IEC textbook values.
use *;
// f32 — exact powf, C0-continuous (6 ULP max)
let linear = srgb_to_linear;
let srgb = linear_to_srgb;
// f64 high-precision
let linear = srgb_to_linear_f64;
// Extended range (HDR/ICC — no clamping)
use ;
let linear = srgb_to_linear_extended;
let srgb = linear_to_srgb_extended;
Slice Processing (Recommended for Batches)
use *;
// In-place f32 conversion (SIMD-accelerated)
let mut values = vec!;
srgb_to_linear_slice;
linear_to_srgb_slice;
// RGBA slices — alpha channel is preserved, only RGB converted
let mut rgba = vec!;
srgb_to_linear_rgba_slice;
assert_eq!; // alpha untouched
// u8 → f32 (LUT-based, extremely fast)
let srgb_bytes: = .collect;
let mut linear = vec!;
srgb_u8_to_linear_slice;
// RGBA u8 → f32 (alpha passed through as a/255, not sRGB-decoded)
let rgba_bytes = vec!;
let mut rgba_linear = vec!;
srgb_u8_to_linear_rgba_slice;
// f32 → u8 (SIMD-accelerated)
let linear_values: = .map.collect;
let mut srgb_bytes = vec!;
linear_to_srgb_u8_slice;
Custom Gamma (Non-sRGB)
For pure power-law gamma without the sRGB linear segment:
use *;
// gamma 2.2 (common in legacy workflows)
let linear = gamma_to_linear;
let encoded = linear_to_gamma;
// Also available for slices
let mut values = vec!;
gamma_to_linear_slice;
LUT for Custom Bit Depths
use ;
// 16-bit linearization (65536 entries)
let lut = new;
let linear = lut.lookup;
// Interpolated encoding
let encode_lut = new;
let srgb = lut_interp_linear_float;
Advanced: Token-Based #[rite] Functions
For zero-overhead SIMD when embedding inside your own #[arcane] code:
use x8;
use arcane;
Module Organization
default— Recommended API. Rational polynomial for f32, LUT for integers, SIMD for slices.precise— Exactpowf()conversions with C0-continuous constants (not IEC textbook). f32/f64, extended range.tokens— Inlineable#[rite]functions for x4/x8/x16 widths. For use inside#[arcane]code.lut— Lookup tables for custom bit depths.tf— Transfer functions: BT.709, PQ, HLG (feature-gated behindtransfer).
Feature Flags
[]
= "0.6" # std enabled by default
# no_std (requires alloc for LUT generation)
= { = "0.6", = false }
# HDR transfer functions (BT.709, PQ, HLG)
= { = "0.6", = ["transfer"] }
std(default): Required for runtime SIMD dispatchtransfer: BT.709, PQ, HLG transfer functionsalt: Alternative/experimental implementations for benchmarking
Accuracy
Transfer function constants
The IEC 61966-2-1 sRGB spec defines a piecewise transfer function with a linear segment and a power curve. The textbook constants (threshold 0.04045 / 0.0031308, offset 0.055) create a tiny discontinuity at the boundary — the two segments don't quite meet (~2.3e-9 in f64).
This crate uses two constant sets, each chosen for correctness in its context:
| Code path | Constants | Threshold (gamma) | Why |
|---|---|---|---|
default (rational poly) |
IEC textbook | 0.04045 | Polynomial was fitted to the IEC power curve |
precise (powf) |
moxcms C0 | 0.039293... | Eliminates the discontinuity |
SIMD / tokens |
IEC textbook | 0.04045 | Same rational polynomial as default |
| LUT tables | IEC textbook | 0.04045 | Identical to moxcms at u8/u16 precision |
The rational polynomial (from libjxl) approximates ((x+0.055)/1.055)^2.4 — the IEC power segment. Using the IEC threshold gives 110 ULP max error. Switching to the moxcms threshold would push values into the linear segment that should be evaluated by the polynomial, causing 3100+ ULP errors. The IEC threshold is optimal for this approximation.
The precise path uses moxcms C0-continuous constants (derived from the moxcms reference implementation) because they make the piecewise function mathematically continuous. With powf() computing the exact power curve, this distinction actually matters.
Accuracy summary (exhaustive f32 sweep)
| Path | Reference | Max error | Avg error |
|---|---|---|---|
default s→l |
IEC f64 | 110 ULP | 0.55 ULP |
default l→s |
IEC f64 | 31 ULP | 0.37 ULP |
precise s→l |
moxcms f64 | 6 ULP | 0.11 ULP |
precise l→s |
moxcms f64 | 3 ULP | 0.10 ULP |
The default path's worst case (110 ULP for s→l) occurs at the piecewise threshold where the linear segment meets the rational polynomial. Away from the threshold, typical error is <8 ULP.
Practical impact
The two constant sets produce identical results at u8 precision (the threshold falls between u8 values 10 and 11). At u16 precision, the maximum difference is ~1 LSB near the threshold. The difference only becomes measurable with raw f32 values in the narrow threshold region (0.039–0.041 gamma-space).
License
MIT OR Apache-2.0
AI-Generated Code Notice
Developed with Claude (Anthropic). All code has been reviewed and benchmarked, but verify critical paths for your use case.