1use image::{ImageBuffer, Rgba};
2use imageproc::drawing;
3use rusttype::Font;
4
5use crate::{hex_to_rgba, SensorType, SensorValue, SensorValueModifier, TextAlign, TextConfig};
6
7pub fn render(
15 image_width: u32,
16 image_height: u32,
17 text_config: &TextConfig,
18 sensor_value_history: &[Vec<SensorValue>],
19 font: &Font,
20) -> ImageBuffer<Rgba<u8>, Vec<u8>> {
21 let font_scale = rusttype::Scale::uniform(text_config.font_size as f32);
23 let font_color: Rgba<u8> = hex_to_rgba(&text_config.font_color);
24 let sensor_id = &text_config.sensor_id;
25
26 let text = replace_placeholders(text_config, sensor_id, sensor_value_history);
28
29 let mut image = image::RgbaImage::new(image_width, image_height);
30
31 drawing::draw_text_mut(
33 &mut image,
34 font_color,
35 25,
36 7,
37 font_scale,
38 font,
39 text.as_str(),
40 );
41
42 let text_bounding_box = get_bounding_box(&image);
44
45 let text_image = image::imageops::crop(
47 &mut image,
48 text_bounding_box.left() as u32,
49 text_bounding_box.top() as u32,
50 text_bounding_box.width(),
51 text_bounding_box.height(),
52 )
53 .to_image();
54
55 let mut image = image::RgbaImage::new(text_config.width, text_config.height);
57
58 let y: u32 = if text_config.height > text_image.height() {
61 (text_config.height - text_image.height()) / 2
62 } else {
63 0
64 };
65 let x: u32 = if text_config.width > text_image.width() {
66 text_config.width - text_image.width()
67 } else {
68 0
69 };
70 match text_config.alignment {
71 TextAlign::Left => {
72 image::imageops::overlay(&mut image, &text_image, 0, y as i64);
73 }
74 TextAlign::Center => {
75 let x = x / 2;
76 image::imageops::overlay(&mut image, &text_image, x as i64, y as i64);
77 }
78 TextAlign::Right => {
79 image::imageops::overlay(&mut image, &text_image, x as i64, y as i64);
80 }
81 }
82
83 image
84}
85
86fn replace_placeholders(
91 text_config: &TextConfig,
92 sensor_id: &str,
93 sensor_value_history: &[Vec<SensorValue>],
94) -> String {
95 let mut text_format = text_config.format.clone();
96
97 if text_format.contains("{value-avg}") {
98 text_format = text_format.replace(
99 "{value-avg}",
100 get_value_avg(sensor_id, sensor_value_history).as_str(),
101 );
102 }
103
104 if text_format.contains("{value-min}") {
105 text_format = text_format.replace(
106 "{value-min}",
107 get_value_min(sensor_id, sensor_value_history).as_str(),
108 );
109 }
110
111 if text_format.contains("{value-max}") {
112 text_format = text_format.replace(
113 "{value-max}",
114 get_value_max(sensor_id, sensor_value_history).as_str(),
115 );
116 }
117
118 if text_format.contains("{value}") {
119 let value = match text_config.value_modifier {
120 SensorValueModifier::None => get_value(sensor_id, sensor_value_history),
121 SensorValueModifier::Avg => get_value_avg(sensor_id, sensor_value_history),
122 SensorValueModifier::Max => get_value_max(sensor_id, sensor_value_history),
123 SensorValueModifier::Min => get_value_min(sensor_id, sensor_value_history),
124 };
125 text_format = text_format.replace("{value}", value.as_str());
126 }
127
128 if text_format.contains("{unit}") {
129 text_format =
130 text_format.replace("{unit}", get_unit(sensor_id, sensor_value_history).as_str());
131 }
132
133 text_format
134}
135
136fn get_unit(sensor_id: &str, sensor_value_history: &[Vec<SensorValue>]) -> String {
138 match get_latest_value(sensor_id, sensor_value_history) {
139 Some(value) => value.unit,
140 None => "".to_string(),
141 }
142}
143
144fn get_value(sensor_id: &str, sensor_value_history: &[Vec<SensorValue>]) -> String {
146 match get_latest_value(sensor_id, sensor_value_history) {
147 Some(value) => value.value,
148 None => "N/A".to_string(),
149 }
150}
151
152fn get_value_min(sensor_id: &str, sensor_value_history: &[Vec<SensorValue>]) -> String {
154 let number_values_history = get_sensor_values_as_number(sensor_id, sensor_value_history);
155
156 if number_values_history.is_empty() {
158 return "N/A".to_string();
159 }
160
161 let min = number_values_history
163 .iter()
164 .min_by(|a, b| a.partial_cmp(b).unwrap())
165 .unwrap();
166
167 format!("{:.2}", min).to_string()
168}
169
170fn get_value_max(sensor_id: &str, sensor_value_history: &[Vec<SensorValue>]) -> String {
172 let number_values_history = get_sensor_values_as_number(sensor_id, sensor_value_history);
173
174 if number_values_history.is_empty() {
176 return "N/A".to_string();
177 }
178
179 let max = number_values_history
181 .iter()
182 .max_by(|a, b| a.partial_cmp(b).unwrap())
183 .unwrap();
184
185 format!("{:.2}", max).to_string()
186}
187
188fn get_value_avg(sensor_id: &str, sensor_value_history: &[Vec<SensorValue>]) -> String {
190 let number_values_history = get_sensor_values_as_number(sensor_id, sensor_value_history);
191
192 if number_values_history.is_empty() {
194 return "N/A".to_string();
195 }
196
197 let avg = number_values_history.iter().sum::<f64>() / number_values_history.len() as f64;
198
199 format!("{:.2}", avg).to_string()
200}
201
202fn get_sensor_values_as_number(
203 sensor_id: &str,
204 sensor_value_history: &[Vec<SensorValue>],
205) -> Vec<f64> {
206 let values = sensor_value_history
207 .iter()
208 .flat_map(|sensor_values| sensor_values.iter().find(|&s| s.id == sensor_id))
209 .filter(|sensor_value| sensor_value.sensor_type == SensorType::Number)
210 .map(|sensor_value| sensor_value.value.parse::<f64>().unwrap())
211 .collect::<Vec<f64>>();
212 values
213}
214
215fn get_latest_value(
216 sensor_id: &str,
217 sensor_value_history: &[Vec<SensorValue>],
218) -> Option<SensorValue> {
219 sensor_value_history[0]
220 .iter()
221 .find(|&s| s.id == sensor_id)
222 .cloned()
223}
224
225fn get_bounding_box(image: &ImageBuffer<Rgba<u8>, Vec<u8>>) -> imageproc::rect::Rect {
228 let mut min_x = 0;
229 let mut min_y = 0;
230 let mut max_x = image.width();
231 let mut max_y = image.height();
232
233 for x in 0..image.width() {
235 let mut line_empty = true;
236 for y in 0..image.height() {
237 let pixel = image.get_pixel(x, y);
238 if pixel != &Rgba([0, 0, 0, 0]) {
239 line_empty = false;
240 break;
241 }
242 }
243
244 if !line_empty {
245 min_x = x;
246 break;
247 }
248 }
249
250 for y in 0..image.height() {
252 let mut line_empty = true;
253 for x in 0..image.width() {
254 let pixel = image.get_pixel(x, y);
255 if pixel != &Rgba([0, 0, 0, 0]) {
256 line_empty = false;
257 break;
258 }
259 }
260
261 if !line_empty {
262 min_y = y - 1;
263 break;
264 }
265 }
266
267 for x in (0..image.width()).rev() {
269 let mut line_empty = true;
270 for y in (0..image.height()).rev() {
271 let pixel = image.get_pixel(x, y);
272 if pixel != &Rgba([0, 0, 0, 0]) {
273 line_empty = false;
274 break;
275 }
276 }
277
278 if !line_empty {
279 max_x = x + 1;
280 break;
281 }
282 }
283
284 for y in (0..image.height()).rev() {
286 let mut line_empty = true;
287 for x in (0..image.width()).rev() {
288 let pixel = image.get_pixel(x, y);
289 if pixel != &Rgba([0, 0, 0, 0]) {
290 line_empty = false;
291 break;
292 }
293 }
294
295 if !line_empty {
296 max_y = y + 1;
297 break;
298 }
299 }
300
301 imageproc::rect::Rect::at(min_x as i32, min_y as i32).of_size(max_x - min_x, max_y - min_y)
302}