Skip to main content

xberg_native_pdf/
annotation_types.rs

1//! Core annotation types and enums per PDF spec ISO 32000-1:2008, Section 12.5.
2//!
3//! This module provides shared types used by both annotation reading and writing.
4
5/// Annotation subtype per PDF spec Table 169.
6#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
7pub enum AnnotationSubtype {
8    /// Text annotation (sticky note) - Section 12.5.6.4
9    Text,
10    /// Link annotation - Section 12.5.6.5
11    Link,
12    /// Free text annotation - Section 12.5.6.6
13    FreeText,
14    /// Line annotation - Section 12.5.6.7
15    Line,
16    /// Square annotation - Section 12.5.6.8
17    Square,
18    /// Circle annotation - Section 12.5.6.8
19    Circle,
20    /// Polygon annotation - Section 12.5.6.9
21    Polygon,
22    /// Polyline annotation - Section 12.5.6.9
23    PolyLine,
24    /// Highlight annotation - Section 12.5.6.10
25    Highlight,
26    /// Underline annotation - Section 12.5.6.10
27    Underline,
28    /// Squiggly underline annotation - Section 12.5.6.10
29    Squiggly,
30    /// Strikeout annotation - Section 12.5.6.10
31    StrikeOut,
32    /// Rubber stamp annotation - Section 12.5.6.12
33    Stamp,
34    /// Caret annotation - Section 12.5.6.11
35    Caret,
36    /// Ink annotation - Section 12.5.6.13
37    Ink,
38    /// Popup annotation - Section 12.5.6.14
39    Popup,
40    /// File attachment annotation - Section 12.5.6.15
41    FileAttachment,
42    /// Sound annotation - Section 12.5.6.16
43    Sound,
44    /// Movie annotation - Section 12.5.6.17
45    Movie,
46    /// Widget annotation (form field) - Section 12.5.6.19
47    Widget,
48    /// Screen annotation - Section 12.5.6.18
49    Screen,
50    /// Printer's mark annotation - Section 12.5.6.20
51    PrinterMark,
52    /// Trap network annotation - Section 12.5.6.21
53    TrapNet,
54    /// Watermark annotation - Section 12.5.6.22
55    Watermark,
56    /// 3D annotation - Section 12.5.6.24
57    ThreeD,
58    /// Redaction annotation - Section 12.5.6.23
59    Redact,
60    /// RichMedia annotation - Adobe Extension Level 3
61    RichMedia,
62    /// Unknown annotation type
63    Unknown,
64}
65
66impl AnnotationSubtype {
67    /// Get the PDF name for this annotation subtype.
68    pub fn pdf_name(&self) -> &'static str {
69        match self {
70            Self::Text => "Text",
71            Self::Link => "Link",
72            Self::FreeText => "FreeText",
73            Self::Line => "Line",
74            Self::Square => "Square",
75            Self::Circle => "Circle",
76            Self::Polygon => "Polygon",
77            Self::PolyLine => "PolyLine",
78            Self::Highlight => "Highlight",
79            Self::Underline => "Underline",
80            Self::Squiggly => "Squiggly",
81            Self::StrikeOut => "StrikeOut",
82            Self::Stamp => "Stamp",
83            Self::Caret => "Caret",
84            Self::Ink => "Ink",
85            Self::Popup => "Popup",
86            Self::FileAttachment => "FileAttachment",
87            Self::Sound => "Sound",
88            Self::Movie => "Movie",
89            Self::Widget => "Widget",
90            Self::Screen => "Screen",
91            Self::PrinterMark => "PrinterMark",
92            Self::TrapNet => "TrapNet",
93            Self::Watermark => "Watermark",
94            Self::ThreeD => "3D",
95            Self::Redact => "Redact",
96            Self::RichMedia => "RichMedia",
97            Self::Unknown => "Unknown",
98        }
99    }
100
101    /// Parse from PDF name.
102    pub fn from_pdf_name(name: &str) -> Self {
103        match name {
104            "Text" => Self::Text,
105            "Link" => Self::Link,
106            "FreeText" => Self::FreeText,
107            "Line" => Self::Line,
108            "Square" => Self::Square,
109            "Circle" => Self::Circle,
110            "Polygon" => Self::Polygon,
111            "PolyLine" => Self::PolyLine,
112            "Highlight" => Self::Highlight,
113            "Underline" => Self::Underline,
114            "Squiggly" => Self::Squiggly,
115            "StrikeOut" => Self::StrikeOut,
116            "Stamp" => Self::Stamp,
117            "Caret" => Self::Caret,
118            "Ink" => Self::Ink,
119            "Popup" => Self::Popup,
120            "FileAttachment" => Self::FileAttachment,
121            "Sound" => Self::Sound,
122            "Movie" => Self::Movie,
123            "Widget" => Self::Widget,
124            "Screen" => Self::Screen,
125            "PrinterMark" => Self::PrinterMark,
126            "TrapNet" => Self::TrapNet,
127            "Watermark" => Self::Watermark,
128            "3D" => Self::ThreeD,
129            "Redact" => Self::Redact,
130            "RichMedia" => Self::RichMedia,
131            _ => Self::Unknown,
132        }
133    }
134
135    /// Check if this is a markup annotation (has popup, replies, etc.)
136    pub fn is_markup(&self) -> bool {
137        matches!(
138            self,
139            Self::Text
140                | Self::FreeText
141                | Self::Line
142                | Self::Square
143                | Self::Circle
144                | Self::Polygon
145                | Self::PolyLine
146                | Self::Highlight
147                | Self::Underline
148                | Self::Squiggly
149                | Self::StrikeOut
150                | Self::Stamp
151                | Self::Caret
152                | Self::Ink
153                | Self::FileAttachment
154                | Self::Sound
155                | Self::Redact
156        )
157    }
158
159    /// Check if this is a text markup annotation.
160    pub fn is_text_markup(&self) -> bool {
161        matches!(
162            self,
163            Self::Highlight | Self::Underline | Self::Squiggly | Self::StrikeOut
164        )
165    }
166}
167
168/// Annotation flags per PDF spec Table 165.
169///
170/// These flags control how the annotation behaves when displayed or printed.
171#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
172pub struct AnnotationFlags(u32);
173
174impl AnnotationFlags {
175    /// Invisible flag (bit 1) - If set, do not display if no AP.
176    pub const INVISIBLE: u32 = 1 << 0;
177    /// Hidden flag (bit 2) - If set, do not display or print.
178    pub const HIDDEN: u32 = 1 << 1;
179    /// Print flag (bit 3) - If set, print annotation when printing.
180    pub const PRINT: u32 = 1 << 2;
181    /// NoZoom flag (bit 4) - If set, do not scale with page zoom.
182    pub const NO_ZOOM: u32 = 1 << 3;
183    /// NoRotate flag (bit 5) - If set, do not rotate with page.
184    pub const NO_ROTATE: u32 = 1 << 4;
185    /// NoView flag (bit 6) - If set, do not display on screen.
186    pub const NO_VIEW: u32 = 1 << 5;
187    /// ReadOnly flag (bit 7) - If set, do not allow interaction.
188    pub const READ_ONLY: u32 = 1 << 6;
189    /// Locked flag (bit 8) - If set, do not allow deletion/modification.
190    pub const LOCKED: u32 = 1 << 7;
191    /// ToggleNoView flag (bit 9) - Invert NoView on mouse events.
192    pub const TOGGLE_NO_VIEW: u32 = 1 << 8;
193    /// LockedContents flag (bit 10) - If set, do not allow content modification.
194    pub const LOCKED_CONTENTS: u32 = 1 << 9;
195
196    /// Create new flags from raw value.
197    pub fn new(value: u32) -> Self {
198        Self(value)
199    }
200
201    /// Create empty flags.
202    pub fn empty() -> Self {
203        Self(0)
204    }
205
206    /// Create default flags for printing (PRINT flag set).
207    pub fn printable() -> Self {
208        Self(Self::PRINT)
209    }
210
211    /// Get raw value.
212    pub fn bits(&self) -> u32 {
213        self.0
214    }
215
216    /// Check if a flag is set.
217    pub fn contains(&self, flag: u32) -> bool {
218        (self.0 & flag) != 0
219    }
220
221    /// Set a flag.
222    pub fn set(&mut self, flag: u32) {
223        self.0 |= flag;
224    }
225
226    /// Clear a flag.
227    pub fn clear(&mut self, flag: u32) {
228        self.0 &= !flag;
229    }
230
231    /// Check if invisible.
232    pub fn is_invisible(&self) -> bool {
233        self.contains(Self::INVISIBLE)
234    }
235
236    /// Check if hidden.
237    pub fn is_hidden(&self) -> bool {
238        self.contains(Self::HIDDEN)
239    }
240
241    /// Check if printable.
242    pub fn is_printable(&self) -> bool {
243        self.contains(Self::PRINT)
244    }
245
246    /// Check if no zoom.
247    pub fn is_no_zoom(&self) -> bool {
248        self.contains(Self::NO_ZOOM)
249    }
250
251    /// Check if no rotate.
252    pub fn is_no_rotate(&self) -> bool {
253        self.contains(Self::NO_ROTATE)
254    }
255
256    /// Check if read only.
257    pub fn is_read_only(&self) -> bool {
258        self.contains(Self::READ_ONLY)
259    }
260
261    /// Check if locked.
262    pub fn is_locked(&self) -> bool {
263        self.contains(Self::LOCKED)
264    }
265}
266
267/// Border style type per PDF spec Table 166.
268#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
269pub enum BorderStyleType {
270    /// Solid border (S)
271    #[default]
272    Solid,
273    /// Dashed border (D)
274    Dashed,
275    /// Beveled border (B)
276    Beveled,
277    /// Inset border (I)
278    Inset,
279    /// Underline border (U)
280    Underline,
281}
282
283impl BorderStyleType {
284    /// Get PDF name for this border style.
285    pub fn pdf_name(&self) -> &'static str {
286        match self {
287            Self::Solid => "S",
288            Self::Dashed => "D",
289            Self::Beveled => "B",
290            Self::Inset => "I",
291            Self::Underline => "U",
292        }
293    }
294
295    /// Parse from PDF name.
296    pub fn from_pdf_name(name: &str) -> Self {
297        match name {
298            "S" => Self::Solid,
299            "D" => Self::Dashed,
300            "B" => Self::Beveled,
301            "I" => Self::Inset,
302            "U" => Self::Underline,
303            _ => Self::Solid,
304        }
305    }
306}
307
308/// Border style dictionary per PDF spec Table 166.
309#[derive(Debug, Clone, Default)]
310pub struct AnnotationBorderStyle {
311    /// Border width in points.
312    pub width: f32,
313    /// Border style type.
314    pub style: BorderStyleType,
315    /// Dash pattern for dashed borders [dash, gap, dash, gap, ...].
316    pub dash_pattern: Option<Vec<f32>>,
317}
318
319impl AnnotationBorderStyle {
320    /// Create a solid border with given width.
321    pub fn solid(width: f32) -> Self {
322        Self {
323            width,
324            style: BorderStyleType::Solid,
325            dash_pattern: None,
326        }
327    }
328
329    /// Create a dashed border.
330    pub fn dashed(width: f32, dash: f32, gap: f32) -> Self {
331        Self {
332            width,
333            style: BorderStyleType::Dashed,
334            dash_pattern: Some(vec![dash, gap]),
335        }
336    }
337
338    /// Create no visible border.
339    pub fn none() -> Self {
340        Self {
341            width: 0.0,
342            style: BorderStyleType::Solid,
343            dash_pattern: None,
344        }
345    }
346}
347
348/// Border effect style per PDF spec Table 167.
349#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
350pub enum BorderEffectStyle {
351    /// No effect (S)
352    #[default]
353    None,
354    /// Cloudy border (C)
355    Cloudy,
356}
357
358impl BorderEffectStyle {
359    /// Get PDF name.
360    pub fn pdf_name(&self) -> &'static str {
361        match self {
362            Self::None => "S",
363            Self::Cloudy => "C",
364        }
365    }
366
367    /// Parse from PDF name.
368    pub fn from_pdf_name(name: &str) -> Self {
369        match name {
370            "C" => Self::Cloudy,
371            _ => Self::None,
372        }
373    }
374}
375
376/// Border effect dictionary per PDF spec Table 167.
377#[derive(Debug, Clone, Default)]
378pub struct BorderEffect {
379    /// Effect style.
380    pub style: BorderEffectStyle,
381    /// Effect intensity (for cloudy effect, 0-2 recommended).
382    pub intensity: f32,
383}
384
385/// Line ending style per PDF spec Table 176.
386#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
387pub enum LineEndingStyle {
388    /// No line ending
389    #[default]
390    None,
391    /// Square filled with interior color
392    Square,
393    /// Circle filled with interior color
394    Circle,
395    /// Diamond filled with interior color
396    Diamond,
397    /// Open arrow (two lines forming acute angle)
398    OpenArrow,
399    /// Closed arrow (filled triangle)
400    ClosedArrow,
401    /// Butt (perpendicular line at endpoint)
402    Butt,
403    /// Reverse open arrow
404    ROpenArrow,
405    /// Reverse closed arrow
406    RClosedArrow,
407    /// Slash (30 degrees from perpendicular)
408    Slash,
409}
410
411impl LineEndingStyle {
412    /// Get PDF name.
413    pub fn pdf_name(&self) -> &'static str {
414        match self {
415            Self::None => "None",
416            Self::Square => "Square",
417            Self::Circle => "Circle",
418            Self::Diamond => "Diamond",
419            Self::OpenArrow => "OpenArrow",
420            Self::ClosedArrow => "ClosedArrow",
421            Self::Butt => "Butt",
422            Self::ROpenArrow => "ROpenArrow",
423            Self::RClosedArrow => "RClosedArrow",
424            Self::Slash => "Slash",
425        }
426    }
427
428    /// Parse from PDF name.
429    pub fn from_pdf_name(name: &str) -> Self {
430        match name {
431            "None" => Self::None,
432            "Square" => Self::Square,
433            "Circle" => Self::Circle,
434            "Diamond" => Self::Diamond,
435            "OpenArrow" => Self::OpenArrow,
436            "ClosedArrow" => Self::ClosedArrow,
437            "Butt" => Self::Butt,
438            "ROpenArrow" => Self::ROpenArrow,
439            "RClosedArrow" => Self::RClosedArrow,
440            "Slash" => Self::Slash,
441            _ => Self::None,
442        }
443    }
444}
445
446/// Annotation color representation.
447///
448/// Colors are specified as values in the range 0.0 to 1.0.
449#[derive(Debug, Clone, PartialEq, Default)]
450pub enum AnnotationColor {
451    /// No color (transparent)
452    #[default]
453    None,
454    /// Grayscale (1 component)
455    Gray(f32),
456    /// RGB color (3 components)
457    Rgb(f32, f32, f32),
458    /// CMYK color (4 components)
459    Cmyk(f32, f32, f32, f32),
460}
461
462impl AnnotationColor {
463    /// Create yellow color (common for highlights).
464    pub fn yellow() -> Self {
465        Self::Rgb(1.0, 1.0, 0.0)
466    }
467
468    /// Create red color.
469    pub fn red() -> Self {
470        Self::Rgb(1.0, 0.0, 0.0)
471    }
472
473    /// Create green color.
474    pub fn green() -> Self {
475        Self::Rgb(0.0, 1.0, 0.0)
476    }
477
478    /// Create blue color.
479    pub fn blue() -> Self {
480        Self::Rgb(0.0, 0.0, 1.0)
481    }
482
483    /// Create black color.
484    pub fn black() -> Self {
485        Self::Gray(0.0)
486    }
487
488    /// Create white color.
489    pub fn white() -> Self {
490        Self::Gray(1.0)
491    }
492
493    /// Convert to PDF array representation.
494    pub fn to_array(&self) -> Option<Vec<f32>> {
495        match self {
496            Self::None => None,
497            Self::Gray(g) => Some(vec![*g]),
498            Self::Rgb(r, g, b) => Some(vec![*r, *g, *b]),
499            Self::Cmyk(c, m, y, k) => Some(vec![*c, *m, *y, *k]),
500        }
501    }
502
503    /// Parse from PDF array.
504    pub fn from_array(arr: &[f32]) -> Self {
505        match arr.len() {
506            0 => Self::None,
507            1 => Self::Gray(arr[0]),
508            3 => Self::Rgb(arr[0], arr[1], arr[2]),
509            4 => Self::Cmyk(arr[0], arr[1], arr[2], arr[3]),
510            _ => Self::None,
511        }
512    }
513}
514
515/// Text annotation icon types per PDF spec Section 12.5.6.4.
516#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
517pub enum TextAnnotationIcon {
518    /// Comment icon
519    Comment,
520    /// Key icon
521    Key,
522    /// Note icon (default)
523    #[default]
524    Note,
525    /// Help icon
526    Help,
527    /// New paragraph icon
528    NewParagraph,
529    /// Paragraph icon
530    Paragraph,
531    /// Insert icon
532    Insert,
533}
534
535impl TextAnnotationIcon {
536    /// Get PDF name.
537    pub fn pdf_name(&self) -> &'static str {
538        match self {
539            Self::Comment => "Comment",
540            Self::Key => "Key",
541            Self::Note => "Note",
542            Self::Help => "Help",
543            Self::NewParagraph => "NewParagraph",
544            Self::Paragraph => "Paragraph",
545            Self::Insert => "Insert",
546        }
547    }
548
549    /// Parse from PDF name.
550    pub fn from_pdf_name(name: &str) -> Self {
551        match name {
552            "Comment" => Self::Comment,
553            "Key" => Self::Key,
554            "Note" => Self::Note,
555            "Help" => Self::Help,
556            "NewParagraph" => Self::NewParagraph,
557            "Paragraph" => Self::Paragraph,
558            "Insert" => Self::Insert,
559            _ => Self::Note,
560        }
561    }
562}
563
564/// Text markup annotation type.
565#[derive(Debug, Clone, Copy, PartialEq, Eq)]
566pub enum TextMarkupType {
567    /// Highlight annotation
568    Highlight,
569    /// Underline annotation
570    Underline,
571    /// Squiggly underline annotation
572    Squiggly,
573    /// Strikeout annotation
574    StrikeOut,
575}
576
577impl TextMarkupType {
578    /// Get the annotation subtype.
579    pub fn subtype(&self) -> AnnotationSubtype {
580        match self {
581            Self::Highlight => AnnotationSubtype::Highlight,
582            Self::Underline => AnnotationSubtype::Underline,
583            Self::Squiggly => AnnotationSubtype::Squiggly,
584            Self::StrikeOut => AnnotationSubtype::StrikeOut,
585        }
586    }
587
588    /// Get default color for this markup type.
589    pub fn default_color(&self) -> AnnotationColor {
590        match self {
591            Self::Highlight => AnnotationColor::yellow(),
592            Self::Underline => AnnotationColor::green(),
593            Self::Squiggly => AnnotationColor::Rgb(1.0, 0.5, 0.0),
594            Self::StrikeOut => AnnotationColor::red(),
595        }
596    }
597}
598
599/// Standard stamp types per PDF spec Section 12.5.6.12.
600#[derive(Debug, Clone, PartialEq, Eq, Default)]
601pub enum StampType {
602    /// Approved stamp
603    Approved,
604    /// Experimental stamp
605    Experimental,
606    /// Not approved stamp
607    NotApproved,
608    /// As-is stamp
609    AsIs,
610    /// Expired stamp
611    Expired,
612    /// Not for public release stamp
613    NotForPublicRelease,
614    /// Confidential stamp
615    Confidential,
616    /// Final stamp
617    Final,
618    /// Sold stamp
619    Sold,
620    /// Departmental stamp
621    Departmental,
622    /// For comment stamp
623    ForComment,
624    /// Top secret stamp
625    TopSecret,
626    /// Draft stamp
627    #[default]
628    Draft,
629    /// For public release stamp
630    ForPublicRelease,
631    /// Custom stamp name
632    Custom(String),
633}
634
635impl StampType {
636    /// Get PDF name.
637    pub fn pdf_name(&self) -> String {
638        match self {
639            Self::Approved => "Approved".to_string(),
640            Self::Experimental => "Experimental".to_string(),
641            Self::NotApproved => "NotApproved".to_string(),
642            Self::AsIs => "AsIs".to_string(),
643            Self::Expired => "Expired".to_string(),
644            Self::NotForPublicRelease => "NotForPublicRelease".to_string(),
645            Self::Confidential => "Confidential".to_string(),
646            Self::Final => "Final".to_string(),
647            Self::Sold => "Sold".to_string(),
648            Self::Departmental => "Departmental".to_string(),
649            Self::ForComment => "ForComment".to_string(),
650            Self::TopSecret => "TopSecret".to_string(),
651            Self::Draft => "Draft".to_string(),
652            Self::ForPublicRelease => "ForPublicRelease".to_string(),
653            Self::Custom(name) => name.clone(),
654        }
655    }
656
657    /// Parse from PDF name.
658    pub fn from_pdf_name(name: &str) -> Self {
659        match name {
660            "Approved" => Self::Approved,
661            "Experimental" => Self::Experimental,
662            "NotApproved" => Self::NotApproved,
663            "AsIs" => Self::AsIs,
664            "Expired" => Self::Expired,
665            "NotForPublicRelease" => Self::NotForPublicRelease,
666            "Confidential" => Self::Confidential,
667            "Final" => Self::Final,
668            "Sold" => Self::Sold,
669            "Departmental" => Self::Departmental,
670            "ForComment" => Self::ForComment,
671            "TopSecret" => Self::TopSecret,
672            "Draft" => Self::Draft,
673            "ForPublicRelease" => Self::ForPublicRelease,
674            other => Self::Custom(other.to_string()),
675        }
676    }
677}
678
679/// FreeText annotation intent per PDF spec Section 12.5.6.6.
680#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
681pub enum FreeTextIntent {
682    /// Plain free text (text box comment)
683    #[default]
684    FreeText,
685    /// Callout with line pointing to content
686    FreeTextCallout,
687    /// Typewriter-style text
688    FreeTextTypeWriter,
689}
690
691impl FreeTextIntent {
692    /// Get PDF name.
693    pub fn pdf_name(&self) -> &'static str {
694        match self {
695            Self::FreeText => "FreeText",
696            Self::FreeTextCallout => "FreeTextCallout",
697            Self::FreeTextTypeWriter => "FreeTextTypeWriter",
698        }
699    }
700
701    /// Parse from PDF name.
702    pub fn from_pdf_name(name: &str) -> Self {
703        match name {
704            "FreeTextCallout" => Self::FreeTextCallout,
705            "FreeTextTypeWriter" => Self::FreeTextTypeWriter,
706            _ => Self::FreeText,
707        }
708    }
709}
710
711/// Text alignment (quadding) per PDF spec.
712#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
713pub enum TextAlignment {
714    /// Left-justified (0)
715    #[default]
716    Left,
717    /// Centered (1)
718    Center,
719    /// Right-justified (2)
720    Right,
721}
722
723impl TextAlignment {
724    /// Get PDF integer value.
725    pub fn to_pdf_int(&self) -> i32 {
726        match self {
727            Self::Left => 0,
728            Self::Center => 1,
729            Self::Right => 2,
730        }
731    }
732
733    /// Parse from PDF integer.
734    pub fn from_pdf_int(value: i32) -> Self {
735        match value {
736            1 => Self::Center,
737            2 => Self::Right,
738            _ => Self::Left,
739        }
740    }
741}
742
743/// Caret annotation symbol per PDF spec Section 12.5.6.11.
744#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
745pub enum CaretSymbol {
746    /// No symbol
747    #[default]
748    None,
749    /// Paragraph symbol (pilcrow)
750    Paragraph,
751}
752
753impl CaretSymbol {
754    /// Get PDF name.
755    pub fn pdf_name(&self) -> &'static str {
756        match self {
757            Self::None => "None",
758            Self::Paragraph => "P",
759        }
760    }
761
762    /// Parse from PDF name.
763    pub fn from_pdf_name(name: &str) -> Self {
764        match name {
765            "P" => Self::Paragraph,
766            _ => Self::None,
767        }
768    }
769}
770
771/// File attachment annotation icon per PDF spec Section 12.5.6.15.
772#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
773pub enum FileAttachmentIcon {
774    /// Graph/push pin icon
775    GraphPushPin,
776    /// Paperclip tag icon (default)
777    #[default]
778    PaperclipTag,
779    /// Push pin icon
780    PushPin,
781}
782
783impl FileAttachmentIcon {
784    /// Get PDF name.
785    pub fn pdf_name(&self) -> &'static str {
786        match self {
787            Self::GraphPushPin => "GraphPushPin",
788            Self::PaperclipTag => "PaperclipTag",
789            Self::PushPin => "PushPin",
790        }
791    }
792
793    /// Parse from PDF name.
794    pub fn from_pdf_name(name: &str) -> Self {
795        match name {
796            "GraphPushPin" => Self::GraphPushPin,
797            "PushPin" => Self::PushPin,
798            _ => Self::PaperclipTag,
799        }
800    }
801}
802
803/// Reply type for annotation replies per PDF spec Table 170.
804#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
805pub enum ReplyType {
806    /// Reply annotation
807    #[default]
808    Reply,
809    /// Group annotation
810    Group,
811}
812
813impl ReplyType {
814    /// Get PDF name.
815    pub fn pdf_name(&self) -> &'static str {
816        match self {
817            Self::Reply => "R",
818            Self::Group => "Group",
819        }
820    }
821
822    /// Parse from PDF name.
823    pub fn from_pdf_name(name: &str) -> Self {
824        match name {
825            "Group" => Self::Group,
826            _ => Self::Reply,
827        }
828    }
829}
830
831/// Highlight mode for link annotations per PDF spec Table 173.
832#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
833pub enum HighlightMode {
834    /// No highlighting (N)
835    None,
836    /// Invert the contents (I) - default
837    #[default]
838    Invert,
839    /// Invert the border (O)
840    Outline,
841    /// Push effect (P)
842    Push,
843}
844
845impl HighlightMode {
846    /// Get PDF name.
847    pub fn pdf_name(&self) -> &'static str {
848        match self {
849            Self::None => "N",
850            Self::Invert => "I",
851            Self::Outline => "O",
852            Self::Push => "P",
853        }
854    }
855
856    /// Parse from PDF name.
857    pub fn from_pdf_name(name: &str) -> Self {
858        match name {
859            "N" => Self::None,
860            "O" => Self::Outline,
861            "P" => Self::Push,
862            _ => Self::Invert,
863        }
864    }
865}
866
867/// Widget field type for form fields.
868#[derive(Debug, Clone, PartialEq, Default)]
869pub enum WidgetFieldType {
870    /// Text input field
871    #[default]
872    Text,
873    /// Checkbox
874    Checkbox {
875        /// Whether the checkbox is checked
876        checked: bool,
877    },
878    /// Radio button
879    Radio {
880        /// Selected option value
881        selected: Option<String>,
882    },
883    /// Push button
884    Button,
885    /// Choice field (dropdown or list)
886    Choice {
887        /// Available options
888        options: Vec<String>,
889        /// Selected option(s)
890        selected: Option<String>,
891    },
892    /// Signature field
893    Signature,
894    /// Unknown field type
895    Unknown,
896}
897
898/// A quad point specification (8 numbers defining a quadrilateral).
899///
900/// Points are specified as [x1, y1, x2, y2, x3, y3, x4, y4] in counterclockwise order.
901/// The bottom edge is from (x1, y1) to (x2, y2).
902pub type QuadPoint = [f64; 8];
903
904/// Helper functions for quad points.
905pub mod quad_points {
906    use super::QuadPoint;
907    use crate::geometry::Rect;
908
909    /// Create a quad point from a rectangle.
910    pub fn from_rect(rect: &Rect) -> QuadPoint {
911        let x1 = rect.x as f64;
912        let y1 = rect.y as f64;
913        let x2 = (rect.x + rect.width) as f64;
914        let y2 = rect.y as f64;
915        let x3 = (rect.x + rect.width) as f64;
916        let y3 = (rect.y + rect.height) as f64;
917        let x4 = rect.x as f64;
918        let y4 = (rect.y + rect.height) as f64;
919
920        [x1, y1, x2, y2, x3, y3, x4, y4]
921    }
922
923    /// Get the bounding rectangle of a quad point.
924    pub fn bounding_rect(quad: &QuadPoint) -> Rect {
925        let min_x = quad[0].min(quad[2]).min(quad[4]).min(quad[6]) as f32;
926        let max_x = quad[0].max(quad[2]).max(quad[4]).max(quad[6]) as f32;
927        let min_y = quad[1].min(quad[3]).min(quad[5]).min(quad[7]) as f32;
928        let max_y = quad[1].max(quad[3]).max(quad[5]).max(quad[7]) as f32;
929
930        Rect::new(min_x, min_y, max_x - min_x, max_y - min_y)
931    }
932
933    /// Parse quad points from a flat array of numbers.
934    pub fn parse(arr: &[f64]) -> Vec<QuadPoint> {
935        arr.as_chunks::<8>()
936            .0
937            .iter()
938            .map(|chunk| {
939                [
940                    chunk[0], chunk[1], chunk[2], chunk[3], chunk[4], chunk[5], chunk[6], chunk[7],
941                ]
942            })
943            .collect()
944    }
945
946    /// Flatten quad points to a single array.
947    pub fn flatten(quads: &[QuadPoint]) -> Vec<f64> {
948        quads.iter().flat_map(|q| q.iter().copied()).collect()
949    }
950}
951
952#[cfg(test)]
953mod tests;