Skip to main content

pdf_annot/
stamp.rs

1//! Stamp, FileAttachment, FreeText, Sound, and Movie annotations.
2
3extern crate alloc;
4
5use crate::annotation::{self, Annotation};
6use crate::types::LineEnding;
7use pdf_syntax::object::dict::keys::*;
8use pdf_syntax::object::{Array, Dict, Name, Rect, Stream};
9
10/// Standard rubber-stamp names per ISO 32000-2 Table 181.
11///
12/// Used for both reading (parser output) and writing (annotation builder).
13/// PDF viewers ship built-in appearances for the standard variants; using
14/// a recognized name produces consistent visuals across viewers. A
15/// non-standard name is preserved verbatim in [`Self::Custom`] so it is
16/// never silently lost.  [`Self::to_pdf_name`] converts back to the PDF
17/// name string for the write path.
18#[derive(Debug, Clone, PartialEq, Eq)]
19pub enum StampName {
20    /// "APPROVED" — green check-style stamp.
21    Approved,
22    /// "EXPERIMENTAL" — orange/red stamp.
23    Experimental,
24    /// "NOT APPROVED" — red rejection-style stamp.
25    NotApproved,
26    /// "AS IS" — neutral stamp meaning "submitted without warranty".
27    AsIs,
28    /// "EXPIRED" — red stamp indicating a document has lapsed.
29    Expired,
30    /// "NOT FOR PUBLIC RELEASE" — red restriction stamp.
31    NotForPublicRelease,
32    /// "CONFIDENTIAL" — red restriction stamp for sensitive material.
33    Confidential,
34    /// "FINAL" — green completion stamp.
35    Final,
36    /// "SOLD" — typically used in real-estate or auction contexts.
37    Sold,
38    /// "DEPARTMENTAL" — internal-use restriction stamp.
39    Departmental,
40    /// "FOR COMMENT" — review-cycle stamp.
41    ForComment,
42    /// "TOP SECRET" — strongest standard restriction stamp.
43    TopSecret,
44    /// "DRAFT" — work-in-progress stamp. Also the fallback default
45    /// when no `/Name` entry is present on the input annotation.
46    Draft,
47    /// "FOR PUBLIC RELEASE" — explicit clearance stamp.
48    ForPublicRelease,
49    /// Any non-standard stamp name found in the input PDF, preserved
50    /// verbatim. Custom stamps may render with viewer-default
51    /// appearance only.
52    Custom(alloc::string::String),
53}
54
55impl StampName {
56    /// Return the PDF `/Name` string for this stamp, e.g. `"Approved"`.
57    ///
58    /// Used by the write path (`AnnotationBuilder::stamp`) to embed the
59    /// correct name into the annotation dictionary.
60    pub fn to_pdf_name(&self) -> &str {
61        match self {
62            Self::Approved => "Approved",
63            Self::Experimental => "Experimental",
64            Self::NotApproved => "NotApproved",
65            Self::AsIs => "AsIs",
66            Self::Expired => "Expired",
67            Self::NotForPublicRelease => "NotForPublicRelease",
68            Self::Confidential => "Confidential",
69            Self::Final => "Final",
70            Self::Sold => "Sold",
71            Self::Departmental => "Departmental",
72            Self::ForComment => "ForComment",
73            Self::TopSecret => "TopSecret",
74            Self::Draft => "Draft",
75            Self::ForPublicRelease => "ForPublicRelease",
76            Self::Custom(s) => s.as_str(),
77        }
78    }
79
80    /// Parse from a PDF name.
81    pub fn from_name(name: &[u8]) -> Self {
82        match name {
83            b"Approved" => Self::Approved,
84            b"Experimental" => Self::Experimental,
85            b"NotApproved" => Self::NotApproved,
86            b"AsIs" => Self::AsIs,
87            b"Expired" => Self::Expired,
88            b"NotForPublicRelease" => Self::NotForPublicRelease,
89            b"Confidential" => Self::Confidential,
90            b"Final" => Self::Final,
91            b"Sold" => Self::Sold,
92            b"Departmental" => Self::Departmental,
93            b"ForComment" => Self::ForComment,
94            b"TopSecret" => Self::TopSecret,
95            b"Draft" => Self::Draft,
96            b"ForPublicRelease" => Self::ForPublicRelease,
97            other => {
98                let s = core::str::from_utf8(other).unwrap_or("Unknown").to_owned();
99                Self::Custom(s)
100            }
101        }
102    }
103}
104
105/// A Stamp annotation.
106#[derive(Debug)]
107pub struct StampAnnotation {
108    /// The stamp name.
109    pub name: StampName,
110}
111
112impl StampAnnotation {
113    /// Extract stamp annotation properties.
114    pub fn from_annot(annot: &Annotation<'_>) -> Self {
115        let name = annot
116            .dict()
117            .get::<Name>(NAME)
118            .map(|n: Name| StampName::from_name(n.as_ref()))
119            .unwrap_or(StampName::Draft);
120        Self { name }
121    }
122}
123
124/// A FileAttachment annotation.
125#[derive(Debug)]
126pub struct FileAttachmentAnnotation<'a> {
127    dict: Dict<'a>,
128    /// The icon name.
129    pub icon: alloc::string::String,
130}
131
132impl<'a> FileAttachmentAnnotation<'a> {
133    /// Extract file attachment annotation properties.
134    pub fn from_annot(annot: &Annotation<'a>) -> Self {
135        let dict = annot.dict().clone();
136        let icon = dict
137            .get::<Name>(NAME)
138            .map(|n: Name| alloc::string::String::from(n.as_str()))
139            .unwrap_or_else(|| alloc::string::String::from("PushPin"));
140        Self { dict, icon }
141    }
142
143    /// Return the file specification dictionary.
144    pub fn file_spec(&self) -> Option<Dict<'a>> {
145        self.dict.get::<Dict<'_>>(FS)
146    }
147
148    /// Return the embedded file stream.
149    pub fn embedded_file(&self) -> Option<Stream<'a>> {
150        let fs = self.file_spec()?;
151        let ef = fs.get::<Dict<'_>>(EF)?;
152        ef.get::<Stream<'_>>(F).or_else(|| ef.get::<Stream<'_>>(UF))
153    }
154
155    /// Return the filename.
156    pub fn filename(&self) -> Option<alloc::string::String> {
157        let fs = self.file_spec()?;
158        fs.get::<pdf_syntax::object::String>(UF)
159            .or_else(|| fs.get::<pdf_syntax::object::String>(F))
160            .map(|s| annotation::pdf_string_to_string(&s))
161    }
162}
163
164/// A FreeText annotation.
165#[derive(Debug)]
166pub struct FreeTextAnnotation {
167    /// The default appearance string.
168    pub default_appearance: alloc::string::String,
169    /// Text justification (0=left, 1=center, 2=right).
170    pub justification: u32,
171    /// Default style string.
172    pub default_style: Option<alloc::string::String>,
173    /// Rich text content.
174    pub rich_content: Option<alloc::string::String>,
175    /// Callout line points.
176    pub callout_line: Option<Vec<f32>>,
177    /// Intent.
178    pub intent: Option<alloc::string::String>,
179    /// Line ending style for callout.
180    pub line_ending: LineEnding,
181    /// Rectangle differences.
182    pub rd: Option<Rect>,
183}
184
185impl FreeTextAnnotation {
186    /// Extract free text annotation properties.
187    pub fn from_annot(annot: &Annotation<'_>) -> Self {
188        let dict = annot.dict();
189        let default_appearance = dict
190            .get::<pdf_syntax::object::String>(DA)
191            .map(|s| annotation::pdf_string_to_string(&s))
192            .unwrap_or_default();
193        let justification = dict.get::<u32>(Q).unwrap_or(0);
194        let default_style = dict
195            .get::<pdf_syntax::object::String>(DS)
196            .map(|s| annotation::pdf_string_to_string(&s));
197        let rich_content = dict
198            .get::<pdf_syntax::object::String>(RC)
199            .map(|s| annotation::pdf_string_to_string(&s));
200        let callout_line: Option<Vec<f32>> = dict
201            .get::<Array<'_>>(CL)
202            .map(|arr: Array<'_>| arr.iter::<f32>().collect());
203        let intent = dict
204            .get::<Name>(IT)
205            .map(|n: Name| alloc::string::String::from(n.as_str()));
206        let line_ending = dict
207            .get::<Name>(LE)
208            .map(|n: Name| LineEnding::from_name(n.as_ref()))
209            .unwrap_or(LineEnding::None);
210        let rd = dict.get::<Rect>(RD);
211        Self {
212            default_appearance,
213            justification,
214            default_style,
215            rich_content,
216            callout_line,
217            intent,
218            line_ending,
219            rd,
220        }
221    }
222}
223
224/// A Sound annotation.
225#[derive(Debug)]
226pub struct SoundAnnotation {
227    /// The icon name.
228    pub icon: alloc::string::String,
229}
230
231impl SoundAnnotation {
232    /// Extract sound annotation properties.
233    pub fn from_annot(annot: &Annotation<'_>) -> Self {
234        let icon = annot
235            .dict()
236            .get::<Name>(NAME)
237            .map(|n: Name| alloc::string::String::from(n.as_str()))
238            .unwrap_or_else(|| alloc::string::String::from("Speaker"));
239        Self { icon }
240    }
241}
242
243/// A Movie annotation.
244#[derive(Debug)]
245pub struct MovieAnnotation {
246    /// The movie title.
247    pub title: Option<alloc::string::String>,
248}
249
250impl MovieAnnotation {
251    /// Extract movie annotation properties.
252    pub fn from_annot(annot: &Annotation<'_>) -> Self {
253        let title = annot
254            .dict()
255            .get::<pdf_syntax::object::String>(T)
256            .map(|s| annotation::pdf_string_to_string(&s));
257        Self { title }
258    }
259}