rpdfium_doc/
annotation_appearance.rs1use rpdfium_core::{Name, ObjectId, PdfSource};
9use rpdfium_parser::{Object, ObjectStore};
10
11use crate::error::{DocError, DocResult};
12
13#[derive(Debug, Clone)]
15pub struct AnnotationAppearance {
16 pub normal: Option<ObjectId>,
18 pub rollover: Option<ObjectId>,
20 pub down: Option<ObjectId>,
22}
23
24pub fn extract_appearance<S: PdfSource>(
30 ap_obj: &Object,
31 store: &ObjectStore<S>,
32) -> DocResult<AnnotationAppearance> {
33 let resolved = store
34 .deep_resolve(ap_obj)
35 .map_err(|e| DocError::Parser(e.to_string()))?;
36 let ap_dict = resolved.as_dict().ok_or(DocError::UnexpectedType)?;
37
38 let normal = extract_sub_appearance(ap_dict.get(&Name::n()), store);
39 let rollover = extract_sub_appearance(ap_dict.get(&Name::r()), store);
40 let down = extract_sub_appearance(ap_dict.get(&Name::d()), store);
41
42 Ok(AnnotationAppearance {
43 normal,
44 rollover,
45 down,
46 })
47}
48
49fn extract_sub_appearance<S: PdfSource>(
54 obj: Option<&Object>,
55 store: &ObjectStore<S>,
56) -> Option<ObjectId> {
57 let entry = obj?;
58
59 if let Some(id) = entry.as_reference() {
61 return Some(id);
62 }
63
64 let resolved = store.deep_resolve(entry).ok()?;
66
67 if let Some(state_dict) = resolved.as_dict() {
69 for value in state_dict.values() {
70 if let Some(id) = value.as_reference() {
71 return Some(id);
72 }
73 }
74 }
75
76 None
77}
78
79#[cfg(test)]
80mod tests {
81 use super::*;
82 use rpdfium_core::error::ObjectId as CoreObjectId;
83 use std::collections::HashMap;
84
85 fn build_store() -> ObjectStore<Vec<u8>> {
86 let pdf = build_minimal_pdf();
87 ObjectStore::open(pdf, rpdfium_core::ParsingMode::Lenient).unwrap()
88 }
89
90 fn build_minimal_pdf() -> Vec<u8> {
91 let mut pdf = Vec::new();
92 pdf.extend_from_slice(b"%PDF-1.4\n");
93 let obj1_offset = pdf.len();
94 pdf.extend_from_slice(b"1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n");
95 let obj2_offset = pdf.len();
96 pdf.extend_from_slice(b"2 0 obj\n<< /Type /Pages /Kids [] /Count 0 >>\nendobj\n");
97 let xref_offset = pdf.len();
98 pdf.extend_from_slice(b"xref\n0 3\n");
99 pdf.extend_from_slice(b"0000000000 65535 f \r\n");
100 pdf.extend_from_slice(format!("{:010} 00000 n \r\n", obj1_offset).as_bytes());
101 pdf.extend_from_slice(format!("{:010} 00000 n \r\n", obj2_offset).as_bytes());
102 pdf.extend_from_slice(b"trailer\n<< /Size 3 /Root 1 0 R >>\n");
103 pdf.extend_from_slice(format!("startxref\n{}\n%%EOF", xref_offset).as_bytes());
104 pdf
105 }
106
107 #[test]
108 fn test_extract_appearance_with_direct_refs() {
109 let store = build_store();
110 let mut ap_dict = HashMap::new();
111 ap_dict.insert(Name::n(), Object::Reference(CoreObjectId::new(1, 0)));
112 ap_dict.insert(Name::r(), Object::Reference(CoreObjectId::new(2, 0)));
113
114 let ap_obj = Object::Dictionary(ap_dict);
115 let result = extract_appearance(&ap_obj, &store).unwrap();
116 assert_eq!(result.normal, Some(CoreObjectId::new(1, 0)));
117 assert_eq!(result.rollover, Some(CoreObjectId::new(2, 0)));
118 assert!(result.down.is_none());
119 }
120
121 #[test]
122 fn test_extract_appearance_with_state_dict() {
123 let store = build_store();
124 let mut state_dict = HashMap::new();
126 state_dict.insert(
127 Name::from("Yes"),
128 Object::Reference(CoreObjectId::new(1, 0)),
129 );
130
131 let mut ap_dict = HashMap::new();
132 ap_dict.insert(Name::n(), Object::Dictionary(state_dict));
133
134 let ap_obj = Object::Dictionary(ap_dict);
135 let result = extract_appearance(&ap_obj, &store).unwrap();
136 assert!(result.normal.is_some());
138 }
139
140 #[test]
141 fn test_extract_appearance_empty() {
142 let store = build_store();
143 let ap_dict: HashMap<Name, Object> = HashMap::new();
144 let ap_obj = Object::Dictionary(ap_dict);
145 let result = extract_appearance(&ap_obj, &store).unwrap();
146 assert!(result.normal.is_none());
147 assert!(result.rollover.is_none());
148 assert!(result.down.is_none());
149 }
150}