diff --git a/pixstrip-cli/src/main.rs b/pixstrip-cli/src/main.rs index 4d86568..2d95dad 100644 --- a/pixstrip-cli/src/main.rs +++ b/pixstrip-cli/src/main.rs @@ -313,6 +313,7 @@ fn cmd_process(args: CmdProcessArgs) { font_size: 24.0, opacity: args.watermark_opacity, color: [255, 255, 255, 255], + font_family: None, }); } if args.rename_prefix.is_some() || args.rename_suffix.is_some() || args.rename_template.is_some() { diff --git a/pixstrip-core/src/operations/mod.rs b/pixstrip-core/src/operations/mod.rs index df2abe2..da955c6 100644 --- a/pixstrip-core/src/operations/mod.rs +++ b/pixstrip-core/src/operations/mod.rs @@ -203,6 +203,7 @@ pub enum WatermarkConfig { font_size: f32, opacity: f32, color: [u8; 4], + font_family: Option, }, Image { path: std::path::PathBuf, diff --git a/pixstrip-core/src/operations/watermark.rs b/pixstrip-core/src/operations/watermark.rs index 2fc73eb..18e5dbd 100644 --- a/pixstrip-core/src/operations/watermark.rs +++ b/pixstrip-core/src/operations/watermark.rs @@ -46,7 +46,8 @@ pub fn apply_watermark( font_size, opacity, color, - } => apply_text_watermark(img, text, *position, *font_size, *opacity, *color), + font_family, + } => apply_text_watermark(img, text, *position, *font_size, *opacity, *color, font_family.as_deref()), WatermarkConfig::Image { path, position, @@ -56,7 +57,38 @@ pub fn apply_watermark( } } -fn find_system_font() -> Result> { +fn find_system_font(family: Option<&str>) -> Result> { + // If a specific font family was requested, try to find it via fontconfig + if let Some(name) = family { + if !name.is_empty() { + // Try common font paths with the family name + let search_dirs = [ + "/usr/share/fonts", + "/usr/local/share/fonts", + ]; + let name_lower = name.to_lowercase(); + for dir in &search_dirs { + if let Ok(entries) = walkdir(std::path::Path::new(dir)) { + for path in entries { + let file_name = path.file_name() + .and_then(|n| n.to_str()) + .unwrap_or("") + .to_lowercase(); + if file_name.contains(&name_lower) + && (file_name.ends_with(".ttf") || file_name.ends_with(".otf")) + && (file_name.contains("regular") || !file_name.contains("bold") && !file_name.contains("italic")) + { + if let Ok(data) = std::fs::read(&path) { + return Ok(data); + } + } + } + } + } + } + } + + // Fall back to default system fonts let candidates = [ "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", "/usr/share/fonts/TTF/DejaVuSans.ttf", @@ -78,6 +110,25 @@ fn find_system_font() -> Result> { }) } +/// Recursively walk a directory and collect file paths +fn walkdir(dir: &std::path::Path) -> std::io::Result> { + let mut results = Vec::new(); + if dir.is_dir() { + for entry in std::fs::read_dir(dir)? { + let entry = entry?; + let path = entry.path(); + if path.is_dir() { + if let Ok(sub) = walkdir(&path) { + results.extend(sub); + } + } else { + results.push(path); + } + } + } + Ok(results) +} + fn apply_text_watermark( img: DynamicImage, text: &str, @@ -85,8 +136,9 @@ fn apply_text_watermark( font_size: f32, opacity: f32, color: [u8; 4], + font_family: Option<&str>, ) -> Result { - let font_data = find_system_font()?; + let font_data = find_system_font(font_family)?; let font = ab_glyph::FontArc::try_from_vec(font_data).map_err(|_| { PixstripError::Processing { operation: "watermark".into(), diff --git a/pixstrip-gtk/src/app.rs b/pixstrip-gtk/src/app.rs index 74e6f30..3d5d9cd 100644 --- a/pixstrip-gtk/src/app.rs +++ b/pixstrip-gtk/src/app.rs @@ -66,6 +66,7 @@ pub struct JobConfig { pub watermark_opacity: f32, pub watermark_font_size: f32, pub watermark_color: [u8; 4], + pub watermark_font_family: String, pub watermark_use_image: bool, // Rename pub rename_enabled: bool, @@ -360,6 +361,7 @@ fn build_ui(app: &adw::Application) { watermark_opacity: 0.5, watermark_font_size: 24.0, watermark_color: [255, 255, 255, 255], + watermark_font_family: String::new(), watermark_use_image: false, rename_enabled: if remember { sess_state.rename_enabled.unwrap_or(false) } else { false }, rename_prefix: String::new(), @@ -1666,6 +1668,7 @@ fn run_processing(_window: &adw::ApplicationWindow, ui: &WizardUi) { font_size: cfg.watermark_font_size, opacity: cfg.watermark_opacity, color: cfg.watermark_color, + font_family: if cfg.watermark_font_family.is_empty() { None } else { Some(cfg.watermark_font_family.clone()) }, }); } } @@ -2595,6 +2598,7 @@ fn build_preset_from_config(cfg: &JobConfig, name: &str) -> pixstrip_core::prese font_size: cfg.watermark_font_size, opacity: cfg.watermark_opacity, color: cfg.watermark_color, + font_family: if cfg.watermark_font_family.is_empty() { None } else { Some(cfg.watermark_font_family.clone()) }, }) } else { None diff --git a/pixstrip-gtk/src/steps/step_watermark.rs b/pixstrip-gtk/src/steps/step_watermark.rs index 62d5363..6e2f57a 100644 --- a/pixstrip-gtk/src/steps/step_watermark.rs +++ b/pixstrip-gtk/src/steps/step_watermark.rs @@ -61,7 +61,31 @@ pub fn build_watermark_page(state: &AppState) -> adw::NavigationPage { .adjustment(>k::Adjustment::new(cfg.watermark_font_size as f64, 8.0, 200.0, 1.0, 10.0, 0.0)) .build(); + // Font family picker + let font_row = adw::ActionRow::builder() + .title("Font Family") + .subtitle("Choose a typeface for the watermark text") + .build(); + + let font_dialog = gtk::FontDialog::builder() + .title("Choose Watermark Font") + .modal(true) + .build(); + let font_button = gtk::FontDialogButton::builder() + .dialog(&font_dialog) + .valign(gtk::Align::Center) + .build(); + + // Set initial font if one was previously selected + if !cfg.watermark_font_family.is_empty() { + let desc = gtk::pango::FontDescription::from_string(&cfg.watermark_font_family); + font_button.set_font_desc(&desc); + } + + font_row.add_suffix(&font_button); + text_group.add(&text_row); + text_group.add(&font_row); text_group.add(&font_size_row); content.append(&text_group); @@ -415,6 +439,16 @@ pub fn build_watermark_page(state: &AppState) -> adw::NavigationPage { jc.borrow_mut().watermark_font_size = row.value() as f32; }); } + // Wire font family picker + { + let jc = state.job_config.clone(); + font_button.connect_font_desc_notify(move |btn| { + let desc = btn.font_desc(); + if let Some(family) = desc.family() { + jc.borrow_mut().watermark_font_family = family.to_string(); + } + }); + } // Wire position grid buttons for (i, btn) in buttons.iter().enumerate() { let jc = state.job_config.clone();