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
7pub 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
13pub 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
23pub 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
33pub 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
43pub 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 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
68pub 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 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 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
110pub 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
129pub 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#[derive(Debug, Clone, Hash, Serialize, Deserialize, EnumVariantNames, EnumString, Display)]
146#[strum(serialize_all = "title_case")]
147pub enum TextAlignment {
148 TopLeft,
150 TopCenter,
152 TopRight,
154 MiddleLeft,
156 Center,
158 MiddleRight,
160 BottomLeft,
162 BottomCenter,
164 BottomRight,
166}
167
168pub 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
203pub 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
209pub 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}