Skip to main content

pdfium_render/pdf/
rect.rs

1//! Defines the [PdfRect] struct, a rectangle measured in [PdfPoints].
2
3use crate::bindgen::{FPDF_BOOL, FS_RECTF};
4use crate::bindings::PdfiumLibraryBindings;
5use crate::error::{PdfiumError, PdfiumInternalError};
6use crate::pdf::matrix::PdfMatrix;
7use crate::pdf::points::PdfPoints;
8use crate::pdf::quad_points::PdfQuadPoints;
9use itertools::{max, min};
10use std::fmt::{Display, Formatter};
11use std::hash::{Hash, Hasher};
12
13/// A rectangle measured in [PdfPoints].
14///
15/// The coordinate space of a `PdfPage` has its origin (0,0) at the bottom left of the page,
16/// with x values increasing as coordinates move horizontally to the right and
17/// y values increasing as coordinates move vertically up.
18#[derive(Debug, Copy, Clone)]
19pub struct PdfRect {
20    // ~keep TODO: AJRC - 28/12/24 - direct field access to be removed as part of release 0.9.0.
21    #[deprecated(
22        since = "0.8.28",
23        note = "Use the PdfRect::bottom() function instead of direct field access. Direct field access will be removed in release 0.9.0."
24    )]
25    pub bottom: PdfPoints,
26
27    #[deprecated(
28        since = "0.8.28",
29        note = "Use the PdfRect::left() function instead of direct field access. Direct field access will be removed in release 0.9.0."
30    )]
31    pub left: PdfPoints,
32
33    #[deprecated(
34        since = "0.8.28",
35        note = "Use the PdfRect::top() function instead of direct field access. Direct field access will be removed in release 0.9.0."
36    )]
37    pub top: PdfPoints,
38
39    #[deprecated(
40        since = "0.8.28",
41        note = "Use the PdfRect::left() function instead of direct field access. Direct field access will be removed in release 0.9.0."
42    )]
43    pub right: PdfPoints,
44}
45
46impl PdfRect {
47    /// A [PdfRect] object with the identity value (0.0, 0.0, 0.0, 0.0).
48    pub const ZERO: PdfRect = PdfRect::zero();
49
50    /// A [PdfRect] object that encloses the entire addressable `PdfPage` coordinate space of
51    /// ([-PdfPoints::MAX], [-PdfPoints::MAX], [PdfPoints::MAX], [PdfPoints::MAX]).
52    pub const MAX: PdfRect = PdfRect::new(PdfPoints::MIN, PdfPoints::MIN, PdfPoints::MAX, PdfPoints::MAX);
53
54    #[inline]
55    pub(crate) fn from_pdfium(rect: FS_RECTF) -> Self {
56        Self::new_from_values(rect.bottom, rect.left, rect.top, rect.right)
57    }
58
59    #[inline]
60    pub(crate) fn from_pdfium_as_result(
61        result: FPDF_BOOL,
62        rect: FS_RECTF,
63        bindings: &dyn PdfiumLibraryBindings,
64    ) -> Result<PdfRect, PdfiumError> {
65        if !bindings.is_true(result) {
66            Err(PdfiumError::PdfiumLibraryInternalError(PdfiumInternalError::Unknown))
67        } else {
68            Ok(PdfRect::from_pdfium(rect))
69        }
70    }
71
72    /// Creates a new [PdfRect] from the given [PdfPoints] measurements.
73    ///
74    /// The coordinate space of a `PdfPage` has its origin (0,0) at the bottom left of the page,
75    /// with x values increasing as coordinates move horizontally to the right and
76    /// y values increasing as coordinates move vertically up.
77    #[inline]
78    pub const fn new(bottom: PdfPoints, left: PdfPoints, top: PdfPoints, right: PdfPoints) -> Self {
79        let (ordered_bottom, ordered_top) = if bottom.value > top.value {
80            (top, bottom)
81        } else {
82            (bottom, top)
83        };
84
85        let (ordered_left, ordered_right) = if left.value > right.value {
86            (right, left)
87        } else {
88            (left, right)
89        };
90
91        #[allow(deprecated)]
92        Self {
93            bottom: ordered_bottom,
94            left: ordered_left,
95            top: ordered_top,
96            right: ordered_right,
97        }
98    }
99
100    /// Creates a new [PdfRect] from the given raw points values.
101    ///
102    /// The coordinate space of a `PdfPage` has its origin (0,0) at the bottom left of the page,
103    /// with x values increasing as coordinates move horizontally to the right and
104    /// y values increasing as coordinates move vertically up.
105    #[inline]
106    pub const fn new_from_values(bottom: f32, left: f32, top: f32, right: f32) -> Self {
107        Self::new(
108            PdfPoints::new(bottom),
109            PdfPoints::new(left),
110            PdfPoints::new(top),
111            PdfPoints::new(right),
112        )
113    }
114
115    /// Creates a new [PdfRect] object with all values set to 0.0.
116    ///
117    /// Consider using the compile-time constant value [PdfRect::ZERO]
118    /// rather than calling this function directly.
119    #[inline]
120    pub const fn zero() -> Self {
121        Self::new_from_values(0.0, 0.0, 0.0, 0.0)
122    }
123
124    /// Returns the left-most extent of this [PdfRect].
125    #[inline]
126    pub const fn left(&self) -> PdfPoints {
127        #[allow(deprecated)]
128        self.left
129    }
130
131    /// Returns the right-most extent of this [PdfRect].
132    #[inline]
133    pub const fn right(&self) -> PdfPoints {
134        #[allow(deprecated)]
135        self.right
136    }
137
138    /// Returns the bottom-most extent of this [PdfRect].
139    #[inline]
140    pub const fn bottom(&self) -> PdfPoints {
141        #[allow(deprecated)]
142        self.bottom
143    }
144
145    /// Returns the top-most extent of this [PdfRect].
146    #[inline]
147    pub const fn top(&self) -> PdfPoints {
148        #[allow(deprecated)]
149        self.top
150    }
151
152    /// Returns the width of this [PdfRect].
153    #[inline]
154    pub fn width(&self) -> PdfPoints {
155        self.right() - self.left()
156    }
157
158    /// Returns the height of this [PdfRect].
159    #[inline]
160    pub fn height(&self) -> PdfPoints {
161        self.top() - self.bottom()
162    }
163
164    #[inline]
165    /// Returns `true` if the given point lies inside this [PdfRect].
166    pub fn contains(&self, x: PdfPoints, y: PdfPoints) -> bool {
167        self.contains_x(x) && self.contains_y(y)
168    }
169
170    /// Returns `true` if the given horizontal coordinate lies inside this [PdfRect].
171    #[inline]
172    pub fn contains_x(&self, x: PdfPoints) -> bool {
173        self.left() <= x && self.right() >= x
174    }
175
176    /// Returns `true` if the given vertical coordinate lies inside this [PdfRect].
177    #[inline]
178    pub fn contains_y(&self, y: PdfPoints) -> bool {
179        self.bottom() <= y && self.top() >= y
180    }
181
182    /// Returns `true` if the bounds of this [PdfRect] lie entirely within the given rectangle.
183    #[inline]
184    pub fn is_inside(&self, other: &PdfRect) -> bool {
185        self.left() >= other.left()
186            && self.right() <= other.right()
187            && self.top() <= other.top()
188            && self.bottom() >= other.bottom()
189    }
190
191    /// Returns `true` if the bounds of this [PdfRect] lie at least partially within
192    /// the given rectangle.
193    #[inline]
194    pub fn does_overlap(&self, other: &PdfRect) -> bool {
195        self.left() < other.right()
196            && self.right() > other.left()
197            && self.top() > other.bottom()
198            && self.bottom() < other.top()
199    }
200
201    /// Returns the result of applying the given [PdfMatrix] to each corner point of this [PdfRect].
202    #[inline]
203    pub fn transform(&self, matrix: PdfMatrix) -> PdfRect {
204        let (x1, y1) = matrix.apply_to_points(self.left(), self.top());
205        let (x2, y2) = matrix.apply_to_points(self.left(), self.bottom());
206        let (x3, y3) = matrix.apply_to_points(self.right(), self.top());
207        let (x4, y4) = matrix.apply_to_points(self.right(), self.bottom());
208
209        PdfRect::new(
210            min([y1, y2, y3, y4]).unwrap_or(PdfPoints::ZERO),
211            min([x1, x2, x3, x4]).unwrap_or(PdfPoints::ZERO),
212            max([y1, y2, y3, y4]).unwrap_or(PdfPoints::ZERO),
213            max([x1, x2, x3, x4]).unwrap_or(PdfPoints::ZERO),
214        )
215    }
216
217    /// Returns the [PdfQuadPoints] quadrilateral representation of this [PdfRect].
218    #[inline]
219    pub fn to_quad_points(&self) -> PdfQuadPoints {
220        PdfQuadPoints::from_rect(self)
221    }
222
223    #[inline]
224    pub(crate) fn as_pdfium(&self) -> FS_RECTF {
225        FS_RECTF {
226            left: self.left().value,
227            top: self.top().value,
228            right: self.right().value,
229            bottom: self.bottom().value,
230        }
231    }
232}
233
234impl PartialEq for PdfRect {
235    fn eq(&self, other: &Self) -> bool {
236        self.bottom() == other.bottom()
237            && self.left() == other.left()
238            && self.top() == other.top()
239            && self.right() == other.right()
240    }
241}
242
243impl Eq for PdfRect {}
244
245impl Hash for PdfRect {
246    fn hash<H: Hasher>(&self, state: &mut H) {
247        state.write_u32(self.bottom().value.to_bits());
248        state.write_u32(self.left().value.to_bits());
249        state.write_u32(self.top().value.to_bits());
250        state.write_u32(self.right().value.to_bits());
251    }
252}
253
254impl Display for PdfRect {
255    #[inline]
256    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
257        f.write_fmt(format_args!(
258            "PdfRect(bottom: {}, left: {}, top: {}, right: {})",
259            self.bottom().value,
260            self.left().value,
261            self.top().value,
262            self.right().value
263        ))
264    }
265}
266
267#[cfg(test)]
268mod tests {
269    use crate::prelude::*;
270
271    #[test]
272    fn test_rect_is_inside() {
273        assert!(
274            PdfRect::new_from_values(3.0, 3.0, 9.0, 9.0).is_inside(&PdfRect::new_from_values(2.0, 2.0, 10.0, 10.0))
275        );
276
277        assert!(
278            !PdfRect::new_from_values(2.0, 2.0, 10.0, 10.0).is_inside(&PdfRect::new_from_values(3.0, 3.0, 9.0, 9.0))
279        );
280
281        assert!(
282            !PdfRect::new_from_values(2.0, 2.0, 7.0, 7.0).is_inside(&PdfRect::new_from_values(5.0, 4.0, 10.0, 10.0))
283        );
284
285        assert!(
286            !PdfRect::new_from_values(2.0, 2.0, 7.0, 7.0).is_inside(&PdfRect::new_from_values(8.0, 4.0, 10.0, 10.0))
287        );
288
289        assert!(
290            !PdfRect::new_from_values(2.0, 2.0, 7.0, 7.0).is_inside(&PdfRect::new_from_values(5.0, 8.0, 10.0, 10.0))
291        );
292    }
293
294    #[test]
295    fn test_rect_does_overlap() {
296        assert!(
297            PdfRect::new_from_values(2.0, 2.0, 7.0, 7.0).does_overlap(&PdfRect::new_from_values(5.0, 4.0, 10.0, 10.0))
298        );
299
300        assert!(
301            !PdfRect::new_from_values(2.0, 2.0, 7.0, 7.0).does_overlap(&PdfRect::new_from_values(8.0, 4.0, 10.0, 10.0))
302        );
303
304        assert!(
305            !PdfRect::new_from_values(2.0, 2.0, 7.0, 7.0).does_overlap(&PdfRect::new_from_values(5.0, 8.0, 10.0, 10.0))
306        );
307    }
308
309    #[test]
310    fn test_transform_rect() {
311        let delta_x = PdfPoints::new(50.0);
312        let delta_y = PdfPoints::new(-25.0);
313
314        let matrix = PdfMatrix::identity().translate(delta_x, delta_y).unwrap();
315
316        let bottom = PdfPoints::new(100.0);
317        let top = PdfPoints::new(200.0);
318        let left = PdfPoints::new(300.0);
319        let right = PdfPoints::new(400.0);
320
321        let rect = PdfRect::new(bottom, left, top, right);
322
323        let result = rect.transform(matrix);
324
325        assert_eq!(result.bottom(), bottom + delta_y);
326        assert_eq!(result.top(), top + delta_y);
327        assert_eq!(result.left(), left + delta_x);
328        assert_eq!(result.right(), right + delta_x);
329    }
330
331    #[test]
332    fn test_coordinate_space_order_guard() {
333        let result = PdfRect::new_from_values(149.0, 544.0, 73.0, 48.0);
334
335        assert_eq!(result.bottom().value, 73.0);
336        assert_eq!(result.top().value, 149.0);
337        assert_eq!(result.left().value, 48.0);
338        assert_eq!(result.right().value, 544.0);
339    }
340}