streamdeck_oxide/button/
render.rs

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
12/// Renders a button to a DynamicImage
13pub 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
42/// Sets a button on the Stream Deck at the specified index
43pub 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
57// Helper functions for rendering different button types
58fn 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}