Skip to main content

device_envoy_core/cyd/display/
tga.rs

1//! Compile-time image storage, conversion, masking, and drawing internals.
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 storage length required by [`MaskFixed`] for an image size.
12///
13/// Each image pixel uses one bit, and the final byte includes any padding bits
14/// needed when the pixel count is not divisible by eight.
15///
16/// Use this function for `MaskFixed`'s third const argument when calling
17/// [`Image888Fixed::to_mask_magenta`]. See the visual
18/// [`MaskFixed` example](MaskFixed).
19pub const fn mask_byte_count(width: usize, height: usize) -> usize {
20    (width * height).div_ceil(8)
21}
22
23/// A fixed-size RGB888 source image stored directly in the value.
24///
25/// `W` and `H` are the image dimensions and `N` is the pixel count (`W * H`).
26/// Use [`tga!`](macro@crate::cyd::display::tga) to embed and decode an image
27/// file at compile time, then convert it to display-ready RGB565 storage or a
28/// visibility mask. See the visual [`MaskFixed` example](MaskFixed) for
29/// color-key transparency.
30///
31/// # Example
32///
33/// ```rust,no_run
34/// use device_envoy_core::cyd::display::{Image565Fixed, Image888Fixed, tga};
35///
36/// const SOURCE: Image888Fixed<45, 73, { 45 * 73 }> = tga!(concat!(
37///     env!("CARGO_MANIFEST_DIR"),
38///     "/docs/assets/cyd_fill_contiguous.tga"
39/// ));
40/// const IMAGE: Image565Fixed<45, 73, { 45 * 73 }> = SOURCE.to_565();
41///
42/// assert_eq!(SOURCE.pixels.len(), 45 * 73);
43/// assert_eq!(IMAGE.pixels.len(), 45 * 73);
44/// ```
45pub struct Image888Fixed<const W: usize, const H: usize, const N: usize> {
46    /// Row-major, top-left-origin pixels stored as `[red, green, blue]`.
47    pub pixels: [[u8; 3]; N],
48}
49
50/// A fixed-size RGB565 image stored directly in the value.
51///
52/// `W` and `H` are the image dimensions and `N` is the pixel count (`W * H`).
53/// The image has no alpha channel. Use [`MaskFixed`] and
54/// [`MaskedDrawable::draw_masked`] when color-key transparency is needed.
55/// The visual [`MaskFixed` example](MaskFixed) shows the opaque source and
56/// masked result side by side. Convert a compile-time [`Image888Fixed`] with
57/// [`Image888Fixed::to_565`].
58///
59/// Use [`Image565Fixed::view`] to borrow the complete image as an
60/// [`Image565View`](super::Image565View), or [`Image565Fixed::view_rect`] to
61/// borrow a crop without copying pixels.
62///
63/// # Example
64#[cfg_attr(
65    feature = "doc-images",
66    doc = ::embed_doc_image::embed_image!(
67        "image565_fixed",
68        "docs/assets/image565_fixed.png"
69    )
70)]
71#[cfg_attr(
72    feature = "host",
73    doc = r#"
74
75```rust
76use device_envoy_core::{
77    UnwrapInfallible,
78    cyd::{
79        Cyd, CydDisplay,
80        display::{CydFrame, Image565Fixed, tga},
81    },
82};
83use embedded_graphics::{Drawable, prelude::Point};
84
85const IMAGE: Image565Fixed<45, 73, { 45 * 73 }> = tga!(concat!(
86    env!("CARGO_MANIFEST_DIR"),
87    "/docs/assets/cyd_fill_contiguous.tga"
88))
89.to_565();
90
91async fn draw<C: Cyd>(cyd: &mut C) -> Result<(), C::Error> {
92    let display = cyd.display();
93    let mut frame = display.full_frame_mut();
94    for top_left in [
95        Point::new(50, 84),
96        Point::new(138, 84),
97        Point::new(226, 84),
98    ] {
99        IMAGE.at(top_left).draw(&mut frame).unwrap_infallible();
100    }
101    frame.flush().await
102}
103
104# use device_envoy_core::memory::{CydMemory, assert_framebuffer_matches_expected_png};
105# use embedded_graphics::{
106#     mono_font::ascii::FONT_9X15_BOLD,
107#     pixelcolor::Rgb888,
108#     prelude::{RgbColor, Size},
109# };
110# let mut cyd_memory = CydMemory::new(
111#     Size::new(320, 240),
112#     Rgb888::BLACK,
113#     Rgb888::WHITE,
114#     &FONT_9X15_BOLD,
115# );
116# futures_executor::block_on(draw(&mut cyd_memory))?;
117# let golden_result = assert_framebuffer_matches_expected_png(
118#     &cyd_memory,
119#     env!("CARGO_MANIFEST_DIR"),
120#     "image565_fixed.png",
121# );
122# assert!(golden_result.is_ok(), "{golden_result:?}");
123# Ok::<(), device_envoy_core::memory::Error>(())
124```
125
126![The same fixed RGB565 image drawn at three display positions][image565_fixed]
127"#
128)]
129pub struct Image565Fixed<const W: usize, const H: usize, const N: usize> {
130    /// Row-major, top-left-origin pixels packed as `RRRRR_GGGGGG_BBBBB`.
131    pub pixels: [u16; N],
132}
133
134/// A packed binary visibility mask with one bit per image pixel.
135///
136/// Set bits are drawn and clear bits are transparent. Create a mask from an
137/// [`Image888Fixed`] with [`Image888Fixed::to_mask_magenta`], then pass it to
138/// [`MaskedDrawable::draw_masked`].
139///
140/// # Example
141#[cfg_attr(
142    feature = "doc-images",
143    doc = ::embed_doc_image::embed_image!("mask_fixed", "docs/assets/mask_fixed.png")
144)]
145#[cfg_attr(
146    feature = "host",
147    doc = r#"
148
149```rust
150use device_envoy_core::{
151    UnwrapInfallible,
152    cyd::{
153        Cyd, CydDisplay,
154        display::{
155            CydFrame, Image565Fixed, Image888Fixed, MaskFixed, MaskedDrawable,
156            mask_byte_count, tga,
157        },
158    },
159};
160use embedded_graphics::{Drawable, prelude::Point};
161
162const SOURCE: Image888Fixed<45, 73, { 45 * 73 }> = tga!(concat!(
163    env!("CARGO_MANIFEST_DIR"),
164    "/docs/assets/cyd_fill_contiguous.tga"
165));
166const IMAGE: Image565Fixed<45, 73, { 45 * 73 }> = SOURCE.to_565();
167const MASK: MaskFixed<45, 73, { mask_byte_count(45, 73) }> =
168    SOURCE.to_mask_magenta();
169
170async fn draw<C: Cyd>(cyd: &mut C) -> Result<(), C::Error> {
171    let display = cyd.display();
172    let mut frame = display.full_frame_mut();
173    IMAGE
174        .at(Point::new(80, 84))
175        .draw(&mut frame)
176        .unwrap_infallible();
177    IMAGE
178        .at(Point::new(200, 84))
179        .draw_masked(&MASK, &mut frame)
180        .unwrap_infallible();
181    frame.flush().await
182}
183
184assert!(!MASK.is_set(0));
185# use device_envoy_core::memory::{CydMemory, assert_framebuffer_matches_expected_png};
186# use embedded_graphics::{
187#     mono_font::ascii::FONT_9X15_BOLD,
188#     pixelcolor::Rgb888,
189#     prelude::{RgbColor, Size},
190# };
191# let mut cyd_memory = CydMemory::new(
192#     Size::new(320, 240),
193#     Rgb888::BLACK,
194#     Rgb888::WHITE,
195#     &FONT_9X15_BOLD,
196# );
197# futures_executor::block_on(draw(&mut cyd_memory))?;
198# let golden_result = assert_framebuffer_matches_expected_png(
199#     &cyd_memory,
200#     env!("CARGO_MANIFEST_DIR"),
201#     "mask_fixed.png",
202# );
203# assert!(golden_result.is_ok(), "{golden_result:?}");
204# Ok::<(), device_envoy_core::memory::Error>(())
205```
206
207The opaque RGB565 image is on the left. The masked drawing on the right skips
208the magenta background:
209
210![An opaque RGB565 image beside the same image drawn with a transparency mask][mask_fixed]
211"#
212)]
213pub struct MaskFixed<const W: usize, const H: usize, const MASK_N: usize> {
214    /// Row-major visibility bits, least-significant bit first within each byte.
215    pub bits: [u8; MASK_N],
216}
217
218/// Draws a fixed RGB565 image through a matching binary mask.
219///
220/// This image-specific counterpart to
221/// [`Drawable`](https://docs.rs/embedded-graphics/latest/embedded_graphics/trait.Drawable.html)
222/// is implemented for both an
223/// [`Image565Fixed`] and the positioned value returned by
224/// [`Image565Fixed::at`]. The `W` and `H` const arguments require the image and
225/// [`MaskFixed`] dimensions to match at compile time. It is intentionally not
226/// implemented for arbitrary `Drawable` types, whose pixels might not
227/// correspond to a fixed, row-major mask.
228///
229/// Import this trait to make [`draw_masked`](MaskedDrawable::draw_masked)
230/// available. See the visual [`MaskFixed` example](MaskFixed), which draws an
231/// opaque image beside its positioned, masked result.
232pub trait MaskedDrawable<const W: usize, const H: usize>: Drawable<Output = ()> {
233    /// Draws the image, skipping pixels whose corresponding mask bits are clear.
234    ///
235    /// An [`Image565Fixed`] draws at `(0, 0)`. The value returned by
236    /// [`Image565Fixed::at`] draws at the supplied position. See the visual
237    /// [`MaskFixed` example](MaskFixed) for the masked output.
238    fn draw_masked<const MASK_N: usize, D>(
239        &self,
240        mask: &MaskFixed<W, H, MASK_N>,
241        target: &mut D,
242    ) -> Result<(), D::Error>
243    where
244        D: DrawTarget<Color = Self::Color>;
245}
246
247struct PlacedImage565<'a, const W: usize, const H: usize, const N: usize> {
248    image: &'a Image565Fixed<W, H, N>,
249    top_left: Point,
250}
251
252const fn read_u16(bytes: &[u8], offset: usize) -> u16 {
253    bytes[offset] as u16 | ((bytes[offset + 1] as u16) << 8)
254}
255
256const fn parse_header(bytes: &[u8], width: usize, height: usize) -> (usize, usize, bool) {
257    assert!(bytes.len() >= 18, "TGA: file shorter than header");
258    assert!(bytes[1] == 0, "TGA: color maps are not supported");
259    assert!(
260        bytes[2] == 2,
261        "TGA: only uncompressed true-color images are supported"
262    );
263    assert!(
264        bytes[3] == 0 && bytes[4] == 0 && bytes[5] == 0 && bytes[6] == 0 && bytes[7] == 0,
265        "TGA: color map specification is not supported"
266    );
267    assert!(
268        read_u16(bytes, 12) as usize == width,
269        "TGA: width does not match const argument"
270    );
271    assert!(
272        read_u16(bytes, 14) as usize == height,
273        "TGA: height does not match const argument"
274    );
275    assert!(
276        bytes[16] == 24 || bytes[16] == 32,
277        "TGA: only 24-bit BGR or 32-bit BGRA is supported"
278    );
279    assert!(
280        bytes[17] & 0x10 == 0,
281        "TGA: right-to-left origin is not supported"
282    );
283    let bytes_per_pixel = (bytes[16] / 8) as usize;
284    let pixel_start = 18 + bytes[0] as usize;
285    assert!(
286        bytes.len() >= pixel_start + width * height * bytes_per_pixel,
287        "TGA: pixel data is shorter than width * height"
288    );
289    (pixel_start, bytes_per_pixel, bytes[17] & 0x20 != 0)
290}
291
292impl<const W: usize, const H: usize, const N: usize> Image888Fixed<W, H, N> {
293    /// Decodes a supported TGA at compile time, preserving RGB and discarding alpha.
294    ///
295    /// Prefer the public [`tga!`](macro@crate::cyd::display::tga) macro shown in
296    /// its compiled example. This constructor powers that macro when raw TGA
297    /// bytes are already available.
298    ///
299    /// Panics during const evaluation if `N != W * H` or the bytes do not
300    /// contain a supported TGA with matching dimensions.
301    pub const fn from_tga(bytes: &[u8]) -> Self {
302        assert!(N == W * H, "Image888Fixed: N must equal W * H");
303        let (pixel_start, bytes_per_pixel, top_origin) = parse_header(bytes, W, H);
304        let mut pixels = [[0u8; 3]; N];
305        let mut y = 0;
306        while y < H {
307            let mut x = 0;
308            while x < W {
309                let source_y = if top_origin { y } else { H - 1 - y };
310                let offset = pixel_start + (source_y * W + x) * bytes_per_pixel;
311                let red = bytes[offset + 2];
312                let green = bytes[offset + 1];
313                let blue = bytes[offset];
314                pixels[y * W + x] = [red, green, blue];
315                x += 1;
316            }
317            y += 1;
318        }
319        Self { pixels }
320    }
321
322    /// Converts this source image to RGB565.
323    ///
324    /// Each channel is reduced to the RGB565 bit depth. See the
325    /// [`Image565Fixed` example](Image565Fixed).
326    pub const fn to_565(&self) -> Image565Fixed<W, H, N> {
327        let mut pixels = [0u16; N];
328        let mut index = 0;
329        while index < N {
330            let [red, green, blue] = self.pixels[index];
331            pixels[index] =
332                ((red as u16 >> 3) << 11) | ((green as u16 >> 2) << 5) | (blue as u16 >> 3);
333            index += 1;
334        }
335        Image565Fixed { pixels }
336    }
337
338    /// Derives a binary visibility mask using magenta as the transparent color.
339    ///
340    /// Pixels with red and blue at least `200` and green at most `60` become
341    /// transparent; all other pixels remain visible. See the
342    /// [`MaskFixed` example](MaskFixed).
343    ///
344    /// Panics during const evaluation if `MASK_N` does not equal
345    /// [`mask_byte_count(W, H)`](mask_byte_count).
346    pub const fn to_mask_magenta<const MASK_N: usize>(&self) -> MaskFixed<W, H, MASK_N> {
347        assert!(
348            MASK_N == mask_byte_count(W, H),
349            "Mask: MASK_N must match image dimensions"
350        );
351        let mut bits = [0u8; MASK_N];
352        let mut index = 0;
353        while index < N {
354            let pixel = self.pixels[index];
355            let red = pixel[0];
356            let green = pixel[1];
357            let blue = pixel[2];
358            if !(red >= 200 && blue >= 200 && green <= 60) {
359                bits[index / 8] |= 1 << (index % 8);
360            }
361            index += 1;
362        }
363        MaskFixed { bits }
364    }
365}
366
367impl<const W: usize, const H: usize, const N: usize> Image565Fixed<W, H, N> {
368    /// Returns a lightweight value that draws this image at `top_left`.
369    ///
370    /// Drawing the image directly places it at [`Point::zero`]. This method
371    /// changes that drawing position without copying or modifying the image.
372    /// The returned value supports
373    /// [`Drawable::draw`](https://docs.rs/embedded-graphics/latest/embedded_graphics/trait.Drawable.html#tymethod.draw)
374    /// and
375    /// [`MaskedDrawable::draw_masked`]. See the visual
376    /// [`MaskFixed` example](MaskFixed) for the positioned output.
377    pub const fn at(&self, top_left: Point) -> impl MaskedDrawable<W, H, Color = Rgb565> + '_ {
378        PlacedImage565 {
379            image: self,
380            top_left,
381        }
382    }
383
384    /// View the complete image as RGB565 pixels.
385    ///
386    /// This is a zero-copy view over the complete image. See the
387    /// [`Image565View` example](super::Image565View).
388    pub const fn view(&'static self) -> super::Image565View {
389        self.view_rect(Rectangle::new(Point::zero(), Size::new(W as u32, H as u32)))
390    }
391
392    /// View a validated rectangular crop of the image without copying pixels.
393    ///
394    /// `source` uses coordinates in the full image. Coordinates used through
395    /// the returned view are local to that rectangle. See the
396    /// [`Image565View` example](super::Image565View).
397    ///
398    /// Panics if `source` has a negative origin or extends outside the image.
399    pub const fn view_rect(&'static self, source: Rectangle) -> super::Image565View {
400        assert!(
401            source.top_left.x >= 0 && source.top_left.y >= 0,
402            "view_rect: negative origin"
403        );
404        assert!(
405            source.top_left.x as usize + source.size.width as usize <= W
406                && source.top_left.y as usize + source.size.height as usize <= H,
407            "view_rect: rectangle is outside image"
408        );
409        super::Image565View::new_cropped(&self.pixels, W as u32, source)
410    }
411
412    /// Bulk-copies the complete image into a frame with matching dimensions.
413    ///
414    /// This uses [`CydFrame::copy_from_565`] and does not flush the frame.
415    /// Returns [`crate::Error::CopySize`] if the image and frame pixel counts
416    /// differ.
417    ///
418    /// ```rust,no_run
419    /// use device_envoy_core::cyd::display::{CydFrame, Image565Fixed};
420    ///
421    /// fn copy<const W: usize, const H: usize, const N: usize, F: CydFrame>(
422    ///     image: &Image565Fixed<W, H, N>,
423    ///     frame: &mut F,
424    /// ) -> device_envoy_core::Result<()> {
425    ///     image.copy_to(frame)
426    /// }
427    /// ```
428    pub fn copy_to<F: CydFrame>(&self, frame: &mut F) -> crate::Result<()> {
429        frame.copy_from_565(&self.pixels)
430    }
431}
432
433impl<const W: usize, const H: usize, const MASK_N: usize> MaskFixed<W, H, MASK_N> {
434    /// Returns whether the row-major pixel at `index` is visible.
435    ///
436    /// See the [`MaskFixed` example](MaskFixed).
437    pub const fn is_set(&self, index: usize) -> bool {
438        self.bits[index / 8] & (1 << (index % 8)) != 0
439    }
440}
441
442impl<const W: usize, const H: usize, const N: usize> MaskedDrawable<W, H>
443    for PlacedImage565<'_, W, H, N>
444{
445    fn draw_masked<const MASK_N: usize, D>(
446        &self,
447        mask: &MaskFixed<W, H, MASK_N>,
448        target: &mut D,
449    ) -> Result<(), D::Error>
450    where
451        D: DrawTarget<Color = Self::Color>,
452    {
453        let mut pixels = ImagePixels::<W, N> {
454            pixels: &self.image.pixels,
455            top_left: self.top_left,
456            index: 0,
457        };
458        target.draw_iter(core::iter::from_fn(|| {
459            loop {
460                let pixel = pixels.next()?;
461                let index = pixels.index - 1;
462                if mask.is_set(index) {
463                    return Some(pixel);
464                }
465            }
466        }))
467    }
468}
469
470impl<const W: usize, const H: usize, const N: usize> MaskedDrawable<W, H>
471    for Image565Fixed<W, H, N>
472{
473    fn draw_masked<const MASK_N: usize, D>(
474        &self,
475        mask: &MaskFixed<W, H, MASK_N>,
476        target: &mut D,
477    ) -> Result<(), D::Error>
478    where
479        D: DrawTarget<Color = Self::Color>,
480    {
481        self.at(Point::zero()).draw_masked(mask, target)
482    }
483}
484
485struct ImagePixels<'a, const W: usize, const N: usize> {
486    pixels: &'a [u16; N],
487    top_left: Point,
488    index: usize,
489}
490
491impl<const W: usize, const N: usize> Iterator for ImagePixels<'_, W, N> {
492    type Item = Pixel<Rgb565>;
493    fn next(&mut self) -> Option<Self::Item> {
494        if self.index >= N {
495            return None;
496        }
497        let index = self.index;
498        self.index += 1;
499        Some(Pixel(
500            self.top_left + Point::new((index % W) as i32, (index / W) as i32),
501            Rgb565::from(RawU16::new(self.pixels[index])),
502        ))
503    }
504}
505
506impl<const W: usize, const H: usize, const N: usize> Drawable for PlacedImage565<'_, W, H, N> {
507    type Color = Rgb565;
508    type Output = ();
509    /// Draws every image pixel at the display position supplied to
510    /// [`Image565Fixed::at`].
511    fn draw<D>(&self, target: &mut D) -> Result<(), D::Error>
512    where
513        D: DrawTarget<Color = Rgb565>,
514    {
515        target.draw_iter(ImagePixels::<W, N> {
516            pixels: &self.image.pixels,
517            top_left: self.top_left,
518            index: 0,
519        })
520    }
521}
522
523impl<const W: usize, const H: usize, const N: usize> Drawable for Image565Fixed<W, H, N> {
524    type Color = Rgb565;
525    type Output = ();
526    fn draw<D>(&self, target: &mut D) -> Result<(), D::Error>
527    where
528        D: DrawTarget<Color = Rgb565>,
529    {
530        self.at(Point::zero()).draw(target)
531    }
532}
533
534#[doc(hidden)]
535#[macro_export]
536macro_rules! __cyd_tga {
537    ($path:expr) => {
538        $crate::cyd::display::Image888Fixed::from_tga(include_bytes!($path))
539    };
540    ($path:expr, $width:expr, $height:expr) => {
541        $crate::cyd::display::Image888Fixed::<$width, $height, { $width * $height }>::from_tga(
542            include_bytes!($path),
543        )
544    };
545}