1use ab_glyph::{FontRef, PxScale};
2use elgato_streamdeck::{AsyncStreamDeck, StreamDeckError};
3use image::GenericImage;
4use image::{DynamicImage, Rgba};
5use imageproc::drawing::{draw_text_mut, text_size};
6use resvg::tiny_skia::{Color, Pixmap, PremultipliedColorU8, Transform};
7use resvg::usvg::{self, Tree};
8use std::error::Error;
9
10use super::types::{Button, RenderConfig};
11
12pub fn render_button(
14 button: &Button,
15 config: &RenderConfig,
16) -> Result<DynamicImage, Box<dyn std::error::Error>> {
17 match button {
18 Button::Icon {
19 svg_data,
20 background,
21 foreground,
22 } => render_svg(svg_data.as_bytes(), config, *background, *foreground),
23 Button::IconWithText {
24 svg_data,
25 text,
26 foreground,
27 background,
28 } => render_svg_with_text(svg_data.as_bytes(), text, *foreground, *background, config),
29 Button::CustomImage { image } => Ok(image.clone()),
30 Button::Gradient {
31 start_color,
32 end_color,
33 } => render_gradient(*start_color, *end_color, config),
34 Button::Text {
35 text,
36 foreground,
37 background,
38 } => render_text(text, *foreground, *background, config),
39 }
40}
41
42pub async fn set_button(
44 deck: &AsyncStreamDeck,
45 index: u8,
46 button: &Button,
47 config: &RenderConfig,
48) -> Result<(), StreamDeckError> {
49 let image = render_button(button, config).map_err(|e| {
50 eprintln!("Error rendering button: {}", e);
51 StreamDeckError::BadData
52 })?;
53 deck.set_button_image(index, image).await?;
54 Ok(())
55}
56
57fn render_text(
59 text: &str,
60 foreground: Color,
61 background: Color,
62 config: &RenderConfig,
63) -> Result<DynamicImage, Box<dyn Error>> {
64 let mut image = DynamicImage::new_rgba8(config.width, config.height);
65
66 let background = Rgba::<u8>([
67 (background.red() * 255.0) as u8,
68 (background.green() * 255.0) as u8,
69 (background.blue() * 255.0) as u8,
70 255,
71 ]);
72 let foreground = Rgba::<u8>([
73 (foreground.red() * 255.0) as u8,
74 (foreground.green() * 255.0) as u8,
75 (foreground.blue() * 255.0) as u8,
76 255,
77 ]);
78 for x in 0..config.width {
79 for y in 0..config.height {
80 unsafe {
81 image.unsafe_put_pixel(x, y, background);
82 }
83 }
84 }
85
86 let font = FontRef::try_from_slice(config.font_data).map_err(|_| "Failed to load font")?;
87 let scale = PxScale::from(config.font_scale);
88 let text_size = text_size(scale, &font, text);
89
90 draw_text_mut(
91 &mut image,
92 foreground,
93 ((config.width as i32 - text_size.0 as i32) / 2) as i32,
94 (config.height as i32 - text_size.1 as i32 - 6) as i32,
95 scale,
96 &font,
97 text,
98 );
99
100 Ok(image)
101}
102
103fn render_svg(
104 svg_data: &[u8],
105 config: &RenderConfig,
106 background: Color,
107 foreground: Color,
108) -> Result<DynamicImage, Box<dyn std::error::Error>> {
109 let opt = usvg::Options::default();
110 let tree = Tree::from_data(svg_data, &opt)?;
111 let mut raw_pixmap =
112 Pixmap::new(config.width, config.height).ok_or("Failed to create pixmap")?;
113 raw_pixmap.fill(Color::from_rgba8(0, 0, 0, 0));
114
115 let scale = config.width as f32 / 40.0;
116 let transform = Transform::from_scale(scale, scale).pre_translate(8.0, 6.0);
117 resvg::render(&tree, transform, &mut raw_pixmap.as_mut());
118
119 let mut pixmap = Pixmap::new(config.width, config.height).ok_or("Failed to create pixmap")?;
120 pixmap.fill(background);
121 let pixels_mut = pixmap.pixels_mut();
122 for x in 0..config.width {
123 for y in 0..config.height {
124 let pixel = raw_pixmap.pixel(x, y).ok_or("Failed to get pixel")?;
125 if pixel.alpha() > 0 {
126 let foreground_red = (foreground.red() * pixel.alpha() as f32) / 255.0;
127 let foreground_green = (foreground.green() * pixel.alpha() as f32) / 255.0;
128 let foreground_blue = (foreground.blue() * pixel.alpha() as f32) / 255.0;
129 let background_red = background.red() * (1.0 - pixel.alpha() as f32 / 255.0);
130 let background_green = background.green() * (1.0 - pixel.alpha() as f32 / 255.0);
131 let background_blue = background.blue() * (1.0 - pixel.alpha() as f32 / 255.0);
132 let final_red = foreground_red + background_red;
133 let final_green = foreground_green + background_green;
134 let final_blue = foreground_blue + background_blue;
135 pixels_mut[x as usize + y as usize * config.width as usize] =
136 PremultipliedColorU8::from_rgba(
137 (final_red * 255.0) as u8,
138 (final_green * 255.0) as u8,
139 (final_blue * 255.0) as u8,
140 255,
141 )
142 .ok_or(format!(
143 "Failed to create premultiplied color: {}, {}, {}, {}",
144 foreground.red(),
145 foreground.green(),
146 foreground.blue(),
147 pixel.alpha()
148 ))?;
149 }
150 }
151 }
152
153 let image_rgba8 = DynamicImage::ImageRgba8(
154 image::RgbaImage::from_raw(config.width, config.height, pixmap.take())
155 .ok_or("Failed to create RGBA image")?,
156 );
157
158 Ok(image_rgba8)
159}
160
161fn render_svg_with_text(
162 svg_data: &[u8],
163 text: &str,
164 foreground: Color,
165 background: Color,
166 config: &RenderConfig,
167) -> Result<DynamicImage, Box<dyn std::error::Error>> {
168 let mut img = render_svg(svg_data, config, background, foreground)?;
169
170 let font = FontRef::try_from_slice(config.font_data).map_err(|_| "Failed to load font")?;
171 let scale = PxScale::from(config.font_scale);
172 let text_size = text_size(scale, &font, text);
173
174 draw_text_mut(
175 &mut img,
176 Rgba([
177 (foreground.red() * 255.0) as u8,
178 (foreground.green() * 255.0) as u8,
179 (foreground.blue() * 255.0) as u8,
180 255,
181 ]),
182 ((config.width as i32 - text_size.0 as i32) / 2) as i32,
183 (config.height as i32 - text_size.1 as i32 - 6) as i32,
184 scale,
185 &font,
186 text,
187 );
188
189 Ok(img)
190}
191
192fn render_gradient(
193 start_color: Rgba<u8>,
194 end_color: Rgba<u8>,
195 config: &RenderConfig,
196) -> Result<DynamicImage, Box<dyn std::error::Error>> {
197 let mut image = DynamicImage::new_rgba8(config.width, config.height);
198
199 for y in 0..config.height {
200 for x in 0..config.width {
201 let r = interpolate(start_color[0], end_color[0], x, y, config);
202 let g = interpolate(start_color[1], end_color[1], x, y, config);
203 let b = interpolate(start_color[2], end_color[2], x, y, config);
204 image.put_pixel(x, y, Rgba([r, g, b, 255]));
205 }
206 }
207 Ok(image)
208}
209
210fn interpolate(start: u8, end: u8, x: u32, y: u32, config: &RenderConfig) -> u8 {
211 let t = (x as f32 / config.width as f32 + y as f32 / config.height as f32) / 2.0;
212 (start as f32 * (1.0 - t) + end as f32 * t) as u8
213}