From d9ce1f873150b87b77589f1e92211e0884ee3025 Mon Sep 17 00:00:00 2001 From: lashman Date: Fri, 6 Mar 2026 17:12:23 +0200 Subject: [PATCH] Add font family selector for watermark text Font picker using GTK FontDialog/FontDialogButton lets users choose any installed system font for text watermarks. The selected font family is passed through the processing pipeline and used to find the matching font file on disk. --- pixstrip-cli/src/main.rs | 1 + pixstrip-core/src/operations/mod.rs | 1 + pixstrip-core/src/operations/watermark.rs | 58 +++++++++++++++++++++-- pixstrip-gtk/src/app.rs | 4 ++ pixstrip-gtk/src/steps/step_watermark.rs | 34 +++++++++++++ 5 files changed, 95 insertions(+), 3 deletions(-) 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();