embedded_graphics_simulator/
output_image.rs

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/// Output image.
17///
18/// An output image is the result of applying [`OutputSettings`] to a [`SimulatorDisplay`]. It can
19/// be used to save a simulator display to a PNG file.
20///
21#[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    /// Creates a new output image.
36    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    /// Draws a display using the given position and output setting.
52    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    /// Saves the image content to a PNG file.
101    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    /// Returns the image as a base64 encoded PNG.
110    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    /// Returns the output image as an [`image`] crate [`ImageBuffer`].
131    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        // For large areas it's more efficient to prepare a row buffer and copy
168        // the entire row at one.
169        // TODO: the bounds were chosen arbitrarily and might not be optimal
170        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            // out of bounds pixels should be ignored
286            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, //
298                0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //
299                0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //
300                0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //
301                0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //
302                0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x12, 0x34, 0x56, //
303            ]
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, //
322                0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //
323                0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //
324                0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x01, 0x02, 0x03, //
325                0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x01, 0x02, 0x03, //
326                0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x01, 0x02, 0x03, //
327            ]
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            // out of bounds pixels should be ignored
347            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, //
359                0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //
360                0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //
361                0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //
362                0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //
363                0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, //
364            ]
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, //
383                0x00, 0x00, 0x00, 0x00, //
384                0x00, 0x00, 0x00, 0x00, //
385                0x00, 0x00, 0xFF, 0xFF, //
386                0x00, 0x00, 0xFF, 0xFF, //
387                0x00, 0x00, 0xFF, 0xFF, //
388            ]
389        );
390    }
391}