From 3969c91c043c88c90adb4e84d22fc1eb8f43dfd5 Mon Sep 17 00:00:00 2001 From: Ralf Fuest Date: Mon, 3 May 2021 20:58:04 +0200 Subject: [PATCH 1/2] Add features from png-target --- Cargo.toml | 1 + examples/png-base64.rs | 34 +++++++++++ examples/png-file.rs | 34 +++++++++++ examples/themes.rs | 40 +++++++++++++ src/display.rs | 123 +++++++++++++++++++++++++++++--------- src/framebuffer.rs | 110 ---------------------------------- src/lib.rs | 3 +- src/output_image.rs | 132 +++++++++++++++++++++++++++++++++++++++++ src/theme.rs | 6 ++ src/window.rs | 18 +++--- 10 files changed, 353 insertions(+), 148 deletions(-) create mode 100644 examples/png-base64.rs create mode 100644 examples/png-file.rs create mode 100644 examples/themes.rs delete mode 100644 src/framebuffer.rs create mode 100644 src/output_image.rs diff --git a/Cargo.toml b/Cargo.toml index 0528439..f8120a0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,6 +21,7 @@ circle-ci = { repository = "embedded-graphics/simulator", branch = "master" } [dependencies] image = "0.23.0" +base64 = "0.13.0" [dependencies.sdl2] version = "0.32.2" diff --git a/examples/png-base64.rs b/examples/png-base64.rs new file mode 100644 index 0000000..6c8c9f6 --- /dev/null +++ b/examples/png-base64.rs @@ -0,0 +1,34 @@ +use embedded_graphics::{ + mono_font::{ascii::FONT_10X20, MonoTextStyle}, + pixelcolor::BinaryColor, + prelude::*, + text::{Alignment, Baseline, Text, TextStyleBuilder}, +}; +use embedded_graphics_simulator::{OutputSettingsBuilder, SimulatorDisplay}; + +fn main() { + let mut display = SimulatorDisplay::::new(Size::new(256, 64)); + + let large_text = MonoTextStyle::new(&FONT_10X20, BinaryColor::On); + let centered = TextStyleBuilder::new() + .baseline(Baseline::Middle) + .alignment(Alignment::Center) + .build(); + + Text::with_text_style( + "embedded-graphics", + display.bounding_box().center(), + large_text, + centered, + ) + .draw(&mut display) + .unwrap(); + + let output_settings = OutputSettingsBuilder::new().scale(2).build(); + let output_image = display.to_grayscale_output_image(&output_settings); + + println!( + "", + output_image.to_base64_png() + ); +} diff --git a/examples/png-file.rs b/examples/png-file.rs new file mode 100644 index 0000000..3116624 --- /dev/null +++ b/examples/png-file.rs @@ -0,0 +1,34 @@ +use embedded_graphics::{ + mono_font::{ascii::FONT_10X20, MonoTextStyle}, + pixelcolor::BinaryColor, + prelude::*, + text::{Alignment, Baseline, Text, TextStyleBuilder}, +}; +use embedded_graphics_simulator::{OutputSettingsBuilder, SimulatorDisplay}; + +fn main() { + let mut display = SimulatorDisplay::::new(Size::new(256, 64)); + + let large_text = MonoTextStyle::new(&FONT_10X20, BinaryColor::On); + let centered = TextStyleBuilder::new() + .baseline(Baseline::Middle) + .alignment(Alignment::Center) + .build(); + + Text::with_text_style( + "embedded-graphics", + display.bounding_box().center(), + large_text, + centered, + ) + .draw(&mut display) + .unwrap(); + + let output_settings = OutputSettingsBuilder::new().scale(2).build(); + let output_image = display.to_rgb_output_image(&output_settings); + + let path = std::env::args_os() + .nth(1) + .expect("expected PNG file name argument"); + output_image.save_png(path).unwrap(); +} diff --git a/examples/themes.rs b/examples/themes.rs new file mode 100644 index 0000000..a1c1f0e --- /dev/null +++ b/examples/themes.rs @@ -0,0 +1,40 @@ +use embedded_graphics::{ + mono_font::{ascii::FONT_10X20, MonoTextStyle}, + pixelcolor::BinaryColor, + prelude::*, + text::{Alignment, Baseline, Text, TextStyleBuilder}, +}; +use embedded_graphics_simulator::{ + BinaryColorTheme, OutputSettingsBuilder, SimulatorDisplay, Window, +}; + +fn main() { + let mut display = SimulatorDisplay::::new(Size::new(256, 64)); + + let large_text = MonoTextStyle::new(&FONT_10X20, BinaryColor::On); + let centered = TextStyleBuilder::new() + .baseline(Baseline::Middle) + .alignment(Alignment::Center) + .build(); + + Text::with_text_style( + "embedded-graphics", + display.bounding_box().center(), + large_text, + centered, + ) + .draw(&mut display) + .unwrap(); + + // Uncomment one of the `theme` lines to use a different theme. + let output_settings = OutputSettingsBuilder::new() + //.theme(BinaryColorTheme::LcdGreen) + //.theme(BinaryColorTheme::LcdWhite) + .theme(BinaryColorTheme::LcdBlue) + //.theme(BinaryColorTheme::OledBlue) + //.theme(BinaryColorTheme::OledWhite) + .build(); + + let mut window = Window::new("Themes", &output_settings); + window.show_static(&display); +} diff --git a/src/display.rs b/src/display.rs index 6564cbb..212eaa8 100644 --- a/src/display.rs +++ b/src/display.rs @@ -1,10 +1,11 @@ -use crate::{framebuffer::Framebuffer, output_settings::OutputSettings}; +use std::convert::TryFrom; + use embedded_graphics::{ - pixelcolor::{BinaryColor, Rgb888}, + pixelcolor::{BinaryColor, Gray8, Rgb888}, prelude::*, }; -use image::{ImageBuffer, Rgb}; -use std::convert::TryFrom; + +use crate::{output_image::OutputImage, output_settings::OutputSettings}; /// Simulator display. pub struct SimulatorDisplay { @@ -12,10 +13,7 @@ pub struct SimulatorDisplay { pixels: Box<[C]>, } -impl SimulatorDisplay -where - C: PixelColor, -{ +impl SimulatorDisplay { /// Creates a new display filled with a color. /// /// This constructor can be used if `C` doesn't implement `From` or another @@ -65,7 +63,7 @@ impl SimulatorDisplay where C: PixelColor + Into, { - /// Converts the display contents into an image.rs `ImageBuffer`. + /// Converts the display contents into a RGB output image. /// /// # Examples /// @@ -75,30 +73,55 @@ where /// /// let output_settings = OutputSettingsBuilder::new().scale(2).build(); /// - /// let display: SimulatorDisplay = SimulatorDisplay::new(Size::new(128, 64)); + /// let display = SimulatorDisplay::::new(Size::new(128, 64)); /// /// // draw something to the display /// - /// let image_buffer = display.to_image_buffer(&output_settings); - /// assert_eq!(image_buffer.width(), 256); - /// assert_eq!(image_buffer.height(), 128); + /// let output_image = display.to_rgb_output_image(&output_settings); + /// assert_eq!(output_image.size(), Size::new(256, 128)); /// - /// // use image buffer - /// // example: image_buffer.save + /// // use output image: + /// // example: output_image.save_png("out.png")?; /// ``` - pub fn to_image_buffer( + pub fn to_rgb_output_image(&self, output_settings: &OutputSettings) -> OutputImage { + let mut output = OutputImage::new(self, output_settings); + output.update(self); + + output + } + + /// Converts the display contents into a grayscale output image. + /// + /// # Examples + /// + /// ```rust + /// use embedded_graphics::{pixelcolor::Gray8, prelude::*}; + /// use embedded_graphics_simulator::{OutputSettingsBuilder, SimulatorDisplay}; + /// + /// let output_settings = OutputSettingsBuilder::new().scale(2).build(); + /// + /// let display = SimulatorDisplay::::new(Size::new(128, 64)); + /// + /// // draw something to the display + /// + /// let output_image = display.to_grayscale_output_image(&output_settings); + /// assert_eq!(output_image.size(), Size::new(256, 128)); + /// + /// // use output image: + /// // example: output_image.save_png("out.png")?; + /// ``` + pub fn to_grayscale_output_image( &self, output_settings: &OutputSettings, - ) -> ImageBuffer, Box<[u8]>> { - let framebuffer = Framebuffer::new(self, output_settings); - framebuffer.into_image_buffer() + ) -> OutputImage { + let mut output = OutputImage::new(self, output_settings); + output.update(self); + + output } } -impl DrawTarget for SimulatorDisplay -where - C: PixelColor, -{ +impl DrawTarget for SimulatorDisplay { type Color = C; type Error = core::convert::Infallible; @@ -116,11 +139,57 @@ where } } -impl OriginDimensions for SimulatorDisplay -where - C: PixelColor, -{ +impl OriginDimensions for SimulatorDisplay { fn size(&self) -> Size { self.size } } + +#[cfg(test)] +mod tests { + use embedded_graphics::primitives::{Line, PrimitiveStyle}; + + use super::*; + + #[test] + fn rgb_output_image() { + let mut display = SimulatorDisplay::::new(Size::new(2, 4)); + + Line::new(Point::new(0, 0), Point::new(1, 3)) + .into_styled(PrimitiveStyle::with_stroke(BinaryColor::On, 1)) + .draw(&mut display) + .unwrap(); + + let image = display.to_rgb_output_image(&OutputSettings::default()); + assert_eq!(image.size(), display.size()); + + let expected: &[u8] = &[ + 255, 255, 255, 0, 0, 0, // + 255, 255, 255, 0, 0, 0, // + 0, 0, 0, 255, 255, 255, // + 0, 0, 0, 255, 255, 255, // + ]; + assert_eq!(image.data.as_ref(), expected); + } + + #[test] + fn grayscale_image_buffer() { + let mut display = SimulatorDisplay::::new(Size::new(2, 4)); + + Line::new(Point::new(0, 0), Point::new(1, 3)) + .into_styled(PrimitiveStyle::with_stroke(BinaryColor::On, 1)) + .draw(&mut display) + .unwrap(); + + let image = display.to_grayscale_output_image(&OutputSettings::default()); + assert_eq!(image.size(), display.size()); + + let expected: &[u8] = &[ + 255, 0, // + 255, 0, // + 0, 255, // + 0, 255, // + ]; + assert_eq!(image.data.as_ref(), expected); + } +} diff --git a/src/framebuffer.rs b/src/framebuffer.rs deleted file mode 100644 index f4ab624..0000000 --- a/src/framebuffer.rs +++ /dev/null @@ -1,110 +0,0 @@ -use crate::{display::SimulatorDisplay, output_settings::OutputSettings}; -use embedded_graphics::{ - pixelcolor::{Rgb888, RgbColor}, - prelude::*, - primitives::{Primitive, PrimitiveStyle, Rectangle}, -}; -use image::{ImageBuffer, Rgb}; -use std::convert::TryFrom; - -/// Rgb888 framebuffer -#[derive(Debug, PartialEq, Eq, Clone)] -pub struct Framebuffer { - size: Size, - pub(crate) data: Box<[u8]>, - pub(crate) output_settings: OutputSettings, -} - -impl Framebuffer { - /// Creates a new framebuffer filled with `background_color`. - pub fn new(display: &SimulatorDisplay, output_settings: &OutputSettings) -> Self - where - C: PixelColor + Into, - { - let size = output_settings.framebuffer_size(display); - - // Create an empty pixel buffer. - let pixel_count = size.width as usize * size.height as usize; - let data = vec![0; pixel_count * 3].into_boxed_slice(); - - let mut framebuffer = Self { - size, - data, - output_settings: output_settings.clone(), - }; - - // Fill pixel buffer with background color. - let background_color = output_settings.theme.convert(Rgb888::BLACK); - framebuffer.clear(background_color).unwrap(); - - // Update buffer. - framebuffer.update(display); - - framebuffer - } - - /// Updates the framebuffer from a `SimulatorDisplay`. - pub fn update(&mut self, display: &SimulatorDisplay) - where - C: PixelColor + Into, - { - let Size { width, height } = display.size(); - - let pixel_pitch = (self.output_settings.scale + self.output_settings.pixel_spacing) as i32; - let pixel_size = Size::new(self.output_settings.scale, self.output_settings.scale); - - for y in 0..height as i32 { - for x in 0..width as i32 { - let color = display.get_pixel(Point::new(x, y)).into(); - let p = Point::new(x * pixel_pitch, y * pixel_pitch); - - Rectangle::new(p, pixel_size) - .into_styled(PrimitiveStyle::with_fill( - self.output_settings.theme.convert(color), - )) - .draw(self) - .ok(); - } - } - } - - fn get_pixel_mut(&mut self, point: Point) -> Option<&mut [u8]> { - if let Ok((x, y)) = <(u32, u32)>::try_from(point) { - if x < self.size.width && y < self.size.height { - let start_index = (x + y * self.size.width) as usize * 3; - return self.data.get_mut(start_index..start_index + 3); - } - } - - None - } - - /// Converts the framebuffer into an image.rs `ImageBuffer`. - pub fn into_image_buffer(self) -> ImageBuffer, Box<[u8]>> { - ImageBuffer::from_raw(self.size.width, self.size.height, self.data).unwrap() - } -} - -impl DrawTarget for Framebuffer { - type Color = Rgb888; - type Error = core::convert::Infallible; - - fn draw_iter(&mut self, pixels: I) -> Result<(), Self::Error> - where - I: IntoIterator>, - { - for Pixel(point, color) in pixels.into_iter() { - if let Some(pixel) = self.get_pixel_mut(point) { - pixel.copy_from_slice(&color.to_be_bytes()) - } - } - - Ok(()) - } -} - -impl OriginDimensions for Framebuffer { - fn size(&self) -> Size { - self.size - } -} diff --git a/src/lib.rs b/src/lib.rs index a7e9982..bd58dcb 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -117,7 +117,7 @@ #![deny(missing_docs)] mod display; -mod framebuffer; +mod output_image; mod output_settings; mod theme; @@ -143,6 +143,7 @@ pub mod sdl2 { pub use crate::{ display::SimulatorDisplay, + output_image::OutputImage, output_settings::{OutputSettings, OutputSettingsBuilder}, theme::BinaryColorTheme, }; diff --git a/src/output_image.rs b/src/output_image.rs new file mode 100644 index 0000000..978d686 --- /dev/null +++ b/src/output_image.rs @@ -0,0 +1,132 @@ +use std::{convert::TryFrom, marker::PhantomData, path::Path}; + +use embedded_graphics::{ + pixelcolor::{raw::ToBytes, Gray8, Rgb888, RgbColor}, + prelude::*, + primitives::Rectangle, +}; +use image::{png::PngEncoder, ImageBuffer, Luma, Pixel as _, Rgb}; + +use crate::{display::SimulatorDisplay, output_settings::OutputSettings}; + +/// Output image. +/// +/// An output image is the result of applying [`OutputSettings`] to a [`SimulatorDisplay`]. It can +/// be used to save a simulator display to a PNG file. +/// +/// [`OutputSettings`]: struct.OutputSettings.html +/// [`SimulatorDisplay`]: struct.SimulatorDisplay.html +#[derive(Debug, PartialEq, Eq, Clone)] +pub struct OutputImage { + size: Size, + pub(crate) data: Box<[u8]>, + pub(crate) output_settings: OutputSettings, + color_type: PhantomData, +} + +impl OutputImage +where + C: PixelColor + From + ToBytes, + ::Bytes: AsRef<[u8]>, +{ + /// Creates a new output image. + pub(crate) fn new( + display: &SimulatorDisplay, + output_settings: &OutputSettings, + ) -> Self + where + DisplayC: PixelColor + Into, + { + let size = output_settings.framebuffer_size(display); + + // Create an empty pixel buffer, filled with the background color. + let background_color = C::from(output_settings.theme.convert(Rgb888::BLACK)).to_be_bytes(); + let data = background_color + .as_ref() + .iter() + .copied() + .cycle() + .take(size.width as usize * size.height as usize * background_color.as_ref().len()) + .collect::>() + .into_boxed_slice(); + + Self { + size, + data, + output_settings: output_settings.clone(), + color_type: PhantomData, + } + } + + /// Updates the image from a `SimulatorDisplay`. + pub fn update(&mut self, display: &SimulatorDisplay) + where + DisplayC: PixelColor + Into, + { + let pixel_pitch = (self.output_settings.scale + self.output_settings.pixel_spacing) as i32; + let pixel_size = Size::new(self.output_settings.scale, self.output_settings.scale); + + for p in display.bounding_box().points() { + let raw_color = display.get_pixel(p).into(); + let themed_color = self.output_settings.theme.convert(raw_color); + let output_color = C::from(themed_color).to_be_bytes(); + let output_color = output_color.as_ref(); + + for p in Rectangle::new(p * pixel_pitch, pixel_size).points() { + if let Ok((x, y)) = <(u32, u32)>::try_from(p) { + let start_index = (x + y * self.size.width) as usize * output_color.len(); + + self.data[start_index..start_index + output_color.len()] + .copy_from_slice(output_color) + } + } + } + } +} + +impl OutputImage { + /// Saves the image content to a PNG file. + pub fn save_png>(&self, path: PATH) -> image::ImageResult<()> { + self.as_image_buffer() + .save_with_format(path, image::ImageFormat::Png) + } + + /// Returns the image as a base64 encoded PNG. + pub fn to_base64_png(&self) -> String { + let mut png = Vec::new(); + + PngEncoder::new(&mut png) + .encode( + self.data.as_ref(), + self.size.width, + self.size.height, + C::ImageColor::COLOR_TYPE, + ) + .unwrap(); + + base64::encode(&png) + } + + /// Returns the output image as an `image` crate `ImageBuffer`. + pub fn as_image_buffer(&self) -> ImageBuffer { + ImageBuffer::from_raw(self.size.width, self.size.height, self.data.as_ref()).unwrap() + } +} + +impl OriginDimensions for OutputImage { + fn size(&self) -> Size { + self.size + } +} + +pub trait OutputImageColor { + type ImageColor: image::Pixel + 'static; +} + +impl OutputImageColor for Gray8 { + type ImageColor = Luma; +} + +impl OutputImageColor for Rgb888 { + type ImageColor = Rgb; +} diff --git a/src/theme.rs b/src/theme.rs index 8c91d7f..54adf47 100644 --- a/src/theme.rs +++ b/src/theme.rs @@ -6,6 +6,9 @@ pub enum BinaryColorTheme { /// A simple on/off, non-styled display with black background and white pixels Default, + /// Inverted colors. + Inverted, + /// An on/off classic LCD-like display with white background LcdWhite, @@ -34,6 +37,9 @@ impl BinaryColorTheme { pub(crate) fn convert(self, color: Rgb888) -> Rgb888 { match self { BinaryColorTheme::Default => color, + BinaryColorTheme::Inverted => { + Rgb888::new(255 - color.r(), 255 - color.g(), 255 - color.b()) + } BinaryColorTheme::LcdWhite => { map_color(color, Rgb888::new(245, 245, 245), Rgb888::new(32, 32, 32)) } diff --git a/src/window.rs b/src/window.rs index 564917e..d0d87a0 100644 --- a/src/window.rs +++ b/src/window.rs @@ -1,4 +1,6 @@ -use crate::{display::SimulatorDisplay, framebuffer::Framebuffer, output_settings::OutputSettings}; +use crate::{ + display::SimulatorDisplay, output_image::OutputImage, output_settings::OutputSettings, +}; use embedded_graphics::{pixelcolor::Rgb888, prelude::*}; use sdl2::{ event::Event, @@ -60,9 +62,8 @@ pub enum SimulatorEvent { } /// Simulator window -#[allow(dead_code)] pub struct Window { - framebuffer: Option, + framebuffer: Option>, sdl_window: Option, title: String, output_settings: OutputSettings, @@ -86,14 +87,14 @@ impl Window { { if let Ok(path) = std::env::var("EG_SIMULATOR_DUMP") { display - .to_image_buffer(&self.output_settings) - .save(path) + .to_rgb_output_image(&self.output_settings) + .save_png(path) .unwrap(); std::process::exit(0); } if self.framebuffer.is_none() { - self.framebuffer = Some(Framebuffer::new(display, &self.output_settings)); + self.framebuffer = Some(OutputImage::new(display, &self.output_settings)); } if self.sdl_window.is_none() { @@ -138,14 +139,12 @@ impl Window { } } -#[allow(dead_code)] struct SdlWindow { canvas: render::Canvas, event_pump: sdl2::EventPump, } impl SdlWindow { - #[allow(dead_code)] pub fn new( display: &SimulatorDisplay, title: &str, @@ -171,8 +170,7 @@ impl SdlWindow { Self { canvas, event_pump } } - #[allow(dead_code)] - pub fn update(&mut self, framebuffer: &Framebuffer) { + pub fn update(&mut self, framebuffer: &OutputImage) { let Size { width, height } = framebuffer.size(); let texture_creator = self.canvas.texture_creator(); From ce70b254f6482dde477fa5a8e0d1187924e38605 Mon Sep 17 00:00:00 2001 From: Ralf Fuest Date: Mon, 3 May 2021 21:09:52 +0200 Subject: [PATCH 2/2] Update changelog --- CHANGELOG.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4cc760b..1fe25b9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,12 +6,25 @@ ## [Unreleased] - ReleaseDate +### Added + +- [#25](https://github.com/embedded-graphics/simulator/pull/25) Added `OutputImage` to export PNG files and base64 encoded PNGs. +- [#25](https://github.com/embedded-graphics/simulator/pull/25) Added `BinaryColorTheme::Inverted`. + +### Changed + +- **(breaking)** [#25](https://github.com/embedded-graphics/simulator/pull/25) Removed `SimulatorDisplay::to_image_buffer`. Use `to_rgb_output_image` or `to_grayscale_output_image` instead. + ## [0.3.0-beta.1] - 2021-04-24 +### Changed + - [#24](https://github.com/embedded-graphics/simulator/pull/24) Upgrade to embedded-graphics 0.7.0-beta.1. ## [0.3.0-alpha.2] - 2021-02-05 +### Added + - [#16](https://github.com/embedded-graphics/simulator/pull/16) Re-export `sdl2` types. ## [0.3.0-alpha.1] - 2021-01-07