pdfium_render/pdf/
quad_points.rs

1//! Defines the [PdfQuadPoints] struct, a set of four coordinates expressed in [PdfPoints]
2//! that outline the bounds of a four-sided quadrilateral.
3
4use crate::bindgen::{FPDF_BOOL, FS_QUADPOINTSF};
5use crate::bindings::PdfiumLibraryBindings;
6use crate::error::{PdfiumError, PdfiumInternalError};
7use crate::pdf::matrix::PdfMatrix;
8use crate::pdf::points::PdfPoints;
9use crate::pdf::rect::PdfRect;
10use std::fmt::{Display, Formatter};
11use std::hash::{Hash, Hasher};
12
13/// A set of four coordinates expressed in [PdfPoints] that outline the bounds of a
14/// four-sided quadrilateral. The coordinates specify the quadrilateral's four vertices
15/// in counter-clockwise order:
16/// ```
17/// (x4, y4)                     (x3, y3)
18///        ._____________________.
19///        |                     |
20///        |                     |
21///        !_____________________!
22/// (x1, y1)                     (x2, y2)
23/// ```
24/// More information on quad points can be found in Section 8.30 of the PDF Reference Manual,
25/// version 1.7, on page 634.
26#[derive(Debug, Copy, Clone)]
27pub struct PdfQuadPoints {
28    pub x1: PdfPoints,
29    pub y1: PdfPoints,
30    pub x2: PdfPoints,
31    pub y2: PdfPoints,
32    pub x3: PdfPoints,
33    pub y3: PdfPoints,
34    pub x4: PdfPoints,
35    pub y4: PdfPoints,
36    // TODO: AJRC - 28/12/24 - directly available fields are in place purely to achieve
37    // API-compatibility with PdfRect as part of changes to bounds() function implementation
38    // in page objects. Direct field access should be removed in favour of accessor functions
39    // as part of release 0.9.0.
40    #[deprecated(
41        since = "0.8.28",
42        note = "Use the PdfQuadPoints::bottom() function instead of direct field access. Direct field access will be removed in release 0.9.0."
43    )]
44    pub bottom: PdfPoints,
45
46    #[deprecated(
47        since = "0.8.28",
48        note = "Use the PdfQuadPoints::left() function instead of direct field access. Direct field access will be removed in release 0.9.0."
49    )]
50    pub left: PdfPoints,
51
52    #[deprecated(
53        since = "0.8.28",
54        note = "Use the PdfQuadPoints::top() function instead of direct field access. Direct field access will be removed in release 0.9.0."
55    )]
56    pub top: PdfPoints,
57
58    #[deprecated(
59        since = "0.8.28",
60        note = "Use the PdfQuadPoints::right() function instead of direct field access. Direct field access will be removed in release 0.9.0."
61    )]
62    pub right: PdfPoints,
63}
64
65impl PdfQuadPoints {
66    /// A [PdfQuadPoints] object with the identity value (0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0).
67    #[allow(deprecated)]
68    pub const ZERO: PdfQuadPoints = PdfQuadPoints {
69        // TODO: AJRC - 28/12/24 - this in-place initialiser can be reduced to
70        // PdfQuadPoints::zero() once deprecated features are removed as part of release 0.9.0.
71        x1: PdfPoints::zero(),
72        y1: PdfPoints::zero(),
73        x2: PdfPoints::zero(),
74        y2: PdfPoints::zero(),
75        x3: PdfPoints::zero(),
76        y3: PdfPoints::zero(),
77        x4: PdfPoints::zero(),
78        y4: PdfPoints::zero(),
79        bottom: PdfPoints::zero(),
80        left: PdfPoints::zero(),
81        top: PdfPoints::zero(),
82        right: PdfPoints::zero(),
83    };
84
85    #[inline]
86    pub(crate) fn from_pdfium(points: FS_QUADPOINTSF) -> Self {
87        PdfQuadPoints::new_from_values(
88            points.x1, points.y1, points.x2, points.y2, points.x3, points.y3, points.x4, points.y4,
89        )
90    }
91
92    #[inline]
93    pub(crate) fn from_pdfium_as_result(
94        result: FPDF_BOOL,
95        points: FS_QUADPOINTSF,
96        bindings: &dyn PdfiumLibraryBindings,
97    ) -> Result<PdfQuadPoints, PdfiumError> {
98        if !bindings.is_true(result) {
99            Err(PdfiumError::PdfiumLibraryInternalError(
100                PdfiumInternalError::Unknown,
101            ))
102        } else {
103            Ok(PdfQuadPoints::from_pdfium(points))
104        }
105    }
106
107    /// Creates a new [PdfQuadPoints] from the given [PdfPoints] coordinate pairs.
108    ///
109    /// The coordinate space of a `PdfPage` has its origin (0,0) at the bottom left of the page,
110    /// with x values increasing as coordinates move horizontally to the right and
111    /// y values increasing as coordinates move vertically up.
112    #[inline]
113    #[allow(clippy::too_many_arguments)]
114    // TODO: AJRC - 28/12/24 - return to const function once left, right, bottom, top fields
115    // removed as part of release 0.9.0.
116    pub fn new(
117        x1: PdfPoints,
118        y1: PdfPoints,
119        x2: PdfPoints,
120        y2: PdfPoints,
121        x3: PdfPoints,
122        y3: PdfPoints,
123        x4: PdfPoints,
124        y4: PdfPoints,
125    ) -> Self {
126        #[allow(deprecated)]
127        Self {
128            x1,
129            y1,
130            x2,
131            y2,
132            x3,
133            y3,
134            x4,
135            y4,
136            left: *[x1, x2, x3, x4].iter().min().unwrap(),
137            right: *[x1, x2, x3, x4].iter().max().unwrap(),
138            bottom: *[y1, y2, y3, y4].iter().min().unwrap(),
139            top: *[y1, y2, y3, y4].iter().max().unwrap(),
140        }
141    }
142
143    /// Creates a new [PdfQuadPoints] from the given raw points values.
144    ///
145    /// The coordinate space of a `PdfPage` has its origin (0,0) at the bottom left of the page,
146    /// with x values increasing as coordinates move horizontally to the right and
147    /// y values increasing as coordinates move vertically up.
148    #[inline]
149    #[allow(clippy::too_many_arguments)]
150    // TODO: AJRC - 28/12/24 - return to const function once left, right, bottom, top fields
151    // removed as part of release 0.9.0.
152    pub fn new_from_values(
153        x1: f32,
154        y1: f32,
155        x2: f32,
156        y2: f32,
157        x3: f32,
158        y3: f32,
159        x4: f32,
160        y4: f32,
161    ) -> Self {
162        Self::new(
163            PdfPoints::new(x1),
164            PdfPoints::new(y1),
165            PdfPoints::new(x2),
166            PdfPoints::new(y2),
167            PdfPoints::new(x3),
168            PdfPoints::new(y3),
169            PdfPoints::new(x4),
170            PdfPoints::new(y4),
171        )
172    }
173
174    /// Creates a new [PdfQuadPoints] object with all values set to 0.0.
175    ///
176    /// Consider using the compile-time constant value [PdfQuadPoints::ZERO]
177    /// rather than calling this function directly.
178    #[inline]
179    // TODO: AJRC - 28/12/24 - return to const function once left, right, bottom, top fields
180    // removed as part of release 0.9.0.
181    pub fn zero() -> Self {
182        Self::new_from_values(0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0)
183    }
184
185    /// Creates a new [PdfQuadPoints] from the given [PdfRect].
186    #[inline]
187    pub fn from_rect(rect: &PdfRect) -> Self {
188        PdfQuadPoints::new(
189            rect.left(),
190            rect.bottom(),
191            rect.right(),
192            rect.bottom(),
193            rect.right(),
194            rect.top(),
195            rect.left(),
196            rect.top(),
197        )
198    }
199
200    /// Returns the left-most extent of this [PdfQuadPoints].
201    pub fn left(&self) -> PdfPoints {
202        #[allow(deprecated)]
203        // TODO: AJRC - 28/12/24 - replace with inline calculation for release 0.9.0.
204        self.left
205        // *vec![self.x1, self.x2, self.x3, self.x4]
206        //     .iter()
207        //     .min()
208        //     .unwrap()
209    }
210
211    /// Returns the right-most extent of this [PdfQuadPoints].
212    pub fn right(&self) -> PdfPoints {
213        #[allow(deprecated)]
214        // TODO: AJRC - 28/12/24 - replace with inline calculation for release 0.9.0.
215        self.right
216        // *vec![self.x1, self.x2, self.x3, self.x4]
217        //     .iter()
218        //     .max()
219        //     .unwrap()
220    }
221
222    /// Returns the bottom-most extent of this [PdfQuadPoints].
223    pub fn bottom(&self) -> PdfPoints {
224        #[allow(deprecated)]
225        // TODO: AJRC - 28/12/24 - replace with inline calculation for release 0.9.0.
226        self.bottom
227        // *vec![self.y1, self.y2, self.y3, self.y4]
228        //     .iter()
229        //     .min()
230        //     .unwrap()
231    }
232
233    /// Returns the top-most extent of this [PdfQuadPoints].
234    pub fn top(&self) -> PdfPoints {
235        #[allow(deprecated)]
236        // TODO: AJRC - 28/12/24 - replace with inline calculation for release 0.9.0.
237        self.top
238        // *vec![self.y1, self.y2, self.y3, self.y4]
239        //     .iter()
240        //     .max()
241        //     .unwrap()
242    }
243
244    /// Returns the width of this [PdfQuadPoints].
245    #[inline]
246    pub fn width(&self) -> PdfPoints {
247        self.right() - self.left()
248    }
249
250    /// Returns the height of this [PdfQuadPoints].
251    #[inline]
252    pub fn height(&self) -> PdfPoints {
253        self.top() - self.bottom()
254    }
255
256    /// Returns the result of applying the given [PdfMatrix] to each corner point
257    // of this [PdfQuadPoints].
258    #[inline]
259    pub fn transform(&self, matrix: PdfMatrix) -> PdfQuadPoints {
260        let (x1, y1) = matrix.apply_to_points(self.x1, self.y1);
261        let (x2, y2) = matrix.apply_to_points(self.x2, self.y2);
262        let (x3, y3) = matrix.apply_to_points(self.x3, self.y3);
263        let (x4, y4) = matrix.apply_to_points(self.x4, self.y4);
264
265        PdfQuadPoints::new(x1, y1, x2, y2, x3, y3, x4, y4)
266    }
267
268    /// Returns the smallest [PdfRect] that can completely enclose the quadrilateral
269    /// outlined by this [PdfQuadPoints].
270    pub fn to_rect(&self) -> PdfRect {
271        let xs = [self.x1, self.x2, self.x3, self.x4];
272        let ys = [self.y1, self.y2, self.y3, self.y4];
273
274        PdfRect::new(
275            *ys.iter().min().unwrap(),
276            *xs.iter().min().unwrap(),
277            *ys.iter().max().unwrap(),
278            *xs.iter().max().unwrap(),
279        )
280    }
281
282    #[inline]
283    pub(crate) fn as_pdfium(&self) -> FS_QUADPOINTSF {
284        FS_QUADPOINTSF {
285            x1: self.x1.value,
286            y1: self.y1.value,
287            x2: self.x2.value,
288            y2: self.y2.value,
289            x3: self.x3.value,
290            y3: self.y3.value,
291            x4: self.x4.value,
292            y4: self.y4.value,
293        }
294    }
295}
296
297// We could derive PartialEq automatically, but it's good practice to implement PartialEq
298// by hand when implementing Hash.
299
300impl PartialEq for PdfQuadPoints {
301    fn eq(&self, other: &Self) -> bool {
302        self.x1 == other.x1
303            && self.y1 == other.y1
304            && self.x2 == other.x2
305            && self.y2 == other.y2
306            && self.x3 == other.x3
307            && self.y3 == other.y3
308            && self.x4 == other.x4
309            && self.y4 == other.y4
310    }
311}
312
313// The f32 values inside PdfQuadPoints will never be NaN or Infinity, so these implementations
314// of Eq and Hash are safe.
315
316impl Eq for PdfQuadPoints {}
317
318impl Hash for PdfQuadPoints {
319    fn hash<H: Hasher>(&self, state: &mut H) {
320        state.write_u32(self.x1.value.to_bits());
321        state.write_u32(self.y1.value.to_bits());
322        state.write_u32(self.x2.value.to_bits());
323        state.write_u32(self.y2.value.to_bits());
324        state.write_u32(self.x3.value.to_bits());
325        state.write_u32(self.y3.value.to_bits());
326        state.write_u32(self.x4.value.to_bits());
327        state.write_u32(self.y4.value.to_bits());
328    }
329}
330
331impl Display for PdfQuadPoints {
332    #[inline]
333    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
334        f.write_fmt(format_args!(
335            "PdfQuadPoints(x1: {}, y1: {}, x2: {}, y2: {}, x3: {}, y3: {}, x4: {}, y4: {})",
336            self.x1.value,
337            self.y1.value,
338            self.x2.value,
339            self.y2.value,
340            self.x3.value,
341            self.y3.value,
342            self.x4.value,
343            self.y4.value
344        ))
345    }
346}
347
348#[cfg(test)]
349mod tests {
350    use crate::prelude::*;
351
352    #[test]
353    fn test_quadpoints_extents() {
354        let r = PdfRect::new_from_values(50.0, 100.0, 300.0, 200.0);
355
356        assert_eq!(r.to_quad_points().left().value, 100.0);
357        assert_eq!(r.to_quad_points().right().value, 200.0);
358        assert_eq!(r.to_quad_points().top().value, 300.0);
359        assert_eq!(r.to_quad_points().bottom().value, 50.0);
360
361        assert_eq!(r.to_quad_points().width().value, 100.0);
362        assert_eq!(r.to_quad_points().height().value, 250.0);
363    }
364
365    #[test]
366    fn test_quadpoints_to_rect() {
367        let r = PdfRect::new_from_values(100.0, 100.0, 200.0, 200.0);
368        assert_eq!(r.to_quad_points().to_rect(), r);
369
370        let m = PdfMatrix::identity()
371            .rotate_clockwise_degrees(45.0)
372            .unwrap();
373        let r45 = r.transform(m);
374
375        let q = r.to_quad_points();
376        let q45 = q.transform(m);
377
378        assert_eq!(q.to_rect(), r);
379        assert_eq!(q45.to_rect(), r45);
380
381        // It would be incredibly elegant to test
382
383        // assert_eq!(q45.transform(m.invert()).to_rect(), r);
384
385        // but sadly floating point rounding errors means the double-transformed values
386        // are ever-so-slightly off (by a fraction of a PdfPoint). Let's test manually
387        // so we can apply a comparison threshold.
388
389        let s = q45.transform(m.invert()).to_rect();
390        let threshold = PdfPoints::new(0.001);
391        assert!((s.top() - r.top()).abs() < threshold);
392        assert!((s.bottom() - r.bottom()).abs() < threshold);
393        assert!((s.left() - r.left()).abs() < threshold);
394        assert!((s.right() - r.right()).abs() < threshold);
395    }
396}