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