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.
This commit is contained in:
2026-03-06 17:12:23 +02:00
parent 3109f97786
commit d9ce1f8731
5 changed files with 95 additions and 3 deletions

View File

@@ -313,6 +313,7 @@ fn cmd_process(args: CmdProcessArgs) {
font_size: 24.0, font_size: 24.0,
opacity: args.watermark_opacity, opacity: args.watermark_opacity,
color: [255, 255, 255, 255], color: [255, 255, 255, 255],
font_family: None,
}); });
} }
if args.rename_prefix.is_some() || args.rename_suffix.is_some() || args.rename_template.is_some() { if args.rename_prefix.is_some() || args.rename_suffix.is_some() || args.rename_template.is_some() {

View File

@@ -203,6 +203,7 @@ pub enum WatermarkConfig {
font_size: f32, font_size: f32,
opacity: f32, opacity: f32,
color: [u8; 4], color: [u8; 4],
font_family: Option<String>,
}, },
Image { Image {
path: std::path::PathBuf, path: std::path::PathBuf,

View File

@@ -46,7 +46,8 @@ pub fn apply_watermark(
font_size, font_size,
opacity, opacity,
color, 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 { WatermarkConfig::Image {
path, path,
position, position,
@@ -56,7 +57,38 @@ pub fn apply_watermark(
} }
} }
fn find_system_font() -> Result<Vec<u8>> { fn find_system_font(family: Option<&str>) -> Result<Vec<u8>> {
// 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 = [ let candidates = [
"/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
"/usr/share/fonts/TTF/DejaVuSans.ttf", "/usr/share/fonts/TTF/DejaVuSans.ttf",
@@ -78,6 +110,25 @@ fn find_system_font() -> Result<Vec<u8>> {
}) })
} }
/// Recursively walk a directory and collect file paths
fn walkdir(dir: &std::path::Path) -> std::io::Result<Vec<std::path::PathBuf>> {
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( fn apply_text_watermark(
img: DynamicImage, img: DynamicImage,
text: &str, text: &str,
@@ -85,8 +136,9 @@ fn apply_text_watermark(
font_size: f32, font_size: f32,
opacity: f32, opacity: f32,
color: [u8; 4], color: [u8; 4],
font_family: Option<&str>,
) -> Result<DynamicImage> { ) -> Result<DynamicImage> {
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(|_| { let font = ab_glyph::FontArc::try_from_vec(font_data).map_err(|_| {
PixstripError::Processing { PixstripError::Processing {
operation: "watermark".into(), operation: "watermark".into(),

View File

@@ -66,6 +66,7 @@ pub struct JobConfig {
pub watermark_opacity: f32, pub watermark_opacity: f32,
pub watermark_font_size: f32, pub watermark_font_size: f32,
pub watermark_color: [u8; 4], pub watermark_color: [u8; 4],
pub watermark_font_family: String,
pub watermark_use_image: bool, pub watermark_use_image: bool,
// Rename // Rename
pub rename_enabled: bool, pub rename_enabled: bool,
@@ -360,6 +361,7 @@ fn build_ui(app: &adw::Application) {
watermark_opacity: 0.5, watermark_opacity: 0.5,
watermark_font_size: 24.0, watermark_font_size: 24.0,
watermark_color: [255, 255, 255, 255], watermark_color: [255, 255, 255, 255],
watermark_font_family: String::new(),
watermark_use_image: false, watermark_use_image: false,
rename_enabled: if remember { sess_state.rename_enabled.unwrap_or(false) } else { false }, rename_enabled: if remember { sess_state.rename_enabled.unwrap_or(false) } else { false },
rename_prefix: String::new(), rename_prefix: String::new(),
@@ -1666,6 +1668,7 @@ fn run_processing(_window: &adw::ApplicationWindow, ui: &WizardUi) {
font_size: cfg.watermark_font_size, font_size: cfg.watermark_font_size,
opacity: cfg.watermark_opacity, opacity: cfg.watermark_opacity,
color: cfg.watermark_color, 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, font_size: cfg.watermark_font_size,
opacity: cfg.watermark_opacity, opacity: cfg.watermark_opacity,
color: cfg.watermark_color, color: cfg.watermark_color,
font_family: if cfg.watermark_font_family.is_empty() { None } else { Some(cfg.watermark_font_family.clone()) },
}) })
} else { } else {
None None

View File

@@ -61,7 +61,31 @@ pub fn build_watermark_page(state: &AppState) -> adw::NavigationPage {
.adjustment(&gtk::Adjustment::new(cfg.watermark_font_size as f64, 8.0, 200.0, 1.0, 10.0, 0.0)) .adjustment(&gtk::Adjustment::new(cfg.watermark_font_size as f64, 8.0, 200.0, 1.0, 10.0, 0.0))
.build(); .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(&text_row);
text_group.add(&font_row);
text_group.add(&font_size_row); text_group.add(&font_size_row);
content.append(&text_group); 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; 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 // Wire position grid buttons
for (i, btn) in buttons.iter().enumerate() { for (i, btn) in buttons.iter().enumerate() {
let jc = state.job_config.clone(); let jc = state.job_config.clone();