Skip to main content

pdfplumber_core/
edges.rs

1//! Edge derivation from geometric primitives.
2//!
3//! Edges are line segments derived from Lines, Rects, and Curves for
4//! use in table detection algorithms.
5
6use crate::geometry::Orientation;
7use crate::shapes::{Curve, Line, Rect};
8
9/// Source of an edge, tracking which geometric primitive it came from.
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
12pub enum EdgeSource {
13    /// Derived directly from a Line object.
14    Line,
15    /// Top edge of a Rect.
16    RectTop,
17    /// Bottom edge of a Rect.
18    RectBottom,
19    /// Left edge of a Rect.
20    RectLeft,
21    /// Right edge of a Rect.
22    RectRight,
23    /// Approximated from a Curve (chord from start to end).
24    Curve,
25    /// Synthetic edge generated from text alignment patterns (Stream strategy).
26    Stream,
27    /// User-provided explicit line coordinate (Explicit strategy).
28    Explicit,
29}
30
31/// A line segment edge for table detection.
32///
33/// Edges are derived from Lines, Rects, and Curves and are used
34/// by the table detection algorithm to find cell boundaries.
35#[derive(Debug, Clone, PartialEq)]
36#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
37pub struct Edge {
38    /// Left x coordinate.
39    pub x0: f64,
40    /// Top y coordinate (distance from top of page).
41    pub top: f64,
42    /// Right x coordinate.
43    pub x1: f64,
44    /// Bottom y coordinate (distance from top of page).
45    pub bottom: f64,
46    /// Edge orientation.
47    pub orientation: Orientation,
48    /// Where this edge was derived from.
49    pub source: EdgeSource,
50}
51
52/// Derive an Edge from a Line (direct conversion).
53pub fn edge_from_line(line: &Line) -> Edge {
54    Edge {
55        x0: line.x0,
56        top: line.top,
57        x1: line.x1,
58        bottom: line.bottom,
59        orientation: line.orientation,
60        source: EdgeSource::Line,
61    }
62}
63
64/// Derive 4 Edges from a Rect (top, bottom, left, right).
65pub fn edges_from_rect(rect: &Rect) -> Vec<Edge> {
66    vec![
67        Edge {
68            x0: rect.x0,
69            top: rect.top,
70            x1: rect.x1,
71            bottom: rect.top,
72            orientation: Orientation::Horizontal,
73            source: EdgeSource::RectTop,
74        },
75        Edge {
76            x0: rect.x0,
77            top: rect.bottom,
78            x1: rect.x1,
79            bottom: rect.bottom,
80            orientation: Orientation::Horizontal,
81            source: EdgeSource::RectBottom,
82        },
83        Edge {
84            x0: rect.x0,
85            top: rect.top,
86            x1: rect.x0,
87            bottom: rect.bottom,
88            orientation: Orientation::Vertical,
89            source: EdgeSource::RectLeft,
90        },
91        Edge {
92            x0: rect.x1,
93            top: rect.top,
94            x1: rect.x1,
95            bottom: rect.bottom,
96            orientation: Orientation::Vertical,
97            source: EdgeSource::RectRight,
98        },
99    ]
100}
101
102/// Tolerance for floating-point comparison when classifying edge orientation.
103const EDGE_AXIS_TOLERANCE: f64 = 1e-6;
104
105/// Classify orientation for an edge from two points.
106fn classify_edge_orientation(x0: f64, y0: f64, x1: f64, y1: f64) -> Orientation {
107    let dx = (x1 - x0).abs();
108    let dy = (y1 - y0).abs();
109    if dy < EDGE_AXIS_TOLERANCE {
110        Orientation::Horizontal
111    } else if dx < EDGE_AXIS_TOLERANCE {
112        Orientation::Vertical
113    } else {
114        Orientation::Diagonal
115    }
116}
117
118/// Derive an Edge from a Curve using chord approximation (start to end).
119pub fn edge_from_curve(curve: &Curve) -> Edge {
120    let (start_x, start_y) = curve.pts[0];
121    let (end_x, end_y) = curve.pts[curve.pts.len() - 1];
122
123    let x0 = start_x.min(end_x);
124    let x1 = start_x.max(end_x);
125    let top = start_y.min(end_y);
126    let bottom = start_y.max(end_y);
127    let orientation = classify_edge_orientation(start_x, start_y, end_x, end_y);
128
129    Edge {
130        x0,
131        top,
132        x1,
133        bottom,
134        orientation,
135        source: EdgeSource::Curve,
136    }
137}
138
139/// Derive all edges from collections of lines, rects, and curves.
140pub fn derive_edges(lines: &[Line], rects: &[Rect], curves: &[Curve]) -> Vec<Edge> {
141    let mut edges = Vec::new();
142
143    for line in lines {
144        edges.push(edge_from_line(line));
145    }
146
147    for rect in rects {
148        edges.extend(edges_from_rect(rect));
149    }
150
151    for curve in curves {
152        edges.push(edge_from_curve(curve));
153    }
154
155    edges
156}
157
158#[cfg(test)]
159mod tests {
160    use super::*;
161    use crate::painting::Color;
162
163    #[test]
164    fn test_edge_construction_and_field_access() {
165        let edge = Edge {
166            x0: 10.0,
167            top: 20.0,
168            x1: 200.0,
169            bottom: 20.0,
170            orientation: Orientation::Horizontal,
171            source: EdgeSource::Line,
172        };
173        assert_eq!(edge.x0, 10.0);
174        assert_eq!(edge.top, 20.0);
175        assert_eq!(edge.x1, 200.0);
176        assert_eq!(edge.bottom, 20.0);
177        assert_eq!(edge.orientation, Orientation::Horizontal);
178        assert_eq!(edge.source, EdgeSource::Line);
179    }
180
181    #[test]
182    fn test_edge_source_variants() {
183        let sources = [
184            EdgeSource::Line,
185            EdgeSource::RectTop,
186            EdgeSource::RectBottom,
187            EdgeSource::RectLeft,
188            EdgeSource::RectRight,
189            EdgeSource::Curve,
190        ];
191        // EdgeSource derives Copy
192        for source in sources {
193            let copy = source;
194            assert_eq!(source, copy);
195        }
196    }
197
198    fn assert_approx(a: f64, b: f64) {
199        assert!(
200            (a - b).abs() < 1e-6,
201            "expected {b}, got {a}, diff={}",
202            (a - b).abs()
203        );
204    }
205
206    fn make_line(x0: f64, top: f64, x1: f64, bottom: f64, orient: Orientation) -> Line {
207        Line {
208            x0,
209            top,
210            x1,
211            bottom,
212            line_width: 1.0,
213            stroke_color: Color::black(),
214            orientation: orient,
215        }
216    }
217
218    fn make_rect(x0: f64, top: f64, x1: f64, bottom: f64) -> Rect {
219        Rect {
220            x0,
221            top,
222            x1,
223            bottom,
224            line_width: 1.0,
225            stroke: true,
226            fill: false,
227            stroke_color: Color::black(),
228            fill_color: Color::black(),
229        }
230    }
231
232    fn make_curve(pts: Vec<(f64, f64)>) -> Curve {
233        let xs: Vec<f64> = pts.iter().map(|p| p.0).collect();
234        let ys: Vec<f64> = pts.iter().map(|p| p.1).collect();
235        Curve {
236            x0: xs.iter().cloned().fold(f64::INFINITY, f64::min),
237            top: ys.iter().cloned().fold(f64::INFINITY, f64::min),
238            x1: xs.iter().cloned().fold(f64::NEG_INFINITY, f64::max),
239            bottom: ys.iter().cloned().fold(f64::NEG_INFINITY, f64::max),
240            pts,
241            line_width: 1.0,
242            stroke: true,
243            fill: false,
244            stroke_color: Color::black(),
245            fill_color: Color::black(),
246        }
247    }
248
249    // --- Edge from Line ---
250
251    #[test]
252    fn test_edge_from_horizontal_line() {
253        let line = make_line(10.0, 50.0, 100.0, 50.0, Orientation::Horizontal);
254        let edge = edge_from_line(&line);
255
256        assert_approx(edge.x0, 10.0);
257        assert_approx(edge.top, 50.0);
258        assert_approx(edge.x1, 100.0);
259        assert_approx(edge.bottom, 50.0);
260        assert_eq!(edge.orientation, Orientation::Horizontal);
261        assert_eq!(edge.source, EdgeSource::Line);
262    }
263
264    #[test]
265    fn test_edge_from_vertical_line() {
266        let line = make_line(50.0, 10.0, 50.0, 200.0, Orientation::Vertical);
267        let edge = edge_from_line(&line);
268
269        assert_approx(edge.x0, 50.0);
270        assert_approx(edge.top, 10.0);
271        assert_approx(edge.x1, 50.0);
272        assert_approx(edge.bottom, 200.0);
273        assert_eq!(edge.orientation, Orientation::Vertical);
274        assert_eq!(edge.source, EdgeSource::Line);
275    }
276
277    #[test]
278    fn test_edge_from_diagonal_line() {
279        let line = make_line(10.0, 20.0, 100.0, 200.0, Orientation::Diagonal);
280        let edge = edge_from_line(&line);
281
282        assert_eq!(edge.orientation, Orientation::Diagonal);
283        assert_eq!(edge.source, EdgeSource::Line);
284    }
285
286    // --- Edges from Rect ---
287
288    #[test]
289    fn test_edges_from_rect_count() {
290        let rect = make_rect(10.0, 20.0, 110.0, 70.0);
291        let edges = edges_from_rect(&rect);
292        assert_eq!(edges.len(), 4);
293    }
294
295    #[test]
296    fn test_edges_from_rect_top() {
297        let rect = make_rect(10.0, 20.0, 110.0, 70.0);
298        let edges = edges_from_rect(&rect);
299        let top_edge = &edges[0];
300
301        assert_approx(top_edge.x0, 10.0);
302        assert_approx(top_edge.top, 20.0);
303        assert_approx(top_edge.x1, 110.0);
304        assert_approx(top_edge.bottom, 20.0);
305        assert_eq!(top_edge.orientation, Orientation::Horizontal);
306        assert_eq!(top_edge.source, EdgeSource::RectTop);
307    }
308
309    #[test]
310    fn test_edges_from_rect_bottom() {
311        let rect = make_rect(10.0, 20.0, 110.0, 70.0);
312        let edges = edges_from_rect(&rect);
313        let bottom_edge = &edges[1];
314
315        assert_approx(bottom_edge.x0, 10.0);
316        assert_approx(bottom_edge.top, 70.0);
317        assert_approx(bottom_edge.x1, 110.0);
318        assert_approx(bottom_edge.bottom, 70.0);
319        assert_eq!(bottom_edge.orientation, Orientation::Horizontal);
320        assert_eq!(bottom_edge.source, EdgeSource::RectBottom);
321    }
322
323    #[test]
324    fn test_edges_from_rect_left() {
325        let rect = make_rect(10.0, 20.0, 110.0, 70.0);
326        let edges = edges_from_rect(&rect);
327        let left_edge = &edges[2];
328
329        assert_approx(left_edge.x0, 10.0);
330        assert_approx(left_edge.top, 20.0);
331        assert_approx(left_edge.x1, 10.0);
332        assert_approx(left_edge.bottom, 70.0);
333        assert_eq!(left_edge.orientation, Orientation::Vertical);
334        assert_eq!(left_edge.source, EdgeSource::RectLeft);
335    }
336
337    #[test]
338    fn test_edges_from_rect_right() {
339        let rect = make_rect(10.0, 20.0, 110.0, 70.0);
340        let edges = edges_from_rect(&rect);
341        let right_edge = &edges[3];
342
343        assert_approx(right_edge.x0, 110.0);
344        assert_approx(right_edge.top, 20.0);
345        assert_approx(right_edge.x1, 110.0);
346        assert_approx(right_edge.bottom, 70.0);
347        assert_eq!(right_edge.orientation, Orientation::Vertical);
348        assert_eq!(right_edge.source, EdgeSource::RectRight);
349    }
350
351    // --- Edge from Curve (chord approximation) ---
352
353    #[test]
354    fn test_edge_from_curve_horizontal_chord() {
355        // Curve from (0, 100) to (100, 100) — chord is horizontal
356        let curve = make_curve(vec![
357            (0.0, 100.0),
358            (10.0, 50.0),
359            (90.0, 50.0),
360            (100.0, 100.0),
361        ]);
362        let edge = edge_from_curve(&curve);
363
364        assert_approx(edge.x0, 0.0);
365        assert_approx(edge.x1, 100.0);
366        assert_approx(edge.top, 100.0);
367        assert_approx(edge.bottom, 100.0);
368        assert_eq!(edge.orientation, Orientation::Horizontal);
369        assert_eq!(edge.source, EdgeSource::Curve);
370    }
371
372    #[test]
373    fn test_edge_from_curve_vertical_chord() {
374        // Curve from (50, 0) to (50, 100) — chord is vertical
375        let curve = make_curve(vec![
376            (50.0, 0.0),
377            (100.0, 30.0),
378            (100.0, 70.0),
379            (50.0, 100.0),
380        ]);
381        let edge = edge_from_curve(&curve);
382
383        assert_approx(edge.x0, 50.0);
384        assert_approx(edge.x1, 50.0);
385        assert_approx(edge.top, 0.0);
386        assert_approx(edge.bottom, 100.0);
387        assert_eq!(edge.orientation, Orientation::Vertical);
388        assert_eq!(edge.source, EdgeSource::Curve);
389    }
390
391    #[test]
392    fn test_edge_from_curve_diagonal_chord() {
393        // Curve from (0, 0) to (100, 100) — chord is diagonal
394        let curve = make_curve(vec![(0.0, 0.0), (30.0, 70.0), (70.0, 30.0), (100.0, 100.0)]);
395        let edge = edge_from_curve(&curve);
396
397        assert_approx(edge.x0, 0.0);
398        assert_approx(edge.x1, 100.0);
399        assert_approx(edge.top, 0.0);
400        assert_approx(edge.bottom, 100.0);
401        assert_eq!(edge.orientation, Orientation::Diagonal);
402        assert_eq!(edge.source, EdgeSource::Curve);
403    }
404
405    // --- derive_edges (combined) ---
406
407    #[test]
408    fn test_derive_edges_empty_inputs() {
409        let edges = derive_edges(&[], &[], &[]);
410        assert!(edges.is_empty());
411    }
412
413    #[test]
414    fn test_derive_edges_lines_only() {
415        let lines = vec![
416            make_line(0.0, 50.0, 100.0, 50.0, Orientation::Horizontal),
417            make_line(50.0, 0.0, 50.0, 100.0, Orientation::Vertical),
418        ];
419        let edges = derive_edges(&lines, &[], &[]);
420        assert_eq!(edges.len(), 2);
421        assert_eq!(edges[0].source, EdgeSource::Line);
422        assert_eq!(edges[1].source, EdgeSource::Line);
423    }
424
425    #[test]
426    fn test_derive_edges_rects_only() {
427        let rects = vec![make_rect(10.0, 20.0, 110.0, 70.0)];
428        let edges = derive_edges(&[], &rects, &[]);
429        assert_eq!(edges.len(), 4); // 4 edges per rect
430    }
431
432    #[test]
433    fn test_derive_edges_curves_only() {
434        let curves = vec![make_curve(vec![
435            (0.0, 100.0),
436            (10.0, 50.0),
437            (90.0, 50.0),
438            (100.0, 100.0),
439        ])];
440        let edges = derive_edges(&[], &[], &curves);
441        assert_eq!(edges.len(), 1);
442        assert_eq!(edges[0].source, EdgeSource::Curve);
443    }
444
445    #[test]
446    fn test_derive_edges_mixed() {
447        let lines = vec![make_line(0.0, 50.0, 100.0, 50.0, Orientation::Horizontal)];
448        let rects = vec![make_rect(10.0, 20.0, 110.0, 70.0)];
449        let curves = vec![make_curve(vec![
450            (0.0, 100.0),
451            (10.0, 50.0),
452            (90.0, 50.0),
453            (100.0, 100.0),
454        ])];
455        let edges = derive_edges(&lines, &rects, &curves);
456        // 1 from line + 4 from rect + 1 from curve = 6
457        assert_eq!(edges.len(), 6);
458        assert_eq!(edges[0].source, EdgeSource::Line);
459        assert_eq!(edges[1].source, EdgeSource::RectTop);
460        assert_eq!(edges[4].source, EdgeSource::RectRight);
461        assert_eq!(edges[5].source, EdgeSource::Curve);
462    }
463
464    #[test]
465    fn test_derive_edges_multiple_rects() {
466        let rects = vec![
467            make_rect(10.0, 20.0, 110.0, 70.0),
468            make_rect(200.0, 300.0, 350.0, 400.0),
469        ];
470        let edges = derive_edges(&[], &rects, &[]);
471        assert_eq!(edges.len(), 8); // 4 edges per rect × 2
472    }
473}