This commit is contained in:
Julian Freeman
2026-01-19 07:48:32 -04:00
parent 358aae92dc
commit 16bb3e5135
4 changed files with 466 additions and 96 deletions

View File

@@ -335,11 +335,125 @@ fn get_zca_suggestion(path: String) -> Result<ZcaResult, String> {
calculate_zca_internal(&img)
}
#[derive(serde::Serialize)]
struct DetectionResult {
rects: Vec<Rect>,
}
#[derive(serde::Serialize, Clone)]
struct Rect {
x: f64,
y: f64,
width: f64,
height: f64,
}
#[tauri::command]
async fn detect_watermark(path: String) -> Result<DetectionResult, String> {
let img = image::open(&path).map_err(|e| e.to_string())?;
let (width, height) = img.dimensions();
let gray = img.to_luma8();
// Heuristic:
// 1. Scan Top 20% and Bottom 20%
// 2. Look for high brightness pixels (> 230)
// 3. Grid based clustering
let cell_size = 10;
let grid_w = (width + cell_size - 1) / cell_size;
let grid_h = (height + cell_size - 1) / cell_size;
let mut grid = vec![false; (grid_w * grid_h) as usize];
let top_limit = (height as f64 * 0.2) as u32;
let bottom_start = (height as f64 * 0.8) as u32;
for y in 0..height {
// Skip middle section
if y > top_limit && y < bottom_start {
continue;
}
for x in 0..width {
let p = gray.get_pixel(x, y);
if p[0] > 230 { // High brightness threshold
// Check local contrast/edges?
// For now, simple brightness is a good proxy for "white text"
// Mark grid cell
let gx = x / cell_size;
let gy = y / cell_size;
grid[(gy * grid_w + gx) as usize] = true;
}
}
}
// Connected Components on Grid (Simple merging)
let mut rects = Vec::new();
let mut visited = vec![false; grid.len()];
for gy in 0..grid_h {
for gx in 0..grid_w {
let idx = (gy * grid_w + gx) as usize;
if grid[idx] && !visited[idx] {
// Start a new component
// Simple Flood Fill or just greedy expansion
// Let's do a simple greedy expansion for rectangles
let mut min_gx = gx;
let mut max_gx = gx;
let mut min_gy = gy;
let mut max_gy = gy;
let mut stack = vec![(gx, gy)];
visited[idx] = true;
while let Some((cx, cy)) = stack.pop() {
if cx < min_gx { min_gx = cx; }
if cx > max_gx { max_gx = cx; }
if cy < min_gy { min_gy = cy; }
if cy > max_gy { max_gy = cy; }
// Neighbors
let neighbors = [
(cx.wrapping_sub(1), cy), (cx + 1, cy),
(cx, cy.wrapping_sub(1)), (cx, cy + 1)
];
for (nx, ny) in neighbors {
if nx < grid_w && ny < grid_h {
let nidx = (ny * grid_w + nx) as usize;
if grid[nidx] && !visited[nidx] {
visited[nidx] = true;
stack.push((nx, ny));
}
}
}
}
// Convert grid rect to normalized image rect
// Add padding (1 cell)
let px = (min_gx * cell_size) as f64;
let py = (min_gy * cell_size) as f64;
let pw = ((max_gx - min_gx + 1) * cell_size) as f64;
let ph = ((max_gy - min_gy + 1) * cell_size) as f64;
rects.push(Rect {
x: px / width as f64,
y: py / height as f64,
width: pw / width as f64,
height: ph / height as f64,
});
}
}
}
Ok(DetectionResult { rects })
}
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.plugin(tauri_plugin_dialog::init())
.invoke_handler(tauri::generate_handler![scan_dir, get_zca_suggestion, export_batch])
.invoke_handler(tauri::generate_handler![scan_dir, get_zca_suggestion, export_batch, detect_watermark])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}