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