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}