1use std::{convert::TryFrom, marker::PhantomData, path::Path};
2
3use base64::Engine;
4use embedded_graphics::{
5 pixelcolor::{raw::ToBytes, Gray8, Rgb888},
6 prelude::*,
7 primitives::Rectangle,
8};
9use image::{
10 codecs::png::{CompressionType, FilterType, PngEncoder},
11 ImageBuffer, ImageEncoder, Luma, Rgb,
12};
13
14use crate::{display::SimulatorDisplay, output_settings::OutputSettings};
15
16#[derive(Debug, PartialEq, Eq, Clone)]
22pub struct OutputImage<C> {
23 size: Size,
24 pub(crate) data: Box<[u8]>,
25 row_buffer: Vec<u8>,
26
27 color_type: PhantomData<C>,
28}
29
30impl<C> OutputImage<C>
31where
32 C: PixelColor + OutputImageColor + From<Rgb888>,
33 Self: DrawTarget<Color = C, Error = ()>,
34{
35 pub(crate) fn new(size: Size) -> Self {
37 let bytes_per_row = usize::try_from(size.width).unwrap() * C::BYTES_PER_PIXEL;
38 let bytes_total = usize::try_from(size.height).unwrap() * bytes_per_row;
39
40 let data = vec![0; bytes_total].into_boxed_slice();
41 let row_buffer = Vec::with_capacity(bytes_per_row);
42
43 Self {
44 size,
45 data,
46 row_buffer,
47 color_type: PhantomData,
48 }
49 }
50
51 pub fn draw_display<DisplayC>(
53 &mut self,
54 display: &SimulatorDisplay<DisplayC>,
55 position: Point,
56 output_settings: &OutputSettings,
57 ) where
58 DisplayC: PixelColor + Into<Rgb888>,
59 {
60 let display_area = Rectangle::new(position, display.output_size(output_settings));
61 self.fill_solid(
62 &display_area,
63 output_settings.theme.convert(Rgb888::BLACK).into(),
64 )
65 .unwrap();
66
67 if output_settings.scale == 1 {
68 display
69 .bounding_box()
70 .points()
71 .map(|p| {
72 let raw_color = display.get_pixel(p).into();
73 let themed_color = output_settings.theme.convert(raw_color);
74 let output_color = C::from(themed_color);
75
76 Pixel(p + position, output_color)
77 })
78 .draw(self)
79 .unwrap();
80 } else {
81 let pixel_pitch = (output_settings.scale + output_settings.pixel_spacing) as i32;
82 let pixel_size = Size::new(output_settings.scale, output_settings.scale);
83
84 for p in display.bounding_box().points() {
85 let raw_color = display.get_pixel(p).into();
86 let themed_color = output_settings.theme.convert(raw_color);
87 let output_color = C::from(themed_color);
88
89 self.fill_solid(
90 &Rectangle::new(p * pixel_pitch + position, pixel_size),
91 output_color,
92 )
93 .unwrap();
94 }
95 }
96 }
97}
98
99impl<C: OutputImageColor> OutputImage<C> {
100 pub fn save_png<PATH: AsRef<Path>>(&self, path: PATH) -> image::ImageResult<()> {
102 let png = self.encode_png()?;
103
104 std::fs::write(path, png)?;
105
106 Ok(())
107 }
108
109 pub fn to_base64_png(&self) -> image::ImageResult<String> {
111 let png = self.encode_png()?;
112
113 Ok(base64::engine::general_purpose::STANDARD.encode(png))
114 }
115
116 fn encode_png(&self) -> image::ImageResult<Vec<u8>> {
117 let mut png = Vec::new();
118
119 PngEncoder::new_with_quality(&mut png, CompressionType::Best, FilterType::default())
120 .write_image(
121 self.data.as_ref(),
122 self.size.width,
123 self.size.height,
124 C::IMAGE_COLOR_TYPE.into(),
125 )?;
126
127 Ok(png)
128 }
129
130 pub fn as_image_buffer(&self) -> ImageBuffer<C::ImageColor, &[u8]> {
132 ImageBuffer::from_raw(self.size.width, self.size.height, self.data.as_ref()).unwrap()
133 }
134}
135
136impl DrawTarget for OutputImage<Rgb888> {
137 type Color = Rgb888;
138 type Error = ();
139
140 fn draw_iter<I>(&mut self, pixels: I) -> Result<(), Self::Error>
141 where
142 I: IntoIterator<Item = Pixel<Self::Color>>,
143 {
144 for Pixel(p, color) in pixels {
145 if p.x >= 0
146 && p.y >= 0
147 && (p.x as u32) < self.size.width
148 && (p.y as u32) < self.size.height
149 {
150 let bytes = color.to_be_bytes();
151 let (x, y) = (p.x as u32, p.y as u32);
152
153 let start_index = (x + y * self.size.width) as usize * 3;
154 self.data[start_index..start_index + 3].copy_from_slice(bytes.as_ref())
155 }
156 }
157
158 Ok(())
159 }
160
161 fn fill_solid(&mut self, area: &Rectangle, color: Self::Color) -> Result<(), Self::Error> {
162 let area = area.intersection(&self.bounding_box());
163
164 let bytes = color.to_be_bytes();
165 let bytes = bytes.as_ref();
166
167 let large = area.size.width >= 16 && area.size.height >= 16;
171
172 if large {
173 self.row_buffer.clear();
174 for _ in 0..area.size.width {
175 self.row_buffer.extend_from_slice(bytes);
176 }
177 }
178
179 let bytes_per_row = self.size.width as usize * bytes.len();
180 let x_start = area.top_left.x as usize * bytes.len();
181 let x_end = x_start + area.size.width as usize * bytes.len();
182
183 if large {
184 for y in area.rows() {
185 let start = bytes_per_row * y as usize + x_start;
186 self.data[start..start + self.row_buffer.len()].copy_from_slice(&self.row_buffer);
187 }
188 } else {
189 for y in area.rows() {
190 let row_start = bytes_per_row * y as usize;
191 for chunk in
192 self.data[row_start + x_start..row_start + x_end].chunks_exact_mut(bytes.len())
193 {
194 chunk.copy_from_slice(bytes);
195 }
196 }
197 }
198
199 Ok(())
200 }
201}
202
203impl DrawTarget for OutputImage<Gray8> {
204 type Color = Gray8;
205 type Error = ();
206
207 fn draw_iter<I>(&mut self, pixels: I) -> Result<(), Self::Error>
208 where
209 I: IntoIterator<Item = Pixel<Self::Color>>,
210 {
211 for Pixel(p, color) in pixels {
212 if p.x >= 0
213 && p.y >= 0
214 && (p.x as u32) < self.size.width
215 && (p.y as u32) < self.size.height
216 {
217 let (x, y) = (p.x as u32, p.y as u32);
218 let index = (x + y * self.size.width) as usize;
219 self.data[index] = color.into_storage();
220 }
221 }
222
223 Ok(())
224 }
225
226 fn fill_solid(&mut self, area: &Rectangle, color: Self::Color) -> Result<(), Self::Error> {
227 let area = area.intersection(&self.bounding_box());
228
229 let bytes_per_row = self.size.width as usize;
230 let x_start = area.top_left.x as usize;
231 let x_end = x_start + area.size.width as usize;
232
233 for y in area.rows() {
234 let row_start = bytes_per_row * y as usize;
235 self.data[row_start + x_start..row_start + x_end].fill(color.into_storage());
236 }
237
238 Ok(())
239 }
240}
241
242impl<C> OriginDimensions for OutputImage<C> {
243 fn size(&self) -> Size {
244 self.size
245 }
246}
247
248pub trait OutputImageColor {
249 type ImageColor: image::Pixel<Subpixel = u8> + 'static;
250 const IMAGE_COLOR_TYPE: image::ColorType;
251 const BYTES_PER_PIXEL: usize;
252}
253
254impl OutputImageColor for Gray8 {
255 type ImageColor = Luma<u8>;
256 const IMAGE_COLOR_TYPE: image::ColorType = image::ColorType::L8;
257 const BYTES_PER_PIXEL: usize = 1;
258}
259
260impl OutputImageColor for Rgb888 {
261 type ImageColor = Rgb<u8>;
262 const IMAGE_COLOR_TYPE: image::ColorType = image::ColorType::Rgb8;
263 const BYTES_PER_PIXEL: usize = 3;
264}
265
266#[cfg(test)]
267mod tests {
268 use super::*;
269
270 #[test]
271 fn rgb888_default_data() {
272 let image = OutputImage::<Rgb888>::new(Size::new(6, 5));
273 assert_eq!(image.data.as_ref(), &[0u8; 6 * 5 * 3]);
274 }
275
276 #[test]
277 fn rgb888_draw_iter() {
278 let mut image = OutputImage::<Rgb888>::new(Size::new(4, 6));
279
280 [
281 Pixel(Point::new(0, 0), Rgb888::new(0xFF, 0x00, 0x00)),
282 Pixel(Point::new(3, 0), Rgb888::new(0x00, 0xFF, 0x00)),
283 Pixel(Point::new(0, 5), Rgb888::new(0x00, 0x00, 0xFF)),
284 Pixel(Point::new(3, 5), Rgb888::new(0x12, 0x34, 0x56)),
285 Pixel(Point::new(-1, -1), Rgb888::new(0xFF, 0xFF, 0xFF)),
287 Pixel(Point::new(0, 10), Rgb888::new(0xFF, 0xFF, 0xFF)),
288 Pixel(Point::new(10, 0), Rgb888::new(0xFF, 0xFF, 0xFF)),
289 ]
290 .into_iter()
291 .draw(&mut image)
292 .unwrap();
293
294 assert_eq!(
295 image.data.as_ref(),
296 &[
297 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x12, 0x34, 0x56, ]
304 );
305 }
306
307 #[test]
308 fn rgb888_fill_solid() {
309 let mut image = OutputImage::<Rgb888>::new(Size::new(4, 6));
310
311 image
312 .fill_solid(
313 &Rectangle::new(Point::new(2, 3), Size::new(10, 20)),
314 Rgb888::new(0x01, 0x02, 0x03),
315 )
316 .unwrap();
317
318 assert_eq!(
319 image.data.as_ref(),
320 &[
321 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x01, 0x02, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x01, 0x02, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x01, 0x02, 0x03, ]
328 );
329 }
330
331 #[test]
332 fn gray8_default_data() {
333 let image = OutputImage::<Gray8>::new(Size::new(6, 5));
334 assert_eq!(image.data.as_ref(), &[0u8; 6 * 5]);
335 }
336
337 #[test]
338 fn gray8_draw_iter() {
339 let mut image = OutputImage::<Gray8>::new(Size::new(12, 6));
340
341 [
342 Pixel(Point::new(0, 0), Gray8::new(0x01)),
343 Pixel(Point::new(11, 0), Gray8::new(0x02)),
344 Pixel(Point::new(0, 5), Gray8::new(0x03)),
345 Pixel(Point::new(11, 5), Gray8::new(0x04)),
346 Pixel(Point::new(-1, -1), Gray8::new(0xFF)),
348 Pixel(Point::new(0, 10), Gray8::new(0xFF)),
349 Pixel(Point::new(12, 0), Gray8::new(0xFF)),
350 ]
351 .into_iter()
352 .draw(&mut image)
353 .unwrap();
354
355 assert_eq!(
356 image.data.as_ref(),
357 &[
358 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, ]
365 );
366 }
367
368 #[test]
369 fn gray8_fill_solid() {
370 let mut image = OutputImage::<Gray8>::new(Size::new(4, 6));
371
372 image
373 .fill_solid(
374 &Rectangle::new(Point::new(2, 3), Size::new(10, 20)),
375 Gray8::WHITE,
376 )
377 .unwrap();
378
379 assert_eq!(
380 image.data.as_ref(),
381 &[
382 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0xFF, 0xFF, ]
389 );
390 }
391}