bevy_simple_screenshot 0.1.2

A plug-and-play screenshot library for Bevy 0.17+ with ring-buffered capture and automatic saving
//! Burn-in text overlay for screenshots.

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;

/// Apply burn-in text overlay to an image.
///
/// # Panics
///
/// Panics if burn-in is enabled but no font path is configured, or if the font
/// file cannot be loaded.
pub fn apply_burn_in(
    image: &mut DynamicImage,
    screenshot: &CapturedScreenshot,
    config: &BurnInConfig,
) {
    if !config.enabled {
        return;
    }

    // Load font from the configured path
    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);

    // Build the lines of text to render
    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;
    }

    // Calculate text dimensions and positions
    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;

    // Estimate max line width (rough approximation: ~0.6 * font_size per character for monospace)
    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;

    // Calculate base position based on config
    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,
        ),
    };

    // Colors for text rendering
    let text_color = Rgba([255u8, 255u8, 255u8, 255u8]); // White
    let shadow_color = Rgba([0u8, 0u8, 0u8, 255u8]); // Black

    // Convert to rgba8 for drawing
    let rgba_image = image.to_rgba8();
    let mut working_image = rgba_image;

    // Draw each line with shadow for readability
    for (i, line) in lines.iter().enumerate() {
        let y = base_y + (i as i32) * line_height;
        let x = base_x;

        // Draw shadow/outline (offset by 1-2 pixels in multiple directions)
        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 main text
        draw_text_mut(&mut working_image, text_color, x, y, scale, &font, line);
    }

    // Replace the image with the modified version
    *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(); // disabled by default

        // Should not panic and image should be unchanged
        apply_burn_in(&mut image, &screenshot, &config);
    }
}