pdfium_render/pdf/document/page/object/
path.rs

1//! Defines the [PdfPagePathObject] struct, exposing functionality related to a single
2//! page object defining a path.
3
4use crate::bindgen::{
5    FPDF_BOOL, FPDF_DOCUMENT, FPDF_FILLMODE_ALTERNATE, FPDF_FILLMODE_NONE, FPDF_FILLMODE_WINDING,
6    FPDF_PAGEOBJECT,
7};
8use crate::bindings::PdfiumLibraryBindings;
9use crate::error::{PdfiumError, PdfiumInternalError};
10use crate::pdf::color::PdfColor;
11use crate::pdf::document::page::object::private::internal::PdfPageObjectPrivate;
12use crate::pdf::document::page::object::{
13    PdfPageObject, PdfPageObjectCommon, PdfPageObjectOwnership,
14};
15use crate::pdf::document::PdfDocument;
16use crate::pdf::matrix::{PdfMatrix, PdfMatrixValue};
17use crate::pdf::path::segment::{PdfPathSegment, PdfPathSegmentType};
18use crate::pdf::path::segments::{PdfPathSegmentIndex, PdfPathSegments, PdfPathSegmentsIterator};
19use crate::pdf::points::PdfPoints;
20use crate::pdf::rect::PdfRect;
21use crate::{create_transform_getters, create_transform_setters};
22use std::convert::TryInto;
23use std::os::raw::{c_int, c_uint};
24
25/// Sets the method used to determine the path region to fill.
26///
27/// The default fill mode used by `pdfium-render` when creating new [PdfPagePathObject]
28/// instances is [PdfPathFillMode::Winding]. The fill mode can be changed on an
29/// object-by-object basis by calling the [PdfPagePathObject::set_fill_and_stroke_mode()] function.
30#[derive(Copy, Clone, Debug, PartialEq)]
31pub enum PdfPathFillMode {
32    /// The path will not be filled.
33    None = FPDF_FILLMODE_NONE as isize,
34
35    /// The even-odd rule will be used to determine the path region to fill.
36    ///
37    /// The even-odd rule determines whether a point is inside a path by drawing a ray from that
38    /// point in any direction and simply counting the number of path segments that cross the
39    /// ray, regardless of direction. If this number is odd, the point is inside; if even, the
40    /// point is outside. This yields the same results as the nonzero winding number rule
41    /// for paths with simple shapes, but produces different results for more complex shapes.
42    ///
43    /// More information, including visual examples, can be found in Section 4.4.2 of
44    /// the PDF Reference Manual, version 1.7, on page 233.
45    EvenOdd = FPDF_FILLMODE_ALTERNATE as isize,
46
47    /// The non-zero winding number rule will be used to determine the path region to fill.
48    ///
49    /// The nonzero winding number rule determines whether a given point is inside a
50    /// path by conceptually drawing a ray from that point to infinity in any direction
51    /// and then examining the places where a segment of the path crosses the ray. Start-
52    /// ing with a count of 0, the rule adds 1 each time a path segment crosses the ray
53    /// from left to right and subtracts 1 each time a segment crosses from right to left.
54    /// After counting all the crossings, if the result is 0, the point is outside the path;
55    /// otherwise, it is inside.
56    ///
57    /// This is the default fill mode used by `pdfium-render` when creating new [PdfPagePathObject]
58    /// instances. The fill mode can be changed on an object-by-object basis by calling the
59    /// [PdfPagePathObject::set_fill_and_stroke_mode()] function.
60    ///
61    /// More information, including visual examples, can be found in Section 4.4.2 of
62    /// the PDF Reference Manual, version 1.7, on page 232.
63    Winding = FPDF_FILLMODE_WINDING as isize,
64}
65
66impl PdfPathFillMode {
67    #[inline]
68    pub(crate) fn from_pdfium(value: c_int) -> Result<PdfPathFillMode, PdfiumError> {
69        match value as u32 {
70            FPDF_FILLMODE_NONE => Ok(PdfPathFillMode::None),
71            FPDF_FILLMODE_ALTERNATE => Ok(PdfPathFillMode::EvenOdd),
72            FPDF_FILLMODE_WINDING => Ok(PdfPathFillMode::Winding),
73            _ => Err(PdfiumError::UnknownPdfPagePathFillMode),
74        }
75    }
76
77    #[inline]
78    #[allow(dead_code)]
79    // The as_pdfium() function is not currently used, but we expect it to be in future
80    pub(crate) fn as_pdfium(&self) -> c_uint {
81        match self {
82            PdfPathFillMode::None => FPDF_FILLMODE_NONE,
83            PdfPathFillMode::EvenOdd => FPDF_FILLMODE_ALTERNATE,
84            PdfPathFillMode::Winding => FPDF_FILLMODE_WINDING,
85        }
86    }
87}
88
89impl Default for PdfPathFillMode {
90    /// Returns the default fill mode used when creating new [PdfPagePathObject]
91    /// instances. The fill mode can be changed on an object-by-object basis by calling the
92    /// [PdfPagePathObject::set_fill_and_stroke_mode()] function.
93    #[inline]
94    fn default() -> Self {
95        PdfPathFillMode::Winding
96    }
97}
98
99/// A single `PdfPageObject` of type `PdfPageObjectType::Path`. The page object defines a path.
100///
101/// Paths define shapes, trajectories, and regions of all sorts. They are used to draw
102/// lines, define the shapes of filled areas, and specify boundaries for clipping other
103/// graphics. A path is composed of one or more _path segments_, each specifying
104/// a straight or curved line segment. Each segment may connect to one another, forming a
105/// _closed sub-path_, or may be disconnected from one another, forming one or more
106/// _open sub-paths_. A path therefore is made up of one or more disconnected sub-paths, each
107/// comprising a sequence of connected segments. Closed sub-paths can be filled;
108/// both closed and open sub-paths can be stroked. The topology of the path is unrestricted;
109/// it may be concave or convex, may contain multiple sub-paths representing disjoint areas,
110/// and may intersect itself in arbitrary ways.
111///
112/// Page objects can be created either attached to a `PdfPage` (in which case the page object's
113/// memory is owned by the containing page) or detached from any page (in which case the page
114/// object's memory is owned by the object). Page objects are not rendered until they are
115/// attached to a page; page objects that are never attached to a page will be lost when they
116/// fall out of scope.
117///
118/// The simplest way to create a path object that is immediately attached to a page is to call
119/// one of the `PdfPageObjects::create_path_object_*()` functions to create lines, cubic Bézier curves,
120/// rectangles, circles, and ellipses. Alternatively you can create a detached path object using
121/// one of the following functions, but you must add the object to a containing `PdfPageObjects`
122/// collection manually.
123///
124/// * [PdfPagePathObject::new()]: creates an empty detached path object. Segments can be added to the
125///   path by sequentially calling one or more of the [PdfPagePathObject::move_to()],
126///   [PdfPagePathObject::line_to()], or [PdfPagePathObject::bezier_to()] functions.
127///   A closed sub-path can be created by calling the [PdfPagePathObject::close_path()]
128///   function. Convenience functions for adding rectangles, circles, and ellipses are also
129///   available with the [PdfPagePathObject::rect_to()], [PdfPagePathObject::circle_to()],
130///   and [PdfPagePathObject::ellipse_to()] functions, which create the desired shapes by
131///   constructing closed sub-paths from other path segments.
132/// * [PdfPagePathObject::new_line()]: creates a detached path object initialized with a single straight line.
133/// * [PdfPagePathObject::new_bezier()]: creates a detached path object initialized with a single cubic Bézier curve.
134/// * [PdfPagePathObject::new_rect()]: creates a detached path object initialized with a rectangular path.
135/// * [PdfPagePathObject::new_circle()]: creates a detached path object initialized with a circular path,
136///   filling the given rectangle.
137/// * [PdfPagePathObject::new_circle_at()]: creates a detached path object initialized with a circular path,
138///   centered at a particular origin point with a given radius.
139/// * [PdfPagePathObject::new_ellipse()]: creates a detached path object initialized with an elliptical path,
140///   filling the given rectangle.
141/// * [PdfPagePathObject::new_ellipse_at()]: creates a detached path object initialized with an elliptical path,
142///   centered at a particular origin point with given horizontal and vertical radii.
143///
144/// The detached path object can later be attached to a page by calling the
145/// `PdfPageObjects::add_path_object()` function.
146pub struct PdfPagePathObject<'a> {
147    object_handle: FPDF_PAGEOBJECT,
148    ownership: PdfPageObjectOwnership,
149    bindings: &'a dyn PdfiumLibraryBindings,
150    current_point_x: PdfPoints,
151    current_point_y: PdfPoints,
152}
153
154impl<'a> PdfPagePathObject<'a> {
155    #[inline]
156    pub(crate) fn from_pdfium(
157        object_handle: FPDF_PAGEOBJECT,
158        ownership: PdfPageObjectOwnership,
159        bindings: &'a dyn PdfiumLibraryBindings,
160    ) -> Self {
161        PdfPagePathObject {
162            object_handle,
163            ownership,
164            bindings,
165            current_point_x: PdfPoints::ZERO,
166            current_point_y: PdfPoints::ZERO,
167        }
168    }
169
170    /// Creates a new [PdfPagePathObject] from the given arguments. The returned page object
171    /// will not be rendered until it is added to a `PdfPage` using the
172    /// `PdfPageObjects::add_path_object()` function.
173    ///
174    /// The new path will be created with the given initial position and with the given fill and stroke
175    /// settings applied. Both the stroke color and the stroke width must be provided for the
176    /// path to be stroked.
177    ///
178    /// Other than setting the initial position, this path will be empty. Add additional segments
179    /// to this path by calling one or more of the [PdfPagePathObject::move_to()],
180    /// [PdfPagePathObject::line_to()], or [PdfPagePathObject::bezier_to()]
181    /// functions. A closed sub-path can be created by calling the [PdfPagePathObject::close_path()]
182    /// function. Convenience functions for adding rectangles, circles, and ellipses are also
183    /// available with the [PdfPagePathObject::rect_to()], [PdfPagePathObject::circle_to()],
184    /// and [PdfPagePathObject::ellipse_to()] functions, which create the desired shapes by
185    /// constructing closed sub-paths from other path segments.
186    #[inline]
187    pub fn new(
188        document: &PdfDocument<'a>,
189        x: PdfPoints,
190        y: PdfPoints,
191        stroke_color: Option<PdfColor>,
192        stroke_width: Option<PdfPoints>,
193        fill_color: Option<PdfColor>,
194    ) -> Result<Self, PdfiumError> {
195        Self::new_from_bindings(
196            document.bindings(),
197            x,
198            y,
199            stroke_color,
200            stroke_width,
201            fill_color,
202        )
203    }
204
205    pub(crate) fn new_from_bindings(
206        bindings: &'a dyn PdfiumLibraryBindings,
207        x: PdfPoints,
208        y: PdfPoints,
209        stroke_color: Option<PdfColor>,
210        stroke_width: Option<PdfPoints>,
211        fill_color: Option<PdfColor>,
212    ) -> Result<Self, PdfiumError> {
213        let handle = bindings.FPDFPageObj_CreateNewPath(x.value, y.value);
214
215        if handle.is_null() {
216            Err(PdfiumError::PdfiumLibraryInternalError(
217                PdfiumInternalError::Unknown,
218            ))
219        } else {
220            let mut result = PdfPagePathObject {
221                object_handle: handle,
222                ownership: PdfPageObjectOwnership::unowned(),
223                bindings,
224                current_point_x: x,
225                current_point_y: y,
226            };
227
228            result.move_to(x, y)?;
229
230            let do_stroke = if let Some(stroke_color) = stroke_color {
231                if let Some(stroke_width) = stroke_width {
232                    result.set_stroke_color(stroke_color)?;
233                    result.set_stroke_width(stroke_width)?;
234
235                    true
236                } else {
237                    false
238                }
239            } else {
240                false
241            };
242
243            let fill_mode = if let Some(fill_color) = fill_color {
244                result.set_fill_color(fill_color)?;
245
246                PdfPathFillMode::default()
247            } else {
248                PdfPathFillMode::None
249            };
250
251            result.set_fill_and_stroke_mode(fill_mode, do_stroke)?;
252
253            Ok(result)
254        }
255    }
256
257    #[inline]
258    pub(crate) fn new_line_from_bindings(
259        bindings: &'a dyn PdfiumLibraryBindings,
260        x1: PdfPoints,
261        y1: PdfPoints,
262        x2: PdfPoints,
263        y2: PdfPoints,
264        stroke_color: PdfColor,
265        stroke_width: PdfPoints,
266    ) -> Result<Self, PdfiumError> {
267        let mut result = Self::new_from_bindings(
268            bindings,
269            x1,
270            y1,
271            Some(stroke_color),
272            Some(stroke_width),
273            None,
274        )?;
275
276        result.line_to(x2, y2)?;
277
278        Ok(result)
279    }
280
281    /// Creates a new [PdfPagePathObject] from the given arguments. The returned page object
282    /// will not be rendered until it is added to a `PdfPage` using the
283    /// `PdfPageObjects::add_path_object()` function.
284    ///
285    /// The new path will be created with a line with the given start and end coordinates,
286    /// and with the given stroke settings applied.
287    #[inline]
288    pub fn new_line(
289        document: &PdfDocument<'a>,
290        x1: PdfPoints,
291        y1: PdfPoints,
292        x2: PdfPoints,
293        y2: PdfPoints,
294        stroke_color: PdfColor,
295        stroke_width: PdfPoints,
296    ) -> Result<Self, PdfiumError> {
297        Self::new_line_from_bindings(
298            document.bindings(),
299            x1,
300            y1,
301            x2,
302            y2,
303            stroke_color,
304            stroke_width,
305        )
306    }
307
308    #[allow(clippy::too_many_arguments)]
309    #[inline]
310    pub(crate) fn new_bezier_from_bindings(
311        bindings: &'a dyn PdfiumLibraryBindings,
312        x1: PdfPoints,
313        y1: PdfPoints,
314        x2: PdfPoints,
315        y2: PdfPoints,
316        control1_x: PdfPoints,
317        control1_y: PdfPoints,
318        control2_x: PdfPoints,
319        control2_y: PdfPoints,
320        stroke_color: PdfColor,
321        stroke_width: PdfPoints,
322    ) -> Result<Self, PdfiumError> {
323        let mut result = Self::new_from_bindings(
324            bindings,
325            x1,
326            y1,
327            Some(stroke_color),
328            Some(stroke_width),
329            None,
330        )?;
331
332        result.bezier_to(x2, y2, control1_x, control1_y, control2_x, control2_y)?;
333
334        Ok(result)
335    }
336
337    /// Creates a new [PdfPagePathObject] from the given arguments. The returned page object
338    /// will not be rendered until it is added to a `PdfPage` using the
339    /// `PdfPageObjects::add_path_object()` function.
340    ///
341    /// The new path will be created with a cubic Bézier curve with the given start, end,
342    /// and control point coordinates, and with the given stroke settings applied.
343    #[allow(clippy::too_many_arguments)]
344    #[inline]
345    pub fn new_bezier(
346        document: &PdfDocument<'a>,
347        x1: PdfPoints,
348        y1: PdfPoints,
349        x2: PdfPoints,
350        y2: PdfPoints,
351        control1_x: PdfPoints,
352        control1_y: PdfPoints,
353        control2_x: PdfPoints,
354        control2_y: PdfPoints,
355        stroke_color: PdfColor,
356        stroke_width: PdfPoints,
357    ) -> Result<Self, PdfiumError> {
358        Self::new_bezier_from_bindings(
359            document.bindings(),
360            x1,
361            y1,
362            x2,
363            y2,
364            control1_x,
365            control1_y,
366            control2_x,
367            control2_y,
368            stroke_color,
369            stroke_width,
370        )
371    }
372
373    #[inline]
374    pub(crate) fn new_rect_from_bindings(
375        bindings: &'a dyn PdfiumLibraryBindings,
376        rect: PdfRect,
377        stroke_color: Option<PdfColor>,
378        stroke_width: Option<PdfPoints>,
379        fill_color: Option<PdfColor>,
380    ) -> Result<Self, PdfiumError> {
381        let mut result = Self::new_from_bindings(
382            bindings,
383            rect.left(),
384            rect.bottom(),
385            stroke_color,
386            stroke_width,
387            fill_color,
388        )?;
389
390        result.rect_to(rect.right(), rect.top())?;
391
392        Ok(result)
393    }
394
395    /// Creates a new [PdfPagePathObject] from the given arguments. The returned page object
396    /// will not be rendered until it is added to a `PdfPage` using the
397    /// `PdfPageObjects::add_path_object()` function.
398    ///
399    /// The new path will be created with a path for the given rectangle, with the given
400    /// fill and stroke settings applied. Both the stroke color and the stroke width must be
401    /// provided for the rectangle to be stroked.
402    #[inline]
403    pub fn new_rect(
404        document: &PdfDocument<'a>,
405        rect: PdfRect,
406        stroke_color: Option<PdfColor>,
407        stroke_width: Option<PdfPoints>,
408        fill_color: Option<PdfColor>,
409    ) -> Result<Self, PdfiumError> {
410        Self::new_rect_from_bindings(
411            document.bindings(),
412            rect,
413            stroke_color,
414            stroke_width,
415            fill_color,
416        )
417    }
418
419    #[inline]
420    pub(crate) fn new_circle_from_bindings(
421        bindings: &'a dyn PdfiumLibraryBindings,
422        rect: PdfRect,
423        stroke_color: Option<PdfColor>,
424        stroke_width: Option<PdfPoints>,
425        fill_color: Option<PdfColor>,
426    ) -> Result<Self, PdfiumError> {
427        let mut result = Self::new_from_bindings(
428            bindings,
429            rect.left(),
430            rect.bottom(),
431            stroke_color,
432            stroke_width,
433            fill_color,
434        )?;
435
436        result.circle_to(rect.right(), rect.top())?;
437
438        Ok(result)
439    }
440
441    /// Creates a new [PdfPagePathObject] from the given arguments. The returned page object
442    /// will not be rendered until it is added to a `PdfPage` using the
443    /// `PdfPageObjects::add_path_object()` function.
444    ///
445    /// The new path will be created with a circle that fills the given rectangle, with the given
446    /// fill and stroke settings applied. Both the stroke color and the stroke width must be
447    /// provided for the circle to be stroked.
448    #[inline]
449    pub fn new_circle(
450        document: &PdfDocument<'a>,
451        rect: PdfRect,
452        stroke_color: Option<PdfColor>,
453        stroke_width: Option<PdfPoints>,
454        fill_color: Option<PdfColor>,
455    ) -> Result<Self, PdfiumError> {
456        Self::new_circle_from_bindings(
457            document.bindings(),
458            rect,
459            stroke_color,
460            stroke_width,
461            fill_color,
462        )
463    }
464
465    #[inline]
466    pub(crate) fn new_circle_at_from_bindings(
467        bindings: &'a dyn PdfiumLibraryBindings,
468        center_x: PdfPoints,
469        center_y: PdfPoints,
470        radius: PdfPoints,
471        stroke_color: Option<PdfColor>,
472        stroke_width: Option<PdfPoints>,
473        fill_color: Option<PdfColor>,
474    ) -> Result<Self, PdfiumError> {
475        Self::new_circle_from_bindings(
476            bindings,
477            PdfRect::new(
478                center_y - radius,
479                center_x - radius,
480                center_y + radius,
481                center_x + radius,
482            ),
483            stroke_color,
484            stroke_width,
485            fill_color,
486        )
487    }
488
489    /// Creates a new [PdfPagePathObject] from the given arguments. The returned page object
490    /// will not be rendered until it is added to a `PdfPage` using the
491    /// `PdfPageObjects::add_path_object()` function.
492    ///
493    /// The new path will be created with a circle centered at the given coordinates, with the
494    /// given radius, and with the given fill and stroke settings applied. Both the stroke color
495    /// and the stroke width must be provided for the circle to be stroked.
496    #[inline]
497    pub fn new_circle_at(
498        document: &PdfDocument<'a>,
499        center_x: PdfPoints,
500        center_y: PdfPoints,
501        radius: PdfPoints,
502        stroke_color: Option<PdfColor>,
503        stroke_width: Option<PdfPoints>,
504        fill_color: Option<PdfColor>,
505    ) -> Result<Self, PdfiumError> {
506        Self::new_circle_at_from_bindings(
507            document.bindings(),
508            center_x,
509            center_y,
510            radius,
511            stroke_color,
512            stroke_width,
513            fill_color,
514        )
515    }
516
517    #[inline]
518    pub(crate) fn new_ellipse_from_bindings(
519        bindings: &'a dyn PdfiumLibraryBindings,
520        rect: PdfRect,
521        stroke_color: Option<PdfColor>,
522        stroke_width: Option<PdfPoints>,
523        fill_color: Option<PdfColor>,
524    ) -> Result<Self, PdfiumError> {
525        let mut result = Self::new_from_bindings(
526            bindings,
527            rect.left(),
528            rect.bottom(),
529            stroke_color,
530            stroke_width,
531            fill_color,
532        )?;
533
534        result.ellipse_to(rect.right(), rect.top())?;
535
536        Ok(result)
537    }
538
539    /// Creates a new [PdfPagePathObject] from the given arguments. The returned page object
540    /// will not be rendered until it is added to a `PdfPage` using the
541    /// `PdfPageObjects::add_path_object()` function.
542    ///
543    /// The new path will be created with an ellipse that fills the given rectangle, with the given
544    /// fill and stroke settings applied. Both the stroke color and the stroke width must be
545    /// provided for the ellipse to be stroked.
546    #[inline]
547    pub fn new_ellipse(
548        document: &PdfDocument<'a>,
549        rect: PdfRect,
550        stroke_color: Option<PdfColor>,
551        stroke_width: Option<PdfPoints>,
552        fill_color: Option<PdfColor>,
553    ) -> Result<Self, PdfiumError> {
554        Self::new_ellipse_from_bindings(
555            document.bindings(),
556            rect,
557            stroke_color,
558            stroke_width,
559            fill_color,
560        )
561    }
562
563    #[allow(clippy::too_many_arguments)]
564    #[inline]
565    pub(crate) fn new_ellipse_at_from_bindings(
566        bindings: &'a dyn PdfiumLibraryBindings,
567        center_x: PdfPoints,
568        center_y: PdfPoints,
569        x_radius: PdfPoints,
570        y_radius: PdfPoints,
571        stroke_color: Option<PdfColor>,
572        stroke_width: Option<PdfPoints>,
573        fill_color: Option<PdfColor>,
574    ) -> Result<Self, PdfiumError> {
575        Self::new_ellipse_from_bindings(
576            bindings,
577            PdfRect::new(
578                center_y - y_radius,
579                center_x - x_radius,
580                center_y + y_radius,
581                center_x + x_radius,
582            ),
583            stroke_color,
584            stroke_width,
585            fill_color,
586        )
587    }
588
589    /// Creates a new [PdfPagePathObject] from the given arguments. The returned page object
590    /// will not be rendered until it is added to a `PdfPage` using the
591    /// `PdfPageObjects::add_path_object()` function.
592    ///
593    /// The new path will be created with an ellipse centered at the given coordinates, with the
594    /// given horizontal and vertical radii, and with the given fill and stroke settings applied.
595    /// Both the stroke color and the stroke width must be provided for the ellipse to be stroked.
596    #[allow(clippy::too_many_arguments)]
597    #[inline]
598    pub fn new_ellipse_at(
599        document: &PdfDocument<'a>,
600        center_x: PdfPoints,
601        center_y: PdfPoints,
602        x_radius: PdfPoints,
603        y_radius: PdfPoints,
604        stroke_color: Option<PdfColor>,
605        stroke_width: Option<PdfPoints>,
606        fill_color: Option<PdfColor>,
607    ) -> Result<Self, PdfiumError> {
608        Self::new_ellipse_at_from_bindings(
609            document.bindings(),
610            center_x,
611            center_y,
612            x_radius,
613            y_radius,
614            stroke_color,
615            stroke_width,
616            fill_color,
617        )
618    }
619
620    /// Begins a new sub-path in this [PdfPagePathObject] by moving the current point to the
621    /// given coordinates, omitting any connecting line segment.
622    pub fn move_to(&mut self, x: PdfPoints, y: PdfPoints) -> Result<(), PdfiumError> {
623        if self.bindings().is_true(self.bindings().FPDFPath_MoveTo(
624            self.object_handle(),
625            x.value,
626            y.value,
627        )) {
628            self.current_point_x = x;
629            self.current_point_y = y;
630
631            Ok(())
632        } else {
633            Err(PdfiumError::PdfiumLibraryInternalError(
634                PdfiumInternalError::Unknown,
635            ))
636        }
637    }
638
639    /// Appends a straight line segment to this [PdfPagePathObject] from the current point to the
640    /// given coordinates. The new current point is set to the given coordinates.
641    pub fn line_to(&mut self, x: PdfPoints, y: PdfPoints) -> Result<(), PdfiumError> {
642        if self.bindings().is_true(self.bindings().FPDFPath_LineTo(
643            self.object_handle(),
644            x.value,
645            y.value,
646        )) {
647            self.current_point_x = x;
648            self.current_point_y = y;
649
650            Ok(())
651        } else {
652            Err(PdfiumError::PdfiumLibraryInternalError(
653                PdfiumInternalError::Unknown,
654            ))
655        }
656    }
657
658    /// Appends a cubic Bézier curve to this [PdfPagePathObject] from the current point to the
659    /// given coordinates, using the two given Bézier control points. The new current point
660    /// is set to the given coordinates.
661    pub fn bezier_to(
662        &mut self,
663        x: PdfPoints,
664        y: PdfPoints,
665        control1_x: PdfPoints,
666        control1_y: PdfPoints,
667        control2_x: PdfPoints,
668        control2_y: PdfPoints,
669    ) -> Result<(), PdfiumError> {
670        if self.bindings().is_true(self.bindings().FPDFPath_BezierTo(
671            self.object_handle(),
672            control1_x.value,
673            control1_y.value,
674            control2_x.value,
675            control2_y.value,
676            x.value,
677            y.value,
678        )) {
679            self.current_point_x = x;
680            self.current_point_y = y;
681
682            Ok(())
683        } else {
684            Err(PdfiumError::PdfiumLibraryInternalError(
685                PdfiumInternalError::Unknown,
686            ))
687        }
688    }
689
690    /// Appends a rectangle to this [PdfPagePathObject] by drawing four line segments
691    /// from the current point, ending at the given coordinates. The current sub-path will be closed.
692    /// The new current point is set to the given coordinates.
693    pub fn rect_to(&mut self, x: PdfPoints, y: PdfPoints) -> Result<(), PdfiumError> {
694        let orig_x = self.current_point_x;
695
696        let orig_y = self.current_point_y;
697
698        self.close_path()?;
699        self.line_to(orig_x, y)?;
700        self.line_to(x, y)?;
701        self.line_to(x, orig_y)?;
702        self.close_path()?;
703        self.move_to(x, y)
704    }
705
706    /// Appends an ellipse to this [PdfPagePathObject] by drawing four Bézier curves approximating
707    /// an ellipse filling a rectangle from the current point to the given coordinates.
708    /// The current sub-path will be closed. The new current point is set to the given coordinates.
709    pub fn ellipse_to(&mut self, x: PdfPoints, y: PdfPoints) -> Result<(), PdfiumError> {
710        let x_radius = (x - self.current_point_x) / 2.0;
711
712        let y_radius = (y - self.current_point_y) / 2.0;
713
714        self.close_path()?;
715        self.move_to(
716            self.current_point_x + x_radius,
717            self.current_point_y + y_radius,
718        )?;
719        self.ellipse(x_radius, y_radius)?;
720        self.move_to(x, y)
721    }
722
723    /// Appends a circle to this [PdfPagePathObject] by drawing four Bézier curves approximating
724    /// a circle filling a rectangle from the current point to the given coordinates.
725    /// The current sub-path will be closed. The new current point is set to the given coordinates.
726    ///
727    /// Note that perfect circles cannot be represented exactly using Bézier curves. However,
728    /// a very close approximation, more than sufficient to please the human eye, can be achieved
729    /// using four Bézier curves, one for each quadrant of the circle.
730    pub fn circle_to(&mut self, x: PdfPoints, y: PdfPoints) -> Result<(), PdfiumError> {
731        let radius = (x - self.current_point_x) / 2.0;
732
733        self.move_to(self.current_point_x + radius, self.current_point_y + radius)?;
734        self.ellipse(radius, radius)?;
735        self.move_to(x, y)
736    }
737
738    /// Draws an ellipse at the current point using the given horizontal and vertical radii.
739    /// The ellipse will be constructed using four Bézier curves, one for each quadrant.
740    fn ellipse(&mut self, x_radius: PdfPoints, y_radius: PdfPoints) -> Result<(), PdfiumError> {
741        // Ellipse approximation method: https://spencermortensen.com/articles/bezier-circle/
742        // Implementation based on: https://stackoverflow.com/a/2007782
743
744        const C: f32 = 0.551915;
745
746        let x_c = x_radius * C;
747
748        let y_c = y_radius * C;
749
750        let orig_x = self.current_point_x;
751
752        let orig_y = self.current_point_y;
753
754        self.move_to(orig_x - x_radius, orig_y)?;
755        self.bezier_to(
756            orig_x,
757            orig_y + y_radius,
758            orig_x - x_radius,
759            orig_y + y_c,
760            orig_x - x_c,
761            orig_y + y_radius,
762        )?;
763        self.bezier_to(
764            orig_x + x_radius,
765            orig_y,
766            orig_x + x_c,
767            orig_y + y_radius,
768            orig_x + x_radius,
769            orig_y + y_c,
770        )?;
771        self.bezier_to(
772            orig_x,
773            orig_y - y_radius,
774            orig_x + x_radius,
775            orig_y - y_c,
776            orig_x + x_c,
777            orig_y - y_radius,
778        )?;
779        self.bezier_to(
780            orig_x - x_radius,
781            orig_y,
782            orig_x - x_c,
783            orig_y - y_radius,
784            orig_x - x_radius,
785            orig_y - y_c,
786        )?;
787        self.close_path()
788    }
789
790    /// Closes the current sub-path in this [PdfPagePathObject] by appending a straight line segment
791    /// from the current point to the starting point of the sub-path.
792    pub fn close_path(&mut self) -> Result<(), PdfiumError> {
793        if self
794            .bindings
795            .is_true(self.bindings().FPDFPath_Close(self.object_handle))
796        {
797            Ok(())
798        } else {
799            Err(PdfiumError::PdfiumLibraryInternalError(
800                PdfiumInternalError::Unknown,
801            ))
802        }
803    }
804
805    /// Returns the method used to determine which sub-paths of any path in this [PdfPagePathObject]
806    /// should be filled.
807    pub fn fill_mode(&self) -> Result<PdfPathFillMode, PdfiumError> {
808        let mut raw_fill_mode: c_int = 0;
809
810        let mut _raw_stroke: FPDF_BOOL = self.bindings().FALSE();
811
812        if self.bindings().is_true(self.bindings.FPDFPath_GetDrawMode(
813            self.object_handle(),
814            &mut raw_fill_mode,
815            &mut _raw_stroke,
816        )) {
817            PdfPathFillMode::from_pdfium(raw_fill_mode)
818        } else {
819            Err(PdfiumError::PdfiumLibraryInternalError(
820                PdfiumInternalError::Unknown,
821            ))
822        }
823    }
824
825    /// Returns `true` if this [PdfPagePathObject] will be stroked, regardless of the path's
826    /// stroke settings.
827    ///
828    /// Even if this path is set to be stroked, the stroke must be configured with a visible color
829    /// and a non-zero width in order to actually be visible.
830    pub fn is_stroked(&self) -> Result<bool, PdfiumError> {
831        let mut _raw_fill_mode: c_int = 0;
832
833        let mut raw_stroke: FPDF_BOOL = self.bindings().FALSE();
834
835        if self
836            .bindings()
837            .is_true(self.bindings().FPDFPath_GetDrawMode(
838                self.object_handle(),
839                &mut _raw_fill_mode,
840                &mut raw_stroke,
841            ))
842        {
843            Ok(self.bindings().is_true(raw_stroke))
844        } else {
845            Err(PdfiumError::PdfiumLibraryInternalError(
846                PdfiumInternalError::Unknown,
847            ))
848        }
849    }
850
851    /// Sets the method used to determine which sub-paths of any path in this [PdfPagePathObject]
852    /// should be filled, and whether or not any path in this [PdfPagePathObject] should be stroked.
853    ///
854    /// Even if this object's path is set to be stroked, the stroke must be configured with
855    /// a visible color and a non-zero width in order to actually be visible.
856    pub fn set_fill_and_stroke_mode(
857        &mut self,
858        fill_mode: PdfPathFillMode,
859        do_stroke: bool,
860    ) -> Result<(), PdfiumError> {
861        if self
862            .bindings()
863            .is_true(self.bindings().FPDFPath_SetDrawMode(
864                self.object_handle(),
865                fill_mode.as_pdfium() as c_int,
866                self.bindings.bool_to_pdfium(do_stroke),
867            ))
868        {
869            Ok(())
870        } else {
871            Err(PdfiumError::PdfiumLibraryInternalError(
872                PdfiumInternalError::Unknown,
873            ))
874        }
875    }
876
877    /// Returns the collection of path segments currently defined by this [PdfPagePathObject].
878    #[inline]
879    pub fn segments(&self) -> PdfPagePathObjectSegments {
880        PdfPagePathObjectSegments::from_pdfium(self.object_handle(), self.bindings())
881    }
882
883    create_transform_setters!(
884        &mut Self,
885        Result<(), PdfiumError>,
886        "this [PdfPagePathObject]",
887        "this [PdfPagePathObject].",
888        "this [PdfPagePathObject],"
889    );
890
891    // The transform_impl() function required by the create_transform_setters!() macro
892    // is provided by the PdfPageObjectPrivate trait.
893
894    create_transform_getters!(
895        "this [PdfPagePathObject]",
896        "this [PdfPagePathObject].",
897        "this [PdfPagePathObject],"
898    );
899
900    // The get_matrix_impl() function required by the create_transform_getters!() macro
901    // is provided by the PdfPageObjectPrivate trait.
902}
903
904impl<'a> PdfPageObjectPrivate<'a> for PdfPagePathObject<'a> {
905    #[inline]
906    fn object_handle(&self) -> FPDF_PAGEOBJECT {
907        self.object_handle
908    }
909
910    #[inline]
911    fn ownership(&self) -> &PdfPageObjectOwnership {
912        &self.ownership
913    }
914
915    #[inline]
916    fn set_ownership(&mut self, ownership: PdfPageObjectOwnership) {
917        self.ownership = ownership;
918    }
919
920    #[inline]
921    fn bindings(&self) -> &dyn PdfiumLibraryBindings {
922        self.bindings
923    }
924
925    #[inline]
926    fn is_copyable_impl(&self) -> bool {
927        // The path object can only be copied if it contains no Bézier path segments.
928        // Pdfium does not currently provide any way to retrieve the Bézier control points
929        // of an existing Bézier path segment.
930
931        !self
932            .segments()
933            .iter()
934            .any(|segment| segment.segment_type() == PdfPathSegmentType::BezierTo)
935    }
936
937    fn try_copy_impl<'b>(
938        &self,
939        _: FPDF_DOCUMENT,
940        bindings: &'b dyn PdfiumLibraryBindings,
941    ) -> Result<PdfPageObject<'b>, PdfiumError> {
942        let mut copy = PdfPagePathObject::new_from_bindings(
943            bindings,
944            PdfPoints::ZERO,
945            PdfPoints::ZERO,
946            None,
947            None,
948            None,
949        )?;
950
951        copy.set_fill_and_stroke_mode(self.fill_mode()?, self.is_stroked()?)?;
952        copy.set_fill_color(self.fill_color()?)?;
953        copy.set_stroke_color(self.stroke_color()?)?;
954        copy.set_stroke_width(self.stroke_width()?)?;
955        copy.set_line_join(self.line_join()?)?;
956        copy.set_line_cap(self.line_cap()?)?;
957
958        for segment in self.segments().iter() {
959            if segment.segment_type() == PdfPathSegmentType::Unknown {
960                return Err(PdfiumError::PathObjectUnknownSegmentTypeNotCopyable);
961            } else if segment.segment_type() == PdfPathSegmentType::BezierTo {
962                return Err(PdfiumError::PathObjectBezierControlPointsNotCopyable);
963            } else {
964                match segment.segment_type() {
965                    PdfPathSegmentType::Unknown | PdfPathSegmentType::BezierTo => {}
966                    PdfPathSegmentType::LineTo => copy.line_to(segment.x(), segment.y())?,
967                    PdfPathSegmentType::MoveTo => copy.move_to(segment.x(), segment.y())?,
968                }
969
970                if segment.is_close() {
971                    copy.close_path()?;
972                }
973            }
974        }
975
976        copy.reset_matrix(self.matrix()?)?;
977
978        Ok(PdfPageObject::Path(copy))
979    }
980}
981
982/// The collection of [PdfPathSegment] objects inside a path page object.
983///
984/// The coordinates of each segment in the returned iterator will be the untransformed,
985/// raw values supplied at the time the segment was created. Use the
986/// [PdfPagePathObjectSegments::transform()] function to apply a [PdfMatrix] transformation matrix
987/// to the coordinates of each segment as it is returned.
988pub struct PdfPagePathObjectSegments<'a> {
989    handle: FPDF_PAGEOBJECT,
990    matrix: Option<PdfMatrix>,
991    bindings: &'a dyn PdfiumLibraryBindings,
992}
993
994impl<'a> PdfPagePathObjectSegments<'a> {
995    #[inline]
996    pub(crate) fn from_pdfium(
997        handle: FPDF_PAGEOBJECT,
998        bindings: &'a dyn PdfiumLibraryBindings,
999    ) -> Self {
1000        Self {
1001            handle,
1002            matrix: None,
1003            bindings,
1004        }
1005    }
1006
1007    /// Returns a new iterator over this collection of [PdfPathSegment] objects that applies
1008    /// the given [PdfMatrix] to the points in each returned segment.
1009    #[inline]
1010    pub fn transform(&self, matrix: PdfMatrix) -> PdfPagePathObjectSegments<'a> {
1011        Self {
1012            handle: self.handle,
1013            matrix: Some(matrix),
1014            bindings: self.bindings,
1015        }
1016    }
1017
1018    /// Returns a new iterator over this collection of [PdfPathSegment] objects that ensures
1019    /// the points of each returned segment are untransformed raw values.
1020    #[inline]
1021    pub fn raw(&self) -> PdfPagePathObjectSegments<'a> {
1022        Self {
1023            handle: self.handle,
1024            matrix: None,
1025            bindings: self.bindings,
1026        }
1027    }
1028}
1029
1030impl<'a> PdfPathSegments<'a> for PdfPagePathObjectSegments<'a> {
1031    #[inline]
1032    fn bindings(&self) -> &'a dyn PdfiumLibraryBindings {
1033        self.bindings
1034    }
1035
1036    #[inline]
1037    fn len(&self) -> PdfPathSegmentIndex {
1038        self.bindings()
1039            .FPDFPath_CountSegments(self.handle)
1040            .try_into()
1041            .unwrap_or(0)
1042    }
1043
1044    fn get(&self, index: PdfPathSegmentIndex) -> Result<PdfPathSegment<'a>, PdfiumError> {
1045        let handle = self
1046            .bindings()
1047            .FPDFPath_GetPathSegment(self.handle, index as c_int);
1048
1049        if handle.is_null() {
1050            Err(PdfiumError::PdfiumLibraryInternalError(
1051                PdfiumInternalError::Unknown,
1052            ))
1053        } else {
1054            Ok(PdfPathSegment::from_pdfium(
1055                handle,
1056                self.matrix,
1057                self.bindings(),
1058            ))
1059        }
1060    }
1061
1062    #[inline]
1063    fn iter(&'a self) -> PdfPathSegmentsIterator<'a> {
1064        PdfPathSegmentsIterator::new(self)
1065    }
1066}