Fast linear↔sRGB color space conversion with runtime CPU dispatch.
use linear_srgb::default::*;
// Single values
let linear = srgb_to_linear(0.5f32);
let srgb = linear_to_srgb(linear);
// Fast polynomial (~4x faster than powf, 294 ULP max near black, <4 ULP in upper half)
let linear = srgb_to_linear_fast(0.5f32);
let srgb = linear_to_srgb_fast(linear);
// Slices (SIMD-accelerated, polynomial)
let mut values = vec![0.5f32; 10000];
srgb_to_linear_slice(&mut values);
linear_to_srgb_slice(&mut values);
// u8 ↔ f32 (image processing)
let linear = srgb_u8_to_linear(128);
let srgb_byte = linear_to_srgb_u8(linear);| Your situation | Use this |
|---|---|
| One f32 value (exact) | srgb_to_linear(x) / linear_to_srgb(x) |
| One f32 value (fast) | srgb_to_linear_fast(x) / linear_to_srgb_fast(x) |
| One u8 value | srgb_u8_to_linear(x) (LUT, fastest) |
&mut [f32] slice |
srgb_to_linear_slice() / linear_to_srgb_slice() |
&[u8] → &mut [f32] |
srgb_u8_to_linear_slice() |
&[f32] → &mut [u8] |
linear_to_srgb_u8_slice() |
Inside #[arcane] |
default::inline::* (no dispatch) |
| Standalone x8 call | srgb_to_linear_x8() (has dispatch, that's fine) |
use linear_srgb::default::*;
// f32 conversions — powf (exact reference)
let linear = srgb_to_linear(0.5f32);
let srgb = linear_to_srgb(0.214f32);
// f32 conversions — polynomial (~4x faster, 294 ULP max near black, <4 ULP in upper half)
let linear = srgb_to_linear_fast(0.5f32);
let srgb = linear_to_srgb_fast(0.214f32);
// f64 high-precision
let linear = srgb_to_linear_f64(0.5f64);
// u8 conversions (LUT-based)
let linear = srgb_u8_to_linear(128u8); // u8 → f32
let srgb_byte = linear_to_srgb_u8(0.214f32); // f32 → u8
// u16 conversions (LUT-based)
let linear = srgb_u16_to_linear(32768u16); // u16 → f32
let srgb_u16 = linear_to_srgb_u16(0.214f32); // f32 → u16use linear_srgb::default::*;
// In-place f32 conversion (SIMD-accelerated)
let mut values = vec![0.5f32; 10000];
srgb_to_linear_slice(&mut values); // Modifies in-place
linear_to_srgb_slice(&mut values);
// u8 → f32 (LUT-based, extremely fast)
let srgb_bytes: Vec<u8> = (0..=255).collect();
let mut linear = vec![0.0f32; 256];
srgb_u8_to_linear_slice(&srgb_bytes, &mut linear);
// f32 → u8 (SIMD-accelerated)
let linear_values: Vec<f32> = (0..256).map(|i| i as f32 / 255.0).collect();
let mut srgb_bytes = vec![0u8; 256];
linear_to_srgb_u8_slice(&linear_values, &mut srgb_bytes);For pure power-law gamma without the sRGB linear segment:
use linear_srgb::default::*;
// gamma 2.2 (common in legacy workflows)
let linear = gamma_to_linear(0.5f32, 2.2);
let encoded = linear_to_gamma(linear, 2.2);
// Also available for slices
let mut values = vec![0.5f32; 1000];
gamma_to_linear_slice(&mut values, 2.2);The standard functions clamp to [0, 1]. For cross-gamut pipelines (Rec. 2020 → sRGB, scRGB, HDR):
use linear_srgb::scalar::{srgb_to_linear_extended, linear_to_srgb_extended};
let linear = srgb_to_linear_extended(-0.1); // Preserves negatives
let srgb = linear_to_srgb_extended(1.5); // Preserves >1.0See crate docs for when clamped vs extended is appropriate.
use linear_srgb::lut::{LinearTable16, EncodingTable16, lut_interp_linear_float};
// 16-bit linearization (65536 entries)
let lut = LinearTable16::new();
let linear = lut.lookup(32768); // Direct lookup
// Interpolated encoding
let encode_lut = EncodingTable16::new();
let srgb = lut_interp_linear_float(0.5, encode_lut.as_slice());For zero-overhead SIMD when you control the dispatch point:
use linear_srgb::mage;
// Obtain a token once, pass to all calls
mage::srgb_to_linear_slice(&mut values); // Uses archmage incant! internallyFor embedding inside your own #[arcane] code with no dispatch overhead:
use linear_srgb::rites::x8;
use archmage::arcane;
#[arcane]
fn my_pipeline(token: Desktop64, data: &mut [f32]) {
// x8::srgb_to_linear_v3 is #[rite] — inlines into your function
// Available widths: x4 (NEON/WASM), x8 (AVX2), x16 (AVX-512)
}default— Recommended API. Re-exports optimal implementations.default::inline— Dispatch-freewide::f32x8variants for use inside your own SIMD code.simd— Full SIMD API with_dispatchand_inlinevariants.scalar— Single-value functions. Includes_fast(polynomial) and_extended(unclamped) variants.lut— Lookup tables for custom bit depths.mage— Token-based dispatch via archmage (feature-gated).rites— Inlineable#[rite]functions for x4/x8/x16 widths (feature-gated).
[dependencies]
linear-srgb = "0.5" # std enabled by default
# no_std (requires alloc for LUT generation)
linear-srgb = { version = "0.5", default-features = false }
# Token-based dispatch (zero overhead)
linear-srgb = { version = "0.5", features = ["mage"] }
# Inlineable rites for embedding in #[arcane] code
linear-srgb = { version = "0.5", features = ["rites"] }std(default): Required for runtime SIMD dispatchmage: Token-based API using archmagerites: Inlineable#[rite]functions for x4/x8/x16alt: Alternative/experimental implementations for benchmarkingunsafe_simd: Union-based bit manipulation, unchecked indexing
Implements IEC 61966-2-1:1999 sRGB transfer functions with:
- C0-continuous piecewise function (no discontinuity at threshold)
- Constants derived from moxcms reference implementation
- Scalar
powf: exact to f32/f64 precision - Polynomial (
_fast, SIMD): 294 ULP max near threshold, 2-3 ULP in upper half (exhaustive f32 sweep) - f32 roundtrip: ~1e-5 accuracy
- f64 roundtrip: ~1e-10 accuracy
MIT OR Apache-2.0
Developed with Claude (Anthropic). All code has been reviewed and benchmarked, but verify critical paths for your use case.