Skip to main content

rpdfium_doc/
annotation.rs

1// Derived from PDFium's cpdf_annot.h/cpp
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//! PDF annotation types and parsing — `CPDF_Annot` (ISO 32000-2 section 12.5).
7//!
8//! A single annotation: its type, rectangle, contents, appearance, and
9//! subtype-specific fields. List-level parsing (`parse_annotations`) and
10//! popup parent lookup (`find_parent_annotation`) are in [`crate::annot_list`].
11
12use rpdfium_core::{Name, PdfSource};
13use rpdfium_parser::{Object, ObjectId, ObjectStore};
14
15use crate::aaction::{AActionType, AdditionalActions, parse_additional_actions};
16use crate::action::{Action, parse_action};
17use crate::annotation_appearance::{AnnotationAppearance, extract_appearance};
18use crate::ap_settings::{MkDict, parse_mk_dict};
19use crate::default_appearance::parse_default_appearance;
20use crate::destination::{Destination, parse_destination};
21use crate::error::{DocError, DocResult};
22use crate::file_spec::{FileSpec, parse_file_spec};
23use crate::form_field::{ChoiceOption, FormFieldFlags, FormFieldType};
24
25/// PDF object type tag for annotation dictionary values.
26///
27/// Corresponds to the `FPDF_OBJECT_TYPE` enum in the upstream PDFium public API
28/// (`public/fpdf_annot.h`, `FPDFAnnot_GetValueType()`).
29#[derive(Debug, Clone, Copy, PartialEq, Eq)]
30pub enum PdfValueType {
31    /// The key is absent from the annotation dictionary.
32    Unknown = 0,
33    /// The value is a boolean.
34    Boolean = 1,
35    /// The value is an integer or real number.
36    Number = 2,
37    /// The value is a PDF string.
38    String = 3,
39    /// The value is a PDF name.
40    Name = 4,
41    /// The value is an array.
42    Array = 5,
43    /// The value is a dictionary.
44    Dictionary = 6,
45    /// The value is a stream.
46    Stream = 7,
47    /// The value is the PDF null object.
48    Null = 8,
49    /// The value is an indirect reference.
50    Reference = 9,
51}
52
53/// Annotation appearance mode — selects which `/AP` sub-stream to use.
54///
55/// Corresponds to `FPDF_ANNOT_APPEARANCEMODE` in the upstream PDFium API
56/// (`public/fpdf_annot.h`, `FPDFAnnot_GetAP()` / `FPDFAnnot_SetAP()`).
57#[derive(Debug, Clone, Copy, PartialEq, Eq)]
58pub enum AppearanceMode {
59    /// Normal appearance (`/AP /N`).
60    Normal,
61    /// Rollover appearance (`/AP /R`).
62    Rollover,
63    /// Down (mouse-press) appearance (`/AP /D`).
64    Down,
65}
66
67/// Subtype-specific annotation data.
68///
69/// Contains fields that are only relevant to specific annotation subtypes,
70/// such as `/QuadPoints` for markup annotations, `/L` for line annotations, etc.
71#[derive(Debug, Clone, Default)]
72pub struct AnnotationSubtypeData {
73    /// QuadPoints for Highlight, Underline, Squiggly, StrikeOut, Link annotations.
74    /// Each group of 8 values defines a quadrilateral: [x1,y1, x2,y2, x3,y3, x4,y4].
75    pub quad_points: Option<Vec<f32>>,
76    /// Line endpoints for Line annotations: [x1, y1, x2, y2].
77    pub line_points: Option<[f32; 4]>,
78    /// Leader line length for Line annotations.
79    pub leader_line_length: Option<f32>,
80    /// Vertices for Polygon and PolyLine annotations.
81    pub vertices: Option<Vec<f32>>,
82    /// Ink strokes for Ink annotations.
83    /// Each inner Vec represents one stroke as a sequence of [x, y] pairs.
84    pub ink_list: Option<Vec<Vec<f32>>>,
85    /// Default appearance string for FreeText annotations.
86    pub default_appearance: Option<String>,
87    /// Stamp name (icon) for Stamp annotations.
88    pub stamp_name: Option<String>,
89}
90
91/// A PDF annotation parsed from a page's `/Annots` array.
92#[derive(Debug, Clone)]
93pub struct Annotation {
94    /// The type of annotation.
95    pub subtype: AnnotationType,
96    /// Annotation rectangle in user space: `[x1, y1, x2, y2]`.
97    pub rect: [f32; 4],
98    /// Contents of the annotation (the `/Contents` key).
99    pub contents: Option<String>,
100    /// Annotation flags (bit field from `/F`).
101    pub flags: AnnotationFlags,
102    /// Unique name of the annotation (from `/NM`).
103    pub name: Option<String>,
104    /// Appearance streams (from `/AP`).
105    pub appearance: Option<AnnotationAppearance>,
106    /// Color array (from `/C`).
107    pub color: Option<Vec<f32>>,
108    /// Border specification.
109    pub border: Option<AnnotationBorder>,
110    /// Associated action (from `/A`).
111    pub action: Option<Action>,
112    /// Destination (from `/Dest`).
113    pub destination: Option<Destination>,
114    /// Subtype-specific fields parsed from the annotation dictionary.
115    pub subtype_data: AnnotationSubtypeData,
116    /// Widget appearance characteristics (`/MK`).
117    pub mk: Option<MkDict>,
118    /// File specification for FileAttachment annotations (`/FS`).
119    pub file_spec: Option<FileSpec>,
120    /// Parent annotation reference for Popup annotations (`/Parent`).
121    pub parent_ref: Option<ObjectId>,
122    /// This annotation's own object ID (from the indirect reference).
123    pub object_id: Option<ObjectId>,
124    /// Open state for Text/Popup annotations (`/Open`).
125    pub open: Option<bool>,
126    /// Decoded bytes of the normal appearance stream (`/AP /N`), if present.
127    ///
128    /// Populated during parsing. Corresponds to `FPDFAnnot_GetAP()` with
129    /// `FPDF_ANNOT_APPEARANCEMODE_NORMAL`.
130    pub ap_n_bytes: Option<Vec<u8>>,
131    /// Decoded bytes of the rollover appearance stream (`/AP /R`), if present.
132    ///
133    /// Populated during parsing. Corresponds to `FPDFAnnot_GetAP()` with
134    /// `FPDF_ANNOT_APPEARANCEMODE_ROLLOVER`.
135    pub ap_r_bytes: Option<Vec<u8>>,
136    /// Decoded bytes of the down appearance stream (`/AP /D`), if present.
137    ///
138    /// Populated during parsing. Corresponds to `FPDFAnnot_GetAP()` with
139    /// `FPDF_ANNOT_APPEARANCEMODE_DOWN`.
140    pub ap_d_bytes: Option<Vec<u8>>,
141    /// Object ID of the annotation referenced by `/IRT` ("In Reply To"), if any.
142    ///
143    /// Present on annotations that are replies to another annotation.
144    /// Corresponds to `FPDFAnnot_GetLinkedAnnot()`.
145    pub irt_ref: Option<ObjectId>,
146    /// Partial field name from `/T`.
147    ///
148    /// For widget annotations this is the form field name; for other annotation
149    /// types it serves as the annotation title / author label.
150    /// Corresponds to `FPDFAnnot_GetFormFieldName()`.
151    pub field_name: Option<String>,
152    /// Alternate (user-visible) field name from `/TU`.
153    ///
154    /// Only meaningful for widget annotations.
155    /// Corresponds to `FPDFAnnot_GetFormFieldAlternateName()`.
156    pub alternate_name: Option<String>,
157    /// Current field value as a string from `/V`.
158    ///
159    /// For text fields this is the text value; for choice fields it is the
160    /// selected option string; for check-box / radio-button fields it is the
161    /// appearance state name.
162    /// Corresponds to `FPDFAnnot_GetFormFieldValue()`.
163    pub field_value: Option<String>,
164    /// Form field flags from `/Ff` — only meaningful for widget (form field) annotations.
165    ///
166    /// Contains the field-specific flags such as read-only, required, multiline,
167    /// password, etc.
168    ///
169    /// Corresponds to `FPDFAnnot_GetFormFieldFlags()`.
170    pub form_field_flags: Option<FormFieldFlags>,
171    /// Additional actions for Widget annotations (`/AA`).
172    ///
173    /// Contains event-triggered JavaScript actions for form field widgets.
174    /// Populated during parsing for Widget annotations.
175    /// Corresponds to `FPDFAnnot_GetFormAdditionalActionJavaScript()`.
176    pub additional_actions: Option<AdditionalActions>,
177    /// Form field type from `/FT` (Widget annotations).
178    ///
179    /// `Some(Text | Button | Choice | Signature)` when the annotation
180    /// dictionary carries an `/FT` key directly; `None` for non-widget
181    /// annotations or when `/FT` is inherited from the parent field.
182    ///
183    /// Corresponds to `FPDFAnnot_GetFormFieldType()`.
184    pub form_field_type: Option<FormFieldType>,
185    /// Choice-field options parsed from `/Opt` (Widget annotations with `/FT Ch`).
186    ///
187    /// Each entry has an `export_value` (submitted to server) and a
188    /// `display_value` (shown to user).  `None` for non-choice annotations.
189    ///
190    /// Corresponds to `FPDFAnnot_GetOptionCount()` / `FPDFAnnot_GetOptionLabel()`.
191    pub options: Option<Vec<ChoiceOption>>,
192}
193
194/// Annotation subtypes defined by the PDF specification.
195#[derive(Debug, Clone, Copy, PartialEq, Eq)]
196pub enum AnnotationType {
197    /// Text (sticky note) annotation.
198    Text,
199    /// Link annotation.
200    Link,
201    /// Free text annotation.
202    FreeText,
203    /// Line annotation.
204    Line,
205    /// Square annotation.
206    Square,
207    /// Circle annotation.
208    Circle,
209    /// Polygon annotation.
210    Polygon,
211    /// Polyline annotation.
212    PolyLine,
213    /// Highlight markup annotation.
214    Highlight,
215    /// Underline markup annotation.
216    Underline,
217    /// Squiggly underline markup annotation.
218    Squiggly,
219    /// Strikeout markup annotation.
220    StrikeOut,
221    /// Stamp annotation.
222    Stamp,
223    /// Caret annotation.
224    Caret,
225    /// Ink annotation.
226    Ink,
227    /// Popup annotation.
228    Popup,
229    /// File attachment annotation.
230    FileAttachment,
231    /// Sound annotation.
232    Sound,
233    /// Widget annotation (form field).
234    Widget,
235    /// Movie annotation.
236    Movie,
237    /// Screen annotation.
238    Screen,
239    /// Printer's mark annotation.
240    PrinterMark,
241    /// Trap network annotation.
242    TrapNet,
243    /// Watermark annotation.
244    Watermark,
245    /// 3D annotation.
246    ThreeD,
247    /// Rich media annotation.
248    RichMedia,
249    /// XFA widget annotation.
250    XFAWidget,
251    /// Redaction annotation.
252    Redact,
253    /// Unrecognized annotation subtype.
254    Other,
255}
256
257impl AnnotationType {
258    /// Returns `true` if this annotation subtype supports appearance stream (AP)
259    /// object manipulation via `FPDFAnnot_AppendObject` etc.
260    ///
261    /// The supported subtypes are: Stamp, FreeText, Ink, Square, Circle, Polygon,
262    /// PolyLine, Line, Highlight, Underline, Squiggly, StrikeOut.
263    ///
264    /// Corresponds to `FPDFAnnot_IsObjectSupportedSubtype()`.
265    pub fn supports_ap_objects(&self) -> bool {
266        matches!(
267            self,
268            AnnotationType::Stamp
269                | AnnotationType::FreeText
270                | AnnotationType::Ink
271                | AnnotationType::Square
272                | AnnotationType::Circle
273                | AnnotationType::Polygon
274                | AnnotationType::PolyLine
275                | AnnotationType::Line
276                | AnnotationType::Highlight
277                | AnnotationType::Underline
278                | AnnotationType::Squiggly
279                | AnnotationType::StrikeOut
280        )
281    }
282
283    /// ADR-019 alias for [`Self::supports_ap_objects()`].
284    ///
285    /// Corresponds to `FPDFAnnot_IsObjectSupportedSubtype()`.
286    #[inline]
287    pub fn annot_is_object_supported_subtype(&self) -> bool {
288        self.supports_ap_objects()
289    }
290
291    /// Deprecated: use [`annot_is_object_supported_subtype()`](Self::annot_is_object_supported_subtype) instead.
292    #[deprecated(
293        since = "0.1.0",
294        note = "use annot_is_object_supported_subtype() instead"
295    )]
296    #[inline]
297    pub fn is_object_supported_subtype(&self) -> bool {
298        self.supports_ap_objects()
299    }
300
301    /// Deprecated: use [`annot_is_object_supported_subtype()`](Self::annot_is_object_supported_subtype) instead.
302    #[deprecated(
303        since = "0.1.0",
304        note = "use annot_is_object_supported_subtype() instead"
305    )]
306    #[inline]
307    pub fn is_ap_object_supported(&self) -> bool {
308        self.supports_ap_objects()
309    }
310
311    /// Returns whether this annotation subtype is currently supported for creation.
312    ///
313    /// Currently supported subtypes: circle, fileattachment, freetext, highlight,
314    /// ink, link, popup, square, squiggly, stamp, strikeout, text, underline.
315    ///
316    /// Corresponds to `FPDFAnnot_IsSupportedSubtype`.
317    pub fn is_supported_for_creation(&self) -> bool {
318        matches!(
319            self,
320            AnnotationType::Circle
321                | AnnotationType::FileAttachment
322                | AnnotationType::FreeText
323                | AnnotationType::Highlight
324                | AnnotationType::Ink
325                | AnnotationType::Link
326                | AnnotationType::Popup
327                | AnnotationType::Square
328                | AnnotationType::Squiggly
329                | AnnotationType::Stamp
330                | AnnotationType::StrikeOut
331                | AnnotationType::Text
332                | AnnotationType::Underline
333        )
334    }
335
336    /// ADR-019 alias for [`Self::is_supported_for_creation()`].
337    ///
338    /// Corresponds to `FPDFAnnot_IsSupportedSubtype`.
339    #[inline]
340    pub fn annot_is_supported_subtype(&self) -> bool {
341        self.is_supported_for_creation()
342    }
343
344    /// Deprecated: use [`annot_is_supported_subtype()`](Self::annot_is_supported_subtype) instead.
345    #[deprecated(since = "0.1.0", note = "use annot_is_supported_subtype() instead")]
346    #[inline]
347    pub fn is_supported_subtype(&self) -> bool {
348        self.is_supported_for_creation()
349    }
350
351    /// Parse an annotation subtype from a PDF name string.
352    pub fn from_name(name: &str) -> Self {
353        match name {
354            "Text" => Self::Text,
355            "Link" => Self::Link,
356            "FreeText" => Self::FreeText,
357            "Line" => Self::Line,
358            "Square" => Self::Square,
359            "Circle" => Self::Circle,
360            "Polygon" => Self::Polygon,
361            "PolyLine" => Self::PolyLine,
362            "Highlight" => Self::Highlight,
363            "Underline" => Self::Underline,
364            "Squiggly" => Self::Squiggly,
365            "StrikeOut" => Self::StrikeOut,
366            "Stamp" => Self::Stamp,
367            "Caret" => Self::Caret,
368            "Ink" => Self::Ink,
369            "Popup" => Self::Popup,
370            "FileAttachment" => Self::FileAttachment,
371            "Sound" => Self::Sound,
372            "Widget" => Self::Widget,
373            "Movie" => Self::Movie,
374            "Screen" => Self::Screen,
375            "PrinterMark" => Self::PrinterMark,
376            "TrapNet" => Self::TrapNet,
377            "Watermark" => Self::Watermark,
378            "3D" => Self::ThreeD,
379            "RichMedia" => Self::RichMedia,
380            "XFAWidget" => Self::XFAWidget,
381            "Redact" => Self::Redact,
382            _ => Self::Other,
383        }
384    }
385
386    /// Returns the PDF subtype name string for this annotation type.
387    /// Inverse of `from_name()`. Corresponds to upstream `AnnotSubtypeToString()`.
388    pub fn to_name(&self) -> &'static str {
389        match self {
390            Self::Text => "Text",
391            Self::Link => "Link",
392            Self::FreeText => "FreeText",
393            Self::Line => "Line",
394            Self::Square => "Square",
395            Self::Circle => "Circle",
396            Self::Polygon => "Polygon",
397            Self::PolyLine => "PolyLine",
398            Self::Highlight => "Highlight",
399            Self::Underline => "Underline",
400            Self::Squiggly => "Squiggly",
401            Self::StrikeOut => "StrikeOut",
402            Self::Stamp => "Stamp",
403            Self::Caret => "Caret",
404            Self::Ink => "Ink",
405            Self::Popup => "Popup",
406            Self::FileAttachment => "FileAttachment",
407            Self::Sound => "Sound",
408            Self::Widget => "Widget",
409            Self::Movie => "Movie",
410            Self::Screen => "Screen",
411            Self::PrinterMark => "PrinterMark",
412            Self::TrapNet => "TrapNet",
413            Self::Watermark => "Watermark",
414            Self::ThreeD => "3D",
415            Self::RichMedia => "RichMedia",
416            Self::XFAWidget => "XFAWidget",
417            Self::Redact => "Redact",
418            Self::Other => "Unknown",
419        }
420    }
421}
422
423/// Annotation flags (ISO 32000-2 section 12.5.3, Table 165).
424///
425/// The flags are stored as a 32-bit integer where each bit has a specific
426/// meaning defined by the specification.
427#[derive(Debug, Clone, Copy, Default)]
428pub struct AnnotationFlags(u32);
429
430impl AnnotationFlags {
431    /// Create flags from a raw integer value.
432    pub fn from_bits(bits: u32) -> Self {
433        Self(bits)
434    }
435
436    /// Get the raw bits.
437    pub fn bits(&self) -> u32 {
438        self.0
439    }
440
441    /// Bit 1: If set, do not display the annotation if it does not belong to
442    /// one of the standard annotation types.
443    pub fn invisible(&self) -> bool {
444        self.0 & 1 != 0
445    }
446
447    /// Bit 2: If set, do not display or print the annotation.
448    pub fn hidden(&self) -> bool {
449        self.0 & 2 != 0
450    }
451
452    /// Bit 3: If set, print the annotation when the page is printed.
453    pub fn print(&self) -> bool {
454        self.0 & 4 != 0
455    }
456
457    /// Bit 4: If set, do not scale the annotation's appearance to match the
458    /// magnification of the page.
459    pub fn no_zoom(&self) -> bool {
460        self.0 & 8 != 0
461    }
462
463    /// Bit 5: If set, do not rotate the annotation's appearance to match the
464    /// rotation of the page.
465    pub fn no_rotate(&self) -> bool {
466        self.0 & 16 != 0
467    }
468
469    /// Bit 6: If set, do not display the annotation on the screen.
470    pub fn no_view(&self) -> bool {
471        self.0 & 32 != 0
472    }
473
474    /// Bit 7: If set, the annotation is read-only.
475    pub fn read_only(&self) -> bool {
476        self.0 & 64 != 0
477    }
478
479    /// Bit 8: If set, do not allow the annotation to be deleted or modified.
480    pub fn locked(&self) -> bool {
481        self.0 & 128 != 0
482    }
483
484    /// Bit 9: If set, invert the NoView flag when the cursor enters/exits the
485    /// annotation's bounding box.
486    pub fn toggle_no_view(&self) -> bool {
487        self.0 & 256 != 0
488    }
489
490    /// Bit 10: If set, the annotation's contents cannot be changed while the
491    /// annotation is locked.
492    pub fn locked_contents(&self) -> bool {
493        self.0 & 512 != 0
494    }
495
496    /// Returns true if the annotation should be visible for screen display.
497    pub fn is_visible(&self) -> bool {
498        !self.hidden() && !self.no_view()
499    }
500}
501
502/// Annotation border specification.
503#[derive(Debug, Clone)]
504pub struct AnnotationBorder {
505    /// Border width in points.
506    pub width: f32,
507    /// Border style.
508    pub style: BorderStyle,
509}
510
511impl Default for AnnotationBorder {
512    fn default() -> Self {
513        Self {
514            width: 1.0,
515            style: BorderStyle::Solid,
516        }
517    }
518}
519
520/// Border styles for annotations.
521#[derive(Debug, Clone, Copy, PartialEq, Eq)]
522pub enum BorderStyle {
523    /// Solid line.
524    Solid,
525    /// Dashed line.
526    Dashed,
527    /// Beveled (raised) border.
528    Beveled,
529    /// Inset (sunken) border.
530    Inset,
531    /// Underline only.
532    Underline,
533}
534
535impl BorderStyle {
536    /// Parse a border style from a PDF name string.
537    pub fn from_name(name: &str) -> Self {
538        match name {
539            "S" => Self::Solid,
540            "D" => Self::Dashed,
541            "B" => Self::Beveled,
542            "I" => Self::Inset,
543            "U" => Self::Underline,
544            _ => Self::Solid,
545        }
546    }
547}
548
549/// Parse a single annotation from its dictionary.
550pub(crate) fn parse_single_annotation<S: PdfSource>(
551    obj: &Object,
552    store: &ObjectStore<S>,
553    object_id: Option<ObjectId>,
554) -> DocResult<Annotation> {
555    let dict = obj.as_dict().ok_or(DocError::UnexpectedType)?;
556
557    // /Subtype (required)
558    let subtype = dict
559        .get(&Name::subtype())
560        .and_then(|o| {
561            store
562                .deep_resolve(o)
563                .ok()
564                .and_then(|r| r.as_name().map(|n| n.as_str().into_owned()))
565        })
566        .map(|s| AnnotationType::from_name(&s))
567        .unwrap_or(AnnotationType::Other);
568
569    // /Rect (required)
570    let rect = parse_rect(dict, store)?;
571
572    // /Contents (optional)
573    let contents = dict.get(&Name::contents()).and_then(|o| {
574        store
575            .deep_resolve(o)
576            .ok()
577            .and_then(|r| r.as_string().map(|s| s.to_string_lossy()))
578    });
579
580    // /F (optional flags)
581    let flags = dict
582        .get(&Name::f())
583        .and_then(|o| store.deep_resolve(o).ok().and_then(|r| r.as_i64()))
584        .map(|v| AnnotationFlags::from_bits(v as u32))
585        .unwrap_or_default();
586
587    // /NM (optional unique name)
588    let name = dict.get(&Name::nm()).and_then(|o| {
589        store
590            .deep_resolve(o)
591            .ok()
592            .and_then(|r| r.as_string().map(|s| s.to_string_lossy()))
593    });
594
595    // /AP (optional appearance)
596    let appearance = dict
597        .get(&Name::ap())
598        .and_then(|o| extract_appearance(o, store).ok());
599
600    // /C (optional color)
601    let color = dict
602        .get(&Name::c())
603        .and_then(|o| store.deep_resolve(o).ok())
604        .and_then(parse_color_array);
605
606    // /Border or /BS (optional border)
607    let border = parse_border(dict, store);
608
609    // /A (optional action)
610    let action = dict
611        .get(&Name::a())
612        .and_then(|o| parse_action(o, store).ok());
613
614    // /Dest (optional destination)
615    let destination = dict
616        .get(&Name::dest())
617        .and_then(|o| parse_destination(o, store).ok());
618
619    // Subtype-specific fields
620    let subtype_data = parse_subtype_data(dict, store, subtype);
621
622    // /MK — widget appearance characteristics
623    let mk = parse_mk_dict(dict, store);
624
625    // /FS — file specification for FileAttachment annotations
626    let file_spec = if subtype == AnnotationType::FileAttachment {
627        dict.get(&Name::fs())
628            .and_then(|o| parse_file_spec(o, store))
629    } else {
630        None
631    };
632
633    // /Parent — for popup annotations pointing to their parent
634    let parent_ref = if subtype == AnnotationType::Popup {
635        dict.get(&Name::parent()).and_then(|o| o.as_reference())
636    } else {
637        None
638    };
639
640    // /Open — open state for Text and Popup annotations
641    let open = dict.get(&Name::open()).and_then(|o| o.as_bool());
642
643    // /AP decoded stream bytes — extract Normal, Rollover, Down streams
644    let (ap_n_bytes, ap_r_bytes, ap_d_bytes) = extract_ap_stream_bytes(dict, store, object_id);
645
646    // /IRT — "In Reply To" indirect reference to another annotation
647    let irt_ref = dict.get(&Name::irt()).and_then(|o| o.as_reference());
648
649    // /T — partial field name (form field name or annotation title)
650    let field_name = dict.get(&Name::t()).and_then(|o| {
651        store
652            .deep_resolve(o)
653            .ok()
654            .and_then(|r| r.as_string().map(|s| s.to_string_lossy()))
655    });
656
657    // /TU — alternate (user-visible) field name
658    let alternate_name = dict.get(&Name::tu()).and_then(|o| {
659        store
660            .deep_resolve(o)
661            .ok()
662            .and_then(|r| r.as_string().map(|s| s.to_string_lossy()))
663    });
664
665    // /V — current field value (string or name)
666    let field_value = dict.get(&Name::v()).and_then(|o| {
667        store.deep_resolve(o).ok().and_then(|r| {
668            if let Some(s) = r.as_string() {
669                Some(s.to_string_lossy())
670            } else {
671                r.as_name().map(|n| n.as_str().into_owned())
672            }
673        })
674    });
675
676    // /Ff — form field flags (widget annotations only, but parse for all)
677    let form_field_flags = dict
678        .get(&Name::ff())
679        .and_then(|o| store.deep_resolve(o).ok().and_then(|r| r.as_i64()))
680        .map(|v| FormFieldFlags::from_bits(v as u32));
681
682    // /AA — additional actions (widget annotations only)
683    let additional_actions = if subtype == AnnotationType::Widget {
684        dict.get(&Name::aa())
685            .and_then(|o| parse_additional_actions(o, store).ok())
686    } else {
687        None
688    };
689
690    // /FT — form field type (widget annotations; may also be on parent dict)
691    let form_field_type = dict.get(&Name::ft()).and_then(|o| {
692        store.deep_resolve(o).ok().and_then(|r| {
693            r.as_name().map(|n| match n.as_str().as_ref() {
694                "Tx" => FormFieldType::Text,
695                "Btn" => FormFieldType::Button,
696                "Ch" => FormFieldType::Choice,
697                "Sig" => FormFieldType::Signature,
698                _ => FormFieldType::Text,
699            })
700        })
701    });
702
703    // /Opt — choice-field options (widget annotations with /FT Ch)
704    let options = if subtype == AnnotationType::Widget {
705        let opts = parse_choice_options(dict, store);
706        if opts.is_empty() { None } else { Some(opts) }
707    } else {
708        None
709    };
710
711    Ok(Annotation {
712        subtype,
713        rect,
714        contents,
715        flags,
716        name,
717        appearance,
718        color,
719        border,
720        action,
721        destination,
722        subtype_data,
723        mk,
724        file_spec,
725        parent_ref,
726        object_id,
727        open,
728        ap_n_bytes,
729        ap_r_bytes,
730        ap_d_bytes,
731        irt_ref,
732        field_name,
733        alternate_name,
734        field_value,
735        form_field_flags,
736        additional_actions,
737        form_field_type,
738        options,
739    })
740}
741
742impl Annotation {
743    /// Set the annotation rectangle.
744    ///
745    /// Updates the in-memory rectangle. To persist the change to a PDF file,
746    /// use `EditDocument::update_annotation` in rpdfium-edit.
747    pub fn set_rect(&mut self, rect: [f32; 4]) -> DocResult<()> {
748        self.rect = rect;
749        Ok(())
750    }
751
752    /// ADR-019 alias for [`Self::set_rect()`].
753    ///
754    /// Corresponds to `FPDFAnnot_SetRect()`.
755    #[inline]
756    pub fn annot_set_rect(&mut self, rect: [f32; 4]) -> DocResult<()> {
757        self.set_rect(rect)
758    }
759
760    /// Set the open/closed state for Text (sticky-note) and Popup annotations.
761    ///
762    /// Updates the in-memory open state. To persist the change to a PDF file,
763    /// use `EditDocument::update_annotation` in rpdfium-edit.
764    pub fn set_open_state(&mut self, open: bool) -> DocResult<()> {
765        self.open = Some(open);
766        Ok(())
767    }
768
769    /// Returns the quad points for markup annotations (Highlight, Underline, Squiggly,
770    /// StrikeOut, Link, Polygon, PolyLine).
771    ///
772    /// Each group of 8 values defines a quadrilateral: `[x1,y1, x2,y2, x3,y3, x4,y4]`.
773    ///
774    /// Corresponds to upstream `CPDF_Annot::GetQuadPoints()`.
775    pub fn quad_points(&self) -> Option<&[f32]> {
776        self.subtype_data.quad_points.as_deref()
777    }
778
779    /// ADR-019 alias for [`quad_points()`](Self::quad_points).
780    ///
781    /// Corresponds to upstream `CPDF_Annot::GetQuadPoints()`.
782    #[inline]
783    pub fn annot_get_quad_points(&self) -> Option<&[f32]> {
784        self.quad_points()
785    }
786
787    /// Deprecated: use [`annot_get_quad_points()`](Self::annot_get_quad_points) instead.
788    #[deprecated(since = "0.1.0", note = "use annot_get_quad_points() instead")]
789    #[inline]
790    pub fn get_quad_points(&self) -> Option<&[f32]> {
791        self.quad_points()
792    }
793
794    // -----------------------------------------------------------------------
795    // ADR-019 getter API — primary name + #[inline] alias get_*
796    // Corresponds to FPDFAnnot_* read functions in fpdf_annot.h
797    // -----------------------------------------------------------------------
798
799    /// Returns the annotation subtype.
800    ///
801    /// Corresponds to `FPDFAnnot_GetSubtype()`.
802    pub fn subtype(&self) -> AnnotationType {
803        self.subtype
804    }
805
806    /// ADR-019 alias for [`Self::subtype()`].
807    ///
808    /// Corresponds to `FPDFAnnot_GetSubtype()`.
809    #[inline]
810    pub fn annot_get_subtype(&self) -> AnnotationType {
811        self.subtype()
812    }
813
814    /// Deprecated: use [`annot_get_subtype()`](Self::annot_get_subtype) instead.
815    #[deprecated(since = "0.1.0", note = "use annot_get_subtype() instead")]
816    #[inline]
817    pub fn get_subtype(&self) -> AnnotationType {
818        self.subtype()
819    }
820
821    /// Returns the annotation rectangle `[x1, y1, x2, y2]`.
822    ///
823    /// Corresponds to `FPDFAnnot_GetRect()`.
824    pub fn rect(&self) -> [f32; 4] {
825        self.rect
826    }
827
828    /// ADR-019 alias for [`Self::rect()`].
829    ///
830    /// Corresponds to `FPDFAnnot_GetRect()`.
831    #[inline]
832    pub fn annot_get_rect(&self) -> [f32; 4] {
833        self.rect()
834    }
835
836    /// Deprecated: use [`annot_get_rect()`](Self::annot_get_rect) instead.
837    #[deprecated(since = "0.1.0", note = "use annot_get_rect() instead")]
838    #[inline]
839    pub fn get_rect(&self) -> [f32; 4] {
840        self.rect()
841    }
842
843    /// Returns the annotation flags.
844    ///
845    /// Corresponds to `FPDFAnnot_GetFlags()`.
846    pub fn flags(&self) -> AnnotationFlags {
847        self.flags
848    }
849
850    /// ADR-019 alias for [`Self::flags()`].
851    ///
852    /// Corresponds to `FPDFAnnot_GetFlags()`.
853    #[inline]
854    pub fn annot_get_flags(&self) -> AnnotationFlags {
855        self.flags()
856    }
857
858    /// Deprecated: use [`annot_get_flags()`](Self::annot_get_flags) instead.
859    #[deprecated(since = "0.1.0", note = "use annot_get_flags() instead")]
860    #[inline]
861    pub fn get_flags(&self) -> AnnotationFlags {
862        self.flags()
863    }
864
865    /// Returns `true` if this annotation subtype supports attachment (quad) points.
866    ///
867    /// Attachment points are supported by Highlight, Underline, Squiggly, StrikeOut,
868    /// and Link annotations.
869    ///
870    /// Corresponds to `FPDFAnnot_HasAttachmentPoints()`.
871    pub fn has_attachment_points(&self) -> bool {
872        matches!(
873            self.subtype,
874            AnnotationType::Highlight
875                | AnnotationType::Underline
876                | AnnotationType::Squiggly
877                | AnnotationType::StrikeOut
878                | AnnotationType::Link
879        )
880    }
881
882    /// ADR-019 alias for [`Self::has_attachment_points()`].
883    ///
884    /// Corresponds to `FPDFAnnot_HasAttachmentPoints()`.
885    #[inline]
886    pub fn annot_has_attachment_points(&self) -> bool {
887        self.has_attachment_points()
888    }
889
890    /// Returns the number of attachment point groups (quadrilaterals).
891    ///
892    /// Each group is 8 floats `[x1,y1, x2,y2, x3,y3, x4,y4]`.
893    ///
894    /// Corresponds to `FPDFAnnot_CountAttachmentPoints()`.
895    pub fn attachment_point_count(&self) -> usize {
896        self.subtype_data
897            .quad_points
898            .as_ref()
899            .map_or(0, |v| v.len() / 8)
900    }
901
902    /// ADR-019 alias for [`Self::attachment_point_count()`].
903    ///
904    /// Corresponds to `FPDFAnnot_CountAttachmentPoints()`.
905    #[inline]
906    pub fn annot_count_attachment_points(&self) -> usize {
907        self.attachment_point_count()
908    }
909
910    /// Deprecated: use [`annot_count_attachment_points()`](Self::annot_count_attachment_points) instead.
911    #[deprecated(since = "0.1.0", note = "use annot_count_attachment_points() instead")]
912    #[inline]
913    pub fn count_attachment_points(&self) -> usize {
914        self.attachment_point_count()
915    }
916
917    /// Returns the `index`-th attachment point group as 8 floats,
918    /// or `None` if the index is out of bounds.
919    ///
920    /// Corresponds to `FPDFAnnot_GetAttachmentPoints()`.
921    pub fn attachment_points(&self, index: usize) -> Option<[f32; 8]> {
922        let flat = self.subtype_data.quad_points.as_ref()?;
923        let start = index * 8;
924        if start + 8 > flat.len() {
925            return None;
926        }
927        let s = &flat[start..start + 8];
928        Some([s[0], s[1], s[2], s[3], s[4], s[5], s[6], s[7]])
929    }
930
931    /// ADR-019 alias for [`Self::attachment_points()`].
932    ///
933    /// Corresponds to `FPDFAnnot_GetAttachmentPoints()`.
934    #[inline]
935    pub fn annot_get_attachment_points(&self, index: usize) -> Option<[f32; 8]> {
936        self.attachment_points(index)
937    }
938
939    /// Deprecated: use [`annot_get_attachment_points()`](Self::annot_get_attachment_points) instead.
940    #[deprecated(since = "0.1.0", note = "use annot_get_attachment_points() instead")]
941    #[inline]
942    pub fn get_attachment_points(&self, index: usize) -> Option<[f32; 8]> {
943        self.attachment_points(index)
944    }
945
946    /// Returns the vertex array for Polygon and PolyLine annotations as a flat
947    /// `[x, y, x, y, …]` slice, or `None` if absent.
948    ///
949    /// Corresponds to `FPDFAnnot_GetVertices()`.
950    pub fn vertices(&self) -> Option<&[f32]> {
951        self.subtype_data.vertices.as_deref()
952    }
953
954    /// ADR-019 alias for [`Self::vertices()`].
955    ///
956    /// Corresponds to `FPDFAnnot_GetVertices()`.
957    #[inline]
958    pub fn annot_get_vertices(&self) -> Option<&[f32]> {
959        self.vertices()
960    }
961
962    /// Deprecated: use [`annot_get_vertices()`](Self::annot_get_vertices) instead.
963    #[deprecated(since = "0.1.0", note = "use annot_get_vertices() instead")]
964    #[inline]
965    pub fn get_vertices(&self) -> Option<&[f32]> {
966        self.vertices()
967    }
968
969    /// Returns the number of ink strokes for Ink annotations.
970    ///
971    /// Corresponds to `FPDFAnnot_GetInkListCount()`.
972    pub fn ink_list_count(&self) -> usize {
973        self.subtype_data.ink_list.as_ref().map_or(0, |v| v.len())
974    }
975
976    /// ADR-019 alias for [`Self::ink_list_count()`].
977    ///
978    /// Corresponds to `FPDFAnnot_GetInkListCount()`.
979    #[inline]
980    pub fn annot_get_ink_list_count(&self) -> usize {
981        self.ink_list_count()
982    }
983
984    /// Deprecated: use [`annot_get_ink_list_count()`](Self::annot_get_ink_list_count) instead.
985    #[deprecated(since = "0.1.0", note = "use annot_get_ink_list_count() instead")]
986    #[inline]
987    pub fn get_ink_list_count(&self) -> usize {
988        self.ink_list_count()
989    }
990
991    /// Returns the `index`-th ink stroke as a flat `[x, y, x, y, …]` slice, or `None`.
992    ///
993    /// Corresponds to `FPDFAnnot_GetInkListPath()`.
994    pub fn ink_list_path(&self, index: usize) -> Option<&[f32]> {
995        self.subtype_data
996            .ink_list
997            .as_ref()?
998            .get(index)
999            .map(|v| v.as_slice())
1000    }
1001
1002    /// ADR-019 alias for [`Self::ink_list_path()`].
1003    ///
1004    /// Corresponds to `FPDFAnnot_GetInkListPath()`.
1005    #[inline]
1006    pub fn annot_get_ink_list_path(&self, index: usize) -> Option<&[f32]> {
1007        self.ink_list_path(index)
1008    }
1009
1010    /// Deprecated: use [`annot_get_ink_list_path()`](Self::annot_get_ink_list_path) instead.
1011    #[deprecated(since = "0.1.0", note = "use annot_get_ink_list_path() instead")]
1012    #[inline]
1013    pub fn get_ink_list_path(&self, index: usize) -> Option<&[f32]> {
1014        self.ink_list_path(index)
1015    }
1016
1017    /// Returns the line endpoints for a Line annotation as two `[x, y]` pairs:
1018    /// `([x1, y1], [x2, y2])`, or `None` if absent.
1019    ///
1020    /// Corresponds to `FPDFAnnot_GetLine()`.
1021    pub fn line_endpoints(&self) -> Option<([f32; 2], [f32; 2])> {
1022        let pts = self.subtype_data.line_points?;
1023        Some(([pts[0], pts[1]], [pts[2], pts[3]]))
1024    }
1025
1026    /// ADR-019 alias for [`Self::line_endpoints()`].
1027    ///
1028    /// Corresponds to `FPDFAnnot_GetLine()`.
1029    #[inline]
1030    pub fn annot_get_line(&self) -> Option<([f32; 2], [f32; 2])> {
1031        self.line_endpoints()
1032    }
1033
1034    /// Deprecated: use [`annot_get_line()`](Self::annot_get_line) instead.
1035    #[deprecated(since = "0.1.0", note = "use annot_get_line() instead")]
1036    #[inline]
1037    pub fn get_line(&self) -> Option<([f32; 2], [f32; 2])> {
1038        self.line_endpoints()
1039    }
1040
1041    /// Deprecated: use [`annot_get_line()`](Self::annot_get_line) instead.
1042    #[deprecated(since = "0.1.0", note = "use annot_get_line() instead")]
1043    #[inline]
1044    pub fn get_line_endpoints(&self) -> Option<([f32; 2], [f32; 2])> {
1045        self.line_endpoints()
1046    }
1047
1048    /// Returns the border specification as `(horizontal_radius, vertical_radius, width)`.
1049    ///
1050    /// Because `AnnotationBorder` only stores `width` (the `/Border` h/v radii are
1051    /// not retained), the returned tuple is always `(0.0, 0.0, width)`.
1052    ///
1053    /// Corresponds to `FPDFAnnot_GetBorder()`.
1054    pub fn border_style(&self) -> Option<(f32, f32, f32)> {
1055        self.border.as_ref().map(|b| (0.0_f32, 0.0_f32, b.width))
1056    }
1057
1058    /// ADR-019 alias for [`Self::border_style()`].
1059    ///
1060    /// Corresponds to `FPDFAnnot_GetBorder()`.
1061    #[inline]
1062    pub fn annot_get_border(&self) -> Option<(f32, f32, f32)> {
1063        self.border_style()
1064    }
1065
1066    /// Deprecated: use [`annot_get_border()`](Self::annot_get_border) instead.
1067    #[deprecated(since = "0.1.0", note = "use annot_get_border() instead")]
1068    #[inline]
1069    pub fn get_border(&self) -> Option<(f32, f32, f32)> {
1070        self.border_style()
1071    }
1072
1073    /// Deprecated: use [`annot_get_border()`](Self::annot_get_border) instead.
1074    #[deprecated(since = "0.1.0", note = "use annot_get_border() instead")]
1075    #[inline]
1076    pub fn get_border_style(&self) -> Option<(f32, f32, f32)> {
1077        self.border_style()
1078    }
1079
1080    /// Returns the annotation color as `(red, green, blue, alpha)` in 0–255 range.
1081    ///
1082    /// Converts from the PDF `/C` array (0.0–1.0 floats):
1083    /// - 1 component → gray (R=G=B=gray, A=255)
1084    /// - 3 components → RGB, A=255
1085    /// - 4 components → CMYK converted to RGB (approximate), A=255
1086    /// - other → `None`
1087    ///
1088    /// Corresponds to `FPDFAnnot_GetColor()`.
1089    pub fn color_rgba(&self) -> Option<(u8, u8, u8, u8)> {
1090        let c = self.color.as_ref()?;
1091        let to_u8 = |f: f32| (f.clamp(0.0, 1.0) * 255.0).round() as u8;
1092        match c.len() {
1093            1 => {
1094                let gray = to_u8(c[0]);
1095                Some((gray, gray, gray, 255))
1096            }
1097            3 => Some((to_u8(c[0]), to_u8(c[1]), to_u8(c[2]), 255)),
1098            4 => {
1099                // Approximate CMYK → RGB: R = (1−C)*(1−K), etc.
1100                let k = c[3].clamp(0.0, 1.0);
1101                let r = to_u8((1.0 - c[0].clamp(0.0, 1.0)) * (1.0 - k));
1102                let g = to_u8((1.0 - c[1].clamp(0.0, 1.0)) * (1.0 - k));
1103                let b = to_u8((1.0 - c[2].clamp(0.0, 1.0)) * (1.0 - k));
1104                Some((r, g, b, 255))
1105            }
1106            _ => None,
1107        }
1108    }
1109
1110    /// ADR-019 alias for [`Self::color_rgba()`].
1111    ///
1112    /// Corresponds to `FPDFAnnot_GetColor()`.
1113    #[inline]
1114    pub fn annot_get_color(&self) -> Option<(u8, u8, u8, u8)> {
1115        self.color_rgba()
1116    }
1117
1118    /// Deprecated: use [`annot_get_color()`](Self::annot_get_color) instead.
1119    #[deprecated(since = "0.1.0", note = "use annot_get_color() instead")]
1120    #[inline]
1121    pub fn get_color(&self) -> Option<(u8, u8, u8, u8)> {
1122        self.color_rgba()
1123    }
1124
1125    /// Deprecated: use [`annot_get_color()`](Self::annot_get_color) instead.
1126    #[deprecated(since = "0.1.0", note = "use annot_get_color() instead")]
1127    #[inline]
1128    pub fn get_color_rgba(&self) -> Option<(u8, u8, u8, u8)> {
1129        self.color_rgba()
1130    }
1131
1132    /// Returns the string value for a well-known annotation key.
1133    ///
1134    /// Supported keys: `"Contents"`, `"NM"` (annotation name/label),
1135    /// `"T"` (partial field name), `"TU"` (alternate field name),
1136    /// `"V"` (current field value).
1137    ///
1138    /// Corresponds to `FPDFAnnot_GetStringValue()`.
1139    pub fn string_value(&self, key: &str) -> Option<&str> {
1140        match key {
1141            "Contents" => self.contents.as_deref(),
1142            "NM" => self.name.as_deref(),
1143            "T" => self.field_name.as_deref(),
1144            "TU" => self.alternate_name.as_deref(),
1145            "V" => self.field_value.as_deref(),
1146            _ => None,
1147        }
1148    }
1149
1150    /// ADR-019 alias for [`Self::string_value()`].
1151    ///
1152    /// Corresponds to `FPDFAnnot_GetStringValue()`.
1153    #[inline]
1154    pub fn annot_get_string_value(&self, key: &str) -> Option<&str> {
1155        self.string_value(key)
1156    }
1157
1158    /// Deprecated: use [`annot_get_string_value()`](Self::annot_get_string_value) instead.
1159    #[deprecated(since = "0.1.0", note = "use annot_get_string_value() instead")]
1160    #[inline]
1161    pub fn get_string_value(&self, key: &str) -> Option<&str> {
1162        self.string_value(key)
1163    }
1164
1165    /// Returns the partial field name (`/T` key).
1166    ///
1167    /// For widget annotations this is the form field name; for other annotation
1168    /// types it serves as the annotation title / author label.
1169    ///
1170    /// Corresponds to `FPDFAnnot_GetFormFieldName()`.
1171    pub fn field_name(&self) -> Option<&str> {
1172        self.field_name.as_deref()
1173    }
1174
1175    /// ADR-019 alias for [`Self::field_name()`].
1176    ///
1177    /// Corresponds to `FPDFAnnot_GetFormFieldName()`.
1178    #[inline]
1179    pub fn annot_get_form_field_name(&self) -> Option<&str> {
1180        self.field_name()
1181    }
1182
1183    /// Deprecated: use [`annot_get_form_field_name()`](Self::annot_get_form_field_name) instead.
1184    #[deprecated(since = "0.1.0", note = "use annot_get_form_field_name() instead")]
1185    #[inline]
1186    pub fn get_form_field_name(&self) -> Option<&str> {
1187        self.field_name()
1188    }
1189
1190    /// Deprecated: use [`annot_get_form_field_name()`](Self::annot_get_form_field_name) instead.
1191    #[deprecated(since = "0.1.0", note = "use annot_get_form_field_name() instead")]
1192    #[inline]
1193    pub fn get_field_name(&self) -> Option<&str> {
1194        self.field_name()
1195    }
1196
1197    /// Returns the alternate (user-visible) field name (`/TU` key).
1198    ///
1199    /// Only meaningful for widget annotations.
1200    ///
1201    /// Corresponds to `FPDFAnnot_GetFormFieldAlternateName()`.
1202    pub fn alternate_field_name(&self) -> Option<&str> {
1203        self.alternate_name.as_deref()
1204    }
1205
1206    /// ADR-019 alias for [`Self::alternate_field_name()`].
1207    ///
1208    /// Corresponds to `FPDFAnnot_GetFormFieldAlternateName()`.
1209    #[inline]
1210    pub fn annot_get_form_field_alternate_name(&self) -> Option<&str> {
1211        self.alternate_field_name()
1212    }
1213
1214    /// Deprecated: use [`annot_get_form_field_alternate_name()`](Self::annot_get_form_field_alternate_name) instead.
1215    #[deprecated(
1216        since = "0.1.0",
1217        note = "use annot_get_form_field_alternate_name() instead"
1218    )]
1219    #[inline]
1220    pub fn get_form_field_alternate_name(&self) -> Option<&str> {
1221        self.alternate_field_name()
1222    }
1223
1224    /// Deprecated: use [`annot_get_form_field_alternate_name()`](Self::annot_get_form_field_alternate_name) instead.
1225    #[deprecated(
1226        since = "0.1.0",
1227        note = "use annot_get_form_field_alternate_name() instead"
1228    )]
1229    #[inline]
1230    pub fn get_alternate_field_name(&self) -> Option<&str> {
1231        self.alternate_field_name()
1232    }
1233
1234    /// Returns the current field value as a string (`/V` key).
1235    ///
1236    /// For text fields this is the text value; for choice fields it is the
1237    /// selected option string; for check-box / radio-button fields it is the
1238    /// appearance state name.
1239    ///
1240    /// Corresponds to `FPDFAnnot_GetFormFieldValue()`.
1241    pub fn field_value_str(&self) -> Option<&str> {
1242        self.field_value.as_deref()
1243    }
1244
1245    /// ADR-019 alias for [`Self::field_value_str()`].
1246    ///
1247    /// Corresponds to `FPDFAnnot_GetFormFieldValue()`.
1248    #[inline]
1249    pub fn annot_get_form_field_value(&self) -> Option<&str> {
1250        self.field_value_str()
1251    }
1252
1253    /// Deprecated: use [`annot_get_form_field_value()`](Self::annot_get_form_field_value) instead.
1254    #[deprecated(since = "0.1.0", note = "use annot_get_form_field_value() instead")]
1255    #[inline]
1256    pub fn get_form_field_value(&self) -> Option<&str> {
1257        self.field_value_str()
1258    }
1259
1260    /// Deprecated: use [`annot_get_form_field_value()`](Self::annot_get_form_field_value) instead.
1261    #[deprecated(since = "0.1.0", note = "use annot_get_form_field_value() instead")]
1262    #[inline]
1263    pub fn get_field_value(&self) -> Option<&str> {
1264        self.field_value_str()
1265    }
1266
1267    /// Returns `true` if this annotation has a non-`None` value for the given key.
1268    ///
1269    /// Corresponds to `FPDFAnnot_HasKey()`.
1270    pub fn has_key(&self, key: &str) -> bool {
1271        self.string_value(key).is_some()
1272    }
1273
1274    /// ADR-019 alias for [`Self::has_key()`].
1275    ///
1276    /// Corresponds to `FPDFAnnot_HasKey()`.
1277    #[inline]
1278    pub fn annot_has_key(&self, key: &str) -> bool {
1279        self.has_key(key)
1280    }
1281
1282    /// Returns the numeric value for a well-known annotation key, or `None`.
1283    ///
1284    /// Currently supports: `"F"` (flags as u32), `"Border-Width"` (border width in points).
1285    ///
1286    /// Corresponds to `FPDFAnnot_GetNumberValue()`.
1287    pub fn number_value(&self, key: &str) -> Option<f32> {
1288        match key {
1289            "F" => Some(self.flags.bits() as f32),
1290            "Border-Width" => self.border.as_ref().map(|b| b.width),
1291            _ => None,
1292        }
1293    }
1294
1295    /// ADR-019 alias for [`Self::number_value()`].
1296    ///
1297    /// Corresponds to `FPDFAnnot_GetNumberValue()`.
1298    #[inline]
1299    pub fn annot_get_number_value(&self, key: &str) -> Option<f32> {
1300        self.number_value(key)
1301    }
1302
1303    /// Deprecated: use [`annot_get_number_value()`](Self::annot_get_number_value) instead.
1304    #[deprecated(since = "0.1.0", note = "use annot_get_number_value() instead")]
1305    #[inline]
1306    pub fn get_number_value(&self, key: &str) -> Option<f32> {
1307        self.number_value(key)
1308    }
1309
1310    // -----------------------------------------------------------------------
1311    // Gap fixes: value_type, appearance_stream_bytes, linked_annot_ref
1312    // -----------------------------------------------------------------------
1313
1314    /// Returns the PDF object type of the value stored under `key` in the
1315    /// annotation's logical field set.
1316    ///
1317    /// Keys inspected: `"Contents"`, `"NM"` (String), `"F"` (Number),
1318    /// `"Subtype"` (Name), `"AP"` (Dictionary), `"Rect"` (Array),
1319    /// `"C"` (Array), `"Vertices"` (Array), `"QuadPoints"` (Array),
1320    /// `"InkList"` (Array), `"IRT"` (Reference).
1321    ///
1322    /// Returns [`PdfValueType::Unknown`] if the key is not recognised or the
1323    /// corresponding field is `None`.
1324    ///
1325    /// Corresponds to `FPDFAnnot_GetValueType()`.
1326    pub fn value_type(&self, key: &str) -> PdfValueType {
1327        match key {
1328            "Contents" => {
1329                if self.contents.is_some() {
1330                    PdfValueType::String
1331                } else {
1332                    PdfValueType::Unknown
1333                }
1334            }
1335            "NM" => {
1336                if self.name.is_some() {
1337                    PdfValueType::String
1338                } else {
1339                    PdfValueType::Unknown
1340                }
1341            }
1342            "F" => PdfValueType::Number,
1343            "Subtype" => PdfValueType::Name,
1344            "AP" => {
1345                if self.appearance.is_some() {
1346                    PdfValueType::Dictionary
1347                } else {
1348                    PdfValueType::Unknown
1349                }
1350            }
1351            "Rect" => PdfValueType::Array,
1352            "C" => {
1353                if self.color.is_some() {
1354                    PdfValueType::Array
1355                } else {
1356                    PdfValueType::Unknown
1357                }
1358            }
1359            "Vertices" => {
1360                if self.subtype_data.vertices.is_some() {
1361                    PdfValueType::Array
1362                } else {
1363                    PdfValueType::Unknown
1364                }
1365            }
1366            "QuadPoints" => {
1367                if self.subtype_data.quad_points.is_some() {
1368                    PdfValueType::Array
1369                } else {
1370                    PdfValueType::Unknown
1371                }
1372            }
1373            "InkList" => {
1374                if self.subtype_data.ink_list.is_some() {
1375                    PdfValueType::Array
1376                } else {
1377                    PdfValueType::Unknown
1378                }
1379            }
1380            "IRT" => {
1381                if self.irt_ref.is_some() {
1382                    PdfValueType::Reference
1383                } else {
1384                    PdfValueType::Unknown
1385                }
1386            }
1387            _ => PdfValueType::Unknown,
1388        }
1389    }
1390
1391    /// ADR-019 alias for [`Self::value_type()`].
1392    ///
1393    /// Corresponds to `FPDFAnnot_GetValueType()`.
1394    #[inline]
1395    pub fn annot_get_value_type(&self, key: &str) -> PdfValueType {
1396        self.value_type(key)
1397    }
1398
1399    /// Deprecated: use [`annot_get_value_type()`](Self::annot_get_value_type) instead.
1400    #[deprecated(since = "0.1.0", note = "use annot_get_value_type() instead")]
1401    #[inline]
1402    pub fn get_value_type(&self, key: &str) -> PdfValueType {
1403        self.value_type(key)
1404    }
1405
1406    /// Returns the decoded content bytes of the annotation's appearance stream
1407    /// for the given `mode`, or `None` if no appearance stream is available for
1408    /// that mode.
1409    ///
1410    /// The bytes are the decoded (post-filter) PDF content stream data from
1411    /// the `/AP /N` (Normal), `/AP /R` (Rollover), or `/AP /D` (Down)
1412    /// sub-entry.
1413    ///
1414    /// Corresponds to `FPDFAnnot_GetAP()`.
1415    pub fn appearance_stream_bytes(&self, mode: AppearanceMode) -> Option<&[u8]> {
1416        match mode {
1417            AppearanceMode::Normal => self.ap_n_bytes.as_deref(),
1418            AppearanceMode::Rollover => self.ap_r_bytes.as_deref(),
1419            AppearanceMode::Down => self.ap_d_bytes.as_deref(),
1420        }
1421    }
1422
1423    /// ADR-019 alias for [`Self::appearance_stream_bytes()`].
1424    ///
1425    /// Corresponds to `FPDFAnnot_GetAP()`.
1426    #[inline]
1427    pub fn annot_get_ap(&self, mode: AppearanceMode) -> Option<&[u8]> {
1428        self.appearance_stream_bytes(mode)
1429    }
1430
1431    /// Deprecated: use [`annot_get_ap()`](Self::annot_get_ap) instead.
1432    #[deprecated(since = "0.1.0", note = "use annot_get_ap() instead")]
1433    #[inline]
1434    pub fn get_ap(&self, mode: AppearanceMode) -> Option<&[u8]> {
1435        self.appearance_stream_bytes(mode)
1436    }
1437
1438    /// Deprecated: use [`annot_get_ap()`](Self::annot_get_ap) instead.
1439    #[deprecated(since = "0.1.0", note = "use annot_get_ap() instead")]
1440    #[inline]
1441    pub fn get_appearance_stream_bytes(&self, mode: AppearanceMode) -> Option<&[u8]> {
1442        self.appearance_stream_bytes(mode)
1443    }
1444
1445    /// Set the appearance stream for the given mode.
1446    ///
1447    /// # Not Supported
1448    ///
1449    /// AP stream mutation is not yet supported (requires Form XObject creation).
1450    /// This stub is provided for API completeness per ADR-017.
1451    ///
1452    /// Corresponds to `FPDFAnnot_SetAP()`.
1453    pub fn set_appearance_stream_bytes(
1454        &mut self,
1455        _mode: AppearanceMode,
1456        _data: &[u8],
1457    ) -> DocResult<()> {
1458        Err(DocError::NotSupported(
1459            "set_appearance_stream_bytes: AP stream mutation not yet supported".into(),
1460        ))
1461    }
1462
1463    /// ADR-019 alias for [`Self::set_appearance_stream_bytes()`].
1464    ///
1465    /// Corresponds to `FPDFAnnot_SetAP()`.
1466    #[inline]
1467    pub fn annot_set_ap(&mut self, mode: AppearanceMode, data: &[u8]) -> DocResult<()> {
1468        self.set_appearance_stream_bytes(mode, data)
1469    }
1470
1471    /// Deprecated: use [`annot_set_ap()`](Self::annot_set_ap) instead.
1472    #[deprecated(since = "0.1.0", note = "use annot_set_ap() instead")]
1473    #[inline]
1474    pub fn set_ap(&mut self, mode: AppearanceMode, data: &[u8]) -> DocResult<()> {
1475        self.set_appearance_stream_bytes(mode, data)
1476    }
1477
1478    /// Returns the `ObjectId` of the annotation referenced by `/IRT`
1479    /// ("In Reply To"), if any.
1480    ///
1481    /// The `/IRT` entry is present on annotations that are replies to another
1482    /// annotation (e.g., a reply thread in a comment workflow). The returned
1483    /// `ObjectId` identifies the parent annotation object.
1484    ///
1485    /// Corresponds to `FPDFAnnot_GetLinkedAnnot()`.
1486    pub fn linked_annot_ref(&self) -> Option<ObjectId> {
1487        self.irt_ref
1488    }
1489
1490    /// ADR-019 alias for [`Self::linked_annot_ref()`].
1491    ///
1492    /// Corresponds to `FPDFAnnot_GetLinkedAnnot()`.
1493    #[inline]
1494    pub fn annot_get_linked_annot(&self) -> Option<ObjectId> {
1495        self.linked_annot_ref()
1496    }
1497
1498    /// Deprecated: use [`annot_get_linked_annot()`](Self::annot_get_linked_annot) instead.
1499    #[deprecated(since = "0.1.0", note = "use annot_get_linked_annot() instead")]
1500    #[inline]
1501    pub fn get_linked_annot(&self) -> Option<ObjectId> {
1502        self.linked_annot_ref()
1503    }
1504
1505    // -----------------------------------------------------------------------
1506    // Gap fixes: form_field_flags, get_link, file_attachment
1507    // -----------------------------------------------------------------------
1508
1509    /// Returns the form field flags (`/Ff`) for widget annotations, or `None`
1510    /// if the annotation has no `/Ff` entry.
1511    ///
1512    /// The flags encode properties such as read-only, required, multiline,
1513    /// password, etc. (ISO 32000-2 Table 226).
1514    ///
1515    /// Unlike the upstream `FPDFAnnot_GetFormFieldFlags()`, this method does
1516    /// not require a `FORMHANDLE` parameter because rpdfium embeds the flags
1517    /// directly in the parsed `Annotation` struct.
1518    ///
1519    /// Corresponds to `FPDFAnnot_GetFormFieldFlags()`.
1520    pub fn form_field_flags(&self) -> Option<FormFieldFlags> {
1521        self.form_field_flags
1522    }
1523
1524    /// ADR-019 alias for [`Self::form_field_flags()`].
1525    ///
1526    /// Corresponds to `FPDFAnnot_GetFormFieldFlags()`.
1527    #[inline]
1528    pub fn annot_get_form_field_flags(&self) -> Option<FormFieldFlags> {
1529        self.form_field_flags()
1530    }
1531
1532    /// Deprecated: use [`annot_get_form_field_flags()`](Self::annot_get_form_field_flags) instead.
1533    #[deprecated(since = "0.1.0", note = "use annot_get_form_field_flags() instead")]
1534    #[inline]
1535    pub fn get_form_field_flags(&self) -> Option<FormFieldFlags> {
1536        self.form_field_flags()
1537    }
1538
1539    // -----------------------------------------------------------------------
1540    // font_size() / annot_get_font_size()
1541    // -----------------------------------------------------------------------
1542
1543    /// Returns the font size from the default appearance (`/DA`) string.
1544    ///
1545    /// Parses the `Tf` operator in the `/DA` string. Returns `None` if no `/DA`
1546    /// is present or the font size is 0 (variable — determined by field size).
1547    ///
1548    /// Valid for FreeText and Widget annotations.
1549    ///
1550    /// Corresponds to `FPDFAnnot_GetFontSize()`.
1551    pub fn font_size(&self) -> Option<f32> {
1552        let da = self.subtype_data.default_appearance.as_deref()?;
1553        let parsed = parse_default_appearance(da);
1554        let size = parsed.font_size as f32;
1555        if size == 0.0 { None } else { Some(size) }
1556    }
1557
1558    /// ADR-019 alias for [`Self::font_size()`].
1559    ///
1560    /// Corresponds to `FPDFAnnot_GetFontSize()`.
1561    #[inline]
1562    pub fn annot_get_font_size(&self) -> Option<f32> {
1563        self.font_size()
1564    }
1565
1566    // -----------------------------------------------------------------------
1567    // font_color() / annot_get_font_color() / set_font_color() / annot_set_font_color()
1568    // -----------------------------------------------------------------------
1569
1570    /// Returns the font color from the default appearance (`/DA`) string as `(R, G, B)`.
1571    ///
1572    /// Parses the color operator (`g`, `rg`, or `k`) in the `/DA` string and
1573    /// converts to 8-bit RGB values (0–255). Returns `None` if no color is
1574    /// specified in the DA string.
1575    ///
1576    /// Valid for FreeText and Widget annotations.
1577    ///
1578    /// Corresponds to `FPDFAnnot_GetFontColor()`.
1579    pub fn font_color(&self) -> Option<(u8, u8, u8)> {
1580        let da = self.subtype_data.default_appearance.as_deref()?;
1581        let parsed = parse_default_appearance(da);
1582        let color = parsed.color?;
1583        match color.len() {
1584            // Grayscale: g g g
1585            1 => {
1586                let g = (color[0].clamp(0.0, 1.0) * 255.0).round() as u8;
1587                Some((g, g, g))
1588            }
1589            // RGB
1590            3 => {
1591                let r = (color[0].clamp(0.0, 1.0) * 255.0).round() as u8;
1592                let g = (color[1].clamp(0.0, 1.0) * 255.0).round() as u8;
1593                let b = (color[2].clamp(0.0, 1.0) * 255.0).round() as u8;
1594                Some((r, g, b))
1595            }
1596            // CMYK: approximate conversion to RGB
1597            4 => {
1598                let c = color[0].clamp(0.0, 1.0);
1599                let m = color[1].clamp(0.0, 1.0);
1600                let y = color[2].clamp(0.0, 1.0);
1601                let k = color[3].clamp(0.0, 1.0);
1602                let r = ((1.0 - c) * (1.0 - k) * 255.0).round() as u8;
1603                let g = ((1.0 - m) * (1.0 - k) * 255.0).round() as u8;
1604                let b = ((1.0 - y) * (1.0 - k) * 255.0).round() as u8;
1605                Some((r, g, b))
1606            }
1607            _ => None,
1608        }
1609    }
1610
1611    /// ADR-019 alias for [`Self::font_color()`].
1612    ///
1613    /// Corresponds to `FPDFAnnot_GetFontColor()`.
1614    #[inline]
1615    pub fn annot_get_font_color(&self) -> Option<(u8, u8, u8)> {
1616        self.font_color()
1617    }
1618
1619    /// Sets the font color in the default appearance (`/DA`) string.
1620    ///
1621    /// Updates the in-memory default appearance string by replacing or appending
1622    /// an `rg` color operator. To persist, use the edit layer to re-serialize
1623    /// the annotation.
1624    ///
1625    /// Valid for FreeText and Widget annotations. Returns `Err` if the
1626    /// annotation subtype does not support a `/DA` string.
1627    ///
1628    /// Corresponds to `FPDFAnnot_SetFontColor()`.
1629    pub fn set_font_color(&mut self, r: u8, g: u8, b: u8) -> DocResult<()> {
1630        match self.subtype {
1631            AnnotationType::FreeText | AnnotationType::Widget => {}
1632            _ => {
1633                return Err(DocError::NotSupported(
1634                    "set_font_color: only FreeText and Widget annotations support /DA".into(),
1635                ));
1636            }
1637        }
1638        let rf = r as f64 / 255.0;
1639        let gf = g as f64 / 255.0;
1640        let bf = b as f64 / 255.0;
1641
1642        let color_token = format!("{rf:.4} {gf:.4} {bf:.4} rg");
1643        let current = self
1644            .subtype_data
1645            .default_appearance
1646            .get_or_insert_with(String::new);
1647        // Remove any existing color operator (g, rg, k) and append the new one.
1648        let stripped = strip_da_color(current);
1649        *current = format!("{stripped} {color_token}").trim().to_owned();
1650        Ok(())
1651    }
1652
1653    /// ADR-019 alias for [`Self::set_font_color()`].
1654    ///
1655    /// Corresponds to `FPDFAnnot_SetFontColor()`.
1656    #[inline]
1657    pub fn annot_set_font_color(&mut self, r: u8, g: u8, b: u8) -> DocResult<()> {
1658        self.set_font_color(r, g, b)
1659    }
1660
1661    // -----------------------------------------------------------------------
1662    // form_additional_action_javascript() / annot_get_form_additional_action_javascript()
1663    // -----------------------------------------------------------------------
1664
1665    /// Returns the JavaScript code for a form field additional action event.
1666    ///
1667    /// Looks up the `/AA` entry for `event_type` on Widget annotations and
1668    /// returns the JavaScript code if the action is a `JavaScript` action.
1669    /// Returns an empty string if no such action exists or the action is not
1670    /// a JavaScript action (matching upstream `FPDFAnnot_GetFormAdditionalActionJavaScript`
1671    /// behavior).
1672    ///
1673    /// Only meaningful for Widget annotations.
1674    ///
1675    /// Corresponds to `FPDFAnnot_GetFormAdditionalActionJavaScript()`.
1676    pub fn form_additional_action_javascript(&self, event_type: AActionType) -> String {
1677        self.additional_actions
1678            .as_ref()
1679            .and_then(|aa| aa.action(event_type))
1680            .map(|a| a.javascript())
1681            .unwrap_or_default()
1682    }
1683
1684    /// ADR-019 alias for [`Self::form_additional_action_javascript()`].
1685    ///
1686    /// Corresponds to `FPDFAnnot_GetFormAdditionalActionJavaScript()`.
1687    #[inline]
1688    pub fn annot_get_form_additional_action_javascript(&self, event_type: AActionType) -> String {
1689        self.form_additional_action_javascript(event_type)
1690    }
1691
1692    /// Returns the action and destination stored on this annotation, intended
1693    /// for use with Link annotations.
1694    ///
1695    /// Returns `(action, destination)` where each component may be `None` if
1696    /// not present in the annotation dictionary.
1697    ///
1698    /// In the upstream PDFium API, `FPDFAnnot_GetLink()` returns an opaque
1699    /// `FPDF_LINK` handle wrapping the same data. rpdfium instead returns a
1700    /// tuple of references.
1701    ///
1702    /// Corresponds to `FPDFAnnot_GetLink()`.
1703    pub fn link_ref(&self) -> (Option<&Action>, Option<&Destination>) {
1704        (self.action.as_ref(), self.destination.as_ref())
1705    }
1706
1707    /// ADR-019 alias for [`Self::link_ref()`].
1708    ///
1709    /// Corresponds to `FPDFAnnot_GetLink()`.
1710    #[inline]
1711    pub fn annot_get_link(&self) -> (Option<&Action>, Option<&Destination>) {
1712        self.link_ref()
1713    }
1714
1715    /// Deprecated: use [`annot_get_link()`](Self::annot_get_link) instead.
1716    #[deprecated(since = "0.1.0", note = "use annot_get_link() instead")]
1717    #[inline]
1718    pub fn get_link(&self) -> (Option<&Action>, Option<&Destination>) {
1719        self.link_ref()
1720    }
1721
1722    /// Returns the file specification attached to a FileAttachment annotation
1723    /// (the `/FS` entry), or `None` if not present.
1724    ///
1725    /// Corresponds to `FPDFAnnot_GetFileAttachment()`.
1726    pub fn file_attachment(&self) -> Option<&FileSpec> {
1727        self.file_spec.as_ref()
1728    }
1729
1730    /// ADR-019 alias for [`Self::file_attachment()`].
1731    ///
1732    /// Corresponds to `FPDFAnnot_GetFileAttachment()`.
1733    #[inline]
1734    pub fn annot_get_file_attachment(&self) -> Option<&FileSpec> {
1735        self.file_attachment()
1736    }
1737
1738    /// Deprecated: use [`annot_get_file_attachment()`](Self::annot_get_file_attachment) instead.
1739    #[deprecated(since = "0.1.0", note = "use annot_get_file_attachment() instead")]
1740    #[inline]
1741    pub fn get_file_attachment(&self) -> Option<&FileSpec> {
1742        self.file_attachment()
1743    }
1744
1745    // -----------------------------------------------------------------------
1746    // G6: form_field_type() — FPDFAnnot_GetFormFieldType
1747    // -----------------------------------------------------------------------
1748
1749    /// Returns the form field type (`/FT`) for widget annotations.
1750    ///
1751    /// Returns `None` for non-widget annotations or when `/FT` is only
1752    /// present on a parent field dictionary.
1753    ///
1754    /// Corresponds to `FPDFAnnot_GetFormFieldType()`.
1755    pub fn form_field_type(&self) -> Option<FormFieldType> {
1756        self.form_field_type.clone()
1757    }
1758
1759    /// ADR-019 alias for [`Self::form_field_type()`].
1760    ///
1761    /// Corresponds to `FPDFAnnot_GetFormFieldType()`.
1762    #[inline]
1763    pub fn annot_get_form_field_type(&self) -> Option<FormFieldType> {
1764        self.form_field_type()
1765    }
1766
1767    // -----------------------------------------------------------------------
1768    // G6 stubs: form_control_count / form_control_index / form_field_export_value
1769    // -----------------------------------------------------------------------
1770
1771    /// Returns the number of form controls for this annotation in the AcroForm.
1772    ///
1773    /// # Not Supported
1774    ///
1775    /// Requires document-level AcroForm traversal which is not supported in
1776    /// the annotation-level API (ADR-002: read-only, no form session).
1777    ///
1778    /// Corresponds to `FPDFAnnot_GetFormControlCount()`.
1779    pub fn form_control_count(&self) -> DocResult<usize> {
1780        Err(DocError::NotSupported(
1781            "form_control_count: requires document-level AcroForm traversal (ADR-002)".into(),
1782        ))
1783    }
1784
1785    /// ADR-019 alias for [`Self::form_control_count()`].
1786    ///
1787    /// Corresponds to `FPDFAnnot_GetFormControlCount()`.
1788    #[inline]
1789    pub fn annot_get_form_control_count(&self) -> DocResult<usize> {
1790        self.form_control_count()
1791    }
1792
1793    /// Returns the index of this annotation in its form control list.
1794    ///
1795    /// # Not Supported
1796    ///
1797    /// Requires document-level AcroForm traversal which is not supported in
1798    /// the annotation-level API (ADR-002: read-only, no form session).
1799    ///
1800    /// Corresponds to `FPDFAnnot_GetFormControlIndex()`.
1801    pub fn form_control_index(&self) -> DocResult<usize> {
1802        Err(DocError::NotSupported(
1803            "form_control_index: requires document-level AcroForm traversal (ADR-002)".into(),
1804        ))
1805    }
1806
1807    /// ADR-019 alias for [`Self::form_control_index()`].
1808    ///
1809    /// Corresponds to `FPDFAnnot_GetFormControlIndex()`.
1810    #[inline]
1811    pub fn annot_get_form_control_index(&self) -> DocResult<usize> {
1812        self.form_control_index()
1813    }
1814
1815    /// Returns the export value for this form control.
1816    ///
1817    /// # Not Supported
1818    ///
1819    /// Requires document-level AcroForm traversal which is not supported in
1820    /// the annotation-level API (ADR-002: read-only, no form session).
1821    ///
1822    /// Corresponds to `FPDFAnnot_GetFormFieldExportValue()`.
1823    pub fn form_field_export_value(&self) -> DocResult<String> {
1824        Err(DocError::NotSupported(
1825            "form_field_export_value: requires document-level AcroForm traversal (ADR-002)".into(),
1826        ))
1827    }
1828
1829    /// ADR-019 alias for [`Self::form_field_export_value()`].
1830    ///
1831    /// Corresponds to `FPDFAnnot_GetFormFieldExportValue()`.
1832    #[inline]
1833    pub fn annot_get_form_field_export_value(&self) -> DocResult<String> {
1834        self.form_field_export_value()
1835    }
1836
1837    // -----------------------------------------------------------------------
1838    // G7: is_checked / option_count / option_label / is_option_selected
1839    // -----------------------------------------------------------------------
1840
1841    /// Returns `true` if this checkbox or radio-button annotation is checked.
1842    ///
1843    /// A check-box is considered checked when its `/V` value is anything
1844    /// other than `"Off"` or absent.
1845    ///
1846    /// Corresponds to `FPDFAnnot_IsChecked()`.
1847    pub fn is_checked(&self) -> bool {
1848        match self.field_value.as_deref() {
1849            None | Some("Off") => false,
1850            Some(_) => true,
1851        }
1852    }
1853
1854    /// ADR-019 alias for [`Self::is_checked()`].
1855    ///
1856    /// Corresponds to `FPDFAnnot_IsChecked()`.
1857    #[inline]
1858    pub fn annot_is_checked(&self) -> bool {
1859        self.is_checked()
1860    }
1861
1862    /// Returns the number of options in a choice-field annotation.
1863    ///
1864    /// Returns `0` if the annotation has no `/Opt` array (non-choice fields).
1865    ///
1866    /// Corresponds to `FPDFAnnot_GetOptionCount()`.
1867    pub fn option_count(&self) -> usize {
1868        self.options.as_ref().map_or(0, |v| v.len())
1869    }
1870
1871    /// ADR-019 alias for [`Self::option_count()`].
1872    ///
1873    /// Corresponds to `FPDFAnnot_GetOptionCount()`.
1874    #[inline]
1875    pub fn annot_get_option_count(&self) -> usize {
1876        self.option_count()
1877    }
1878
1879    /// Returns the display label for the option at `index`, or `None` if
1880    /// the index is out of bounds.
1881    ///
1882    /// Corresponds to `FPDFAnnot_GetOptionLabel()`.
1883    pub fn option_label(&self, index: usize) -> Option<&str> {
1884        self.options
1885            .as_ref()?
1886            .get(index)
1887            .map(|o| o.display_value.as_str())
1888    }
1889
1890    /// ADR-019 alias for [`Self::option_label()`].
1891    ///
1892    /// Corresponds to `FPDFAnnot_GetOptionLabel()`.
1893    #[inline]
1894    pub fn annot_get_option_label(&self, index: usize) -> Option<&str> {
1895        self.option_label(index)
1896    }
1897
1898    /// Returns `true` if the option at `index` is currently selected.
1899    ///
1900    /// For choice fields the selected option is identified by comparing the
1901    /// option's `export_value` against the annotation's `/V` string.
1902    ///
1903    /// Corresponds to `FPDFAnnot_IsOptionSelected()`.
1904    pub fn is_option_selected(&self, index: usize) -> bool {
1905        let Some(opts) = self.options.as_ref() else {
1906            return false;
1907        };
1908        let Some(opt) = opts.get(index) else {
1909            return false;
1910        };
1911        match self.field_value.as_deref() {
1912            Some(v) => v == opt.export_value,
1913            None => false,
1914        }
1915    }
1916
1917    /// ADR-019 alias for [`Self::is_option_selected()`].
1918    ///
1919    /// Corresponds to `FPDFAnnot_IsOptionSelected()`.
1920    #[inline]
1921    pub fn annot_is_option_selected(&self, index: usize) -> bool {
1922        self.is_option_selected(index)
1923    }
1924
1925    // -----------------------------------------------------------------------
1926    // G8 stubs: focusable subtypes — FPDFAnnot_GetFocusableSubtypesCount /
1927    //           FPDFAnnot_GetFocusableSubtypes / FPDFAnnot_SetFocusableSubtypes
1928    // -----------------------------------------------------------------------
1929
1930    /// Returns the count of focusable annotation subtypes configured on the viewer.
1931    ///
1932    /// # Not Supported
1933    ///
1934    /// Focusable subtype configuration is a viewer/form-session concept that
1935    /// requires a `FPDF_FORMHANDLE` and is not supported in rpdfium's
1936    /// document-level read-only API (ADR-002).
1937    ///
1938    /// Corresponds to `FPDFAnnot_GetFocusableSubtypesCount()`.
1939    pub fn focusable_subtype_count(&self) -> DocResult<usize> {
1940        Err(DocError::NotSupported(
1941            "focusable_subtype_count: requires FPDF_FORMHANDLE viewer session (ADR-002)".into(),
1942        ))
1943    }
1944
1945    /// ADR-019 alias for [`Self::focusable_subtype_count()`].
1946    ///
1947    /// Corresponds to `FPDFAnnot_GetFocusableSubtypesCount()`.
1948    #[inline]
1949    pub fn annot_get_focusable_subtypes_count(&self) -> DocResult<usize> {
1950        self.focusable_subtype_count()
1951    }
1952
1953    /// Sets the focusable annotation subtypes on the viewer.
1954    ///
1955    /// # Not Supported
1956    ///
1957    /// Focusable subtype configuration is a viewer/form-session concept that
1958    /// requires a `FPDF_FORMHANDLE` and is not supported in rpdfium's
1959    /// document-level read-only API (ADR-002).
1960    ///
1961    /// Corresponds to `FPDFAnnot_SetFocusableSubtypes()`.
1962    pub fn set_focusable_subtypes(&mut self, _subtypes: &[AnnotationType]) -> DocResult<()> {
1963        Err(DocError::NotSupported(
1964            "set_focusable_subtypes: requires FPDF_FORMHANDLE viewer session (ADR-002)".into(),
1965        ))
1966    }
1967
1968    /// ADR-019 alias for [`Self::set_focusable_subtypes()`].
1969    ///
1970    /// Corresponds to `FPDFAnnot_SetFocusableSubtypes()`.
1971    #[inline]
1972    pub fn annot_set_focusable_subtypes(&mut self, subtypes: &[AnnotationType]) -> DocResult<()> {
1973        self.set_focusable_subtypes(subtypes)
1974    }
1975
1976    // -----------------------------------------------------------------------
1977    // G9: set_flags / set_form_field_flags
1978    // -----------------------------------------------------------------------
1979
1980    /// Sets the annotation flags (`/F`).
1981    ///
1982    /// Updates the in-memory flags. To persist, re-serialize the annotation
1983    /// via the edit layer.
1984    ///
1985    /// Corresponds to `FPDFAnnot_SetFlags()`.
1986    pub fn set_flags(&mut self, flags: AnnotationFlags) -> DocResult<()> {
1987        self.flags = flags;
1988        Ok(())
1989    }
1990
1991    /// ADR-019 alias for [`Self::set_flags()`].
1992    ///
1993    /// Corresponds to `FPDFAnnot_SetFlags()`.
1994    #[inline]
1995    pub fn annot_set_flags(&mut self, flags: AnnotationFlags) -> DocResult<()> {
1996        self.set_flags(flags)
1997    }
1998
1999    /// Sets the form field flags (`/Ff`) for widget annotations.
2000    ///
2001    /// Updates the in-memory flags. To persist, re-serialize the annotation
2002    /// via the edit layer.
2003    ///
2004    /// Corresponds to `FPDFAnnot_SetFormFieldFlags()` (planned upstream API).
2005    pub fn set_form_field_flags(&mut self, flags: FormFieldFlags) -> DocResult<()> {
2006        self.form_field_flags = Some(flags);
2007        Ok(())
2008    }
2009
2010    /// ADR-019 alias for [`Self::set_form_field_flags()`].
2011    ///
2012    /// Corresponds to `FPDFAnnot_SetFormFieldFlags()`.
2013    #[inline]
2014    pub fn annot_set_form_field_flags(&mut self, flags: FormFieldFlags) -> DocResult<()> {
2015        self.set_form_field_flags(flags)
2016    }
2017}
2018
2019/// Decode AP stream bytes for Normal, Rollover, and Down modes.
2020///
2021/// Reads the `/AP` dictionary, resolves each sub-entry (`/N`, `/R`, `/D`),
2022/// and decodes the stream data if present. Failures are silently mapped to
2023/// `None` so that a missing or undecodable stream does not prevent annotation
2024/// parsing from succeeding.
2025#[allow(clippy::type_complexity)]
2026fn extract_ap_stream_bytes<S: PdfSource>(
2027    dict: &std::collections::HashMap<Name, Object>,
2028    store: &ObjectStore<S>,
2029    object_id: Option<ObjectId>,
2030) -> (Option<Vec<u8>>, Option<Vec<u8>>, Option<Vec<u8>>) {
2031    let ap_entry = match dict.get(&Name::ap()) {
2032        None => return (None, None, None),
2033        Some(o) => o,
2034    };
2035    let resolved_ap = match store.deep_resolve(ap_entry) {
2036        Ok(o) => o,
2037        Err(_) => return (None, None, None),
2038    };
2039    let ap_dict = match resolved_ap.as_dict() {
2040        None => return (None, None, None),
2041        Some(d) => d,
2042    };
2043
2044    let decode_sub = |key: &Name| -> Option<Vec<u8>> {
2045        let sub = ap_dict.get(key)?;
2046        // Sub-entry may be an indirect reference to a stream object
2047        let stream_obj = if let Some(ref_id) = sub.as_reference() {
2048            match store.resolve(ref_id) {
2049                Ok(o) => o,
2050                Err(_) => return None,
2051            }
2052        } else {
2053            // Inline stream or dictionary — resolve in place
2054            match store.deep_resolve(sub) {
2055                Ok(o) => o,
2056                Err(_) => return None,
2057            }
2058        };
2059        // For state dicts, pick the first stream entry
2060        let actual_stream =
2061            if stream_obj.as_dict().is_some() && stream_obj.as_stream_dict().is_none() {
2062                // It's a plain dictionary (state dict), find first reference value
2063                let state_dict = stream_obj.as_dict()?;
2064                let mut found = None;
2065                for val in state_dict.values() {
2066                    if let Some(ref_id) = val.as_reference() {
2067                        if let Ok(s) = store.resolve(ref_id) {
2068                            found = Some(s);
2069                            break;
2070                        }
2071                    }
2072                }
2073                found?
2074            } else {
2075                stream_obj
2076            };
2077        // Decode the stream; use object_id for encryption context if available
2078        let dummy_id = ObjectId::new(0, 0);
2079        let oid = object_id.unwrap_or(dummy_id);
2080        store.decode_stream_for_object(actual_stream, oid).ok()
2081    };
2082
2083    let n = decode_sub(&Name::n());
2084    let r = decode_sub(&Name::r());
2085    let d = decode_sub(&Name::d());
2086    (n, r, d)
2087}
2088
2089/// Parse the `/Rect` array from an annotation dictionary.
2090fn parse_rect<S: PdfSource>(
2091    dict: &std::collections::HashMap<Name, Object>,
2092    store: &ObjectStore<S>,
2093) -> DocResult<[f32; 4]> {
2094    let rect_obj = dict
2095        .get(&Name::rect())
2096        .ok_or_else(|| DocError::MissingKey("/Rect".into()))?;
2097    let resolved = store
2098        .deep_resolve(rect_obj)
2099        .map_err(|e| DocError::Parser(e.to_string()))?;
2100    let arr = resolved.as_array().ok_or(DocError::UnexpectedType)?;
2101    if arr.len() < 4 {
2102        return Err(DocError::UnexpectedType);
2103    }
2104
2105    let get = |idx: usize| -> f32 {
2106        arr.get(idx)
2107            .and_then(|o| o.as_f64())
2108            .map(|f| f as f32)
2109            .unwrap_or(0.0)
2110    };
2111
2112    Ok([get(0), get(1), get(2), get(3)])
2113}
2114
2115/// Parse a color array from a resolved object.
2116fn parse_color_array(obj: &Object) -> Option<Vec<f32>> {
2117    let arr = obj.as_array()?;
2118    let colors: Vec<f32> = arr
2119        .iter()
2120        .filter_map(|o| o.as_f64().map(|f| f as f32))
2121        .collect();
2122    if colors.is_empty() && !arr.is_empty() {
2123        return None;
2124    }
2125    Some(colors)
2126}
2127
2128/// Parse border information from `/Border` array or `/BS` dictionary.
2129fn parse_border<S: PdfSource>(
2130    dict: &std::collections::HashMap<Name, Object>,
2131    store: &ObjectStore<S>,
2132) -> Option<AnnotationBorder> {
2133    // Try /BS (border style dictionary) first — it takes precedence
2134    if let Some(bs_obj) = dict.get(&Name::bs()) {
2135        if let Ok(resolved) = store.deep_resolve(bs_obj) {
2136            if let Some(bs_dict) = resolved.as_dict() {
2137                let width = bs_dict
2138                    .get(&Name::w())
2139                    .and_then(|o| o.as_f64())
2140                    .map(|f| f as f32)
2141                    .unwrap_or(1.0);
2142                let style = bs_dict
2143                    .get(&Name::s())
2144                    .and_then(|o| o.as_name())
2145                    .map(|n| BorderStyle::from_name(&n.as_str()))
2146                    .unwrap_or(BorderStyle::Solid);
2147                return Some(AnnotationBorder { width, style });
2148            }
2149        }
2150    }
2151
2152    // Fall back to /Border array: [horizontal_radius, vertical_radius, width]
2153    if let Some(border_obj) = dict.get(&Name::border()) {
2154        if let Ok(resolved) = store.deep_resolve(border_obj) {
2155            if let Some(arr) = resolved.as_array() {
2156                // Border array has at least 3 elements
2157                let width = arr
2158                    .get(2)
2159                    .and_then(|o| o.as_f64())
2160                    .map(|f| f as f32)
2161                    .unwrap_or(1.0);
2162                return Some(AnnotationBorder {
2163                    width,
2164                    style: BorderStyle::Solid,
2165                });
2166            }
2167        }
2168    }
2169
2170    None
2171}
2172
2173/// Parse subtype-specific fields from the annotation dictionary.
2174fn parse_subtype_data<S: PdfSource>(
2175    dict: &std::collections::HashMap<Name, Object>,
2176    store: &ObjectStore<S>,
2177    subtype: AnnotationType,
2178) -> AnnotationSubtypeData {
2179    let mut data = AnnotationSubtypeData::default();
2180
2181    match subtype {
2182        AnnotationType::Highlight
2183        | AnnotationType::Underline
2184        | AnnotationType::Squiggly
2185        | AnnotationType::StrikeOut
2186        | AnnotationType::Link => {
2187            data.quad_points = parse_f32_array_field(dict, store, &Name::quad_points());
2188        }
2189        AnnotationType::Line => {
2190            data.line_points = parse_line_points(dict, store);
2191            data.leader_line_length = parse_f32_field(dict, store, &Name::ll());
2192        }
2193        AnnotationType::Polygon | AnnotationType::PolyLine => {
2194            data.vertices = parse_f32_array_field(dict, store, &Name::vertices());
2195        }
2196        AnnotationType::Ink => {
2197            data.ink_list = parse_ink_list(dict, store);
2198        }
2199        AnnotationType::FreeText | AnnotationType::Widget => {
2200            data.default_appearance = parse_string_field(dict, store, &Name::da());
2201        }
2202        AnnotationType::Stamp => {
2203            data.stamp_name = dict
2204                .get(&Name::name_key())
2205                .and_then(|o| store.deep_resolve(o).ok())
2206                .and_then(|r| r.as_name().map(|n| n.as_str().into_owned()));
2207        }
2208        _ => {}
2209    }
2210
2211    data
2212}
2213
2214fn parse_f32_array_field<S: PdfSource>(
2215    dict: &std::collections::HashMap<Name, Object>,
2216    store: &ObjectStore<S>,
2217    key: &Name,
2218) -> Option<Vec<f32>> {
2219    let obj = dict.get(key)?;
2220    let resolved = store.deep_resolve(obj).ok()?;
2221    let arr = resolved.as_array()?;
2222    let values: Vec<f32> = arr
2223        .iter()
2224        .filter_map(|o| o.as_f64().map(|f| f as f32))
2225        .collect();
2226    if values.is_empty() {
2227        None
2228    } else {
2229        Some(values)
2230    }
2231}
2232
2233fn parse_f32_field<S: PdfSource>(
2234    dict: &std::collections::HashMap<Name, Object>,
2235    store: &ObjectStore<S>,
2236    key: &Name,
2237) -> Option<f32> {
2238    let obj = dict.get(key)?;
2239    store.deep_resolve(obj).ok()?.as_f64().map(|f| f as f32)
2240}
2241
2242fn parse_string_field<S: PdfSource>(
2243    dict: &std::collections::HashMap<Name, Object>,
2244    store: &ObjectStore<S>,
2245    key: &Name,
2246) -> Option<String> {
2247    let obj = dict.get(key)?;
2248    let resolved = store.deep_resolve(obj).ok()?;
2249    resolved.as_string().map(|s| s.to_string_lossy())
2250}
2251
2252fn parse_line_points<S: PdfSource>(
2253    dict: &std::collections::HashMap<Name, Object>,
2254    store: &ObjectStore<S>,
2255) -> Option<[f32; 4]> {
2256    let values = parse_f32_array_field(dict, store, &Name::l())?;
2257    if values.len() >= 4 {
2258        Some([values[0], values[1], values[2], values[3]])
2259    } else {
2260        None
2261    }
2262}
2263
2264/// Strip existing color operators (`g`, `rg`, `k`) from a DA string.
2265///
2266/// Used by `set_font_color` to replace the color in an existing `/DA` string
2267/// before appending the new color operator.
2268fn strip_da_color(da: &str) -> String {
2269    // Tokenise the DA string and remove any color-setting operator + its operands.
2270    // PDF operators appear AFTER their operands, so when we encounter a color operator
2271    // we pop the already-accumulated operands from the result buffer.
2272    //
2273    // Color operators and their operand counts:
2274    //   g  (1 operand) — grayscale
2275    //   rg (3 operands) — RGB
2276    //   k  (4 operands) — CMYK
2277    let mut result: Vec<&str> = Vec::new();
2278    for token in da.split_whitespace() {
2279        match token {
2280            "g" => {
2281                if !result.is_empty() {
2282                    result.pop();
2283                }
2284            }
2285            "rg" => {
2286                let len = result.len();
2287                if len >= 3 {
2288                    result.truncate(len - 3);
2289                }
2290            }
2291            "k" => {
2292                let len = result.len();
2293                if len >= 4 {
2294                    result.truncate(len - 4);
2295                }
2296            }
2297            _ => {
2298                result.push(token);
2299            }
2300        }
2301    }
2302    result.join(" ")
2303}
2304
2305/// Parse `/Opt` choice-field options from an annotation (or form-field) dictionary.
2306///
2307/// Each element can be a string (export == display) or a 2-element array
2308/// `[export_value, display_value]`.  Mirrors `form_field::parse_options`.
2309fn parse_choice_options<S: PdfSource>(
2310    dict: &std::collections::HashMap<Name, Object>,
2311    store: &ObjectStore<S>,
2312) -> Vec<ChoiceOption> {
2313    let opt_obj = match dict.get(&Name::opt()) {
2314        Some(o) => o,
2315        None => return Vec::new(),
2316    };
2317    let resolved = match store.deep_resolve(opt_obj).ok() {
2318        Some(o) => o,
2319        None => return Vec::new(),
2320    };
2321    let arr = match resolved.as_array() {
2322        Some(a) => a,
2323        None => return Vec::new(),
2324    };
2325
2326    let mut options = Vec::with_capacity(arr.len());
2327    for item in arr {
2328        let ri = match store.deep_resolve(item).ok() {
2329            Some(o) => o,
2330            None => continue,
2331        };
2332        if let Some(sub) = ri.as_array() {
2333            if sub.len() >= 2 {
2334                let export = store
2335                    .deep_resolve(&sub[0])
2336                    .ok()
2337                    .and_then(|o| o.as_string().map(|s| s.to_string_lossy()))
2338                    .unwrap_or_default();
2339                let display = store
2340                    .deep_resolve(&sub[1])
2341                    .ok()
2342                    .and_then(|o| o.as_string().map(|s| s.to_string_lossy()))
2343                    .unwrap_or_default();
2344                options.push(ChoiceOption {
2345                    export_value: export,
2346                    display_value: display,
2347                });
2348            }
2349        } else if let Some(s) = ri.as_string() {
2350            let val = s.to_string_lossy();
2351            options.push(ChoiceOption {
2352                export_value: val.clone(),
2353                display_value: val,
2354            });
2355        }
2356    }
2357    options
2358}
2359
2360fn parse_ink_list<S: PdfSource>(
2361    dict: &std::collections::HashMap<Name, Object>,
2362    store: &ObjectStore<S>,
2363) -> Option<Vec<Vec<f32>>> {
2364    let obj = dict.get(&Name::ink_list())?;
2365    let resolved = store.deep_resolve(obj).ok()?;
2366    let outer = resolved.as_array()?;
2367    let mut strokes = Vec::new();
2368    for item in outer {
2369        if let Ok(inner) = store.deep_resolve(item) {
2370            if let Some(arr) = inner.as_array() {
2371                let points: Vec<f32> = arr
2372                    .iter()
2373                    .filter_map(|o| o.as_f64().map(|f| f as f32))
2374                    .collect();
2375                if !points.is_empty() {
2376                    strokes.push(points);
2377                }
2378            }
2379        }
2380    }
2381    if strokes.is_empty() {
2382        None
2383    } else {
2384        Some(strokes)
2385    }
2386}
2387
2388#[cfg(test)]
2389mod tests {
2390    use super::*;
2391    use rpdfium_parser::Object;
2392
2393    // Tests for parse_annotations and find_parent_annotation are in annot_list.rs.
2394
2395    #[test]
2396    fn test_annotation_flags_parsing() {
2397        // Hidden (bit 2) + Print (bit 3) = 6
2398        let flags = AnnotationFlags::from_bits(6);
2399        assert!(!flags.invisible());
2400        assert!(flags.hidden());
2401        assert!(flags.print());
2402        assert!(!flags.no_zoom());
2403        assert!(!flags.read_only());
2404    }
2405
2406    #[test]
2407    fn test_annotation_flags_invisible_hidden_print() {
2408        let invisible = AnnotationFlags::from_bits(1);
2409        assert!(invisible.invisible());
2410        assert!(!invisible.hidden());
2411
2412        let hidden = AnnotationFlags::from_bits(2);
2413        assert!(hidden.hidden());
2414        assert!(!hidden.is_visible());
2415
2416        let print = AnnotationFlags::from_bits(4);
2417        assert!(print.print());
2418        assert!(print.is_visible());
2419
2420        let no_view = AnnotationFlags::from_bits(32);
2421        assert!(no_view.no_view());
2422        assert!(!no_view.is_visible());
2423    }
2424
2425    #[test]
2426    fn test_annotation_flags_toggle_no_view_locked_contents() {
2427        let toggle = AnnotationFlags::from_bits(256);
2428        assert!(toggle.toggle_no_view());
2429        assert!(!toggle.locked_contents());
2430
2431        let lc = AnnotationFlags::from_bits(512);
2432        assert!(!lc.toggle_no_view());
2433        assert!(lc.locked_contents());
2434
2435        let both = AnnotationFlags::from_bits(256 | 512);
2436        assert!(both.toggle_no_view());
2437        assert!(both.locked_contents());
2438    }
2439
2440    #[test]
2441    fn test_annotation_flags_locked_read_only() {
2442        let locked = AnnotationFlags::from_bits(128);
2443        assert!(locked.locked());
2444        assert!(!locked.read_only());
2445
2446        let read_only = AnnotationFlags::from_bits(64);
2447        assert!(read_only.read_only());
2448        assert!(!read_only.locked());
2449    }
2450
2451    #[test]
2452    fn test_annotation_type_from_name() {
2453        assert_eq!(AnnotationType::from_name("Text"), AnnotationType::Text);
2454        assert_eq!(AnnotationType::from_name("Link"), AnnotationType::Link);
2455        assert_eq!(
2456            AnnotationType::from_name("FreeText"),
2457            AnnotationType::FreeText
2458        );
2459        assert_eq!(
2460            AnnotationType::from_name("Highlight"),
2461            AnnotationType::Highlight
2462        );
2463        assert_eq!(
2464            AnnotationType::from_name("StrikeOut"),
2465            AnnotationType::StrikeOut
2466        );
2467        assert_eq!(AnnotationType::from_name("Widget"), AnnotationType::Widget);
2468        assert_eq!(AnnotationType::from_name("Movie"), AnnotationType::Movie);
2469        assert_eq!(AnnotationType::from_name("Screen"), AnnotationType::Screen);
2470        assert_eq!(
2471            AnnotationType::from_name("PrinterMark"),
2472            AnnotationType::PrinterMark
2473        );
2474        assert_eq!(
2475            AnnotationType::from_name("TrapNet"),
2476            AnnotationType::TrapNet
2477        );
2478        assert_eq!(
2479            AnnotationType::from_name("Watermark"),
2480            AnnotationType::Watermark
2481        );
2482        assert_eq!(AnnotationType::from_name("3D"), AnnotationType::ThreeD);
2483        assert_eq!(
2484            AnnotationType::from_name("RichMedia"),
2485            AnnotationType::RichMedia
2486        );
2487        assert_eq!(
2488            AnnotationType::from_name("XFAWidget"),
2489            AnnotationType::XFAWidget
2490        );
2491        assert_eq!(AnnotationType::from_name("Redact"), AnnotationType::Redact);
2492        assert_eq!(AnnotationType::from_name("Unknown"), AnnotationType::Other);
2493    }
2494
2495    #[test]
2496    fn test_border_style_from_name() {
2497        assert_eq!(BorderStyle::from_name("S"), BorderStyle::Solid);
2498        assert_eq!(BorderStyle::from_name("D"), BorderStyle::Dashed);
2499        assert_eq!(BorderStyle::from_name("B"), BorderStyle::Beveled);
2500        assert_eq!(BorderStyle::from_name("I"), BorderStyle::Inset);
2501        assert_eq!(BorderStyle::from_name("U"), BorderStyle::Underline);
2502        assert_eq!(BorderStyle::from_name("X"), BorderStyle::Solid); // unknown
2503    }
2504
2505    #[test]
2506    fn test_annotation_border_default() {
2507        let border = AnnotationBorder::default();
2508        assert_eq!(border.width, 1.0);
2509        assert_eq!(border.style, BorderStyle::Solid);
2510    }
2511
2512    #[test]
2513    fn test_color_array_parsing() {
2514        // 0 components (empty)
2515        let empty = parse_color_array(&Object::Array(vec![]));
2516        assert_eq!(empty, Some(vec![]));
2517
2518        // 1 component (gray)
2519        let gray = parse_color_array(&Object::Array(vec![Object::Real(0.5)]));
2520        assert_eq!(gray, Some(vec![0.5]));
2521
2522        // 3 components (RGB)
2523        let rgb = parse_color_array(&Object::Array(vec![
2524            Object::Real(1.0),
2525            Object::Real(0.0),
2526            Object::Real(0.0),
2527        ]));
2528        assert_eq!(rgb, Some(vec![1.0, 0.0, 0.0]));
2529    }
2530
2531    #[test]
2532    fn test_set_rect_updates_in_memory() {
2533        let mut annot = Annotation {
2534            subtype: AnnotationType::Text,
2535            rect: [0.0, 0.0, 50.0, 50.0],
2536            contents: None,
2537            flags: AnnotationFlags::from_bits(0),
2538            name: None,
2539            appearance: None,
2540            color: None,
2541            border: None,
2542            action: None,
2543            destination: None,
2544            subtype_data: AnnotationSubtypeData::default(),
2545            mk: None,
2546            file_spec: None,
2547            parent_ref: None,
2548            object_id: None,
2549            open: None,
2550            ap_n_bytes: None,
2551            ap_r_bytes: None,
2552            ap_d_bytes: None,
2553            irt_ref: None,
2554            field_name: None,
2555            alternate_name: None,
2556            field_value: None,
2557            form_field_flags: None,
2558            additional_actions: None,
2559            form_field_type: None,
2560            options: None,
2561        };
2562        annot.set_rect([10.0, 20.0, 100.0, 50.0]).unwrap();
2563        assert_eq!(annot.rect, [10.0, 20.0, 100.0, 50.0]);
2564    }
2565
2566    #[test]
2567    fn test_set_open_state_updates_in_memory() {
2568        let mut annot = Annotation {
2569            subtype: AnnotationType::Text,
2570            rect: [0.0, 0.0, 50.0, 50.0],
2571            contents: None,
2572            flags: AnnotationFlags::from_bits(0),
2573            name: None,
2574            appearance: None,
2575            color: None,
2576            border: None,
2577            action: None,
2578            destination: None,
2579            subtype_data: AnnotationSubtypeData::default(),
2580            mk: None,
2581            file_spec: None,
2582            parent_ref: None,
2583            object_id: None,
2584            open: None,
2585            ap_n_bytes: None,
2586            ap_r_bytes: None,
2587            ap_d_bytes: None,
2588            irt_ref: None,
2589            field_name: None,
2590            alternate_name: None,
2591            field_value: None,
2592            form_field_flags: None,
2593            additional_actions: None,
2594            form_field_type: None,
2595            options: None,
2596        };
2597        annot.set_open_state(true).unwrap();
2598        assert_eq!(annot.open, Some(true));
2599    }
2600
2601    #[test]
2602    fn test_get_quad_points_returns_none_when_absent() {
2603        let annot = Annotation {
2604            subtype: AnnotationType::Text,
2605            rect: [0.0, 0.0, 50.0, 50.0],
2606            contents: None,
2607            flags: AnnotationFlags::from_bits(0),
2608            name: None,
2609            appearance: None,
2610            color: None,
2611            border: None,
2612            action: None,
2613            destination: None,
2614            subtype_data: AnnotationSubtypeData::default(),
2615            mk: None,
2616            file_spec: None,
2617            parent_ref: None,
2618            object_id: None,
2619            open: None,
2620            ap_n_bytes: None,
2621            ap_r_bytes: None,
2622            ap_d_bytes: None,
2623            irt_ref: None,
2624            field_name: None,
2625            alternate_name: None,
2626            field_value: None,
2627            form_field_flags: None,
2628            additional_actions: None,
2629            form_field_type: None,
2630            options: None,
2631        };
2632        assert!(annot.annot_get_quad_points().is_none());
2633    }
2634
2635    #[test]
2636    fn test_annotation_type_to_name_round_trip() {
2637        // Every variant except Other should round-trip through to_name/from_name.
2638        let variants = [
2639            AnnotationType::Text,
2640            AnnotationType::Link,
2641            AnnotationType::FreeText,
2642            AnnotationType::Line,
2643            AnnotationType::Square,
2644            AnnotationType::Circle,
2645            AnnotationType::Polygon,
2646            AnnotationType::PolyLine,
2647            AnnotationType::Highlight,
2648            AnnotationType::Underline,
2649            AnnotationType::Squiggly,
2650            AnnotationType::StrikeOut,
2651            AnnotationType::Stamp,
2652            AnnotationType::Caret,
2653            AnnotationType::Ink,
2654            AnnotationType::Popup,
2655            AnnotationType::FileAttachment,
2656            AnnotationType::Sound,
2657            AnnotationType::Widget,
2658            AnnotationType::Movie,
2659            AnnotationType::Screen,
2660            AnnotationType::PrinterMark,
2661            AnnotationType::TrapNet,
2662            AnnotationType::Watermark,
2663            AnnotationType::ThreeD,
2664            AnnotationType::RichMedia,
2665            AnnotationType::XFAWidget,
2666            AnnotationType::Redact,
2667        ];
2668        for variant in &variants {
2669            let name = variant.to_name();
2670            let parsed = AnnotationType::from_name(name);
2671            assert_eq!(
2672                *variant, parsed,
2673                "Round-trip failed for {name}: to_name produced {name:?}, from_name produced {parsed:?}"
2674            );
2675        }
2676    }
2677
2678    #[test]
2679    fn test_annotation_type_other_to_name() {
2680        assert_eq!(AnnotationType::Other.to_name(), "Unknown");
2681    }
2682
2683    #[test]
2684    fn test_get_quad_points_returns_values_when_present() {
2685        let quad = vec![10.0_f32, 20.0, 100.0, 20.0, 100.0, 30.0, 10.0, 30.0];
2686        let annot = Annotation {
2687            subtype: AnnotationType::Highlight,
2688            rect: [0.0, 0.0, 200.0, 50.0],
2689            contents: None,
2690            flags: AnnotationFlags::from_bits(0),
2691            name: None,
2692            appearance: None,
2693            color: None,
2694            border: None,
2695            action: None,
2696            destination: None,
2697            subtype_data: AnnotationSubtypeData {
2698                quad_points: Some(quad.clone()),
2699                ..Default::default()
2700            },
2701            mk: None,
2702            file_spec: None,
2703            parent_ref: None,
2704            object_id: None,
2705            open: None,
2706            ap_n_bytes: None,
2707            ap_r_bytes: None,
2708            ap_d_bytes: None,
2709            irt_ref: None,
2710            field_name: None,
2711            alternate_name: None,
2712            field_value: None,
2713            form_field_flags: None,
2714            additional_actions: None,
2715            form_field_type: None,
2716            options: None,
2717        };
2718        let qp = annot.annot_get_quad_points().unwrap();
2719        assert_eq!(qp.len(), 8);
2720        assert_eq!(qp[0], 10.0);
2721        assert_eq!(qp[4], 100.0);
2722    }
2723
2724    // Helper to build a minimal Annotation for testing getter methods.
2725    fn make_annot(subtype: AnnotationType) -> Annotation {
2726        Annotation {
2727            subtype,
2728            rect: [10.0, 20.0, 100.0, 50.0],
2729            contents: Some("Test content".into()),
2730            flags: AnnotationFlags::from_bits(4), // Print
2731            name: Some("annot-001".into()),
2732            appearance: None,
2733            color: None,
2734            border: None,
2735            action: None,
2736            destination: None,
2737            subtype_data: AnnotationSubtypeData::default(),
2738            mk: None,
2739            file_spec: None,
2740            parent_ref: None,
2741            object_id: None,
2742            open: None,
2743            ap_n_bytes: None,
2744            ap_r_bytes: None,
2745            ap_d_bytes: None,
2746            irt_ref: None,
2747            field_name: None,
2748            alternate_name: None,
2749            field_value: None,
2750            form_field_flags: None,
2751            additional_actions: None,
2752            form_field_type: None,
2753            options: None,
2754        }
2755    }
2756
2757    // -----------------------------------------------------------------------
2758    // subtype() / get_subtype()
2759    // -----------------------------------------------------------------------
2760    #[test]
2761    fn test_subtype_getter_returns_correct_variant() {
2762        let a = make_annot(AnnotationType::Highlight);
2763        assert_eq!(a.subtype(), AnnotationType::Highlight);
2764        assert_eq!(a.annot_get_subtype(), AnnotationType::Highlight);
2765    }
2766
2767    // -----------------------------------------------------------------------
2768    // rect() / get_rect()
2769    // -----------------------------------------------------------------------
2770    #[test]
2771    fn test_rect_getter_returns_rect() {
2772        let a = make_annot(AnnotationType::Text);
2773        assert_eq!(a.rect(), [10.0, 20.0, 100.0, 50.0]);
2774        assert_eq!(a.annot_get_rect(), [10.0, 20.0, 100.0, 50.0]);
2775    }
2776
2777    // -----------------------------------------------------------------------
2778    // flags() / get_flags()
2779    // -----------------------------------------------------------------------
2780    #[test]
2781    fn test_flags_getter_returns_flags() {
2782        let a = make_annot(AnnotationType::Text);
2783        assert!(a.flags().print());
2784        assert_eq!(a.annot_get_flags().bits(), 4);
2785    }
2786
2787    // -----------------------------------------------------------------------
2788    // has_attachment_points()
2789    // -----------------------------------------------------------------------
2790    #[test]
2791    fn test_has_attachment_points_true_for_markup_subtypes() {
2792        for subtype in [
2793            AnnotationType::Highlight,
2794            AnnotationType::Underline,
2795            AnnotationType::Squiggly,
2796            AnnotationType::StrikeOut,
2797            AnnotationType::Link,
2798        ] {
2799            let a = make_annot(subtype);
2800            assert!(
2801                a.has_attachment_points(),
2802                "{subtype:?} should support attachment points"
2803            );
2804        }
2805    }
2806
2807    #[test]
2808    fn test_has_attachment_points_false_for_other_subtypes() {
2809        for subtype in [
2810            AnnotationType::Text,
2811            AnnotationType::Ink,
2812            AnnotationType::Line,
2813        ] {
2814            let a = make_annot(subtype);
2815            assert!(
2816                !a.has_attachment_points(),
2817                "{subtype:?} should not support attachment points"
2818            );
2819        }
2820    }
2821
2822    // -----------------------------------------------------------------------
2823    // attachment_point_count() / count_attachment_points()
2824    // attachment_points() / get_attachment_points()
2825    // -----------------------------------------------------------------------
2826    #[test]
2827    fn test_attachment_point_count_no_quad_points() {
2828        let a = make_annot(AnnotationType::Highlight);
2829        assert_eq!(a.attachment_point_count(), 0);
2830        assert_eq!(a.annot_count_attachment_points(), 0);
2831    }
2832
2833    #[test]
2834    fn test_attachment_point_count_with_two_quads() {
2835        let mut a = make_annot(AnnotationType::Highlight);
2836        // Two quadrilaterals = 16 floats
2837        a.subtype_data.quad_points = Some(vec![
2838            1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, // quad 0
2839            9.0, 10.0, 11.0, 12.0, 13.0, 14.0, 15.0, 16.0, // quad 1
2840        ]);
2841        assert_eq!(a.attachment_point_count(), 2);
2842        let q0 = a.attachment_points(0).unwrap();
2843        assert_eq!(q0, [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0]);
2844        let q1 = a.annot_get_attachment_points(1).unwrap();
2845        assert_eq!(q1[0], 9.0);
2846        // Out of range
2847        assert!(a.attachment_points(2).is_none());
2848    }
2849
2850    // -----------------------------------------------------------------------
2851    // vertices() / get_vertices()
2852    // -----------------------------------------------------------------------
2853    #[test]
2854    fn test_vertices_none_when_absent() {
2855        let a = make_annot(AnnotationType::Polygon);
2856        assert!(a.vertices().is_none());
2857        assert!(a.annot_get_vertices().is_none());
2858    }
2859
2860    #[test]
2861    fn test_vertices_returns_flat_slice() {
2862        let mut a = make_annot(AnnotationType::Polygon);
2863        a.subtype_data.vertices = Some(vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0]);
2864        let v = a.vertices().unwrap();
2865        assert_eq!(v.len(), 6);
2866        assert_eq!(v[0], 1.0);
2867        assert_eq!(a.annot_get_vertices().unwrap()[2], 3.0);
2868    }
2869
2870    // -----------------------------------------------------------------------
2871    // ink_list_count() / get_ink_list_count()
2872    // ink_list_path() / get_ink_list_path()
2873    // -----------------------------------------------------------------------
2874    #[test]
2875    fn test_ink_list_count_zero_when_absent() {
2876        let a = make_annot(AnnotationType::Ink);
2877        assert_eq!(a.ink_list_count(), 0);
2878        assert_eq!(a.annot_get_ink_list_count(), 0);
2879        assert!(a.ink_list_path(0).is_none());
2880    }
2881
2882    #[test]
2883    fn test_ink_list_returns_strokes() {
2884        let mut a = make_annot(AnnotationType::Ink);
2885        a.subtype_data.ink_list = Some(vec![
2886            vec![1.0, 2.0, 3.0, 4.0], // stroke 0: 2 points
2887            vec![5.0, 6.0],           // stroke 1: 1 point
2888        ]);
2889        assert_eq!(a.ink_list_count(), 2);
2890        assert_eq!(a.annot_get_ink_list_count(), 2);
2891
2892        let s0 = a.ink_list_path(0).unwrap();
2893        assert_eq!(s0, &[1.0, 2.0, 3.0, 4.0]);
2894
2895        let s1 = a.annot_get_ink_list_path(1).unwrap();
2896        assert_eq!(s1, &[5.0, 6.0]);
2897
2898        assert!(a.ink_list_path(2).is_none());
2899    }
2900
2901    // -----------------------------------------------------------------------
2902    // line_endpoints() / get_line()
2903    // -----------------------------------------------------------------------
2904    #[test]
2905    fn test_line_endpoints_none_when_absent() {
2906        let a = make_annot(AnnotationType::Line);
2907        assert!(a.line_endpoints().is_none());
2908        assert!(a.annot_get_line().is_none());
2909    }
2910
2911    #[test]
2912    fn test_line_endpoints_returns_two_points() {
2913        let mut a = make_annot(AnnotationType::Line);
2914        a.subtype_data.line_points = Some([10.0, 20.0, 300.0, 400.0]);
2915        let (p1, p2) = a.line_endpoints().unwrap();
2916        assert_eq!(p1, [10.0, 20.0]);
2917        assert_eq!(p2, [300.0, 400.0]);
2918
2919        let (p1b, p2b) = a.annot_get_line().unwrap();
2920        assert_eq!(p1b, [10.0, 20.0]);
2921        assert_eq!(p2b, [300.0, 400.0]);
2922    }
2923
2924    // -----------------------------------------------------------------------
2925    // border_style() / get_border()
2926    // -----------------------------------------------------------------------
2927    #[test]
2928    fn test_border_style_none_when_absent() {
2929        let a = make_annot(AnnotationType::Text);
2930        assert!(a.border_style().is_none());
2931        assert!(a.annot_get_border().is_none());
2932    }
2933
2934    #[test]
2935    fn test_border_style_returns_width_with_zero_radii() {
2936        let mut a = make_annot(AnnotationType::Text);
2937        a.border = Some(AnnotationBorder {
2938            width: 2.5,
2939            style: BorderStyle::Dashed,
2940        });
2941        let (h, v, w) = a.border_style().unwrap();
2942        assert_eq!(h, 0.0);
2943        assert_eq!(v, 0.0);
2944        assert_eq!(w, 2.5);
2945        assert_eq!(a.annot_get_border().unwrap().2, 2.5);
2946    }
2947
2948    // -----------------------------------------------------------------------
2949    // color_rgba() / get_color()
2950    // -----------------------------------------------------------------------
2951    #[test]
2952    fn test_color_rgba_none_when_absent() {
2953        let a = make_annot(AnnotationType::Text);
2954        assert!(a.color_rgba().is_none());
2955        assert!(a.annot_get_color().is_none());
2956    }
2957
2958    #[test]
2959    fn test_color_rgba_gray_one_component() {
2960        let mut a = make_annot(AnnotationType::Text);
2961        a.color = Some(vec![0.5]);
2962        let (r, g, b, al) = a.color_rgba().unwrap();
2963        assert_eq!(r, 128);
2964        assert_eq!(g, 128);
2965        assert_eq!(b, 128);
2966        assert_eq!(al, 255);
2967    }
2968
2969    #[test]
2970    fn test_color_rgba_rgb_three_components() {
2971        let mut a = make_annot(AnnotationType::Highlight);
2972        a.color = Some(vec![1.0, 0.0, 0.0]); // red
2973        let (r, g, b, al) = a.color_rgba().unwrap();
2974        assert_eq!(r, 255);
2975        assert_eq!(g, 0);
2976        assert_eq!(b, 0);
2977        assert_eq!(al, 255);
2978    }
2979
2980    #[test]
2981    fn test_color_rgba_cmyk_four_components() {
2982        let mut a = make_annot(AnnotationType::Text);
2983        // CMYK (0, 0, 0, 1) = black
2984        a.color = Some(vec![0.0, 0.0, 0.0, 1.0]);
2985        let (r, g, b, al) = a.color_rgba().unwrap();
2986        assert_eq!(r, 0);
2987        assert_eq!(g, 0);
2988        assert_eq!(b, 0);
2989        assert_eq!(al, 255);
2990    }
2991
2992    #[test]
2993    fn test_color_rgba_none_for_zero_components() {
2994        let mut a = make_annot(AnnotationType::Text);
2995        a.color = Some(vec![]); // empty /C means "no color"
2996        // 0-length → None (empty vec returns Some(vec![]), but get_color returns None for len 0)
2997        // Actually the function matches on len 0 → falls through to _ => None
2998        assert!(a.color_rgba().is_none());
2999    }
3000
3001    // -----------------------------------------------------------------------
3002    // string_value() / get_string_value() / has_key()
3003    // -----------------------------------------------------------------------
3004    #[test]
3005    fn test_string_value_contents_key() {
3006        let a = make_annot(AnnotationType::Text);
3007        assert_eq!(a.string_value("Contents"), Some("Test content"));
3008        assert_eq!(a.annot_get_string_value("Contents"), Some("Test content"));
3009        assert!(a.has_key("Contents"));
3010    }
3011
3012    #[test]
3013    fn test_string_value_nm_key() {
3014        let a = make_annot(AnnotationType::Text);
3015        assert_eq!(a.string_value("NM"), Some("annot-001"));
3016        assert_eq!(a.annot_get_string_value("NM"), Some("annot-001"));
3017        assert!(a.has_key("NM"));
3018    }
3019
3020    #[test]
3021    fn test_string_value_unknown_key_returns_none() {
3022        let a = make_annot(AnnotationType::Text);
3023        assert!(a.string_value("UnknownKey").is_none());
3024        assert!(!a.has_key("UnknownKey"));
3025    }
3026
3027    // -----------------------------------------------------------------------
3028    // number_value() / get_number_value()
3029    // -----------------------------------------------------------------------
3030    #[test]
3031    fn test_number_value_f_key_returns_flags() {
3032        let a = make_annot(AnnotationType::Text); // flags = 4 (Print)
3033        assert_eq!(a.number_value("F"), Some(4.0));
3034        assert_eq!(a.annot_get_number_value("F"), Some(4.0));
3035    }
3036
3037    #[test]
3038    fn test_number_value_border_width_key() {
3039        let mut a = make_annot(AnnotationType::Text);
3040        a.border = Some(AnnotationBorder {
3041            width: 1.5,
3042            style: BorderStyle::Solid,
3043        });
3044        assert_eq!(a.number_value("Border-Width"), Some(1.5));
3045        assert_eq!(a.annot_get_number_value("Border-Width"), Some(1.5));
3046    }
3047
3048    #[test]
3049    fn test_number_value_unknown_key_returns_none() {
3050        let a = make_annot(AnnotationType::Text);
3051        assert!(a.number_value("UnknownKey").is_none());
3052    }
3053
3054    // -----------------------------------------------------------------------
3055    // Gap 2: value_type() / get_value_type() — FPDFAnnot_GetValueType
3056    // -----------------------------------------------------------------------
3057
3058    #[test]
3059    fn test_value_type_string_keys_return_string() {
3060        let a = make_annot(AnnotationType::Text); // has contents + name
3061        assert_eq!(a.value_type("Contents"), PdfValueType::String);
3062        assert_eq!(a.value_type("NM"), PdfValueType::String);
3063        assert_eq!(a.annot_get_value_type("Contents"), PdfValueType::String);
3064    }
3065
3066    #[test]
3067    fn test_value_type_fixed_type_keys() {
3068        let a = make_annot(AnnotationType::Text);
3069        // "F" is always Number (flags field always exists)
3070        assert_eq!(a.value_type("F"), PdfValueType::Number);
3071        // "Subtype" is always Name
3072        assert_eq!(a.value_type("Subtype"), PdfValueType::Name);
3073        // "Rect" is always Array
3074        assert_eq!(a.value_type("Rect"), PdfValueType::Array);
3075    }
3076
3077    #[test]
3078    fn test_value_type_absent_optional_fields_return_unknown() {
3079        let a = make_annot(AnnotationType::Text); // no color, no appearance, no irt
3080        assert_eq!(a.value_type("C"), PdfValueType::Unknown);
3081        assert_eq!(a.value_type("AP"), PdfValueType::Unknown);
3082        assert_eq!(a.value_type("IRT"), PdfValueType::Unknown);
3083        assert_eq!(a.value_type("NonExistentKey"), PdfValueType::Unknown);
3084    }
3085
3086    #[test]
3087    fn test_value_type_present_optional_fields() {
3088        let mut a = make_annot(AnnotationType::Text);
3089        a.color = Some(vec![1.0, 0.0, 0.0]);
3090        assert_eq!(a.value_type("C"), PdfValueType::Array);
3091    }
3092
3093    #[test]
3094    fn test_value_type_irt_reference_when_set() {
3095        use rpdfium_parser::ObjectId;
3096        let mut a = make_annot(AnnotationType::Text);
3097        a.irt_ref = Some(ObjectId::new(7, 0));
3098        assert_eq!(a.value_type("IRT"), PdfValueType::Reference);
3099        assert_eq!(a.annot_get_value_type("IRT"), PdfValueType::Reference);
3100    }
3101
3102    // -----------------------------------------------------------------------
3103    // Gap 3: appearance_stream_bytes() / get_ap() — FPDFAnnot_GetAP
3104    // -----------------------------------------------------------------------
3105
3106    #[test]
3107    fn test_appearance_stream_bytes_none_when_absent() {
3108        let a = make_annot(AnnotationType::Text);
3109        assert!(a.appearance_stream_bytes(AppearanceMode::Normal).is_none());
3110        assert!(a.annot_get_ap(AppearanceMode::Rollover).is_none());
3111        assert!(a.annot_get_ap(AppearanceMode::Down).is_none());
3112    }
3113
3114    #[test]
3115    fn test_appearance_stream_bytes_returns_stored_bytes() {
3116        let mut a = make_annot(AnnotationType::Text);
3117        a.ap_n_bytes = Some(b"q BT /F1 12 Tf ET Q".to_vec());
3118        let bytes = a.appearance_stream_bytes(AppearanceMode::Normal).unwrap();
3119        assert_eq!(bytes, b"q BT /F1 12 Tf ET Q");
3120    }
3121
3122    #[test]
3123    fn test_appearance_stream_bytes_mode_isolation() {
3124        let mut a = make_annot(AnnotationType::Text);
3125        a.ap_n_bytes = Some(b"normal".to_vec());
3126        a.ap_r_bytes = Some(b"rollover".to_vec());
3127        // Down is absent
3128        assert_eq!(
3129            a.appearance_stream_bytes(AppearanceMode::Normal).unwrap(),
3130            b"normal"
3131        );
3132        assert_eq!(
3133            a.annot_get_ap(AppearanceMode::Rollover).unwrap(),
3134            b"rollover"
3135        );
3136        assert!(a.appearance_stream_bytes(AppearanceMode::Down).is_none());
3137    }
3138
3139    #[test]
3140    fn test_set_appearance_stream_bytes_returns_not_supported() {
3141        let mut a = make_annot(AnnotationType::Text);
3142        let result = a.set_appearance_stream_bytes(AppearanceMode::Normal, b"data");
3143        assert!(matches!(result, Err(DocError::NotSupported(_))));
3144    }
3145
3146    // -----------------------------------------------------------------------
3147    // Gap 4: linked_annot_ref() / get_linked_annot() — FPDFAnnot_GetLinkedAnnot
3148    // -----------------------------------------------------------------------
3149
3150    #[test]
3151    fn test_linked_annot_ref_none_when_absent() {
3152        let a = make_annot(AnnotationType::Text);
3153        assert!(a.linked_annot_ref().is_none());
3154        assert!(a.annot_get_linked_annot().is_none());
3155    }
3156
3157    #[test]
3158    fn test_linked_annot_ref_returns_object_id() {
3159        use rpdfium_parser::ObjectId;
3160        let mut a = make_annot(AnnotationType::Text);
3161        a.irt_ref = Some(ObjectId::new(42, 0));
3162        let oid = a.linked_annot_ref().unwrap();
3163        assert_eq!(oid.number, 42);
3164        assert_eq!(oid.generation, 0);
3165        // ADR-019 alias
3166        assert_eq!(a.annot_get_linked_annot().unwrap().number, 42);
3167    }
3168
3169    #[test]
3170    fn test_linked_annot_ref_alias_equals_primary() {
3171        use rpdfium_parser::ObjectId;
3172        let mut a = make_annot(AnnotationType::Text);
3173        a.irt_ref = Some(ObjectId::new(10, 1));
3174        assert_eq!(a.linked_annot_ref(), a.annot_get_linked_annot());
3175    }
3176
3177    // -----------------------------------------------------------------------
3178    // Task 2: field_name / alternate_field_name / field_value_str
3179    // FPDFAnnot_GetFormFieldName / FPDFAnnot_GetFormFieldAlternateName /
3180    // FPDFAnnot_GetFormFieldValue
3181    // -----------------------------------------------------------------------
3182
3183    #[test]
3184    fn test_field_name_none_when_absent() {
3185        let a = make_annot(AnnotationType::Widget);
3186        assert!(a.field_name().is_none());
3187        assert!(a.annot_get_form_field_name().is_none());
3188    }
3189
3190    #[test]
3191    fn test_field_name_returns_t_value() {
3192        let mut a = make_annot(AnnotationType::Widget);
3193        a.field_name = Some("FirstName".into());
3194        assert_eq!(a.field_name(), Some("FirstName"));
3195        assert_eq!(a.annot_get_form_field_name(), Some("FirstName"));
3196        // string_value("T") should also work
3197        assert_eq!(a.string_value("T"), Some("FirstName"));
3198    }
3199
3200    #[test]
3201    fn test_alternate_field_name_and_field_value() {
3202        let mut a = make_annot(AnnotationType::Widget);
3203        a.alternate_name = Some("Your first name".into());
3204        a.field_value = Some("Alice".into());
3205        assert_eq!(a.alternate_field_name(), Some("Your first name"));
3206        assert_eq!(
3207            a.annot_get_form_field_alternate_name(),
3208            Some("Your first name")
3209        );
3210        assert_eq!(a.field_value_str(), Some("Alice"));
3211        assert_eq!(a.annot_get_form_field_value(), Some("Alice"));
3212        // string_value() aliases
3213        assert_eq!(a.string_value("TU"), Some("Your first name"));
3214        assert_eq!(a.string_value("V"), Some("Alice"));
3215    }
3216
3217    #[test]
3218    fn test_field_value_none_for_non_widget() {
3219        // Text annotations typically have no /V; make sure None is correct.
3220        let a = make_annot(AnnotationType::Text);
3221        assert!(a.field_value_str().is_none());
3222        assert!(a.annot_get_form_field_value().is_none());
3223    }
3224
3225    // -----------------------------------------------------------------------
3226    // Task 3: AnnotationType::supports_ap_objects()
3227    // FPDFAnnot_IsObjectSupportedSubtype
3228    // -----------------------------------------------------------------------
3229
3230    #[test]
3231    fn test_supports_ap_objects_true_for_supported_subtypes() {
3232        let supported = [
3233            AnnotationType::Stamp,
3234            AnnotationType::FreeText,
3235            AnnotationType::Ink,
3236            AnnotationType::Square,
3237            AnnotationType::Circle,
3238            AnnotationType::Polygon,
3239            AnnotationType::PolyLine,
3240            AnnotationType::Line,
3241            AnnotationType::Highlight,
3242            AnnotationType::Underline,
3243            AnnotationType::Squiggly,
3244            AnnotationType::StrikeOut,
3245        ];
3246        for subtype in &supported {
3247            assert!(
3248                subtype.supports_ap_objects(),
3249                "{subtype:?} should support AP objects"
3250            );
3251            assert!(
3252                subtype.annot_is_object_supported_subtype(),
3253                "{subtype:?} alias should also return true"
3254            );
3255        }
3256    }
3257
3258    #[test]
3259    fn test_supports_ap_objects_false_for_text_annotation() {
3260        let a = AnnotationType::Text;
3261        assert!(!a.supports_ap_objects());
3262        assert!(!a.annot_is_object_supported_subtype());
3263    }
3264
3265    #[test]
3266    fn test_supports_ap_objects_false_for_widget() {
3267        let w = AnnotationType::Widget;
3268        assert!(!w.supports_ap_objects());
3269        assert!(!w.annot_is_object_supported_subtype());
3270    }
3271
3272    // -----------------------------------------------------------------------
3273    // Gap: form_field_flags() / get_form_field_flags() — FPDFAnnot_GetFormFieldFlags
3274    // -----------------------------------------------------------------------
3275
3276    #[test]
3277    fn test_form_field_flags_none_when_absent() {
3278        let a = make_annot(AnnotationType::Widget);
3279        assert!(a.form_field_flags().is_none());
3280        assert!(a.annot_get_form_field_flags().is_none());
3281    }
3282
3283    #[test]
3284    fn test_form_field_flags_returns_value_when_present() {
3285        use crate::form_field::FormFieldFlags;
3286        let mut a = make_annot(AnnotationType::Widget);
3287        // Bit 1 (0x1) = ReadOnly
3288        a.form_field_flags = Some(FormFieldFlags::from_bits(1));
3289        let flags = a.form_field_flags().unwrap();
3290        assert!(flags.is_read_only());
3291        // ADR-019 alias
3292        let flags2 = a.annot_get_form_field_flags().unwrap();
3293        assert!(flags2.is_read_only());
3294    }
3295
3296    #[test]
3297    fn test_form_field_flags_alias_equals_primary() {
3298        use crate::form_field::FormFieldFlags;
3299        let mut a = make_annot(AnnotationType::Widget);
3300        a.form_field_flags = Some(FormFieldFlags::from_bits(2)); // Required
3301        assert_eq!(a.form_field_flags(), a.annot_get_form_field_flags());
3302    }
3303
3304    // -----------------------------------------------------------------------
3305    // Gap: link_ref() / get_link() — FPDFAnnot_GetLink
3306    // -----------------------------------------------------------------------
3307
3308    #[test]
3309    fn test_link_ref_none_when_no_action_or_dest() {
3310        let a = make_annot(AnnotationType::Link);
3311        let (action, dest) = a.link_ref();
3312        assert!(action.is_none());
3313        assert!(dest.is_none());
3314        // ADR-019 alias
3315        let (action2, dest2) = a.annot_get_link();
3316        assert!(action2.is_none());
3317        assert!(dest2.is_none());
3318    }
3319
3320    #[test]
3321    fn test_link_ref_returns_destination_when_present() {
3322        use crate::destination::{Destination, PageFit};
3323        let mut a = make_annot(AnnotationType::Link);
3324        a.destination = Some(Destination::Page {
3325            page_index: 3,
3326            page_ref: None,
3327            fit: PageFit::Fit,
3328        });
3329        let (action, dest) = a.link_ref();
3330        assert!(action.is_none());
3331        let dest = dest.unwrap();
3332        assert_eq!(dest.dest_page_index(), Some(3));
3333    }
3334
3335    // -----------------------------------------------------------------------
3336    // Gap: file_attachment() / get_file_attachment() — FPDFAnnot_GetFileAttachment
3337    // -----------------------------------------------------------------------
3338
3339    #[test]
3340    fn test_file_attachment_none_when_absent() {
3341        let a = make_annot(AnnotationType::FileAttachment);
3342        assert!(a.file_attachment().is_none());
3343        assert!(a.annot_get_file_attachment().is_none());
3344    }
3345
3346    #[test]
3347    fn test_file_attachment_returns_file_spec_when_present() {
3348        use crate::file_spec::FileSpec;
3349        let mut a = make_annot(AnnotationType::FileAttachment);
3350        a.file_spec = Some(FileSpec {
3351            file_system: None,
3352            filename: Some("attachment.pdf".to_string()),
3353            unicode_filename: None,
3354            dos_filename: None,
3355            unix_filename: None,
3356            embedded_file: None,
3357            description: None,
3358            data: None,
3359        });
3360        let fs = a.file_attachment().unwrap();
3361        assert_eq!(fs.filename, Some("attachment.pdf".to_string()));
3362        // ADR-019 alias
3363        let fs2 = a.annot_get_file_attachment().unwrap();
3364        assert_eq!(fs2.filename, Some("attachment.pdf".to_string()));
3365    }
3366
3367    // -----------------------------------------------------------------------
3368    // Batch 18: is_supported_for_creation() — FPDFAnnot_IsSupportedSubtype
3369    // -----------------------------------------------------------------------
3370
3371    #[test]
3372    fn test_is_supported_for_creation_true_for_highlight() {
3373        assert!(AnnotationType::Highlight.is_supported_for_creation());
3374    }
3375
3376    #[test]
3377    fn test_is_supported_for_creation_false_for_widget() {
3378        assert!(!AnnotationType::Widget.is_supported_for_creation());
3379    }
3380
3381    // -----------------------------------------------------------------------
3382    // Upstream: CPDF_Annot quad-point utility tests
3383    // -----------------------------------------------------------------------
3384
3385    /// Helper: given a flat array of f32 values representing QuadPoints,
3386    /// extract the rect for one quadrilateral at `quad_index`.
3387    ///
3388    /// Each quad is 8 floats: [x1,y1, x2,y2, x3,y3, x4,y4].
3389    /// The rect is (left=x3, bottom=y3, right=x2, top=y2) — matching
3390    /// upstream CPDF_Annot::RectFromQuadPointsArray.
3391    fn rect_from_quad_points_array(points: &[f32], quad_index: usize) -> [f32; 4] {
3392        let base = quad_index * 8;
3393        if base + 7 >= points.len() {
3394            return [0.0, 0.0, 0.0, 0.0];
3395        }
3396        // Upstream layout: x1,y1 x2,y2 x3,y3 x4,y4
3397        // Rect: left=x3, bottom=y3, right=x2, top=y2
3398        let x2 = points[base + 2];
3399        let y2 = points[base + 3];
3400        let x3 = points[base + 4];
3401        let y3 = points[base + 5];
3402        [x3, y3, x2, y2] // [left, bottom, right, top]
3403    }
3404
3405    /// Helper: count how many complete quadrilaterals (groups of 8 floats)
3406    /// are in the given array.
3407    fn quad_point_count(points: &[f32]) -> usize {
3408        points.len() / 8
3409    }
3410
3411    /// Helper: compute the bounding rect of all quad points in a flat array.
3412    fn bounding_rect_from_quads(points: &[f32]) -> [f32; 4] {
3413        let count = quad_point_count(points);
3414        if count == 0 {
3415            return [0.0, 0.0, 0.0, 0.0];
3416        }
3417        let mut result = rect_from_quad_points_array(points, 0);
3418        for i in 1..count {
3419            let r = rect_from_quad_points_array(points, i);
3420            result[0] = result[0].min(r[0]); // left
3421            result[1] = result[1].min(r[1]); // bottom
3422            result[2] = result[2].max(r[2]); // right
3423            result[3] = result[3].max(r[3]); // top
3424        }
3425        result
3426    }
3427
3428    /// Upstream: TEST(CPDFAnnotTest, RectFromQuadPointsArray)
3429    #[test]
3430    fn test_cpdf_annot_rect_from_quad_points_array() {
3431        let points: Vec<f32> = vec![
3432            0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, // quad 0
3433            8.0, 7.0, 6.0, 5.0, 4.0, 3.0, 2.0, 1.0, // quad 1
3434        ];
3435
3436        let rect0 = rect_from_quad_points_array(&points, 0);
3437        assert_eq!(rect0, [4.0, 5.0, 2.0, 3.0]);
3438
3439        let rect1 = rect_from_quad_points_array(&points, 1);
3440        assert_eq!(rect1, [4.0, 3.0, 6.0, 5.0]);
3441    }
3442
3443    /// Upstream: TEST(CPDFAnnotTest, BoundingRectFromQuadPoints)
3444    #[test]
3445    fn test_cpdf_annot_bounding_rect_from_quad_points() {
3446        // Empty array
3447        let empty: Vec<f32> = vec![];
3448        assert_eq!(bounding_rect_from_quads(&empty), [0.0, 0.0, 0.0, 0.0]);
3449
3450        // Too few elements (not a complete quad)
3451        let few = vec![0.0, 1.0, 2.0];
3452        assert_eq!(bounding_rect_from_quads(&few), [0.0, 0.0, 0.0, 0.0]);
3453
3454        // One quad
3455        let one_quad = vec![0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0];
3456        assert_eq!(bounding_rect_from_quads(&one_quad), [4.0, 5.0, 2.0, 3.0]);
3457
3458        // Three quads — bounding rect should be the union
3459        let three_quads = vec![
3460            0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, // quad 0
3461            8.0, 7.0, 6.0, 5.0, 4.0, 3.0, 2.0, 1.0, // quad 1
3462            9.0, 2.0, 5.0, 7.0, 3.0, 6.0, 4.0, 1.0, // quad 2
3463        ];
3464        let rect = bounding_rect_from_quads(&three_quads);
3465        assert_eq!(rect[0], 3.0); // min left
3466        assert_eq!(rect[1], 3.0); // min bottom
3467        assert_eq!(rect[2], 6.0); // max right
3468        assert_eq!(rect[3], 7.0); // max top
3469    }
3470
3471    /// Upstream: TEST(CPDFAnnotTest, RectFromQuadPoints)
3472    ///
3473    /// Tests rect extraction from quad points at various indices,
3474    /// including out-of-range indices returning zero rect.
3475    #[test]
3476    fn test_cpdf_annot_rect_from_quad_points() {
3477        // Empty
3478        let empty: Vec<f32> = vec![];
3479        assert_eq!(rect_from_quad_points_array(&empty, 0), [0.0, 0.0, 0.0, 0.0]);
3480        assert_eq!(rect_from_quad_points_array(&empty, 5), [0.0, 0.0, 0.0, 0.0]);
3481
3482        // One quad
3483        let one = vec![0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0];
3484        assert_eq!(rect_from_quad_points_array(&one, 0), [4.0, 5.0, 2.0, 3.0]);
3485        assert_eq!(rect_from_quad_points_array(&one, 5), [0.0, 0.0, 0.0, 0.0]);
3486
3487        // Three quads
3488        let three = vec![
3489            0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 7.0, 6.0, 5.0, 4.0, 3.0, 2.0, 1.0, 9.0,
3490            2.0, 5.0, 7.0, 3.0, 6.0, 4.0, 1.0,
3491        ];
3492        assert_eq!(rect_from_quad_points_array(&three, 0), [4.0, 5.0, 2.0, 3.0]);
3493        assert_eq!(rect_from_quad_points_array(&three, 1), [4.0, 3.0, 6.0, 5.0]);
3494        assert_eq!(rect_from_quad_points_array(&three, 2), [3.0, 6.0, 5.0, 7.0]);
3495    }
3496
3497    /// Upstream: TEST(CPDFAnnotTest, QuadPointCount)
3498    #[test]
3499    fn test_cpdf_annot_quad_point_count() {
3500        let empty: Vec<f32> = vec![];
3501        assert_eq!(quad_point_count(&empty), 0);
3502
3503        // 0..7 elements => 0 quads
3504        for n in 0..8 {
3505            let arr: Vec<f32> = vec![0.0; n];
3506            assert_eq!(quad_point_count(&arr), 0);
3507        }
3508        // 8..15 elements => 1 quad
3509        for n in 8..16 {
3510            let arr: Vec<f32> = vec![0.0; n];
3511            assert_eq!(quad_point_count(&arr), 1);
3512        }
3513        // 65 elements => 8 quads (65 / 8 = 8)
3514        let arr: Vec<f32> = vec![0.0; 65];
3515        assert_eq!(quad_point_count(&arr), 8);
3516    }
3517
3518    // -----------------------------------------------------------------------
3519    // font_size() / annot_get_font_size()
3520    // -----------------------------------------------------------------------
3521
3522    #[test]
3523    fn test_font_size_returns_size_from_da() {
3524        let mut a = make_annot(AnnotationType::FreeText);
3525        a.subtype_data.default_appearance = Some("/Helv 12 Tf 0 g".into());
3526        assert_eq!(a.font_size(), Some(12.0));
3527        assert_eq!(a.annot_get_font_size(), Some(12.0));
3528    }
3529
3530    #[test]
3531    fn test_font_size_returns_none_when_no_da() {
3532        let a = make_annot(AnnotationType::FreeText);
3533        assert!(a.font_size().is_none());
3534    }
3535
3536    #[test]
3537    fn test_font_size_returns_none_for_zero_size() {
3538        let mut a = make_annot(AnnotationType::FreeText);
3539        a.subtype_data.default_appearance = Some("/Helv 0 Tf".into());
3540        assert!(a.font_size().is_none());
3541    }
3542
3543    // -----------------------------------------------------------------------
3544    // font_color() / annot_get_font_color() / set_font_color() / annot_set_font_color()
3545    // -----------------------------------------------------------------------
3546
3547    #[test]
3548    fn test_font_color_rgb_from_da() {
3549        let mut a = make_annot(AnnotationType::FreeText);
3550        a.subtype_data.default_appearance = Some("/Helv 12 Tf 1 0 0 rg".into());
3551        assert_eq!(a.font_color(), Some((255, 0, 0)));
3552        assert_eq!(a.annot_get_font_color(), Some((255, 0, 0)));
3553    }
3554
3555    #[test]
3556    fn test_font_color_grayscale_from_da() {
3557        let mut a = make_annot(AnnotationType::FreeText);
3558        a.subtype_data.default_appearance = Some("0.5 g /Helv 12 Tf".into());
3559        let (r, g, b) = a.font_color().unwrap();
3560        assert_eq!(r, g);
3561        assert_eq!(g, b);
3562        assert!(r >= 127 && r <= 128);
3563    }
3564
3565    #[test]
3566    fn test_font_color_returns_none_when_no_da() {
3567        let a = make_annot(AnnotationType::FreeText);
3568        assert!(a.font_color().is_none());
3569    }
3570
3571    #[test]
3572    fn test_set_font_color_updates_da() {
3573        let mut a = make_annot(AnnotationType::FreeText);
3574        a.subtype_data.default_appearance = Some("/Helv 12 Tf 0 g".into());
3575        a.set_font_color(255, 0, 0).unwrap();
3576        let da = a.subtype_data.default_appearance.as_deref().unwrap();
3577        assert!(da.contains("rg"), "DA should contain rg operator: {da}");
3578        assert_eq!(a.font_color(), Some((255, 0, 0)));
3579    }
3580
3581    #[test]
3582    fn test_set_font_color_on_widget() {
3583        let mut a = make_annot(AnnotationType::Widget);
3584        a.set_font_color(0, 128, 0).unwrap();
3585        let (r, g, b) = a.font_color().unwrap();
3586        assert_eq!(r, 0);
3587        assert!(g >= 127 && g <= 128);
3588        assert_eq!(b, 0);
3589    }
3590
3591    #[test]
3592    fn test_set_font_color_errors_on_non_widget() {
3593        let mut a = make_annot(AnnotationType::Text);
3594        assert!(a.set_font_color(0, 0, 0).is_err());
3595    }
3596
3597    // -----------------------------------------------------------------------
3598    // form_additional_action_javascript() / annot_get_form_additional_action_javascript()
3599    // -----------------------------------------------------------------------
3600
3601    // -----------------------------------------------------------------------
3602    // strip_da_color() — internal helper
3603    // -----------------------------------------------------------------------
3604
3605    #[test]
3606    fn test_strip_da_color_removes_grayscale() {
3607        assert_eq!(strip_da_color("/Helv 12 Tf 0 g"), "/Helv 12 Tf");
3608    }
3609
3610    #[test]
3611    fn test_strip_da_color_removes_rgb() {
3612        assert_eq!(strip_da_color("/Helv 12 Tf 1 0 0 rg"), "/Helv 12 Tf");
3613    }
3614
3615    #[test]
3616    fn test_strip_da_color_removes_cmyk() {
3617        assert_eq!(strip_da_color("/Helv 12 Tf 0 0 0 1 k"), "/Helv 12 Tf");
3618    }
3619
3620    #[test]
3621    fn test_strip_da_color_color_before_font() {
3622        assert_eq!(strip_da_color("0 g /Helv 12 Tf"), "/Helv 12 Tf");
3623    }
3624
3625    #[test]
3626    fn test_strip_da_color_no_color_unchanged() {
3627        assert_eq!(strip_da_color("/Helv 12 Tf"), "/Helv 12 Tf");
3628    }
3629
3630    #[test]
3631    fn test_strip_da_color_empty() {
3632        assert_eq!(strip_da_color(""), "");
3633    }
3634
3635    #[test]
3636    fn test_form_additional_action_javascript_none_when_no_aa() {
3637        let a = make_annot(AnnotationType::Widget);
3638        use crate::aaction::AActionType;
3639        assert_eq!(
3640            a.form_additional_action_javascript(AActionType::KeyStroke),
3641            ""
3642        );
3643    }
3644
3645    #[test]
3646    fn test_form_additional_action_javascript_returns_code() {
3647        use crate::aaction::{AActionType, AdditionalActions};
3648        use crate::action::Action;
3649        use std::collections::HashMap;
3650
3651        let mut entries = HashMap::new();
3652        entries.insert(
3653            "K".to_string(),
3654            Action::JavaScript {
3655                code: "alert(1)".into(),
3656            },
3657        );
3658        let aa = AdditionalActions { entries };
3659        let mut a = make_annot(AnnotationType::Widget);
3660        a.additional_actions = Some(aa);
3661
3662        assert_eq!(
3663            a.form_additional_action_javascript(AActionType::KeyStroke),
3664            "alert(1)"
3665        );
3666        assert_eq!(
3667            a.annot_get_form_additional_action_javascript(AActionType::KeyStroke),
3668            "alert(1)"
3669        );
3670    }
3671
3672    // -----------------------------------------------------------------------
3673    // G6: form_field_type / form_control stubs
3674    // -----------------------------------------------------------------------
3675
3676    #[test]
3677    fn test_form_field_type_none_for_non_widget() {
3678        let a = make_annot(AnnotationType::Text);
3679        assert!(a.form_field_type().is_none());
3680        assert!(a.annot_get_form_field_type().is_none());
3681    }
3682
3683    #[test]
3684    fn test_form_field_type_returns_set_value() {
3685        let mut a = make_annot(AnnotationType::Widget);
3686        a.form_field_type = Some(FormFieldType::Text);
3687        assert_eq!(a.form_field_type(), Some(FormFieldType::Text));
3688        a.form_field_type = Some(FormFieldType::Choice);
3689        assert_eq!(a.annot_get_form_field_type(), Some(FormFieldType::Choice));
3690    }
3691
3692    #[test]
3693    fn test_form_control_count_returns_not_supported() {
3694        let a = make_annot(AnnotationType::Widget);
3695        assert!(a.form_control_count().is_err());
3696        assert!(a.annot_get_form_control_count().is_err());
3697    }
3698
3699    #[test]
3700    fn test_form_control_index_returns_not_supported() {
3701        let a = make_annot(AnnotationType::Widget);
3702        assert!(a.form_control_index().is_err());
3703        assert!(a.annot_get_form_control_index().is_err());
3704    }
3705
3706    #[test]
3707    fn test_form_field_export_value_returns_not_supported() {
3708        let a = make_annot(AnnotationType::Widget);
3709        assert!(a.form_field_export_value().is_err());
3710        assert!(a.annot_get_form_field_export_value().is_err());
3711    }
3712
3713    // -----------------------------------------------------------------------
3714    // G7: is_checked / option_count / option_label / is_option_selected
3715    // -----------------------------------------------------------------------
3716
3717    #[test]
3718    fn test_is_checked_off_is_unchecked() {
3719        let mut a = make_annot(AnnotationType::Widget);
3720        a.field_value = Some("Off".into());
3721        assert!(!a.is_checked());
3722        assert!(!a.annot_is_checked());
3723    }
3724
3725    #[test]
3726    fn test_is_checked_none_is_unchecked() {
3727        let mut a = make_annot(AnnotationType::Widget);
3728        a.field_value = None;
3729        assert!(!a.is_checked());
3730    }
3731
3732    #[test]
3733    fn test_is_checked_yes_is_checked() {
3734        let mut a = make_annot(AnnotationType::Widget);
3735        a.field_value = Some("Yes".into());
3736        assert!(a.is_checked());
3737        assert!(a.annot_is_checked());
3738    }
3739
3740    #[test]
3741    fn test_option_count_none_when_no_options() {
3742        let a = make_annot(AnnotationType::Widget);
3743        assert_eq!(a.option_count(), 0);
3744        assert_eq!(a.annot_get_option_count(), 0);
3745    }
3746
3747    #[test]
3748    fn test_option_count_and_label() {
3749        let mut a = make_annot(AnnotationType::Widget);
3750        a.options = Some(vec![
3751            ChoiceOption {
3752                export_value: "a".into(),
3753                display_value: "Apple".into(),
3754            },
3755            ChoiceOption {
3756                export_value: "b".into(),
3757                display_value: "Banana".into(),
3758            },
3759        ]);
3760        assert_eq!(a.option_count(), 2);
3761        assert_eq!(a.option_label(0), Some("Apple"));
3762        assert_eq!(a.annot_get_option_label(1), Some("Banana"));
3763        assert!(a.option_label(2).is_none());
3764    }
3765
3766    #[test]
3767    fn test_is_option_selected() {
3768        let mut a = make_annot(AnnotationType::Widget);
3769        a.options = Some(vec![
3770            ChoiceOption {
3771                export_value: "a".into(),
3772                display_value: "Apple".into(),
3773            },
3774            ChoiceOption {
3775                export_value: "b".into(),
3776                display_value: "Banana".into(),
3777            },
3778        ]);
3779        a.field_value = Some("b".into());
3780        assert!(!a.is_option_selected(0));
3781        assert!(a.is_option_selected(1));
3782        assert!(!a.annot_is_option_selected(2)); // out of bounds
3783    }
3784
3785    // -----------------------------------------------------------------------
3786    // G8: focusable subtype stubs
3787    // -----------------------------------------------------------------------
3788
3789    #[test]
3790    fn test_focusable_subtype_count_returns_not_supported() {
3791        let a = make_annot(AnnotationType::Widget);
3792        assert!(a.focusable_subtype_count().is_err());
3793        assert!(a.annot_get_focusable_subtypes_count().is_err());
3794    }
3795
3796    #[test]
3797    fn test_set_focusable_subtypes_returns_not_supported() {
3798        let mut a = make_annot(AnnotationType::Widget);
3799        assert!(a.set_focusable_subtypes(&[]).is_err());
3800        assert!(a.annot_set_focusable_subtypes(&[]).is_err());
3801    }
3802
3803    // -----------------------------------------------------------------------
3804    // G9: set_flags / set_form_field_flags
3805    // -----------------------------------------------------------------------
3806
3807    #[test]
3808    fn test_set_flags_updates_in_memory() {
3809        let mut a = make_annot(AnnotationType::Text);
3810        assert!(a.flags().print()); // starts with Print flag (bits=4)
3811        a.set_flags(AnnotationFlags::from_bits(0)).unwrap();
3812        assert!(!a.flags().print());
3813        a.annot_set_flags(AnnotationFlags::from_bits(2)).unwrap();
3814        assert!(a.flags().hidden());
3815    }
3816
3817    #[test]
3818    fn test_set_form_field_flags_updates_in_memory() {
3819        use crate::form_field::FormFieldFlags;
3820        let mut a = make_annot(AnnotationType::Widget);
3821        assert!(a.form_field_flags().is_none());
3822        a.set_form_field_flags(FormFieldFlags::from_bits(1))
3823            .unwrap();
3824        assert_eq!(a.form_field_flags().map(|f| f.bits()), Some(1));
3825        a.annot_set_form_field_flags(FormFieldFlags::from_bits(3))
3826            .unwrap();
3827        assert_eq!(a.form_field_flags().map(|f| f.bits()), Some(3));
3828    }
3829}