Skip to main content

rawshift_core/
pixel.rs

1//! Pixel type system for generic image processing.
2//!
3//! Provides traits and type aliases for working with different pixel
4//! representations (u8, u16, f32) in a uniform way.
5
6/// Trait for scalar sample values used in image processing.
7///
8/// Implemented for `u8`, `u16`, and `f32` — the common pixel sample types
9/// in raw image processing pipelines.
10pub trait Sample:
11    Copy + Clone + PartialOrd + Default + Send + Sync + 'static + Into<f32> + FromF32
12{
13    /// The maximum representable value for this sample type.
14    const MAX: Self;
15
16    /// The minimum representable value for this sample type.
17    const MIN: Self;
18
19    /// The number of bits used to represent this sample.
20    const BIT_DEPTH: u8;
21
22    /// Clamp a value to the valid range.
23    fn clamp_sample(self) -> Self;
24
25    /// Convert from a normalized f32 value in [0.0, 1.0] to this sample type.
26    fn from_normalized(v: f32) -> Self;
27
28    /// Convert this sample to a normalized f32 value in [0.0, 1.0].
29    fn to_normalized(self) -> f32;
30}
31
32/// Conversion trait from f32 to a sample type.
33pub trait FromF32 {
34    fn from_f32(v: f32) -> Self;
35}
36
37impl FromF32 for u8 {
38    #[inline]
39    fn from_f32(v: f32) -> Self {
40        v.round().clamp(0.0, 255.0) as u8
41    }
42}
43
44impl FromF32 for u16 {
45    #[inline]
46    fn from_f32(v: f32) -> Self {
47        v.round().clamp(0.0, 65535.0) as u16
48    }
49}
50
51impl FromF32 for f32 {
52    #[inline]
53    fn from_f32(v: f32) -> Self {
54        v
55    }
56}
57
58impl Sample for u8 {
59    const MAX: Self = 255;
60    const MIN: Self = 0;
61    const BIT_DEPTH: u8 = 8;
62
63    #[inline]
64    fn clamp_sample(self) -> Self {
65        self // u8 is always in range
66    }
67
68    #[inline]
69    fn from_normalized(v: f32) -> Self {
70        (v * 255.0).round().clamp(0.0, 255.0) as u8
71    }
72
73    #[inline]
74    fn to_normalized(self) -> f32 {
75        self as f32 / 255.0
76    }
77}
78
79impl Sample for u16 {
80    const MAX: Self = 65535;
81    const MIN: Self = 0;
82    const BIT_DEPTH: u8 = 16;
83
84    #[inline]
85    fn clamp_sample(self) -> Self {
86        self // u16 is always in range
87    }
88
89    #[inline]
90    fn from_normalized(v: f32) -> Self {
91        (v * 65535.0).round().clamp(0.0, 65535.0) as u16
92    }
93
94    #[inline]
95    fn to_normalized(self) -> f32 {
96        self as f32 / 65535.0
97    }
98}
99
100impl Sample for f32 {
101    const MAX: Self = 1.0;
102    const MIN: Self = 0.0;
103    const BIT_DEPTH: u8 = 32;
104
105    #[inline]
106    fn clamp_sample(self) -> Self {
107        self.clamp(0.0, 1.0)
108    }
109
110    #[inline]
111    fn from_normalized(v: f32) -> Self {
112        v
113    }
114
115    #[inline]
116    fn to_normalized(self) -> f32 {
117        self
118    }
119}
120
121/// An RGB pixel with three components.
122#[derive(Debug, Clone, Copy, PartialEq)]
123pub struct Rgb<S: Sample> {
124    pub r: S,
125    pub g: S,
126    pub b: S,
127}
128
129impl<S: Sample> Rgb<S> {
130    /// Create a new RGB pixel.
131    #[inline]
132    pub fn new(r: S, g: S, b: S) -> Self {
133        Self { r, g, b }
134    }
135
136    /// Convert to normalized f32 RGB.
137    #[inline]
138    pub fn to_f32(self) -> Rgb<f32> {
139        Rgb {
140            r: self.r.to_normalized(),
141            g: self.g.to_normalized(),
142            b: self.b.to_normalized(),
143        }
144    }
145
146    /// Convert from normalized f32 RGB.
147    #[inline]
148    pub fn from_f32(src: Rgb<f32>) -> Self {
149        Rgb {
150            r: S::from_normalized(src.r),
151            g: S::from_normalized(src.g),
152            b: S::from_normalized(src.b),
153        }
154    }
155}
156
157impl<S: Sample> Default for Rgb<S> {
158    fn default() -> Self {
159        Self {
160            r: S::default(),
161            g: S::default(),
162            b: S::default(),
163        }
164    }
165}
166
167/// An RGBA pixel with four components.
168#[derive(Debug, Clone, Copy, PartialEq)]
169pub struct Rgba<S: Sample> {
170    pub r: S,
171    pub g: S,
172    pub b: S,
173    pub a: S,
174}
175
176impl<S: Sample> Rgba<S> {
177    /// Create a new RGBA pixel.
178    #[inline]
179    pub fn new(r: S, g: S, b: S, a: S) -> Self {
180        Self { r, g, b, a }
181    }
182
183    /// Convert to RGB, discarding the alpha channel.
184    #[inline]
185    pub fn to_rgb(self) -> Rgb<S> {
186        Rgb::new(self.r, self.g, self.b)
187    }
188}
189
190impl<S: Sample> Default for Rgba<S> {
191    fn default() -> Self {
192        Self {
193            r: S::default(),
194            g: S::default(),
195            b: S::default(),
196            a: S::MAX,
197        }
198    }
199}
200
201/// Convenience type aliases for common pixel representations.
202pub type Rgb8 = Rgb<u8>;
203pub type Rgb16 = Rgb<u16>;
204pub type RgbF32 = Rgb<f32>;
205pub type Rgba8 = Rgba<u8>;
206pub type Rgba16 = Rgba<u16>;
207pub type RgbaF32 = Rgba<f32>;
208
209/// Convert a slice of interleaved u16 RGB data to a Vec of Rgb16 pixels.
210pub fn rgb16_from_interleaved(data: &[u16]) -> Vec<Rgb16> {
211    debug_assert!(data.len().is_multiple_of(3));
212    data.chunks_exact(3)
213        .map(|c| Rgb16::new(c[0], c[1], c[2]))
214        .collect()
215}
216
217/// Convert a slice of Rgb16 pixels to interleaved u16 data.
218pub fn rgb16_to_interleaved(pixels: &[Rgb16]) -> Vec<u16> {
219    let mut data = Vec::with_capacity(pixels.len() * 3);
220    for p in pixels {
221        data.push(p.r);
222        data.push(p.g);
223        data.push(p.b);
224    }
225    data
226}
227
228#[cfg(test)]
229mod tests {
230    use super::*;
231
232    #[test]
233    fn test_u8_sample() {
234        assert_eq!(u8::MAX, 255);
235        assert_eq!(u8::from_normalized(1.0), 255);
236        assert_eq!(u8::from_normalized(0.0), 0);
237        assert_eq!(u8::from_normalized(0.5), 128);
238        assert!((128u8.to_normalized() - 0.502).abs() < 0.01);
239    }
240
241    #[test]
242    fn test_u16_sample() {
243        assert_eq!(u16::MAX, 65535);
244        assert_eq!(u16::from_normalized(1.0), 65535);
245        assert_eq!(u16::from_normalized(0.0), 0);
246        let half = u16::from_normalized(0.5);
247        assert!((half as i32 - 32768).abs() <= 1);
248    }
249
250    #[test]
251    fn test_f32_sample() {
252        assert_eq!(f32::from_normalized(0.5), 0.5);
253        assert_eq!((0.5f32).to_normalized(), 0.5);
254        assert_eq!((1.5f32).clamp_sample(), 1.0);
255        assert_eq!((-0.5f32).clamp_sample(), 0.0);
256    }
257
258    #[test]
259    fn test_rgb_pixel() {
260        let p = Rgb16::new(1000, 2000, 3000);
261        assert_eq!(p.r, 1000);
262        assert_eq!(p.g, 2000);
263        assert_eq!(p.b, 3000);
264    }
265
266    #[test]
267    fn test_rgb_roundtrip() {
268        let p = Rgb16::new(1000, 2000, 3000);
269        let f = p.to_f32();
270        let back = Rgb16::from_f32(f);
271        assert!((back.r as i32 - 1000).abs() <= 1);
272        assert!((back.g as i32 - 2000).abs() <= 1);
273        assert!((back.b as i32 - 3000).abs() <= 1);
274    }
275
276    #[test]
277    fn test_rgba_default() {
278        let p = Rgba16::default();
279        assert_eq!(p.r, 0);
280        assert_eq!(p.g, 0);
281        assert_eq!(p.b, 0);
282        assert_eq!(p.a, 65535);
283    }
284
285    #[test]
286    fn test_interleaved_roundtrip() {
287        let data = vec![100u16, 200, 300, 400, 500, 600];
288        let pixels = rgb16_from_interleaved(&data);
289        assert_eq!(pixels.len(), 2);
290        assert_eq!(pixels[0], Rgb16::new(100, 200, 300));
291        let back = rgb16_to_interleaved(&pixels);
292        assert_eq!(back, data);
293    }
294}