use ab_glyph::{FontRef, PxScale};
use image::{DynamicImage, Rgba};
use imageproc::drawing::draw_text_mut;
use std::fs;
use crate::buffer::CapturedScreenshot;
use crate::config::BurnInConfig;
pub fn apply_burn_in(
image: &mut DynamicImage,
screenshot: &CapturedScreenshot,
config: &BurnInConfig,
) {
if !config.enabled {
return;
}
let font_path = config.font_path.as_ref().unwrap_or_else(|| {
panic!(
"Burn-in is enabled but no font_path is configured. \
Please set burn_in.font_path in your screenshots.toml or use \
BurnInConfig::enabled().with_font(\"path/to/font.ttf\")"
);
});
let font_data = fs::read(font_path).unwrap_or_else(|e| {
panic!(
"Failed to read font file '{}': {}. \
Please ensure the font file exists and is readable.",
font_path.display(),
e
);
});
let font = FontRef::try_from_slice(&font_data).unwrap_or_else(|e| {
panic!(
"Failed to parse font file '{}': {}. \
Please ensure the file is a valid TTF font.",
font_path.display(),
e
);
});
let scale = PxScale::from(config.font_size);
let mut lines: Vec<String> = Vec::new();
if config.show_frame {
lines.push(format!("Frame: {}", screenshot.frame_number));
}
if config.show_key {
lines.push(format!("Key: {}", screenshot.key));
}
if config.show_description && !screenshot.description.is_empty() {
lines.push(format!("Desc: {}", screenshot.description));
}
if lines.is_empty() {
return;
}
let (img_width, img_height) = (image.width(), image.height());
let line_height = (config.font_size * 1.2) as i32;
let total_text_height = (lines.len() as i32) * line_height;
let char_width = config.font_size * 0.6;
let max_line_len = lines.iter().map(|l| l.len()).max().unwrap_or(0);
let text_width = (max_line_len as f32 * char_width) as i32;
let padding = config.padding as i32;
let (base_x, base_y) = match config.position {
crate::config::BurnInPosition::UpperLeft => (padding, padding),
crate::config::BurnInPosition::UpperRight => {
(img_width as i32 - text_width - padding, padding)
}
crate::config::BurnInPosition::LowerLeft => {
(padding, img_height as i32 - total_text_height - padding)
}
crate::config::BurnInPosition::LowerRight => (
img_width as i32 - text_width - padding,
img_height as i32 - total_text_height - padding,
),
};
let text_color = Rgba([255u8, 255u8, 255u8, 255u8]); let shadow_color = Rgba([0u8, 0u8, 0u8, 255u8]);
let rgba_image = image.to_rgba8();
let mut working_image = rgba_image;
for (i, line) in lines.iter().enumerate() {
let y = base_y + (i as i32) * line_height;
let x = base_x;
for dx in -1..=1 {
for dy in -1..=1 {
if dx != 0 || dy != 0 {
draw_text_mut(
&mut working_image,
shadow_color,
x + dx,
y + dy,
scale,
&font,
line,
);
}
}
}
draw_text_mut(&mut working_image, text_color, x, y, scale, &font, line);
}
*image = DynamicImage::ImageRgba8(working_image);
}
#[cfg(test)]
mod tests {
use super::*;
use chrono::Local;
#[test]
fn test_burn_in_disabled() {
let mut image = DynamicImage::new_rgba8(100, 100);
let screenshot = CapturedScreenshot {
image: bevy::prelude::Image::default(),
timestamp: Local::now(),
description: "test".to_string(),
key: "default".to_string(),
frame_number: 42,
crop_region: None,
};
let config = BurnInConfig::default();
apply_burn_in(&mut image, &screenshot, &config);
}
}