Skip to main content

pdfrum_doc/annot/
quad.rs

1//! `/QuadPoints`: eight numbers per quadrilateral, and the rectangle derived
2//! from them.
3//!
4//! A quadrilateral is written `[x1 y1 x2 y2 x3 y3 x4 y4]` — nominally the
5//! four corners in the order top-left, top-right, bottom-left, bottom-right.
6//! The rectangle taken from it uses **(x3, y3) as left/bottom and (x2, y2) as
7//! right/top**, so with the canonical corner ordering the result is a
8//! well-formed rectangle, and with any other ordering it is whatever the
9//! index arithmetic produces. Ported as arithmetic, not as an interpretation:
10//! the corpus contains files that rely on the inverted result.
11
12use kurbo::Rect;
13use pdfrum_object::Array;
14
15use crate::geom;
16
17/// How many complete quadrilaterals an array holds; a trailing partial one is
18/// ignored.
19///
20/// ```
21/// use pdfrum_doc::annot::quad_point_count;
22/// use pdfrum_object::Array;
23///
24/// let one = Array::of([0, 8, 10, 8, 0, 0, 10, 0]);
25/// assert_eq!(quad_point_count(Some(&one)), 1);
26/// // A trailing partial quadrilateral does not count.
27/// let partial = Array::of([0, 8, 10, 8, 0]);
28/// assert_eq!(quad_point_count(Some(&partial)), 0);
29/// assert_eq!(quad_point_count(None), 0);
30/// ```
31#[must_use]
32pub fn quad_point_count(array: Option<&Array>) -> usize {
33    array.map_or(0, |a| a.len() / 8)
34}
35
36/// The rectangle for quadrilateral `index`, straight from the index
37/// arithmetic and **not** normalized.
38#[must_use]
39pub(crate) fn rect_from_quad_points_array(array: &Array, index: usize) -> Rect {
40    let base = index * 8;
41    let at = |offset: usize| array.number_at_or_zero(base + offset);
42    geom::rect(at(4), at(5), at(2), at(3))
43}
44
45/// The rectangle for quadrilateral `index`, or the zero rectangle when the
46/// index is past the end. Deliberately not clamped to the last quadrilateral.
47///
48/// ```
49/// use pdfrum_doc::annot::rect_from_quad_points;
50/// use pdfrum_doc::geom;
51/// use pdfrum_object::Array;
52///
53/// // `[x1 y1 x2 y2 x3 y3 x4 y4]`: (x3, y3) is left/bottom, (x2, y2) right/top.
54/// let quad = Array::of([0, 8, 10, 8, 0, 0, 10, 0]);
55/// assert_eq!(rect_from_quad_points(Some(&quad), 0), geom::rect(0.0, 0.0, 10.0, 8.0));
56/// // Past the end is the zero rectangle, not the last quadrilateral.
57/// assert_eq!(rect_from_quad_points(Some(&quad), 1), kurbo::Rect::ZERO);
58/// ```
59#[must_use]
60pub fn rect_from_quad_points(array: Option<&Array>, index: usize) -> Rect {
61    let array = array.filter(|a| index < a.len() / 8);
62    array.map_or(Rect::ZERO, |a| rect_from_quad_points_array(a, index))
63}
64
65/// The bounding box of every quadrilateral.
66///
67/// With no quadrilaterals this is the zero rectangle. With exactly one it is
68/// that quadrilateral's rectangle **unchanged** — including inverted, because
69/// no union runs. Two or more normalize, because the union does.
70#[must_use]
71pub(crate) fn bounding_rect_from_quad_points(array: Option<&Array>) -> Rect {
72    let count = quad_point_count(array);
73    let Some(array) = array.filter(|_| count > 0) else {
74        return Rect::ZERO;
75    };
76    let mut bounds = rect_from_quad_points_array(array, 0);
77    for index in 1..count {
78        bounds = geom::union(bounds, rect_from_quad_points_array(array, index));
79    }
80    bounds
81}
82
83#[cfg(test)]
84mod tests {
85    use super::{
86        bounding_rect_from_quad_points, quad_point_count, rect_from_quad_points,
87        rect_from_quad_points_array,
88    };
89    use crate::geom;
90    use kurbo::Rect;
91    use pdfrum_object::{Array, Object};
92
93    /// `count` quadrilaterals whose coordinates run 0, 1, 2, … so the index
94    /// arithmetic is readable straight off the assertion.
95    fn quads(count: usize) -> Array {
96        Array::of((0..count * 8).map(|n| Object::Int(i64::try_from(n).unwrap_or(0))))
97    }
98
99    #[test]
100    fn the_derived_rectangle_is_index_arithmetic_not_a_normalization() {
101        // With the values 0..7 the arithmetic gives left=4, bottom=5,
102        // right=2, top=3 — an inverted rectangle that stays inverted.
103        let array = quads(2);
104        assert_eq!(
105            rect_from_quad_points_array(&array, 0),
106            geom::rect(4.0, 5.0, 2.0, 3.0)
107        );
108        assert_eq!(
109            rect_from_quad_points_array(&array, 1),
110            geom::rect(12.0, 13.0, 10.0, 11.0)
111        );
112    }
113
114    #[test]
115    fn one_quadrilateral_is_never_normalized_but_two_are() {
116        let one = quads(1);
117        assert_eq!(
118            bounding_rect_from_quad_points(Some(&one)),
119            geom::rect(4.0, 5.0, 2.0, 3.0)
120        );
121        let three = quads(3);
122        assert_eq!(
123            bounding_rect_from_quad_points(Some(&three)),
124            geom::rect(2.0, 3.0, 20.0, 21.0)
125        );
126    }
127
128    #[test]
129    fn no_quadrilaterals_gives_the_zero_rectangle() {
130        assert_eq!(bounding_rect_from_quad_points(None), Rect::ZERO);
131        assert_eq!(
132            bounding_rect_from_quad_points(Some(&Array::of([Object::Int(1), Object::Int(2)]))),
133            Rect::ZERO
134        );
135    }
136
137    #[test]
138    fn an_out_of_range_index_gives_zero_rather_than_the_last_quadrilateral() {
139        let array = quads(2);
140        assert_eq!(rect_from_quad_points(Some(&array), 2), Rect::ZERO);
141        assert_eq!(
142            rect_from_quad_points(Some(&array), 1),
143            geom::rect(12.0, 13.0, 10.0, 11.0)
144        );
145    }
146
147    #[test]
148    fn the_count_ignores_a_trailing_partial_quadrilateral() {
149        for len in 0..8 {
150            let array = Array::of((0..len).map(Object::Int));
151            assert_eq!(quad_point_count(Some(&array)), 0, "len {len}");
152        }
153        for len in 8..16 {
154            let array = Array::of((0..len).map(Object::Int));
155            assert_eq!(quad_point_count(Some(&array)), 1, "len {len}");
156        }
157        let array = Array::of((0..65).map(Object::Int));
158        assert_eq!(quad_point_count(Some(&array)), 8);
159    }
160}