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