use image::{ImageBuffer, Rgba};
use imageproc::drawing;
use rusttype::Font;
use crate::{hex_to_rgba, SensorType, SensorValue, SensorValueModifier, TextAlign, TextConfig};
pub fn render(
image_width: u32,
image_height: u32,
text_config: &TextConfig,
sensor_value_history: &[Vec<SensorValue>],
font: &Font,
) -> ImageBuffer<Rgba<u8>, Vec<u8>> {
let font_scale = rusttype::Scale::uniform(text_config.font_size as f32);
let font_color: Rgba<u8> = hex_to_rgba(&text_config.font_color);
let sensor_id = &text_config.sensor_id;
let text = replace_placeholders(text_config, sensor_id, sensor_value_history);
let mut image = image::RgbaImage::new(image_width, image_height);
drawing::draw_text_mut(
&mut image,
font_color,
25,
7,
font_scale,
font,
text.as_str(),
);
let text_bounding_box = get_bounding_box(&image);
let text_image = image::imageops::crop(
&mut image,
text_bounding_box.left() as u32,
text_bounding_box.top() as u32,
text_bounding_box.width(),
text_bounding_box.height(),
)
.to_image();
let mut image = image::RgbaImage::new(text_config.width, text_config.height);
let y: u32 = if text_config.height > text_image.height() {
(text_config.height - text_image.height()) / 2
} else {
0
};
let x: u32 = if text_config.width > text_image.width() {
text_config.width - text_image.width()
} else {
0
};
match text_config.alignment {
TextAlign::Left => {
image::imageops::overlay(&mut image, &text_image, 0, y as i64);
}
TextAlign::Center => {
let x = x / 2;
image::imageops::overlay(&mut image, &text_image, x as i64, y as i64);
}
TextAlign::Right => {
image::imageops::overlay(&mut image, &text_image, x as i64, y as i64);
}
}
image
}
fn replace_placeholders(
text_config: &TextConfig,
sensor_id: &str,
sensor_value_history: &[Vec<SensorValue>],
) -> String {
let mut text_format = text_config.format.clone();
if text_format.contains("{value-avg}") {
text_format = text_format.replace(
"{value-avg}",
get_value_avg(sensor_id, sensor_value_history).as_str(),
);
}
if text_format.contains("{value-min}") {
text_format = text_format.replace(
"{value-min}",
get_value_min(sensor_id, sensor_value_history).as_str(),
);
}
if text_format.contains("{value-max}") {
text_format = text_format.replace(
"{value-max}",
get_value_max(sensor_id, sensor_value_history).as_str(),
);
}
if text_format.contains("{value}") {
let value = match text_config.value_modifier {
SensorValueModifier::None => get_value(sensor_id, sensor_value_history),
SensorValueModifier::Avg => get_value_avg(sensor_id, sensor_value_history),
SensorValueModifier::Max => get_value_max(sensor_id, sensor_value_history),
SensorValueModifier::Min => get_value_min(sensor_id, sensor_value_history),
};
text_format = text_format.replace("{value}", value.as_str());
}
if text_format.contains("{unit}") {
text_format =
text_format.replace("{unit}", get_unit(sensor_id, sensor_value_history).as_str());
}
text_format
}
fn get_unit(sensor_id: &str, sensor_value_history: &[Vec<SensorValue>]) -> String {
match get_latest_value(sensor_id, sensor_value_history) {
Some(value) => value.unit,
None => "".to_string(),
}
}
fn get_value(sensor_id: &str, sensor_value_history: &[Vec<SensorValue>]) -> String {
match get_latest_value(sensor_id, sensor_value_history) {
Some(value) => value.value,
None => "N/A".to_string(),
}
}
fn get_value_min(sensor_id: &str, sensor_value_history: &[Vec<SensorValue>]) -> String {
let number_values_history = get_sensor_values_as_number(sensor_id, sensor_value_history);
if number_values_history.is_empty() {
return "N/A".to_string();
}
let min = number_values_history
.iter()
.min_by(|a, b| a.partial_cmp(b).unwrap())
.unwrap();
format!("{:.2}", min).to_string()
}
fn get_value_max(sensor_id: &str, sensor_value_history: &[Vec<SensorValue>]) -> String {
let number_values_history = get_sensor_values_as_number(sensor_id, sensor_value_history);
if number_values_history.is_empty() {
return "N/A".to_string();
}
let max = number_values_history
.iter()
.max_by(|a, b| a.partial_cmp(b).unwrap())
.unwrap();
format!("{:.2}", max).to_string()
}
fn get_value_avg(sensor_id: &str, sensor_value_history: &[Vec<SensorValue>]) -> String {
let number_values_history = get_sensor_values_as_number(sensor_id, sensor_value_history);
if number_values_history.is_empty() {
return "N/A".to_string();
}
let avg = number_values_history.iter().sum::<f64>() / number_values_history.len() as f64;
format!("{:.2}", avg).to_string()
}
fn get_sensor_values_as_number(
sensor_id: &str,
sensor_value_history: &[Vec<SensorValue>],
) -> Vec<f64> {
let values = sensor_value_history
.iter()
.flat_map(|sensor_values| sensor_values.iter().find(|&s| s.id == sensor_id))
.filter(|sensor_value| sensor_value.sensor_type == SensorType::Number)
.map(|sensor_value| sensor_value.value.parse::<f64>().unwrap())
.collect::<Vec<f64>>();
values
}
fn get_latest_value(
sensor_id: &str,
sensor_value_history: &[Vec<SensorValue>],
) -> Option<SensorValue> {
sensor_value_history[0]
.iter()
.find(|&s| s.id == sensor_id)
.cloned()
}
fn get_bounding_box(image: &ImageBuffer<Rgba<u8>, Vec<u8>>) -> imageproc::rect::Rect {
let mut min_x = 0;
let mut min_y = 0;
let mut max_x = image.width();
let mut max_y = image.height();
for x in 0..image.width() {
let mut line_empty = true;
for y in 0..image.height() {
let pixel = image.get_pixel(x, y);
if pixel != &Rgba([0, 0, 0, 0]) {
line_empty = false;
break;
}
}
if !line_empty {
min_x = x;
break;
}
}
for y in 0..image.height() {
let mut line_empty = true;
for x in 0..image.width() {
let pixel = image.get_pixel(x, y);
if pixel != &Rgba([0, 0, 0, 0]) {
line_empty = false;
break;
}
}
if !line_empty {
min_y = y - 1;
break;
}
}
for x in (0..image.width()).rev() {
let mut line_empty = true;
for y in (0..image.height()).rev() {
let pixel = image.get_pixel(x, y);
if pixel != &Rgba([0, 0, 0, 0]) {
line_empty = false;
break;
}
}
if !line_empty {
max_x = x + 1;
break;
}
}
for y in (0..image.height()).rev() {
let mut line_empty = true;
for x in (0..image.width()).rev() {
let pixel = image.get_pixel(x, y);
if pixel != &Rgba([0, 0, 0, 0]) {
line_empty = false;
break;
}
}
if !line_empty {
max_y = y + 1;
break;
}
}
imageproc::rect::Rect::at(min_x as i32, min_y as i32).of_size(max_x - min_x, max_y - min_y)
}