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    // 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(
53        PdfPoints::MIN,
54        PdfPoints::MIN,
55        PdfPoints::MAX,
56        PdfPoints::MAX,
57    );
58
59    #[inline]
60    pub(crate) fn from_pdfium(rect: FS_RECTF) -> Self {
61        #[allow(deprecated)]
62        Self {
63            bottom: PdfPoints::new(rect.bottom),
64            left: PdfPoints::new(rect.left),
65            top: PdfPoints::new(rect.top),
66            right: PdfPoints::new(rect.right),
67        }
68    }
69
70    #[inline]
71    pub(crate) fn from_pdfium_as_result(
72        result: FPDF_BOOL,
73        rect: FS_RECTF,
74        bindings: &dyn PdfiumLibraryBindings,
75    ) -> Result<PdfRect, PdfiumError> {
76        if !bindings.is_true(result) {
77            Err(PdfiumError::PdfiumLibraryInternalError(
78                PdfiumInternalError::Unknown,
79            ))
80        } else {
81            Ok(PdfRect::from_pdfium(rect))
82        }
83    }
84
85    /// Creates a new [PdfRect] from the given [PdfPoints] measurements.
86    ///
87    /// The coordinate space of a `PdfPage` has its origin (0,0) at the bottom left of the page,
88    /// with x values increasing as coordinates move horizontally to the right and
89    /// y values increasing as coordinates move vertically up.
90    #[inline]
91    pub const fn new(bottom: PdfPoints, left: PdfPoints, top: PdfPoints, right: PdfPoints) -> Self {
92        #[allow(deprecated)]
93        Self {
94            bottom,
95            left,
96            top,
97            right,
98        }
99    }
100
101    /// Creates a new [PdfRect] from the given raw points values.
102    ///
103    /// The coordinate space of a `PdfPage` has its origin (0,0) at the bottom left of the page,
104    /// with x values increasing as coordinates move horizontally to the right and
105    /// y values increasing as coordinates move vertically up.
106    #[inline]
107    pub const fn new_from_values(bottom: f32, left: f32, top: f32, right: f32) -> Self {
108        Self::new(
109            PdfPoints::new(bottom),
110            PdfPoints::new(left),
111            PdfPoints::new(top),
112            PdfPoints::new(right),
113        )
114    }
115
116    /// Creates a new [PdfRect] object with all values set to 0.0.
117    ///
118    /// Consider using the compile-time constant value [PdfRect::ZERO]
119    /// rather than calling this function directly.
120    #[inline]
121    pub const fn zero() -> Self {
122        Self::new_from_values(0.0, 0.0, 0.0, 0.0)
123    }
124
125    /// Returns the left-most extent of this [PdfRect].
126    #[inline]
127    pub const fn left(&self) -> PdfPoints {
128        #[allow(deprecated)]
129        self.left
130    }
131
132    /// Returns the right-most extent of this [PdfRect].
133    #[inline]
134    pub const fn right(&self) -> PdfPoints {
135        #[allow(deprecated)]
136        self.right
137    }
138
139    /// Returns the bottom-most extent of this [PdfRect].
140    #[inline]
141    pub const fn bottom(&self) -> PdfPoints {
142        #[allow(deprecated)]
143        self.bottom
144    }
145
146    /// Returns the top-most extent of this [PdfRect].
147    #[inline]
148    pub const fn top(&self) -> PdfPoints {
149        #[allow(deprecated)]
150        self.top
151    }
152
153    /// Returns the width of this [PdfRect].
154    #[inline]
155    pub fn width(&self) -> PdfPoints {
156        self.right() - self.left()
157    }
158
159    /// Returns the height of this [PdfRect].
160    #[inline]
161    pub fn height(&self) -> PdfPoints {
162        self.top() - self.bottom()
163    }
164
165    #[inline]
166    /// Returns `true` if the given point lies inside this [PdfRect].
167    pub fn contains(&self, x: PdfPoints, y: PdfPoints) -> bool {
168        self.contains_x(x) && self.contains_y(y)
169    }
170
171    /// Returns `true` if the given horizontal coordinate lies inside this [PdfRect].
172    #[inline]
173    pub fn contains_x(&self, x: PdfPoints) -> bool {
174        self.left() <= x && self.right() >= x
175    }
176
177    /// Returns `true` if the given vertical coordinate lies inside this [PdfRect].
178    #[inline]
179    pub fn contains_y(&self, y: PdfPoints) -> bool {
180        self.bottom() <= y && self.top() >= y
181    }
182
183    /// Returns `true` if the bounds of this [PdfRect] lie entirely within the given rectangle.
184    #[inline]
185    pub fn is_inside(&self, other: &PdfRect) -> bool {
186        self.left() >= other.left()
187            && self.right() <= other.right()
188            && self.top() <= other.top()
189            && self.bottom() >= other.bottom()
190    }
191
192    /// Returns `true` if the bounds of this [PdfRect] lie at least partially within
193    /// the given rectangle.
194    #[inline]
195    pub fn does_overlap(&self, other: &PdfRect) -> bool {
196        // As per https://stackoverflow.com/questions/306316/determine-if-two-rectangles-overlap-each-other
197
198        self.left() < other.right()
199            && self.right() > other.left()
200            && self.top() > other.bottom()
201            && self.bottom() < other.top()
202    }
203
204    /// Returns the result of applying the given [PdfMatrix] to each corner point of this [PdfRect].
205    #[inline]
206    pub fn transform(&self, matrix: PdfMatrix) -> PdfRect {
207        let (x1, y1) = matrix.apply_to_points(self.left(), self.top());
208        let (x2, y2) = matrix.apply_to_points(self.left(), self.bottom());
209        let (x3, y3) = matrix.apply_to_points(self.right(), self.top());
210        let (x4, y4) = matrix.apply_to_points(self.right(), self.bottom());
211
212        PdfRect::new(
213            min([y1, y2, y3, y4]).unwrap_or(PdfPoints::ZERO),
214            min([x1, x2, x3, x4]).unwrap_or(PdfPoints::ZERO),
215            max([y1, y2, y3, y4]).unwrap_or(PdfPoints::ZERO),
216            max([x1, x2, x3, x4]).unwrap_or(PdfPoints::ZERO),
217        )
218    }
219
220    /// Returns the [PdfQuadPoints] quadrilateral representation of this [PdfRect].
221    #[inline]
222    pub fn to_quad_points(&self) -> PdfQuadPoints {
223        PdfQuadPoints::from_rect(self)
224    }
225
226    #[inline]
227    pub(crate) fn as_pdfium(&self) -> FS_RECTF {
228        FS_RECTF {
229            left: self.left().value,
230            top: self.top().value,
231            right: self.right().value,
232            bottom: self.bottom().value,
233        }
234    }
235}
236
237// We could derive PartialEq automatically, but it's good practice to implement PartialEq
238// by hand when implementing Hash.
239
240impl PartialEq for PdfRect {
241    fn eq(&self, other: &Self) -> bool {
242        self.bottom() == other.bottom()
243            && self.left() == other.left()
244            && self.top() == other.top()
245            && self.right() == other.right()
246    }
247}
248
249// The f32 values inside PdfRect will never be NaN or Infinity, so these implementations
250// of Eq and Hash are safe.
251
252impl Eq for PdfRect {}
253
254impl Hash for PdfRect {
255    fn hash<H: Hasher>(&self, state: &mut H) {
256        state.write_u32(self.bottom().value.to_bits());
257        state.write_u32(self.left().value.to_bits());
258        state.write_u32(self.top().value.to_bits());
259        state.write_u32(self.right().value.to_bits());
260    }
261}
262
263impl Display for PdfRect {
264    #[inline]
265    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
266        f.write_fmt(format_args!(
267            "PdfRect(bottom: {}, left: {}, top: {}, right: {})",
268            self.bottom().value,
269            self.left().value,
270            self.top().value,
271            self.right().value
272        ))
273    }
274}
275
276#[cfg(test)]
277mod tests {
278    use crate::prelude::*;
279
280    #[test]
281    fn test_rect_is_inside() {
282        assert!(PdfRect::new_from_values(3.0, 3.0, 9.0, 9.0)
283            .is_inside(&PdfRect::new_from_values(2.0, 2.0, 10.0, 10.0)));
284
285        assert!(!PdfRect::new_from_values(2.0, 2.0, 10.0, 10.0)
286            .is_inside(&PdfRect::new_from_values(3.0, 3.0, 9.0, 9.0)));
287
288        assert!(!PdfRect::new_from_values(2.0, 2.0, 7.0, 7.0)
289            .is_inside(&PdfRect::new_from_values(5.0, 4.0, 10.0, 10.0)));
290
291        assert!(!PdfRect::new_from_values(2.0, 2.0, 7.0, 7.0)
292            .is_inside(&PdfRect::new_from_values(8.0, 4.0, 10.0, 10.0)));
293
294        assert!(!PdfRect::new_from_values(2.0, 2.0, 7.0, 7.0)
295            .is_inside(&PdfRect::new_from_values(5.0, 8.0, 10.0, 10.0)));
296    }
297
298    #[test]
299    fn test_rect_does_overlap() {
300        assert!(PdfRect::new_from_values(2.0, 2.0, 7.0, 7.0)
301            .does_overlap(&PdfRect::new_from_values(5.0, 4.0, 10.0, 10.0)));
302
303        assert!(!PdfRect::new_from_values(2.0, 2.0, 7.0, 7.0)
304            .does_overlap(&PdfRect::new_from_values(8.0, 4.0, 10.0, 10.0)));
305
306        assert!(!PdfRect::new_from_values(2.0, 2.0, 7.0, 7.0)
307            .does_overlap(&PdfRect::new_from_values(5.0, 8.0, 10.0, 10.0)));
308    }
309
310    #[test]
311    fn test_transform_rect() {
312        let delta_x = PdfPoints::new(50.0);
313        let delta_y = PdfPoints::new(-25.0);
314
315        let matrix = PdfMatrix::identity().translate(delta_x, delta_y).unwrap();
316
317        let bottom = PdfPoints::new(100.0);
318        let top = PdfPoints::new(200.0);
319        let left = PdfPoints::new(300.0);
320        let right = PdfPoints::new(400.0);
321
322        let rect = PdfRect::new(bottom, left, top, right);
323
324        let result = rect.transform(matrix);
325
326        assert_eq!(result.bottom(), bottom + delta_y);
327        assert_eq!(result.top(), top + delta_y);
328        assert_eq!(result.left(), left + delta_x);
329        assert_eq!(result.right(), right + delta_x);
330    }
331}