darkmedia-x-api / engine /src /renderer /image_processor.rs
cybermedia's picture
Upload folder using huggingface_hub
343eed9 verified
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,
}
}
#[derive(Debug, Clone)]
pub struct ImageStats {
pub width: u32,
pub height: u32,
pub avg_red: u8,
pub avg_green: u8,
pub avg_blue: u8,
}