1use 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
11pub const fn mask_byte_count(width: usize, height: usize) -> usize {
20 (width * height).div_ceil(8)
21}
22
23pub struct Image888Fixed<const W: usize, const H: usize, const N: usize> {
46 pub pixels: [[u8; 3]; N],
48}
49
50#[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 pub pixels: [u16; N],
132}
133
134#[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 pub bits: [u8; MASK_N],
216}
217
218pub trait MaskedDrawable<const W: usize, const H: usize>: Drawable<Output = ()> {
233 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 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 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 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 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 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 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 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 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 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}