Fix 5 deferred performance/UX issues from audit
M8: Pre-compile regex once before rename preview loop instead of recompiling per file. Adds apply_simple_compiled() to RenameConfig. M9: Cache font data in watermark module using OnceLock (default font) and Mutex<HashMap> (named fonts) to avoid repeated filesystem walks during preview updates. M12: Add 150ms debounce to watermark opacity, rotation, margin, and scale sliders to avoid spawning preview threads on every pixel of slider movement. M13: Add 150ms debounce to compress per-format quality sliders (JPEG, PNG, WebP, AVIF) for the same reason. M14: Move thumbnail loading to background threads instead of blocking the GTK main loop. Each thumbnail is decoded via image crate in a spawned thread and delivered to the main thread via channel polling.
This commit is contained in:
@@ -318,6 +318,11 @@ pub struct RenameConfig {
|
||||
fn default_counter_position() -> u32 { 3 }
|
||||
|
||||
impl RenameConfig {
|
||||
/// Pre-compile the regex for batch use. Call once before a loop of apply_simple_compiled.
|
||||
pub fn compile_regex(&self) -> Option<regex::Regex> {
|
||||
rename::compile_rename_regex(&self.regex_find)
|
||||
}
|
||||
|
||||
pub fn apply_simple(&self, original_name: &str, extension: &str, index: u32) -> String {
|
||||
// 1. Apply regex find-and-replace on the original name
|
||||
let working_name = rename::apply_regex_replace(original_name, &self.regex_find, &self.regex_replace);
|
||||
@@ -378,4 +383,68 @@ impl RenameConfig {
|
||||
|
||||
format!("{}.{}", result, extension)
|
||||
}
|
||||
|
||||
/// Like apply_simple but uses a pre-compiled regex (avoids recompiling per file).
|
||||
pub fn apply_simple_compiled(&self, original_name: &str, extension: &str, index: u32, compiled_re: Option<®ex::Regex>) -> String {
|
||||
// 1. Apply regex find-and-replace on the original name
|
||||
let working_name = match compiled_re {
|
||||
Some(re) => rename::apply_regex_replace_compiled(original_name, re, &self.regex_replace),
|
||||
None => original_name.to_string(),
|
||||
};
|
||||
|
||||
// 2. Apply space replacement
|
||||
let working_name = rename::apply_space_replacement(&working_name, self.replace_spaces);
|
||||
|
||||
// 3. Apply special character filtering
|
||||
let working_name = rename::apply_special_chars(&working_name, self.special_chars);
|
||||
|
||||
// 4. Build counter string
|
||||
let counter_str = if self.counter_enabled {
|
||||
let counter = self.counter_start.saturating_add(index.saturating_sub(1));
|
||||
let padding = (self.counter_padding as usize).min(10);
|
||||
format!("{:0>width$}", counter, width = padding)
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
|
||||
let has_counter = self.counter_enabled && !counter_str.is_empty();
|
||||
|
||||
// 5. Assemble parts based on counter position
|
||||
let mut result = String::new();
|
||||
|
||||
if has_counter && self.counter_position == 0 {
|
||||
result.push_str(&counter_str);
|
||||
result.push('_');
|
||||
}
|
||||
|
||||
result.push_str(&self.prefix);
|
||||
|
||||
if has_counter && self.counter_position == 1 {
|
||||
result.push_str(&counter_str);
|
||||
result.push('_');
|
||||
}
|
||||
|
||||
if has_counter && self.counter_position == 4 {
|
||||
result.push_str(&counter_str);
|
||||
} else {
|
||||
result.push_str(&working_name);
|
||||
}
|
||||
|
||||
if has_counter && self.counter_position == 2 {
|
||||
result.push('_');
|
||||
result.push_str(&counter_str);
|
||||
}
|
||||
|
||||
result.push_str(&self.suffix);
|
||||
|
||||
if has_counter && self.counter_position == 3 {
|
||||
result.push('_');
|
||||
result.push_str(&counter_str);
|
||||
}
|
||||
|
||||
// 6. Apply case conversion
|
||||
let result = rename::apply_case_conversion(&result, self.case_mode);
|
||||
|
||||
format!("{}.{}", result, extension)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,16 +75,34 @@ pub fn apply_watermark(
|
||||
}
|
||||
}
|
||||
|
||||
/// Cache for font data to avoid repeated filesystem walks during preview updates
|
||||
static FONT_CACHE: std::sync::Mutex<Option<FontCache>> = std::sync::Mutex::new(None);
|
||||
static DEFAULT_FONT_CACHE: std::sync::OnceLock<Option<Vec<u8>>> = std::sync::OnceLock::new();
|
||||
|
||||
struct FontCache {
|
||||
entries: std::collections::HashMap<String, 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 a specific font family was requested, check the cache first
|
||||
if let Some(name) = family {
|
||||
if !name.is_empty() {
|
||||
// Try common font paths with the family name
|
||||
let name_lower = name.to_lowercase();
|
||||
|
||||
// Check cache
|
||||
if let Ok(cache) = FONT_CACHE.lock() {
|
||||
if let Some(ref c) = *cache {
|
||||
if let Some(data) = c.entries.get(&name_lower) {
|
||||
return Ok(data.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Cache miss - search filesystem
|
||||
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 {
|
||||
@@ -97,6 +115,13 @@ fn find_system_font(family: Option<&str>) -> Result<Vec<u8>> {
|
||||
&& (file_name.contains("regular") || (!file_name.contains("bold") && !file_name.contains("italic")))
|
||||
{
|
||||
if let Ok(data) = std::fs::read(&path) {
|
||||
// Store in cache
|
||||
if let Ok(mut cache) = FONT_CACHE.lock() {
|
||||
let c = cache.get_or_insert_with(|| FontCache {
|
||||
entries: std::collections::HashMap::new(),
|
||||
});
|
||||
c.entries.insert(name_lower, data.clone());
|
||||
}
|
||||
return Ok(data);
|
||||
}
|
||||
}
|
||||
@@ -106,26 +131,31 @@ fn find_system_font(family: Option<&str>) -> Result<Vec<u8>> {
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back to default system fonts
|
||||
let candidates = [
|
||||
"/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
|
||||
"/usr/share/fonts/TTF/DejaVuSans.ttf",
|
||||
"/usr/share/fonts/dejavu-sans-fonts/DejaVuSans.ttf",
|
||||
"/usr/share/fonts/truetype/liberation/LiberationSans-Regular.ttf",
|
||||
"/usr/share/fonts/truetype/noto/NotoSans-Regular.ttf",
|
||||
"/usr/share/fonts/noto/NotoSans-Regular.ttf",
|
||||
];
|
||||
|
||||
for path in &candidates {
|
||||
if let Ok(data) = std::fs::read(path) {
|
||||
return Ok(data);
|
||||
// Fall back to default system fonts (cached via OnceLock)
|
||||
let default = DEFAULT_FONT_CACHE.get_or_init(|| {
|
||||
let candidates = [
|
||||
"/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
|
||||
"/usr/share/fonts/TTF/DejaVuSans.ttf",
|
||||
"/usr/share/fonts/dejavu-sans-fonts/DejaVuSans.ttf",
|
||||
"/usr/share/fonts/truetype/liberation/LiberationSans-Regular.ttf",
|
||||
"/usr/share/fonts/truetype/noto/NotoSans-Regular.ttf",
|
||||
"/usr/share/fonts/noto/NotoSans-Regular.ttf",
|
||||
];
|
||||
for path in &candidates {
|
||||
if let Ok(data) = std::fs::read(path) {
|
||||
return Some(data);
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
});
|
||||
|
||||
Err(PixstripError::Processing {
|
||||
operation: "watermark".into(),
|
||||
reason: "No system font found for text watermark".into(),
|
||||
})
|
||||
match default {
|
||||
Some(data) => Ok(data.clone()),
|
||||
None => Err(PixstripError::Processing {
|
||||
operation: "watermark".into(),
|
||||
reason: "No system font found for text watermark".into(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
/// Recursively walk a directory and collect file paths (max depth 5)
|
||||
|
||||
Reference in New Issue
Block a user