use crate::gui::{Rect, TextDirection};
use anyhow::Result;
use image::{DynamicImage, Rgba, RgbaImage};
use imageproc::drawing::draw_text_mut;
use rusttype::{Font, Scale};
use std::path::Path;
pub struct ImageProcessor {
original_image: DynamicImage,
}
impl ImageProcessor {
pub fn new(image_path: &str) -> Self {
let img = image::open(image_path).expect("无法打开图片");
Self {
original_image: img,
}
}
pub fn draw_text_in_rect(&self, output_path: &str, text: &str, rect: &Rect) -> Result<()> {
self.draw_text_in_rect_with_color_variation(output_path, text, rect, false, 0.0, 0)
}
pub fn draw_text_in_rect_with_color_variation(
&self,
output_path: &str,
text: &str,
rect: &Rect,
enable_color_variation: bool,
base_hue: f32,
index: u32,
) -> Result<()> {
let img = self.original_image.clone();
let mut rgba_img = img.to_rgba8();
let font_size = self.calculate_font_size(text, rect.width, rect.height);
let font = self
.load_system_font()
.ok_or_else(|| anyhow::anyhow!("Failed to load font"))?;
self.draw_text_with_direction(&mut rgba_img, text, &font, font_size, rect, rect.text_color);
if enable_color_variation {
self.apply_color_variation(&mut rgba_img, base_hue, index);
}
let final_img = DynamicImage::ImageRgba8(rgba_img);
final_img.save(output_path)?;
Ok(())
}
fn calculate_font_size(&self, text: &str, rect_width: u32, rect_height: u32) -> f32 {
let text_len = text.len() as f32;
let width_ratio = rect_width as f32 / (text_len * 0.6); let height_ratio = rect_height as f32 * 0.8;
let font_size = width_ratio.min(height_ratio);
font_size.max(12.0).min(200.0)
}
fn measure_text_width(&self, text: &str, font: &Font, scale: Scale) -> f32 {
let mut width: f32 = 0.0;
for glyph in font.layout(text, scale, rusttype::point(0.0, 0.0)) {
if let Some(bounding_box) = glyph.pixel_bounding_box() {
width = width.max(bounding_box.max.x as f32);
}
}
width
}
fn load_system_font(&self) -> Option<Font<'static>> {
let system_fonts = [
"/System/Library/Fonts/Arial.ttf", "/System/Library/Fonts/Helvetica.ttc", "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", "C:\\Windows\\Fonts\\arial.ttf", ];
for font_path in &system_fonts {
if Path::new(font_path).exists() {
if let Ok(font_data) = std::fs::read(font_path) {
let font_data = Box::leak(font_data.into_boxed_slice());
if let Some(font) = Font::try_from_bytes(font_data) {
return Some(font);
}
}
}
}
None
}
fn apply_color_variation(&self, rgba_img: &mut image::RgbaImage, _base_hue: f32, index: u32) {
let golden_ratio = 1.618033988749895;
let current_hue = (index as f32 * 360.0 * golden_ratio) % 360.0;
let hue_rad = current_hue.to_radians();
for pixel in rgba_img.pixels_mut() {
let r = pixel[0] as f32 / 255.0;
let g = pixel[1] as f32 / 255.0;
let b = pixel[2] as f32 / 255.0;
let brightness = (r + g + b) / 3.0;
if brightness < 0.1 || brightness > 0.9 {
continue;
}
let r_phase = hue_rad;
let g_phase = hue_rad + 2.0943951023931953; let b_phase = hue_rad + 4.1887902047863905;
let new_r = (brightness + 0.3 * r_phase.cos()).clamp(0.0, 1.0);
let new_g = (brightness + 0.3 * g_phase.cos()).clamp(0.0, 1.0);
let new_b = (brightness + 0.3 * b_phase.cos()).clamp(0.0, 1.0);
pixel[0] = (new_r * 255.0) as u8;
pixel[1] = (new_g * 255.0) as u8;
pixel[2] = (new_b * 255.0) as u8;
}
}
fn draw_text_with_direction(
&self,
rgba_img: &mut RgbaImage,
text: &str,
font: &Font,
font_size: f32,
rect: &Rect,
color: egui::Color32,
) {
let scale = Scale::uniform(font_size);
let text_width = self.measure_text_width(text, font, scale);
let text_height = font.v_metrics(scale).ascent - font.v_metrics(scale).descent;
match rect.text_direction {
TextDirection::Down => {
let text_x = rect.x + (rect.width as i32 - text_width as i32) / 2;
let text_y = rect.y + (rect.height as i32 - text_height as i32) / 2;
draw_text_mut(
rgba_img,
Rgba([color.r(), color.g(), color.b(), 255]),
text_x,
text_y,
scale,
font,
text,
);
}
TextDirection::Up => {
let text_x = rect.x + (rect.width as i32 - text_width as i32) / 2;
let text_y = rect.y + (rect.height as i32 - text_height as i32) / 2;
self.draw_rotated_text(rgba_img, text, font, scale, text_x, text_y, 180.0, color);
}
TextDirection::Right => {
let text_x = rect.x + (rect.width as i32 - text_height as i32) / 2;
let text_y = rect.y + (rect.height as i32 - text_width as i32) / 2;
self.draw_rotated_text(rgba_img, text, font, scale, text_x, text_y, 270.0, color);
}
TextDirection::Left => {
let text_x = rect.x + (rect.width as i32 - text_height as i32) / 2;
let text_y = rect.y + (rect.height as i32 - text_width as i32) / 2;
self.draw_rotated_text(rgba_img, text, font, scale, text_x, text_y, 90.0, color);
}
}
}
fn draw_rotated_text(
&self,
rgba_img: &mut RgbaImage,
text: &str,
font: &Font,
scale: Scale,
center_x: i32,
center_y: i32,
angle_degrees: f32,
color: egui::Color32,
) {
let text_width = self.measure_text_width(text, font, scale) as u32;
let text_height = (font.v_metrics(scale).ascent - font.v_metrics(scale).descent) as u32;
let margin = 10;
let temp_width = text_width + margin * 2;
let temp_height = text_height + margin * 2;
let mut temp_img = RgbaImage::new(temp_width, temp_height);
draw_text_mut(
&mut temp_img,
Rgba([color.r(), color.g(), color.b(), 255]),
margin as i32,
margin as i32,
scale,
font,
text,
);
self.copy_rotated_image(rgba_img, &temp_img, center_x, center_y, angle_degrees);
}
fn copy_rotated_image(
&self,
target: &mut RgbaImage,
source: &RgbaImage,
center_x: i32,
center_y: i32,
angle_degrees: f32,
) {
let angle_rad = angle_degrees.to_radians();
let cos_angle = angle_rad.cos();
let sin_angle = angle_rad.sin();
let source_width = source.width() as i32;
let source_height = source.height() as i32;
let target_width = target.width() as i32;
let target_height = target.height() as i32;
let half_width = source_width / 2;
let half_height = source_height / 2;
for y in 0..source_height {
for x in 0..source_width {
let rel_x = x - half_width;
let rel_y = y - half_height;
let new_x = (rel_x as f32 * cos_angle - rel_y as f32 * sin_angle) as i32;
let new_y = (rel_x as f32 * sin_angle + rel_y as f32 * cos_angle) as i32;
let target_x = center_x + new_x;
let target_y = center_y + new_y;
if target_x >= 0
&& target_x < target_width
&& target_y >= 0
&& target_y < target_height
{
let source_pixel = source.get_pixel(x as u32, y as u32);
if source_pixel[3] > 0 {
target.put_pixel(target_x as u32, target_y as u32, *source_pixel);
}
}
}
}
}
}