linear_srgb/
lib.rs

1//! Fast linear↔sRGB color space conversion.
2//!
3//! This crate is `no_std` compatible by default. Enable the `std` feature
4//! if you need std library support.
5//!
6//! This crate provides efficient conversion between linear light values and
7//! sRGB gamma-encoded values following the IEC 61966-2-1:1999 standard.
8//!
9//! # Features
10//!
11//! - **Direct computation**: Single value conversion with piecewise functions
12//! - **FMA acceleration**: Uses hardware FMA when available (x86 FMA, ARM64 NEON)
13//! - **LUT-based conversion**: Pre-computed tables for batch processing
14//! - **Multiple precisions**: f32, f64, and u8/u16 conversions
15//!
16//! # Quick Start
17//!
18//! ```rust
19//! use linear_srgb::{srgb_to_linear, linear_to_srgb};
20//!
21//! // Convert sRGB 0.5 to linear
22//! let linear = srgb_to_linear(0.5);
23//! assert!((linear - 0.214).abs() < 0.001);
24//!
25//! // Convert back to sRGB
26//! let srgb = linear_to_srgb(linear);
27//! assert!((srgb - 0.5).abs() < 0.001);
28//! ```
29//!
30//! # LUT-based Conversion
31//!
32//! For batch processing, use `SrgbConverter` which pre-computes lookup tables:
33//!
34//! ```rust
35//! use linear_srgb::SrgbConverter;
36//!
37//! let conv = SrgbConverter::new();
38//!
39//! // Fast 8-bit conversions
40//! let linear = conv.srgb_u8_to_linear(128);
41//! let srgb = conv.linear_to_srgb_u8(linear);
42//! ```
43//!
44//! # Performance
45//!
46//! The implementation uses several optimizations:
47//! - Piecewise functions avoid `pow()` for ~1.2% of values in the linear segment
48//! - Early exit for out-of-range values avoids expensive transcendentals
49//! - FMA instructions combine multiply+add into single-cycle operations
50//! - Pre-computed LUTs trade memory for compute time
51//!
52//! # Feature Flags
53//!
54//! - `fast-math`: Use faster but slightly less accurate pow approximation for
55//!   extended range conversions (affects `linear_to_srgb_extended` only)
56//!
57//! # SIMD Acceleration
58//!
59//! For maximum throughput on large batches, use the `simd` module:
60//!
61//! ```rust
62//! use linear_srgb::simd;
63//!
64//! let mut values = vec![0.5f32; 10000];
65//! simd::srgb_to_linear_slice(&mut values);
66//! ```
67
68#![cfg_attr(not(feature = "std"), no_std)]
69#![cfg_attr(not(feature = "unsafe_simd"), deny(unsafe_code))]
70#![warn(missing_docs)]
71
72#[cfg(not(feature = "std"))]
73extern crate alloc;
74
75#[cfg(all(test, not(feature = "std")))]
76extern crate std;
77
78pub mod lut;
79mod mlaf;
80pub mod simd;
81mod targets;
82pub mod transfer;
83
84// Internal fast math for SIMD (not public API)
85pub(crate) mod fast_math;
86
87// Pre-computed const lookup tables (embedded in binary)
88mod const_luts;
89
90// Alternative/experimental implementations (for benchmarking)
91#[cfg(feature = "alt")]
92pub mod alt;
93
94// Re-export main types and functions
95pub use lut::{
96    EncodeTable8, EncodeTable12, EncodeTable16, EncodingTable, LinearTable8, LinearTable10,
97    LinearTable12, LinearTable16, LinearizationTable, SrgbConverter, lut_interp_linear_float,
98    lut_interp_linear_u16,
99};
100
101pub use transfer::{
102    linear_to_srgb, linear_to_srgb_extended, linear_to_srgb_f64, linear_to_srgb_u8, srgb_to_linear,
103    srgb_to_linear_extended, srgb_to_linear_f64, srgb_u8_to_linear,
104};
105
106/// Convert a slice of sRGB f32 values to linear in-place.
107#[inline]
108pub fn srgb_to_linear_slice(values: &mut [f32]) {
109    for v in values.iter_mut() {
110        *v = srgb_to_linear(*v);
111    }
112}
113
114/// Convert a slice of linear f32 values to sRGB in-place.
115#[inline]
116pub fn linear_to_srgb_slice(values: &mut [f32]) {
117    for v in values.iter_mut() {
118        *v = linear_to_srgb(*v);
119    }
120}
121
122#[cfg(test)]
123mod tests {
124    use super::*;
125
126    #[cfg(not(feature = "std"))]
127    use alloc::vec::Vec;
128
129    #[test]
130    fn test_api_consistency() {
131        // Ensure direct and LUT-based conversions are consistent
132        let conv = SrgbConverter::new();
133
134        for i in 0..=255u8 {
135            let direct = srgb_u8_to_linear(i);
136            let lut = conv.srgb_u8_to_linear(i);
137            assert!(
138                (direct - lut).abs() < 1e-5,
139                "Mismatch at {}: direct={}, lut={}",
140                i,
141                direct,
142                lut
143            );
144        }
145    }
146
147    #[test]
148    fn test_slice_conversion() {
149        let mut values: Vec<f32> = (0..=10).map(|i| i as f32 / 10.0).collect();
150        let original = values.clone();
151
152        srgb_to_linear_slice(&mut values);
153        linear_to_srgb_slice(&mut values);
154
155        for (i, (orig, conv)) in original.iter().zip(values.iter()).enumerate() {
156            assert!(
157                (orig - conv).abs() < 1e-5,
158                "Slice roundtrip failed at {}: {} -> {}",
159                i,
160                orig,
161                conv
162            );
163        }
164    }
165}