Skip to main content

rpdfium_edit/
page_object.rs

1// Derived from PDFium's fpdfsdk/fpdf_edit_impl.cpp / core/fpdfapi/page
2// Original: Copyright 2014 The PDFium Authors
3// Licensed under BSD-3-Clause / Apache-2.0
4// See pdfium-upstream/LICENSE for the original license.
5
6//! Page objects — typed wrappers for path, text, image, and form XObject
7//! content that can be placed on a page.
8
9use rpdfium_core::{Matrix, Name};
10use rpdfium_font::FontEncoding;
11use rpdfium_graphics::{
12    Bitmap, BitmapFormat, BlendMode, Color, DashPattern, PathOp, TextRenderingMode,
13};
14use rpdfium_parser::object::ObjectId;
15
16// ---------------------------------------------------------------------------
17// Content Marks API  (FPDFPageObj*Mark* functions — tagged PDF / accessibility)
18// ---------------------------------------------------------------------------
19
20/// The value of a content mark parameter.
21///
22/// Corresponds to the three parameter types in the PDF spec for marked content
23/// (`BMC`/`BDC`/`EMC` operators): integer, string, raw blob, and inline
24/// dictionary.
25#[derive(Debug, Clone, PartialEq)]
26pub enum MarkParamValue {
27    /// An integer parameter value.
28    ///
29    /// Corresponds to `FPDFPageObjMark_GetParamValueType` returning
30    /// `FPDF_OBJECT_NUMBER` + `FPDFPageObjMark_GetParamIntValue`.
31    Integer(i64),
32    /// A float parameter value.
33    ///
34    /// Corresponds to `FPDFPageObjMark_GetParamValueType` returning
35    /// `FPDF_OBJECT_NUMBER` (real) + `FPDFPageObjMark_GetParamFloatValue`.
36    Float(f32),
37    /// A UTF-8 string parameter value.
38    ///
39    /// Corresponds to `FPDFPageObjMark_GetParamValueType` returning
40    /// `FPDF_OBJECT_STRING` + `FPDFPageObjMark_GetParamStringValue`.
41    String(String),
42    /// A raw byte blob parameter value.
43    ///
44    /// Corresponds to `FPDFPageObjMark_GetParamValueType` returning
45    /// `FPDF_OBJECT_STRING` (binary) + `FPDFPageObjMark_GetParamBlobValue`.
46    Blob(Vec<u8>),
47    /// An inline dictionary parameter value.
48    ///
49    /// Serialized as `<< /Key Value ... >>` inline in the content stream.
50    /// Entries are stored in insertion order (`Vec<(key, value)>`).
51    ///
52    /// This is used when a BDC mark carries a full parameter dictionary rather
53    /// than a single typed value — for example, PDF/UA tagged content where the
54    /// mark properties are specified inline rather than referencing a properties
55    /// resource.
56    Dictionary(Vec<(String, MarkParamValue)>),
57}
58
59/// A content mark attached to a page object (for tagged PDF / accessibility).
60///
61/// Content marks correspond to the BMC/BDC/EMC operators in PDF content
62/// streams. Each mark has a name (the tag name, e.g. `"Span"`, `"P"`, `"Figure"`)
63/// and an optional parameter dictionary (key → value pairs).
64///
65/// The most common parameter is `"MCID"` (Marked Content Identifier), an
66/// integer that links the page object into the logical structure tree.
67///
68/// # Upstream Correspondence
69///
70/// - `FPDFPageObj_CountMarks` / `FPDFPageObj_GetMark` / `FPDFPageObj_AddMark` /
71///   `FPDFPageObj_RemoveMark`
72/// - `FPDFPageObjMark_GetName` / `FPDFPageObjMark_CountParams` /
73///   `FPDFPageObjMark_GetParamKey` / `FPDFPageObjMark_GetParamValueType`
74/// - `FPDFPageObjMark_GetIntParam` / `FPDFPageObjMark_GetStringParam` /
75///   `FPDFPageObjMark_GetBlobParam`
76/// - `FPDFPageObjMark_SetIntParam` / `FPDFPageObjMark_SetStringParam` /
77///   `FPDFPageObjMark_SetBlobParam` / `FPDFPageObjMark_RemoveParam`
78#[derive(Debug, Clone, PartialEq)]
79pub struct ContentMark {
80    /// The tag name (e.g. `"Span"`, `"P"`, `"Figure"`).
81    pub name: String,
82    /// Key–value parameters in insertion order.
83    pub params: Vec<(String, MarkParamValue)>,
84}
85
86impl ContentMark {
87    /// Create a new content mark with the given tag name and no parameters.
88    ///
89    /// Corresponds to `FPDFPageObj_AddMark`.
90    pub fn new(name: impl Into<String>) -> Self {
91        Self {
92            name: name.into(),
93            params: Vec::new(),
94        }
95    }
96
97    /// Returns the tag name of this content mark.
98    ///
99    /// Corresponds to `FPDFPageObjMark_GetName`.
100    pub fn name(&self) -> &str {
101        &self.name
102    }
103
104    /// Upstream-aligned alias for [`name()`](Self::name).
105    ///
106    /// Corresponds to `FPDFPageObjMark_GetName`.
107    #[inline]
108    pub fn page_obj_mark_get_name(&self) -> &str {
109        self.name()
110    }
111
112    /// Non-upstream alias — use [`page_obj_mark_get_name()`](Self::page_obj_mark_get_name).
113    #[deprecated(
114        note = "use `page_obj_mark_get_name()` — matches upstream `FPDFPageObjMark_GetName`"
115    )]
116    #[inline]
117    pub fn get_name(&self) -> &str {
118        self.name()
119    }
120
121    /// Return the number of parameters on this mark.
122    ///
123    /// Corresponds to `FPDFPageObjMark_CountParams`.
124    pub fn param_count(&self) -> usize {
125        self.params.len()
126    }
127
128    /// Upstream-aligned alias for [`param_count()`](Self::param_count).
129    ///
130    /// Corresponds to `FPDFPageObjMark_CountParams`.
131    #[inline]
132    pub fn page_obj_mark_count_params(&self) -> usize {
133        self.param_count()
134    }
135
136    /// Non-upstream alias — use [`page_obj_mark_count_params()`](Self::page_obj_mark_count_params).
137    #[deprecated(
138        note = "use `page_obj_mark_count_params()` — matches upstream `FPDFPageObjMark_CountParams`"
139    )]
140    #[inline]
141    pub fn count_params(&self) -> usize {
142        self.param_count()
143    }
144
145    /// Return the key at the given parameter index, or `None` if out of range.
146    ///
147    /// Corresponds to `FPDFPageObjMark_GetParamKey`.
148    pub fn param_key(&self, index: usize) -> Option<&str> {
149        self.params.get(index).map(|(k, _)| k.as_str())
150    }
151
152    /// Upstream-aligned alias for [`param_key()`](Self::param_key).
153    ///
154    /// Corresponds to `FPDFPageObjMark_GetParamKey`.
155    #[inline]
156    pub fn page_obj_mark_get_param_key(&self, index: usize) -> Option<&str> {
157        self.param_key(index)
158    }
159
160    /// Non-upstream alias — use [`page_obj_mark_get_param_key()`](Self::page_obj_mark_get_param_key).
161    #[deprecated(
162        note = "use `page_obj_mark_get_param_key()` — matches upstream `FPDFPageObjMark_GetParamKey`"
163    )]
164    #[inline]
165    pub fn get_param_key(&self, index: usize) -> Option<&str> {
166        self.param_key(index)
167    }
168
169    /// Return the integer value of the parameter with the given key, or `None`
170    /// if the key is absent or has a non-integer value.
171    ///
172    /// Corresponds to `FPDFPageObjMark_GetParamIntValue`.
173    pub fn int_param(&self, key: &str) -> Option<i64> {
174        self.params.iter().find_map(|(k, v)| {
175            if k == key {
176                if let MarkParamValue::Integer(n) = v {
177                    Some(*n)
178                } else {
179                    None
180                }
181            } else {
182                None
183            }
184        })
185    }
186
187    /// Upstream-aligned alias for [`int_param()`](Self::int_param).
188    ///
189    /// Corresponds to `FPDFPageObjMark_GetParamIntValue`.
190    #[inline]
191    pub fn page_obj_mark_get_param_int_value(&self, key: &str) -> Option<i64> {
192        self.int_param(key)
193    }
194
195    #[deprecated(
196        note = "use `page_obj_mark_get_param_int_value()` — matches upstream `FPDFPageObjMark_GetParamIntValue`"
197    )]
198    #[inline]
199    pub fn get_param_int_value(&self, key: &str) -> Option<i64> {
200        self.int_param(key)
201    }
202
203    /// Non-upstream alias — use [`page_obj_mark_get_param_int_value()`](Self::page_obj_mark_get_param_int_value).
204    #[deprecated(
205        note = "use `page_obj_mark_get_param_int_value()` — matches upstream `FPDFPageObjMark_GetParamIntValue`"
206    )]
207    #[inline]
208    pub fn get_int_param(&self, key: &str) -> Option<i64> {
209        self.int_param(key)
210    }
211
212    /// Return the string value of the parameter with the given key, or `None`
213    /// if the key is absent or has a non-string value.
214    ///
215    /// Corresponds to `FPDFPageObjMark_GetParamStringValue`.
216    pub fn string_param(&self, key: &str) -> Option<&str> {
217        self.params.iter().find_map(|(k, v)| {
218            if k == key {
219                if let MarkParamValue::String(s) = v {
220                    Some(s.as_str())
221                } else {
222                    None
223                }
224            } else {
225                None
226            }
227        })
228    }
229
230    /// Upstream-aligned alias for [`string_param()`](Self::string_param).
231    ///
232    /// Corresponds to `FPDFPageObjMark_GetParamStringValue`.
233    #[inline]
234    pub fn page_obj_mark_get_param_string_value(&self, key: &str) -> Option<&str> {
235        self.string_param(key)
236    }
237
238    #[deprecated(
239        note = "use `page_obj_mark_get_param_string_value()` — matches upstream `FPDFPageObjMark_GetParamStringValue`"
240    )]
241    #[inline]
242    pub fn get_param_string_value(&self, key: &str) -> Option<&str> {
243        self.string_param(key)
244    }
245
246    /// Non-upstream alias — use [`page_obj_mark_get_param_string_value()`](Self::page_obj_mark_get_param_string_value).
247    #[deprecated(
248        note = "use `page_obj_mark_get_param_string_value()` — matches upstream `FPDFPageObjMark_GetParamStringValue`"
249    )]
250    #[inline]
251    pub fn get_string_param(&self, key: &str) -> Option<&str> {
252        self.string_param(key)
253    }
254
255    /// Return the blob value of the parameter with the given key, or `None`
256    /// if the key is absent or has a non-blob value.
257    ///
258    /// Corresponds to `FPDFPageObjMark_GetParamBlobValue`.
259    pub fn blob_param(&self, key: &str) -> Option<&[u8]> {
260        self.params.iter().find_map(|(k, v)| {
261            if k == key {
262                if let MarkParamValue::Blob(b) = v {
263                    Some(b.as_slice())
264                } else {
265                    None
266                }
267            } else {
268                None
269            }
270        })
271    }
272
273    /// Upstream-aligned alias for [`blob_param()`](Self::blob_param).
274    ///
275    /// Corresponds to `FPDFPageObjMark_GetParamBlobValue`.
276    #[inline]
277    pub fn page_obj_mark_get_param_blob_value(&self, key: &str) -> Option<&[u8]> {
278        self.blob_param(key)
279    }
280
281    #[deprecated(
282        note = "use `page_obj_mark_get_param_blob_value()` — matches upstream `FPDFPageObjMark_GetParamBlobValue`"
283    )]
284    #[inline]
285    pub fn get_param_blob_value(&self, key: &str) -> Option<&[u8]> {
286        self.blob_param(key)
287    }
288
289    /// Non-upstream alias — use [`page_obj_mark_get_param_blob_value()`](Self::page_obj_mark_get_param_blob_value).
290    #[deprecated(
291        note = "use `page_obj_mark_get_param_blob_value()` — matches upstream `FPDFPageObjMark_GetParamBlobValue`"
292    )]
293    #[inline]
294    pub fn get_blob_param(&self, key: &str) -> Option<&[u8]> {
295        self.blob_param(key)
296    }
297
298    /// Set (or replace) an integer parameter.
299    ///
300    /// If a parameter with the same key already exists, its value is replaced;
301    /// otherwise a new entry is appended.
302    ///
303    /// Corresponds to `FPDFPageObjMark_SetIntParam`.
304    pub fn set_int_param(&mut self, key: impl Into<String>, value: i64) {
305        let key = key.into();
306        for (k, v) in &mut self.params {
307            if *k == key {
308                *v = MarkParamValue::Integer(value);
309                return;
310            }
311        }
312        self.params.push((key, MarkParamValue::Integer(value)));
313    }
314
315    /// Upstream-aligned alias for [`set_int_param()`](Self::set_int_param).
316    ///
317    /// Corresponds to `FPDFPageObjMark_SetIntParam`.
318    #[inline]
319    pub fn page_obj_mark_set_int_param(&mut self, key: impl Into<String>, value: i64) {
320        self.set_int_param(key, value)
321    }
322
323    /// Set (or replace) a string parameter.
324    ///
325    /// If a parameter with the same key already exists, its value is replaced;
326    /// otherwise a new entry is appended.
327    ///
328    /// Corresponds to `FPDFPageObjMark_SetStringParam`.
329    pub fn set_string_param(&mut self, key: impl Into<String>, value: impl Into<String>) {
330        let key = key.into();
331        let value = value.into();
332        for (k, v) in &mut self.params {
333            if *k == key {
334                *v = MarkParamValue::String(value);
335                return;
336            }
337        }
338        self.params.push((key, MarkParamValue::String(value)));
339    }
340
341    /// Upstream-aligned alias for [`set_string_param()`](Self::set_string_param).
342    ///
343    /// Corresponds to `FPDFPageObjMark_SetStringParam`.
344    #[inline]
345    pub fn page_obj_mark_set_string_param(
346        &mut self,
347        key: impl Into<String>,
348        value: impl Into<String>,
349    ) {
350        self.set_string_param(key, value)
351    }
352
353    /// Return the float value of the parameter with the given key, or `None`
354    /// if the key is absent or has a non-float value.
355    ///
356    /// Note: In this implementation, floats are stored as a sub-case of the
357    /// `Integer` variant using the bit pattern. If a float was stored via
358    /// [`set_float_param`](Self::set_float_param), it can be retrieved here.
359    ///
360    /// Corresponds to `FPDFPageObjMark_GetParamFloatValue`.
361    pub fn float_param(&self, key: &str) -> Option<f32> {
362        self.params.iter().find_map(|(k, v)| {
363            if k == key {
364                if let MarkParamValue::Float(f) = v {
365                    Some(*f)
366                } else {
367                    None
368                }
369            } else {
370                None
371            }
372        })
373    }
374
375    /// Upstream-aligned alias for [`float_param()`](Self::float_param).
376    ///
377    /// Corresponds to `FPDFPageObjMark_GetParamFloatValue`.
378    #[inline]
379    pub fn page_obj_mark_get_param_float_value(&self, key: &str) -> Option<f32> {
380        self.float_param(key)
381    }
382
383    #[deprecated(
384        note = "use `page_obj_mark_get_param_float_value()` — matches upstream `FPDFPageObjMark_GetParamFloatValue`"
385    )]
386    #[inline]
387    pub fn get_param_float_value(&self, key: &str) -> Option<f32> {
388        self.float_param(key)
389    }
390
391    /// Set (or replace) a float parameter.
392    ///
393    /// If a parameter with the same key already exists, its value is replaced;
394    /// otherwise a new entry is appended.
395    ///
396    /// Corresponds to `FPDFPageObjMark_SetFloatParam`.
397    pub fn set_float_param(&mut self, key: impl Into<String>, value: f32) {
398        let key = key.into();
399        for (k, v) in &mut self.params {
400            if *k == key {
401                *v = MarkParamValue::Float(value);
402                return;
403            }
404        }
405        self.params.push((key, MarkParamValue::Float(value)));
406    }
407
408    /// Upstream-aligned alias for [`set_float_param()`](Self::set_float_param).
409    ///
410    /// Corresponds to `FPDFPageObjMark_SetFloatParam`.
411    #[inline]
412    pub fn page_obj_mark_set_float_param(&mut self, key: impl Into<String>, value: f32) {
413        self.set_float_param(key, value)
414    }
415
416    /// Set (or replace) a blob parameter.
417    ///
418    /// If a parameter with the same key already exists, its value is replaced;
419    /// otherwise a new entry is appended.
420    ///
421    /// Corresponds to `FPDFPageObjMark_SetBlobParam`.
422    pub fn set_blob_param(&mut self, key: impl Into<String>, value: Vec<u8>) {
423        let key = key.into();
424        for (k, v) in &mut self.params {
425            if *k == key {
426                *v = MarkParamValue::Blob(value);
427                return;
428            }
429        }
430        self.params.push((key, MarkParamValue::Blob(value)));
431    }
432
433    /// Upstream-aligned alias for [`set_blob_param()`](Self::set_blob_param).
434    ///
435    /// Corresponds to `FPDFPageObjMark_SetBlobParam`.
436    #[inline]
437    pub fn page_obj_mark_set_blob_param(&mut self, key: impl Into<String>, value: Vec<u8>) {
438        self.set_blob_param(key, value)
439    }
440
441    /// Remove a parameter by key.
442    ///
443    /// Returns `true` if the parameter was found and removed, `false` if not
444    /// found.
445    ///
446    /// Corresponds to `FPDFPageObjMark_RemoveParam`.
447    pub fn remove_param(&mut self, key: &str) -> bool {
448        let before = self.params.len();
449        self.params.retain(|(k, _)| k != key);
450        self.params.len() < before
451    }
452
453    /// Upstream-aligned alias for [`remove_param()`](Self::remove_param).
454    ///
455    /// Corresponds to `FPDFPageObjMark_RemoveParam`.
456    #[inline]
457    pub fn page_obj_mark_remove_param(&mut self, key: &str) -> bool {
458        self.remove_param(key)
459    }
460
461    /// Return the `/MCID` integer value from this mark, if present.
462    ///
463    /// This is a convenience wrapper around `int_param("MCID")`.
464    ///
465    /// Corresponds to `FPDFPageObj_GetMarkedContentID` (for the specific mark
466    /// that carries the MCID).
467    pub fn marked_content_id(&self) -> Option<i64> {
468        self.int_param("MCID")
469    }
470
471    /// Returns the PDF object type code of the parameter with the given key.
472    ///
473    /// Return values follow PDFium's `FPDF_OBJECT_*` constants:
474    /// - `2` (`FPDF_OBJECT_NUMBER`) for integer or float parameters
475    /// - `3` (`FPDF_OBJECT_STRING`) for string or blob parameters
476    /// - `7` (`FPDF_OBJECT_DICTIONARY`) for dictionary parameters
477    /// - `0` (`FPDF_OBJECT_UNKNOWN`) if the key is not found
478    ///
479    /// Corresponds to `FPDFPageObjMark_GetParamValueType`.
480    pub fn param_value_type(&self, key: &str) -> u32 {
481        self.params
482            .iter()
483            .find_map(|(k, v)| {
484                if k == key {
485                    Some(match v {
486                        MarkParamValue::Integer(_) | MarkParamValue::Float(_) => 2,
487                        MarkParamValue::String(_) | MarkParamValue::Blob(_) => 3,
488                        MarkParamValue::Dictionary(_) => 7,
489                    })
490                } else {
491                    None
492                }
493            })
494            .unwrap_or(0)
495    }
496
497    /// Upstream-aligned alias for [`param_value_type()`](Self::param_value_type).
498    ///
499    /// Corresponds to `FPDFPageObjMark_GetParamValueType`.
500    #[inline]
501    pub fn page_obj_mark_get_param_value_type(&self, key: &str) -> u32 {
502        self.param_value_type(key)
503    }
504
505    /// Non-upstream alias — use [`page_obj_mark_get_param_value_type()`](Self::page_obj_mark_get_param_value_type).
506    #[deprecated(
507        note = "use `page_obj_mark_get_param_value_type()` — matches upstream `FPDFPageObjMark_GetParamValueType`"
508    )]
509    #[inline]
510    pub fn get_param_value_type(&self, key: &str) -> u32 {
511        self.param_value_type(key)
512    }
513}
514
515// ---------------------------------------------------------------------------
516// Clip Path API  (fpdf_transformpage.h — FPDF_CreateClipPath / FPDFClipPath_*)
517// ---------------------------------------------------------------------------
518
519/// A clipping path that can be attached to page objects or inserted into a page.
520///
521/// A clip path consists of one or more sub-paths. Each sub-path is a sequence
522/// of [`PathSegment`]s. When applied, the intersection of all sub-paths defines
523/// the clipping region.
524///
525/// # Upstream Correspondence
526///
527/// - `FPDF_CreateClipPath` — [`ClipPath::from_rect`]
528/// - `FPDF_DestroyClipPath` — Rust: just drop
529/// - `FPDFClipPath_CountPaths` — [`ClipPath::path_count`]
530/// - `FPDFClipPath_CountPathSegments` — [`ClipPath::segment_count`]
531/// - `FPDFClipPath_GetPathSegment` — [`ClipPath::segment`]
532/// - `FPDFPageObj_GetClipPath` — [`PageObject::clip_path`]
533/// - `FPDFPageObj_TransformClipPath` — [`PageObject::transform_clip_path`]
534/// - `FPDFPage_InsertClipPath` — [`crate::document::EditDocument::insert_clip_path`]
535#[derive(Debug, Clone)]
536pub struct ClipPath {
537    /// Sub-paths that define the clipping region.
538    pub paths: Vec<Vec<PathSegment>>,
539}
540
541impl ClipPath {
542    /// Create a rectangular clip path from left, bottom, right, top coordinates.
543    ///
544    /// Corresponds to `FPDF_CreateClipPath(left, bottom, right, top)`.
545    pub fn from_rect(left: f64, bottom: f64, right: f64, top: f64) -> Self {
546        let rect_path = vec![PathSegment::Rect(left, bottom, right - left, top - bottom)];
547        Self {
548            paths: vec![rect_path],
549        }
550    }
551
552    /// Upstream-aligned alias for [`from_rect()`](Self::from_rect).
553    #[inline]
554    pub fn create_clip_path(left: f64, bottom: f64, right: f64, top: f64) -> Self {
555        Self::from_rect(left, bottom, right, top)
556    }
557
558    /// Create a clip path from a list of sub-paths.
559    pub fn new(paths: Vec<Vec<PathSegment>>) -> Self {
560        Self { paths }
561    }
562
563    /// Returns the number of sub-paths in the clip path.
564    ///
565    /// Corresponds to `FPDFClipPath_CountPaths`.
566    pub fn path_count(&self) -> usize {
567        self.paths.len()
568    }
569
570    /// Upstream-aligned alias for [`path_count()`](Self::path_count).
571    ///
572    /// Corresponds to `FPDFClipPath_CountPaths`.
573    #[inline]
574    pub fn clip_path_count_paths(&self) -> usize {
575        self.path_count()
576    }
577
578    /// Non-upstream alias — use [`clip_path_count_paths()`](Self::clip_path_count_paths).
579    #[deprecated(
580        note = "use `clip_path_count_paths()` — matches upstream `FPDFClipPath_CountPaths`"
581    )]
582    #[inline]
583    pub fn count_paths(&self) -> usize {
584        self.path_count()
585    }
586
587    /// Returns the number of segments in the sub-path at `path_index`.
588    ///
589    /// Returns `0` if `path_index` is out of range.
590    ///
591    /// Corresponds to `FPDFClipPath_CountPathSegments`.
592    pub fn segment_count(&self, path_index: usize) -> usize {
593        self.paths.get(path_index).map(|p| p.len()).unwrap_or(0)
594    }
595
596    /// Upstream-aligned alias for [`segment_count()`](Self::segment_count).
597    ///
598    /// Corresponds to `FPDFClipPath_CountPathSegments`.
599    #[inline]
600    pub fn clip_path_count_path_segments(&self, path_index: usize) -> usize {
601        self.segment_count(path_index)
602    }
603
604    /// Non-upstream alias — use [`clip_path_count_path_segments()`](Self::clip_path_count_path_segments).
605    #[deprecated(
606        note = "use `clip_path_count_path_segments()` — matches upstream `FPDFClipPath_CountPathSegments`"
607    )]
608    #[inline]
609    pub fn count_path_segments(&self, path_index: usize) -> usize {
610        self.segment_count(path_index)
611    }
612
613    /// Returns the segment at `(path_index, seg_index)`, or `None` if out of range.
614    ///
615    /// Corresponds to `FPDFClipPath_GetPathSegment`.
616    pub fn segment(&self, path_index: usize, seg_index: usize) -> Option<&PathSegment> {
617        self.paths.get(path_index)?.get(seg_index)
618    }
619
620    /// Upstream-aligned alias for [`segment()`](Self::segment).
621    ///
622    /// Corresponds to `FPDFClipPath_GetPathSegment`.
623    #[inline]
624    pub fn clip_path_get_path_segment(
625        &self,
626        path_index: usize,
627        seg_index: usize,
628    ) -> Option<&PathSegment> {
629        self.segment(path_index, seg_index)
630    }
631
632    /// Non-upstream alias — use [`clip_path_get_path_segment()`](Self::clip_path_get_path_segment).
633    #[deprecated(
634        note = "use `clip_path_get_path_segment()` — matches upstream `FPDFClipPath_GetPathSegment`"
635    )]
636    #[inline]
637    pub fn get_path_segment(&self, path_index: usize, seg_index: usize) -> Option<&PathSegment> {
638        self.segment(path_index, seg_index)
639    }
640
641    /// Applies a matrix transform to all segments in all sub-paths.
642    ///
643    /// Corresponds to `FPDFPageObj_TransformClipPath` (transform portion).
644    pub fn transform(&mut self, matrix: &Matrix) {
645        use rpdfium_core::Point;
646        for sub_path in &mut self.paths {
647            for seg in sub_path {
648                match seg {
649                    PathSegment::MoveTo(x, y) | PathSegment::LineTo(x, y) => {
650                        let p = matrix.transform_point(Point::new(*x, *y));
651                        *x = p.x;
652                        *y = p.y;
653                    }
654                    PathSegment::CurveTo(x1, y1, x2, y2, x3, y3) => {
655                        let p1 = matrix.transform_point(Point::new(*x1, *y1));
656                        let p2 = matrix.transform_point(Point::new(*x2, *y2));
657                        let p3 = matrix.transform_point(Point::new(*x3, *y3));
658                        *x1 = p1.x;
659                        *y1 = p1.y;
660                        *x2 = p2.x;
661                        *y2 = p2.y;
662                        *x3 = p3.x;
663                        *y3 = p3.y;
664                    }
665                    PathSegment::Rect(x, y, w, h) => {
666                        let p = matrix.transform_point(Point::new(*x, *y));
667                        let scale_x = (matrix.a * matrix.a + matrix.b * matrix.b).sqrt();
668                        let scale_y = (matrix.c * matrix.c + matrix.d * matrix.d).sqrt();
669                        *x = p.x;
670                        *y = p.y;
671                        *w *= scale_x;
672                        *h *= scale_y;
673                    }
674                    PathSegment::Close => {}
675                }
676            }
677        }
678    }
679}
680
681/// A page object that can be placed on a page.
682#[derive(Debug, Clone)]
683pub enum PageObject {
684    /// A path/shape object.
685    Path(PathObject),
686    /// A text object.
687    Text(TextObject),
688    /// An image XObject.
689    Image(ImageObject),
690    /// A form XObject reference.
691    Form(FormObject),
692}
693
694impl PageObject {
695    /// Explicitly destroy (free) a page object that has not been inserted into a page.
696    ///
697    /// # Not Supported
698    ///
699    /// Explicit page object lifecycle management is not supported in rpdfium.
700    /// Page objects are owned by Rust and released automatically when dropped
701    /// (RAII). Calling this method is unnecessary and returns an error.
702    ///
703    /// Corresponds to `FPDFPageObj_Destroy()`.
704    pub fn page_obj_destroy(self) -> crate::error::EditResult<()> {
705        // self is consumed (and thus dropped) by this method.
706        // We return NotSupported to signal that explicit destruction is unnecessary
707        // in rpdfium — Rust's RAII handles it automatically.
708        Err(crate::error::EditError::NotSupported(
709            "page_obj_destroy: object lifecycle is managed by RAII (ADR-002: Rust ownership model)"
710                .into(),
711        ))
712    }
713
714    /// Returns whether this page object is active (visible in generated content).
715    ///
716    /// Inactive objects are skipped by `generate_content_stream`.
717    ///
718    /// Corresponds to `FPDFPageObj_GetIsActive`.
719    pub fn is_active(&self) -> bool {
720        match self {
721            PageObject::Path(p) => p.active,
722            PageObject::Text(t) => t.active,
723            PageObject::Image(img) => img.active,
724            PageObject::Form(f) => f.active,
725        }
726    }
727
728    /// Upstream-aligned alias for [`is_active()`](Self::is_active).
729    ///
730    /// Corresponds to `FPDFPageObj_GetIsActive`.
731    #[inline]
732    pub fn page_obj_get_is_active(&self) -> bool {
733        self.is_active()
734    }
735
736    /// Non-upstream alias — use [`page_obj_get_is_active()`](Self::page_obj_get_is_active).
737    #[deprecated(
738        note = "use `page_obj_get_is_active()` — matches upstream `FPDFPageObj_GetIsActive`"
739    )]
740    #[inline]
741    pub fn get_is_active(&self) -> bool {
742        self.is_active()
743    }
744
745    /// Sets the active flag on this page object.
746    ///
747    /// Inactive objects are skipped by `generate_content_stream`.
748    ///
749    /// Corresponds to `FPDFPageObj_SetIsActive`.
750    pub fn set_active(&mut self, active: bool) {
751        match self {
752            PageObject::Path(p) => p.active = active,
753            PageObject::Text(t) => t.active = active,
754            PageObject::Image(img) => img.active = active,
755            PageObject::Form(f) => f.active = active,
756        }
757    }
758
759    /// Upstream-aligned alias for [`set_active()`](Self::set_active).
760    ///
761    /// Corresponds to `FPDFPageObj_SetIsActive`.
762    #[inline]
763    pub fn page_obj_set_is_active(&mut self, active: bool) {
764        self.set_active(active)
765    }
766
767    /// Non-upstream alias — use [`page_obj_set_is_active()`](Self::page_obj_set_is_active).
768    #[deprecated(
769        note = "use `page_obj_set_is_active()` — matches upstream `FPDFPageObj_SetIsActive`"
770    )]
771    #[inline]
772    pub fn set_is_active(&mut self, active: bool) {
773        self.set_active(active)
774    }
775
776    /// Sets the fill color on path and text objects.
777    ///
778    /// Image and form objects are no-ops.
779    ///
780    /// Corresponds to `FPDFPageObj_SetFillColor`.
781    pub fn set_fill_color(&mut self, color: Color) {
782        match self {
783            PageObject::Path(p) => p.set_fill_color(color),
784            PageObject::Text(t) => t.set_fill_color(color),
785            PageObject::Image(_) | PageObject::Form(_) => {}
786        }
787    }
788
789    /// Upstream-aligned alias for [`set_fill_color()`](Self::set_fill_color).
790    ///
791    /// Corresponds to `FPDFPageObj_SetFillColor`.
792    #[inline]
793    pub fn page_obj_set_fill_color(&mut self, color: Color) {
794        self.set_fill_color(color)
795    }
796
797    /// Returns the fill color from path or text objects.
798    ///
799    /// Returns `None` for image and form objects.
800    ///
801    /// Corresponds to `FPDFPageObj_GetFillColor`.
802    pub fn fill_color(&self) -> Option<&Color> {
803        match self {
804            PageObject::Path(p) => p.fill_color(),
805            PageObject::Text(t) => t.fill_color(),
806            PageObject::Image(_) | PageObject::Form(_) => None,
807        }
808    }
809
810    /// Upstream-aligned alias for [`fill_color()`](Self::fill_color).
811    ///
812    /// Corresponds to `FPDFPageObj_GetFillColor`.
813    #[inline]
814    pub fn page_obj_get_fill_color(&self) -> Option<&Color> {
815        self.fill_color()
816    }
817
818    /// Non-upstream alias — use [`page_obj_get_fill_color()`](Self::page_obj_get_fill_color).
819    #[deprecated(
820        note = "use `page_obj_get_fill_color()` — matches upstream `FPDFPageObj_GetFillColor`"
821    )]
822    #[inline]
823    pub fn get_fill_color(&self) -> Option<&Color> {
824        self.fill_color()
825    }
826
827    /// Sets the stroke color on path and text objects.
828    ///
829    /// Image and form objects are no-ops.
830    ///
831    /// Corresponds to `FPDFPageObj_SetStrokeColor`.
832    pub fn set_stroke_color(&mut self, color: Color) {
833        match self {
834            PageObject::Path(p) => p.set_stroke_color(color),
835            PageObject::Text(t) => t.set_stroke_color(color),
836            PageObject::Image(_) | PageObject::Form(_) => {}
837        }
838    }
839
840    /// Upstream-aligned alias for [`set_stroke_color()`](Self::set_stroke_color).
841    ///
842    /// Corresponds to `FPDFPageObj_SetStrokeColor`.
843    #[inline]
844    pub fn page_obj_set_stroke_color(&mut self, color: Color) {
845        self.set_stroke_color(color)
846    }
847
848    /// Returns the stroke color from path or text objects.
849    ///
850    /// Returns `None` for image and form objects.
851    ///
852    /// Corresponds to `FPDFPageObj_GetStrokeColor`.
853    pub fn stroke_color(&self) -> Option<&Color> {
854        match self {
855            PageObject::Path(p) => p.stroke_color(),
856            PageObject::Text(t) => t.stroke_color(),
857            PageObject::Image(_) | PageObject::Form(_) => None,
858        }
859    }
860
861    /// Upstream-aligned alias for [`stroke_color()`](Self::stroke_color).
862    ///
863    /// Corresponds to `FPDFPageObj_GetStrokeColor`.
864    #[inline]
865    pub fn page_obj_get_stroke_color(&self) -> Option<&Color> {
866        self.stroke_color()
867    }
868
869    /// Non-upstream alias — use [`page_obj_get_stroke_color()`](Self::page_obj_get_stroke_color).
870    #[deprecated(
871        note = "use `page_obj_get_stroke_color()` — matches upstream `FPDFPageObj_GetStrokeColor`"
872    )]
873    #[inline]
874    pub fn get_stroke_color(&self) -> Option<&Color> {
875        self.stroke_color()
876    }
877
878    /// Returns the axis-aligned bounding box of the page object in page space.
879    ///
880    /// For `Path` objects, this is the tight bounding box of all segment
881    /// control points.  For `Image` and `Form` objects the unit square
882    /// `[0,0]–[1,1]` is mapped through the object matrix.  Returns `None`
883    /// for `Text` objects (font metrics are required) and for objects with
884    /// no geometry (empty path / degenerate matrix).
885    ///
886    /// Corresponds to `FPDFPageObj_GetBounds`.
887    pub fn bounds(&self) -> Option<rpdfium_core::Rect> {
888        match self {
889            PageObject::Path(p) => p.bounds(),
890            PageObject::Image(img) => img.bounds(),
891            PageObject::Form(f) => f.bounds(),
892            PageObject::Text(_) => None,
893        }
894    }
895
896    /// Upstream-aligned alias for [`bounds()`](Self::bounds).
897    ///
898    /// Corresponds to `FPDFPageObj_GetBounds`.
899    #[inline]
900    pub fn page_obj_get_bounds(&self) -> Option<rpdfium_core::Rect> {
901        self.bounds()
902    }
903
904    /// Non-upstream alias — use [`page_obj_get_bounds()`](Self::page_obj_get_bounds).
905    #[deprecated(note = "use `page_obj_get_bounds()` — matches upstream `FPDFPageObj_GetBounds`")]
906    #[inline]
907    pub fn get_bounds(&self) -> Option<rpdfium_core::Rect> {
908        self.bounds()
909    }
910
911    /// Returns the tight rotated bounding quadrilateral for this page object
912    /// as 4 corner points in page coordinates.
913    ///
914    /// For `Text` and `Image` objects, the matrix is used to compute the
915    /// rotated corners of the object.  For `Path` and `Form` objects, the
916    /// axis-aligned bounding box corners are returned.  Returns `None` if
917    /// the object has no computable bounds.
918    ///
919    /// Corresponds to `FPDFPageObj_GetRotatedBounds`.
920    pub fn rotated_bounds(&self) -> Option<[rpdfium_core::Point; 4]> {
921        match self {
922            PageObject::Text(t) => t.rotated_bounds(),
923            PageObject::Image(img) => img.rotated_bounds(),
924            PageObject::Path(p) => p.bounds().map(|r| {
925                [
926                    rpdfium_core::Point {
927                        x: r.left,
928                        y: r.bottom,
929                    },
930                    rpdfium_core::Point {
931                        x: r.right,
932                        y: r.bottom,
933                    },
934                    rpdfium_core::Point {
935                        x: r.right,
936                        y: r.top,
937                    },
938                    rpdfium_core::Point {
939                        x: r.left,
940                        y: r.top,
941                    },
942                ]
943            }),
944            PageObject::Form(f) => f.bounds().map(|r| {
945                [
946                    rpdfium_core::Point {
947                        x: r.left,
948                        y: r.bottom,
949                    },
950                    rpdfium_core::Point {
951                        x: r.right,
952                        y: r.bottom,
953                    },
954                    rpdfium_core::Point {
955                        x: r.right,
956                        y: r.top,
957                    },
958                    rpdfium_core::Point {
959                        x: r.left,
960                        y: r.top,
961                    },
962                ]
963            }),
964        }
965    }
966
967    /// Upstream-aligned alias for [`rotated_bounds()`](Self::rotated_bounds).
968    ///
969    /// Corresponds to `FPDFPageObj_GetRotatedBounds`.
970    #[inline]
971    pub fn page_obj_get_rotated_bounds(&self) -> Option<[rpdfium_core::Point; 4]> {
972        self.rotated_bounds()
973    }
974
975    /// Non-upstream alias — use [`page_obj_get_rotated_bounds()`](Self::page_obj_get_rotated_bounds).
976    #[deprecated(
977        note = "use `page_obj_get_rotated_bounds()` — matches upstream `FPDFPageObj_GetRotatedBounds`"
978    )]
979    #[inline]
980    pub fn get_rotated_bounds(&self) -> Option<[rpdfium_core::Point; 4]> {
981        self.rotated_bounds()
982    }
983
984    /// Applies a matrix transform to the page object.
985    ///
986    /// For `Path` objects the transform is applied to each segment point.
987    /// For `Text`, `Image`, and `Form` objects the transform matrix is
988    /// pre-multiplied into the object's own matrix.
989    ///
990    /// Corresponds to `FPDFPageObj_Transform`.
991    pub fn transform(&mut self, matrix: &Matrix) {
992        match self {
993            PageObject::Path(p) => p.transform(matrix),
994            PageObject::Text(t) => t.transform(matrix),
995            PageObject::Image(img) => img.transform(matrix),
996            PageObject::Form(f) => f.transform(matrix),
997        }
998    }
999
1000    /// Upstream-aligned alias for [`transform()`](Self::transform).
1001    ///
1002    /// Corresponds to `FPDFPageObj_Transform`.
1003    #[inline]
1004    pub fn page_obj_transform(&mut self, matrix: &Matrix) {
1005        self.transform(matrix)
1006    }
1007
1008    /// Upstream-aligned alias for [`transform()`](Self::transform).
1009    ///
1010    /// `FPDFPageObj_TransformF` takes an `FS_MATRIX*` struct pointer instead
1011    /// of six scalar parameters; in rpdfium both variants accept `&Matrix`.
1012    ///
1013    /// Corresponds to `FPDFPageObj_TransformF`.
1014    #[inline]
1015    pub fn page_obj_transform_f(&mut self, matrix: &Matrix) {
1016        self.transform(matrix)
1017    }
1018
1019    #[deprecated(note = "use `page_obj_transform_f()` — matches upstream `FPDFPageObj_TransformF`")]
1020    #[inline]
1021    pub fn transform_f(&mut self, matrix: &Matrix) {
1022        self.transform(matrix)
1023    }
1024
1025    /// Returns the blend mode of this page object.
1026    ///
1027    /// Returns `None` for `Normal` (the default, meaning no special blending).
1028    ///
1029    /// Corresponds to `FPDFPageObj_GetBlendMode`.
1030    pub fn blend_mode(&self) -> Option<BlendMode> {
1031        match self {
1032            PageObject::Path(p) => p.blend_mode,
1033            PageObject::Text(t) => t.blend_mode,
1034            PageObject::Image(img) => img.blend_mode,
1035            PageObject::Form(f) => f.blend_mode,
1036        }
1037    }
1038
1039    /// Upstream-aligned alias for [`blend_mode()`](Self::blend_mode).
1040    ///
1041    /// Corresponds to `FPDFPageObj_GetBlendMode`.
1042    #[inline]
1043    pub fn page_obj_get_blend_mode(&self) -> Option<BlendMode> {
1044        self.blend_mode()
1045    }
1046
1047    /// Non-upstream alias — use [`page_obj_get_blend_mode()`](Self::page_obj_get_blend_mode).
1048    #[deprecated(
1049        note = "use `page_obj_get_blend_mode()` — matches upstream `FPDFPageObj_GetBlendMode`"
1050    )]
1051    #[inline]
1052    pub fn get_blend_mode(&self) -> Option<BlendMode> {
1053        self.blend_mode()
1054    }
1055
1056    /// Sets the blend mode on this page object.
1057    ///
1058    /// Pass `None` (or `Some(BlendMode::Normal)`) for the default blending.
1059    ///
1060    /// Corresponds to `FPDFPageObj_SetBlendMode`.
1061    pub fn set_blend_mode(&mut self, mode: Option<BlendMode>) {
1062        match self {
1063            PageObject::Path(p) => p.blend_mode = mode,
1064            PageObject::Text(t) => t.blend_mode = mode,
1065            PageObject::Image(img) => img.blend_mode = mode,
1066            PageObject::Form(f) => f.blend_mode = mode,
1067        }
1068    }
1069
1070    /// Upstream-aligned alias for [`set_blend_mode()`](Self::set_blend_mode).
1071    ///
1072    /// Corresponds to `FPDFPageObj_SetBlendMode`.
1073    #[inline]
1074    pub fn page_obj_set_blend_mode(&mut self, mode: Option<BlendMode>) {
1075        self.set_blend_mode(mode)
1076    }
1077
1078    // -----------------------------------------------------------------------
1079    // Matrix dispatch  (FPDFPageObj_GetMatrix / FPDFPageObj_SetMatrix)
1080    // -----------------------------------------------------------------------
1081
1082    /// Returns the transformation matrix of this page object.
1083    ///
1084    /// For `Image` and `Form` objects this is the object's explicit placement
1085    /// matrix.  For `Path` objects the identity matrix is returned (path
1086    /// coordinates are in absolute page space).  For `Text` objects the
1087    /// identity matrix is returned (text uses its own `Tm` matrix).
1088    ///
1089    /// Corresponds to `FPDFPageObj_GetMatrix`.
1090    pub fn matrix(&self) -> Matrix {
1091        match self {
1092            PageObject::Image(img) => *img.matrix(),
1093            PageObject::Form(f) => *f.matrix(),
1094            PageObject::Path(p) => p.matrix(),
1095            PageObject::Text(_) => Matrix::identity(),
1096        }
1097    }
1098
1099    /// Upstream-aligned alias for [`matrix()`](Self::matrix).
1100    ///
1101    /// Corresponds to `FPDFPageObj_GetMatrix`.
1102    #[inline]
1103    pub fn page_obj_get_matrix(&self) -> Matrix {
1104        self.matrix()
1105    }
1106
1107    /// Non-upstream alias — use [`page_obj_get_matrix()`](Self::page_obj_get_matrix).
1108    #[deprecated(note = "use `page_obj_get_matrix()` — matches upstream `FPDFPageObj_GetMatrix`")]
1109    #[inline]
1110    pub fn get_matrix(&self) -> Matrix {
1111        self.matrix()
1112    }
1113
1114    /// Set the transformation matrix on this page object.
1115    ///
1116    /// Effective for `Image` and `Form` objects.  No-op for `Path` and `Text`
1117    /// objects (use [`transform()`](Self::transform) to apply a transform to
1118    /// those types).
1119    ///
1120    /// Corresponds to `FPDFPageObj_SetMatrix`.
1121    pub fn set_matrix(&mut self, matrix: Matrix) {
1122        match self {
1123            PageObject::Image(img) => img.set_matrix(matrix),
1124            PageObject::Form(f) => f.set_matrix(matrix),
1125            PageObject::Path(_) | PageObject::Text(_) => {}
1126        }
1127    }
1128
1129    /// Upstream-aligned alias for [`set_matrix()`](Self::set_matrix).
1130    ///
1131    /// Corresponds to `FPDFPageObj_SetMatrix`.
1132    #[inline]
1133    pub fn page_obj_set_matrix(&mut self, matrix: Matrix) {
1134        self.set_matrix(matrix)
1135    }
1136
1137    // -----------------------------------------------------------------------
1138    // Transparency dispatch  (FPDFPageObj_HasTransparency)
1139    // -----------------------------------------------------------------------
1140
1141    /// Returns `true` if this page object has any transparency.
1142    ///
1143    /// An object is considered transparent when it has a non-`Normal` blend
1144    /// mode set.
1145    ///
1146    /// Corresponds to `FPDFPageObj_HasTransparency`.
1147    pub fn has_transparency(&self) -> bool {
1148        match self {
1149            PageObject::Path(p) => p.has_transparency(),
1150            PageObject::Text(t) => t.has_transparency(),
1151            PageObject::Image(img) => img.has_transparency(),
1152            PageObject::Form(f) => f.has_transparency(),
1153        }
1154    }
1155
1156    /// Non-upstream convenience alias for [`has_transparency()`](Self::has_transparency).
1157    ///
1158    /// Prefer [`has_transparency()`](Self::has_transparency), which matches
1159    /// the upstream `FPDFPageObj_HasTransparency` name exactly.
1160    #[deprecated(note = "use `has_transparency()` — matches upstream FPDFPageObj_HasTransparency")]
1161    #[inline]
1162    pub fn is_transparent(&self) -> bool {
1163        self.has_transparency()
1164    }
1165
1166    // -----------------------------------------------------------------------
1167    // Content marks dispatch  (FPDFPageObj_CountMarks / GetMark / AddMark /
1168    //                          RemoveMark / GetMarkedContentID)
1169    // -----------------------------------------------------------------------
1170
1171    /// Return the number of content marks on this page object.
1172    ///
1173    /// Corresponds to `FPDFPageObj_CountMarks`.
1174    pub fn mark_count(&self) -> usize {
1175        match self {
1176            PageObject::Path(p) => p.mark_count(),
1177            PageObject::Text(t) => t.mark_count(),
1178            PageObject::Image(img) => img.mark_count(),
1179            PageObject::Form(f) => f.mark_count(),
1180        }
1181    }
1182
1183    /// Upstream-aligned alias for [`mark_count()`](Self::mark_count).
1184    ///
1185    /// Corresponds to `FPDFPageObj_CountMarks`.
1186    #[inline]
1187    pub fn page_obj_count_marks(&self) -> usize {
1188        self.mark_count()
1189    }
1190
1191    /// Non-upstream alias — use [`page_obj_count_marks()`](Self::page_obj_count_marks).
1192    #[deprecated(note = "use `page_obj_count_marks()` — matches upstream `FPDFPageObj_CountMarks`")]
1193    #[inline]
1194    pub fn count_marks(&self) -> usize {
1195        self.mark_count()
1196    }
1197
1198    /// Return the content mark at the given index, or `None` if out of range.
1199    ///
1200    /// Corresponds to `FPDFPageObj_GetMark`.
1201    pub fn mark(&self, index: usize) -> Option<&ContentMark> {
1202        match self {
1203            PageObject::Path(p) => p.mark(index),
1204            PageObject::Text(t) => t.mark(index),
1205            PageObject::Image(img) => img.mark(index),
1206            PageObject::Form(f) => f.mark(index),
1207        }
1208    }
1209
1210    /// Upstream-aligned alias for [`mark()`](Self::mark).
1211    ///
1212    /// Corresponds to `FPDFPageObj_GetMark`.
1213    #[inline]
1214    pub fn page_obj_get_mark(&self, index: usize) -> Option<&ContentMark> {
1215        self.mark(index)
1216    }
1217
1218    /// Non-upstream alias — use [`page_obj_get_mark()`](Self::page_obj_get_mark).
1219    #[deprecated(note = "use `page_obj_get_mark()` — matches upstream `FPDFPageObj_GetMark`")]
1220    #[inline]
1221    pub fn get_mark(&self, index: usize) -> Option<&ContentMark> {
1222        self.mark(index)
1223    }
1224
1225    /// Add a new content mark with the given name and return a mutable
1226    /// reference to it.
1227    ///
1228    /// Corresponds to `FPDFPageObj_AddMark`.
1229    pub fn add_mark(&mut self, name: impl Into<String>) -> &mut ContentMark {
1230        match self {
1231            PageObject::Path(p) => p.add_mark(name),
1232            PageObject::Text(t) => t.add_mark(name),
1233            PageObject::Image(img) => img.add_mark(name),
1234            PageObject::Form(f) => f.add_mark(name),
1235        }
1236    }
1237
1238    /// Upstream-aligned alias for [`add_mark()`](Self::add_mark).
1239    ///
1240    /// Corresponds to `FPDFPageObj_AddMark`.
1241    #[inline]
1242    pub fn page_obj_add_mark(&mut self, name: impl Into<String>) -> &mut ContentMark {
1243        self.add_mark(name)
1244    }
1245
1246    /// Remove the content mark at the given index.
1247    ///
1248    /// Returns `true` if the index was valid and the mark was removed, `false`
1249    /// if out of range.
1250    ///
1251    /// Corresponds to `FPDFPageObj_RemoveMark`.
1252    pub fn remove_mark(&mut self, index: usize) -> bool {
1253        match self {
1254            PageObject::Path(p) => p.remove_mark(index),
1255            PageObject::Text(t) => t.remove_mark(index),
1256            PageObject::Image(img) => img.remove_mark(index),
1257            PageObject::Form(f) => f.remove_mark(index),
1258        }
1259    }
1260
1261    /// Upstream-aligned alias for [`remove_mark()`](Self::remove_mark).
1262    ///
1263    /// Corresponds to `FPDFPageObj_RemoveMark`.
1264    #[inline]
1265    pub fn page_obj_remove_mark(&mut self, index: usize) -> bool {
1266        self.remove_mark(index)
1267    }
1268
1269    /// Return the marked content ID (`/MCID`) from the first mark that has one,
1270    /// or `None` if no mark carries an MCID.
1271    ///
1272    /// Corresponds to `FPDFPageObj_GetMarkedContentID`.
1273    pub fn marked_content_id(&self) -> Option<i64> {
1274        match self {
1275            PageObject::Path(p) => p.marked_content_id(),
1276            PageObject::Text(t) => t.marked_content_id(),
1277            PageObject::Image(img) => img.marked_content_id(),
1278            PageObject::Form(f) => f.marked_content_id(),
1279        }
1280    }
1281
1282    /// Upstream-aligned alias for [`marked_content_id()`](Self::marked_content_id).
1283    ///
1284    /// Corresponds to `FPDFPageObj_GetMarkedContentID`.
1285    #[inline]
1286    pub fn page_obj_get_marked_content_id(&self) -> Option<i64> {
1287        self.marked_content_id()
1288    }
1289
1290    /// Non-upstream alias — use [`page_obj_get_marked_content_id()`](Self::page_obj_get_marked_content_id).
1291    #[deprecated(
1292        note = "use `page_obj_get_marked_content_id()` — matches upstream `FPDFPageObj_GetMarkedContentID`"
1293    )]
1294    #[inline]
1295    pub fn get_marked_content_id(&self) -> Option<i64> {
1296        self.marked_content_id()
1297    }
1298
1299    // -----------------------------------------------------------------------
1300    // Clip path dispatch  (FPDFPageObj_GetClipPath / TransformClipPath)
1301    // -----------------------------------------------------------------------
1302
1303    /// Returns a reference to the clip path, if one is attached.
1304    ///
1305    /// Corresponds to `FPDFPageObj_GetClipPath`.
1306    pub fn clip_path(&self) -> Option<&ClipPath> {
1307        match self {
1308            PageObject::Path(p) => p.clip_path.as_ref(),
1309            PageObject::Text(t) => t.clip_path.as_ref(),
1310            PageObject::Image(img) => img.clip_path.as_ref(),
1311            PageObject::Form(f) => f.clip_path.as_ref(),
1312        }
1313    }
1314
1315    /// Upstream-aligned alias for [`clip_path()`](Self::clip_path).
1316    ///
1317    /// Corresponds to `FPDFPageObj_GetClipPath`.
1318    #[inline]
1319    pub fn page_obj_get_clip_path(&self) -> Option<&ClipPath> {
1320        self.clip_path()
1321    }
1322
1323    /// Deprecated — use [`page_obj_get_clip_path()`](Self::page_obj_get_clip_path) instead.
1324    ///
1325    /// Corresponds to `FPDFPageObj_GetClipPath`.
1326    #[deprecated(
1327        note = "use `page_obj_get_clip_path()` — matches upstream `FPDFPageObj_GetClipPath`"
1328    )]
1329    #[inline]
1330    pub fn get_clip_path(&self) -> Option<&ClipPath> {
1331        self.clip_path()
1332    }
1333
1334    /// Sets or clears the clip path on this page object.
1335    pub fn set_clip_path(&mut self, clip: Option<ClipPath>) {
1336        match self {
1337            PageObject::Path(p) => p.clip_path = clip,
1338            PageObject::Text(t) => t.clip_path = clip,
1339            PageObject::Image(img) => img.clip_path = clip,
1340            PageObject::Form(f) => f.clip_path = clip,
1341        }
1342    }
1343
1344    /// Applies a matrix transform to the clip path attached to this object.
1345    ///
1346    /// No-op if the object has no clip path.
1347    ///
1348    /// Corresponds to `FPDFPageObj_TransformClipPath`.
1349    pub fn transform_clip_path(&mut self, matrix: &Matrix) {
1350        let clip = match self {
1351            PageObject::Path(p) => p.clip_path.as_mut(),
1352            PageObject::Text(t) => t.clip_path.as_mut(),
1353            PageObject::Image(img) => img.clip_path.as_mut(),
1354            PageObject::Form(f) => f.clip_path.as_mut(),
1355        };
1356        if let Some(c) = clip {
1357            c.transform(matrix);
1358        }
1359    }
1360
1361    /// Upstream alias for [`PageObject::transform_clip_path`].
1362    ///
1363    /// Tier-2 name for `FPDFPageObj_TransformClipPath`.
1364    #[inline]
1365    pub fn page_obj_transform_clip_path(&mut self, matrix: &Matrix) {
1366        self.transform_clip_path(matrix);
1367    }
1368
1369    /// Returns `true` if this is a text object (`FPDF_PAGEOBJ_TEXT`).
1370    pub fn is_text(&self) -> bool {
1371        matches!(self, PageObject::Text(_))
1372    }
1373
1374    /// Returns `true` if this is a path object (`FPDF_PAGEOBJ_PATH`).
1375    pub fn is_path(&self) -> bool {
1376        matches!(self, PageObject::Path(_))
1377    }
1378
1379    /// Returns `true` if this is an image object (`FPDF_PAGEOBJ_IMAGE`).
1380    pub fn is_image(&self) -> bool {
1381        matches!(self, PageObject::Image(_))
1382    }
1383
1384    /// Returns `true` if this is a form XObject reference (`FPDF_PAGEOBJ_FORM`).
1385    pub fn is_form(&self) -> bool {
1386        matches!(self, PageObject::Form(_))
1387    }
1388
1389    /// Returns the PDFium page-object-type constant for this object.
1390    ///
1391    /// - Path  → `2` (`FPDF_PAGEOBJ_PATH`)
1392    /// - Text  → `1` (`FPDF_PAGEOBJ_TEXT`)
1393    /// - Image → `3` (`FPDF_PAGEOBJ_IMAGE`)
1394    /// - Form  → `5` (`FPDF_PAGEOBJ_FORM`)
1395    ///
1396    /// Corresponds to `FPDFPageObj_GetType`.
1397    pub fn object_type(&self) -> u32 {
1398        match self {
1399            PageObject::Path(o) => o.object_type(),
1400            PageObject::Text(o) => o.object_type(),
1401            PageObject::Image(o) => o.object_type(),
1402            PageObject::Form(o) => o.object_type(),
1403        }
1404    }
1405
1406    /// Upstream-aligned alias for [`object_type()`](Self::object_type).
1407    ///
1408    /// Corresponds to `FPDFPageObj_GetType`.
1409    #[inline]
1410    pub fn page_obj_get_type(&self) -> u32 {
1411        self.object_type()
1412    }
1413
1414    /// Non-upstream alias — use [`page_obj_get_type()`](Self::page_obj_get_type).
1415    ///
1416    /// The actual upstream function is `FPDFPageObj_GetType`; there is no
1417    /// `FPDFPageObj_GetObjectType`.
1418    #[deprecated(note = "use `page_obj_get_type()` — matches upstream FPDFPageObj_GetType")]
1419    #[inline]
1420    pub fn get_object_type(&self) -> u32 {
1421        self.object_type()
1422    }
1423
1424    /// Non-upstream alias — use [`page_obj_get_type()`](Self::page_obj_get_type).
1425    #[deprecated(note = "use `page_obj_get_type()` — matches upstream FPDFPageObj_GetType")]
1426    #[inline]
1427    pub fn get_type(&self) -> u32 {
1428        self.object_type()
1429    }
1430
1431    // -----------------------------------------------------------------------
1432    // Stroke width dispatch  (FPDFPageObj_GetStrokeWidth / SetStrokeWidth)
1433    // -----------------------------------------------------------------------
1434
1435    /// Returns the stroke width of this page object.
1436    ///
1437    /// Returns `None` for text, image, and form objects (only path objects
1438    /// track a per-object stroke width in rpdfium's current model).
1439    ///
1440    /// Corresponds to `FPDFPageObj_GetStrokeWidth`.
1441    pub fn stroke_width(&self) -> Option<f32> {
1442        match self {
1443            PageObject::Path(p) => Some(p.line_width()),
1444            PageObject::Text(_) | PageObject::Image(_) | PageObject::Form(_) => None,
1445        }
1446    }
1447
1448    /// Upstream-aligned alias for [`stroke_width()`](Self::stroke_width).
1449    ///
1450    /// Corresponds to `FPDFPageObj_GetStrokeWidth`.
1451    #[inline]
1452    pub fn page_obj_get_stroke_width(&self) -> Option<f32> {
1453        self.stroke_width()
1454    }
1455
1456    #[deprecated(
1457        note = "use `page_obj_get_stroke_width()` — matches upstream `FPDFPageObj_GetStrokeWidth`"
1458    )]
1459    #[inline]
1460    pub fn get_stroke_width(&self) -> Option<f32> {
1461        self.stroke_width()
1462    }
1463
1464    /// Sets the stroke width on path objects.
1465    ///
1466    /// No-op for text, image, and form objects.
1467    ///
1468    /// Corresponds to `FPDFPageObj_SetStrokeWidth`.
1469    pub fn set_stroke_width(&mut self, width: f32) {
1470        match self {
1471            PageObject::Path(p) => p.set_line_width(width),
1472            PageObject::Text(_) | PageObject::Image(_) | PageObject::Form(_) => {}
1473        }
1474    }
1475
1476    /// Upstream-aligned alias for [`set_stroke_width()`](Self::set_stroke_width).
1477    ///
1478    /// Corresponds to `FPDFPageObj_SetStrokeWidth`.
1479    #[inline]
1480    pub fn page_obj_set_stroke_width(&mut self, width: f32) {
1481        self.set_stroke_width(width)
1482    }
1483
1484    // -----------------------------------------------------------------------
1485    // Line join dispatch  (FPDFPageObj_GetLineJoin / SetLineJoin)
1486    // -----------------------------------------------------------------------
1487
1488    /// Returns the line join style of this page object.
1489    ///
1490    /// Returns `None` for non-path objects that have no line join.
1491    ///
1492    /// Corresponds to `FPDFPageObj_GetLineJoin`.
1493    pub fn line_join(&self) -> Option<u8> {
1494        match self {
1495            PageObject::Path(p) => Some(p.line_join()),
1496            PageObject::Text(_) | PageObject::Image(_) | PageObject::Form(_) => None,
1497        }
1498    }
1499
1500    /// Upstream-aligned alias for [`line_join()`](Self::line_join).
1501    ///
1502    /// Corresponds to `FPDFPageObj_GetLineJoin`.
1503    #[inline]
1504    pub fn page_obj_get_line_join(&self) -> Option<u8> {
1505        self.line_join()
1506    }
1507
1508    #[deprecated(
1509        note = "use `page_obj_get_line_join()` — matches upstream `FPDFPageObj_GetLineJoin`"
1510    )]
1511    #[inline]
1512    pub fn get_line_join(&self) -> Option<u8> {
1513        self.line_join()
1514    }
1515
1516    /// Sets the line join style on path objects.
1517    ///
1518    /// No-op for text, image, and form objects.
1519    ///
1520    /// Corresponds to `FPDFPageObj_SetLineJoin`.
1521    pub fn set_line_join(&mut self, join: u8) {
1522        match self {
1523            PageObject::Path(p) => p.set_line_join(join),
1524            PageObject::Text(_) | PageObject::Image(_) | PageObject::Form(_) => {}
1525        }
1526    }
1527
1528    /// Upstream-aligned alias for [`set_line_join()`](Self::set_line_join).
1529    ///
1530    /// Corresponds to `FPDFPageObj_SetLineJoin`.
1531    #[inline]
1532    pub fn page_obj_set_line_join(&mut self, join: u8) {
1533        self.set_line_join(join)
1534    }
1535
1536    // -----------------------------------------------------------------------
1537    // Line cap dispatch  (FPDFPageObj_GetLineCap / SetLineCap)
1538    // -----------------------------------------------------------------------
1539
1540    /// Returns the line cap style of this page object.
1541    ///
1542    /// Returns `None` for non-path objects that have no line cap.
1543    ///
1544    /// Corresponds to `FPDFPageObj_GetLineCap`.
1545    pub fn line_cap(&self) -> Option<u8> {
1546        match self {
1547            PageObject::Path(p) => Some(p.line_cap()),
1548            PageObject::Text(_) | PageObject::Image(_) | PageObject::Form(_) => None,
1549        }
1550    }
1551
1552    /// Upstream-aligned alias for [`line_cap()`](Self::line_cap).
1553    ///
1554    /// Corresponds to `FPDFPageObj_GetLineCap`.
1555    #[inline]
1556    pub fn page_obj_get_line_cap(&self) -> Option<u8> {
1557        self.line_cap()
1558    }
1559
1560    #[deprecated(
1561        note = "use `page_obj_get_line_cap()` — matches upstream `FPDFPageObj_GetLineCap`"
1562    )]
1563    #[inline]
1564    pub fn get_line_cap(&self) -> Option<u8> {
1565        self.line_cap()
1566    }
1567
1568    /// Sets the line cap style on path objects.
1569    ///
1570    /// No-op for text, image, and form objects.
1571    ///
1572    /// Corresponds to `FPDFPageObj_SetLineCap`.
1573    pub fn set_line_cap(&mut self, cap: u8) {
1574        match self {
1575            PageObject::Path(p) => p.set_line_cap(cap),
1576            PageObject::Text(_) | PageObject::Image(_) | PageObject::Form(_) => {}
1577        }
1578    }
1579
1580    /// Upstream-aligned alias for [`set_line_cap()`](Self::set_line_cap).
1581    ///
1582    /// Corresponds to `FPDFPageObj_SetLineCap`.
1583    #[inline]
1584    pub fn page_obj_set_line_cap(&mut self, cap: u8) {
1585        self.set_line_cap(cap)
1586    }
1587
1588    // -----------------------------------------------------------------------
1589    // Dash pattern dispatch  (FPDFPageObj_GetDashPhase / SetDashPhase /
1590    //                         FPDFPageObj_GetDashCount / GetDashArray /
1591    //                         SetDashArray)
1592    // -----------------------------------------------------------------------
1593
1594    /// Returns the dash phase of this page object.
1595    ///
1596    /// Returns `None` for non-path objects that have no dash pattern.
1597    ///
1598    /// Corresponds to `FPDFPageObj_GetDashPhase`.
1599    pub fn dash_phase(&self) -> Option<f32> {
1600        match self {
1601            PageObject::Path(p) => Some(p.dash_phase()),
1602            PageObject::Text(_) | PageObject::Image(_) | PageObject::Form(_) => None,
1603        }
1604    }
1605
1606    /// Upstream-aligned alias for [`dash_phase()`](Self::dash_phase).
1607    ///
1608    /// Corresponds to `FPDFPageObj_GetDashPhase`.
1609    #[inline]
1610    pub fn page_obj_get_dash_phase(&self) -> Option<f32> {
1611        self.dash_phase()
1612    }
1613
1614    #[deprecated(
1615        note = "use `page_obj_get_dash_phase()` — matches upstream `FPDFPageObj_GetDashPhase`"
1616    )]
1617    #[inline]
1618    pub fn get_dash_phase(&self) -> Option<f32> {
1619        self.dash_phase()
1620    }
1621
1622    /// Sets the dash phase on path objects.
1623    ///
1624    /// No-op for text, image, and form objects.
1625    ///
1626    /// Corresponds to `FPDFPageObj_SetDashPhase`.
1627    pub fn set_dash_phase(&mut self, phase: f32) {
1628        match self {
1629            PageObject::Path(p) => p.set_dash_phase(phase),
1630            PageObject::Text(_) | PageObject::Image(_) | PageObject::Form(_) => {}
1631        }
1632    }
1633
1634    /// Upstream-aligned alias for [`set_dash_phase()`](Self::set_dash_phase).
1635    ///
1636    /// Corresponds to `FPDFPageObj_SetDashPhase`.
1637    #[inline]
1638    pub fn page_obj_set_dash_phase(&mut self, phase: f32) {
1639        self.set_dash_phase(phase)
1640    }
1641
1642    /// Returns the number of elements in the dash array of this page object.
1643    ///
1644    /// Returns `None` for non-path objects that have no dash pattern.
1645    ///
1646    /// Corresponds to `FPDFPageObj_GetDashCount`.
1647    pub fn dash_count(&self) -> Option<usize> {
1648        match self {
1649            PageObject::Path(p) => Some(p.dash_count()),
1650            PageObject::Text(_) | PageObject::Image(_) | PageObject::Form(_) => None,
1651        }
1652    }
1653
1654    /// Upstream-aligned alias for [`dash_count()`](Self::dash_count).
1655    ///
1656    /// Corresponds to `FPDFPageObj_GetDashCount`.
1657    #[inline]
1658    pub fn page_obj_get_dash_count(&self) -> Option<usize> {
1659        self.dash_count()
1660    }
1661
1662    #[deprecated(
1663        note = "use `page_obj_get_dash_count()` — matches upstream `FPDFPageObj_GetDashCount`"
1664    )]
1665    #[inline]
1666    pub fn get_dash_count(&self) -> Option<usize> {
1667        self.dash_count()
1668    }
1669
1670    /// Returns the dash array of this page object.
1671    ///
1672    /// Returns `None` for non-path objects that have no dash pattern.
1673    ///
1674    /// Corresponds to `FPDFPageObj_GetDashArray`.
1675    pub fn dash_array(&self) -> Option<&[f32]> {
1676        match self {
1677            PageObject::Path(p) => Some(p.dash_array()),
1678            PageObject::Text(_) | PageObject::Image(_) | PageObject::Form(_) => None,
1679        }
1680    }
1681
1682    /// Upstream-aligned alias for [`dash_array()`](Self::dash_array).
1683    ///
1684    /// Corresponds to `FPDFPageObj_GetDashArray`.
1685    #[inline]
1686    pub fn page_obj_get_dash_array(&self) -> Option<&[f32]> {
1687        self.dash_array()
1688    }
1689
1690    #[deprecated(
1691        note = "use `page_obj_get_dash_array()` — matches upstream `FPDFPageObj_GetDashArray`"
1692    )]
1693    #[inline]
1694    pub fn get_dash_array(&self) -> Option<&[f32]> {
1695        self.dash_array()
1696    }
1697
1698    /// Sets the dash array and phase on path objects.
1699    ///
1700    /// No-op for text, image, and form objects.
1701    ///
1702    /// Corresponds to `FPDFPageObj_SetDashArray`.
1703    pub fn set_dash_array(&mut self, array: Vec<f32>, phase: f32) {
1704        match self {
1705            PageObject::Path(p) => p.set_dash_array(array, phase),
1706            PageObject::Text(_) | PageObject::Image(_) | PageObject::Form(_) => {}
1707        }
1708    }
1709
1710    /// Upstream-aligned alias for [`set_dash_array()`](Self::set_dash_array).
1711    ///
1712    /// Corresponds to `FPDFPageObj_SetDashArray`.
1713    #[inline]
1714    pub fn page_obj_set_dash_array(&mut self, array: Vec<f32>, phase: f32) {
1715        self.set_dash_array(array, phase)
1716    }
1717}
1718
1719/// Fill mode for path objects.
1720#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1721pub enum FillMode {
1722    /// No fill.
1723    None,
1724    /// Even-odd fill rule (alternate).
1725    EvenOdd,
1726    /// Non-zero winding fill rule.
1727    NonZero,
1728}
1729
1730/// A segment of a path.
1731#[derive(Debug, Clone)]
1732pub enum PathSegment {
1733    /// Move to (x, y).
1734    MoveTo(f64, f64),
1735    /// Line to (x, y).
1736    LineTo(f64, f64),
1737    /// Cubic Bezier to (x1, y1, x2, y2, x3, y3).
1738    CurveTo(f64, f64, f64, f64, f64, f64),
1739    /// Rectangle at (x, y, w, h).
1740    Rect(f64, f64, f64, f64),
1741    /// Close the current subpath.
1742    Close,
1743}
1744
1745impl PathSegment {
1746    /// Returns the endpoint coordinates of this segment, or `None` for
1747    /// `Close` segments.
1748    ///
1749    /// For `MoveTo` and `LineTo` segments returns `Some((x, y))`.
1750    /// For `CurveTo` segments returns `Some((x3, y3))` — the endpoint.
1751    /// For `Rect` segments returns `Some((x, y))` — the origin corner.
1752    /// For `Close` returns `None`.
1753    ///
1754    /// Corresponds to `FPDFPathSegment_GetPoint`.
1755    pub fn point(&self) -> Option<(f64, f64)> {
1756        match self {
1757            PathSegment::MoveTo(x, y) => Some((*x, *y)),
1758            PathSegment::LineTo(x, y) => Some((*x, *y)),
1759            PathSegment::CurveTo(_, _, _, _, x3, y3) => Some((*x3, *y3)),
1760            PathSegment::Rect(x, y, _, _) => Some((*x, *y)),
1761            PathSegment::Close => None,
1762        }
1763    }
1764
1765    /// Upstream-aligned alias for [`point()`](Self::point).
1766    ///
1767    /// Corresponds to `FPDFPathSegment_GetPoint`.
1768    #[inline]
1769    pub fn path_segment_get_point(&self) -> Option<(f64, f64)> {
1770        self.point()
1771    }
1772
1773    #[deprecated(
1774        note = "use `path_segment_get_point()` — matches upstream `FPDFPathSegment_GetPoint`"
1775    )]
1776    #[inline]
1777    pub fn get_point(&self) -> Option<(f64, f64)> {
1778        self.point()
1779    }
1780
1781    /// Returns the numeric segment type code for this segment.
1782    ///
1783    /// Returns:
1784    /// - `FPDF_SEGMENT_MOVETO` (1) for `MoveTo`
1785    /// - `FPDF_SEGMENT_LINETO` (2) for `LineTo`
1786    /// - `FPDF_SEGMENT_BEZIERTO` (3) for `CurveTo` (Bezier)
1787    /// - `FPDF_SEGMENT_UNKNOWN` (−1) for `Close` and `Rect`
1788    ///
1789    /// Corresponds to `FPDFPathSegment_GetType`.
1790    pub fn segment_type(&self) -> i32 {
1791        match self {
1792            PathSegment::MoveTo(_, _) => 1,              // FPDF_SEGMENT_MOVETO
1793            PathSegment::LineTo(_, _) => 2,              // FPDF_SEGMENT_LINETO
1794            PathSegment::CurveTo(_, _, _, _, _, _) => 3, // FPDF_SEGMENT_BEZIERTO
1795            PathSegment::Rect(_, _, _, _) => -1,         // FPDF_SEGMENT_UNKNOWN
1796            PathSegment::Close => -1,                    // FPDF_SEGMENT_UNKNOWN
1797        }
1798    }
1799
1800    /// Upstream-aligned alias for [`segment_type()`](Self::segment_type).
1801    ///
1802    /// Corresponds to `FPDFPathSegment_GetType`.
1803    #[inline]
1804    pub fn path_segment_get_type(&self) -> i32 {
1805        self.segment_type()
1806    }
1807
1808    #[deprecated(
1809        note = "use `path_segment_get_type()` — matches upstream `FPDFPathSegment_GetType`"
1810    )]
1811    #[inline]
1812    pub fn get_type(&self) -> i32 {
1813        self.segment_type()
1814    }
1815
1816    /// Returns whether this segment closes the current subpath.
1817    ///
1818    /// Returns `true` only for `Close` segments.
1819    ///
1820    /// Corresponds to `FPDFPathSegment_GetClose`.
1821    pub fn is_close(&self) -> bool {
1822        matches!(self, PathSegment::Close)
1823    }
1824
1825    /// Upstream-aligned alias for [`is_close()`](Self::is_close).
1826    ///
1827    /// Corresponds to `FPDFPathSegment_GetClose`.
1828    #[inline]
1829    pub fn path_segment_get_close(&self) -> bool {
1830        self.is_close()
1831    }
1832
1833    #[deprecated(
1834        note = "use `path_segment_get_close()` — matches upstream `FPDFPathSegment_GetClose`"
1835    )]
1836    #[inline]
1837    pub fn get_close(&self) -> bool {
1838        self.is_close()
1839    }
1840}
1841
1842/// A path/shape object with fill and stroke properties.
1843#[derive(Debug, Clone)]
1844pub struct PathObject {
1845    /// Path segments.
1846    pub segments: Vec<PathSegment>,
1847    /// Transformation matrix.
1848    pub matrix: Matrix,
1849    /// Fill color (None = no fill).
1850    pub fill_color: Option<Color>,
1851    /// Stroke color (None = no stroke).
1852    pub stroke_color: Option<Color>,
1853    /// Line width for stroking.
1854    pub line_width: f32,
1855    /// Line cap style (0=butt, 1=round, 2=square).
1856    pub line_cap: u8,
1857    /// Line join style (0=miter, 1=round, 2=bevel).
1858    pub line_join: u8,
1859    /// Fill mode.
1860    pub fill_mode: FillMode,
1861    /// Dash pattern for stroking (None = solid line).
1862    ///
1863    /// Corresponds to `FPDFPath_GetDashArray` / `FPDFPath_SetDashArray`.
1864    pub dash_pattern: Option<DashPattern>,
1865    /// Blend mode for compositing.
1866    ///
1867    /// Corresponds to `FPDFPageObj_GetBlendMode` / `FPDFPageObj_SetBlendMode`.
1868    pub blend_mode: Option<BlendMode>,
1869    /// Whether the object is active (visible).
1870    ///
1871    /// Corresponds to `FPDFPageObj_GetIsActive` / `FPDFPageObj_SetIsActive`.
1872    pub active: bool,
1873    /// Content marks attached to this object (for tagged PDF / accessibility).
1874    ///
1875    /// Each mark corresponds to a `BDC`…`EMC` pair wrapping this object in
1876    /// the content stream.
1877    ///
1878    /// Corresponds to `FPDFPageObj_CountMarks` / `FPDFPageObj_GetMark` / etc.
1879    pub marks: Vec<ContentMark>,
1880    /// Optional clipping path attached to this object.
1881    ///
1882    /// Corresponds to `FPDFPageObj_GetClipPath` / `FPDFPageObj_TransformClipPath`.
1883    pub clip_path: Option<ClipPath>,
1884}
1885
1886impl Default for PathObject {
1887    fn default() -> Self {
1888        Self {
1889            segments: Vec::new(),
1890            matrix: Matrix::identity(),
1891            fill_color: None,
1892            stroke_color: None,
1893            line_width: 1.0,
1894            line_cap: 0,
1895            line_join: 0,
1896            fill_mode: FillMode::None,
1897            dash_pattern: None,
1898            blend_mode: None,
1899            active: true,
1900            marks: Vec::new(),
1901            clip_path: None,
1902        }
1903    }
1904}
1905
1906impl PathObject {
1907    /// Sets the fill color.
1908    ///
1909    /// Corresponds to `FPDFPageObj_SetFillColor`.
1910    pub fn set_fill_color(&mut self, color: Color) {
1911        self.fill_color = Some(color);
1912    }
1913
1914    /// Returns the fill color, if set (upstream `FPDFPageObj_GetFillColor`).
1915    pub fn fill_color(&self) -> Option<&Color> {
1916        self.fill_color.as_ref()
1917    }
1918
1919    /// Upstream-aligned alias for [`fill_color()`](Self::fill_color).
1920    ///
1921    /// Corresponds to `FPDFPageObj_GetFillColor`.
1922    #[inline]
1923    pub fn page_obj_get_fill_color(&self) -> Option<&Color> {
1924        self.fill_color()
1925    }
1926
1927    /// Non-upstream alias — use [`page_obj_get_fill_color()`](Self::page_obj_get_fill_color).
1928    ///
1929    /// Corresponds to `FPDFPageObj_GetFillColor`.
1930    #[deprecated(
1931        note = "use `page_obj_get_fill_color()` — matches upstream `FPDFPageObj_GetFillColor`"
1932    )]
1933    #[inline]
1934    pub fn get_fill_color(&self) -> Option<&Color> {
1935        self.fill_color()
1936    }
1937
1938    /// Sets the stroke color.
1939    ///
1940    /// Corresponds to `FPDFPageObj_SetStrokeColor`.
1941    pub fn set_stroke_color(&mut self, color: Color) {
1942        self.stroke_color = Some(color);
1943    }
1944
1945    /// Returns the stroke color, if set (upstream `FPDFPageObj_GetStrokeColor`).
1946    pub fn stroke_color(&self) -> Option<&Color> {
1947        self.stroke_color.as_ref()
1948    }
1949
1950    /// Upstream-aligned alias for [`stroke_color()`](Self::stroke_color).
1951    ///
1952    /// Corresponds to `FPDFPageObj_GetStrokeColor`.
1953    #[inline]
1954    pub fn page_obj_get_stroke_color(&self) -> Option<&Color> {
1955        self.stroke_color()
1956    }
1957
1958    /// Non-upstream alias — use [`page_obj_get_stroke_color()`](Self::page_obj_get_stroke_color).
1959    ///
1960    /// Corresponds to `FPDFPageObj_GetStrokeColor`.
1961    #[deprecated(
1962        note = "use `page_obj_get_stroke_color()` — matches upstream `FPDFPageObj_GetStrokeColor`"
1963    )]
1964    #[inline]
1965    pub fn get_stroke_color(&self) -> Option<&Color> {
1966        self.stroke_color()
1967    }
1968
1969    /// Sets the stroke (line) width.
1970    ///
1971    /// Corresponds to `FPDFPageObj_SetStrokeWidth`.
1972    pub fn set_line_width(&mut self, width: f32) {
1973        self.line_width = width;
1974    }
1975
1976    /// Upstream-aligned alias for [`Self::set_line_width()`].
1977    ///
1978    /// Corresponds to `FPDFPageObj_SetStrokeWidth`.
1979    #[inline]
1980    pub fn page_obj_set_stroke_width(&mut self, width: f32) {
1981        self.set_line_width(width);
1982    }
1983
1984    /// Non-upstream alias — use [`page_obj_set_stroke_width()`](Self::page_obj_set_stroke_width).
1985    ///
1986    /// Corresponds to `FPDFPageObj_SetStrokeWidth`.
1987    #[deprecated(
1988        note = "use `page_obj_set_stroke_width()` — matches upstream `FPDFPageObj_SetStrokeWidth`"
1989    )]
1990    #[inline]
1991    pub fn set_stroke_width(&mut self, width: f32) {
1992        self.set_line_width(width);
1993    }
1994
1995    /// Returns the stroke (line) width (upstream `FPDFPageObj_GetStrokeWidth`).
1996    pub fn line_width(&self) -> f32 {
1997        self.line_width
1998    }
1999
2000    /// Non-upstream alias — use [`page_obj_get_stroke_width()`](Self::page_obj_get_stroke_width).
2001    ///
2002    /// There is no `FPDFPageObj_GetLineWidth`; the upstream function is
2003    /// `FPDFPageObj_GetStrokeWidth`.
2004    #[deprecated(
2005        note = "use `page_obj_get_stroke_width()` — matches upstream FPDFPageObj_GetStrokeWidth"
2006    )]
2007    #[inline]
2008    pub fn get_line_width(&self) -> f32 {
2009        self.line_width()
2010    }
2011
2012    /// Upstream-aligned alias for [`line_width()`](Self::line_width).
2013    ///
2014    /// Corresponds to `FPDFPageObj_GetStrokeWidth`.
2015    #[inline]
2016    pub fn page_obj_get_stroke_width(&self) -> f32 {
2017        self.line_width()
2018    }
2019
2020    /// Non-upstream alias — use [`page_obj_get_stroke_width()`](Self::page_obj_get_stroke_width).
2021    ///
2022    /// Corresponds to `FPDFPageObj_GetStrokeWidth`.
2023    #[deprecated(
2024        note = "use `page_obj_get_stroke_width()` — matches upstream `FPDFPageObj_GetStrokeWidth`"
2025    )]
2026    #[inline]
2027    pub fn get_stroke_width(&self) -> f32 {
2028        self.line_width()
2029    }
2030
2031    /// Sets the line join style (0=miter, 1=round, 2=bevel).
2032    ///
2033    /// Corresponds to `FPDFPageObj_SetLineJoin`.
2034    pub fn set_line_join(&mut self, join: u8) {
2035        self.line_join = join;
2036    }
2037
2038    /// Returns the line join style (upstream `FPDFPageObj_GetLineJoin`).
2039    pub fn line_join(&self) -> u8 {
2040        self.line_join
2041    }
2042
2043    /// Upstream-aligned alias for [`line_join()`](Self::line_join).
2044    ///
2045    /// Corresponds to `FPDFPageObj_GetLineJoin`.
2046    #[inline]
2047    pub fn page_obj_get_line_join(&self) -> u8 {
2048        self.line_join()
2049    }
2050
2051    /// Non-upstream alias — use [`page_obj_get_line_join()`](Self::page_obj_get_line_join).
2052    ///
2053    /// Corresponds to `FPDFPageObj_GetLineJoin`.
2054    #[deprecated(
2055        note = "use `page_obj_get_line_join()` — matches upstream `FPDFPageObj_GetLineJoin`"
2056    )]
2057    #[inline]
2058    pub fn get_line_join(&self) -> u8 {
2059        self.line_join()
2060    }
2061
2062    /// Sets the line cap style (0=butt, 1=round, 2=square).
2063    ///
2064    /// Corresponds to `FPDFPageObj_SetLineCap`.
2065    pub fn set_line_cap(&mut self, cap: u8) {
2066        self.line_cap = cap;
2067    }
2068
2069    /// Returns the line cap style (upstream `FPDFPageObj_GetLineCap`).
2070    pub fn line_cap(&self) -> u8 {
2071        self.line_cap
2072    }
2073
2074    /// Upstream-aligned alias for [`line_cap()`](Self::line_cap).
2075    ///
2076    /// Corresponds to `FPDFPageObj_GetLineCap`.
2077    #[inline]
2078    pub fn page_obj_get_line_cap(&self) -> u8 {
2079        self.line_cap()
2080    }
2081
2082    /// Non-upstream alias — use [`page_obj_get_line_cap()`](Self::page_obj_get_line_cap).
2083    ///
2084    /// Corresponds to `FPDFPageObj_GetLineCap`.
2085    #[deprecated(
2086        note = "use `page_obj_get_line_cap()` — matches upstream `FPDFPageObj_GetLineCap`"
2087    )]
2088    #[inline]
2089    pub fn get_line_cap(&self) -> u8 {
2090        self.line_cap()
2091    }
2092
2093    // -----------------------------------------------------------------------
2094    // Dash pattern API  (FPDFPath_GetDashPhase / GetDashCount / GetDashArray /
2095    //                    FPDFPath_SetDashArray / SetDashPhase)
2096    // -----------------------------------------------------------------------
2097
2098    /// Returns the current dash pattern, if any.
2099    ///
2100    /// Corresponds to `FPDFPath_GetDashArray` + `FPDFPath_GetDashPhase`.
2101    pub fn dash_pattern(&self) -> Option<&DashPattern> {
2102        self.dash_pattern.as_ref()
2103    }
2104
2105    /// Sets or clears the dash pattern.
2106    ///
2107    /// Pass `None` to revert to a solid stroke.
2108    ///
2109    /// Corresponds to `FPDFPath_SetDashArray`.
2110    pub fn set_dash_pattern(&mut self, pattern: Option<DashPattern>) {
2111        self.dash_pattern = pattern;
2112    }
2113
2114    /// Returns the dash phase offset.
2115    ///
2116    /// Returns `0.0` when no dash pattern is set.
2117    ///
2118    /// Corresponds to `FPDFPath_GetDashPhase`.
2119    pub fn dash_phase(&self) -> f32 {
2120        self.dash_pattern.as_ref().map(|d| d.phase).unwrap_or(0.0)
2121    }
2122
2123    /// Upstream-aligned alias for [`dash_phase()`](Self::dash_phase).
2124    ///
2125    /// Corresponds to `FPDFPath_GetDashPhase`.
2126    #[inline]
2127    pub fn path_get_dash_phase(&self) -> f32 {
2128        self.dash_phase()
2129    }
2130
2131    /// Non-upstream alias — use [`path_get_dash_phase()`](Self::path_get_dash_phase).
2132    ///
2133    /// Corresponds to `FPDFPath_GetDashPhase`.
2134    #[deprecated(note = "use `path_get_dash_phase()` — matches upstream `FPDFPath_GetDashPhase`")]
2135    #[inline]
2136    pub fn get_dash_phase(&self) -> f32 {
2137        self.dash_phase()
2138    }
2139
2140    /// Sets the dash phase on the existing dash pattern.
2141    ///
2142    /// If no dash pattern is set, this is a no-op.
2143    ///
2144    /// Corresponds to `FPDFPath_SetDashPhase`.
2145    pub fn set_dash_phase(&mut self, phase: f32) {
2146        if let Some(ref mut d) = self.dash_pattern {
2147            d.phase = phase;
2148        }
2149    }
2150
2151    /// Returns the number of elements in the dash array.
2152    ///
2153    /// Returns `0` when no dash pattern is set.
2154    ///
2155    /// Corresponds to `FPDFPageObj_GetDashCount`.
2156    pub fn dash_count(&self) -> usize {
2157        self.dash_pattern
2158            .as_ref()
2159            .map(|d| d.array.len())
2160            .unwrap_or(0)
2161    }
2162
2163    /// Upstream-aligned alias for [`dash_count()`](Self::dash_count).
2164    ///
2165    /// Corresponds to `FPDFPageObj_GetDashCount`.
2166    #[inline]
2167    pub fn page_obj_get_dash_count(&self) -> usize {
2168        self.dash_count()
2169    }
2170
2171    /// Non-upstream alias — use [`page_obj_get_dash_count()`](Self::page_obj_get_dash_count).
2172    ///
2173    /// Corresponds to `FPDFPageObj_GetDashCount`.
2174    #[deprecated(
2175        note = "use `page_obj_get_dash_count()` — matches upstream `FPDFPageObj_GetDashCount`"
2176    )]
2177    #[inline]
2178    pub fn get_dash_count(&self) -> usize {
2179        self.dash_count()
2180    }
2181
2182    /// Returns the dash array (alternating dash/gap lengths).
2183    ///
2184    /// Returns an empty slice when no dash pattern is set.
2185    ///
2186    /// Corresponds to `FPDFPath_GetDashArray`.
2187    pub fn dash_array(&self) -> &[f32] {
2188        self.dash_pattern
2189            .as_ref()
2190            .map(|d| d.array.as_slice())
2191            .unwrap_or(&[])
2192    }
2193
2194    /// Upstream-aligned alias for [`dash_array()`](Self::dash_array).
2195    ///
2196    /// Corresponds to `FPDFPath_GetDashArray`.
2197    #[inline]
2198    pub fn path_get_dash_array(&self) -> &[f32] {
2199        self.dash_array()
2200    }
2201
2202    /// Non-upstream alias — use [`path_get_dash_array()`](Self::path_get_dash_array).
2203    ///
2204    /// Corresponds to `FPDFPath_GetDashArray`.
2205    #[deprecated(note = "use `path_get_dash_array()` — matches upstream `FPDFPath_GetDashArray`")]
2206    #[inline]
2207    pub fn get_dash_array(&self) -> &[f32] {
2208        self.dash_array()
2209    }
2210
2211    /// Sets the dash array and phase, replacing any existing dash pattern.
2212    ///
2213    /// Passing an empty array clears the dash pattern (solid line).
2214    ///
2215    /// Corresponds to `FPDFPath_SetDashArray`.
2216    pub fn set_dash_array(&mut self, array: Vec<f32>, phase: f32) {
2217        if array.is_empty() {
2218            self.dash_pattern = None;
2219        } else {
2220            self.dash_pattern = Some(DashPattern { array, phase });
2221        }
2222    }
2223
2224    // -----------------------------------------------------------------------
2225    // Blend mode  (FPDFPageObj_GetBlendMode / FPDFPageObj_SetBlendMode)
2226    // -----------------------------------------------------------------------
2227
2228    /// Returns the blend mode for this object.
2229    ///
2230    /// Corresponds to `FPDFPageObj_GetBlendMode`.
2231    pub fn blend_mode(&self) -> Option<BlendMode> {
2232        self.blend_mode
2233    }
2234
2235    /// Upstream-aligned alias for [`blend_mode()`](Self::blend_mode).
2236    ///
2237    /// Corresponds to `FPDFPageObj_GetBlendMode`.
2238    #[inline]
2239    pub fn page_obj_get_blend_mode(&self) -> Option<BlendMode> {
2240        self.blend_mode()
2241    }
2242
2243    /// Non-upstream alias — use [`page_obj_get_blend_mode()`](Self::page_obj_get_blend_mode).
2244    ///
2245    /// Corresponds to `FPDFPageObj_GetBlendMode`.
2246    #[deprecated(
2247        note = "use `page_obj_get_blend_mode()` — matches upstream `FPDFPageObj_GetBlendMode`"
2248    )]
2249    #[inline]
2250    pub fn get_blend_mode(&self) -> Option<BlendMode> {
2251        self.blend_mode()
2252    }
2253
2254    /// Sets the blend mode for this object.
2255    ///
2256    /// Corresponds to `FPDFPageObj_SetBlendMode`.
2257    pub fn set_blend_mode(&mut self, mode: Option<BlendMode>) {
2258        self.blend_mode = mode;
2259    }
2260
2261    // -----------------------------------------------------------------------
2262    // Content marks  (FPDFPageObj_CountMarks / GetMark / AddMark / RemoveMark)
2263    // -----------------------------------------------------------------------
2264
2265    /// Return the number of content marks on this object.
2266    ///
2267    /// Corresponds to `FPDFPageObj_CountMarks`.
2268    pub fn mark_count(&self) -> usize {
2269        self.marks.len()
2270    }
2271
2272    /// Upstream-aligned alias for [`mark_count()`](Self::mark_count).
2273    ///
2274    /// Corresponds to `FPDFPageObj_CountMarks`.
2275    #[inline]
2276    pub fn page_obj_count_marks(&self) -> usize {
2277        self.mark_count()
2278    }
2279
2280    /// Non-upstream alias — use [`page_obj_count_marks()`](Self::page_obj_count_marks).
2281    ///
2282    /// Corresponds to `FPDFPageObj_CountMarks`.
2283    #[deprecated(note = "use `page_obj_count_marks()` — matches upstream `FPDFPageObj_CountMarks`")]
2284    #[inline]
2285    pub fn count_marks(&self) -> usize {
2286        self.mark_count()
2287    }
2288
2289    /// Return the content mark at the given index, or `None` if out of range.
2290    ///
2291    /// Corresponds to `FPDFPageObj_GetMark`.
2292    pub fn mark(&self, index: usize) -> Option<&ContentMark> {
2293        self.marks.get(index)
2294    }
2295
2296    /// Upstream-aligned alias for [`mark()`](Self::mark).
2297    ///
2298    /// Corresponds to `FPDFPageObj_GetMark`.
2299    #[inline]
2300    pub fn page_obj_get_mark(&self, index: usize) -> Option<&ContentMark> {
2301        self.mark(index)
2302    }
2303
2304    /// Non-upstream alias — use [`page_obj_get_mark()`](Self::page_obj_get_mark).
2305    ///
2306    /// Corresponds to `FPDFPageObj_GetMark`.
2307    #[deprecated(note = "use `page_obj_get_mark()` — matches upstream `FPDFPageObj_GetMark`")]
2308    #[inline]
2309    pub fn get_mark(&self, index: usize) -> Option<&ContentMark> {
2310        self.mark(index)
2311    }
2312
2313    /// Add a new content mark with the given name and return a mutable
2314    /// reference to it.
2315    ///
2316    /// Corresponds to `FPDFPageObj_AddMark`.
2317    pub fn add_mark(&mut self, name: impl Into<String>) -> &mut ContentMark {
2318        self.marks.push(ContentMark::new(name));
2319        self.marks.last_mut().unwrap()
2320    }
2321
2322    /// Remove the content mark at the given index.
2323    ///
2324    /// Returns `true` if the index was valid and the mark was removed, `false`
2325    /// if out of range.
2326    ///
2327    /// Corresponds to `FPDFPageObj_RemoveMark`.
2328    pub fn remove_mark(&mut self, index: usize) -> bool {
2329        if index < self.marks.len() {
2330            self.marks.remove(index);
2331            true
2332        } else {
2333            false
2334        }
2335    }
2336
2337    /// Return a slice of all content marks on this object.
2338    pub fn marks(&self) -> &[ContentMark] {
2339        &self.marks
2340    }
2341
2342    /// Return the marked content ID (`/MCID`) from the first mark that has one,
2343    /// or `None` if no mark carries an MCID.
2344    ///
2345    /// Corresponds to `FPDFPageObj_GetMarkedContentID`.
2346    pub fn marked_content_id(&self) -> Option<i64> {
2347        self.marks.iter().find_map(|m| m.marked_content_id())
2348    }
2349
2350    /// Upstream-aligned alias for [`marked_content_id()`](Self::marked_content_id).
2351    ///
2352    /// Corresponds to `FPDFPageObj_GetMarkedContentID`.
2353    #[inline]
2354    pub fn page_obj_get_marked_content_id(&self) -> Option<i64> {
2355        self.marked_content_id()
2356    }
2357
2358    /// Non-upstream alias — use [`page_obj_get_marked_content_id()`](Self::page_obj_get_marked_content_id).
2359    ///
2360    /// Corresponds to `FPDFPageObj_GetMarkedContentID`.
2361    #[deprecated(
2362        note = "use `page_obj_get_marked_content_id()` — matches upstream `FPDFPageObj_GetMarkedContentID`"
2363    )]
2364    #[inline]
2365    pub fn get_marked_content_id(&self) -> Option<i64> {
2366        self.marked_content_id()
2367    }
2368
2369    /// Returns `true` if this path object has any transparency.
2370    ///
2371    /// A path is considered transparent when it has a non-`Normal` blend mode.
2372    ///
2373    /// Corresponds to `FPDFPageObj_HasTransparency`.
2374    pub fn has_transparency(&self) -> bool {
2375        matches!(self.blend_mode, Some(bm) if bm != BlendMode::Normal)
2376    }
2377
2378    /// Non-upstream convenience alias for [`has_transparency()`](Self::has_transparency).
2379    ///
2380    /// Prefer [`has_transparency()`](Self::has_transparency), which matches
2381    /// the upstream `FPDFPageObj_HasTransparency` name exactly.
2382    #[deprecated(note = "use `has_transparency()` — matches upstream FPDFPageObj_HasTransparency")]
2383    #[inline]
2384    pub fn is_transparent(&self) -> bool {
2385        self.has_transparency()
2386    }
2387
2388    /// Returns the PDFium page-object-type constant for path objects: `2`.
2389    ///
2390    /// Corresponds to `FPDFPageObj_GetType` returning `FPDF_PAGEOBJ_PATH`.
2391    pub fn object_type(&self) -> u32 {
2392        2
2393    }
2394
2395    /// Non-upstream alias — use [`page_obj_get_type()`](Self::page_obj_get_type).
2396    ///
2397    /// The actual upstream function is `FPDFPageObj_GetType`; there is no
2398    /// `FPDFPageObj_GetObjectType`.
2399    #[deprecated(note = "use `page_obj_get_type()` — matches upstream `FPDFPageObj_GetType`")]
2400    #[inline]
2401    pub fn get_object_type(&self) -> u32 {
2402        self.object_type()
2403    }
2404
2405    /// Upstream-aligned alias for [`object_type()`](Self::object_type).
2406    ///
2407    /// Corresponds to `FPDFPageObj_GetType`.
2408    #[inline]
2409    pub fn page_obj_get_type(&self) -> u32 {
2410        self.object_type()
2411    }
2412
2413    #[deprecated(note = "use `page_obj_get_type()` — matches upstream `FPDFPageObj_GetType`")]
2414    #[inline]
2415    pub fn get_type(&self) -> u32 {
2416        self.object_type()
2417    }
2418
2419    /// Applies a matrix transform to all path segments.
2420    ///
2421    /// Each control point is transformed by `matrix`. For `Rect` segments,
2422    /// the origin is transformed and width/height are uniformly scaled.
2423    ///
2424    /// Corresponds to `FPDFPageObj_Transform`.
2425    pub fn transform(&mut self, matrix: &Matrix) {
2426        use rpdfium_core::Point;
2427        for seg in &mut self.segments {
2428            match seg {
2429                PathSegment::MoveTo(x, y) | PathSegment::LineTo(x, y) => {
2430                    let p = matrix.transform_point(Point::new(*x, *y));
2431                    *x = p.x;
2432                    *y = p.y;
2433                }
2434                PathSegment::CurveTo(x1, y1, x2, y2, x3, y3) => {
2435                    let p1 = matrix.transform_point(Point::new(*x1, *y1));
2436                    let p2 = matrix.transform_point(Point::new(*x2, *y2));
2437                    let p3 = matrix.transform_point(Point::new(*x3, *y3));
2438                    *x1 = p1.x;
2439                    *y1 = p1.y;
2440                    *x2 = p2.x;
2441                    *y2 = p2.y;
2442                    *x3 = p3.x;
2443                    *y3 = p3.y;
2444                }
2445                PathSegment::Rect(x, y, w, h) => {
2446                    let p = matrix.transform_point(Point::new(*x, *y));
2447                    let scale_x = (matrix.a * matrix.a + matrix.b * matrix.b).sqrt();
2448                    let scale_y = (matrix.c * matrix.c + matrix.d * matrix.d).sqrt();
2449                    *x = p.x;
2450                    *y = p.y;
2451                    *w *= scale_x;
2452                    *h *= scale_y;
2453                }
2454                PathSegment::Close => {}
2455            }
2456        }
2457    }
2458
2459    /// Returns the transformation matrix for this path object.
2460    ///
2461    /// For path objects, coordinates are stored in page space (absolute),
2462    /// so this returns the identity matrix unless a matrix was explicitly
2463    /// assigned via the `matrix` field.
2464    ///
2465    /// Corresponds to `FPDFPageObj_GetMatrix`.
2466    pub fn matrix(&self) -> Matrix {
2467        self.matrix
2468    }
2469
2470    /// Upstream-aligned alias for [`matrix()`](Self::matrix).
2471    ///
2472    /// Corresponds to `FPDFPageObj_GetMatrix`.
2473    #[inline]
2474    pub fn page_obj_get_matrix(&self) -> Matrix {
2475        self.matrix()
2476    }
2477
2478    #[deprecated(note = "use `page_obj_get_matrix()` — matches upstream `FPDFPageObj_GetMatrix`")]
2479    #[inline]
2480    pub fn get_matrix(&self) -> Matrix {
2481        self.matrix()
2482    }
2483
2484    /// Returns the number of path segments in this path object.
2485    ///
2486    /// Corresponds to `FPDFPath_CountSegments`.
2487    pub fn segment_count(&self) -> usize {
2488        self.segments.len()
2489    }
2490
2491    /// Upstream-aligned alias for [`segment_count()`](Self::segment_count).
2492    ///
2493    /// Corresponds to `FPDFPath_CountSegments`.
2494    #[inline]
2495    pub fn path_count_segments(&self) -> usize {
2496        self.segment_count()
2497    }
2498
2499    #[deprecated(note = "use `path_count_segments()` — matches upstream `FPDFPath_CountSegments`")]
2500    #[inline]
2501    pub fn count_segments(&self) -> usize {
2502        self.segment_count()
2503    }
2504
2505    /// Returns the draw mode of this path object as `(fill_mode, stroke)`.
2506    ///
2507    /// `fill_mode` is the current [`FillMode`]; `stroke` is `true` when this
2508    /// path has a stroke color set.
2509    ///
2510    /// Corresponds to `FPDFPath_GetDrawMode`.
2511    pub fn draw_mode(&self) -> (FillMode, bool) {
2512        (self.fill_mode, self.stroke_color.is_some())
2513    }
2514
2515    /// Upstream-aligned alias for [`draw_mode()`](Self::draw_mode).
2516    ///
2517    /// Corresponds to `FPDFPath_GetDrawMode`.
2518    #[inline]
2519    pub fn path_get_draw_mode(&self) -> (FillMode, bool) {
2520        self.draw_mode()
2521    }
2522
2523    #[deprecated(note = "use `path_get_draw_mode()` — matches upstream `FPDFPath_GetDrawMode`")]
2524    #[inline]
2525    pub fn get_draw_mode(&self) -> (FillMode, bool) {
2526        self.draw_mode()
2527    }
2528
2529    /// Sets the draw mode of this path object.
2530    ///
2531    /// `fill_mode` controls whether and how the path interior is filled.
2532    /// `stroke` controls whether a stroke color is retained; when `false` the
2533    /// existing stroke color is cleared.
2534    ///
2535    /// Corresponds to `FPDFPath_SetDrawMode`.
2536    pub fn set_draw_mode(&mut self, fill_mode: FillMode, stroke: bool) {
2537        self.fill_mode = fill_mode;
2538        if !stroke {
2539            self.stroke_color = None;
2540        }
2541    }
2542
2543    /// Upstream-aligned alias for [`set_draw_mode()`](Self::set_draw_mode).
2544    ///
2545    /// Corresponds to `FPDFPath_SetDrawMode`.
2546    #[inline]
2547    pub fn path_set_draw_mode(&mut self, fill_mode: FillMode, stroke: bool) {
2548        self.set_draw_mode(fill_mode, stroke)
2549    }
2550
2551    /// Returns the fill mode for this path object.
2552    ///
2553    /// Corresponds to the `fillmode` out-parameter of `FPDFPath_GetDrawMode`.
2554    pub fn fill_mode(&self) -> FillMode {
2555        self.fill_mode
2556    }
2557
2558    /// Sets the fill mode for this path object.
2559    ///
2560    /// Corresponds to the `fillmode` parameter of `FPDFPath_SetDrawMode`.
2561    pub fn set_fill_mode(&mut self, mode: FillMode) {
2562        self.fill_mode = mode;
2563    }
2564
2565    /// Returns whether this path is stroked (i.e. has a stroke color set).
2566    ///
2567    /// Corresponds to the `stroke` out-parameter of `FPDFPath_GetDrawMode`.
2568    pub fn is_stroked(&self) -> bool {
2569        self.stroke_color.is_some()
2570    }
2571
2572    /// Corresponds to `FPDFPageObj_GetBounds`.
2573    pub fn bounds(&self) -> Option<rpdfium_core::Rect> {
2574        let mut min_x = f64::MAX;
2575        let mut min_y = f64::MAX;
2576        let mut max_x = f64::MIN;
2577        let mut max_y = f64::MIN;
2578        let mut has_point = false;
2579
2580        for seg in &self.segments {
2581            match seg {
2582                PathSegment::MoveTo(x, y) | PathSegment::LineTo(x, y) => {
2583                    min_x = min_x.min(*x);
2584                    max_x = max_x.max(*x);
2585                    min_y = min_y.min(*y);
2586                    max_y = max_y.max(*y);
2587                    has_point = true;
2588                }
2589                PathSegment::CurveTo(x1, y1, x2, y2, x3, y3) => {
2590                    for &(x, y) in &[(*x1, *y1), (*x2, *y2), (*x3, *y3)] {
2591                        min_x = min_x.min(x);
2592                        max_x = max_x.max(x);
2593                        min_y = min_y.min(y);
2594                        max_y = max_y.max(y);
2595                    }
2596                    has_point = true;
2597                }
2598                PathSegment::Rect(x, y, w, h) => {
2599                    min_x = min_x.min(*x);
2600                    max_x = max_x.max(*x + *w);
2601                    min_y = min_y.min(*y);
2602                    max_y = max_y.max(*y + *h);
2603                    has_point = true;
2604                }
2605                PathSegment::Close => {}
2606            }
2607        }
2608
2609        if has_point {
2610            Some(rpdfium_core::Rect {
2611                left: min_x,
2612                bottom: min_y,
2613                right: max_x,
2614                top: max_y,
2615            })
2616        } else {
2617            None
2618        }
2619    }
2620
2621    /// Returns the path segment at `index`, or `None` if out of range.
2622    ///
2623    /// Corresponds to `FPDFPath_GetPathSegment`.
2624    pub fn segment(&self, index: usize) -> Option<&PathSegment> {
2625        self.segments.get(index)
2626    }
2627
2628    /// Upstream-aligned alias for [`segment()`](Self::segment).
2629    ///
2630    /// Corresponds to `FPDFPath_GetPathSegment`.
2631    #[inline]
2632    pub fn path_get_path_segment(&self, index: usize) -> Option<&PathSegment> {
2633        self.segment(index)
2634    }
2635
2636    #[deprecated(
2637        note = "use `path_get_path_segment()` — matches upstream `FPDFPath_GetPathSegment`"
2638    )]
2639    #[inline]
2640    pub fn get_path_segment(&self, index: usize) -> Option<&PathSegment> {
2641        self.segment(index)
2642    }
2643
2644    /// Creates a new path object with an initial MoveTo at `(x, y)`.
2645    ///
2646    /// The path starts with default stroke (black, 1pt) and no fill.
2647    /// Use the builder-style `move_to`, `line_to`, `bezier_to`, and `close`
2648    /// methods to append further segments.
2649    ///
2650    /// Corresponds to `FPDFPageObj_CreateNewPath(x, y)`.
2651    pub fn create_at(x: f64, y: f64) -> Self {
2652        let mut path = PathObject::default();
2653        path.segments.push(PathSegment::MoveTo(x, y));
2654        path
2655    }
2656
2657    /// Upstream-aligned alias for [`create_at()`](Self::create_at).
2658    ///
2659    /// Corresponds to `FPDFPageObj_CreateNewPath`.
2660    #[inline]
2661    pub fn page_obj_create_new_path(x: f64, y: f64) -> Self {
2662        Self::create_at(x, y)
2663    }
2664
2665    /// Creates a new rectangular path object at `(x, y)` with `width × height`.
2666    ///
2667    /// The rectangle is encoded as a closed `Rect` segment matching upstream
2668    /// `CPDF_PathObject::AppendRect(x, y, x+w, y+h)`. Default stroke is black,
2669    /// 1pt; no fill is set.
2670    ///
2671    /// Corresponds to `FPDFPageObj_CreateNewRect(x, y, w, h)`.
2672    pub fn create_rect(x: f64, y: f64, width: f64, height: f64) -> Self {
2673        let mut path = PathObject::default();
2674        path.segments.push(PathSegment::Rect(x, y, width, height));
2675        path
2676    }
2677
2678    /// Upstream-aligned alias for [`create_rect()`](Self::create_rect).
2679    ///
2680    /// Corresponds to `FPDFPageObj_CreateNewRect`.
2681    #[inline]
2682    pub fn page_obj_create_new_rect(x: f64, y: f64, width: f64, height: f64) -> Self {
2683        Self::create_rect(x, y, width, height)
2684    }
2685
2686    /// Append a MoveTo segment to this path.
2687    ///
2688    /// Corresponds to `FPDFPath_MoveTo`.
2689    pub fn move_to(&mut self, x: f64, y: f64) -> &mut Self {
2690        self.segments.push(PathSegment::MoveTo(x, y));
2691        self
2692    }
2693
2694    /// Upstream-aligned alias for [`move_to()`](Self::move_to).
2695    ///
2696    /// Corresponds to `FPDFPath_MoveTo`.
2697    #[inline]
2698    pub fn path_move_to(&mut self, x: f64, y: f64) -> &mut Self {
2699        self.move_to(x, y)
2700    }
2701
2702    /// Append a LineTo segment to this path.
2703    ///
2704    /// Corresponds to `FPDFPath_LineTo`.
2705    pub fn line_to(&mut self, x: f64, y: f64) -> &mut Self {
2706        self.segments.push(PathSegment::LineTo(x, y));
2707        self
2708    }
2709
2710    /// Upstream-aligned alias for [`line_to()`](Self::line_to).
2711    ///
2712    /// Corresponds to `FPDFPath_LineTo`.
2713    #[inline]
2714    pub fn path_line_to(&mut self, x: f64, y: f64) -> &mut Self {
2715        self.line_to(x, y)
2716    }
2717
2718    /// Append a cubic Bézier curve segment to this path.
2719    ///
2720    /// `(x1, y1)` and `(x2, y2)` are the control points; `(x3, y3)` is the
2721    /// end point.
2722    ///
2723    /// Corresponds to `FPDFPath_BezierTo`.
2724    pub fn bezier_to(&mut self, x1: f64, y1: f64, x2: f64, y2: f64, x3: f64, y3: f64) -> &mut Self {
2725        self.segments
2726            .push(PathSegment::CurveTo(x1, y1, x2, y2, x3, y3));
2727        self
2728    }
2729
2730    /// Upstream-aligned alias for [`bezier_to()`](Self::bezier_to).
2731    ///
2732    /// Corresponds to `FPDFPath_BezierTo`.
2733    #[inline]
2734    pub fn path_bezier_to(
2735        &mut self,
2736        x1: f64,
2737        y1: f64,
2738        x2: f64,
2739        y2: f64,
2740        x3: f64,
2741        y3: f64,
2742    ) -> &mut Self {
2743        self.bezier_to(x1, y1, x2, y2, x3, y3)
2744    }
2745
2746    /// Close the current subpath.
2747    ///
2748    /// Corresponds to `FPDFPath_Close`.
2749    pub fn close(&mut self) -> &mut Self {
2750        self.segments.push(PathSegment::Close);
2751        self
2752    }
2753
2754    /// Upstream-aligned alias for [`close()`](Self::close).
2755    ///
2756    /// Corresponds to `FPDFPath_Close`.
2757    #[inline]
2758    pub fn path_close(&mut self) -> &mut Self {
2759        self.close()
2760    }
2761}
2762
2763/// A text object with font, position, and color.
2764#[derive(Debug, Clone)]
2765pub struct TextObject {
2766    /// Font name (must match a resource font name).
2767    pub font_name: Name,
2768    /// Font size in points.
2769    pub font_size: f32,
2770    /// Transformation matrix (includes position).
2771    pub matrix: Matrix,
2772    /// The text string to render.
2773    pub text: String,
2774    /// Fill color.
2775    pub fill_color: Option<Color>,
2776    /// Stroke color.
2777    pub stroke_color: Option<Color>,
2778    /// Text rendering mode.
2779    pub rendering_mode: TextRenderingMode,
2780    /// Character spacing.
2781    pub char_spacing: f32,
2782    /// Word spacing.
2783    pub word_spacing: f32,
2784    /// Object ID of the registered font dictionary, if created via
2785    /// [`TextObject::with_font`].  `None` means the legacy path where the
2786    /// font resource is resolved by name alone.
2787    pub(crate) font_object_id: Option<ObjectId>,
2788    /// Font encoding for content stream generation.
2789    ///
2790    /// Controls how Unicode characters in `text` are encoded to PDF byte codes.
2791    /// Defaults to `WinAnsi` (the most common PDF encoding).
2792    pub(crate) encoding: FontEncoding,
2793    /// Blend mode for compositing.
2794    ///
2795    /// Corresponds to `FPDFPageObj_GetBlendMode` / `FPDFPageObj_SetBlendMode`.
2796    pub blend_mode: Option<BlendMode>,
2797    /// Whether the object is active (visible).
2798    ///
2799    /// Corresponds to `FPDFPageObj_GetIsActive` / `FPDFPageObj_SetIsActive`.
2800    pub active: bool,
2801    /// Content marks attached to this object (for tagged PDF / accessibility).
2802    ///
2803    /// Each mark corresponds to a `BDC`…`EMC` pair wrapping this object in
2804    /// the content stream.
2805    ///
2806    /// Corresponds to `FPDFPageObj_CountMarks` / `FPDFPageObj_GetMark` / etc.
2807    pub marks: Vec<ContentMark>,
2808    /// Optional clipping path attached to this object.
2809    ///
2810    /// Corresponds to `FPDFPageObj_GetClipPath` / `FPDFPageObj_TransformClipPath`.
2811    pub clip_path: Option<ClipPath>,
2812}
2813
2814impl Default for TextObject {
2815    fn default() -> Self {
2816        Self {
2817            font_name: Name::from_bytes(b"Helvetica".to_vec()),
2818            font_size: 12.0,
2819            matrix: Matrix::identity(),
2820            text: String::new(),
2821            fill_color: Some(Color::gray(0.0)),
2822            stroke_color: None,
2823            rendering_mode: TextRenderingMode::Fill,
2824            char_spacing: 0.0,
2825            word_spacing: 0.0,
2826            font_object_id: None,
2827            encoding: FontEncoding::WinAnsi,
2828            blend_mode: None,
2829            active: true,
2830            marks: Vec::new(),
2831            clip_path: None,
2832        }
2833    }
2834}
2835
2836impl TextObject {
2837    /// Create a text object using a Standard-14 font name.
2838    ///
2839    /// This is a convenience constructor that sets the font name directly.
2840    /// For precise resource registration, prefer
2841    /// [`TextObject::with_font`] together with
2842    /// [`EditDocument::load_standard_font`](crate::document::EditDocument::load_standard_font).
2843    ///
2844    /// Corresponds to `FPDFPageObj_NewTextObj`.
2845    pub fn with_standard_font(font_name: &str, size: f32) -> Self {
2846        Self {
2847            font_name: Name::from_bytes(font_name.as_bytes().to_vec()),
2848            font_size: size,
2849            ..Default::default()
2850        }
2851    }
2852
2853    /// Create a text object from a [`crate::font_reg::FontRegistration`].
2854    ///
2855    /// Sets `font_object_id` so that content-stream generation uses the exact
2856    /// PDF object ID of the registered font dictionary.
2857    ///
2858    /// Corresponds to `FPDFPageObj_CreateTextObj`.
2859    pub fn with_font(registration: &crate::font_reg::FontRegistration, size: f32) -> Self {
2860        Self {
2861            font_name: Name::from_bytes(registration.base_font_name().as_bytes().to_vec()),
2862            font_size: size,
2863            font_object_id: Some(registration.object_id),
2864            ..Default::default()
2865        }
2866    }
2867
2868    /// Non-upstream alias — use [`with_font()`](Self::with_font).
2869    ///
2870    /// `FPDFPageObj_CreateTextObj` maps to `create_text_obj`; this name
2871    /// (`from_font_registration`) is not an upstream FPDF_* name.
2872    #[deprecated(
2873        note = "use `with_font()` — `from_font_registration` is not an upstream FPDF name"
2874    )]
2875    #[inline]
2876    pub fn from_font_registration(
2877        font_registration: &crate::font_reg::FontRegistration,
2878        font_size: f32,
2879    ) -> Self {
2880        Self::with_font(font_registration, font_size)
2881    }
2882
2883    // -----------------------------------------------------------------------
2884    // Getters  (FPDFTextObj_Get* equivalents)
2885    // -----------------------------------------------------------------------
2886
2887    /// Return the text string stored in this object.
2888    ///
2889    /// Corresponds to `FPDFTextObj_GetText` (without requiring a `text_page`).
2890    pub fn text(&self) -> &str {
2891        &self.text
2892    }
2893
2894    /// Upstream-aligned alias for [`text()`](Self::text).
2895    ///
2896    /// Corresponds to `FPDFTextObj_GetText`.
2897    #[inline]
2898    pub fn text_obj_get_text(&self) -> &str {
2899        self.text()
2900    }
2901
2902    #[deprecated(note = "use `text_obj_get_text()` — matches upstream `FPDFTextObj_GetText`")]
2903    #[inline]
2904    pub fn get_text(&self) -> &str {
2905        self.text()
2906    }
2907
2908    /// Return the font size in points.
2909    ///
2910    /// Corresponds to `FPDFTextObj_GetFontSize`.
2911    pub fn font_size(&self) -> f32 {
2912        self.font_size
2913    }
2914
2915    /// Upstream-aligned alias for [`font_size()`](Self::font_size).
2916    ///
2917    /// Corresponds to `FPDFTextObj_GetFontSize`.
2918    #[inline]
2919    pub fn text_obj_get_font_size(&self) -> f32 {
2920        self.font_size()
2921    }
2922
2923    #[deprecated(
2924        note = "use `text_obj_get_font_size()` — matches upstream `FPDFTextObj_GetFontSize`"
2925    )]
2926    #[inline]
2927    pub fn get_font_size(&self) -> f32 {
2928        self.font_size()
2929    }
2930
2931    /// Return the text rendering mode.
2932    ///
2933    /// Corresponds to `FPDFTextObj_GetTextRenderMode`.
2934    pub fn rendering_mode(&self) -> TextRenderingMode {
2935        self.rendering_mode
2936    }
2937
2938    /// Upstream-aligned alias for [`rendering_mode()`](Self::rendering_mode).
2939    ///
2940    /// Corresponds to `FPDFTextObj_GetTextRenderMode`.
2941    #[inline]
2942    pub fn text_obj_get_text_render_mode(&self) -> TextRenderingMode {
2943        self.rendering_mode()
2944    }
2945
2946    #[deprecated(
2947        note = "use `text_obj_get_text_render_mode()` — matches upstream `FPDFTextObj_GetTextRenderMode`"
2948    )]
2949    #[inline]
2950    pub fn get_text_render_mode(&self) -> TextRenderingMode {
2951        self.rendering_mode()
2952    }
2953
2954    /// Return the font name as a PDF [`Name`].
2955    ///
2956    /// Corresponds to `FPDFTextObj_GetFont` (name only; for full font metrics
2957    /// use the rpdfium-font crate).
2958    pub fn font_name(&self) -> &Name {
2959        &self.font_name
2960    }
2961
2962    /// Upstream-aligned alias for [`font_name()`](Self::font_name).
2963    ///
2964    /// Corresponds to `FPDFTextObj_GetFont`.
2965    #[inline]
2966    pub fn text_obj_get_font(&self) -> &Name {
2967        self.font_name()
2968    }
2969
2970    #[deprecated(note = "use `text_obj_get_font()` — matches upstream `FPDFTextObj_GetFont`")]
2971    #[inline]
2972    pub fn get_font(&self) -> &Name {
2973        self.font_name()
2974    }
2975
2976    /// Sets the fill color.
2977    ///
2978    /// Corresponds to `FPDFPageObj_SetFillColor`.
2979    pub fn set_fill_color(&mut self, color: Color) {
2980        self.fill_color = Some(color);
2981    }
2982
2983    /// Returns the fill color, if set (upstream `FPDFPageObj_GetFillColor`).
2984    pub fn fill_color(&self) -> Option<&Color> {
2985        self.fill_color.as_ref()
2986    }
2987
2988    /// Upstream-aligned alias for [`fill_color()`](Self::fill_color).
2989    ///
2990    /// Corresponds to `FPDFPageObj_GetFillColor`.
2991    #[inline]
2992    pub fn page_obj_get_fill_color(&self) -> Option<&Color> {
2993        self.fill_color()
2994    }
2995
2996    /// Non-upstream alias — use [`page_obj_get_fill_color()`](Self::page_obj_get_fill_color).
2997    ///
2998    /// Corresponds to `FPDFPageObj_GetFillColor`.
2999    #[deprecated(
3000        note = "use `page_obj_get_fill_color()` — matches upstream `FPDFPageObj_GetFillColor`"
3001    )]
3002    #[inline]
3003    pub fn get_fill_color(&self) -> Option<&Color> {
3004        self.fill_color()
3005    }
3006
3007    /// Sets the stroke color.
3008    ///
3009    /// Corresponds to `FPDFPageObj_SetStrokeColor`.
3010    pub fn set_stroke_color(&mut self, color: Color) {
3011        self.stroke_color = Some(color);
3012    }
3013
3014    /// Returns the stroke color, if set (upstream `FPDFPageObj_GetStrokeColor`).
3015    pub fn stroke_color(&self) -> Option<&Color> {
3016        self.stroke_color.as_ref()
3017    }
3018
3019    /// Upstream-aligned alias for [`stroke_color()`](Self::stroke_color).
3020    ///
3021    /// Corresponds to `FPDFPageObj_GetStrokeColor`.
3022    #[inline]
3023    pub fn page_obj_get_stroke_color(&self) -> Option<&Color> {
3024        self.stroke_color()
3025    }
3026
3027    /// Non-upstream alias — use [`page_obj_get_stroke_color()`](Self::page_obj_get_stroke_color).
3028    ///
3029    /// Corresponds to `FPDFPageObj_GetStrokeColor`.
3030    #[deprecated(
3031        note = "use `page_obj_get_stroke_color()` — matches upstream `FPDFPageObj_GetStrokeColor`"
3032    )]
3033    #[inline]
3034    pub fn get_stroke_color(&self) -> Option<&Color> {
3035        self.stroke_color()
3036    }
3037
3038    /// Sets the font size in points.
3039    ///
3040    /// Corresponds to `FPDFTextObj_SetFontSize` (not a direct upstream function,
3041    /// but enables mutation after construction).
3042    pub fn set_font_size(&mut self, size: f32) {
3043        self.font_size = size;
3044    }
3045
3046    /// Sets the text content of this text object.
3047    ///
3048    /// Corresponds to `FPDFTextObj_SetText`.
3049    pub fn set_text(&mut self, text: impl Into<String>) {
3050        self.text = text.into();
3051    }
3052
3053    /// Upstream-aligned alias for [`set_text()`](Self::set_text).
3054    ///
3055    /// Corresponds to `FPDFTextObj_SetText`.
3056    #[inline]
3057    pub fn text_obj_set_text(&mut self, text: impl Into<String>) {
3058        self.set_text(text)
3059    }
3060
3061    /// Non-upstream convenience alias for [`set_text()`](Self::set_text).
3062    ///
3063    /// Prefer [`set_text()`](Self::set_text), which matches the upstream
3064    /// `FPDFText_SetText` name exactly.
3065    #[deprecated(note = "use `set_text()` — matches upstream FPDFText_SetText")]
3066    #[inline]
3067    pub fn set_text_content(&mut self, text: String) {
3068        self.set_text(text);
3069    }
3070
3071    /// Sets the text rendering mode.
3072    ///
3073    /// Corresponds to `FPDFTextObj_SetTextRenderMode`.
3074    pub fn set_rendering_mode(&mut self, mode: TextRenderingMode) {
3075        self.rendering_mode = mode;
3076    }
3077
3078    /// Upstream-aligned alias for [`Self::set_rendering_mode()`].
3079    ///
3080    /// Corresponds to `FPDFTextObj_SetTextRenderMode`.
3081    #[inline]
3082    pub fn text_obj_set_text_render_mode(&mut self, mode: TextRenderingMode) {
3083        self.set_rendering_mode(mode);
3084    }
3085
3086    /// Non-upstream alias — use [`text_obj_set_text_render_mode()`](Self::text_obj_set_text_render_mode).
3087    ///
3088    /// Corresponds to `FPDFTextObj_SetTextRenderMode`.
3089    #[deprecated(
3090        note = "use `text_obj_set_text_render_mode()` — matches upstream `FPDFTextObj_SetTextRenderMode`"
3091    )]
3092    #[inline]
3093    pub fn set_text_render_mode(&mut self, mode: TextRenderingMode) {
3094        self.set_rendering_mode(mode);
3095    }
3096
3097    /// Applies a matrix transform by pre-multiplying into the object's matrix.
3098    ///
3099    /// Corresponds to `FPDFPageObj_Transform`.
3100    pub fn transform(&mut self, m: &Matrix) {
3101        self.matrix = m.pre_concat(&self.matrix);
3102    }
3103
3104    /// Returns the blend mode for this object.
3105    ///
3106    /// Corresponds to `FPDFPageObj_GetBlendMode`.
3107    pub fn blend_mode(&self) -> Option<BlendMode> {
3108        self.blend_mode
3109    }
3110
3111    /// Upstream-aligned alias for [`blend_mode()`](Self::blend_mode).
3112    ///
3113    /// Corresponds to `FPDFPageObj_GetBlendMode`.
3114    #[inline]
3115    pub fn page_obj_get_blend_mode(&self) -> Option<BlendMode> {
3116        self.blend_mode()
3117    }
3118
3119    /// Non-upstream alias — use [`page_obj_get_blend_mode()`](Self::page_obj_get_blend_mode).
3120    ///
3121    /// Corresponds to `FPDFPageObj_GetBlendMode`.
3122    #[deprecated(
3123        note = "use `page_obj_get_blend_mode()` — matches upstream `FPDFPageObj_GetBlendMode`"
3124    )]
3125    #[inline]
3126    pub fn get_blend_mode(&self) -> Option<BlendMode> {
3127        self.blend_mode()
3128    }
3129
3130    /// Sets the blend mode for this object.
3131    ///
3132    /// Corresponds to `FPDFPageObj_SetBlendMode`.
3133    pub fn set_blend_mode(&mut self, mode: Option<BlendMode>) {
3134        self.blend_mode = mode;
3135    }
3136
3137    // -----------------------------------------------------------------------
3138    // Content marks  (FPDFPageObj_CountMarks / GetMark / AddMark / RemoveMark)
3139    // -----------------------------------------------------------------------
3140
3141    /// Return the number of content marks on this object.
3142    ///
3143    /// Corresponds to `FPDFPageObj_CountMarks`.
3144    pub fn mark_count(&self) -> usize {
3145        self.marks.len()
3146    }
3147
3148    /// Upstream-aligned alias for [`mark_count()`](Self::mark_count).
3149    ///
3150    /// Corresponds to `FPDFPageObj_CountMarks`.
3151    #[inline]
3152    pub fn page_obj_count_marks(&self) -> usize {
3153        self.mark_count()
3154    }
3155
3156    /// Non-upstream alias — use [`page_obj_count_marks()`](Self::page_obj_count_marks).
3157    ///
3158    /// Corresponds to `FPDFPageObj_CountMarks`.
3159    #[deprecated(note = "use `page_obj_count_marks()` — matches upstream `FPDFPageObj_CountMarks`")]
3160    #[inline]
3161    pub fn count_marks(&self) -> usize {
3162        self.mark_count()
3163    }
3164
3165    /// Return the content mark at the given index, or `None` if out of range.
3166    ///
3167    /// Corresponds to `FPDFPageObj_GetMark`.
3168    pub fn mark(&self, index: usize) -> Option<&ContentMark> {
3169        self.marks.get(index)
3170    }
3171
3172    /// Upstream-aligned alias for [`mark()`](Self::mark).
3173    ///
3174    /// Corresponds to `FPDFPageObj_GetMark`.
3175    #[inline]
3176    pub fn page_obj_get_mark(&self, index: usize) -> Option<&ContentMark> {
3177        self.mark(index)
3178    }
3179
3180    /// Non-upstream alias — use [`page_obj_get_mark()`](Self::page_obj_get_mark).
3181    ///
3182    /// Corresponds to `FPDFPageObj_GetMark`.
3183    #[deprecated(note = "use `page_obj_get_mark()` — matches upstream `FPDFPageObj_GetMark`")]
3184    #[inline]
3185    pub fn get_mark(&self, index: usize) -> Option<&ContentMark> {
3186        self.mark(index)
3187    }
3188
3189    /// Add a new content mark with the given name and return a mutable
3190    /// reference to it.
3191    ///
3192    /// Corresponds to `FPDFPageObj_AddMark`.
3193    pub fn add_mark(&mut self, name: impl Into<String>) -> &mut ContentMark {
3194        self.marks.push(ContentMark::new(name));
3195        self.marks.last_mut().unwrap()
3196    }
3197
3198    /// Remove the content mark at the given index.
3199    ///
3200    /// Returns `true` if the index was valid and the mark was removed, `false`
3201    /// if out of range.
3202    ///
3203    /// Corresponds to `FPDFPageObj_RemoveMark`.
3204    pub fn remove_mark(&mut self, index: usize) -> bool {
3205        if index < self.marks.len() {
3206            self.marks.remove(index);
3207            true
3208        } else {
3209            false
3210        }
3211    }
3212
3213    /// Return a slice of all content marks on this object.
3214    pub fn marks(&self) -> &[ContentMark] {
3215        &self.marks
3216    }
3217
3218    /// Return the marked content ID (`/MCID`) from the first mark that has one,
3219    /// or `None` if no mark carries an MCID.
3220    ///
3221    /// Corresponds to `FPDFPageObj_GetMarkedContentID`.
3222    pub fn marked_content_id(&self) -> Option<i64> {
3223        self.marks.iter().find_map(|m| m.marked_content_id())
3224    }
3225
3226    /// Upstream-aligned alias for [`marked_content_id()`](Self::marked_content_id).
3227    ///
3228    /// Corresponds to `FPDFPageObj_GetMarkedContentID`.
3229    #[inline]
3230    pub fn page_obj_get_marked_content_id(&self) -> Option<i64> {
3231        self.marked_content_id()
3232    }
3233
3234    /// Non-upstream alias — use [`page_obj_get_marked_content_id()`](Self::page_obj_get_marked_content_id).
3235    ///
3236    /// Corresponds to `FPDFPageObj_GetMarkedContentID`.
3237    #[deprecated(
3238        note = "use `page_obj_get_marked_content_id()` — matches upstream `FPDFPageObj_GetMarkedContentID`"
3239    )]
3240    #[inline]
3241    pub fn get_marked_content_id(&self) -> Option<i64> {
3242        self.marked_content_id()
3243    }
3244
3245    /// Returns `true` if this text object has any transparency.
3246    ///
3247    /// A text object is considered transparent when it has a non-`Normal`
3248    /// blend mode.
3249    ///
3250    /// Corresponds to `FPDFPageObj_HasTransparency`.
3251    pub fn has_transparency(&self) -> bool {
3252        matches!(self.blend_mode, Some(bm) if bm != BlendMode::Normal)
3253    }
3254
3255    /// Non-upstream convenience alias for [`has_transparency()`](Self::has_transparency).
3256    ///
3257    /// Prefer [`has_transparency()`](Self::has_transparency), which matches
3258    /// the upstream `FPDFPageObj_HasTransparency` name exactly.
3259    #[deprecated(note = "use `has_transparency()` — matches upstream FPDFPageObj_HasTransparency")]
3260    #[inline]
3261    pub fn is_transparent(&self) -> bool {
3262        self.has_transparency()
3263    }
3264
3265    /// Returns the PDFium page-object-type constant for text objects: `1`.
3266    ///
3267    /// Corresponds to `FPDFPageObj_GetType` returning `FPDF_PAGEOBJ_TEXT`.
3268    pub fn object_type(&self) -> u32 {
3269        1
3270    }
3271
3272    /// Non-upstream alias — use [`get_type()`](Self::get_type).
3273    ///
3274    /// The actual upstream function is `FPDFPageObj_GetType`; there is no
3275    /// `FPDFPageObj_GetObjectType`.
3276    #[deprecated(note = "use `page_obj_get_type()` — matches upstream `FPDFPageObj_GetType`")]
3277    #[inline]
3278    pub fn get_object_type(&self) -> u32 {
3279        self.object_type()
3280    }
3281
3282    /// Upstream-aligned alias for [`object_type()`](Self::object_type).
3283    ///
3284    /// Corresponds to `FPDFPageObj_GetType`.
3285    #[inline]
3286    pub fn page_obj_get_type(&self) -> u32 {
3287        self.object_type()
3288    }
3289
3290    #[deprecated(note = "use `page_obj_get_type()` — matches upstream `FPDFPageObj_GetType`")]
3291    #[inline]
3292    pub fn get_type(&self) -> u32 {
3293        self.object_type()
3294    }
3295
3296    /// Returns the tight rotated bounding quadrilateral for this text object
3297    /// as 4 corner points in page coordinates.
3298    ///
3299    /// The corners are computed by transforming the unit square through the
3300    /// object's text matrix, which encodes position, scale, and rotation.
3301    ///
3302    /// Corresponds to `FPDFPageObj_GetRotatedBounds`.
3303    pub fn rotated_bounds(&self) -> Option<[rpdfium_core::Point; 4]> {
3304        let m = &self.matrix;
3305        let corners = [
3306            rpdfium_core::Point { x: m.e, y: m.f },
3307            rpdfium_core::Point {
3308                x: m.a + m.e,
3309                y: m.b + m.f,
3310            },
3311            rpdfium_core::Point {
3312                x: m.a + m.c + m.e,
3313                y: m.b + m.d + m.f,
3314            },
3315            rpdfium_core::Point {
3316                x: m.c + m.e,
3317                y: m.d + m.f,
3318            },
3319        ];
3320        Some(corners)
3321    }
3322
3323    /// Upstream-aligned alias for [`rotated_bounds()`](Self::rotated_bounds).
3324    ///
3325    /// Corresponds to `FPDFPageObj_GetRotatedBounds`.
3326    #[inline]
3327    pub fn page_obj_get_rotated_bounds(&self) -> Option<[rpdfium_core::Point; 4]> {
3328        self.rotated_bounds()
3329    }
3330
3331    /// Non-upstream alias — use [`page_obj_get_rotated_bounds()`](Self::page_obj_get_rotated_bounds).
3332    ///
3333    /// Corresponds to `FPDFPageObj_GetRotatedBounds`.
3334    #[deprecated(
3335        note = "use `page_obj_get_rotated_bounds()` — matches upstream `FPDFPageObj_GetRotatedBounds`"
3336    )]
3337    #[inline]
3338    pub fn get_rotated_bounds(&self) -> Option<[rpdfium_core::Point; 4]> {
3339        self.rotated_bounds()
3340    }
3341
3342    /// Sets text content from raw PDF character codes.
3343    ///
3344    /// Unlike [`set_text()`](Self::set_text) which takes a Unicode string,
3345    /// this takes PDF-specific character codes directly. Each code is
3346    /// converted to a Unicode scalar value if valid, or the replacement
3347    /// character (`U+FFFD`) otherwise.
3348    ///
3349    /// Corresponds to `FPDFText_SetCharcodes`.
3350    pub fn set_char_codes(&mut self, codes: &[u32]) {
3351        self.text = codes
3352            .iter()
3353            .map(|&c| char::from_u32(c).unwrap_or('\u{FFFD}'))
3354            .collect();
3355    }
3356
3357    /// Upstream-aligned alias for [`set_char_codes()`](Self::set_char_codes).
3358    ///
3359    /// Corresponds to `FPDFText_SetCharcodes`.
3360    #[inline]
3361    pub fn text_set_charcodes(&mut self, codes: &[u32]) {
3362        self.set_char_codes(codes)
3363    }
3364
3365    /// Non-upstream alias — use [`text_set_charcodes()`](Self::text_set_charcodes).
3366    ///
3367    /// Corresponds to `FPDFText_SetCharcodes`.
3368    #[deprecated(note = "use `text_set_charcodes()` — matches upstream `FPDFText_SetCharcodes`")]
3369    #[inline]
3370    pub fn set_charcodes(&mut self, codes: &[u32]) {
3371        self.set_char_codes(codes)
3372    }
3373
3374    /// Returns the rendered bitmap for this text object at the given scale.
3375    ///
3376    /// # Not Supported
3377    ///
3378    /// Rendering a page object to bitmap requires a live CPDF_Document/CPDF_Page
3379    /// handle not available in rpdfium's edit layer (read-only, ADR-002).
3380    ///
3381    /// Corresponds to `FPDFTextObj_GetRenderedBitmap`.
3382    pub fn rendered_bitmap(
3383        &self,
3384        _page_index: usize,
3385        _scale: f32,
3386    ) -> Result<Bitmap, crate::error::EditError> {
3387        Err(crate::error::EditError::NotSupported(
3388            "rendered_bitmap: rendering page objects to bitmap requires document context (ADR-002: read-only)".into()
3389        ))
3390    }
3391
3392    /// Upstream-aligned alias for [`rendered_bitmap()`](Self::rendered_bitmap).
3393    ///
3394    /// Corresponds to `FPDFTextObj_GetRenderedBitmap`.
3395    #[inline]
3396    pub fn text_obj_get_rendered_bitmap(
3397        &self,
3398        page_index: usize,
3399        scale: f32,
3400    ) -> Result<Bitmap, crate::error::EditError> {
3401        self.rendered_bitmap(page_index, scale)
3402    }
3403
3404    /// Non-upstream alias — use [`text_obj_get_rendered_bitmap()`](Self::text_obj_get_rendered_bitmap).
3405    ///
3406    /// Corresponds to `FPDFTextObj_GetRenderedBitmap`.
3407    #[deprecated(
3408        note = "use `text_obj_get_rendered_bitmap()` — matches upstream `FPDFTextObj_GetRenderedBitmap`"
3409    )]
3410    #[inline]
3411    pub fn get_rendered_bitmap(
3412        &self,
3413        page_index: usize,
3414        scale: f32,
3415    ) -> Result<Bitmap, crate::error::EditError> {
3416        self.rendered_bitmap(page_index, scale)
3417    }
3418}
3419
3420/// An image XObject to be placed on a page.
3421#[derive(Debug, Clone)]
3422pub struct ImageObject {
3423    /// Raw image data (format depends on filter).
3424    pub image_data: Vec<u8>,
3425    /// Image width in pixels.
3426    pub width: u32,
3427    /// Image height in pixels.
3428    pub height: u32,
3429    /// Bits per color component.
3430    pub bits_per_component: u8,
3431    /// Color space name (e.g., DeviceRGB, DeviceGray).
3432    pub color_space: Name,
3433    /// Transformation matrix (scales/positions the image on the page).
3434    pub matrix: Matrix,
3435    /// Optional filter name (e.g., FlateDecode, DCTDecode).
3436    pub filter: Option<Name>,
3437    /// Optional pre-existing XObject ID. If set, the image is already stored
3438    /// in the document and will be referenced by this ID. If `None`, the
3439    /// caller must create the image stream and assign an ID before generating
3440    /// the content stream.
3441    pub xobject_id: Option<ObjectId>,
3442    /// Blend mode for compositing.
3443    ///
3444    /// Corresponds to `FPDFPageObj_GetBlendMode` / `FPDFPageObj_SetBlendMode`.
3445    pub blend_mode: Option<BlendMode>,
3446    /// Whether the object is active (visible).
3447    ///
3448    /// Corresponds to `FPDFPageObj_GetIsActive` / `FPDFPageObj_SetIsActive`.
3449    pub active: bool,
3450    /// Content marks attached to this object (for tagged PDF / accessibility).
3451    ///
3452    /// Each mark corresponds to a `BDC`…`EMC` pair wrapping this object in
3453    /// the content stream.
3454    ///
3455    /// Corresponds to `FPDFPageObj_CountMarks` / `FPDFPageObj_GetMark` / etc.
3456    pub marks: Vec<ContentMark>,
3457    /// Optional clipping path attached to this object.
3458    ///
3459    /// Corresponds to `FPDFPageObj_GetClipPath` / `FPDFPageObj_TransformClipPath`.
3460    pub clip_path: Option<ClipPath>,
3461}
3462
3463/// Metadata about an image XObject.
3464///
3465/// Corresponds to the output of `FPDFImageObj_GetImageMetadata`.
3466#[derive(Debug, Clone, PartialEq)]
3467pub struct ImageMetadata {
3468    /// Image width in pixels.
3469    pub width: u32,
3470    /// Image height in pixels.
3471    pub height: u32,
3472    /// Horizontal resolution in pixels per inch.
3473    ///
3474    /// Computed from the image's transformation matrix: `width * 72 / |x-axis length|`.
3475    /// Returns `0.0` if the matrix scale is zero (degenerate transform).
3476    pub horizontal_dpi: f32,
3477    /// Vertical resolution in pixels per inch.
3478    ///
3479    /// Computed from the image's transformation matrix: `height * 72 / |y-axis length|`.
3480    /// Returns `0.0` if the matrix scale is zero (degenerate transform).
3481    pub vertical_dpi: f32,
3482    /// Bits per color component.
3483    pub bits_per_component: u8,
3484    /// Color space name (e.g., `DeviceRGB`, `DeviceGray`).
3485    pub color_space: Name,
3486    /// Filter name (e.g., `FlateDecode`); `None` if the image is uncompressed.
3487    pub filter: Option<Name>,
3488    /// Marked content ID, or `-1` if the image is not associated with a marked-content sequence.
3489    ///
3490    /// Corresponds to `FPDF_IMAGEOBJ_METADATA::marked_content_id`.
3491    pub marked_content_id: i32,
3492}
3493
3494/// A reference to a form XObject (pre-existing content).
3495#[derive(Debug, Clone)]
3496pub struct FormObject {
3497    /// The object ID of the form XObject.
3498    pub xobject_id: ObjectId,
3499    /// Transformation matrix.
3500    pub matrix: Matrix,
3501    /// Blend mode for compositing.
3502    ///
3503    /// Corresponds to `FPDFPageObj_GetBlendMode` / `FPDFPageObj_SetBlendMode`.
3504    pub blend_mode: Option<BlendMode>,
3505    /// Whether the object is active (visible).
3506    ///
3507    /// Corresponds to `FPDFPageObj_GetIsActive` / `FPDFPageObj_SetIsActive`.
3508    pub active: bool,
3509    /// Content marks attached to this object (for tagged PDF / accessibility).
3510    ///
3511    /// Each mark corresponds to a `BDC`…`EMC` pair wrapping this object in
3512    /// the content stream.
3513    ///
3514    /// Corresponds to `FPDFPageObj_CountMarks` / `FPDFPageObj_GetMark` / etc.
3515    pub marks: Vec<ContentMark>,
3516    /// Optional clipping path attached to this object.
3517    ///
3518    /// Corresponds to `FPDFPageObj_GetClipPath` / `FPDFPageObj_TransformClipPath`.
3519    pub clip_path: Option<ClipPath>,
3520}
3521
3522impl FormObject {
3523    /// Create a `FormObject` referencing an existing Form XObject.
3524    ///
3525    /// `xobject_id` is the PDF object ID of the Form XObject stream in the
3526    /// document.  `matrix` controls placement on the destination page.
3527    ///
3528    /// Corresponds to `FPDF_NewFormObjectFromXObject`.
3529    pub fn from_xobject(xobject_id: ObjectId, matrix: Matrix) -> Self {
3530        Self {
3531            xobject_id,
3532            matrix,
3533            blend_mode: None,
3534            active: true,
3535            marks: Vec::new(),
3536            clip_path: None,
3537        }
3538    }
3539
3540    /// Upstream-aligned alias for [`from_xobject()`](Self::from_xobject).
3541    ///
3542    /// Corresponds to `FPDF_NewFormObjectFromXObject`.
3543    #[inline]
3544    pub fn new_form_object_from_x_object(xobject_id: ObjectId, matrix: Matrix) -> Self {
3545        Self::from_xobject(xobject_id, matrix)
3546    }
3547
3548    /// Deprecated — use [`new_form_object_from_x_object()`](Self::new_form_object_from_x_object) instead.
3549    ///
3550    /// Corresponds to `FPDF_NewFormObjectFromXObject`.
3551    #[deprecated(
3552        note = "use `new_form_object_from_x_object()` — matches upstream `FPDF_NewFormObjectFromXObject`"
3553    )]
3554    #[inline]
3555    pub fn new_form_object_from_xobject(xobject_id: ObjectId, matrix: Matrix) -> Self {
3556        Self::from_xobject(xobject_id, matrix)
3557    }
3558
3559    /// Applies a matrix transform by pre-multiplying into the object's matrix.
3560    ///
3561    /// Corresponds to `FPDFPageObj_Transform`.
3562    pub fn transform(&mut self, m: &Matrix) {
3563        self.matrix = m.pre_concat(&self.matrix);
3564    }
3565
3566    /// Returns the blend mode for this object.
3567    ///
3568    /// Corresponds to `FPDFPageObj_GetBlendMode`.
3569    pub fn blend_mode(&self) -> Option<BlendMode> {
3570        self.blend_mode
3571    }
3572
3573    /// Upstream-aligned alias for [`blend_mode()`](Self::blend_mode).
3574    ///
3575    /// Corresponds to `FPDFPageObj_GetBlendMode`.
3576    #[inline]
3577    pub fn page_obj_get_blend_mode(&self) -> Option<BlendMode> {
3578        self.blend_mode()
3579    }
3580
3581    /// Non-upstream alias — use [`page_obj_get_blend_mode()`](Self::page_obj_get_blend_mode).
3582    ///
3583    /// Corresponds to `FPDFPageObj_GetBlendMode`.
3584    #[deprecated(
3585        note = "use `page_obj_get_blend_mode()` — matches upstream `FPDFPageObj_GetBlendMode`"
3586    )]
3587    #[inline]
3588    pub fn get_blend_mode(&self) -> Option<BlendMode> {
3589        self.blend_mode()
3590    }
3591
3592    /// Sets the blend mode for this object.
3593    ///
3594    /// Corresponds to `FPDFPageObj_SetBlendMode`.
3595    pub fn set_blend_mode(&mut self, mode: Option<BlendMode>) {
3596        self.blend_mode = mode;
3597    }
3598
3599    /// Returns a reference to the transformation matrix.
3600    ///
3601    /// Corresponds to `FPDFPageObj_GetMatrix`.
3602    pub fn matrix(&self) -> &Matrix {
3603        &self.matrix
3604    }
3605
3606    /// Upstream-aligned alias for [`matrix()`](Self::matrix).
3607    ///
3608    /// Corresponds to `FPDFPageObj_GetMatrix`.
3609    #[inline]
3610    pub fn page_obj_get_matrix(&self) -> &Matrix {
3611        self.matrix()
3612    }
3613
3614    #[deprecated(note = "use `page_obj_get_matrix()` — matches upstream `FPDFPageObj_GetMatrix`")]
3615    #[inline]
3616    pub fn get_matrix(&self) -> &Matrix {
3617        self.matrix()
3618    }
3619
3620    /// Set the transformation matrix.
3621    ///
3622    /// Corresponds to `FPDFPageObj_SetMatrix`.
3623    pub fn set_matrix(&mut self, matrix: Matrix) {
3624        self.matrix = matrix;
3625    }
3626
3627    /// Returns the bounding box of this form object in page coordinates.
3628    ///
3629    /// The unit square `[0,0]–[1,1]` is mapped through `self.matrix` and the
3630    /// axis-aligned bounding box of the four transformed corners is returned.
3631    ///
3632    /// Corresponds to `FPDFPageObj_GetBounds` for form objects.
3633    pub fn bounds(&self) -> Option<rpdfium_core::Rect> {
3634        let unit = rpdfium_core::Rect::new(0.0, 0.0, 1.0, 1.0);
3635        let r = self.matrix.transform_rect(unit);
3636        if r.is_empty() { None } else { Some(r) }
3637    }
3638
3639    // -----------------------------------------------------------------------
3640    // Content marks  (FPDFPageObj_CountMarks / GetMark / AddMark / RemoveMark)
3641    // -----------------------------------------------------------------------
3642
3643    /// Return the number of content marks on this object.
3644    ///
3645    /// Corresponds to `FPDFPageObj_CountMarks`.
3646    pub fn mark_count(&self) -> usize {
3647        self.marks.len()
3648    }
3649
3650    /// Upstream-aligned alias for [`mark_count()`](Self::mark_count).
3651    ///
3652    /// Corresponds to `FPDFPageObj_CountMarks`.
3653    #[inline]
3654    pub fn page_obj_count_marks(&self) -> usize {
3655        self.mark_count()
3656    }
3657
3658    /// Non-upstream alias — use [`page_obj_count_marks()`](Self::page_obj_count_marks).
3659    ///
3660    /// Corresponds to `FPDFPageObj_CountMarks`.
3661    #[deprecated(note = "use `page_obj_count_marks()` — matches upstream `FPDFPageObj_CountMarks`")]
3662    #[inline]
3663    pub fn count_marks(&self) -> usize {
3664        self.mark_count()
3665    }
3666
3667    /// Return the content mark at the given index, or `None` if out of range.
3668    ///
3669    /// Corresponds to `FPDFPageObj_GetMark`.
3670    pub fn mark(&self, index: usize) -> Option<&ContentMark> {
3671        self.marks.get(index)
3672    }
3673
3674    /// Upstream-aligned alias for [`mark()`](Self::mark).
3675    ///
3676    /// Corresponds to `FPDFPageObj_GetMark`.
3677    #[inline]
3678    pub fn page_obj_get_mark(&self, index: usize) -> Option<&ContentMark> {
3679        self.mark(index)
3680    }
3681
3682    /// Non-upstream alias — use [`page_obj_get_mark()`](Self::page_obj_get_mark).
3683    ///
3684    /// Corresponds to `FPDFPageObj_GetMark`.
3685    #[deprecated(note = "use `page_obj_get_mark()` — matches upstream `FPDFPageObj_GetMark`")]
3686    #[inline]
3687    pub fn get_mark(&self, index: usize) -> Option<&ContentMark> {
3688        self.mark(index)
3689    }
3690
3691    /// Add a new content mark with the given name and return a mutable
3692    /// reference to it.
3693    ///
3694    /// Corresponds to `FPDFPageObj_AddMark`.
3695    pub fn add_mark(&mut self, name: impl Into<String>) -> &mut ContentMark {
3696        self.marks.push(ContentMark::new(name));
3697        self.marks.last_mut().unwrap()
3698    }
3699
3700    /// Remove the content mark at the given index.
3701    ///
3702    /// Returns `true` if the index was valid and the mark was removed, `false`
3703    /// if out of range.
3704    ///
3705    /// Corresponds to `FPDFPageObj_RemoveMark`.
3706    pub fn remove_mark(&mut self, index: usize) -> bool {
3707        if index < self.marks.len() {
3708            self.marks.remove(index);
3709            true
3710        } else {
3711            false
3712        }
3713    }
3714
3715    /// Return a slice of all content marks on this object.
3716    pub fn marks(&self) -> &[ContentMark] {
3717        &self.marks
3718    }
3719
3720    /// Return the marked content ID (`/MCID`) from the first mark that has one,
3721    /// or `None` if no mark carries an MCID.
3722    ///
3723    /// Corresponds to `FPDFPageObj_GetMarkedContentID`.
3724    pub fn marked_content_id(&self) -> Option<i64> {
3725        self.marks.iter().find_map(|m| m.marked_content_id())
3726    }
3727
3728    /// Upstream-aligned alias for [`marked_content_id()`](Self::marked_content_id).
3729    ///
3730    /// Corresponds to `FPDFPageObj_GetMarkedContentID`.
3731    #[inline]
3732    pub fn page_obj_get_marked_content_id(&self) -> Option<i64> {
3733        self.marked_content_id()
3734    }
3735
3736    /// Non-upstream alias — use [`page_obj_get_marked_content_id()`](Self::page_obj_get_marked_content_id).
3737    ///
3738    /// Corresponds to `FPDFPageObj_GetMarkedContentID`.
3739    #[deprecated(
3740        note = "use `page_obj_get_marked_content_id()` — matches upstream `FPDFPageObj_GetMarkedContentID`"
3741    )]
3742    #[inline]
3743    pub fn get_marked_content_id(&self) -> Option<i64> {
3744        self.marked_content_id()
3745    }
3746
3747    /// Returns `true` if this form object has any transparency.
3748    ///
3749    /// A form object is considered transparent when it has a non-`Normal`
3750    /// blend mode.
3751    ///
3752    /// Corresponds to `FPDFPageObj_HasTransparency`.
3753    pub fn has_transparency(&self) -> bool {
3754        matches!(self.blend_mode, Some(bm) if bm != BlendMode::Normal)
3755    }
3756
3757    /// Non-upstream convenience alias for [`has_transparency()`](Self::has_transparency).
3758    ///
3759    /// Prefer [`has_transparency()`](Self::has_transparency), which matches
3760    /// the upstream `FPDFPageObj_HasTransparency` name exactly.
3761    #[deprecated(note = "use `has_transparency()` — matches upstream FPDFPageObj_HasTransparency")]
3762    #[inline]
3763    pub fn is_transparent(&self) -> bool {
3764        self.has_transparency()
3765    }
3766
3767    /// Returns the PDFium page-object-type constant for form objects: `5`.
3768    ///
3769    /// Corresponds to `FPDFPageObj_GetType` returning `FPDF_PAGEOBJ_FORM`.
3770    pub fn object_type(&self) -> u32 {
3771        5
3772    }
3773
3774    /// Non-upstream alias — use [`get_type()`](Self::get_type).
3775    ///
3776    /// The actual upstream function is `FPDFPageObj_GetType`; there is no
3777    /// `FPDFPageObj_GetObjectType`.
3778    #[deprecated(note = "use `page_obj_get_type()` — matches upstream `FPDFPageObj_GetType`")]
3779    #[inline]
3780    pub fn get_object_type(&self) -> u32 {
3781        self.object_type()
3782    }
3783
3784    /// Upstream-aligned alias for [`object_type()`](Self::object_type).
3785    ///
3786    /// Corresponds to `FPDFPageObj_GetType`.
3787    #[inline]
3788    pub fn page_obj_get_type(&self) -> u32 {
3789        self.object_type()
3790    }
3791
3792    #[deprecated(note = "use `page_obj_get_type()` — matches upstream `FPDFPageObj_GetType`")]
3793    #[inline]
3794    pub fn get_type(&self) -> u32 {
3795        self.object_type()
3796    }
3797
3798    /// Returns the number of page objects within this Form XObject.
3799    ///
3800    /// # Not Supported
3801    ///
3802    /// Reading child objects from Form XObject content streams requires a live
3803    /// document reference not available at the page object level.
3804    ///
3805    /// Corresponds to `FPDFFormObj_CountObjects`.
3806    pub fn object_count(&self) -> Result<usize, crate::error::EditError> {
3807        Err(crate::error::EditError::NotSupported(
3808            "object_count: Form XObject child enumeration not supported (requires document context)"
3809                .into(),
3810        ))
3811    }
3812
3813    /// Upstream-aligned alias for [`object_count()`](Self::object_count).
3814    ///
3815    /// Corresponds to `FPDFFormObj_CountObjects`.
3816    #[inline]
3817    pub fn form_obj_count_objects(&self) -> Result<usize, crate::error::EditError> {
3818        self.object_count()
3819    }
3820
3821    /// Non-upstream alias — use [`form_obj_count_objects()`](Self::form_obj_count_objects).
3822    ///
3823    /// Corresponds to `FPDFFormObj_CountObjects`.
3824    #[deprecated(
3825        note = "use `form_obj_count_objects()` — matches upstream `FPDFFormObj_CountObjects`"
3826    )]
3827    #[inline]
3828    pub fn count_objects(&self) -> Result<usize, crate::error::EditError> {
3829        self.object_count()
3830    }
3831
3832    /// Returns the page object at `index` within this Form XObject.
3833    ///
3834    /// # Not Supported
3835    ///
3836    /// See [`object_count()`](Self::object_count) for explanation.
3837    ///
3838    /// Corresponds to `FPDFFormObj_GetObject`.
3839    pub fn object_at(&self, _index: usize) -> Result<PageObject, crate::error::EditError> {
3840        Err(crate::error::EditError::NotSupported(
3841            "object_at: Form XObject child enumeration not supported (requires document context)"
3842                .into(),
3843        ))
3844    }
3845
3846    /// Upstream-aligned alias for [`object_at()`](Self::object_at).
3847    ///
3848    /// Corresponds to `FPDFFormObj_GetObject`.
3849    #[inline]
3850    pub fn form_obj_get_object(&self, index: usize) -> Result<PageObject, crate::error::EditError> {
3851        self.object_at(index)
3852    }
3853
3854    /// Non-upstream alias — use [`form_obj_get_object()`](Self::form_obj_get_object).
3855    ///
3856    /// Corresponds to `FPDFFormObj_GetObject`.
3857    #[deprecated(note = "use `form_obj_get_object()` — matches upstream `FPDFFormObj_GetObject`")]
3858    #[inline]
3859    pub fn get_object(&self, index: usize) -> Result<PageObject, crate::error::EditError> {
3860        self.object_at(index)
3861    }
3862
3863    /// Removes a page object from this Form XObject.
3864    ///
3865    /// # Not Supported
3866    ///
3867    /// Mutation of Form XObject content streams is not currently supported.
3868    ///
3869    /// Corresponds to `FPDFFormObj_RemoveObject`.
3870    pub fn remove_object(&mut self, _index: usize) -> Result<PageObject, crate::error::EditError> {
3871        Err(crate::error::EditError::NotSupported(
3872            "remove_object: Form XObject child mutation not supported".into(),
3873        ))
3874    }
3875}
3876
3877impl ImageObject {
3878    /// Create an `ImageObject` from a rendered [`Bitmap`].
3879    ///
3880    /// Converts an `Rgba32` (premultiplied-alpha) bitmap to RGB24, applies
3881    /// `FlateDecode` compression, and returns an `ImageObject` ready for
3882    /// embedding in a PDF page.
3883    ///
3884    /// The `matrix` parameter controls placement on the page.  A typical
3885    /// value for a *w*×*h* image at page coordinates `(x, y)` is
3886    /// `Matrix::new(w as f64, 0.0, 0.0, h as f64, x, y)`.
3887    ///
3888    /// # Panics
3889    ///
3890    /// Panics if `bitmap.format != BitmapFormat::Rgba32`.
3891    pub fn from_bitmap(bitmap: &Bitmap, matrix: Matrix) -> Self {
3892        assert_eq!(
3893            bitmap.format,
3894            BitmapFormat::Rgba32,
3895            "from_bitmap requires Rgba32 format"
3896        );
3897
3898        let w = bitmap.width as usize;
3899        let h = bitmap.height as usize;
3900        let stride = bitmap.stride as usize;
3901        let mut rgb_data = Vec::with_capacity(w * h * 3);
3902
3903        for row in 0..h {
3904            for col in 0..w {
3905                let offset = row * stride + col * 4;
3906                let r = bitmap.data[offset];
3907                let g = bitmap.data[offset + 1];
3908                let b = bitmap.data[offset + 2];
3909                let a = bitmap.data[offset + 3];
3910                if a == 0 {
3911                    rgb_data.extend_from_slice(&[0, 0, 0]);
3912                } else if a == 255 {
3913                    rgb_data.extend_from_slice(&[r, g, b]);
3914                } else {
3915                    let a32 = a as u32;
3916                    let ru = ((r as u32 * 255 + a32 / 2) / a32).min(255) as u8;
3917                    let gu = ((g as u32 * 255 + a32 / 2) / a32).min(255) as u8;
3918                    let bu = ((b as u32 * 255 + a32 / 2) / a32).min(255) as u8;
3919                    rgb_data.extend_from_slice(&[ru, gu, bu]);
3920                }
3921            }
3922        }
3923
3924        let compressed = crate::cpdf_flateencoder::flate_compress(&rgb_data);
3925
3926        Self {
3927            image_data: compressed,
3928            width: bitmap.width,
3929            height: bitmap.height,
3930            bits_per_component: 8,
3931            color_space: Name::from_bytes(b"DeviceRGB".to_vec()),
3932            matrix,
3933            filter: Some(Name::from_bytes(b"FlateDecode".to_vec())),
3934            xobject_id: None,
3935            blend_mode: None,
3936            active: true,
3937            marks: Vec::new(),
3938            clip_path: None,
3939        }
3940    }
3941
3942    /// Creates an image object embedding raw JPEG bytes directly (DCTDecode filter).
3943    ///
3944    /// No decompression or recompression occurs — the JPEG data is stored as-is with
3945    /// a `/Filter /DCTDecode` stream entry. The JPEG SOF header is parsed to extract
3946    /// the image dimensions.
3947    ///
3948    /// Returns an error if the provided bytes are not a recognisable JPEG (missing
3949    /// valid SOF0 or SOF2 marker).
3950    ///
3951    /// Corresponds to `FPDFImageObj_LoadJpegFile` / `FPDFImageObj_LoadJpegFileInline`
3952    /// in PDFium's `fpdf_edit.h`.
3953    pub fn from_jpeg_bytes(
3954        jpeg_data: Vec<u8>,
3955        matrix: Matrix,
3956    ) -> Result<Self, crate::error::EditError> {
3957        let (width, height) = parse_jpeg_dimensions(&jpeg_data).ok_or_else(|| {
3958            crate::error::EditError::Other(
3959                "from_jpeg_bytes: could not parse JPEG dimensions (no SOF0/SOF2 marker found)"
3960                    .to_owned(),
3961            )
3962        })?;
3963
3964        Ok(Self {
3965            image_data: jpeg_data,
3966            width,
3967            height,
3968            bits_per_component: 8,
3969            color_space: Name::from_bytes(b"DeviceRGB".to_vec()),
3970            matrix,
3971            filter: Some(Name::from_bytes(b"DCTDecode".to_vec())),
3972            xobject_id: None,
3973            blend_mode: None,
3974            active: true,
3975            marks: Vec::new(),
3976            clip_path: None,
3977        })
3978    }
3979
3980    /// Applies a matrix transform by pre-multiplying into the object's matrix.
3981    ///
3982    /// Corresponds to `FPDFPageObj_Transform`.
3983    pub fn transform(&mut self, m: &Matrix) {
3984        self.matrix = m.pre_concat(&self.matrix);
3985    }
3986
3987    /// Returns the blend mode for this object.
3988    ///
3989    /// Corresponds to `FPDFPageObj_GetBlendMode`.
3990    pub fn blend_mode(&self) -> Option<BlendMode> {
3991        self.blend_mode
3992    }
3993
3994    /// Upstream-aligned alias for [`blend_mode()`](Self::blend_mode).
3995    ///
3996    /// Corresponds to `FPDFPageObj_GetBlendMode`.
3997    #[inline]
3998    pub fn page_obj_get_blend_mode(&self) -> Option<BlendMode> {
3999        self.blend_mode()
4000    }
4001
4002    /// Non-upstream alias — use [`page_obj_get_blend_mode()`](Self::page_obj_get_blend_mode).
4003    ///
4004    /// Corresponds to `FPDFPageObj_GetBlendMode`.
4005    #[deprecated(
4006        note = "use `page_obj_get_blend_mode()` — matches upstream `FPDFPageObj_GetBlendMode`"
4007    )]
4008    #[inline]
4009    pub fn get_blend_mode(&self) -> Option<BlendMode> {
4010        self.blend_mode()
4011    }
4012
4013    /// Sets the blend mode for this object.
4014    ///
4015    /// Corresponds to `FPDFPageObj_SetBlendMode`.
4016    pub fn set_blend_mode(&mut self, mode: Option<BlendMode>) {
4017        self.blend_mode = mode;
4018    }
4019
4020    /// Returns metadata about this image object.
4021    ///
4022    /// Corresponds to `FPDFImageObj_GetImageMetadata`.
4023    pub fn metadata(&self) -> ImageMetadata {
4024        // Compute DPI from the transformation matrix.
4025        // The matrix maps from image space (unit square) to page space (user units, 72 per inch).
4026        // x-axis scale = sqrt(a^2 + b^2), y-axis scale = sqrt(c^2 + d^2).
4027        let m = &self.matrix;
4028        let scale_x = (m.a * m.a + m.b * m.b).sqrt() as f32;
4029        let scale_y = (m.c * m.c + m.d * m.d).sqrt() as f32;
4030        let horizontal_dpi = if scale_x > 0.0 {
4031            self.width as f32 * 72.0 / scale_x
4032        } else {
4033            0.0
4034        };
4035        let vertical_dpi = if scale_y > 0.0 {
4036            self.height as f32 * 72.0 / scale_y
4037        } else {
4038            0.0
4039        };
4040
4041        ImageMetadata {
4042            width: self.width,
4043            height: self.height,
4044            horizontal_dpi,
4045            vertical_dpi,
4046            bits_per_component: self.bits_per_component,
4047            color_space: self.color_space.clone(),
4048            filter: self.filter.clone(),
4049            marked_content_id: -1,
4050        }
4051    }
4052
4053    /// Upstream-aligned alias for [`metadata()`](Self::metadata).
4054    ///
4055    /// Corresponds to `FPDFImageObj_GetImageMetadata`.
4056    #[inline]
4057    pub fn image_obj_get_image_metadata(&self) -> ImageMetadata {
4058        self.metadata()
4059    }
4060
4061    #[deprecated(
4062        note = "use `image_obj_get_image_metadata()` — matches upstream `FPDFImageObj_GetImageMetadata`"
4063    )]
4064    #[inline]
4065    pub fn get_image_metadata(&self) -> ImageMetadata {
4066        self.metadata()
4067    }
4068
4069    /// Returns the raw (possibly compressed) image data.
4070    ///
4071    /// Corresponds to `FPDFImageObj_GetImageDataRaw`.
4072    pub fn raw_data(&self) -> &[u8] {
4073        &self.image_data
4074    }
4075
4076    /// Upstream-aligned alias for [`raw_data()`](Self::raw_data).
4077    ///
4078    /// Corresponds to `FPDFImageObj_GetImageDataRaw`.
4079    #[inline]
4080    pub fn image_obj_get_image_data_raw(&self) -> &[u8] {
4081        self.raw_data()
4082    }
4083
4084    #[deprecated(
4085        note = "use `image_obj_get_image_data_raw()` — matches upstream `FPDFImageObj_GetImageDataRaw`"
4086    )]
4087    #[inline]
4088    pub fn get_image_data_raw(&self) -> &[u8] {
4089        self.raw_data()
4090    }
4091
4092    /// Returns the decoded (decompressed) image data.
4093    ///
4094    /// If a filter is set (e.g. `FlateDecode`), the data is decompressed via
4095    /// the filter pipeline.  If the image is uncompressed, returns the raw
4096    /// data unchanged.
4097    ///
4098    /// Corresponds to `FPDFImageObj_GetImageDataDecoded`.
4099    pub fn decoded_data(&self) -> Result<Vec<u8>, crate::error::EditError> {
4100        if let Some(ref filter_name) = self.filter {
4101            let filter = parse_image_filter(filter_name)?;
4102            rpdfium_codec::apply_filter_chain(
4103                &self.image_data,
4104                &[(filter, rpdfium_codec::FilterParams::default())],
4105            )
4106            .map_err(|e| crate::error::EditError::Other(format!("image decode error: {e}")))
4107        } else {
4108            Ok(self.image_data.clone())
4109        }
4110    }
4111
4112    /// Upstream-aligned alias for [`decoded_data()`](Self::decoded_data).
4113    ///
4114    /// Corresponds to `FPDFImageObj_GetImageDataDecoded`.
4115    #[inline]
4116    pub fn image_obj_get_image_data_decoded(&self) -> Result<Vec<u8>, crate::error::EditError> {
4117        self.decoded_data()
4118    }
4119
4120    #[deprecated(
4121        note = "use `image_obj_get_image_data_decoded()` — matches upstream `FPDFImageObj_GetImageDataDecoded`"
4122    )]
4123    #[inline]
4124    pub fn get_image_data_decoded(&self) -> Result<Vec<u8>, crate::error::EditError> {
4125        self.decoded_data()
4126    }
4127
4128    /// Decodes the image data and returns it as a [`Bitmap`].
4129    ///
4130    /// - `DeviceRGB` images → `BitmapFormat::Rgba32` (alpha = 255)
4131    /// - `DeviceGray` images → `BitmapFormat::Gray8`
4132    ///
4133    /// Returns an error for unsupported color spaces or if the decoded data is
4134    /// too short for the declared dimensions.
4135    ///
4136    /// Corresponds to `FPDFImageObj_GetBitmap`.
4137    pub fn to_bitmap(&self) -> Result<Bitmap, crate::error::EditError> {
4138        let decoded = self.decoded_data()?;
4139        let cs = self.color_space.as_bytes();
4140        if cs == b"DeviceRGB" || cs == b"RGB" {
4141            let w = self.width as usize;
4142            let h = self.height as usize;
4143            let expected = w * h * 3;
4144            if decoded.len() < expected {
4145                return Err(crate::error::EditError::Other(format!(
4146                    "decoded image too short: {} < {} (DeviceRGB {}x{})",
4147                    decoded.len(),
4148                    expected,
4149                    self.width,
4150                    self.height
4151                )));
4152            }
4153            let mut bmp = Bitmap::new(self.width, self.height, BitmapFormat::Rgba32);
4154            let stride = bmp.stride as usize;
4155            for row in 0..h {
4156                for col in 0..w {
4157                    let src = (row * w + col) * 3;
4158                    let dst = row * stride + col * 4;
4159                    bmp.data[dst] = decoded[src];
4160                    bmp.data[dst + 1] = decoded[src + 1];
4161                    bmp.data[dst + 2] = decoded[src + 2];
4162                    bmp.data[dst + 3] = 255;
4163                }
4164            }
4165            Ok(bmp)
4166        } else if cs == b"DeviceGray" || cs == b"Gray" {
4167            let w = self.width as usize;
4168            let h = self.height as usize;
4169            let expected = w * h;
4170            if decoded.len() < expected {
4171                return Err(crate::error::EditError::Other(format!(
4172                    "decoded image too short: {} < {} (DeviceGray {}x{})",
4173                    decoded.len(),
4174                    expected,
4175                    self.width,
4176                    self.height
4177                )));
4178            }
4179            let mut bmp = Bitmap::new(self.width, self.height, BitmapFormat::Gray8);
4180            let stride = bmp.stride as usize;
4181            for row in 0..h {
4182                let src = row * w;
4183                let dst = row * stride;
4184                bmp.data[dst..dst + w].copy_from_slice(&decoded[src..src + w]);
4185            }
4186            Ok(bmp)
4187        } else {
4188            Err(crate::error::EditError::Other(format!(
4189                "to_bitmap: unsupported color space {:?}",
4190                self.color_space
4191            )))
4192        }
4193    }
4194
4195    /// Upstream-aligned alias for [`to_bitmap()`](Self::to_bitmap).
4196    ///
4197    /// Corresponds to `FPDFImageObj_GetBitmap`.
4198    #[inline]
4199    pub fn image_obj_get_bitmap(&self) -> Result<Bitmap, crate::error::EditError> {
4200        self.to_bitmap()
4201    }
4202
4203    /// Non-upstream alias — use [`image_obj_get_bitmap()`](Self::image_obj_get_bitmap).
4204    ///
4205    /// Corresponds to `FPDFImageObj_GetBitmap`.
4206    #[deprecated(note = "use `image_obj_get_bitmap()` — matches upstream `FPDFImageObj_GetBitmap`")]
4207    #[inline]
4208    pub fn get_bitmap(&self) -> Result<Bitmap, crate::error::EditError> {
4209        self.to_bitmap()
4210    }
4211
4212    /// Returns a reference to the transformation matrix.
4213    ///
4214    /// Corresponds to `FPDFPageObj_GetMatrix`.
4215    pub fn matrix(&self) -> &Matrix {
4216        &self.matrix
4217    }
4218
4219    /// Upstream-aligned alias for [`matrix()`](Self::matrix).
4220    ///
4221    /// Corresponds to `FPDFPageObj_GetMatrix`.
4222    #[inline]
4223    pub fn page_obj_get_matrix(&self) -> &Matrix {
4224        self.matrix()
4225    }
4226
4227    #[deprecated(note = "use `page_obj_get_matrix()` — matches upstream `FPDFPageObj_GetMatrix`")]
4228    #[inline]
4229    pub fn get_matrix(&self) -> &Matrix {
4230        self.matrix()
4231    }
4232
4233    /// Set the transformation matrix.
4234    ///
4235    /// Corresponds to `FPDFImageObj_SetMatrix`.
4236    pub fn set_matrix(&mut self, matrix: Matrix) {
4237        self.matrix = matrix;
4238    }
4239
4240    /// Upstream-aligned alias for [`set_matrix()`](Self::set_matrix).
4241    ///
4242    /// Corresponds to `FPDFImageObj_SetMatrix`.
4243    #[inline]
4244    pub fn image_obj_set_matrix(&mut self, matrix: Matrix) {
4245        self.set_matrix(matrix)
4246    }
4247
4248    /// Returns the bounding box of this image object in page coordinates.
4249    ///
4250    /// The unit square `[0,0]–[1,1]` is mapped through `self.matrix` and the
4251    /// axis-aligned bounding box of the four transformed corners is returned.
4252    ///
4253    /// Corresponds to `FPDFPageObj_GetBounds` for image objects.
4254    pub fn bounds(&self) -> Option<rpdfium_core::Rect> {
4255        let unit = rpdfium_core::Rect::new(0.0, 0.0, 1.0, 1.0);
4256        let r = self.matrix.transform_rect(unit);
4257        if r.is_empty() { None } else { Some(r) }
4258    }
4259
4260    // -----------------------------------------------------------------------
4261    // Content marks  (FPDFPageObj_CountMarks / GetMark / AddMark / RemoveMark)
4262    // -----------------------------------------------------------------------
4263
4264    /// Return the number of content marks on this object.
4265    ///
4266    /// Corresponds to `FPDFPageObj_CountMarks`.
4267    pub fn mark_count(&self) -> usize {
4268        self.marks.len()
4269    }
4270
4271    /// Upstream-aligned alias for [`mark_count()`](Self::mark_count).
4272    ///
4273    /// Corresponds to `FPDFPageObj_CountMarks`.
4274    #[inline]
4275    pub fn page_obj_count_marks(&self) -> usize {
4276        self.mark_count()
4277    }
4278
4279    /// Non-upstream alias — use [`page_obj_count_marks()`](Self::page_obj_count_marks).
4280    ///
4281    /// Corresponds to `FPDFPageObj_CountMarks`.
4282    #[deprecated(note = "use `page_obj_count_marks()` — matches upstream `FPDFPageObj_CountMarks`")]
4283    #[inline]
4284    pub fn count_marks(&self) -> usize {
4285        self.mark_count()
4286    }
4287
4288    /// Return the content mark at the given index, or `None` if out of range.
4289    ///
4290    /// Corresponds to `FPDFPageObj_GetMark`.
4291    pub fn mark(&self, index: usize) -> Option<&ContentMark> {
4292        self.marks.get(index)
4293    }
4294
4295    /// Upstream-aligned alias for [`mark()`](Self::mark).
4296    ///
4297    /// Corresponds to `FPDFPageObj_GetMark`.
4298    #[inline]
4299    pub fn page_obj_get_mark(&self, index: usize) -> Option<&ContentMark> {
4300        self.mark(index)
4301    }
4302
4303    /// Non-upstream alias — use [`page_obj_get_mark()`](Self::page_obj_get_mark).
4304    ///
4305    /// Corresponds to `FPDFPageObj_GetMark`.
4306    #[deprecated(note = "use `page_obj_get_mark()` — matches upstream `FPDFPageObj_GetMark`")]
4307    #[inline]
4308    pub fn get_mark(&self, index: usize) -> Option<&ContentMark> {
4309        self.mark(index)
4310    }
4311
4312    /// Add a new content mark with the given name and return a mutable
4313    /// reference to it.
4314    ///
4315    /// Corresponds to `FPDFPageObj_AddMark`.
4316    pub fn add_mark(&mut self, name: impl Into<String>) -> &mut ContentMark {
4317        self.marks.push(ContentMark::new(name));
4318        self.marks.last_mut().unwrap()
4319    }
4320
4321    /// Remove the content mark at the given index.
4322    ///
4323    /// Returns `true` if the index was valid and the mark was removed, `false`
4324    /// if out of range.
4325    ///
4326    /// Corresponds to `FPDFPageObj_RemoveMark`.
4327    pub fn remove_mark(&mut self, index: usize) -> bool {
4328        if index < self.marks.len() {
4329            self.marks.remove(index);
4330            true
4331        } else {
4332            false
4333        }
4334    }
4335
4336    /// Return a slice of all content marks on this object.
4337    pub fn marks(&self) -> &[ContentMark] {
4338        &self.marks
4339    }
4340
4341    /// Return the marked content ID (`/MCID`) from the first mark that has one,
4342    /// or `None` if no mark carries an MCID.
4343    ///
4344    /// Corresponds to `FPDFPageObj_GetMarkedContentID`.
4345    pub fn marked_content_id(&self) -> Option<i64> {
4346        self.marks.iter().find_map(|m| m.marked_content_id())
4347    }
4348
4349    /// Upstream-aligned alias for [`marked_content_id()`](Self::marked_content_id).
4350    ///
4351    /// Corresponds to `FPDFPageObj_GetMarkedContentID`.
4352    #[inline]
4353    pub fn page_obj_get_marked_content_id(&self) -> Option<i64> {
4354        self.marked_content_id()
4355    }
4356
4357    /// Non-upstream alias — use [`page_obj_get_marked_content_id()`](Self::page_obj_get_marked_content_id).
4358    ///
4359    /// Corresponds to `FPDFPageObj_GetMarkedContentID`.
4360    #[deprecated(
4361        note = "use `page_obj_get_marked_content_id()` — matches upstream `FPDFPageObj_GetMarkedContentID`"
4362    )]
4363    #[inline]
4364    pub fn get_marked_content_id(&self) -> Option<i64> {
4365        self.marked_content_id()
4366    }
4367
4368    /// Returns `true` if this image object has any transparency.
4369    ///
4370    /// An image object is considered transparent when it has a non-`Normal`
4371    /// blend mode.
4372    ///
4373    /// Corresponds to `FPDFPageObj_HasTransparency`.
4374    pub fn has_transparency(&self) -> bool {
4375        matches!(self.blend_mode, Some(bm) if bm != BlendMode::Normal)
4376    }
4377
4378    /// Non-upstream convenience alias for [`has_transparency()`](Self::has_transparency).
4379    ///
4380    /// Prefer [`has_transparency()`](Self::has_transparency), which matches
4381    /// the upstream `FPDFPageObj_HasTransparency` name exactly.
4382    #[deprecated(note = "use `has_transparency()` — matches upstream FPDFPageObj_HasTransparency")]
4383    #[inline]
4384    pub fn is_transparent(&self) -> bool {
4385        self.has_transparency()
4386    }
4387
4388    /// Returns the PDFium page-object-type constant for image objects: `3`.
4389    ///
4390    /// Corresponds to `FPDFPageObj_GetType` returning `FPDF_PAGEOBJ_IMAGE`.
4391    pub fn object_type(&self) -> u32 {
4392        3
4393    }
4394
4395    /// Non-upstream alias — use [`page_obj_get_type()`](Self::page_obj_get_type).
4396    ///
4397    /// The actual upstream function is `FPDFPageObj_GetType`; there is no
4398    /// `FPDFPageObj_GetObjectType`.
4399    #[deprecated(note = "use `page_obj_get_type()` — matches upstream `FPDFPageObj_GetType`")]
4400    #[inline]
4401    pub fn get_object_type(&self) -> u32 {
4402        self.object_type()
4403    }
4404
4405    /// Upstream-aligned alias for [`object_type()`](Self::object_type).
4406    ///
4407    /// Corresponds to `FPDFPageObj_GetType`.
4408    #[inline]
4409    pub fn page_obj_get_type(&self) -> u32 {
4410        self.object_type()
4411    }
4412
4413    #[deprecated(note = "use `page_obj_get_type()` — matches upstream `FPDFPageObj_GetType`")]
4414    #[inline]
4415    pub fn get_type(&self) -> u32 {
4416        self.object_type()
4417    }
4418
4419    /// Returns the number of filters applied to this image's data stream.
4420    ///
4421    /// This implementation stores at most one filter; returns `1` when a filter
4422    /// is present, `0` otherwise.
4423    ///
4424    /// Corresponds to `FPDFImageObj_GetImageFilterCount`.
4425    pub fn filter_count(&self) -> usize {
4426        if self.filter.is_some() { 1 } else { 0 }
4427    }
4428
4429    /// Upstream-aligned alias for [`filter_count()`](Self::filter_count).
4430    ///
4431    /// Corresponds to `FPDFImageObj_GetImageFilterCount`.
4432    #[inline]
4433    pub fn image_obj_get_image_filter_count(&self) -> usize {
4434        self.filter_count()
4435    }
4436
4437    /// Non-upstream alias — use [`image_obj_get_image_filter_count()`](Self::image_obj_get_image_filter_count).
4438    ///
4439    /// Corresponds to `FPDFImageObj_GetImageFilterCount`.
4440    #[deprecated(
4441        note = "use `image_obj_get_image_filter_count()` — matches upstream `FPDFImageObj_GetImageFilterCount`"
4442    )]
4443    #[inline]
4444    pub fn get_image_filter_count(&self) -> usize {
4445        self.filter_count()
4446    }
4447
4448    /// Returns the filter name at the given index as a `&str`, or `None` if the
4449    /// index is out of range.
4450    ///
4451    /// Only index `0` is valid; any other index returns `None`.
4452    ///
4453    /// Corresponds to `FPDFImageObj_GetImageFilter`.
4454    pub fn filter(&self, index: usize) -> Option<&str> {
4455        if index == 0 {
4456            self.filter
4457                .as_ref()
4458                .and_then(|n| std::str::from_utf8(n.as_bytes()).ok())
4459        } else {
4460            None
4461        }
4462    }
4463
4464    /// Upstream-aligned alias for [`filter()`](Self::filter).
4465    ///
4466    /// Corresponds to `FPDFImageObj_GetImageFilter`.
4467    #[inline]
4468    pub fn image_obj_get_image_filter(&self, index: usize) -> Option<&str> {
4469        self.filter(index)
4470    }
4471
4472    /// Non-upstream alias — use [`image_obj_get_image_filter()`](Self::image_obj_get_image_filter).
4473    ///
4474    /// Corresponds to `FPDFImageObj_GetImageFilter`.
4475    #[deprecated(
4476        note = "use `image_obj_get_image_filter()` — matches upstream `FPDFImageObj_GetImageFilter`"
4477    )]
4478    #[inline]
4479    pub fn get_image_filter(&self, index: usize) -> Option<&str> {
4480        self.filter(index)
4481    }
4482
4483    /// Non-upstream alias — use [`image_obj_get_image_filter()`](Self::image_obj_get_image_filter).
4484    ///
4485    /// Corresponds to `FPDFImageObj_GetImageFilter`.
4486    #[deprecated(
4487        note = "use `image_obj_get_image_filter()` — matches upstream FPDFImageObj_GetImageFilter"
4488    )]
4489    #[inline]
4490    pub fn get_filter(&self, index: usize) -> Option<&str> {
4491        self.image_obj_get_image_filter(index)
4492    }
4493
4494    /// Returns the pixel dimensions of this image object.
4495    ///
4496    /// Returns `(width, height)` in pixels.
4497    ///
4498    /// Corresponds to `FPDFImageObj_GetImagePixelSize`.
4499    pub fn pixel_size(&self) -> (u32, u32) {
4500        (self.width, self.height)
4501    }
4502
4503    /// Upstream-aligned alias for [`pixel_size()`](Self::pixel_size).
4504    ///
4505    /// Corresponds to `FPDFImageObj_GetImagePixelSize`.
4506    #[inline]
4507    pub fn image_obj_get_image_pixel_size(&self) -> (u32, u32) {
4508        self.pixel_size()
4509    }
4510
4511    /// Non-upstream alias — use [`image_obj_get_image_pixel_size()`](Self::image_obj_get_image_pixel_size).
4512    ///
4513    /// Corresponds to `FPDFImageObj_GetImagePixelSize`.
4514    #[deprecated(
4515        note = "use `image_obj_get_image_pixel_size()` — matches upstream `FPDFImageObj_GetImagePixelSize`"
4516    )]
4517    #[inline]
4518    pub fn get_image_pixel_size(&self) -> (u32, u32) {
4519        self.pixel_size()
4520    }
4521
4522    /// Returns the tight rotated bounding quadrilateral for this image object
4523    /// as 4 corner points in page coordinates.
4524    ///
4525    /// The corners are computed by transforming the unit square
4526    /// `[0,0]–[1,1]` through the image's placement matrix, giving the
4527    /// exact (possibly rotated) bounding quad.
4528    ///
4529    /// Corresponds to `FPDFPageObj_GetRotatedBounds`.
4530    pub fn rotated_bounds(&self) -> Option<[rpdfium_core::Point; 4]> {
4531        let m = &self.matrix;
4532        let corners = [
4533            rpdfium_core::Point { x: m.e, y: m.f },
4534            rpdfium_core::Point {
4535                x: m.a + m.e,
4536                y: m.b + m.f,
4537            },
4538            rpdfium_core::Point {
4539                x: m.a + m.c + m.e,
4540                y: m.b + m.d + m.f,
4541            },
4542            rpdfium_core::Point {
4543                x: m.c + m.e,
4544                y: m.d + m.f,
4545            },
4546        ];
4547        Some(corners)
4548    }
4549
4550    /// Upstream-aligned alias for [`rotated_bounds()`](Self::rotated_bounds).
4551    ///
4552    /// Corresponds to `FPDFPageObj_GetRotatedBounds`.
4553    #[inline]
4554    pub fn page_obj_get_rotated_bounds(&self) -> Option<[rpdfium_core::Point; 4]> {
4555        self.rotated_bounds()
4556    }
4557
4558    /// Non-upstream alias — use [`page_obj_get_rotated_bounds()`](Self::page_obj_get_rotated_bounds).
4559    ///
4560    /// Corresponds to `FPDFPageObj_GetRotatedBounds`.
4561    #[deprecated(
4562        note = "use `page_obj_get_rotated_bounds()` — matches upstream `FPDFPageObj_GetRotatedBounds`"
4563    )]
4564    #[inline]
4565    pub fn get_rotated_bounds(&self) -> Option<[rpdfium_core::Point; 4]> {
4566        self.rotated_bounds()
4567    }
4568
4569    /// Returns the rendered bitmap for this image object at the given scale.
4570    ///
4571    /// # Not Supported
4572    ///
4573    /// Rendering a page object to bitmap requires a live CPDF_Document/CPDF_Page
4574    /// handle not available in rpdfium's edit layer (read-only, ADR-002).
4575    ///
4576    /// Corresponds to `FPDFImageObj_GetRenderedBitmap`.
4577    pub fn rendered_bitmap(
4578        &self,
4579        _page_index: usize,
4580        _scale: f32,
4581    ) -> Result<Bitmap, crate::error::EditError> {
4582        Err(crate::error::EditError::NotSupported(
4583            "rendered_bitmap: rendering page objects to bitmap requires document context (ADR-002: read-only)".into()
4584        ))
4585    }
4586
4587    /// Upstream-aligned alias for [`rendered_bitmap()`](Self::rendered_bitmap).
4588    ///
4589    /// Corresponds to `FPDFImageObj_GetRenderedBitmap`.
4590    #[inline]
4591    pub fn image_obj_get_rendered_bitmap(
4592        &self,
4593        page_index: usize,
4594        scale: f32,
4595    ) -> Result<Bitmap, crate::error::EditError> {
4596        self.rendered_bitmap(page_index, scale)
4597    }
4598
4599    /// Non-upstream alias — use [`image_obj_get_rendered_bitmap()`](Self::image_obj_get_rendered_bitmap).
4600    ///
4601    /// Corresponds to `FPDFImageObj_GetRenderedBitmap`.
4602    #[deprecated(
4603        note = "use `image_obj_get_rendered_bitmap()` — matches upstream `FPDFImageObj_GetRenderedBitmap`"
4604    )]
4605    #[inline]
4606    pub fn get_rendered_bitmap(
4607        &self,
4608        page_index: usize,
4609        scale: f32,
4610    ) -> Result<Bitmap, crate::error::EditError> {
4611        self.rendered_bitmap(page_index, scale)
4612    }
4613
4614    /// Returns the decoded ICC profile bytes embedded in this image object, if any.
4615    ///
4616    /// Returns `None` if the image has no embedded ICC profile.
4617    /// ICC profile extraction requires parsing the image's color space stream,
4618    /// which is not performed at the edit layer.
4619    ///
4620    /// Corresponds to `FPDFImageObj_GetIccProfileDataDecoded`.
4621    pub fn icc_profile_data_decoded(&self) -> Option<&[u8]> {
4622        // ICC profile data is stored in the /ColorSpace stream for images.
4623        // Extraction requires parsing the image's color space stream,
4624        // which is not performed at the edit layer.
4625        None
4626    }
4627
4628    /// Upstream-aligned alias for [`icc_profile_data_decoded()`](Self::icc_profile_data_decoded).
4629    ///
4630    /// Corresponds to `FPDFImageObj_GetIccProfileDataDecoded`.
4631    #[inline]
4632    pub fn image_obj_get_icc_profile_data_decoded(&self) -> Option<&[u8]> {
4633        self.icc_profile_data_decoded()
4634    }
4635
4636    /// Non-upstream alias — use [`image_obj_get_icc_profile_data_decoded()`](Self::image_obj_get_icc_profile_data_decoded).
4637    ///
4638    /// Corresponds to `FPDFImageObj_GetIccProfileDataDecoded`.
4639    #[deprecated(
4640        note = "use `image_obj_get_icc_profile_data_decoded()` — matches upstream `FPDFImageObj_GetIccProfileDataDecoded`"
4641    )]
4642    #[inline]
4643    pub fn get_icc_profile_data_decoded(&self) -> Option<&[u8]> {
4644        self.icc_profile_data_decoded()
4645    }
4646}
4647
4648/// Convert a slice of renderer [`PathOp`]s to [`PathSegment`]s.
4649///
4650/// Coordinates are widened from `f32` to `f64`.  `PathOp::Close` maps to
4651/// `PathSegment::Close`.  No rectangle detection is performed.
4652pub fn path_ops_to_segments(ops: &[PathOp]) -> Vec<PathSegment> {
4653    ops.iter()
4654        .map(|op| match op {
4655            PathOp::MoveTo { x, y } => PathSegment::MoveTo(*x as f64, *y as f64),
4656            PathOp::LineTo { x, y } => PathSegment::LineTo(*x as f64, *y as f64),
4657            PathOp::CurveTo {
4658                x1,
4659                y1,
4660                x2,
4661                y2,
4662                x3,
4663                y3,
4664            } => PathSegment::CurveTo(
4665                *x1 as f64, *y1 as f64, *x2 as f64, *y2 as f64, *x3 as f64, *y3 as f64,
4666            ),
4667            PathOp::Close => PathSegment::Close,
4668        })
4669        .collect()
4670}
4671
4672/// Convert a slice of [`PathSegment`]s to renderer [`PathOp`]s.
4673///
4674/// Coordinates are narrowed from `f64` to `f32`.
4675/// `PathSegment::Rect(x, y, w, h)` expands to five `PathOp`s:
4676/// `MoveTo`, three `LineTo`s forming the rectangle, and `Close`.
4677pub fn segments_to_path_ops(segs: &[PathSegment]) -> Vec<PathOp> {
4678    let mut out = Vec::with_capacity(segs.len());
4679    for seg in segs {
4680        match seg {
4681            PathSegment::MoveTo(x, y) => {
4682                out.push(PathOp::MoveTo {
4683                    x: *x as f32,
4684                    y: *y as f32,
4685                });
4686            }
4687            PathSegment::LineTo(x, y) => {
4688                out.push(PathOp::LineTo {
4689                    x: *x as f32,
4690                    y: *y as f32,
4691                });
4692            }
4693            PathSegment::CurveTo(x1, y1, x2, y2, x3, y3) => {
4694                out.push(PathOp::CurveTo {
4695                    x1: *x1 as f32,
4696                    y1: *y1 as f32,
4697                    x2: *x2 as f32,
4698                    y2: *y2 as f32,
4699                    x3: *x3 as f32,
4700                    y3: *y3 as f32,
4701                });
4702            }
4703            PathSegment::Rect(x, y, w, h) => {
4704                let (x, y, w, h) = (*x as f32, *y as f32, *w as f32, *h as f32);
4705                out.push(PathOp::MoveTo { x, y });
4706                out.push(PathOp::LineTo { x: x + w, y });
4707                out.push(PathOp::LineTo { x: x + w, y: y + h });
4708                out.push(PathOp::LineTo { x, y: y + h });
4709                out.push(PathOp::Close);
4710            }
4711            PathSegment::Close => {
4712                out.push(PathOp::Close);
4713            }
4714        }
4715    }
4716    out
4717}
4718
4719/// Map a PDF filter name to a [`rpdfium_codec::DecodeFilter`] variant.
4720///
4721/// Accepts both the full name (`FlateDecode`) and the abbreviated alias (`Fl`).
4722fn parse_image_filter(name: &Name) -> Result<rpdfium_codec::DecodeFilter, crate::error::EditError> {
4723    match name.as_bytes() {
4724        b"FlateDecode" | b"Fl" => Ok(rpdfium_codec::DecodeFilter::Flate),
4725        b"DCTDecode" | b"DCT" => Ok(rpdfium_codec::DecodeFilter::DCT),
4726        b"LZWDecode" | b"LZW" => Ok(rpdfium_codec::DecodeFilter::LZW),
4727        b"RunLengthDecode" | b"RL" => Ok(rpdfium_codec::DecodeFilter::RunLength),
4728        b"ASCII85Decode" | b"A85" => Ok(rpdfium_codec::DecodeFilter::ASCII85),
4729        b"ASCIIHexDecode" | b"AHx" => Ok(rpdfium_codec::DecodeFilter::ASCIIHex),
4730        b"CCITTFaxDecode" | b"CCF" => Ok(rpdfium_codec::DecodeFilter::CCITTFax),
4731        b"JBIG2Decode" => Ok(rpdfium_codec::DecodeFilter::JBIG2),
4732        b"JPXDecode" => Ok(rpdfium_codec::DecodeFilter::JPX),
4733        _ => Err(crate::error::EditError::Other(format!(
4734            "unsupported image filter: {:?}",
4735            name
4736        ))),
4737    }
4738}
4739
4740/// Parse the pixel dimensions from a JPEG byte stream.
4741///
4742/// Scans the JPEG markers for a Start-Of-Frame (SOF) segment: either
4743/// `SOF0` (0xFFC0) for baseline DCT or `SOF2` (0xFFC2) for progressive DCT.
4744/// Both store the image height at bytes +3..+4 and width at bytes +5..+6
4745/// within the segment (after the 2-byte length field).
4746///
4747/// Returns `Some((width, height))` when the SOF marker is found, or `None`
4748/// if the data is too short or contains no recognisable SOF segment.
4749///
4750/// Corresponds to the dimension-extraction logic inside
4751/// `FPDFImageObj_LoadJpegFile` in PDFium.
4752pub fn parse_jpeg_dimensions(data: &[u8]) -> Option<(u32, u32)> {
4753    // JPEG streams must start with SOI (0xFF 0xD8).
4754    if data.len() < 2 || data[0] != 0xFF || data[1] != 0xD8 {
4755        return None;
4756    }
4757
4758    let mut i = 2usize;
4759    while i + 3 < data.len() {
4760        // Each marker begins with 0xFF (possibly padded).
4761        if data[i] != 0xFF {
4762            return None;
4763        }
4764        // Skip any 0xFF padding bytes.
4765        while i < data.len() && data[i] == 0xFF {
4766            i += 1;
4767        }
4768        if i >= data.len() {
4769            return None;
4770        }
4771        let marker = data[i];
4772        i += 1;
4773
4774        // Skip stand-alone markers that have no length field.
4775        match marker {
4776            0xD8 | 0xD9 | 0x01 => continue,
4777            _ => {}
4778        }
4779
4780        // Read 2-byte segment length (includes the 2 length bytes themselves).
4781        if i + 2 > data.len() {
4782            return None;
4783        }
4784        let seg_len = u16::from_be_bytes([data[i], data[i + 1]]) as usize;
4785        if seg_len < 2 {
4786            return None;
4787        }
4788
4789        // SOF0 = 0xC0 (baseline), SOF2 = 0xC2 (progressive).
4790        if marker == 0xC0 || marker == 0xC2 {
4791            // Segment layout: [length(2), precision(1), height(2), width(2), ...]
4792            // i points at start of length field.
4793            //   i+0, i+1: length (already read above)
4794            //   i+2:       precision (bits per sample)
4795            //   i+3, i+4:  height (big-endian u16)
4796            //   i+5, i+6:  width  (big-endian u16)
4797            if i + 7 > data.len() {
4798                return None;
4799            }
4800            let height = u16::from_be_bytes([data[i + 3], data[i + 4]]) as u32;
4801            let width = u16::from_be_bytes([data[i + 5], data[i + 6]]) as u32;
4802            return Some((width, height));
4803        }
4804
4805        // Advance past this segment.
4806        let next = i.checked_add(seg_len)?;
4807        if next > data.len() {
4808            return None;
4809        }
4810        i = next;
4811    }
4812    None
4813}
4814
4815#[cfg(test)]
4816mod tests {
4817    use super::*;
4818    use crate::font_reg::{FontRegistration, FontType};
4819
4820    fn make_id(n: u32) -> ObjectId {
4821        ObjectId::new(n, 0)
4822    }
4823
4824    #[test]
4825    fn test_path_object_default() {
4826        let p = PathObject::default();
4827        assert!(p.segments.is_empty());
4828        assert!(p.fill_color.is_none());
4829        assert!(p.stroke_color.is_none());
4830        assert_eq!(p.fill_mode, FillMode::None);
4831        assert!(p.active, "PathObject::default() should be active");
4832    }
4833
4834    #[test]
4835    fn test_path_object_create_at_has_move_to() {
4836        let p = PathObject::create_at(10.0, 20.0);
4837        assert_eq!(p.segments.len(), 1);
4838        assert!(matches!(p.segments[0], PathSegment::MoveTo(x, y) if x == 10.0 && y == 20.0));
4839    }
4840
4841    #[test]
4842    fn test_path_object_create_at_alias() {
4843        let p1 = PathObject::create_at(5.0, 7.0);
4844        let p2 = PathObject::page_obj_create_new_path(5.0, 7.0);
4845        assert_eq!(p1.segments.len(), p2.segments.len());
4846        assert!(matches!(p1.segments[0], PathSegment::MoveTo(x, y) if x == 5.0 && y == 7.0));
4847    }
4848
4849    #[test]
4850    fn test_path_object_create_rect_has_rect_segment() {
4851        let p = PathObject::create_rect(0.0, 0.0, 100.0, 50.0);
4852        assert_eq!(p.segments.len(), 1);
4853        assert!(matches!(
4854            p.segments[0],
4855            PathSegment::Rect(x, y, w, h) if x == 0.0 && y == 0.0 && w == 100.0 && h == 50.0
4856        ));
4857    }
4858
4859    #[test]
4860    fn test_path_object_create_rect_alias() {
4861        let p1 = PathObject::create_rect(1.0, 2.0, 3.0, 4.0);
4862        let p2 = PathObject::page_obj_create_new_rect(1.0, 2.0, 3.0, 4.0);
4863        assert_eq!(p1.segments.len(), p2.segments.len());
4864    }
4865
4866    #[test]
4867    fn test_page_obj_destroy_returns_not_supported() {
4868        let p = PageObject::Path(PathObject::default());
4869        let result = p.page_obj_destroy();
4870        assert!(result.is_err());
4871    }
4872
4873    #[test]
4874    fn test_text_object_default() {
4875        let t = TextObject::default();
4876        assert_eq!(t.font_size, 12.0);
4877        assert!(t.fill_color.is_some());
4878        assert!(t.text.is_empty());
4879        assert!(t.font_object_id.is_none());
4880        assert!(t.active, "TextObject::default() should be active");
4881    }
4882
4883    #[test]
4884    fn test_page_object_is_active_set_active() {
4885        let mut p = PageObject::Path(PathObject::default());
4886        assert!(p.is_active());
4887        p.set_active(false);
4888        assert!(!p.is_active());
4889        p.set_active(true);
4890        assert!(p.is_active());
4891
4892        let mut t = PageObject::Text(TextObject::default());
4893        assert!(t.is_active());
4894        t.set_active(false);
4895        assert!(!t.is_active());
4896    }
4897
4898    #[test]
4899    fn test_fill_mode_variants() {
4900        assert_ne!(FillMode::None, FillMode::EvenOdd);
4901        assert_ne!(FillMode::EvenOdd, FillMode::NonZero);
4902    }
4903
4904    #[test]
4905    fn test_page_object_enum_variants() {
4906        let p = PageObject::Path(PathObject::default());
4907        assert!(matches!(p, PageObject::Path(_)));
4908
4909        let t = PageObject::Text(TextObject::default());
4910        assert!(matches!(t, PageObject::Text(_)));
4911    }
4912
4913    // ------------------------------------------------------------------
4914    // New API tests
4915    // ------------------------------------------------------------------
4916
4917    #[test]
4918    fn test_text_object_with_standard_font_name_matches() {
4919        let t = TextObject::with_standard_font("Times-Roman", 14.0);
4920        assert_eq!(t.font_name(), &Name::from_bytes(b"Times-Roman".to_vec()));
4921        assert_eq!(t.font_size(), 14.0);
4922        assert!(t.font_object_id.is_none());
4923    }
4924
4925    #[test]
4926    fn test_text_object_with_font_uses_registration_id() {
4927        let reg = FontRegistration::new_standard(make_id(42), "Courier");
4928        let t = TextObject::with_font(&reg, 10.0);
4929        assert_eq!(t.font_object_id, Some(make_id(42)));
4930        assert_eq!(t.font_name(), &Name::from_bytes(b"Courier".to_vec()));
4931        assert_eq!(t.font_size(), 10.0);
4932    }
4933
4934    #[test]
4935    fn test_text_object_getters_return_correct_values() {
4936        let mut t = TextObject::with_standard_font("Helvetica", 12.0);
4937        t.text = "Hello, PDF!".into();
4938        t.rendering_mode = TextRenderingMode::FillStroke;
4939
4940        assert_eq!(t.text(), "Hello, PDF!");
4941        assert_eq!(t.font_size(), 12.0);
4942        assert_eq!(t.rendering_mode(), TextRenderingMode::FillStroke);
4943        assert_eq!(t.font_name(), &Name::from_bytes(b"Helvetica".to_vec()));
4944    }
4945
4946    #[test]
4947    fn test_text_object_with_embedded_font() {
4948        let reg = FontRegistration::new_embedded(
4949            make_id(99),
4950            "MyFont",
4951            FontType::TrueType,
4952            vec![1, 2, 3],
4953        );
4954        let t = TextObject::with_font(&reg, 16.0);
4955        assert_eq!(t.font_object_id, Some(make_id(99)));
4956        assert_eq!(t.font_name(), &Name::from_bytes(b"MyFont".to_vec()));
4957    }
4958
4959    // ------------------------------------------------------------------
4960    // Feature 1: PathOp ↔ PathSegment bridge
4961    // ------------------------------------------------------------------
4962
4963    #[test]
4964    fn test_round_trip_basic_path() {
4965        let ops = vec![
4966            PathOp::MoveTo { x: 10.0, y: 20.0 },
4967            PathOp::LineTo { x: 50.0, y: 60.0 },
4968            PathOp::Close,
4969        ];
4970        let segs = path_ops_to_segments(&ops);
4971        assert!(matches!(segs[0], PathSegment::MoveTo(10.0, 20.0)));
4972        assert!(matches!(segs[1], PathSegment::LineTo(50.0, 60.0)));
4973        assert!(matches!(segs[2], PathSegment::Close));
4974
4975        let back = segments_to_path_ops(&segs);
4976        assert_eq!(back.len(), ops.len());
4977        assert!(matches!(back[0], PathOp::MoveTo { x, y } if x == 10.0 && y == 20.0));
4978        assert!(matches!(back[1], PathOp::LineTo { x, y } if x == 50.0 && y == 60.0));
4979        assert!(matches!(back[2], PathOp::Close));
4980    }
4981
4982    #[test]
4983    fn test_rect_segment_expands_to_5_ops() {
4984        let segs = vec![PathSegment::Rect(0.0, 0.0, 10.0, 20.0)];
4985        let ops = segments_to_path_ops(&segs);
4986        assert_eq!(ops.len(), 5);
4987        assert!(matches!(ops[0], PathOp::MoveTo { x, y } if x == 0.0 && y == 0.0));
4988        assert!(matches!(ops[1], PathOp::LineTo { x, y } if x == 10.0 && y == 0.0));
4989        assert!(matches!(ops[2], PathOp::LineTo { x, y } if x == 10.0 && y == 20.0));
4990        assert!(matches!(ops[3], PathOp::LineTo { x, y } if x == 0.0 && y == 20.0));
4991        assert!(matches!(ops[4], PathOp::Close));
4992    }
4993
4994    #[test]
4995    fn test_f32_precision_preserved() {
4996        let ops = vec![PathOp::MoveTo {
4997            x: 0.5_f32,
4998            y: 1.25_f32,
4999        }];
5000        let segs = path_ops_to_segments(&ops);
5001        if let PathSegment::MoveTo(x, y) = segs[0] {
5002            assert!((x - 0.5_f64).abs() < 1e-6);
5003            assert!((y - 1.25_f64).abs() < 1e-6);
5004        } else {
5005            panic!("expected MoveTo");
5006        }
5007    }
5008
5009    // ------------------------------------------------------------------
5010    // Feature 3: Bitmap → ImageObject
5011    // ------------------------------------------------------------------
5012
5013    fn make_rgba_bitmap(pixels: &[(u8, u8, u8, u8)], w: u32, h: u32) -> Bitmap {
5014        let mut bmp = Bitmap::new(w, h, BitmapFormat::Rgba32);
5015        for (i, &(r, g, b, a)) in pixels.iter().enumerate() {
5016            let offset = i * 4;
5017            bmp.data[offset] = r;
5018            bmp.data[offset + 1] = g;
5019            bmp.data[offset + 2] = b;
5020            bmp.data[offset + 3] = a;
5021        }
5022        bmp
5023    }
5024
5025    #[test]
5026    fn test_bitmap_to_image_object_dimensions() {
5027        let bmp = Bitmap::new(2, 2, BitmapFormat::Rgba32);
5028        let obj = ImageObject::from_bitmap(&bmp, Matrix::identity());
5029        assert_eq!(obj.width, 2);
5030        assert_eq!(obj.height, 2);
5031        assert_eq!(obj.bits_per_component, 8);
5032    }
5033
5034    #[test]
5035    fn test_bitmap_to_image_object_unpremultiply() {
5036        // Premultiplied: r=128, g=64, b=0, a=128 → unpremultiplied: r≈255, g≈128, b=0
5037        let bmp = make_rgba_bitmap(&[(128, 64, 0, 128)], 1, 1);
5038        let obj = ImageObject::from_bitmap(&bmp, Matrix::identity());
5039        // Decompress the image data to verify the RGB values
5040        let rgb = rpdfium_codec::apply_filter_chain(
5041            &obj.image_data,
5042            &[(
5043                rpdfium_codec::DecodeFilter::Flate,
5044                rpdfium_codec::FilterParams::default(),
5045            )],
5046        )
5047        .unwrap();
5048        assert_eq!(rgb.len(), 3);
5049        assert_eq!(rgb[0], 255); // r: 128*255/128 = 255
5050        assert_eq!(rgb[1], 128); // g: 64*255/128 = 128 (rounded)
5051        assert_eq!(rgb[2], 0); // b: 0
5052    }
5053
5054    #[test]
5055    fn test_bitmap_to_image_object_opaque_pixel_unchanged() {
5056        let bmp = make_rgba_bitmap(&[(200, 100, 50, 255)], 1, 1);
5057        let obj = ImageObject::from_bitmap(&bmp, Matrix::identity());
5058        let rgb = rpdfium_codec::apply_filter_chain(
5059            &obj.image_data,
5060            &[(
5061                rpdfium_codec::DecodeFilter::Flate,
5062                rpdfium_codec::FilterParams::default(),
5063            )],
5064        )
5065        .unwrap();
5066        assert_eq!(rgb, vec![200, 100, 50]);
5067    }
5068
5069    #[test]
5070    fn test_bitmap_to_image_object_transparent_pixel_is_black() {
5071        let bmp = make_rgba_bitmap(&[(100, 100, 100, 0)], 1, 1);
5072        let obj = ImageObject::from_bitmap(&bmp, Matrix::identity());
5073        let rgb = rpdfium_codec::apply_filter_chain(
5074            &obj.image_data,
5075            &[(
5076                rpdfium_codec::DecodeFilter::Flate,
5077                rpdfium_codec::FilterParams::default(),
5078            )],
5079        )
5080        .unwrap();
5081        assert_eq!(rgb, vec![0, 0, 0]);
5082    }
5083
5084    #[test]
5085    fn test_bitmap_to_image_object_filter_is_flatedecode() {
5086        let bmp = Bitmap::new(1, 1, BitmapFormat::Rgba32);
5087        let obj = ImageObject::from_bitmap(&bmp, Matrix::identity());
5088        assert_eq!(obj.filter, Some(Name::from_bytes(b"FlateDecode".to_vec())));
5089    }
5090
5091    // ------------------------------------------------------------------
5092    // B1: PageObject setters / getters / transform / bounds
5093    // ------------------------------------------------------------------
5094
5095    #[test]
5096    fn test_path_object_set_fill_color() {
5097        let mut p = PathObject::default();
5098        assert!(p.fill_color().is_none());
5099        p.set_fill_color(Color::rgb(1.0, 0.0, 0.0));
5100        let c = p.fill_color().unwrap();
5101        assert_eq!(c.components, vec![1.0, 0.0, 0.0]);
5102    }
5103
5104    #[test]
5105    fn test_path_object_set_stroke_color() {
5106        let mut p = PathObject::default();
5107        assert!(p.stroke_color().is_none());
5108        p.set_stroke_color(Color::gray(0.5));
5109        let c = p.stroke_color().unwrap();
5110        assert_eq!(c.components, vec![0.5]);
5111    }
5112
5113    #[test]
5114    fn test_path_object_set_line_width() {
5115        let mut p = PathObject::default();
5116        assert_eq!(p.line_width(), 1.0);
5117        p.set_line_width(3.5);
5118        assert_eq!(p.line_width(), 3.5);
5119    }
5120
5121    #[test]
5122    fn test_path_object_set_line_join_and_cap() {
5123        let mut p = PathObject::default();
5124        assert_eq!(p.line_join(), 0);
5125        assert_eq!(p.line_cap(), 0);
5126        p.set_line_join(2);
5127        p.set_line_cap(1);
5128        assert_eq!(p.line_join(), 2);
5129        assert_eq!(p.line_cap(), 1);
5130    }
5131
5132    /// Upstream: TEST_F(PDFEditTest, LineCap)
5133    #[test]
5134    fn test_pdf_edit_line_cap() {
5135        let mut path = PathObject::default();
5136        // Default is butt cap (0).
5137        assert_eq!(path.line_cap(), 0);
5138
5139        // Set to projecting square (2) and verify.
5140        path.set_line_cap(2);
5141        assert_eq!(path.line_cap(), 2);
5142
5143        // Set to round (1) and verify.
5144        path.set_line_cap(1);
5145        assert_eq!(path.line_cap(), 1);
5146
5147        // Set back to butt (0) and verify.
5148        path.set_line_cap(0);
5149        assert_eq!(path.line_cap(), 0);
5150
5151        // PageObject dispatch: line_cap returns Some for Path, None for others.
5152        let page_obj = PageObject::Path(path);
5153        assert_eq!(page_obj.line_cap(), Some(0));
5154
5155        let text_obj = PageObject::Text(TextObject::default());
5156        assert_eq!(text_obj.line_cap(), None);
5157    }
5158
5159    #[test]
5160    fn test_path_object_bounds_simple_rect() {
5161        let p = PathObject {
5162            segments: vec![PathSegment::Rect(10.0, 20.0, 100.0, 50.0)],
5163            ..Default::default()
5164        };
5165        let b = p.bounds().unwrap();
5166        assert_eq!(b.left, 10.0);
5167        assert_eq!(b.bottom, 20.0);
5168        assert_eq!(b.right, 110.0);
5169        assert_eq!(b.top, 70.0);
5170    }
5171
5172    #[test]
5173    fn test_path_object_bounds_with_lines() {
5174        let p = PathObject {
5175            segments: vec![
5176                PathSegment::MoveTo(0.0, 0.0),
5177                PathSegment::LineTo(100.0, 0.0),
5178                PathSegment::LineTo(100.0, 50.0),
5179                PathSegment::Close,
5180            ],
5181            ..Default::default()
5182        };
5183        let b = p.bounds().unwrap();
5184        assert_eq!(b.left, 0.0);
5185        assert_eq!(b.bottom, 0.0);
5186        assert_eq!(b.right, 100.0);
5187        assert_eq!(b.top, 50.0);
5188    }
5189
5190    #[test]
5191    fn test_path_object_bounds_empty_returns_none() {
5192        let p = PathObject {
5193            segments: vec![PathSegment::Close],
5194            ..Default::default()
5195        };
5196        assert!(p.bounds().is_none());
5197    }
5198
5199    #[test]
5200    fn test_path_object_transform_translate() {
5201        let mut p = PathObject {
5202            segments: vec![
5203                PathSegment::MoveTo(10.0, 20.0),
5204                PathSegment::LineTo(30.0, 40.0),
5205            ],
5206            ..Default::default()
5207        };
5208        let m = Matrix::new(1.0, 0.0, 0.0, 1.0, 100.0, 200.0);
5209        p.transform(&m);
5210        assert!(
5211            matches!(p.segments[0], PathSegment::MoveTo(x, y) if (x - 110.0).abs() < 1e-9 && (y - 220.0).abs() < 1e-9)
5212        );
5213        assert!(
5214            matches!(p.segments[1], PathSegment::LineTo(x, y) if (x - 130.0).abs() < 1e-9 && (y - 240.0).abs() < 1e-9)
5215        );
5216    }
5217
5218    #[test]
5219    fn test_path_object_transform_scale_curve() {
5220        let mut p = PathObject {
5221            segments: vec![PathSegment::CurveTo(1.0, 2.0, 3.0, 4.0, 5.0, 6.0)],
5222            ..Default::default()
5223        };
5224        let m = Matrix::new(2.0, 0.0, 0.0, 3.0, 0.0, 0.0);
5225        p.transform(&m);
5226        if let PathSegment::CurveTo(x1, y1, x2, y2, x3, y3) = p.segments[0] {
5227            assert!((x1 - 2.0).abs() < 1e-9);
5228            assert!((y1 - 6.0).abs() < 1e-9);
5229            assert!((x2 - 6.0).abs() < 1e-9);
5230            assert!((y2 - 12.0).abs() < 1e-9);
5231            assert!((x3 - 10.0).abs() < 1e-9);
5232            assert!((y3 - 18.0).abs() < 1e-9);
5233        } else {
5234            panic!("expected CurveTo");
5235        }
5236    }
5237
5238    #[test]
5239    fn test_text_object_set_fill_and_stroke_color() {
5240        let mut t = TextObject::default();
5241        t.set_fill_color(Color::rgb(0.0, 1.0, 0.0));
5242        assert_eq!(t.fill_color().unwrap().components, vec![0.0, 1.0, 0.0]);
5243        t.set_stroke_color(Color::gray(0.25));
5244        assert_eq!(t.stroke_color().unwrap().components, vec![0.25]);
5245    }
5246
5247    #[test]
5248    fn test_text_object_set_font_size_and_text() {
5249        let mut t = TextObject::default();
5250        t.set_font_size(24.0);
5251        assert_eq!(t.font_size(), 24.0);
5252        t.set_text("new text");
5253        assert_eq!(t.text(), "new text");
5254    }
5255
5256    #[test]
5257    fn test_text_object_set_rendering_mode() {
5258        let mut t = TextObject::default();
5259        t.set_rendering_mode(TextRenderingMode::Stroke);
5260        assert_eq!(t.rendering_mode(), TextRenderingMode::Stroke);
5261    }
5262
5263    #[test]
5264    fn test_text_object_set_text_updates_content() {
5265        let mut t = TextObject::default();
5266        assert_eq!(t.text(), "");
5267        t.set_text("Hello, world!");
5268        assert_eq!(t.text(), "Hello, world!");
5269    }
5270
5271    #[test]
5272    fn test_text_object_set_text_accepts_string_and_str() {
5273        let mut t = TextObject::default();
5274        // &str via Into<String>
5275        t.set_text("foo");
5276        assert_eq!(t.text(), "foo");
5277        // String via Into<String>
5278        t.set_text(String::from("bar"));
5279        assert_eq!(t.text(), "bar");
5280    }
5281
5282    #[test]
5283    fn test_page_object_delegated_fill_color() {
5284        let mut po = PageObject::Path(PathObject::default());
5285        po.set_fill_color(Color::rgb(1.0, 0.5, 0.0));
5286        assert_eq!(po.fill_color().unwrap().components, vec![1.0, 0.5, 0.0]);
5287
5288        let mut to = PageObject::Text(TextObject::default());
5289        to.set_fill_color(Color::gray(0.8));
5290        assert_eq!(to.fill_color().unwrap().components, vec![0.8]);
5291    }
5292
5293    #[test]
5294    fn test_page_object_delegated_bounds() {
5295        let po = PageObject::Path(PathObject {
5296            segments: vec![PathSegment::Rect(0.0, 0.0, 50.0, 30.0)],
5297            ..Default::default()
5298        });
5299        let b = po.bounds().unwrap();
5300        assert_eq!(b.right, 50.0);
5301        assert_eq!(b.top, 30.0);
5302
5303        // Text objects return None for bounds
5304        let to = PageObject::Text(TextObject::default());
5305        assert!(to.bounds().is_none());
5306
5307        // Image object: unit square scaled to 100x200 at (50, 60)
5308        let img_matrix = Matrix::new(100.0, 0.0, 0.0, 200.0, 50.0, 60.0);
5309        let img = ImageObject {
5310            image_data: vec![],
5311            width: 1,
5312            height: 1,
5313            bits_per_component: 8,
5314            color_space: Name::from_bytes(b"DeviceRGB".to_vec()),
5315            matrix: img_matrix,
5316            filter: None,
5317            xobject_id: None,
5318            blend_mode: None,
5319            active: true,
5320            marks: vec![],
5321            clip_path: None,
5322        };
5323        let ib = PageObject::Image(img).bounds().unwrap();
5324        assert!((ib.left - 50.0).abs() < 1e-9);
5325        assert!((ib.bottom - 60.0).abs() < 1e-9);
5326        assert!((ib.right - 150.0).abs() < 1e-9);
5327        assert!((ib.top - 260.0).abs() < 1e-9);
5328
5329        // Form object: unit square rotated 90° at origin
5330        let form_matrix = Matrix::new(0.0, 1.0, -1.0, 0.0, 5.0, 10.0);
5331        let form = FormObject {
5332            xobject_id: rpdfium_parser::object::ObjectId::new(1, 0),
5333            matrix: form_matrix,
5334            blend_mode: None,
5335            active: true,
5336            marks: vec![],
5337            clip_path: None,
5338        };
5339        let fb = PageObject::Form(form).bounds().unwrap();
5340        // corners after 90° rotation + translate:
5341        // (0,0)→(5,10), (1,0)→(5,11), (0,1)→(4,10), (1,1)→(4,11)
5342        assert!((fb.left - 4.0).abs() < 1e-9);
5343        assert!((fb.bottom - 10.0).abs() < 1e-9);
5344        assert!((fb.right - 5.0).abs() < 1e-9);
5345        assert!((fb.top - 11.0).abs() < 1e-9);
5346    }
5347
5348    // ------------------------------------------------------------------
5349    // ImageObject read API
5350    // ------------------------------------------------------------------
5351
5352    #[test]
5353    fn test_image_object_metadata_round_trip() {
5354        let bmp = Bitmap::new(4, 3, BitmapFormat::Rgba32);
5355        let obj = ImageObject::from_bitmap(&bmp, Matrix::identity());
5356        let meta = obj.metadata();
5357        assert_eq!(meta.width, 4);
5358        assert_eq!(meta.height, 3);
5359        // Identity matrix: scale=1 user unit per image unit; 4px at 72dpi/user-unit = 288 dpi
5360        assert_eq!(meta.horizontal_dpi, 4.0 * 72.0);
5361        assert_eq!(meta.vertical_dpi, 3.0 * 72.0);
5362        assert_eq!(meta.bits_per_component, 8);
5363        assert_eq!(meta.color_space, Name::from_bytes(b"DeviceRGB".to_vec()));
5364        assert_eq!(meta.filter, Some(Name::from_bytes(b"FlateDecode".to_vec())));
5365        assert_eq!(meta.marked_content_id, -1);
5366    }
5367
5368    #[test]
5369    fn test_image_object_metadata_dpi_from_matrix() {
5370        // 100×200 image rendered into a 2.0 × 4.0 user-unit area (scale matrix).
5371        // 2 user units wide, 4 user units tall.
5372        // horizontal_dpi = 100 * 72 / 2.0 = 3600
5373        // vertical_dpi   = 200 * 72 / 4.0 = 3600
5374        let bmp = Bitmap::new(100, 200, BitmapFormat::Rgba32);
5375        let mat = Matrix {
5376            a: 2.0,
5377            b: 0.0,
5378            c: 0.0,
5379            d: 4.0,
5380            e: 0.0,
5381            f: 0.0,
5382        };
5383        let obj = ImageObject::from_bitmap(&bmp, mat);
5384        let meta = obj.metadata();
5385        assert!((meta.horizontal_dpi - 3600.0).abs() < 0.1);
5386        assert!((meta.vertical_dpi - 3600.0).abs() < 0.1);
5387    }
5388
5389    #[test]
5390    fn test_image_object_metadata_dpi_degenerate_matrix() {
5391        // Zero-scale matrix should produce 0.0 DPI (no division by zero).
5392        let bmp = Bitmap::new(10, 10, BitmapFormat::Rgba32);
5393        let mat = Matrix {
5394            a: 0.0,
5395            b: 0.0,
5396            c: 0.0,
5397            d: 0.0,
5398            e: 0.0,
5399            f: 0.0,
5400        };
5401        let obj = ImageObject::from_bitmap(&bmp, mat);
5402        let meta = obj.metadata();
5403        assert_eq!(meta.horizontal_dpi, 0.0);
5404        assert_eq!(meta.vertical_dpi, 0.0);
5405    }
5406
5407    #[test]
5408    fn test_image_object_raw_data_is_compressed() {
5409        let bmp = Bitmap::new(2, 2, BitmapFormat::Rgba32);
5410        let obj = ImageObject::from_bitmap(&bmp, Matrix::identity());
5411        // raw_data returns the FlateDecode-compressed bytes; can't be equal to decoded
5412        let raw = obj.raw_data();
5413        assert!(!raw.is_empty());
5414    }
5415
5416    #[test]
5417    fn test_image_object_decoded_data_matches_rgb_pixels() {
5418        // 1x1 opaque red pixel
5419        let mut bmp = Bitmap::new(1, 1, BitmapFormat::Rgba32);
5420        bmp.data[0] = 200; // R
5421        bmp.data[1] = 100; // G
5422        bmp.data[2] = 50; // B
5423        bmp.data[3] = 255; // A
5424        let obj = ImageObject::from_bitmap(&bmp, Matrix::identity());
5425        let decoded = obj.decoded_data().unwrap();
5426        // DeviceRGB packed → 3 bytes per pixel
5427        assert_eq!(decoded, vec![200, 100, 50]);
5428    }
5429
5430    #[test]
5431    fn test_image_object_decoded_data_no_filter_returns_raw() {
5432        let obj = ImageObject {
5433            image_data: vec![1, 2, 3, 4, 5],
5434            width: 5,
5435            height: 1,
5436            bits_per_component: 8,
5437            color_space: Name::from_bytes(b"DeviceGray".to_vec()),
5438            matrix: Matrix::identity(),
5439            filter: None,
5440            xobject_id: None,
5441            blend_mode: None,
5442            active: true,
5443            marks: vec![],
5444            clip_path: None,
5445        };
5446        let decoded = obj.decoded_data().unwrap();
5447        assert_eq!(decoded, vec![1, 2, 3, 4, 5]);
5448    }
5449
5450    #[test]
5451    fn test_image_object_to_bitmap_rgb() {
5452        let mut bmp_src = Bitmap::new(2, 1, BitmapFormat::Rgba32);
5453        // pixel 0: R=10, G=20, B=30, A=255
5454        bmp_src.data[0] = 10;
5455        bmp_src.data[1] = 20;
5456        bmp_src.data[2] = 30;
5457        bmp_src.data[3] = 255;
5458        // pixel 1: R=40, G=50, B=60, A=255
5459        bmp_src.data[4] = 40;
5460        bmp_src.data[5] = 50;
5461        bmp_src.data[6] = 60;
5462        bmp_src.data[7] = 255;
5463        let obj = ImageObject::from_bitmap(&bmp_src, Matrix::identity());
5464        let bmp_out = obj.to_bitmap().unwrap();
5465        assert_eq!(bmp_out.width, 2);
5466        assert_eq!(bmp_out.height, 1);
5467        assert_eq!(bmp_out.format, BitmapFormat::Rgba32);
5468        // pixel 0
5469        assert_eq!(bmp_out.data[0], 10);
5470        assert_eq!(bmp_out.data[1], 20);
5471        assert_eq!(bmp_out.data[2], 30);
5472        assert_eq!(bmp_out.data[3], 255);
5473        // pixel 1
5474        assert_eq!(bmp_out.data[4], 40);
5475        assert_eq!(bmp_out.data[5], 50);
5476        assert_eq!(bmp_out.data[6], 60);
5477        assert_eq!(bmp_out.data[7], 255);
5478    }
5479
5480    #[test]
5481    fn test_image_object_to_bitmap_gray() {
5482        let obj = ImageObject {
5483            image_data: vec![128, 64],
5484            width: 2,
5485            height: 1,
5486            bits_per_component: 8,
5487            color_space: Name::from_bytes(b"DeviceGray".to_vec()),
5488            matrix: Matrix::identity(),
5489            filter: None,
5490            xobject_id: None,
5491            blend_mode: None,
5492            active: true,
5493            marks: vec![],
5494            clip_path: None,
5495        };
5496        let bmp = obj.to_bitmap().unwrap();
5497        assert_eq!(bmp.format, BitmapFormat::Gray8);
5498        assert_eq!(bmp.data[0], 128);
5499        assert_eq!(bmp.data[1], 64);
5500    }
5501
5502    // ------------------------------------------------------------------
5503    // FormObject::from_xobject
5504    // ------------------------------------------------------------------
5505
5506    #[test]
5507    fn test_form_object_from_xobject_stores_id_and_matrix() {
5508        let id = ObjectId::new(42, 0);
5509        let m = Matrix::new(2.0, 0.0, 0.0, 3.0, 10.0, 20.0);
5510        let fo = FormObject::from_xobject(id, m);
5511        assert_eq!(fo.xobject_id, id);
5512        assert_eq!(fo.matrix.a, 2.0);
5513        assert_eq!(fo.matrix.f, 20.0);
5514        assert!(fo.active);
5515    }
5516
5517    // ------------------------------------------------------------------
5518    // Fix A: Dash pattern API
5519    // ------------------------------------------------------------------
5520
5521    #[test]
5522    fn test_dash_pattern_get_set_roundtrip() {
5523        let mut p = PathObject::default();
5524        assert!(p.dash_pattern().is_none());
5525        assert_eq!(p.dash_phase(), 0.0);
5526        assert_eq!(p.dash_array(), &[] as &[f32]);
5527
5528        p.set_dash_array(vec![5.0, 3.0], 1.0);
5529        assert_eq!(p.dash_array(), &[5.0, 3.0]);
5530        assert_eq!(p.dash_phase(), 1.0);
5531
5532        // Update phase only
5533        p.set_dash_phase(2.5);
5534        assert_eq!(p.dash_phase(), 2.5);
5535
5536        // Clear with empty array
5537        p.set_dash_array(vec![], 0.0);
5538        assert!(p.dash_pattern().is_none());
5539    }
5540
5541    #[test]
5542    fn test_dash_pattern_set_and_clear() {
5543        let mut p = PathObject::default();
5544        p.set_dash_pattern(Some(DashPattern {
5545            array: vec![10.0, 5.0],
5546            phase: 3.0,
5547        }));
5548        assert_eq!(p.dash_pattern().unwrap().array, vec![10.0, 5.0]);
5549        p.set_dash_pattern(None);
5550        assert!(p.dash_pattern().is_none());
5551    }
5552
5553    // ------------------------------------------------------------------
5554    // Fix B: Transform on Text / Image / Form
5555    // ------------------------------------------------------------------
5556
5557    #[test]
5558    fn test_text_object_transform_translates_matrix() {
5559        let mut t = TextObject::default();
5560        // Start at identity, translate by (10, 20)
5561        let m = Matrix::new(1.0, 0.0, 0.0, 1.0, 10.0, 20.0);
5562        t.transform(&m);
5563        assert!((t.matrix.e - 10.0).abs() < 1e-9);
5564        assert!((t.matrix.f - 20.0).abs() < 1e-9);
5565    }
5566
5567    #[test]
5568    fn test_image_object_transform_scales_matrix() {
5569        let bmp = Bitmap::new(2, 2, BitmapFormat::Rgba32);
5570        let mut img = ImageObject::from_bitmap(&bmp, Matrix::identity());
5571        let scale = Matrix::new(2.0, 0.0, 0.0, 3.0, 0.0, 0.0);
5572        img.transform(&scale);
5573        assert!((img.matrix.a - 2.0).abs() < 1e-9);
5574        assert!((img.matrix.d - 3.0).abs() < 1e-9);
5575    }
5576
5577    #[test]
5578    fn test_form_object_transform_updates_matrix() {
5579        let id = ObjectId::new(1, 0);
5580        let mut fo = FormObject::from_xobject(id, Matrix::identity());
5581        let m = Matrix::new(1.0, 0.0, 0.0, 1.0, 5.0, 10.0);
5582        fo.transform(&m);
5583        assert!((fo.matrix.e - 5.0).abs() < 1e-9);
5584        assert!((fo.matrix.f - 10.0).abs() < 1e-9);
5585    }
5586
5587    #[test]
5588    fn test_page_object_transform_dispatches_to_all_types() {
5589        let m = Matrix::new(1.0, 0.0, 0.0, 1.0, 3.0, 4.0);
5590
5591        let mut t = PageObject::Text(TextObject::default());
5592        t.transform(&m);
5593        if let PageObject::Text(inner) = &t {
5594            assert!((inner.matrix.e - 3.0).abs() < 1e-9);
5595        }
5596
5597        let bmp = Bitmap::new(1, 1, BitmapFormat::Rgba32);
5598        let mut img = PageObject::Image(ImageObject::from_bitmap(&bmp, Matrix::identity()));
5599        img.transform(&m);
5600        if let PageObject::Image(inner) = &img {
5601            assert!((inner.matrix.e - 3.0).abs() < 1e-9);
5602        }
5603
5604        let id = ObjectId::new(1, 0);
5605        let mut fo = PageObject::Form(FormObject::from_xobject(id, Matrix::identity()));
5606        fo.transform(&m);
5607        if let PageObject::Form(inner) = &fo {
5608            assert!((inner.matrix.e - 3.0).abs() < 1e-9);
5609        }
5610    }
5611
5612    // ------------------------------------------------------------------
5613    // Fix C: Blend mode API
5614    // ------------------------------------------------------------------
5615
5616    #[test]
5617    fn test_blend_mode_get_set_path() {
5618        let mut p = PathObject::default();
5619        assert!(p.blend_mode().is_none());
5620        p.set_blend_mode(Some(BlendMode::Multiply));
5621        assert_eq!(p.blend_mode(), Some(BlendMode::Multiply));
5622        p.set_blend_mode(None);
5623        assert!(p.blend_mode().is_none());
5624    }
5625
5626    #[test]
5627    fn test_blend_mode_page_object_enum() {
5628        let mut po = PageObject::Path(PathObject::default());
5629        assert!(po.blend_mode().is_none());
5630        po.set_blend_mode(Some(BlendMode::Screen));
5631        assert_eq!(po.blend_mode(), Some(BlendMode::Screen));
5632
5633        let mut to = PageObject::Text(TextObject::default());
5634        to.set_blend_mode(Some(BlendMode::Overlay));
5635        assert_eq!(to.blend_mode(), Some(BlendMode::Overlay));
5636    }
5637
5638    // ------------------------------------------------------------------
5639    // Item 2: parse_jpeg_dimensions + ImageObject::from_jpeg_bytes
5640    // ------------------------------------------------------------------
5641
5642    /// Build a minimal well-formed JPEG byte sequence with a SOF0 segment
5643    /// declaring the given `width` × `height` dimensions.
5644    fn make_minimal_jpeg(width: u16, height: u16) -> Vec<u8> {
5645        let mut data: Vec<u8> = Vec::new();
5646        // SOI
5647        data.extend_from_slice(&[0xFF, 0xD8]);
5648        // SOF0 marker
5649        data.extend_from_slice(&[0xFF, 0xC0]);
5650        // Segment length: 2 (len) + 1 (precision) + 2 (height) + 2 (width) + 1 (components) = 8
5651        let seg_len: u16 = 8;
5652        data.extend_from_slice(&seg_len.to_be_bytes());
5653        // Precision (8-bit)
5654        data.push(8);
5655        // Height
5656        data.extend_from_slice(&height.to_be_bytes());
5657        // Width
5658        data.extend_from_slice(&width.to_be_bytes());
5659        // Number of components (3 = RGB)
5660        data.push(3);
5661        // EOI
5662        data.extend_from_slice(&[0xFF, 0xD9]);
5663        data
5664    }
5665
5666    #[test]
5667    fn test_parse_jpeg_dimensions_baseline() {
5668        let jpeg = make_minimal_jpeg(320, 240);
5669        let result = parse_jpeg_dimensions(&jpeg);
5670        assert_eq!(result, Some((320, 240)));
5671    }
5672
5673    #[test]
5674    fn test_parse_jpeg_dimensions_progressive() {
5675        // Build a JPEG with SOF2 (progressive) instead of SOF0
5676        let mut data: Vec<u8> = Vec::new();
5677        data.extend_from_slice(&[0xFF, 0xD8]);
5678        data.extend_from_slice(&[0xFF, 0xC2]); // SOF2
5679        let seg_len: u16 = 8;
5680        data.extend_from_slice(&seg_len.to_be_bytes());
5681        data.push(8); // precision
5682        data.extend_from_slice(&100u16.to_be_bytes()); // height
5683        data.extend_from_slice(&200u16.to_be_bytes()); // width
5684        data.push(3);
5685        data.extend_from_slice(&[0xFF, 0xD9]);
5686        assert_eq!(parse_jpeg_dimensions(&data), Some((200, 100)));
5687    }
5688
5689    #[test]
5690    fn test_parse_jpeg_dimensions_sof_after_app0() {
5691        // Realistic JPEG: SOI + APP0 + SOF0
5692        let mut data: Vec<u8> = Vec::new();
5693        // SOI
5694        data.extend_from_slice(&[0xFF, 0xD8]);
5695        // APP0 marker with 16 bytes of dummy content
5696        data.extend_from_slice(&[0xFF, 0xE0]);
5697        let app0_len: u16 = 18; // 2 (len) + 16 bytes payload
5698        data.extend_from_slice(&app0_len.to_be_bytes());
5699        data.extend_from_slice(&[0u8; 16]);
5700        // SOF0
5701        data.extend_from_slice(&[0xFF, 0xC0]);
5702        let seg_len: u16 = 8;
5703        data.extend_from_slice(&seg_len.to_be_bytes());
5704        data.push(8); // precision
5705        data.extend_from_slice(&480u16.to_be_bytes()); // height
5706        data.extend_from_slice(&640u16.to_be_bytes()); // width
5707        data.push(3);
5708        // EOI
5709        data.extend_from_slice(&[0xFF, 0xD9]);
5710        assert_eq!(parse_jpeg_dimensions(&data), Some((640, 480)));
5711    }
5712
5713    #[test]
5714    fn test_parse_jpeg_dimensions_invalid_no_soi() {
5715        let data = vec![0x00, 0x01, 0x02];
5716        assert_eq!(parse_jpeg_dimensions(&data), None);
5717    }
5718
5719    #[test]
5720    fn test_parse_jpeg_dimensions_empty() {
5721        assert_eq!(parse_jpeg_dimensions(&[]), None);
5722    }
5723
5724    #[test]
5725    fn test_parse_jpeg_dimensions_truncated() {
5726        // SOI only, no further markers
5727        let data = vec![0xFF, 0xD8];
5728        assert_eq!(parse_jpeg_dimensions(&data), None);
5729    }
5730
5731    #[test]
5732    fn test_from_jpeg_bytes_creates_image_object() {
5733        let jpeg = make_minimal_jpeg(100, 75);
5734        let obj =
5735            ImageObject::from_jpeg_bytes(jpeg, Matrix::identity()).expect("should parse JPEG");
5736        assert_eq!(obj.width, 100);
5737        assert_eq!(obj.height, 75);
5738        assert_eq!(
5739            obj.filter.as_ref().map(|n| n.as_bytes()),
5740            Some(b"DCTDecode".as_ref())
5741        );
5742        assert_eq!(obj.bits_per_component, 8);
5743        assert!(obj.active);
5744        assert!(obj.xobject_id.is_none());
5745    }
5746
5747    #[test]
5748    fn test_from_jpeg_bytes_metadata_dimensions() {
5749        let jpeg = make_minimal_jpeg(800, 600);
5750        let obj = ImageObject::from_jpeg_bytes(jpeg, Matrix::new(800.0, 0.0, 0.0, 600.0, 0.0, 0.0))
5751            .expect("should parse JPEG");
5752        let meta = obj.metadata();
5753        assert_eq!(meta.width, 800);
5754        assert_eq!(meta.height, 600);
5755        assert_eq!(
5756            meta.filter.as_ref().map(|n| n.as_bytes()),
5757            Some(b"DCTDecode".as_ref())
5758        );
5759        // DPI should be ~72 when width/height exactly match matrix scale.
5760        assert!(
5761            (meta.horizontal_dpi - 72.0).abs() < 0.1,
5762            "horizontal_dpi={}",
5763            meta.horizontal_dpi
5764        );
5765        assert!(
5766            (meta.vertical_dpi - 72.0).abs() < 0.1,
5767            "vertical_dpi={}",
5768            meta.vertical_dpi
5769        );
5770    }
5771
5772    #[test]
5773    fn test_from_jpeg_bytes_invalid_data_returns_error() {
5774        let bad_data = vec![0xDE, 0xAD, 0xBE, 0xEF];
5775        let result = ImageObject::from_jpeg_bytes(bad_data, Matrix::identity());
5776        assert!(result.is_err());
5777    }
5778
5779    #[test]
5780    fn test_from_jpeg_bytes_raw_data_preserved() {
5781        // raw_data() should return the original JPEG bytes unchanged.
5782        let jpeg = make_minimal_jpeg(50, 50);
5783        let jpeg_clone = jpeg.clone();
5784        let obj = ImageObject::from_jpeg_bytes(jpeg, Matrix::identity()).unwrap();
5785        assert_eq!(obj.raw_data(), jpeg_clone.as_slice());
5786    }
5787
5788    // ------------------------------------------------------------------
5789    // Content Marks API  (FPDFPageObj*Mark* functions — tagged PDF)
5790    // ------------------------------------------------------------------
5791
5792    #[test]
5793    fn test_content_mark_add_and_count() {
5794        let mut p = PathObject::default();
5795        assert_eq!(p.mark_count(), 0);
5796        p.add_mark("Span");
5797        assert_eq!(p.mark_count(), 1);
5798        p.add_mark("P");
5799        assert_eq!(p.mark_count(), 2);
5800    }
5801
5802    #[test]
5803    fn test_content_mark_get_by_index() {
5804        let mut p = PathObject::default();
5805        p.add_mark("Figure");
5806        p.add_mark("Artifact");
5807
5808        let m0 = p.mark(0).unwrap();
5809        assert_eq!(m0.name, "Figure");
5810
5811        let m1 = p.page_obj_get_mark(1).unwrap();
5812        assert_eq!(m1.name, "Artifact");
5813
5814        // out-of-range returns None
5815        assert!(p.mark(2).is_none());
5816        assert!(p.page_obj_get_mark(99).is_none());
5817    }
5818
5819    #[test]
5820    fn test_content_mark_int_param_roundtrip() {
5821        let mut mark = ContentMark::new("Span");
5822        assert_eq!(mark.param_count(), 0);
5823        assert_eq!(mark.page_obj_mark_count_params(), 0);
5824
5825        mark.set_int_param("MCID", 7);
5826        assert_eq!(mark.param_count(), 1);
5827        assert_eq!(mark.int_param("MCID"), Some(7));
5828        assert_eq!(mark.page_obj_mark_get_param_int_value("MCID"), Some(7)); // alias
5829
5830        // Replace existing key — still only 1 entry
5831        mark.set_int_param("MCID", 42);
5832        assert_eq!(mark.param_count(), 1);
5833        assert_eq!(mark.int_param("MCID"), Some(42));
5834
5835        // Wrong type returns None
5836        assert!(mark.string_param("MCID").is_none());
5837        assert!(mark.blob_param("MCID").is_none());
5838    }
5839
5840    #[test]
5841    fn test_content_mark_string_param_roundtrip() {
5842        let mut mark = ContentMark::new("Link");
5843        mark.set_string_param("ActualText", "Hello world");
5844        assert_eq!(mark.string_param("ActualText"), Some("Hello world"));
5845        assert_eq!(
5846            mark.page_obj_mark_get_param_string_value("ActualText"),
5847            Some("Hello world")
5848        ); // alias
5849        assert_eq!(mark.param_key(0), Some("ActualText"));
5850        assert_eq!(mark.page_obj_mark_get_param_key(0), Some("ActualText"));
5851
5852        // Replace
5853        mark.set_string_param("ActualText", "Goodbye");
5854        assert_eq!(mark.string_param("ActualText"), Some("Goodbye"));
5855        assert_eq!(mark.param_count(), 1);
5856    }
5857
5858    #[test]
5859    fn test_content_mark_blob_param_roundtrip() {
5860        let mut mark = ContentMark::new("CustomTag");
5861        mark.set_blob_param("Raw", vec![0xDE, 0xAD, 0xBE, 0xEF]);
5862        assert_eq!(mark.blob_param("Raw"), Some(&[0xDE, 0xAD, 0xBE, 0xEF][..]));
5863        assert_eq!(
5864            mark.page_obj_mark_get_param_blob_value("Raw"),
5865            Some(&[0xDE, 0xAD, 0xBE, 0xEF][..])
5866        ); // alias
5867
5868        // Replace blob
5869        mark.set_blob_param("Raw", vec![0x01, 0x02]);
5870        assert_eq!(mark.blob_param("Raw"), Some(&[0x01, 0x02][..]));
5871        assert_eq!(mark.param_count(), 1);
5872    }
5873
5874    #[test]
5875    fn test_content_mark_marked_content_id() {
5876        let mut p = PathObject::default();
5877
5878        // No marks → None
5879        assert!(p.marked_content_id().is_none());
5880
5881        // Mark without MCID → None
5882        p.add_mark("NoId");
5883        assert!(p.marked_content_id().is_none());
5884
5885        // Mark with MCID
5886        p.add_mark("Span").set_int_param("MCID", 5);
5887        assert_eq!(p.marked_content_id(), Some(5));
5888
5889        // ContentMark itself
5890        let mut m = ContentMark::new("P");
5891        m.set_int_param("MCID", 99);
5892        assert_eq!(m.marked_content_id(), Some(99));
5893    }
5894
5895    #[test]
5896    fn test_content_mark_remove_mark_valid_and_invalid_index() {
5897        let mut p = PathObject::default();
5898        p.add_mark("A");
5899        p.add_mark("B");
5900
5901        // Invalid index returns false
5902        assert!(!p.remove_mark(5));
5903        assert_eq!(p.mark_count(), 2);
5904
5905        // Valid index returns true and decrements count
5906        assert!(p.remove_mark(0));
5907        assert_eq!(p.mark_count(), 1);
5908        assert_eq!(p.mark(0).unwrap().name, "B");
5909    }
5910
5911    #[test]
5912    fn test_content_mark_remove_param() {
5913        let mut mark = ContentMark::new("Tag");
5914        mark.set_int_param("A", 1);
5915        mark.set_string_param("B", "hello");
5916        assert_eq!(mark.param_count(), 2);
5917
5918        // Remove existing key returns true
5919        assert!(mark.remove_param("A"));
5920        assert_eq!(mark.param_count(), 1);
5921        assert!(mark.int_param("A").is_none());
5922
5923        // Remove non-existent key returns false
5924        assert!(!mark.remove_param("NotHere"));
5925        assert_eq!(mark.param_count(), 1);
5926    }
5927
5928    #[test]
5929    fn test_content_mark_page_object_enum_dispatch() {
5930        // Test that PageObject enum properly dispatches all content-mark methods.
5931        let mut po = PageObject::Text(TextObject::default());
5932
5933        assert_eq!(po.mark_count(), 0);
5934        po.add_mark("Span").set_int_param("MCID", 3);
5935        assert_eq!(po.mark_count(), 1);
5936        assert_eq!(po.mark(0).unwrap().name, "Span");
5937        assert_eq!(po.marked_content_id(), Some(3));
5938
5939        assert!(po.remove_mark(0));
5940        assert_eq!(po.mark_count(), 0);
5941        assert!(po.marked_content_id().is_none());
5942    }
5943
5944    // -----------------------------------------------------------------------
5945    // ClipPath tests
5946    // -----------------------------------------------------------------------
5947
5948    #[test]
5949    fn test_clip_path_from_rect() {
5950        let clip = ClipPath::from_rect(10.0, 20.0, 110.0, 120.0);
5951        assert_eq!(clip.path_count(), 1);
5952        assert_eq!(clip.clip_path_count_paths(), 1);
5953        assert_eq!(clip.segment_count(0), 1);
5954        assert_eq!(clip.clip_path_count_path_segments(0), 1);
5955
5956        // Check that the rect segment has correct dimensions
5957        match clip.segment(0, 0) {
5958            Some(PathSegment::Rect(x, y, w, h)) => {
5959                assert!((x - 10.0).abs() < 1e-9);
5960                assert!((y - 20.0).abs() < 1e-9);
5961                assert!((w - 100.0).abs() < 1e-9);
5962                assert!((h - 100.0).abs() < 1e-9);
5963            }
5964            other => panic!("expected Rect segment, got {:?}", other),
5965        }
5966    }
5967
5968    #[test]
5969    fn test_clip_path_from_rect_alias() {
5970        let clip = ClipPath::create_clip_path(0.0, 0.0, 100.0, 200.0);
5971        assert_eq!(clip.path_count(), 1);
5972        match clip.segment(0, 0) {
5973            Some(PathSegment::Rect(x, y, w, h)) => {
5974                assert!((x - 0.0).abs() < 1e-9);
5975                assert!((y - 0.0).abs() < 1e-9);
5976                assert!((w - 100.0).abs() < 1e-9);
5977                assert!((h - 200.0).abs() < 1e-9);
5978            }
5979            other => panic!("expected Rect, got {:?}", other),
5980        }
5981    }
5982
5983    #[test]
5984    fn test_clip_path_multi_sub_paths() {
5985        let clip = ClipPath::new(vec![
5986            vec![
5987                PathSegment::MoveTo(0.0, 0.0),
5988                PathSegment::LineTo(100.0, 0.0),
5989                PathSegment::LineTo(100.0, 100.0),
5990                PathSegment::Close,
5991            ],
5992            vec![
5993                PathSegment::MoveTo(200.0, 200.0),
5994                PathSegment::LineTo(300.0, 200.0),
5995                PathSegment::Close,
5996            ],
5997        ]);
5998
5999        assert_eq!(clip.path_count(), 2);
6000        assert_eq!(clip.segment_count(0), 4);
6001        assert_eq!(clip.segment_count(1), 3);
6002        assert_eq!(clip.segment_count(999), 0); // out of range
6003
6004        assert!(clip.segment(0, 0).is_some());
6005        assert!(clip.segment(0, 3).is_some());
6006        assert!(clip.segment(0, 4).is_none());
6007        assert!(clip.segment(1, 2).is_some());
6008        assert!(clip.segment(999, 0).is_none());
6009
6010        // Aliases
6011        assert!(clip.clip_path_get_path_segment(0, 0).is_some());
6012    }
6013
6014    #[test]
6015    fn test_clip_path_transform() {
6016        let mut clip = ClipPath::from_rect(0.0, 0.0, 100.0, 100.0);
6017        let matrix = Matrix::new(2.0, 0.0, 0.0, 2.0, 10.0, 20.0);
6018        clip.transform(&matrix);
6019
6020        match clip.segment(0, 0) {
6021            Some(PathSegment::Rect(x, y, w, h)) => {
6022                assert!((x - 10.0).abs() < 1e-9, "x={x}");
6023                assert!((y - 20.0).abs() < 1e-9, "y={y}");
6024                assert!((w - 200.0).abs() < 1e-9, "w={w}");
6025                assert!((h - 200.0).abs() < 1e-9, "h={h}");
6026            }
6027            other => panic!("expected Rect, got {:?}", other),
6028        }
6029    }
6030
6031    #[test]
6032    fn test_page_object_clip_path_dispatch() {
6033        let mut po = PageObject::Path(PathObject::default());
6034        assert!(po.clip_path().is_none());
6035        assert!(po.page_obj_get_clip_path().is_none());
6036
6037        let clip = ClipPath::from_rect(0.0, 0.0, 100.0, 100.0);
6038        po.set_clip_path(Some(clip));
6039        assert!(po.clip_path().is_some());
6040        assert_eq!(po.clip_path().unwrap().path_count(), 1);
6041
6042        // Transform the clip path
6043        let matrix = Matrix::new(1.0, 0.0, 0.0, 1.0, 50.0, 50.0);
6044        po.transform_clip_path(&matrix);
6045        // Should still exist
6046        assert!(po.clip_path().is_some());
6047
6048        // Clear
6049        po.set_clip_path(None);
6050        assert!(po.clip_path().is_none());
6051
6052        // No-op transform on no clip
6053        po.transform_clip_path(&Matrix::identity()); // should not panic
6054    }
6055
6056    #[test]
6057    fn test_page_object_clip_path_all_types() {
6058        // Verify clip_path field works for all four page object types
6059        for mut po in [
6060            PageObject::Path(PathObject::default()),
6061            PageObject::Text(TextObject::default()),
6062            PageObject::Image(ImageObject {
6063                image_data: vec![],
6064                width: 1,
6065                height: 1,
6066                bits_per_component: 8,
6067                color_space: Name::from_bytes(b"DeviceRGB".to_vec()),
6068                matrix: Matrix::identity(),
6069                filter: None,
6070                xobject_id: None,
6071                blend_mode: None,
6072                active: true,
6073                marks: Vec::new(),
6074                clip_path: None,
6075            }),
6076            PageObject::Form(FormObject::from_xobject(make_id(1), Matrix::identity())),
6077        ] {
6078            assert!(po.clip_path().is_none());
6079            po.set_clip_path(Some(ClipPath::from_rect(0.0, 0.0, 50.0, 50.0)));
6080            assert!(po.clip_path().is_some());
6081        }
6082    }
6083
6084    // ------------------------------------------------------------------
6085    // Gap 3: has_transparency() (upstream-aligned primary)
6086    // ------------------------------------------------------------------
6087
6088    #[test]
6089    fn test_path_object_has_transparency_false_when_no_blend_mode() {
6090        let p = PathObject::default();
6091        assert!(!p.has_transparency());
6092    }
6093
6094    #[test]
6095    fn test_path_object_has_transparency_true_for_non_normal_blend_mode() {
6096        let mut p = PathObject::default();
6097        p.set_blend_mode(Some(BlendMode::Multiply));
6098        assert!(p.has_transparency());
6099
6100        // Normal blend mode → not transparent
6101        p.set_blend_mode(Some(BlendMode::Normal));
6102        assert!(!p.has_transparency());
6103    }
6104
6105    #[test]
6106    fn test_page_object_has_transparency_dispatches_all_types() {
6107        // Path: non-Normal blend mode → transparent
6108        let mut path_po = PageObject::Path(PathObject::default());
6109        assert!(!path_po.has_transparency());
6110        path_po.set_blend_mode(Some(BlendMode::Screen));
6111        assert!(path_po.has_transparency());
6112
6113        // Text: same behaviour
6114        let mut text_po = PageObject::Text(TextObject::default());
6115        assert!(!text_po.has_transparency());
6116        text_po.set_blend_mode(Some(BlendMode::Overlay));
6117        assert!(text_po.has_transparency());
6118
6119        // Image: transparent when blend mode set
6120        let img_po = PageObject::Image(ImageObject {
6121            image_data: vec![],
6122            width: 1,
6123            height: 1,
6124            bits_per_component: 8,
6125            color_space: Name::from_bytes(b"DeviceRGB".to_vec()),
6126            matrix: Matrix::identity(),
6127            filter: None,
6128            xobject_id: None,
6129            blend_mode: Some(BlendMode::Darken),
6130            active: true,
6131            marks: Vec::new(),
6132            clip_path: None,
6133        });
6134        assert!(img_po.has_transparency());
6135
6136        // Form: transparent when blend mode set
6137        let mut form_po =
6138            PageObject::Form(FormObject::from_xobject(make_id(2), Matrix::identity()));
6139        assert!(!form_po.has_transparency());
6140        form_po.set_blend_mode(Some(BlendMode::Luminosity));
6141        assert!(form_po.has_transparency());
6142    }
6143
6144    // ------------------------------------------------------------------
6145    // Gap 2: matrix() / set_matrix() on page objects
6146    // ------------------------------------------------------------------
6147
6148    #[test]
6149    fn test_image_object_matrix_get_set() {
6150        let scale = Matrix::new(100.0, 0.0, 0.0, 150.0, 50.0, 75.0);
6151        let mut img = ImageObject {
6152            image_data: vec![],
6153            width: 10,
6154            height: 10,
6155            bits_per_component: 8,
6156            color_space: Name::from_bytes(b"DeviceRGB".to_vec()),
6157            matrix: Matrix::identity(),
6158            filter: None,
6159            xobject_id: None,
6160            blend_mode: None,
6161            active: true,
6162            marks: Vec::new(),
6163            clip_path: None,
6164        };
6165        // Default matrix is identity
6166        assert_eq!(*img.matrix(), Matrix::identity());
6167        // Set a new matrix
6168        img.set_matrix(scale);
6169        assert_eq!(img.matrix().a, 100.0);
6170        assert_eq!(img.matrix().d, 150.0);
6171        assert_eq!(img.matrix().e, 50.0);
6172        assert_eq!(img.matrix().f, 75.0);
6173    }
6174
6175    #[test]
6176    fn test_form_object_matrix_get_set() {
6177        let m = Matrix::new(2.0, 0.0, 0.0, 3.0, 10.0, 20.0);
6178        let mut f = FormObject::from_xobject(make_id(5), Matrix::identity());
6179        assert_eq!(*f.matrix(), Matrix::identity());
6180        f.set_matrix(m);
6181        assert_eq!(f.matrix().a, 2.0);
6182        assert_eq!(f.matrix().d, 3.0);
6183    }
6184
6185    #[test]
6186    fn test_page_object_matrix_dispatch() {
6187        let m = Matrix::new(4.0, 0.0, 0.0, 4.0, 0.0, 0.0);
6188
6189        // Image: matrix is stored and returned
6190        let mut img_po = PageObject::Image(ImageObject {
6191            image_data: vec![],
6192            width: 1,
6193            height: 1,
6194            bits_per_component: 8,
6195            color_space: Name::from_bytes(b"DeviceRGB".to_vec()),
6196            matrix: Matrix::identity(),
6197            filter: None,
6198            xobject_id: None,
6199            blend_mode: None,
6200            active: true,
6201            marks: Vec::new(),
6202            clip_path: None,
6203        });
6204        assert_eq!(img_po.matrix(), Matrix::identity());
6205        img_po.set_matrix(m);
6206        assert_eq!(img_po.matrix().a, 4.0);
6207
6208        // Path: always returns identity; set_matrix is a no-op
6209        let mut path_po = PageObject::Path(PathObject::default());
6210        assert_eq!(path_po.matrix(), Matrix::identity());
6211        path_po.set_matrix(m); // no-op
6212        assert_eq!(path_po.matrix(), Matrix::identity());
6213
6214        // Text: always returns identity
6215        let text_po = PageObject::Text(TextObject::default());
6216        assert_eq!(text_po.matrix(), Matrix::identity());
6217    }
6218
6219    // ------------------------------------------------------------------
6220    // Task 2: object_type() — FPDFPageObj_GetType
6221    // ------------------------------------------------------------------
6222
6223    #[test]
6224    fn test_path_object_type_is_2() {
6225        let p = PathObject::default();
6226        assert_eq!(p.object_type(), 2);
6227        assert_eq!(p.page_obj_get_type(), 2);
6228        let po = PageObject::Path(PathObject::default());
6229        assert_eq!(po.object_type(), 2);
6230        assert_eq!(po.page_obj_get_type(), 2);
6231    }
6232
6233    #[test]
6234    fn test_text_object_type_is_1() {
6235        let t = TextObject::default();
6236        assert_eq!(t.object_type(), 1);
6237        assert_eq!(t.page_obj_get_type(), 1);
6238        let po = PageObject::Text(TextObject::default());
6239        assert_eq!(po.object_type(), 1);
6240        assert_eq!(po.page_obj_get_type(), 1);
6241    }
6242
6243    #[test]
6244    fn test_image_object_type_is_3() {
6245        let img = ImageObject {
6246            image_data: vec![],
6247            width: 1,
6248            height: 1,
6249            bits_per_component: 8,
6250            color_space: Name::from_bytes(b"DeviceRGB".to_vec()),
6251            matrix: Matrix::identity(),
6252            filter: None,
6253            xobject_id: None,
6254            blend_mode: None,
6255            active: true,
6256            marks: Vec::new(),
6257            clip_path: None,
6258        };
6259        assert_eq!(img.object_type(), 3);
6260        assert_eq!(img.page_obj_get_type(), 3);
6261        let img2 = ImageObject {
6262            image_data: vec![],
6263            width: 1,
6264            height: 1,
6265            bits_per_component: 8,
6266            color_space: Name::from_bytes(b"DeviceRGB".to_vec()),
6267            matrix: Matrix::identity(),
6268            filter: None,
6269            xobject_id: None,
6270            blend_mode: None,
6271            active: true,
6272            marks: Vec::new(),
6273            clip_path: None,
6274        };
6275        let po = PageObject::Image(img2);
6276        assert_eq!(po.object_type(), 3);
6277        assert_eq!(po.page_obj_get_type(), 3);
6278    }
6279
6280    #[test]
6281    fn test_form_object_type_is_5() {
6282        let fo = FormObject::from_xobject(ObjectId::new(1, 0), Matrix::identity());
6283        assert_eq!(fo.object_type(), 5);
6284        assert_eq!(fo.page_obj_get_type(), 5);
6285        let fo2 = FormObject::from_xobject(ObjectId::new(2, 0), Matrix::identity());
6286        let po = PageObject::Form(fo2);
6287        assert_eq!(po.object_type(), 5);
6288        assert_eq!(po.page_obj_get_type(), 5);
6289    }
6290
6291    // ------------------------------------------------------------------
6292    // Task 3: filter_count() / filter() — FPDFImageObj_GetImageFilterCount /
6293    //         FPDFImageObj_GetImageFilter
6294    // ------------------------------------------------------------------
6295
6296    #[test]
6297    fn test_image_filter_count_no_filter() {
6298        let img = ImageObject {
6299            image_data: vec![],
6300            width: 1,
6301            height: 1,
6302            bits_per_component: 8,
6303            color_space: Name::from_bytes(b"DeviceGray".to_vec()),
6304            matrix: Matrix::identity(),
6305            filter: None,
6306            xobject_id: None,
6307            blend_mode: None,
6308            active: true,
6309            marks: Vec::new(),
6310            clip_path: None,
6311        };
6312        assert_eq!(img.filter_count(), 0);
6313        assert_eq!(img.image_obj_get_image_filter_count(), 0);
6314        assert!(img.filter(0).is_none());
6315        assert!(img.image_obj_get_image_filter(0).is_none());
6316    }
6317
6318    #[test]
6319    fn test_image_filter_count_with_filter() {
6320        let img = ImageObject {
6321            image_data: vec![],
6322            width: 1,
6323            height: 1,
6324            bits_per_component: 8,
6325            color_space: Name::from_bytes(b"DeviceRGB".to_vec()),
6326            matrix: Matrix::identity(),
6327            filter: Some(Name::from_bytes(b"FlateDecode".to_vec())),
6328            xobject_id: None,
6329            blend_mode: None,
6330            active: true,
6331            marks: Vec::new(),
6332            clip_path: None,
6333        };
6334        assert_eq!(img.filter_count(), 1);
6335        assert_eq!(img.image_obj_get_image_filter_count(), 1);
6336        assert_eq!(img.filter(0), Some("FlateDecode"));
6337        assert_eq!(img.image_obj_get_image_filter(0), Some("FlateDecode"));
6338    }
6339
6340    #[test]
6341    fn test_image_filter_index_out_of_range_returns_none() {
6342        let img = ImageObject {
6343            image_data: vec![],
6344            width: 1,
6345            height: 1,
6346            bits_per_component: 8,
6347            color_space: Name::from_bytes(b"DeviceRGB".to_vec()),
6348            matrix: Matrix::identity(),
6349            filter: Some(Name::from_bytes(b"DCTDecode".to_vec())),
6350            xobject_id: None,
6351            blend_mode: None,
6352            active: true,
6353            marks: Vec::new(),
6354            clip_path: None,
6355        };
6356        // Index 0 is valid.
6357        assert_eq!(img.filter(0), Some("DCTDecode"));
6358        // Index 1 is out of range — only one filter is stored.
6359        assert!(img.filter(1).is_none());
6360        assert!(img.image_obj_get_image_filter(1).is_none());
6361    }
6362
6363    // -----------------------------------------------------------------------
6364    // Batch 17: rendered_bitmap / icc_profile_data_decoded stubs
6365    // -----------------------------------------------------------------------
6366
6367    #[test]
6368    fn test_image_obj_rendered_bitmap_returns_not_supported() {
6369        let img = ImageObject {
6370            image_data: vec![],
6371            width: 1,
6372            height: 1,
6373            bits_per_component: 8,
6374            color_space: Name::from_bytes(b"DeviceRGB".to_vec()),
6375            matrix: Matrix::identity(),
6376            filter: None,
6377            xobject_id: None,
6378            blend_mode: None,
6379            active: true,
6380            marks: Vec::new(),
6381            clip_path: None,
6382        };
6383        assert!(img.rendered_bitmap(0, 1.0).is_err());
6384        assert!(img.image_obj_get_rendered_bitmap(0, 1.0).is_err());
6385    }
6386
6387    #[test]
6388    fn test_image_obj_icc_profile_returns_none() {
6389        let img = ImageObject {
6390            image_data: vec![],
6391            width: 1,
6392            height: 1,
6393            bits_per_component: 8,
6394            color_space: Name::from_bytes(b"DeviceRGB".to_vec()),
6395            matrix: Matrix::identity(),
6396            filter: None,
6397            xobject_id: None,
6398            blend_mode: None,
6399            active: true,
6400            marks: Vec::new(),
6401            clip_path: None,
6402        };
6403        assert!(img.icc_profile_data_decoded().is_none());
6404        assert!(img.image_obj_get_icc_profile_data_decoded().is_none());
6405    }
6406
6407    #[test]
6408    fn test_text_obj_rendered_bitmap_returns_not_supported() {
6409        let obj = TextObject::with_standard_font("Helvetica", 12.0);
6410        assert!(obj.rendered_bitmap(0, 1.0).is_err());
6411        assert!(obj.text_obj_get_rendered_bitmap(0, 1.0).is_err());
6412    }
6413
6414    // ------------------------------------------------------------------
6415    // Batch 17b: rotated_bounds, set_char_codes, float params, Form stubs
6416    // ------------------------------------------------------------------
6417
6418    #[test]
6419    fn test_text_obj_set_char_codes_ascii() {
6420        let mut t = TextObject::default();
6421        t.set_char_codes(&[0x41, 0x42, 0x43]);
6422        assert_eq!(t.text(), "ABC");
6423    }
6424
6425    #[test]
6426    fn test_text_obj_set_char_codes_alias() {
6427        let mut t = TextObject::default();
6428        t.text_set_charcodes(&[0x48, 0x69]); // 'H', 'i'
6429        assert_eq!(t.text(), "Hi");
6430    }
6431
6432    #[test]
6433    fn test_text_obj_set_char_codes_invalid_replaced_with_replacement_char() {
6434        let mut t = TextObject::default();
6435        // 0xD800 is a surrogate, not a valid Unicode scalar
6436        t.set_char_codes(&[0x41, 0xD800, 0x42]);
6437        let s = t.text();
6438        let chars: Vec<char> = s.chars().collect();
6439        assert_eq!(chars[0], 'A');
6440        assert_eq!(chars[1], '\u{FFFD}');
6441        assert_eq!(chars[2], 'B');
6442    }
6443
6444    #[allow(deprecated)]
6445    #[test]
6446    fn test_text_obj_from_font_registration_has_correct_size() {
6447        let reg = FontRegistration::new_standard(make_id(10), "Courier-Bold");
6448        let t = TextObject::from_font_registration(&reg, 12.0);
6449        assert_eq!(t.font_size(), 12.0);
6450        assert_eq!(t.font_name(), &Name::from_bytes(b"Courier-Bold".to_vec()));
6451        assert_eq!(t.font_object_id, Some(make_id(10)));
6452    }
6453
6454    #[allow(deprecated)]
6455    #[test]
6456    fn test_text_obj_from_font_registration_equals_with_font() {
6457        let reg = FontRegistration::new_standard(make_id(5), "Helvetica");
6458        let t1 = TextObject::with_font(&reg, 14.0);
6459        let t2 = TextObject::from_font_registration(&reg, 14.0);
6460        assert_eq!(t1.font_name(), t2.font_name());
6461        assert_eq!(t1.font_size(), t2.font_size());
6462        assert_eq!(t1.font_object_id, t2.font_object_id);
6463    }
6464
6465    #[test]
6466    fn test_page_obj_mark_float_param_set_and_get() {
6467        let mut mark = ContentMark::new("Tag");
6468        mark.set_float_param("Scale", 1.5_f32);
6469        assert_eq!(mark.float_param("Scale"), Some(1.5_f32));
6470        assert_eq!(
6471            mark.page_obj_mark_get_param_float_value("Scale"),
6472            Some(1.5_f32)
6473        );
6474    }
6475
6476    #[test]
6477    fn test_page_obj_mark_float_param_replace_existing() {
6478        let mut mark = ContentMark::new("Tag");
6479        mark.set_float_param("X", 0.5_f32);
6480        mark.set_float_param("X", 2.0_f32);
6481        assert_eq!(mark.float_param("X"), Some(2.0_f32));
6482        assert_eq!(mark.param_count(), 1);
6483    }
6484
6485    #[test]
6486    fn test_page_obj_mark_float_param_absent_returns_none() {
6487        let mark = ContentMark::new("Tag");
6488        assert!(mark.float_param("Missing").is_none());
6489    }
6490
6491    #[test]
6492    fn test_page_obj_mark_float_wrong_type_returns_none() {
6493        let mut mark = ContentMark::new("Tag");
6494        mark.set_int_param("Key", 42);
6495        assert!(mark.float_param("Key").is_none());
6496        mark.set_float_param("FKey", 1.0);
6497        assert!(mark.int_param("FKey").is_none());
6498    }
6499
6500    #[test]
6501    fn test_form_obj_object_count_stub() {
6502        let id = ObjectId::new(1, 0);
6503        let fo = FormObject::from_xobject(id, Matrix::identity());
6504        let result = fo.object_count();
6505        assert!(matches!(
6506            result,
6507            Err(crate::error::EditError::NotSupported(_))
6508        ));
6509        let result2 = fo.form_obj_count_objects();
6510        assert!(matches!(
6511            result2,
6512            Err(crate::error::EditError::NotSupported(_))
6513        ));
6514    }
6515
6516    #[test]
6517    fn test_form_obj_object_at_stub() {
6518        let id = ObjectId::new(1, 0);
6519        let fo = FormObject::from_xobject(id, Matrix::identity());
6520        let result = fo.object_at(0);
6521        assert!(matches!(
6522            result,
6523            Err(crate::error::EditError::NotSupported(_))
6524        ));
6525        let result2 = fo.form_obj_get_object(0);
6526        assert!(matches!(
6527            result2,
6528            Err(crate::error::EditError::NotSupported(_))
6529        ));
6530    }
6531
6532    #[test]
6533    fn test_form_obj_remove_object_stub() {
6534        let id = ObjectId::new(1, 0);
6535        let mut fo = FormObject::from_xobject(id, Matrix::identity());
6536        let result = fo.remove_object(0);
6537        assert!(matches!(
6538            result,
6539            Err(crate::error::EditError::NotSupported(_))
6540        ));
6541    }
6542
6543    #[test]
6544    fn test_text_obj_rotated_bounds_identity_matrix() {
6545        let mut t = TextObject::default();
6546        t.matrix = Matrix::new(1.0, 0.0, 0.0, 1.0, 10.0, 20.0);
6547        let bounds = t.rotated_bounds().unwrap();
6548        assert!((bounds[0].x - 10.0).abs() < 1e-9);
6549        assert!((bounds[0].y - 20.0).abs() < 1e-9);
6550    }
6551
6552    #[test]
6553    fn test_image_obj_rotated_bounds_uses_matrix() {
6554        let bmp = Bitmap::new(1, 1, BitmapFormat::Rgba32);
6555        let c = std::f64::consts::FRAC_1_SQRT_2;
6556        let mat = Matrix {
6557            a: c,
6558            b: c,
6559            c: -c,
6560            d: c,
6561            e: 0.0,
6562            f: 0.0,
6563        };
6564        let obj = ImageObject::from_bitmap(&bmp, mat);
6565        let bounds = obj.rotated_bounds().unwrap();
6566        assert_eq!(bounds.len(), 4);
6567        assert!((bounds[0].x).abs() < 1e-9);
6568        assert!((bounds[0].y).abs() < 1e-9);
6569    }
6570
6571    #[test]
6572    fn test_page_object_rotated_bounds_dispatch() {
6573        let t = PageObject::Text(TextObject::default());
6574        assert!(t.rotated_bounds().is_some());
6575        assert!(t.page_obj_get_rotated_bounds().is_some());
6576
6577        let bmp = Bitmap::new(1, 1, BitmapFormat::Rgba32);
6578        let img = PageObject::Image(ImageObject::from_bitmap(&bmp, Matrix::identity()));
6579        assert!(img.rotated_bounds().is_some());
6580
6581        // Empty path → no bounds → None
6582        let path_empty = PageObject::Path(PathObject::default());
6583        assert!(path_empty.rotated_bounds().is_none());
6584
6585        // Non-empty path → Some
6586        let path_with_segs = PageObject::Path(PathObject {
6587            segments: vec![PathSegment::Rect(0.0, 0.0, 10.0, 20.0)],
6588            ..Default::default()
6589        });
6590        assert!(path_with_segs.rotated_bounds().is_some());
6591
6592        // Form with non-degenerate matrix → Some
6593        let id = ObjectId::new(1, 0);
6594        let fo = PageObject::Form(FormObject::from_xobject(
6595            id,
6596            Matrix::new(2.0, 0.0, 0.0, 3.0, 5.0, 10.0),
6597        ));
6598        assert!(fo.rotated_bounds().is_some());
6599    }
6600}