gardesk/garshot / 7a50daa

Browse files

annotate: add blur/pixelation tool

Authored by mfwolffe <wolffemf@dukes.jmu.edu>
SHA
7a50daa94d2609c6e8b4339a02df94debe1b52f0
Parents
cf74bd4
Tree
1039c34

1 changed file

StatusFile+-
A garshot/src/annotate/tools/blur.rs 147 0
garshot/src/annotate/tools/blur.rsadded
@@ -0,0 +1,147 @@
1
+//! Blur tool - pixelate regions for redaction.
2
+
3
+use super::Tool;
4
+use crate::annotate::state::ToolProperties;
5
+use gartk_core::{InputEvent, MouseButton, Point, Rect};
6
+use gartk_render::cairo::Context;
7
+use gartk_render::stroke_rect;
8
+use gartk_x11::CursorShape;
9
+
10
+/// Blur/pixelate tool for redacting sensitive information.
11
+pub struct BlurTool {
12
+    /// Starting corner of the region.
13
+    start: Option<Point>,
14
+    /// Opposite corner of the region.
15
+    end: Option<Point>,
16
+    /// Whether we're currently drawing.
17
+    drawing: bool,
18
+}
19
+
20
+impl BlurTool {
21
+    /// Create a new blur tool.
22
+    pub fn new() -> Self {
23
+        Self {
24
+            start: None,
25
+            end: None,
26
+            drawing: false,
27
+        }
28
+    }
29
+
30
+    /// Calculate normalized rectangle from start and end points.
31
+    pub fn calculate_rect(&self) -> Option<Rect> {
32
+        let (start, end) = (self.start?, self.end?);
33
+
34
+        let x = start.x.min(end.x);
35
+        let y = start.y.min(end.y);
36
+        let width = (end.x - start.x).unsigned_abs();
37
+        let height = (end.y - start.y).unsigned_abs();
38
+
39
+        if width > 0 && height > 0 {
40
+            Some(Rect::new(x, y, width, height))
41
+        } else {
42
+            None
43
+        }
44
+    }
45
+
46
+    /// Draw the selection rectangle outline.
47
+    fn draw_selection(&self, ctx: &Context, props: &ToolProperties) {
48
+        if let Some(rect) = self.calculate_rect() {
49
+            // Draw dashed outline to indicate blur region
50
+            ctx.set_dash(&[5.0, 5.0], 0.0);
51
+            stroke_rect(ctx, rect, props.color, 2.0);
52
+            ctx.set_dash(&[], 0.0);
53
+        }
54
+    }
55
+}
56
+
57
+impl Default for BlurTool {
58
+    fn default() -> Self {
59
+        Self::new()
60
+    }
61
+}
62
+
63
+impl Tool for BlurTool {
64
+    fn handle_event(&mut self, event: &InputEvent, _props: &ToolProperties) -> bool {
65
+        match event {
66
+            InputEvent::MousePress(e) if e.button == Some(MouseButton::Left) => {
67
+                self.start = Some(e.position);
68
+                self.end = Some(e.position);
69
+                self.drawing = true;
70
+                true
71
+            }
72
+            InputEvent::MouseMove(e) if self.drawing => {
73
+                self.end = Some(e.position);
74
+                true
75
+            }
76
+            InputEvent::MouseRelease(e) if e.button == Some(MouseButton::Left) && self.drawing => {
77
+                self.end = Some(e.position);
78
+                self.drawing = false;
79
+                true
80
+            }
81
+            _ => false,
82
+        }
83
+    }
84
+
85
+    fn draw_preview(&self, ctx: &Context, props: &ToolProperties) {
86
+        self.draw_selection(ctx, props);
87
+    }
88
+
89
+    fn commit(&self, ctx: &Context, props: &ToolProperties) {
90
+        // For the preview/commit, we draw a pixelated pattern
91
+        // The actual blur is applied by the overlay when committing
92
+        if let Some(rect) = self.calculate_rect() {
93
+            // Draw a checkerboard pattern to indicate blur
94
+            let block_size = props.blur_radius.max(8) as f64;
95
+
96
+            ctx.save().ok();
97
+            ctx.rectangle(
98
+                rect.x as f64,
99
+                rect.y as f64,
100
+                rect.width as f64,
101
+                rect.height as f64,
102
+            );
103
+            ctx.clip();
104
+
105
+            // Draw checkerboard pattern
106
+            let mut dark = true;
107
+            let mut y = rect.y as f64;
108
+            while y < (rect.y + rect.height as i32) as f64 {
109
+                let mut x = rect.x as f64;
110
+                let row_start_dark = dark;
111
+                while x < (rect.x + rect.width as i32) as f64 {
112
+                    if dark {
113
+                        ctx.set_source_rgba(0.2, 0.2, 0.2, 0.8);
114
+                    } else {
115
+                        ctx.set_source_rgba(0.3, 0.3, 0.3, 0.8);
116
+                    }
117
+                    ctx.rectangle(x, y, block_size, block_size);
118
+                    let _ = ctx.fill();
119
+                    dark = !dark;
120
+                    x += block_size;
121
+                }
122
+                dark = !row_start_dark;
123
+                y += block_size;
124
+            }
125
+
126
+            ctx.restore().ok();
127
+        }
128
+    }
129
+
130
+    fn reset(&mut self) {
131
+        self.start = None;
132
+        self.end = None;
133
+        self.drawing = false;
134
+    }
135
+
136
+    fn cursor(&self) -> CursorShape {
137
+        CursorShape::Crosshair
138
+    }
139
+
140
+    fn is_drawing(&self) -> bool {
141
+        self.drawing
142
+    }
143
+
144
+    fn can_commit(&self) -> bool {
145
+        self.calculate_rect().is_some()
146
+    }
147
+}