Spaces:
Sleeping
Sleeping
| 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<DynamicImage> { | |
| 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<DynamicImage> { | |
| 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<String> { | |
| 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<DynamicImage> { | |
| 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<DynamicImage> { | |
| 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, | |
| } | |
| } | |
| pub struct ImageStats { | |
| pub width: u32, | |
| pub height: u32, | |
| pub avg_red: u8, | |
| pub avg_green: u8, | |
| pub avg_blue: u8, | |
| } | |