tx2_iff/
color.rs

1//! Color space conversions and subsampling
2//!
3//! Implements YCoCg-R (Reversible) color space and 4:2:0 chroma subsampling.
4//!
5//! ## YCoCg-R
6//! Reversible transformation between RGB and YCoCg.
7//!
8//! Forward:
9//! Co = R - B
10//! tmp = B + Co/2
11//! Cg = G - tmp
12//! Y = tmp + Cg/2
13//!
14//! Inverse:
15//! tmp = Y - Cg/2
16//! G = Cg + tmp
17//! B = tmp - Co/2
18//! R = B + Co
19
20use crate::error::{IffError, Result};
21
22/// Image channel data
23#[derive(Debug, Clone)]
24pub struct Channel {
25    pub width: usize,
26    pub height: usize,
27    pub data: Vec<i32>,
28}
29
30impl Channel {
31    pub fn new(width: usize, height: usize) -> Self {
32        Channel {
33            width,
34            height,
35            data: vec![0; width * height],
36        }
37    }
38}
39
40/// YCoCg image
41#[derive(Debug, Clone)]
42pub struct YCoCgImage {
43    pub width: usize,
44    pub height: usize,
45    pub y: Channel,
46    pub co: Channel,
47    pub cg: Channel,
48}
49
50/// Convert RGB to YCoCg-R
51pub fn rgb_to_ycocg(rgb: &[[u8; 3]], width: usize, height: usize) -> YCoCgImage {
52    let mut y_channel = Channel::new(width, height);
53    let mut co_channel = Channel::new(width, height);
54    let mut cg_channel = Channel::new(width, height);
55
56    for i in 0..rgb.len() {
57        let r = rgb[i][0] as i32;
58        let g = rgb[i][1] as i32;
59        let b = rgb[i][2] as i32;
60
61        let co = r - b;
62        let tmp = b + (co >> 1);
63        let cg = g - tmp;
64        let y = tmp + (cg >> 1);
65
66        y_channel.data[i] = y;
67        co_channel.data[i] = co;
68        cg_channel.data[i] = cg;
69    }
70
71    YCoCgImage {
72        width,
73        height,
74        y: y_channel,
75        co: co_channel,
76        cg: cg_channel,
77    }
78}
79
80/// Convert YCoCg-R to RGB
81pub fn ycocg_to_rgb(image: &YCoCgImage) -> Result<Vec<[u8; 3]>> {
82    let width = image.width;
83    let height = image.height;
84    let len = width * height;
85
86    if image.y.data.len() != len || image.co.data.len() != len || image.cg.data.len() != len {
87        return Err(IffError::Other("Channel dimensions mismatch".to_string()));
88    }
89
90    let mut rgb = Vec::with_capacity(len);
91
92    for i in 0..len {
93        let y = image.y.data[i];
94        let co = image.co.data[i];
95        let cg = image.cg.data[i];
96
97        let tmp = y - (cg >> 1);
98        let g = cg + tmp;
99        let b = tmp - (co >> 1);
100        let r = b + co;
101
102        rgb.push([
103            r.clamp(0, 255) as u8,
104            g.clamp(0, 255) as u8,
105            b.clamp(0, 255) as u8,
106        ]);
107    }
108
109    Ok(rgb)
110}
111
112/// Subsample channel (2x2 average)
113pub fn subsample_420(channel: &Channel) -> Channel {
114    let new_width = channel.width / 2;
115    let new_height = channel.height / 2;
116    let mut new_data = Vec::with_capacity(new_width * new_height);
117
118    for y in 0..new_height {
119        for x in 0..new_width {
120            // Average 2x2 block
121            let idx = (y * 2) * channel.width + (x * 2);
122            let sum = channel.data[idx]
123                + channel.data[idx + 1]
124                + channel.data[idx + channel.width]
125                + channel.data[idx + channel.width + 1];
126            new_data.push(sum / 4);
127        }
128    }
129
130    Channel {
131        width: new_width,
132        height: new_height,
133        data: new_data,
134    }
135}
136
137/// Upsample channel (nearest neighbor or bilinear)
138/// Using simple duplication for now to match subsample
139pub fn upsample_420(channel: &Channel, target_width: usize, target_height: usize) -> Channel {
140    let mut new_data = vec![0; target_width * target_height];
141
142    for y in 0..target_height {
143        for x in 0..target_width {
144            let src_x = (x / 2).min(channel.width - 1);
145            let src_y = (y / 2).min(channel.height - 1);
146            let val = channel.data[src_y * channel.width + src_x];
147            new_data[y * target_width + x] = val;
148        }
149    }
150
151    Channel {
152        width: target_width,
153        height: target_height,
154        data: new_data,
155    }
156}
157
158#[cfg(test)]
159mod tests {
160    use super::*;
161
162    #[test]
163    fn test_ycocg_reversible() {
164        let rgb = vec![[100, 150, 200]];
165        let ycocg = rgb_to_ycocg(&rgb, 1, 1);
166        let restored = ycocg_to_rgb(&ycocg).unwrap();
167        assert_eq!(rgb, restored);
168    }
169
170    #[test]
171    fn test_subsample() {
172        let data = vec![10, 10, 20, 20];
173        let channel = Channel {
174            width: 2,
175            height: 2,
176            data,
177        };
178        let sub = subsample_420(&channel);
179        assert_eq!(sub.width, 1);
180        assert_eq!(sub.height, 1);
181        assert_eq!(sub.data[0], 15);
182    }
183}