use crate::error::Result; use image::{DynamicImage, GenericImageView, ImageBuffer, Rgba}; use imageproc::drawing::{draw_text_mut, draw_filled_rect_mut}; use ab_glyph::{FontRef, PxScale}; use std::path::Path; const FONT_PATH: &str = "/usr/share/fonts/truetype/liberation/LiberationSans-Bold.ttf"; /// Charger une image depuis un fichier pub fn load_image(path: &Path) -> Result { image::open(path).map_err(|e| { crate::error::GeneratorError::ImageGenerationFailed(format!("Failed to load image: {}", e)) }) } /// Sauvegarder une image pub fn save_image(img: &DynamicImage, path: &Path) -> Result<()> { img.save(path).map_err(|e| { crate::error::GeneratorError::ImageGenerationFailed(format!("Failed to save image: {}", e)) }) } /// Appliquer un overlay de texte sur l'image pub fn add_text_overlay(img: DynamicImage, title: Option<&str>, subtitle: Option<&str>) -> Result { let mut rgba = img.to_rgba8(); let (w, h) = rgba.dimensions(); let font_data = std::fs::read(FONT_PATH).map_err(|e| { crate::error::GeneratorError::ImageGenerationFailed(format!("Failed to read font: {}", e)) })?; let font = FontRef::try_from_slice(&font_data).map_err(|e| { crate::error::GeneratorError::ImageGenerationFailed(format!("Failed to load font: {}", e)) })?; // 1. DESSINER LE TITRE if let Some(t) = title { if t != "None" && !t.is_empty() { let scale = PxScale::from(90.0); let lines = wrap_text(t, 12); let mut y_text = h as i32 / 5; for line in lines { let (tw, _) = get_text_size(&font, scale, &line); let x = (w as i32 - tw as i32) / 2; // Outline Rouge Sang let red = Rgba([150, 0, 0, 255]); for off_x in [-3, 3] { for off_y in [-3, 3] { draw_text_mut(&mut rgba, red, x + off_x, y_text + off_y, scale, &font, &line); } } // Texte Blanc draw_text_mut(&mut rgba, Rgba([255, 255, 255, 255]), x, y_text, scale, &font, &line); y_text += 100; } } } // 2. DESSINER LA NARRATION (Subtitle) if let Some(s) = subtitle { if s != "None" && !s.is_empty() { let scale_sub = PxScale::from(65.0); let lines = wrap_text(s, 22); let line_height = 85; let total_h = lines.len() as i32 * line_height; let y_start_base = h as i32 - 450; // Boîte de fond noire draw_filled_rect_mut( &mut rgba, imageproc::rect::Rect::at(50, y_start_base - 20).of_size(w - 100, total_h as u32 + 40), Rgba([0, 0, 0, 200]) ); let mut y_curr = y_start_base; for line in lines { let (tw, _) = get_text_size(&font, scale_sub, &line); let x = (w as i32 - tw as i32) / 2; // Contour Rouge let red_sub = Rgba([200, 0, 0, 255]); for off_x in [-2, 2] { for off_y in [-2, 2] { draw_text_mut(&mut rgba, red_sub, x + off_x, y_curr + off_y, scale_sub, &font, &line); } } // Texte Jaune Karaoké draw_text_mut(&mut rgba, Rgba([255, 255, 0, 255]), x, y_curr, scale_sub, &font, &line); y_curr += line_height; } } } Ok(DynamicImage::ImageRgba8(rgba)) } fn wrap_text(text: &str, max_chars: usize) -> Vec { let mut lines = Vec::new(); for paragraph in text.split('\n') { let words: Vec<&str> = paragraph.split_whitespace().collect(); let mut current_line = String::new(); for word in words { if current_line.is_empty() { current_line.push_str(word); } else if current_line.len() + 1 + word.len() <= max_chars { current_line.push(' '); current_line.push_str(word); } else { lines.push(current_line); current_line = String::from(word); } } if !current_line.is_empty() { lines.push(current_line); } } lines } fn get_text_size(_font: &FontRef, scale: PxScale, text: &str) -> (u32, u32) { let char_width = scale.x * 0.6; let width = (text.len() as f32 * char_width).ceil() as u32; let height = scale.y.ceil() as u32; (width, height) } /// Appliquer un overlay de texte sur l'image (compatibilité existante) pub fn render_text_overlay(img: DynamicImage, text: &str) -> Result { add_text_overlay(img, None, Some(text)) } /// Redimensionner l'image pub fn resize(img: DynamicImage, width: u32, height: u32) -> DynamicImage { img.resize_exact(width, height, image::imageops::FilterType::Lanczos3) } /// Appliquer un filtre de saturation pub fn adjust_saturation(img: DynamicImage, factor: f32) -> Result { let rgba = img.to_rgba8(); let mut output = ImageBuffer::new(rgba.width(), rgba.height()); for (x, y, pixel) in rgba.enumerate_pixels() { let [r, g, b, a] = pixel.0; let (h, s, v) = rgb_to_hsv(r, g, b); let new_s = (s * factor).min(1.0); let (new_r, new_g, new_b) = hsv_to_rgb(h, new_s, v); output.put_pixel(x, y, Rgba([new_r, new_g, new_b, a])); } Ok(DynamicImage::ImageRgba8(output)) } /// Convertir RGB en HSV fn rgb_to_hsv(r: u8, g: u8, b: u8) -> (f32, f32, f32) { let r = r as f32 / 255.0; let g = g as f32 / 255.0; let b = b as f32 / 255.0; let max = r.max(g).max(b); let min = r.min(g).min(b); let delta = max - min; let h = if delta == 0.0 { 0.0 } else if max == r { (60.0 * ((g - b) / delta) + 360.0) % 360.0 } else if max == g { (60.0 * ((b - r) / delta) + 120.0) % 360.0 } else { (60.0 * ((r - g) / delta) + 240.0) % 360.0 }; let s = if max == 0.0 { 0.0 } else { delta / max }; let v = max; (h, s, v) } /// Convertir HSV en RGB fn hsv_to_rgb(h: f32, s: f32, v: f32) -> (u8, u8, u8) { let c = v * s; let hp = h / 60.0; let x = c * (1.0 - (hp % 2.0 - 1.0).abs()); let (r1, g1, b1) = match hp as i32 { 0 => (c, x, 0.0), 1 => (x, c, 0.0), 2 => (0.0, c, x), 3 => (0.0, x, c), 4 => (x, 0.0, c), _ => (c, 0.0, x), }; let m = v - c; let r = ((r1 + m) * 255.0) as u8; let g = ((g1 + m) * 255.0) as u8; let b = ((b1 + m) * 255.0) as u8; (r, g, b) } /// Extraire les statistiques d'une image pub fn get_image_stats(img: &DynamicImage) -> ImageStats { let (width, height) = img.dimensions(); let rgba = img.to_rgba8(); let pixel_count = (width * height) as usize; let mut r_sum = 0u64; let mut g_sum = 0u64; let mut b_sum = 0u64; for pixel in rgba.pixels() { r_sum += pixel.0[0] as u64; g_sum += pixel.0[1] as u64; b_sum += pixel.0[2] as u64; } ImageStats { width, height, avg_red: (r_sum / pixel_count as u64) as u8, avg_green: (g_sum / pixel_count as u64) as u8, avg_blue: (b_sum / pixel_count as u64) as u8, } } #[derive(Debug, Clone)] pub struct ImageStats { pub width: u32, pub height: u32, pub avg_red: u8, pub avg_green: u8, pub avg_blue: u8, }