diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index ae11618..10453ff 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -144,7 +144,7 @@ fn parse_hex_color(hex: &str) -> image::Rgba { } #[tauri::command] -async fn export_batch(images: Vec, watermark: WatermarkSettings, output_dir: String) -> Result { +async fn export_batch(images: Vec, watermark: WatermarkSettings, output_dir: String, mode: String) -> Result { let font = FontRef::try_from_slice(FONT_DATA).map_err(|e| format!("Font error: {}", e))?; // Note: Settings are now resolved per-task @@ -157,92 +157,98 @@ async fn export_batch(images: Vec, watermark: WatermarkSettings let mut base_img = dynamic_img.to_rgba8(); let (width, height) = base_img.dimensions(); - // Determine effective settings (Task > Global) - let eff_scale = task.scale.unwrap_or(watermark.scale); - let eff_opacity = task.opacity.unwrap_or(watermark.opacity); - let eff_color_hex = task.color.as_ref().unwrap_or(&watermark.color); + // ONLY EXECUTE WATERMARK LOGIC IF MODE IS 'ADD' + if mode == "add" { + // Determine effective settings (Task > Global) + let eff_scale = task.scale.unwrap_or(watermark.scale); + let eff_opacity = task.opacity.unwrap_or(watermark.opacity); + let eff_color_hex = task.color.as_ref().unwrap_or(&watermark.color); - // Calculate final color - let base_color = parse_hex_color(eff_color_hex); - let alpha = (eff_opacity * 255.0) as u8; - let text_color = image::Rgba([base_color[0], base_color[1], base_color[2], alpha]); + // Calculate final color + let base_color = parse_hex_color(eff_color_hex); + let alpha = (eff_opacity * 255.0) as u8; + let text_color = image::Rgba([base_color[0], base_color[1], base_color[2], alpha]); - // 1. Calculate Font Scale based on Image Height - let mut scale_px = height as f32 * eff_scale as f32; - - // 2. Measure Text - 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 15%) - let max_width = (width as f32 * 0.85) as u32; - if t_width > max_width { - let ratio = max_width as f32 / t_width as f32; - scale_px *= ratio; - } - let final_scale = PxScale::from(scale_px); - let (final_t_width, final_t_height) = imageproc::drawing::text_size(final_scale, &font, &watermark.text); + // 1. Calculate Font Scale based on Image Height + let mut scale_px = height as f32 * eff_scale as f32; + + // 2. Measure Text + 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 15%) + let max_width = (width as f32 * 0.85) as u32; + if t_width > max_width { + let ratio = max_width as f32 / t_width as f32; + scale_px *= ratio; + } + let final_scale = PxScale::from(scale_px); + let (final_t_width, final_t_height) = imageproc::drawing::text_size(final_scale, &font, &watermark.text); - // 4. Determine Position (Task Specific > ZCA) - // If task has manual_position, use it. Otherwise calculate ZCA. - let (pos_x_pct, pos_y_pct) = if let Some(pos) = &task.manual_position { - (pos.x, pos.y) - } else { - match calculate_zca_internal(&dynamic_img) { - Ok(res) => (res.x, res.y), - Err(_) => (0.5, 0.97), - } - }; + // 4. Determine Position (Task Specific > ZCA) + // If task has manual_position, use it. Otherwise calculate ZCA. + let (pos_x_pct, pos_y_pct) = if let Some(pos) = &task.manual_position { + (pos.x, pos.y) + } else { + match calculate_zca_internal(&dynamic_img) { + Ok(res) => (res.x, res.y), + Err(_) => (0.5, 0.97), + } + }; - // Calculate initial top-left based on center - let center_x = width as f64 * pos_x_pct; - let center_y = height as f64 * pos_y_pct; + // Calculate initial top-left based on center + let center_x = width as f64 * pos_x_pct; + let center_y = height as f64 * pos_y_pct; - let mut x = (center_x - (final_t_width as f64 / 2.0)) as i32; - let mut y = (center_y - (final_t_height as f64 / 2.0)) as i32; - - // 5. Strict Boundary Clamping - // We ensure the text box (final_t_width, final_t_height) is always inside (0, 0, width, height) - let min_padding = 2; // Absolute minimum pixels from edge - - if x < min_padding { x = min_padding; } - if y < min_padding { y = min_padding; } - if x + final_t_width as i32 > width as i32 - min_padding { - x = width as i32 - final_t_width as i32 - min_padding; - } - if y + final_t_height as i32 > height as i32 - min_padding { - y = height as i32 - final_t_height as i32 - min_padding; - } - - // Re-clamp just in case of very small images where text is larger than image - x = x.max(0); - y = y.max(0); - - // 6. Draw Stroke (Simple 4-direction offset for black outline) - // Stroke alpha should match text alpha - let stroke_color = image::Rgba([0, 0, 0, text_color[3]]); - for offset in [(-1, -1), (-1, 1), (1, -1), (1, 1)] { - draw_text_mut( - &mut base_img, - stroke_color, - x + offset.0, - y + offset.1, - final_scale, - &font, - &watermark.text, - ); - } + let mut x = (center_x - (final_t_width as f64 / 2.0)) as i32; + let mut y = (center_y - (final_t_height as f64 / 2.0)) as i32; + + // 5. Strict Boundary Clamping + // We ensure the text box (final_t_width, final_t_height) is always inside (0, 0, width, height) + let min_padding = 2; // Absolute minimum pixels from edge + + if x < min_padding { x = min_padding; } + if y < min_padding { y = min_padding; } + if x + final_t_width as i32 > width as i32 - min_padding { + x = width as i32 - final_t_width as i32 - min_padding; + } + if y + final_t_height as i32 > height as i32 - min_padding { + y = height as i32 - final_t_height as i32 - min_padding; + } + + // Re-clamp just in case of very small images where text is larger than image + x = x.max(0); + y = y.max(0); + + // SKIP DRAWING if text is empty (e.g. Remove Mode) + if !watermark.text.trim().is_empty() { + // 6. Draw Stroke (Simple 4-direction offset for black outline) + // Stroke alpha should match text alpha + let stroke_color = image::Rgba([0, 0, 0, text_color[3]]); + for offset in [(-1, -1), (-1, 1), (1, -1), (1, 1)] { + draw_text_mut( + &mut base_img, + stroke_color, + x + offset.0, + y + offset.1, + final_scale, + &font, + &watermark.text, + ); + } - // 7. Draw Main Text - draw_text_mut( - &mut base_img, - text_color, - x, - y, - final_scale, - &font, - &watermark.text, - ); + // 7. Draw Main Text + draw_text_mut( + &mut base_img, + text_color, + x, + y, + final_scale, + &font, + &watermark.text, + ); + } + } // END IF MODE == ADD // Save let file_name = input_path.file_name().unwrap_or_default(); diff --git a/src/App.vue b/src/App.vue index 19cb1e0..1837287 100644 --- a/src/App.vue +++ b/src/App.vue @@ -32,7 +32,9 @@ async function openFolder() { async function exportBatch() { if (store.images.length === 0) return; - if (!store.watermarkSettings.text) { + + // Only require text if in ADD mode + if (store.editMode === 'add' && !store.watermarkSettings.text) { alert("请输入水印文字。"); return; } @@ -67,7 +69,8 @@ async function exportBatch() { await invoke('export_batch', { images: exportTasks, watermark: rustWatermarkSettings, - outputDir: outputDir + outputDir: outputDir, + mode: store.editMode }); alert("批量导出完成!"); }