1use folio_core::{ColorPt, FolioError, PdfDate, Rect, Result};
4use folio_cos::{CosDoc, ObjectId, PdfObject};
5use indexmap::IndexMap;
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9pub enum AnnotType {
10 Text,
11 Link,
12 FreeText,
13 Line,
14 Square,
15 Circle,
16 Polygon,
17 PolyLine,
18 Highlight,
19 Underline,
20 Squiggly,
21 StrikeOut,
22 Stamp,
23 Caret,
24 Ink,
25 Popup,
26 FileAttachment,
27 Sound,
28 Movie,
29 Widget,
30 Screen,
31 PrinterMark,
32 TrapNet,
33 Watermark,
34 ThreeD,
35 Redact,
36 Projection,
37 RichMedia,
38 Unknown,
39}
40
41impl AnnotType {
42 pub fn from_name(name: &[u8]) -> Self {
44 match name {
45 b"Text" => Self::Text,
46 b"Link" => Self::Link,
47 b"FreeText" => Self::FreeText,
48 b"Line" => Self::Line,
49 b"Square" => Self::Square,
50 b"Circle" => Self::Circle,
51 b"Polygon" => Self::Polygon,
52 b"PolyLine" => Self::PolyLine,
53 b"Highlight" => Self::Highlight,
54 b"Underline" => Self::Underline,
55 b"Squiggly" => Self::Squiggly,
56 b"StrikeOut" => Self::StrikeOut,
57 b"Stamp" => Self::Stamp,
58 b"Caret" => Self::Caret,
59 b"Ink" => Self::Ink,
60 b"Popup" => Self::Popup,
61 b"FileAttachment" => Self::FileAttachment,
62 b"Sound" => Self::Sound,
63 b"Movie" => Self::Movie,
64 b"Widget" => Self::Widget,
65 b"Screen" => Self::Screen,
66 b"PrinterMark" => Self::PrinterMark,
67 b"TrapNet" => Self::TrapNet,
68 b"Watermark" => Self::Watermark,
69 b"3D" => Self::ThreeD,
70 b"Redact" => Self::Redact,
71 b"Projection" => Self::Projection,
72 b"RichMedia" => Self::RichMedia,
73 _ => Self::Unknown,
74 }
75 }
76
77 pub fn to_name(&self) -> &'static [u8] {
79 match self {
80 Self::Text => b"Text",
81 Self::Link => b"Link",
82 Self::FreeText => b"FreeText",
83 Self::Line => b"Line",
84 Self::Square => b"Square",
85 Self::Circle => b"Circle",
86 Self::Polygon => b"Polygon",
87 Self::PolyLine => b"PolyLine",
88 Self::Highlight => b"Highlight",
89 Self::Underline => b"Underline",
90 Self::Squiggly => b"Squiggly",
91 Self::StrikeOut => b"StrikeOut",
92 Self::Stamp => b"Stamp",
93 Self::Caret => b"Caret",
94 Self::Ink => b"Ink",
95 Self::Popup => b"Popup",
96 Self::FileAttachment => b"FileAttachment",
97 Self::Sound => b"Sound",
98 Self::Movie => b"Movie",
99 Self::Widget => b"Widget",
100 Self::Screen => b"Screen",
101 Self::PrinterMark => b"PrinterMark",
102 Self::TrapNet => b"TrapNet",
103 Self::Watermark => b"Watermark",
104 Self::ThreeD => b"3D",
105 Self::Redact => b"Redact",
106 Self::Projection => b"Projection",
107 Self::RichMedia => b"RichMedia",
108 Self::Unknown => b"Unknown",
109 }
110 }
111
112 pub fn is_markup(&self) -> bool {
114 matches!(
115 self,
116 Self::Text
117 | Self::FreeText
118 | Self::Line
119 | Self::Square
120 | Self::Circle
121 | Self::Polygon
122 | Self::PolyLine
123 | Self::Highlight
124 | Self::Underline
125 | Self::Squiggly
126 | Self::StrikeOut
127 | Self::Stamp
128 | Self::Caret
129 | Self::Ink
130 | Self::Sound
131 | Self::FileAttachment
132 | Self::Redact
133 )
134 }
135}
136
137bitflags::bitflags! {
138 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
140 pub struct AnnotFlags: u32 {
141 const INVISIBLE = 1 << 0;
142 const HIDDEN = 1 << 1;
143 const PRINT = 1 << 2;
144 const NO_ZOOM = 1 << 3;
145 const NO_ROTATE = 1 << 4;
146 const NO_VIEW = 1 << 5;
147 const READ_ONLY = 1 << 6;
148 const LOCKED = 1 << 7;
149 const TOGGLE_NO_VIEW = 1 << 8;
150 const LOCKED_CONTENTS = 1 << 9;
151 }
152}
153
154#[derive(Debug, Clone)]
156pub struct Annot {
157 dict: IndexMap<Vec<u8>, PdfObject>,
159 id: Option<ObjectId>,
161}
162
163impl Annot {
164 pub fn from_dict(dict: IndexMap<Vec<u8>, PdfObject>, id: Option<ObjectId>) -> Self {
166 Self { dict, id }
167 }
168
169 pub fn load(obj_num: u32, doc: &mut CosDoc) -> Result<Self> {
171 let obj = doc
172 .get_object(obj_num)?
173 .ok_or_else(|| FolioError::InvalidObject(format!("Annotation {} not found", obj_num)))?
174 .clone();
175 let dict = obj
176 .as_dict()
177 .ok_or_else(|| FolioError::InvalidObject("Annotation is not a dict".into()))?
178 .clone();
179 Ok(Self {
180 dict,
181 id: Some(ObjectId::new(obj_num, 0)),
182 })
183 }
184
185 pub fn create(annot_type: AnnotType, rect: Rect) -> Self {
187 let mut dict = IndexMap::new();
188 dict.insert(b"Type".to_vec(), PdfObject::Name(b"Annot".to_vec()));
189 dict.insert(
190 b"Subtype".to_vec(),
191 PdfObject::Name(annot_type.to_name().to_vec()),
192 );
193 dict.insert(
194 b"Rect".to_vec(),
195 PdfObject::Array(vec![
196 PdfObject::Real(rect.x1),
197 PdfObject::Real(rect.y1),
198 PdfObject::Real(rect.x2),
199 PdfObject::Real(rect.y2),
200 ]),
201 );
202 Self { dict, id: None }
203 }
204
205 pub fn dict(&self) -> &IndexMap<Vec<u8>, PdfObject> {
207 &self.dict
208 }
209
210 pub fn dict_mut(&mut self) -> &mut IndexMap<Vec<u8>, PdfObject> {
212 &mut self.dict
213 }
214
215 pub fn id(&self) -> Option<ObjectId> {
217 self.id
218 }
219
220 pub fn annot_type(&self) -> AnnotType {
222 self.dict
223 .get(b"Subtype".as_slice())
224 .and_then(|o| o.as_name())
225 .map(AnnotType::from_name)
226 .unwrap_or(AnnotType::Unknown)
227 }
228
229 pub fn rect(&self) -> Rect {
231 extract_rect(&self.dict, b"Rect").unwrap_or_default()
232 }
233
234 pub fn set_rect(&mut self, rect: Rect) {
236 self.dict.insert(
237 b"Rect".to_vec(),
238 PdfObject::Array(vec![
239 PdfObject::Real(rect.x1),
240 PdfObject::Real(rect.y1),
241 PdfObject::Real(rect.x2),
242 PdfObject::Real(rect.y2),
243 ]),
244 );
245 }
246
247 pub fn flags(&self) -> AnnotFlags {
249 let bits = self
250 .dict
251 .get(b"F".as_slice())
252 .and_then(|o| o.as_i64())
253 .unwrap_or(0) as u32;
254 AnnotFlags::from_bits_truncate(bits)
255 }
256
257 pub fn set_flags(&mut self, flags: AnnotFlags) {
259 self.dict
260 .insert(b"F".to_vec(), PdfObject::Integer(flags.bits() as i64));
261 }
262
263 pub fn contents(&self) -> Option<String> {
265 self.dict
266 .get(b"Contents".as_slice())
267 .and_then(|o| o.as_str())
268 .map(|s| decode_text(s))
269 }
270
271 pub fn set_contents(&mut self, text: &str) {
273 self.dict.insert(
274 b"Contents".to_vec(),
275 PdfObject::Str(text.as_bytes().to_vec()),
276 );
277 }
278
279 pub fn name(&self) -> Option<String> {
281 self.dict
282 .get(b"NM".as_slice())
283 .and_then(|o| o.as_str())
284 .map(|s| decode_text(s))
285 }
286
287 pub fn modified_date(&self) -> Option<PdfDate> {
289 self.dict
290 .get(b"M".as_slice())
291 .and_then(|o| o.as_str())
292 .and_then(|s| PdfDate::parse(&decode_text(s)))
293 }
294
295 pub fn color(&self) -> Option<ColorPt> {
297 let arr = self.dict.get(b"C".as_slice())?.as_array()?;
298 match arr.len() {
299 0 => Some(ColorPt::new(0.0, 0.0, 0.0, 0.0)), 1 => Some(ColorPt::gray(arr[0].as_f64()?)),
301 3 => Some(ColorPt::rgb(
302 arr[0].as_f64()?,
303 arr[1].as_f64()?,
304 arr[2].as_f64()?,
305 )),
306 4 => Some(ColorPt::cmyk(
307 arr[0].as_f64()?,
308 arr[1].as_f64()?,
309 arr[2].as_f64()?,
310 arr[3].as_f64()?,
311 )),
312 _ => None,
313 }
314 }
315
316 pub fn set_color(&mut self, color: ColorPt) {
318 self.dict.insert(
319 b"C".to_vec(),
320 PdfObject::Array(vec![
321 PdfObject::Real(color.c0),
322 PdfObject::Real(color.c1),
323 PdfObject::Real(color.c2),
324 ]),
325 );
326 }
327
328 pub fn title(&self) -> Option<String> {
332 self.dict
333 .get(b"T".as_slice())
334 .and_then(|o| o.as_str())
335 .map(|s| decode_text(s))
336 }
337
338 pub fn subject(&self) -> Option<String> {
340 self.dict
341 .get(b"Subj".as_slice())
342 .and_then(|o| o.as_str())
343 .map(|s| decode_text(s))
344 }
345
346 pub fn opacity(&self) -> f64 {
348 self.dict
349 .get(b"CA".as_slice())
350 .and_then(|o| o.as_f64())
351 .unwrap_or(1.0)
352 }
353
354 pub fn creation_date(&self) -> Option<PdfDate> {
356 self.dict
357 .get(b"CreationDate".as_slice())
358 .and_then(|o| o.as_str())
359 .and_then(|s| PdfDate::parse(&decode_text(s)))
360 }
361
362 pub fn popup(&self) -> Option<ObjectId> {
364 self.dict
365 .get(b"Popup".as_slice())
366 .and_then(|o| o.as_reference())
367 }
368
369 pub fn to_pdf_object(&self) -> PdfObject {
371 PdfObject::Dict(self.dict.clone())
372 }
373}
374
375fn extract_rect(dict: &IndexMap<Vec<u8>, PdfObject>, key: &[u8]) -> Option<Rect> {
377 let arr = dict.get(key)?.as_array()?;
378 if arr.len() >= 4 {
379 Some(Rect::new(
380 arr[0].as_f64()?,
381 arr[1].as_f64()?,
382 arr[2].as_f64()?,
383 arr[3].as_f64()?,
384 ))
385 } else {
386 None
387 }
388}
389
390fn decode_text(data: &[u8]) -> String {
392 if data.len() >= 2 && data[0] == 0xFE && data[1] == 0xFF {
393 let mut chars = Vec::new();
394 let mut i = 2;
395 while i + 1 < data.len() {
396 chars.push(((data[i] as u16) << 8) | (data[i + 1] as u16));
397 i += 2;
398 }
399 String::from_utf16_lossy(&chars)
400 } else {
401 String::from_utf8_lossy(data).into_owned()
402 }
403}
404
405#[cfg(test)]
406mod tests {
407 use super::*;
408
409 #[test]
410 fn test_create_annotation() {
411 let annot = Annot::create(AnnotType::Highlight, Rect::new(100.0, 200.0, 300.0, 220.0));
412 assert_eq!(annot.annot_type(), AnnotType::Highlight);
413 assert_eq!(annot.rect(), Rect::new(100.0, 200.0, 300.0, 220.0));
414 assert!(annot.annot_type().is_markup());
415 }
416
417 #[test]
418 fn test_annotation_flags() {
419 let mut annot = Annot::create(AnnotType::Text, Rect::new(0.0, 0.0, 50.0, 50.0));
420 annot.set_flags(AnnotFlags::PRINT | AnnotFlags::LOCKED);
421 let flags = annot.flags();
422 assert!(flags.contains(AnnotFlags::PRINT));
423 assert!(flags.contains(AnnotFlags::LOCKED));
424 assert!(!flags.contains(AnnotFlags::HIDDEN));
425 }
426
427 #[test]
428 fn test_annotation_properties() {
429 let mut annot = Annot::create(AnnotType::Text, Rect::new(0.0, 0.0, 50.0, 50.0));
430 annot.set_contents("Hello World");
431 annot.set_color(ColorPt::rgb(1.0, 0.0, 0.0));
432 assert_eq!(annot.contents(), Some("Hello World".into()));
433 }
434
435 #[test]
436 fn test_annot_type_names() {
437 assert_eq!(AnnotType::from_name(b"Highlight"), AnnotType::Highlight);
438 assert_eq!(AnnotType::from_name(b"Widget"), AnnotType::Widget);
439 assert_eq!(AnnotType::from_name(b"Unknown"), AnnotType::Unknown);
440 assert!(AnnotType::Text.is_markup());
441 assert!(!AnnotType::Link.is_markup());
442 assert!(!AnnotType::Widget.is_markup());
443 }
444}