Skip to main content

rawshift_core/
image.rs

1//! Core image structures and types.
2//!
3//! This module defines the fundamental structures for representing
4//! image dimensions, coordinates, and raw image data.
5
6use crate::color::ColorSpace;
7
8/// Compute the maximum pixel value (white level) for a given bit depth, clamped to `u16`.
9///
10/// Returns `u16::MAX` when `bit_depth >= 16`, and `(1 << bit_depth) - 1` otherwise.
11#[inline]
12pub fn white_level_from_bit_depth(bit_depth: u8) -> u16 {
13    if bit_depth >= 16 {
14        u16::MAX
15    } else if bit_depth == 0 {
16        0
17    } else {
18        (1u16 << bit_depth) - 1
19    }
20}
21
22/// Image dimensions.
23#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
25pub struct Size {
26    /// Width in pixels
27    pub width: u32,
28    /// Height in pixels
29    pub height: u32,
30}
31
32impl Size {
33    /// Create a new Size.
34    pub fn new(width: u32, height: u32) -> Self {
35        Self { width, height }
36    }
37
38    /// Check if dimensions are valid (non-zero).
39    pub fn is_valid(&self) -> bool {
40        self.width > 0 && self.height > 0
41    }
42
43    /// Total number of pixels.
44    pub fn pixel_count(&self) -> u64 {
45        self.width as u64 * self.height as u64
46    }
47}
48
49/// A point in image coordinates.
50#[derive(Debug, Clone, Copy, PartialEq, Eq)]
51#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
52pub struct Point {
53    /// X coordinate
54    pub x: u32,
55    /// Y coordinate
56    pub y: u32,
57}
58
59impl Point {
60    /// Create a new Point.
61    pub fn new(x: u32, y: u32) -> Self {
62        Self { x, y }
63    }
64
65    /// Origin point (0, 0).
66    pub const ORIGIN: Point = Point { x: 0, y: 0 };
67}
68
69/// A rectangular region.
70#[derive(Debug, Clone, Copy, PartialEq, Eq)]
71#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
72pub struct Rect {
73    /// Origin (top-left corner)
74    pub origin: Point,
75    /// Size of the rectangle
76    pub size: Size,
77}
78
79impl Rect {
80    /// Create a new Rect.
81    pub fn new(origin: Point, size: Size) -> Self {
82        Self { origin, size }
83    }
84
85    /// Create a rect from coordinates.
86    pub fn from_coords(x: u32, y: u32, width: u32, height: u32) -> Self {
87        Self {
88            origin: Point::new(x, y),
89            size: Size::new(width, height),
90        }
91    }
92
93    /// Right edge (x + width).
94    pub fn right(&self) -> u32 {
95        self.origin.x.saturating_add(self.size.width)
96    }
97
98    /// Bottom edge (y + height).
99    pub fn bottom(&self) -> u32 {
100        self.origin.y.saturating_add(self.size.height)
101    }
102}
103
104/// CFA (Color Filter Array) pattern.
105///
106/// Represents the Bayer pattern used in the camera's sensor.
107#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
108#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
109pub enum CfaPattern {
110    /// Red-Green / Green-Blue
111    Rggb,
112    /// Green-Red / Blue-Green
113    Grbg,
114    /// Blue-Green / Green-Red
115    Bggr,
116    /// Green-Blue / Red-Green
117    Gbrg,
118}
119
120impl CfaPattern {
121    /// Parse from a 4-element array (row-major 2x2).
122    ///
123    /// Values: 0=Red, 1=Green, 2=Blue
124    pub fn from_array(pattern: [u8; 4]) -> Option<Self> {
125        match pattern {
126            [0, 1, 1, 2] => Some(CfaPattern::Rggb),
127            [1, 0, 2, 1] => Some(CfaPattern::Grbg),
128            [2, 1, 1, 0] => Some(CfaPattern::Bggr),
129            [1, 2, 0, 1] => Some(CfaPattern::Gbrg),
130            _ => None,
131        }
132    }
133
134    /// Convert to a 4-element array.
135    pub fn to_array(self) -> [u8; 4] {
136        match self {
137            CfaPattern::Rggb => [0, 1, 1, 2],
138            CfaPattern::Grbg => [1, 0, 2, 1],
139            CfaPattern::Bggr => [2, 1, 1, 0],
140            CfaPattern::Gbrg => [1, 2, 0, 1],
141        }
142    }
143
144    /// Get a human-readable name.
145    pub fn name(&self) -> &'static str {
146        match self {
147            CfaPattern::Rggb => "RGGB",
148            CfaPattern::Grbg => "GRBG",
149            CfaPattern::Bggr => "BGGR",
150            CfaPattern::Gbrg => "GBRG",
151        }
152    }
153}
154
155/// X-Trans CFA pattern (6x6 repeating tile).
156///
157/// Values: 0=Red, 1=Green, 2=Blue
158/// Row-major order, 36 elements total.
159#[derive(Debug, Clone, Copy, PartialEq, Eq)]
160#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
161pub struct XTransPattern {
162    /// 6x6 grid of color indices: 0=Red, 1=Green, 2=Blue
163    pub cells: [[u8; 6]; 6],
164}
165
166impl XTransPattern {
167    /// Standard Fujifilm X-Trans I pattern (as used in RawTherapee/darktable).
168    pub fn standard() -> Self {
169        Self {
170            cells: [
171                [1, 2, 1, 1, 0, 1],
172                [0, 1, 0, 2, 1, 2],
173                [1, 2, 1, 1, 0, 1],
174                [1, 0, 1, 2, 1, 2],
175                [1, 2, 1, 1, 0, 1],
176                [0, 1, 0, 2, 1, 2],
177            ],
178        }
179    }
180
181    /// Get color at absolute sensor position (x, y) using 6x6 tile wrapping.
182    ///
183    /// Returns 0=Red, 1=Green, 2=Blue.
184    #[inline]
185    pub fn color_at(&self, x: usize, y: usize) -> u8 {
186        self.cells[y % 6][x % 6]
187    }
188}
189
190/// Raw image data container.
191///
192/// Holds the decoded raw sensor data along with associated metadata.
193/// Use [`RawImageBuilder`] to construct new instances.
194#[derive(Debug, Clone)]
195pub struct RawImage {
196    size: Size,
197    active_area: Rect,
198    bit_depth: u8,
199    cfa_pattern: CfaPattern,
200    xtrans_pattern: Option<XTransPattern>,
201    black_levels: [u16; 4],
202    white_level: u16,
203    /// Raw pixel data (16-bit values, one per sensor pixel).
204    /// Stored in row-major order: data[y * width + x]
205    pub data: Vec<u16>,
206    baseline_exposure: Option<f32>,
207    default_crop: Option<Rect>,
208}
209
210impl RawImage {
211    /// Create a new empty RawImage with the given parameters.
212    pub fn new(size: Size, active_area: Rect, bit_depth: u8, cfa_pattern: CfaPattern) -> Self {
213        let pixel_count = size.pixel_count() as usize;
214        Self {
215            size,
216            active_area,
217            bit_depth,
218            cfa_pattern,
219            xtrans_pattern: None,
220            black_levels: [0; 4],
221            white_level: white_level_from_bit_depth(bit_depth),
222            data: vec![0u16; pixel_count],
223            baseline_exposure: None,
224            default_crop: None,
225        }
226    }
227
228    /// Create a builder for constructing a RawImage.
229    pub fn builder(
230        size: Size,
231        active_area: Rect,
232        bit_depth: u8,
233        cfa_pattern: CfaPattern,
234    ) -> RawImageBuilder {
235        RawImageBuilder {
236            size,
237            active_area,
238            bit_depth,
239            cfa_pattern,
240            xtrans_pattern: None,
241            black_levels: [0; 4],
242            white_level: white_level_from_bit_depth(bit_depth),
243            data: None,
244            baseline_exposure: None,
245            default_crop: None,
246        }
247    }
248
249    // ── Read accessors ───────────────────────────────────────────────────
250
251    /// Full sensor dimensions.
252    pub fn size(&self) -> Size {
253        self.size
254    }
255
256    /// Sensor width in pixels.
257    pub fn width(&self) -> u32 {
258        self.size.width
259    }
260
261    /// Sensor height in pixels.
262    pub fn height(&self) -> u32 {
263        self.size.height
264    }
265
266    /// Active/crop area (usable image region).
267    pub fn active_area(&self) -> Rect {
268        self.active_area
269    }
270
271    /// Bits per sample (typically 12, 14, or 16).
272    pub fn bit_depth(&self) -> u8 {
273        self.bit_depth
274    }
275
276    /// CFA (Bayer) pattern.
277    pub fn cfa_pattern(&self) -> CfaPattern {
278        self.cfa_pattern
279    }
280
281    /// X-Trans CFA pattern, if applicable.
282    pub fn xtrans_pattern(&self) -> Option<&XTransPattern> {
283        self.xtrans_pattern.as_ref()
284    }
285
286    /// Black level values (per CFA color channel).
287    pub fn black_levels(&self) -> &[u16; 4] {
288        &self.black_levels
289    }
290
291    /// White/saturation level.
292    pub fn white_level(&self) -> u16 {
293        self.white_level
294    }
295
296    /// Baseline exposure offset in EV.
297    pub fn baseline_exposure(&self) -> Option<f32> {
298        self.baseline_exposure
299    }
300
301    /// Default crop rectangle.
302    pub fn default_crop(&self) -> Option<Rect> {
303        self.default_crop
304    }
305
306    // ── Write accessors ──────────────────────────────────────────────────
307
308    /// Set black level values.
309    pub fn set_black_levels(&mut self, levels: [u16; 4]) {
310        self.black_levels = levels;
311    }
312
313    /// Set white/saturation level.
314    pub fn set_white_level(&mut self, level: u16) {
315        self.white_level = level;
316    }
317
318    /// Set baseline exposure offset.
319    pub fn set_baseline_exposure(&mut self, ev: Option<f32>) {
320        self.baseline_exposure = ev;
321    }
322
323    /// Set default crop rectangle.
324    pub fn set_default_crop(&mut self, crop: Option<Rect>) {
325        self.default_crop = crop;
326    }
327
328    /// Set X-Trans pattern.
329    pub fn set_xtrans_pattern(&mut self, pattern: Option<XTransPattern>) {
330        self.xtrans_pattern = pattern;
331    }
332
333    /// Set bit depth.
334    pub fn set_bit_depth(&mut self, bit_depth: u8) {
335        self.bit_depth = bit_depth;
336    }
337
338    // ── Pixel access ─────────────────────────────────────────────────────
339
340    /// Get pixel value at (x, y).
341    pub fn get_pixel(&self, x: u32, y: u32) -> Option<u16> {
342        if x < self.size.width && y < self.size.height {
343            let idx = (y as usize) * (self.size.width as usize) + (x as usize);
344            Some(self.data[idx])
345        } else {
346            None
347        }
348    }
349
350    /// Set pixel value at (x, y).
351    pub fn set_pixel(&mut self, x: u32, y: u32, value: u16) {
352        if x < self.size.width && y < self.size.height {
353            let idx = (y as usize) * (self.size.width as usize) + (x as usize);
354            self.data[idx] = value;
355        }
356    }
357}
358
359/// Builder for constructing [`RawImage`] instances.
360pub struct RawImageBuilder {
361    size: Size,
362    active_area: Rect,
363    bit_depth: u8,
364    cfa_pattern: CfaPattern,
365    xtrans_pattern: Option<XTransPattern>,
366    black_levels: [u16; 4],
367    white_level: u16,
368    data: Option<Vec<u16>>,
369    baseline_exposure: Option<f32>,
370    default_crop: Option<Rect>,
371}
372
373impl RawImageBuilder {
374    /// Set black level values.
375    pub fn black_levels(mut self, levels: [u16; 4]) -> Self {
376        self.black_levels = levels;
377        self
378    }
379
380    /// Set white/saturation level.
381    pub fn white_level(mut self, level: u16) -> Self {
382        self.white_level = level;
383        self
384    }
385
386    /// Set X-Trans pattern.
387    pub fn xtrans_pattern(mut self, pattern: XTransPattern) -> Self {
388        self.xtrans_pattern = Some(pattern);
389        self
390    }
391
392    /// Set baseline exposure offset in EV.
393    pub fn baseline_exposure(mut self, ev: f32) -> Self {
394        self.baseline_exposure = Some(ev);
395        self
396    }
397
398    /// Set default crop rectangle.
399    pub fn default_crop(mut self, crop: Rect) -> Self {
400        self.default_crop = Some(crop);
401        self
402    }
403
404    /// Set pixel data.
405    pub fn data(mut self, data: Vec<u16>) -> Self {
406        self.data = Some(data);
407        self
408    }
409
410    /// Build the RawImage.
411    pub fn build(self) -> RawImage {
412        let data = self
413            .data
414            .unwrap_or_else(|| vec![0u16; self.size.pixel_count() as usize]);
415        RawImage {
416            size: self.size,
417            active_area: self.active_area,
418            bit_depth: self.bit_depth,
419            cfa_pattern: self.cfa_pattern,
420            xtrans_pattern: self.xtrans_pattern,
421            black_levels: self.black_levels,
422            white_level: self.white_level,
423            data,
424            baseline_exposure: self.baseline_exposure,
425            default_crop: self.default_crop,
426        }
427    }
428}
429
430/// A simple container for RGB image data.
431#[derive(Debug, Clone)]
432pub struct RgbImage {
433    size: Size,
434    /// Interleaved RGB data (R, G, B, R, G, B...)
435    pub data: Vec<u16>,
436    baseline_exposure: Option<f32>,
437    default_crop: Option<Rect>,
438    color_space: ColorSpace,
439}
440
441impl RgbImage {
442    /// Create a new RgbImage with an unknown color space.
443    ///
444    /// Use [`with_color_space`](Self::with_color_space) or
445    /// [`set_color_space`](Self::set_color_space) when the space is known.
446    pub fn new(width: u32, height: u32, data: Vec<u16>) -> Self {
447        Self {
448            size: Size::new(width, height),
449            data,
450            baseline_exposure: None,
451            default_crop: None,
452            color_space: ColorSpace::Unknown,
453        }
454    }
455
456    /// Create a new RgbImage tagged with a known color space.
457    pub fn with_color_space(
458        width: u32,
459        height: u32,
460        data: Vec<u16>,
461        color_space: ColorSpace,
462    ) -> Self {
463        Self {
464            size: Size::new(width, height),
465            data,
466            baseline_exposure: None,
467            default_crop: None,
468            color_space,
469        }
470    }
471
472    // ── Read accessors ───────────────────────────────────────────────────
473
474    /// Image dimensions.
475    pub fn size(&self) -> Size {
476        self.size
477    }
478
479    /// Image width in pixels.
480    pub fn width(&self) -> u32 {
481        self.size.width
482    }
483
484    /// Image height in pixels.
485    pub fn height(&self) -> u32 {
486        self.size.height
487    }
488
489    /// Baseline exposure offset in EV.
490    pub fn baseline_exposure(&self) -> Option<f32> {
491        self.baseline_exposure
492    }
493
494    /// Default crop rectangle.
495    pub fn default_crop(&self) -> Option<Rect> {
496        self.default_crop
497    }
498
499    /// The color space the RGB samples are in.
500    pub fn color_space(&self) -> ColorSpace {
501        self.color_space
502    }
503
504    // ── Write accessors ──────────────────────────────────────────────────
505
506    /// Set baseline exposure offset.
507    pub fn set_baseline_exposure(&mut self, ev: Option<f32>) {
508        self.baseline_exposure = ev;
509    }
510
511    /// Set the color space tag for the RGB samples.
512    pub fn set_color_space(&mut self, color_space: ColorSpace) {
513        self.color_space = color_space;
514    }
515
516    /// Set default crop rectangle.
517    pub fn set_default_crop(&mut self, crop: Option<Rect>) {
518        self.default_crop = crop;
519    }
520
521    /// Set image dimensions (used by orientation transforms).
522    pub fn set_size(&mut self, size: Size) {
523        self.size = size;
524    }
525}
526
527#[cfg(test)]
528mod tests {
529    use super::*;
530
531    #[test]
532    fn test_white_level_from_bit_depth() {
533        assert_eq!(white_level_from_bit_depth(0), 0);
534        assert_eq!(white_level_from_bit_depth(1), 1);
535        assert_eq!(white_level_from_bit_depth(8), 255);
536        assert_eq!(white_level_from_bit_depth(12), 4095);
537        assert_eq!(white_level_from_bit_depth(14), 16383);
538        assert_eq!(white_level_from_bit_depth(15), 32767);
539        // bit_depth >= 16 clamps to u16::MAX
540        assert_eq!(white_level_from_bit_depth(16), u16::MAX);
541        assert_eq!(white_level_from_bit_depth(32), u16::MAX);
542        assert_eq!(white_level_from_bit_depth(255), u16::MAX);
543    }
544
545    #[test]
546    fn test_size() {
547        let size = Size::new(100, 200);
548        assert_eq!(size.pixel_count(), 20000);
549        assert!(size.is_valid());
550
551        let empty = Size::new(0, 100);
552        assert!(!empty.is_valid());
553    }
554
555    #[test]
556    fn test_cfa_pattern() {
557        assert_eq!(CfaPattern::from_array([0, 1, 1, 2]), Some(CfaPattern::Rggb));
558        assert_eq!(CfaPattern::Rggb.to_array(), [0, 1, 1, 2]);
559        assert_eq!(CfaPattern::Rggb.name(), "RGGB");
560    }
561
562    #[test]
563    fn test_raw_image() {
564        let size = Size::new(10, 10);
565        let active = Rect::from_coords(0, 0, 10, 10);
566        let mut img = RawImage::new(size, active, 14, CfaPattern::Rggb);
567
568        img.set_pixel(5, 5, 1000);
569        assert_eq!(img.get_pixel(5, 5), Some(1000));
570        assert_eq!(img.get_pixel(100, 100), None);
571    }
572
573    #[test]
574    fn test_raw_image_pixel_access() {
575        let size = Size::new(4, 4);
576        let active = Rect::from_coords(0, 0, 4, 4);
577        let mut img = RawImage::new(size, active, 14, CfaPattern::Rggb);
578
579        // Set several pixels and verify get_pixel returns correct values
580        img.set_pixel(0, 0, 100);
581        img.set_pixel(3, 0, 200);
582        img.set_pixel(0, 3, 300);
583        img.set_pixel(3, 3, 400);
584        img.set_pixel(2, 1, 500);
585
586        assert_eq!(img.get_pixel(0, 0), Some(100));
587        assert_eq!(img.get_pixel(3, 0), Some(200));
588        assert_eq!(img.get_pixel(0, 3), Some(300));
589        assert_eq!(img.get_pixel(3, 3), Some(400));
590        assert_eq!(img.get_pixel(2, 1), Some(500));
591
592        // Out-of-bounds returns None
593        assert_eq!(img.get_pixel(4, 0), None);
594        assert_eq!(img.get_pixel(0, 4), None);
595        assert_eq!(img.get_pixel(u32::MAX, u32::MAX), None);
596    }
597
598    #[test]
599    fn test_rgb_image_indexing() {
600        // RgbImage stores interleaved RGB: R G B R G B ...
601        let data = vec![
602            100u16, 200, 300, // pixel 0: R=100, G=200, B=300
603            400, 500, 600, // pixel 1: R=400, G=500, B=600
604        ];
605        let img = RgbImage::new(2, 1, data.clone());
606
607        assert_eq!(img.data[0], 100, "pixel 0 R");
608        assert_eq!(img.data[1], 200, "pixel 0 G");
609        assert_eq!(img.data[2], 300, "pixel 0 B");
610        assert_eq!(img.data[3], 400, "pixel 1 R");
611        assert_eq!(img.data[4], 500, "pixel 1 G");
612        assert_eq!(img.data[5], 600, "pixel 1 B");
613
614        assert_eq!(img.width(), 2);
615        assert_eq!(img.height(), 1);
616        assert_eq!(img.data.len(), 6);
617    }
618
619    #[test]
620    fn test_size_pixel_count() {
621        let s = Size::new(1920, 1080);
622        assert_eq!(s.pixel_count(), 1920 * 1080);
623
624        // Zero dimension
625        let s = Size::new(0, 100);
626        assert_eq!(s.pixel_count(), 0);
627
628        // Large dimensions (check u64 doesn't overflow)
629        let s = Size::new(10000, 10000);
630        assert_eq!(s.pixel_count(), 100_000_000u64);
631    }
632
633    #[test]
634    fn test_rect_dimensions() {
635        let r = Rect::from_coords(10, 20, 100, 200);
636        assert_eq!(r.origin.x, 10);
637        assert_eq!(r.origin.y, 20);
638        assert_eq!(r.size.width, 100);
639        assert_eq!(r.size.height, 200);
640        assert_eq!(r.right(), 110);
641        assert_eq!(r.bottom(), 220);
642    }
643
644    #[test]
645    fn test_raw_image_builder() {
646        let size = Size::new(10, 10);
647        let active = Rect::from_coords(0, 0, 10, 10);
648        let img = RawImage::builder(size, active, 14, CfaPattern::Rggb)
649            .black_levels([100, 100, 100, 100])
650            .white_level(16383)
651            .build();
652
653        assert_eq!(img.size(), size);
654        assert_eq!(img.active_area(), active);
655        assert_eq!(img.bit_depth(), 14);
656        assert_eq!(img.cfa_pattern(), CfaPattern::Rggb);
657        assert_eq!(*img.black_levels(), [100, 100, 100, 100]);
658        assert_eq!(img.white_level(), 16383);
659        assert_eq!(img.data.len(), 100);
660    }
661
662    #[test]
663    fn test_raw_image_builder_with_data() {
664        let size = Size::new(2, 2);
665        let active = Rect::from_coords(0, 0, 2, 2);
666        let img = RawImage::builder(size, active, 14, CfaPattern::Rggb)
667            .data(vec![1000, 2000, 3000, 4000])
668            .build();
669
670        assert_eq!(img.data, vec![1000, 2000, 3000, 4000]);
671    }
672
673    #[test]
674    fn test_rgb_image_accessors() {
675        let img = RgbImage::new(100, 200, vec![0u16; 100 * 200 * 3]);
676        assert_eq!(img.width(), 100);
677        assert_eq!(img.height(), 200);
678        assert_eq!(img.size(), Size::new(100, 200));
679        assert_eq!(img.baseline_exposure(), None);
680        assert_eq!(img.default_crop(), None);
681    }
682
683    #[test]
684    fn test_raw_image_setters() {
685        let size = Size::new(4, 4);
686        let active = Rect::from_coords(0, 0, 4, 4);
687        let mut img = RawImage::new(size, active, 14, CfaPattern::Rggb);
688
689        img.set_black_levels([100, 100, 100, 100]);
690        assert_eq!(*img.black_levels(), [100, 100, 100, 100]);
691
692        img.set_white_level(4095);
693        assert_eq!(img.white_level(), 4095);
694
695        img.set_baseline_exposure(Some(-0.8));
696        assert_eq!(img.baseline_exposure(), Some(-0.8));
697
698        let crop = Rect::from_coords(1, 1, 2, 2);
699        img.set_default_crop(Some(crop));
700        assert_eq!(img.default_crop(), Some(crop));
701
702        img.set_xtrans_pattern(Some(XTransPattern::standard()));
703        assert!(img.xtrans_pattern().is_some());
704    }
705}