From e1f2c8efc8cb17a5b6a56c26021c18c62f2cf5ee Mon Sep 17 00:00:00 2001 From: Julian Freeman Date: Mon, 19 Jan 2026 08:41:45 -0400 Subject: [PATCH] apply custom text --- src-tauri/src/lib.rs | 181 +++++++++++++++++++++++-------- src/components/SettingsPanel.vue | 27 +++-- src/stores/gallery.ts | 27 ++++- 3 files changed, 178 insertions(+), 57 deletions(-) diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 977af33..dec8289 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -170,8 +170,8 @@ async fn export_batch(images: Vec, watermark: WatermarkSettings let scaled_font = PxScale::from(scale_px); let (t_width, _t_height) = imageproc::drawing::text_size(scaled_font, &font, &watermark.text); - // 3. Ensure it fits width (Padding 5%) - let max_width = (width as f32 * 0.95) as u32; + // 3. Ensure it fits width (Padding 10%) + let max_width = (width as f32 * 0.90) as u32; if t_width > max_width { let ratio = max_width as f32 / t_width as f32; scale_px *= ratio; @@ -277,56 +277,94 @@ async fn export_batch(images: Vec, watermark: WatermarkSettings fn calculate_zca_internal(img: &image::DynamicImage) -> Result { let (width, height) = img.dimensions(); - let bottom_start_y = (height as f64 * 0.8) as u32; - let zone_height = height - bottom_start_y; - let zone_width = width / 3; + // Greedy Layered Search + // Priority: Bottom -> Up + let y_levels = [0.97, 0.94, 0.91, 0.88]; + let x_cols = [1.0/6.0, 3.0/6.0, 5.0/6.0]; // Left, Center, Right centers + let col_names = ["Left", "Center", "Right"]; - let zones = [ - ("Left", 0, bottom_start_y), - ("Center", zone_width, bottom_start_y), - ("Right", zone_width * 2, bottom_start_y), - ]; + // Box Size for analysis (approx watermark size) + let box_w = (width as f64 * 0.30) as u32; + let box_h = (height as f64 * 0.05) as u32; + let half_box_w = box_w / 2; + let half_box_h = box_h / 2; - let mut min_std_dev = f64::MAX; - let mut best_zone = "Center"; - let mut best_pos = (0.5, 0.97); + let mut global_best_score = f64::MAX; + let mut global_best_result = ZcaResult { x: 0.5, y: 0.97, zone: "Center".to_string() }; - for (name, start_x, start_y) in zones.iter() { - let mut luma_values = Vec::with_capacity((zone_width * zone_height) as usize); - - for y in *start_y..height { - for x in *start_x..(*start_x + zone_width) { - if x >= width { continue; } - let pixel = img.get_pixel(x, y); - let rgb = pixel.to_rgb(); - let luma = 0.299 * rgb[0] as f64 + 0.587 * rgb[1] as f64 + 0.114 * rgb[2] as f64; - luma_values.push(luma); + for &y_pct in y_levels.iter() { + let mut row_best_score = f64::MAX; + let mut row_best_idx = 1; // Default Center + let mut row_stats = Vec::new(); // (mean, std_dev) + + for (col_idx, &x_pct) in x_cols.iter().enumerate() { + let cx = (width as f64 * x_pct) as u32; + let cy = (height as f64 * y_pct) as u32; + + let start_x = if cx > half_box_w { cx - half_box_w } else { 0 }; + let start_y = if cy > half_box_h { cy - half_box_h } else { 0 }; + let end_x = (start_x + box_w).min(width); + let end_y = (start_y + box_h).min(height); + + let mut luma_values = Vec::with_capacity((box_w * box_h) as usize); + + for y in start_y..end_y { + for x in start_x..end_x { + let pixel = img.get_pixel(x, y); + let rgb = pixel.to_rgb(); + let luma = 0.299 * rgb[0] as f64 + 0.587 * rgb[1] as f64 + 0.114 * rgb[2] as f64; + luma_values.push(luma); + } + } + + let count = luma_values.len() as f64; + if count == 0.0 { + row_stats.push((0.0, f64::MAX)); + continue; + } + + let mean = luma_values.iter().sum::() / count; + let variance = luma_values.iter().map(|v| (v - mean).powi(2)).sum::() / count; + let std_dev = variance.sqrt(); + + row_stats.push((mean, std_dev)); + + // For choosing "Best in Row", we strictly prefer Flatness (StdDev) + if std_dev < row_best_score { + row_best_score = std_dev; + row_best_idx = col_idx; + } + + // Update Global Best (fallback) + if std_dev < global_best_score { + global_best_score = std_dev; + global_best_result = ZcaResult { + x: x_pct, + y: y_pct, + zone: col_names[col_idx].to_string(), + }; } } - let count = luma_values.len() as f64; - if count == 0.0 { continue; } - - let mean = luma_values.iter().sum::() / count; - let variance = luma_values.iter().map(|v| (v - mean).powi(2)).sum::() / count; - let std_dev = variance.sqrt(); + // Analyze the Best Zone in this Row + let (mean, std_dev) = row_stats[row_best_idx]; - if std_dev < min_std_dev { - min_std_dev = std_dev; - best_zone = name; - let center_x_px = *start_x as f64 + (zone_width as f64 / 2.0); - // Position closer to bottom - // 0.8 + 0.2 * 0.85 = 0.97 - let center_y_px = *start_y as f64 + (zone_height as f64 * 0.85); - best_pos = (center_x_px / width as f64, center_y_px / height as f64); + // Safety Check: Is this zone "White Text"? + // Condition: Mean > 180 (Bright-ish) AND StdDev > 20 (Busy/Text) + let is_unsafe_white_text = mean > 180.0 && std_dev > 20.0; + let is_unsafe_bright = mean > 230.0; + + if !is_unsafe_white_text && !is_unsafe_bright { + // Safe! + return Ok(ZcaResult { + x: x_cols[row_best_idx], + y: y_pct, + zone: col_names[row_best_idx].to_string(), + }); } } - Ok(ZcaResult { - x: best_pos.0, - y: best_pos.1, - zone: best_zone.to_string(), - }) + Ok(global_best_result) } #[tauri::command] @@ -335,6 +373,63 @@ fn get_zca_suggestion(path: String) -> Result { calculate_zca_internal(&img) } +#[derive(serde::Serialize)] +struct LayoutResult { + x: f64, + y: f64, + scale: f64, +} + +#[tauri::command] +async fn layout_watermark(path: String, text: String, base_scale: f64) -> Result { + let img = image::open(&path).map_err(|e| e.to_string())?; + let (width, height) = img.dimensions(); + let font = FontRef::try_from_slice(FONT_DATA).map_err(|e| format!("Font error: {}", e))?; + + // 1. Run ZCA to find best zone center (now with dark preference) + let zca = calculate_zca_internal(&img)?; + + // 2. Calculate Text Dimensions at Base Scale + let mut scale_val = base_scale; + let mut scale_px = height as f32 * scale_val as f32; + let mut font_scale = PxScale::from(scale_px); + let (mut t_width, mut t_height) = imageproc::drawing::text_size(font_scale, &font, &text); + + // 3. Auto-Fit Width (Limit to 90% of image width) + let max_width = (width as f32 * 0.90) as u32; + if t_width > max_width { + let ratio = max_width as f32 / t_width as f32; + scale_val *= ratio as f64; + scale_px *= ratio; + font_scale = PxScale::from(scale_px); + let dims = imageproc::drawing::text_size(font_scale, &font, &text); + t_width = dims.0; + t_height = dims.1; + } + + // 4. Smart Clamping + let center_x = zca.x * width as f64; + let center_y = zca.y * height as f64; + + let half_w = t_width as f64 / 2.0; + let half_h = t_height as f64 / 2.0; + let padding = width as f64 * 0.02; + + let min_x = half_w + padding; + let max_x = width as f64 - half_w - padding; + let final_x = center_x.clamp(min_x, max_x); + + let min_y = half_h + padding; + let max_y = height as f64 - half_h - padding; + let final_y = center_y.clamp(min_y, max_y); + + Ok(LayoutResult { + x: final_x / width as f64, + y: final_y / height as f64, + scale: scale_val, + }) +} + #[derive(serde::Serialize)] struct DetectionResult { rects: Vec, @@ -453,7 +548,7 @@ async fn detect_watermark(path: String) -> Result { pub fn run() { tauri::Builder::default() .plugin(tauri_plugin_dialog::init()) - .invoke_handler(tauri::generate_handler![scan_dir, get_zca_suggestion, export_batch, detect_watermark]) + .invoke_handler(tauri::generate_handler![scan_dir, get_zca_suggestion, export_batch, detect_watermark, layout_watermark]) .run(tauri::generate_context!()) .expect("error while running tauri application"); } diff --git a/src/components/SettingsPanel.vue b/src/components/SettingsPanel.vue index 381dd43..1db7ecb 100644 --- a/src/components/SettingsPanel.vue +++ b/src/components/SettingsPanel.vue @@ -1,6 +1,6 @@