Skip to main content

Crate linear_srgb

Crate linear_srgb 

Source
Expand description

Fast linear↔sRGB color space conversion.

This crate provides efficient conversion between linear light values and sRGB gamma-encoded values following the IEC 61966-2-1:1999 standard.

§Module Organization

  • default - Recommended API with optimal implementations for each use case
  • simd - SIMD-accelerated functions with full control over dispatch
  • scalar - Single-value conversion functions (f32/f64)
  • lut - Lookup table types for custom bit depths

§Quick Start

use linear_srgb::default::{srgb_to_linear, linear_to_srgb};

// Convert sRGB 0.5 to linear
let linear = srgb_to_linear(0.5);
assert!((linear - 0.214).abs() < 0.001);

// Convert back to sRGB
let srgb = linear_to_srgb(linear);
assert!((srgb - 0.5).abs() < 0.001);

§Batch Processing (SIMD)

For maximum throughput on slices:

use linear_srgb::default::{srgb_to_linear_slice, linear_to_srgb_slice};

let mut values = vec![0.5f32; 10000];
srgb_to_linear_slice(&mut values);  // SIMD-accelerated
linear_to_srgb_slice(&mut values);

§Custom Gamma

For non-sRGB gamma (pure power function without linear segment):

use linear_srgb::default::{gamma_to_linear, linear_to_gamma};

let linear = gamma_to_linear(0.5, 2.2);  // gamma 2.2
let encoded = linear_to_gamma(linear, 2.2);

§LUT-based Conversion

For batch processing with pre-computed lookup tables:

use linear_srgb::default::SrgbConverter;

let conv = SrgbConverter::new();  // Zero-cost, const tables

// Fast 8-bit conversions
let linear = conv.srgb_u8_to_linear(128);
let srgb = conv.linear_to_srgb_u8(linear);

§Choosing the Right API

Use CaseRecommended Function
Single f32 valuedefault::srgb_to_linear
Single u8 valuedefault::srgb_u8_to_linear
f32 slice (in-place)default::srgb_to_linear_slice
u8 slice → f32 slicedefault::srgb_u8_to_linear_slice
Manual SIMD (8 values)default::srgb_to_linear_x8
Inside #[magetypes]default::inline::srgb_to_linear_x8
Inside #[arcane] (token)[rites::x8::srgb_to_linear_v3]
Custom bit depth LUTlut::LinearTable16

§Clamping and Extended Range

The f32↔f32 conversion functions come in two flavors: clamped (default) and extended (unclamped). Integer paths (u8, u16) always clamp since out-of-range values can’t be represented in the output format.

§Clamped (default) — use for same-gamut pipelines

All functions except the _extended variants clamp inputs to [0, 1]: negatives become 0, values above 1 become 1.

This is correct whenever the source and destination share the same color space (gamut + transfer function). The typical pipeline:

  1. Decode sRGB image (u8 → linear f32 via LUT, or f32 via TRC)
  2. Process in linear light (resize, blur, blend, composite)
  3. Re-encode to sRGB (linear f32 → sRGB f32 or u8)

In this pipeline, out-of-range values only come from processing artifacts: resize filters with negative lobes (Lanczos, Mitchell, etc.) produce small negatives near dark edges and values slightly above 1.0 near bright edges. These are ringing artifacts, not real colors — clamping is correct.

Float decoders like jpegli can also produce small out-of-range values from YCbCr quantization noise. When the image is sRGB, these are compression artifacts and clamping is correct — gives the same result as decoding to u8 first.

§Extended (unclamped) — use for cross-gamut pipelines

scalar::srgb_to_linear_extended and scalar::linear_to_srgb_extended do not clamp. They follow the mathematical sRGB transfer function for all inputs: negatives pass through the linear segment, values above 1.0 pass through the power segment.

Use these when the sRGB transfer function is applied to values from a different, wider gamut. A 3×3 matrix converting Rec. 2020 linear or Display P3 linear to sRGB linear can produce values well outside [0, 1]: a saturated Rec. 2020 green maps to deeply negative sRGB red and blue. These are real out-of-gamut colors, not artifacts — clamping destroys information that downstream gamut mapping or compositing may need.

This matters in practice: JPEG and JPEG XL images can carry Rec. 2020 or Display P3 ICC profiles. Phones shoot Rec. 2020 HLG, cameras embed wide-gamut profiles. Decoding such an image and converting to sRGB for display produces out-of-gamut values that should survive until final output.

If a float decoder (jpegli, libjxl) outputs wide-gamut data directly to f32, the output contains both small compression artifacts and real out-of-gamut values. The artifacts are tiny; the gamut excursions dominate. Using _extended preserves both — the artifacts are harmless noise that vanishes at quantization.

The _extended variants also cover scRGB (float sRGB with values outside [0, 1] for HDR and wide color) and any pipeline where intermediate f32 values are not yet at the final output stage.

§Summary

FunctionRangePipeline
All simd::*, mage::*, rites::*, lut::*[0, 1]Same-gamut batch processing
scalar::srgb_to_linear[0, 1]Same-gamut single values
scalar::linear_to_srgb[0, 1]Same-gamut single values
scalar::srgb_to_linear_extendedUnboundedCross-gamut, scRGB, HDR
scalar::linear_to_srgb_extendedUnboundedCross-gamut, scRGB, HDR
All u8/u16 paths[0, 1]Final quantization (clamp inherent)

No SIMD extended-range variants exist yet. The fast polynomial approximation is fitted to [0, 1] and produces garbage outside that domain. Extended-range SIMD would use pow instead of the polynomial (~3× slower, still faster than scalar for linear_to_srgb). For batch extended-range conversion today, loop over the scalar _extended functions.

§Feature Flags

  • std (default): Enable std library support
  • unsafe_simd: Enable unsafe optimizations for maximum performance

§no_std Support

This crate is no_std compatible. Disable the std feature:

linear-srgb = { version = "0.2", default-features = false }

Modules§

default
Recommended API with optimal implementations for each use case.
lut
Lookup table types for sRGB conversion.
scalar
Scalar (single-value) conversion functions.
simd
SIMD-accelerated conversion functions.