Skip to main content

device_envoy_core/cyd/display/
tga.rs

1//! Compile-time decoding and drawing of the supported subset of TGA images.
2
3use crate::cyd::display::CydFrame;
4use embedded_graphics::{
5    Drawable, Pixel,
6    pixelcolor::{Rgb565, raw::RawU16},
7    prelude::{DrawTarget, Point, Size},
8    primitives::Rectangle,
9};
10
11/// Returns the number of bytes required for a packed one-bit mask.
12pub const fn mask_byte_count(width: usize, height: usize) -> usize {
13    (width * height).div_ceil(8)
14}
15
16/// A decoded, row-major RGB888 image.
17pub struct Image888Fixed<const W: usize, const H: usize, const N: usize> {
18    /// Row-major top-left-origin pixels stored as `[red, green, blue]`.
19    pub pixels: [[u8; 3]; N],
20}
21
22/// An opaque RGB565 image.
23pub struct Image565Fixed<const W: usize, const H: usize, const N: usize> {
24    /// Row-major top-left-origin pixels.
25    pub pixels: [u16; N],
26}
27
28/// A packed binary visibility mask with one bit per image pixel.
29pub struct MaskFixed<const W: usize, const H: usize, const MASK_N: usize> {
30    /// Row-major bits, least-significant bit first. Set means visible.
31    pub bits: [u8; MASK_N],
32}
33
34pub struct PlacedImage565<'a, const W: usize, const H: usize, const N: usize> {
35    image: &'a Image565Fixed<W, H, N>,
36    top_left: Point,
37}
38
39const fn read_u16(bytes: &[u8], offset: usize) -> u16 {
40    bytes[offset] as u16 | ((bytes[offset + 1] as u16) << 8)
41}
42
43const fn parse_header(bytes: &[u8], width: usize, height: usize) -> (usize, usize, bool) {
44    assert!(bytes.len() >= 18, "TGA: file shorter than header");
45    assert!(bytes[1] == 0, "TGA: color maps are not supported");
46    assert!(
47        bytes[2] == 2,
48        "TGA: only uncompressed true-color images are supported"
49    );
50    assert!(
51        bytes[3] == 0 && bytes[4] == 0 && bytes[5] == 0 && bytes[6] == 0 && bytes[7] == 0,
52        "TGA: color map specification is not supported"
53    );
54    assert!(
55        read_u16(bytes, 12) as usize == width,
56        "TGA: width does not match const argument"
57    );
58    assert!(
59        read_u16(bytes, 14) as usize == height,
60        "TGA: height does not match const argument"
61    );
62    assert!(
63        bytes[16] == 24 || bytes[16] == 32,
64        "TGA: only 24-bit BGR or 32-bit BGRA is supported"
65    );
66    assert!(
67        bytes[17] & 0x10 == 0,
68        "TGA: right-to-left origin is not supported"
69    );
70    let bytes_per_pixel = (bytes[16] / 8) as usize;
71    let pixel_start = 18 + bytes[0] as usize;
72    assert!(
73        bytes.len() >= pixel_start + width * height * bytes_per_pixel,
74        "TGA: pixel data is shorter than width * height"
75    );
76    (pixel_start, bytes_per_pixel, bytes[17] & 0x20 != 0)
77}
78
79impl<const W: usize, const H: usize, const N: usize> Image888Fixed<W, H, N> {
80    /// Decodes a supported TGA at compile time, preserving RGB and discarding alpha.
81    pub const fn from_tga(bytes: &[u8]) -> Self {
82        assert!(N == W * H, "Image888Fixed: N must equal W * H");
83        let (pixel_start, bytes_per_pixel, top_origin) = parse_header(bytes, W, H);
84        let mut pixels = [[0u8; 3]; N];
85        let mut y = 0;
86        while y < H {
87            let mut x = 0;
88            while x < W {
89                let source_y = if top_origin { y } else { H - 1 - y };
90                let offset = pixel_start + (source_y * W + x) * bytes_per_pixel;
91                let red = bytes[offset + 2];
92                let green = bytes[offset + 1];
93                let blue = bytes[offset];
94                pixels[y * W + x] = [red, green, blue];
95                x += 1;
96            }
97            y += 1;
98        }
99        Self { pixels }
100    }
101
102    /// Converts this source image to RGB565.
103    pub const fn to_565(&self) -> Image565Fixed<W, H, N> {
104        let mut pixels = [0u16; N];
105        let mut index = 0;
106        while index < N {
107            let [red, green, blue] = self.pixels[index];
108            pixels[index] =
109                ((red as u16 >> 3) << 11) | ((green as u16 >> 2) << 5) | (blue as u16 >> 3);
110            index += 1;
111        }
112        Image565Fixed { pixels }
113    }
114
115    /// Derives a binary mask using the magenta color key.
116    pub const fn to_mask_magenta<const MASK_N: usize>(&self) -> MaskFixed<W, H, MASK_N> {
117        assert!(
118            MASK_N == mask_byte_count(W, H),
119            "Mask: MASK_N must match image dimensions"
120        );
121        let mut bits = [0u8; MASK_N];
122        let mut index = 0;
123        while index < N {
124            let pixel = self.pixels[index];
125            let red = pixel[0];
126            let green = pixel[1];
127            let blue = pixel[2];
128            if !(red >= 200 && blue >= 200 && green <= 60) {
129                bits[index / 8] |= 1 << (index % 8);
130            }
131            index += 1;
132        }
133        MaskFixed { bits }
134    }
135}
136
137impl<const W: usize, const H: usize, const N: usize> Image565Fixed<W, H, N> {
138    pub const fn at(&self, top_left: Point) -> PlacedImage565<'_, W, H, N> {
139        PlacedImage565 {
140            image: self,
141            top_left,
142        }
143    }
144
145    pub const fn view(&'static self) -> super::Image565View {
146        self.view_rect(Rectangle::new(Point::zero(), Size::new(W as u32, H as u32)))
147    }
148
149    pub const fn view_rect(&'static self, source: Rectangle) -> super::Image565View {
150        assert!(
151            source.top_left.x >= 0 && source.top_left.y >= 0,
152            "view_rect: negative origin"
153        );
154        assert!(
155            source.top_left.x as usize + source.size.width as usize <= W
156                && source.top_left.y as usize + source.size.height as usize <= H,
157            "view_rect: rectangle is outside image"
158        );
159        super::Image565View::new_cropped(&self.pixels, W as u32, source)
160    }
161
162    pub fn copy_to<F: CydFrame>(&self, frame: &mut F) -> crate::Result<()> {
163        frame.copy_from_565(&self.pixels)
164    }
165}
166
167impl<const W: usize, const H: usize, const MASK_N: usize> MaskFixed<W, H, MASK_N> {
168    pub const fn is_set(&self, index: usize) -> bool {
169        self.bits[index / 8] & (1 << (index % 8)) != 0
170    }
171}
172
173impl<'a, const W: usize, const H: usize, const N: usize> PlacedImage565<'a, W, H, N> {
174    pub fn draw_masked<const MASK_N: usize, D>(
175        &self,
176        mask: &MaskFixed<W, H, MASK_N>,
177        target: &mut D,
178    ) -> Result<(), D::Error>
179    where
180        D: DrawTarget<Color = Rgb565>,
181    {
182        let mut pixels = ImagePixels::<W, N> {
183            pixels: &self.image.pixels,
184            top_left: self.top_left,
185            index: 0,
186        };
187        target.draw_iter(core::iter::from_fn(|| {
188            loop {
189                let pixel = pixels.next()?;
190                let index = pixels.index - 1;
191                if mask.is_set(index) {
192                    return Some(pixel);
193                }
194            }
195        }))
196    }
197}
198
199struct ImagePixels<'a, const W: usize, const N: usize> {
200    pixels: &'a [u16; N],
201    top_left: Point,
202    index: usize,
203}
204
205impl<const W: usize, const N: usize> Iterator for ImagePixels<'_, W, N> {
206    type Item = Pixel<Rgb565>;
207    fn next(&mut self) -> Option<Self::Item> {
208        if self.index >= N {
209            return None;
210        }
211        let index = self.index;
212        self.index += 1;
213        Some(Pixel(
214            self.top_left + Point::new((index % W) as i32, (index / W) as i32),
215            Rgb565::from(RawU16::new(self.pixels[index])),
216        ))
217    }
218}
219
220impl<const W: usize, const H: usize, const N: usize> Drawable for PlacedImage565<'_, W, H, N> {
221    type Color = Rgb565;
222    type Output = ();
223    fn draw<D>(&self, target: &mut D) -> Result<(), D::Error>
224    where
225        D: DrawTarget<Color = Rgb565>,
226    {
227        target.draw_iter(ImagePixels::<W, N> {
228            pixels: &self.image.pixels,
229            top_left: self.top_left,
230            index: 0,
231        })
232    }
233}
234
235impl<const W: usize, const H: usize, const N: usize> Drawable for Image565Fixed<W, H, N> {
236    type Color = Rgb565;
237    type Output = ();
238    fn draw<D>(&self, target: &mut D) -> Result<(), D::Error>
239    where
240        D: DrawTarget<Color = Rgb565>,
241    {
242        self.at(Point::zero()).draw(target)
243    }
244}
245
246#[doc(hidden)]
247#[macro_export]
248macro_rules! __cyd_tga {
249    ($path:expr) => {
250        $crate::cyd::display::Image888Fixed::from_tga(include_bytes!($path))
251    };
252    ($path:expr, $width:expr, $height:expr) => {
253        $crate::cyd::display::Image888Fixed::<$width, $height, { $width * $height }>::from_tga(
254            include_bytes!($path),
255        )
256    };
257}