1#[derive(Debug, Clone, Copy, PartialEq)]
28pub struct CheckBox {
29 pub l: f32,
30 pub t: f32,
31 pub r: f32,
32 pub b: f32,
33 pub checked: bool,
34}
35
36#[derive(Debug, Clone, Copy, PartialEq)]
38pub(crate) enum Ink {
39 Seg { x0: f64, y0: f64, x1: f64, y1: f64 },
41 Rect { l: f64, b: f64, r: f64, t: f64 },
43 Blot { l: f64, b: f64, r: f64, t: f64 },
46}
47
48pub(crate) const MIN_SIDE: f64 = 5.0;
51pub(crate) const MAX_SIDE: f64 = 24.0;
52const TOL: f64 = 0.75;
54
55impl Ink {
56 fn bbox(&self) -> (f64, f64, f64, f64) {
57 match *self {
58 Ink::Seg { x0, y0, x1, y1 } => (x0.min(x1), y0.min(y1), x0.max(x1), y0.max(y1)),
59 Ink::Rect { l, b, r, t } | Ink::Blot { l, b, r, t } => (l, b, r, t),
60 }
61 }
62
63 pub(crate) fn worth_keeping(&self) -> bool {
67 let (l, b, r, t) = self.bbox();
68 let reach = MAX_SIDE + TOL;
69 (r - l) <= reach && (t - b) <= reach
70 }
71}
72
73type Square = ((f64, f64, f64, f64), Vec<usize>);
75
76fn square_sides(w: f64, h: f64) -> bool {
77 let (lo, hi) = (w.min(h), w.max(h));
78 lo >= MIN_SIDE && hi <= MAX_SIDE && hi - lo <= 0.15 * hi
79}
80
81pub(crate) fn find(inks: &[Ink], page_h: f32) -> Vec<CheckBox> {
84 let mut squares: Vec<Square> = Vec::new();
85 for (i, ink) in inks.iter().enumerate() {
86 if let Ink::Rect { l, b, r, t } = *ink {
87 if square_sides(r - l, t - b) {
88 squares.push(((l, b, r, t), vec![i]));
89 }
90 }
91 }
92 let near = |a: f64, b: f64| (a - b).abs() <= TOL;
95 let mut hs: Vec<(usize, f64, f64, f64)> = Vec::new(); let mut vs: Vec<(usize, f64, f64, f64)> = Vec::new(); for (i, ink) in inks.iter().enumerate() {
98 if let Ink::Seg { x0, y0, x1, y1 } = *ink {
99 if near(y0, y1) && (x1 - x0).abs() >= MIN_SIDE - TOL {
100 hs.push((i, (y0 + y1) / 2.0, x0.min(x1), x0.max(x1)));
101 } else if near(x0, x1) && (y1 - y0).abs() >= MIN_SIDE - TOL {
102 vs.push((i, (x0 + x1) / 2.0, y0.min(y1), y0.max(y1)));
103 }
104 }
105 }
106 if hs.len() <= 4000 && vs.len() <= 4000 {
109 let side = |lo: f64, hi: f64, a: f64, b: f64| near(lo, a) && near(hi, b);
110 for (x, &(hi, ya, xl, xr)) in hs.iter().enumerate() {
111 for &(hj, yb, xl2, xr2) in &hs[x + 1..] {
112 if !(near(xl, xl2) && near(xr, xr2) && square_sides(xr - xl, (ya - yb).abs())) {
113 continue;
114 }
115 let (bot, top) = (ya.min(yb), ya.max(yb));
116 let left = vs
117 .iter()
118 .find(|&&(_, vx, y0, y1)| near(vx, xl) && side(y0, y1, bot, top));
119 let right = vs
120 .iter()
121 .find(|&&(_, vx, y0, y1)| near(vx, xr) && side(y0, y1, bot, top));
122 if let (Some(&(lv, ..)), Some(&(rv, ..))) = (left, right) {
123 squares.push(((xl, bot, xr, top), vec![hi, hj, lv, rv]));
124 }
125 }
126 }
127 }
128 let mut out: Vec<CheckBox> = Vec::new();
129 for (k, &((l, b, r, t), ref own)) in squares.iter().enumerate() {
130 let same =
131 |o: &Square| near(o.0 .0, l) && near(o.0 .1, b) && near(o.0 .2, r) && near(o.0 .3, t);
132 if squares[..k].iter().any(same) {
133 continue;
134 }
135 let edges: Vec<usize> = squares
138 .iter()
139 .filter(|o| same(o))
140 .flat_map(|o| o.1.iter().copied())
141 .chain(own.iter().copied())
142 .collect();
143 let swatch = inks.iter().any(|ink| {
146 matches!(ink, Ink::Blot { .. }) && {
147 let (il, ib, ir, it) = ink.bbox();
148 let fit = 1.5;
149 (il - l).abs() <= fit
150 && (ir - r).abs() <= fit
151 && (ib - b).abs() <= fit
152 && (it - t).abs() <= fit
153 }
154 });
155 if swatch {
156 continue;
157 }
158 let inset = 0.2 * (r - l);
159 let checked = inks.iter().enumerate().any(|(i, ink)| {
160 if edges.contains(&i) {
161 return false;
162 }
163 let (il, ib, ir, it) = ink.bbox();
164 let (cx, cy) = ((il + ir) / 2.0, (ib + it) / 2.0);
165 il >= l - TOL
166 && ir <= r + TOL
167 && ib >= b - TOL
168 && it <= t + TOL
169 && cx > l + inset
170 && cx < r - inset
171 && cy > b + inset
172 && cy < t - inset
173 });
174 out.push(CheckBox {
175 l: l as f32,
176 t: page_h - t as f32,
177 r: r as f32,
178 b: page_h - b as f32,
179 checked,
180 });
181 }
182 out
183}
184
185#[cfg(test)]
186mod tests {
187 use super::*;
188
189 fn seg(x0: f64, y0: f64, x1: f64, y1: f64) -> Ink {
190 Ink::Seg { x0, y0, x1, y1 }
191 }
192
193 fn four_edges(x: f64, y: f64) -> Vec<Ink> {
195 let s = 12.96;
196 vec![
197 seg(x, y + s, x + s, y + s),
198 seg(x, y, x + s, y),
199 seg(x, y, x, y + s),
200 seg(x + s, y, x + s, y + s),
201 ]
202 }
203
204 #[test]
205 fn four_stroked_edges_make_one_square() {
206 let boxes = find(&four_edges(119.52, 449.04), 792.0);
207 assert_eq!(boxes.len(), 1);
208 let b = boxes[0];
209 assert!((b.l - 119.52).abs() < 0.01 && (b.r - 132.48).abs() < 0.01);
210 assert!((b.t - (792.0 - 462.0)).abs() < 0.01, "{b:?}");
211 assert!(!b.checked);
212 }
213
214 #[test]
215 fn a_stroked_rect_is_a_square_and_a_tick_inside_checks_it() {
216 let mut inks = vec![Ink::Rect {
217 l: 100.0,
218 b: 100.0,
219 r: 110.0,
220 t: 110.0,
221 }];
222 assert!(!find(&inks, 800.0)[0].checked);
223 inks.push(seg(102.0, 105.0, 104.5, 102.0));
224 inks.push(seg(104.5, 102.0, 108.5, 108.5));
225 assert!(find(&inks, 800.0)[0].checked);
226 }
227
228 #[test]
229 fn frames_bullets_and_open_shapes_are_not_checkboxes() {
230 let rect = |l, b, r, t| Ink::Rect { l, b, r, t };
231 assert!(find(&[rect(0.0, 0.0, 40.0, 40.0)], 800.0).is_empty());
233 assert!(find(&[rect(0.0, 0.0, 3.0, 3.0)], 800.0).is_empty());
234 assert!(find(&[rect(0.0, 0.0, 20.0, 8.0)], 800.0).is_empty());
235 let mut open = four_edges(50.0, 50.0);
237 open.pop();
238 assert!(find(&open, 800.0).is_empty());
239 }
240
241 #[test]
244 fn a_colour_filled_square_is_a_swatch() {
245 let mut inks = four_edges(10.0, 10.0);
246 inks.push(Ink::Blot {
247 l: 10.0,
248 b: 10.0,
249 r: 22.96,
250 t: 22.96,
251 });
252 assert!(find(&inks, 800.0).is_empty());
253 }
254
255 #[test]
256 fn a_square_stroked_twice_is_one_unchecked_box() {
257 let mut inks = four_edges(10.0, 10.0);
258 inks.extend(four_edges(10.0, 10.0));
259 let boxes = find(&inks, 800.0);
260 assert_eq!(boxes.len(), 1);
261 assert!(!boxes[0].checked);
262 }
263}