streamduck_core/thread/
util.rs

1use serde::{Serialize, Deserialize};
2use strum_macros::{EnumVariantNames, EnumString, Display};
3use image::{DynamicImage, GenericImage, GenericImageView, Rgba};
4use image::imageops::{FilterType, horizontal_gradient, vertical_gradient};
5use rusttype::{Font, Point, point, Scale};
6
7/// Resizes image to specified size
8pub fn resize_for_streamdeck(size: (usize, usize), image: DynamicImage) -> DynamicImage {
9    let (sx, sy) = size;
10    DynamicImage::from(image.to_rgba8()).resize_to_fill(sx as u32, sy as u32, FilterType::Lanczos3)
11}
12
13/// Generates solid color image of specified size
14pub fn image_from_solid(size: (usize, usize), color: Rgba<u8>) -> DynamicImage {
15    let (sx, sy) = size;
16    let mut image = DynamicImage::new_rgba8(sx as u32, sy as u32);
17
18    horizontal_gradient(&mut image, &color, &color);
19
20    image
21}
22
23/// Generates horizontal gradient image of specified size
24pub fn image_from_horiz_gradient(size: (usize, usize), start: Rgba<u8>, end: Rgba<u8>) -> DynamicImage {
25    let (sx, sy) = size;
26    let mut image = DynamicImage::new_rgba8(sx as u32, sy as u32);
27
28    horizontal_gradient(&mut image, &start, &end);
29
30    image
31}
32
33/// Generates vertical gradient image of specified size
34pub fn image_from_vert_gradient(size: (usize, usize), start: Rgba<u8>, end: Rgba<u8>) -> DynamicImage {
35    let (sx, sy) = size;
36    let mut image = DynamicImage::new_rgba8(sx as u32, sy as u32);
37
38    vertical_gradient(&mut image, &start, &end);
39
40    image
41}
42
43/// Renders text from font and parameters onto provided image
44pub fn render_text_on_image(image: &mut DynamicImage, font: &Font, text: &str, scale: Scale, point: Point<f32>, color: (u8, u8, u8, u8)) {
45    let (size_x, size_y) = image.dimensions();
46    for glyph in font.layout(text, scale, point) {
47        if let Some(bounding_box) = glyph.pixel_bounding_box() {
48            glyph.draw(|x, y, v| {
49                let bound_x = (x as i32 + bounding_box.min.x) as u32;
50                let bound_y = (y as i32 + bounding_box.min.y) as u32;
51
52                if (bound_x < size_x) && (bound_y < size_y) {
53                    let pixel = image.get_pixel(bound_x, bound_y);
54                    let color_mul = (v * (color.3 as f32 / 255.0)).clamp(0.0, 1.0);
55
56                    image.put_pixel(
57                        bound_x,
58                        bound_y,
59                        // Turn the coverage into an alpha value
60                        Rgba([(pixel.0[0] as f32 * (1.0 - color_mul) + color.0 as f32 * color_mul) as u8, (pixel.0[1] as f32 * (1.0 - color_mul) + color.1 as f32 * color_mul) as u8, (pixel.0[2] as f32 * (1.0 - color_mul) + color.2 as f32 * color_mul) as u8, 255]),
61                    )
62                }
63            })
64        }
65    }
66}
67
68/// Renders text with shadows from font and parameters onto provided image
69pub fn render_shadowed_text_on_image(image: &mut DynamicImage, font: &Font, text: &str, scale: Scale, point: Point<f32>, color: (u8, u8, u8, u8), shadow_offset: (i32, i32), shadow_color: (u8, u8, u8, u8)) {
70    let (size_x, size_y) = image.dimensions();
71    for glyph in font.layout(text, scale, point) {
72        if let Some(bounding_box) = glyph.pixel_bounding_box() {
73            glyph.draw(|x, y, v| {
74                let bound_x = (x as i32 + bounding_box.min.x) as u32;
75                let bound_y = (y as i32 + bounding_box.min.y) as u32;
76
77                if (bound_x < size_x) && (bound_y < size_y) {
78                    let pixel = image.get_pixel(bound_x, bound_y);
79                    let color_mul = (v * (color.3 as f32 / 255.0)).clamp(0.0, 1.0);
80
81                    image.put_pixel(
82                        bound_x,
83                        bound_y,
84                        // Turn the coverage into an alpha value
85                        Rgba([(pixel.0[0] as f32 * (1.0 - color_mul) + color.0 as f32 * color_mul) as u8, (pixel.0[1] as f32 * (1.0 - color_mul) + color.1 as f32 * color_mul) as u8, (pixel.0[2] as f32 * (1.0 - color_mul) + color.2 as f32 * color_mul) as u8, 255]),
86                    );
87
88                    let shadow_x = (bound_x as i32 + shadow_offset.0) as u32;
89                    let shadow_y = (bound_y as i32 + shadow_offset.1) as u32;
90
91                    if (shadow_x < size_x) && (shadow_y < size_y) {
92                        let pixel = image.get_pixel(shadow_x, shadow_y);
93                        let shadow_mul = (v * (shadow_color.3 as f32 / 255.0)).clamp(0.0, 1.0);
94
95                        if shadow_mul > 0.01 {
96                            image.put_pixel(
97                                shadow_x,
98                                shadow_y,
99                                // Turn the coverage into an alpha value
100                                Rgba([(pixel.0[0] as f32 * (1.0 - shadow_mul) + shadow_color.0 as f32 * shadow_mul) as u8, (pixel.0[1] as f32 * (1.0 - shadow_mul) + shadow_color.1 as f32 * shadow_mul) as u8, (pixel.0[2] as f32 * (1.0 - shadow_mul) + shadow_color.2 as f32 * shadow_mul) as u8, 255]),
101                            );
102                        }
103                    }
104                }
105            })
106        }
107    }
108}
109
110/// Renders box with provided parameters onto provided image
111pub fn render_box_on_image(image: &mut DynamicImage, scale: Scale, point: Point<f32>, color: (u8, u8, u8, u8)) {
112    let (size_x, size_y) = image.dimensions();
113    for x in 0..(scale.x as isize) {
114        for y in 0..(scale.y as isize) {
115            let offset_x = (point.x + x as f32) as u32;
116            let offset_y = (point.y - y as f32 - 1.0) as u32;
117
118            if (offset_x < size_x) && (offset_y < size_y) {
119                image.put_pixel(
120                    offset_x,
121                    offset_y,
122                    Rgba([color.0, color.1, color.2, 255]),
123                )
124            }
125        }
126    }
127}
128
129/// Calculates bounds for text with provided font and parameters
130pub fn calculate_bounds_for_text(font: &Font, text: &str, scale: Scale) -> (u32, u32) {
131    let mut w: u32 = 0;
132    let mut h: u32 = 0;
133
134    for glyph in font.layout(text, scale, point(0.0, 0.0)) {
135        if let Some(bounding) = glyph.pixel_bounding_box() {
136            h = h.max(bounding.height() as u32);
137            w = w.max(bounding.max.x as u32);
138        }
139    }
140
141    (w, h)
142}
143
144/// Alignment enumeration
145#[derive(Debug, Clone, Hash, Serialize, Deserialize, EnumVariantNames, EnumString, Display)]
146#[strum(serialize_all = "title_case")]
147pub enum TextAlignment {
148    /// Top left alignment
149    TopLeft,
150    /// Top center alignment
151    TopCenter,
152    /// Top right alignment
153    TopRight,
154    /// Middle left alignment
155    MiddleLeft,
156    /// Center alignment
157    Center,
158    /// Middle right alignment
159    MiddleRight,
160    /// Bottom left alignment
161    BottomLeft,
162    /// Bottom center alignment
163    BottomCenter,
164    /// Bottom right alignment
165    BottomRight,
166}
167
168/// Calculates where text should be rendered for specified alignment and other parameters
169pub fn get_alignment_position_for_text(size: (usize, usize), font: &Font, text: &str, scale: Scale, align: TextAlignment, padding: u32, offset: (f32, f32)) -> Point<f32> {
170    let (sw, sh) = size;
171    let (tw, th) = calculate_bounds_for_text(font, text, scale);
172
173    point(
174        match align {
175            TextAlignment::TopLeft | TextAlignment::MiddleLeft | TextAlignment::BottomLeft => {
176                (padding) as f32 + offset.0
177            }
178
179            TextAlignment::TopCenter | TextAlignment::Center | TextAlignment::BottomCenter => {
180                (sw as i32 / 2 - tw as i32 / 2 - 1) as f32 + offset.0
181            }
182
183            TextAlignment::TopRight | TextAlignment::MiddleRight | TextAlignment::BottomRight => {
184                (sw as i32 - tw as i32 - padding as i32) as f32 + offset.0
185            }
186        },
187        match align {
188            TextAlignment::TopLeft | TextAlignment::TopCenter | TextAlignment::TopRight => {
189                (padding + th) as f32 + offset.1
190            }
191
192            TextAlignment::MiddleLeft | TextAlignment::Center | TextAlignment::MiddleRight => {
193                (sh as i32 / 2 - th as i32 / 2 + th as i32 - 1) as f32 + offset.1
194            }
195
196            TextAlignment::BottomLeft | TextAlignment::BottomCenter | TextAlignment::BottomRight => {
197                (sh as i32 - padding as i32) as f32 + offset.1
198            }
199        },
200    )
201}
202
203/// Renders aligned text onto provided image with specified parameters
204pub fn render_aligned_text_on_image(size: (usize, usize), image: &mut DynamicImage, font: &Font, text: &str, scale: Scale, align: TextAlignment, padding: u32, offset: (f32, f32), color: (u8, u8, u8, u8)) {
205    let point = get_alignment_position_for_text(size, font, text, scale.clone(), align, padding, offset);
206    render_text_on_image(image, &font, text, scale, point, color);
207}
208
209/// Renders aligned shadowed text onto provided image with specified parameters
210pub fn render_aligned_shadowed_text_on_image(size: (usize, usize), image: &mut DynamicImage, font: &Font, text: &str, scale: Scale, align: TextAlignment, padding: u32, offset: (f32, f32), color: (u8, u8, u8, u8), shadow_offset: (i32, i32), shadow_color: (u8, u8, u8, u8)) {
211    let point = get_alignment_position_for_text(size, font, text, scale.clone(), align, padding, offset);
212    render_shadowed_text_on_image(image, &font, text, scale, point, color, shadow_offset, shadow_color);
213}