sensor_core/
text_renderer.rs

1use image::{ImageBuffer, Rgba};
2use imageproc::drawing;
3use rusttype::Font;
4
5use crate::{hex_to_rgba, SensorType, SensorValue, SensorValueModifier, TextAlign, TextConfig};
6
7/// Renders the text element to a png image.
8/// Render Pipeline:
9///     1. Draw text on empty rgba buffer on display size
10///     2. Calculate bounding box of text
11///     3. Crop buffer to the visible bounding box of the text
12///     4. Create a new Image buffer in the size of the text element
13///     5. Overlay the text image on the new image buffer according to the text alignment
14pub 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    // Initialize image buffer
22    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    // Replace placeholders in text format
27    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    // 1. Draw text on empty rgba buffer on display size
32    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    // 2. Calculate bounding box of text
43    let text_bounding_box = get_bounding_box(&image);
44
45    // 3. Crop buffer to the visible bounding box of the text
46    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    // 4. Create a new Image buffer in the size of the text element
56    let mut image = image::RgbaImage::new(text_config.width, text_config.height);
57
58    // 5. Overlay the text image on the new image buffer according to the text alignment
59    // Center text vertically
60    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
86/// Replaces the placeholders in the text format with the actual values
87/// FIXME: The special placeholders like {value-avg} may be calculated multiple times
88///        This is not a problem for now because 95% of the time they are not or rarely used
89///        But if we encounter performance issues, we should optimize this (Esp. if the history is long)
90fn 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
136/// Returns the sensor unit of the latest sensor value
137fn 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
144// Returns the latest sensor value
145fn 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
152/// Returns the minimum sensor value of all sensor values in the history
153fn 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 there are no values, return N/A
157    if number_values_history.is_empty() {
158        return "N/A".to_string();
159    }
160
161    // Get the minimum value
162    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
170/// Returns the maximum sensor value of all sensor values in the history
171fn 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 there are no values, return N/A
175    if number_values_history.is_empty() {
176        return "N/A".to_string();
177    }
178
179    // Get the maximum value
180    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
188/// Returns the average sensor value of all sensor values in the history
189fn 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 there are no values, return N/A
193    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
225/// Calculates the bounding box of the text in the image
226/// This is done by detecting the first and last non-transparent pixel in each direction
227fn 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    // Detect bounding box from left
234    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    // Detect bounding box from top
251    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    // Detect bounding box from right
268    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    // Detect bounding box from bottom
285    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}