1use std::collections::HashMap;
9
10use rpdfium_core::Name;
11use rpdfium_doc::{Annotation, AnnotationFlags, AnnotationType, generate_annotation_appearance};
12use rpdfium_parser::object::{Object, ObjectId, StreamData};
13
14use crate::cpdf_pagecontentgenerator::{
15 ResourceCollector, build_resource_dict, generate_content_stream,
16};
17use crate::document::EditDocument;
18use crate::error::EditError;
19use crate::page_object::PageObject;
20
21#[derive(Debug, Clone)]
23pub struct AnnotationSpec {
24 pub subtype: AnnotationType,
26 pub rect: [f32; 4],
28 pub contents: Option<String>,
30 pub color: Option<Vec<f32>>,
32 pub flags: AnnotationFlags,
34 pub quad_points: Option<Vec<f32>>,
36}
37
38pub struct AnnotationBuilder {
40 spec: AnnotationSpec,
41}
42
43impl AnnotationBuilder {
44 pub fn new(subtype: AnnotationType, rect: [f32; 4]) -> Self {
46 Self {
47 spec: AnnotationSpec {
48 subtype,
49 rect,
50 contents: None,
51 color: None,
52 flags: AnnotationFlags::default(),
53 quad_points: None,
54 },
55 }
56 }
57
58 pub fn contents(mut self, text: &str) -> Self {
60 self.spec.contents = Some(text.to_string());
61 self
62 }
63
64 pub fn color(mut self, color: Vec<f32>) -> Self {
66 self.spec.color = Some(color);
67 self
68 }
69
70 pub fn flags(mut self, flags: AnnotationFlags) -> Self {
72 self.spec.flags = flags;
73 self
74 }
75
76 pub fn quad_points(mut self, points: Vec<f32>) -> Self {
78 self.spec.quad_points = Some(points);
79 self
80 }
81
82 pub fn build(self) -> AnnotationSpec {
84 self.spec
85 }
86}
87
88#[derive(Debug, Clone, Default)]
90pub struct AnnotationUpdates {
91 pub contents: Option<String>,
93 pub color: Option<Vec<f32>>,
95 pub flags: Option<AnnotationFlags>,
97 pub rect: Option<[f32; 4]>,
99 pub open: Option<bool>,
101 pub subject: Option<String>,
105 pub author: Option<String>,
109 pub border_width: Option<f32>,
116
117 pub border_style: Option<(f32, f32, f32)>,
123
124 pub color_rgba: Option<(u8, u8, u8, u8)>,
131
132 pub attachment_points: Option<Vec<[f32; 8]>>,
140
141 pub ink_list: Option<Vec<Vec<f32>>>,
148
149 pub extra_string: Option<(String, String)>,
156
157 pub vertices: Option<Vec<[f32; 2]>>,
164
165 pub uri_action: Option<String>,
172}
173
174impl AnnotationUpdates {
175 pub fn append_attachment_points(&mut self, quad: [f32; 8]) -> &mut Self {
186 self.attachment_points
187 .get_or_insert_with(Vec::new)
188 .push(quad);
189 self
190 }
191}
192
193impl EditDocument {
194 pub fn add_annotation(
198 &mut self,
199 page_index: usize,
200 spec: AnnotationSpec,
201 ) -> Result<ObjectId, EditError> {
202 let count = self.page_count();
203 if page_index >= count {
204 return Err(EditError::PageOutOfRange {
205 index: page_index,
206 count,
207 });
208 }
209
210 let mut annot_dict = HashMap::new();
212 annot_dict.insert(Name::r#type(), Object::Name(Name::annot()));
213 annot_dict.insert(
214 Name::subtype(),
215 Object::Name(Name::from_bytes(
216 annotation_type_name(&spec.subtype).to_vec(),
217 )),
218 );
219 annot_dict.insert(
220 Name::rect(),
221 Object::Array(vec![
222 Object::Real(spec.rect[0] as f64),
223 Object::Real(spec.rect[1] as f64),
224 Object::Real(spec.rect[2] as f64),
225 Object::Real(spec.rect[3] as f64),
226 ]),
227 );
228
229 if let Some(text) = &spec.contents {
230 annot_dict.insert(
231 Name::contents(),
232 Object::String(rpdfium_core::PdfString::from_bytes(
233 text.as_bytes().to_vec(),
234 )),
235 );
236 }
237
238 if let Some(color) = &spec.color {
239 let color_arr: Vec<Object> = color.iter().map(|&c| Object::Real(c as f64)).collect();
240 annot_dict.insert(Name::c(), Object::Array(color_arr));
241 }
242
243 let flags_val = spec.flags.bits();
244 if flags_val != 0 {
245 annot_dict.insert(Name::f(), Object::Integer(flags_val as i64));
246 }
247
248 if let Some(qp) = &spec.quad_points {
249 let qp_arr: Vec<Object> = qp.iter().map(|&p| Object::Real(p as f64)).collect();
250 annot_dict.insert(Name::quad_points(), Object::Array(qp_arr));
251 }
252
253 let page_id = self.page_ids()[page_index];
255 annot_dict.insert(Name::p(), Object::Reference(page_id));
256
257 let temp_annot = Annotation {
260 subtype: spec.subtype,
261 rect: spec.rect,
262 contents: spec.contents.clone(),
263 flags: spec.flags,
264 color: spec.color.clone(),
265 name: None,
266 appearance: None,
267 border: None,
268 action: None,
269 destination: None,
270 subtype_data: Default::default(),
271 mk: None,
272 file_spec: None,
273 parent_ref: None,
274 object_id: None,
275 open: None,
276 ap_n_bytes: None,
277 ap_r_bytes: None,
278 ap_d_bytes: None,
279 irt_ref: None,
280 field_name: None,
281 alternate_name: None,
282 field_value: None,
283 form_field_flags: None,
284 additional_actions: None,
285 form_field_type: None,
286 options: None,
287 };
288 if let Some(ap_bytes) = generate_annotation_appearance(&temp_annot) {
289 let w = (spec.rect[2] - spec.rect[0]).abs();
291 let h = (spec.rect[3] - spec.rect[1]).abs();
292 let mut ap_stream_dict = HashMap::new();
293 ap_stream_dict.insert(Name::r#type(), Object::Name(Name::x_object()));
294 ap_stream_dict.insert(Name::subtype(), Object::Name(Name::form()));
295 ap_stream_dict.insert(
296 Name::b_box(),
297 Object::Array(vec![
298 Object::Real(0.0),
299 Object::Real(0.0),
300 Object::Real(w as f64),
301 Object::Real(h as f64),
302 ]),
303 );
304 let ap_stream = Object::Stream {
305 dict: ap_stream_dict,
306 data: StreamData::Decoded { data: ap_bytes },
307 };
308 let ap_id = self.add_object(ap_stream);
309
310 let mut ap_dict = HashMap::new();
312 ap_dict.insert(Name::n(), Object::Reference(ap_id));
313 annot_dict.insert(Name::ap(), Object::Dictionary(ap_dict));
314 }
315
316 let annot_id = self.add_object(Object::Dictionary(annot_dict));
317
318 let page = self.get_mut(page_id)?;
320 if let Object::Dictionary(dict) = page {
321 let annots = dict
322 .entry(Name::annots())
323 .or_insert_with(|| Object::Array(Vec::new()));
324 if let Object::Array(arr) = annots {
325 arr.push(Object::Reference(annot_id));
326 }
327 }
328
329 Ok(annot_id)
330 }
331
332 pub fn delete_annotation(
334 &mut self,
335 page_index: usize,
336 annot_id: ObjectId,
337 ) -> Result<(), EditError> {
338 let count = self.page_count();
339 if page_index >= count {
340 return Err(EditError::PageOutOfRange {
341 index: page_index,
342 count,
343 });
344 }
345
346 let page_id = self.page_ids()[page_index];
347 let page = self.get_mut(page_id)?;
348 if let Object::Dictionary(dict) = page {
349 if let Some(Object::Array(arr)) = dict.get_mut(&Name::annots()) {
350 arr.retain(|obj| {
351 if let Object::Reference(id) = obj {
352 *id != annot_id
353 } else {
354 true
355 }
356 });
357 }
358 }
359
360 self.delete_object(annot_id);
361 Ok(())
362 }
363
364 pub fn update_annotation(
370 &mut self,
371 annot_id: ObjectId,
372 updates: AnnotationUpdates,
373 ) -> Result<(), EditError> {
374 {
376 let annot = self.get_mut(annot_id)?;
377 let dict = annot
378 .as_dict_mut()
379 .ok_or(EditError::AnnotationNotFound(annot_id))?;
380
381 if let Some(text) = &updates.contents {
382 dict.insert(
383 Name::contents(),
384 Object::String(rpdfium_core::PdfString::from_bytes(
385 text.as_bytes().to_vec(),
386 )),
387 );
388 }
389
390 if let Some(color) = &updates.color {
391 let color_arr: Vec<Object> =
392 color.iter().map(|&c| Object::Real(c as f64)).collect();
393 dict.insert(Name::c(), Object::Array(color_arr));
394 }
395
396 if let Some(flags) = &updates.flags {
397 dict.insert(Name::f(), Object::Integer(flags.bits() as i64));
398 }
399
400 if let Some(rect) = &updates.rect {
401 dict.insert(
402 Name::rect(),
403 Object::Array(vec![
404 Object::Real(rect[0] as f64),
405 Object::Real(rect[1] as f64),
406 Object::Real(rect[2] as f64),
407 Object::Real(rect[3] as f64),
408 ]),
409 );
410 }
411
412 if let Some(open) = updates.open {
413 dict.insert(Name::open(), Object::Boolean(open));
414 }
415
416 if let Some(ref subject) = updates.subject {
417 dict.insert(
418 Name::subj(),
419 Object::String(rpdfium_core::PdfString::from_bytes(
420 subject.as_bytes().to_vec(),
421 )),
422 );
423 }
424
425 if let Some(ref author) = updates.author {
426 dict.insert(
427 Name::t(),
428 Object::String(rpdfium_core::PdfString::from_bytes(
429 author.as_bytes().to_vec(),
430 )),
431 );
432 }
433
434 if let Some(width) = updates.border_width {
435 let mut bs_dict = HashMap::new();
437 bs_dict.insert(Name::from_bytes(b"W".to_vec()), Object::Real(width as f64));
438 dict.insert(Name::bs(), Object::Dictionary(bs_dict));
439 }
440
441 if let Some((h, v, w)) = updates.border_style {
443 dict.insert(
444 Name::border(),
445 Object::Array(vec![
446 Object::Real(h as f64),
447 Object::Real(v as f64),
448 Object::Real(w as f64),
449 ]),
450 );
451 }
452
453 if let Some((r, g, b, _a)) = updates.color_rgba {
455 let to_f = |byte: u8| Object::Real(byte as f64 / 255.0);
456 dict.insert(Name::c(), Object::Array(vec![to_f(r), to_f(g), to_f(b)]));
457 }
458
459 if let Some(ref groups) = updates.attachment_points {
461 let flat: Vec<Object> = groups
462 .iter()
463 .flat_map(|g| g.iter().map(|&f| Object::Real(f as f64)))
464 .collect();
465 dict.insert(Name::quad_points(), Object::Array(flat));
466 }
467
468 if let Some(ref strokes) = updates.ink_list {
470 let outer: Vec<Object> = strokes
471 .iter()
472 .map(|stroke| {
473 Object::Array(stroke.iter().map(|&f| Object::Real(f as f64)).collect())
474 })
475 .collect();
476 dict.insert(Name::ink_list(), Object::Array(outer));
477 }
478
479 if let Some((ref key, ref value)) = updates.extra_string {
481 dict.insert(
482 Name::from_bytes(key.as_bytes().to_vec()),
483 Object::String(rpdfium_core::PdfString::from_bytes(
484 value.as_bytes().to_vec(),
485 )),
486 );
487 }
488
489 if let Some(ref verts) = updates.vertices {
491 let flat: Vec<Object> = verts
492 .iter()
493 .flat_map(|v| [Object::Real(v[0] as f64), Object::Real(v[1] as f64)])
494 .collect();
495 dict.insert(Name::vertices(), Object::Array(flat));
496 }
497
498 if let Some(ref uri) = updates.uri_action {
500 let mut action_dict = HashMap::new();
501 action_dict.insert(Name::s(), Object::Name(Name::uri()));
502 action_dict.insert(
503 Name::uri(),
504 Object::String(rpdfium_core::PdfString::from_bytes(uri.as_bytes().to_vec())),
505 );
506 dict.insert(Name::a(), Object::Dictionary(action_dict));
507 }
508 }
509
510 regenerate_annotation_ap(self, annot_id)?;
512
513 Ok(())
514 }
515
516 pub fn append_annotation_object(
524 &mut self,
525 annot_id: ObjectId,
526 object: PageObject,
527 ) -> Result<(), EditError> {
528 self.ap_objects.entry(annot_id).or_default().push(object);
529 Ok(())
530 }
531
532 #[inline]
536 pub fn annot_append_object(
537 &mut self,
538 annot_id: ObjectId,
539 object: PageObject,
540 ) -> Result<(), EditError> {
541 self.append_annotation_object(annot_id, object)
542 }
543
544 #[deprecated(note = "use `annot_append_object()` — matches upstream `FPDFAnnot_AppendObject`")]
546 #[inline]
547 pub fn append_object(
548 &mut self,
549 annot_id: ObjectId,
550 object: PageObject,
551 ) -> Result<(), EditError> {
552 self.append_annotation_object(annot_id, object)
553 }
554
555 pub fn annotation_object_count(&self, annot_id: ObjectId) -> usize {
561 self.ap_objects.get(&annot_id).map_or(0, Vec::len)
562 }
563
564 #[deprecated(
569 note = "use `annot_get_object_count()` — matches upstream `FPDFAnnot_GetObjectCount`"
570 )]
571 #[inline]
572 pub fn count_annotation_objects(&self, annot_id: ObjectId) -> usize {
573 self.annotation_object_count(annot_id)
574 }
575
576 #[inline]
580 pub fn annot_get_object_count(&self, annot_id: ObjectId) -> usize {
581 self.annotation_object_count(annot_id)
582 }
583
584 #[deprecated(
586 note = "use `annot_get_object_count()` — matches upstream `FPDFAnnot_GetObjectCount`"
587 )]
588 #[inline]
589 pub fn get_object_count(&self, annot_id: ObjectId) -> usize {
590 self.annotation_object_count(annot_id)
591 }
592
593 pub fn annotation_object_at(&self, annot_id: ObjectId, index: usize) -> Option<&PageObject> {
599 self.ap_objects.get(&annot_id)?.get(index)
600 }
601
602 #[deprecated(note = "use `get_object()` — matches upstream FPDFAnnot_GetObject")]
607 #[inline]
608 pub fn get_annotation_object(&self, annot_id: ObjectId, index: usize) -> Option<&PageObject> {
609 self.annotation_object_at(annot_id, index)
610 }
611
612 #[deprecated(note = "use `doc.annot_objects(annot_id).get_object(index)` — \
625 namespaced via AnnotObjectCtx to avoid collision with FPDFPage_GetObject")]
626 #[inline]
627 pub fn get_object(&self, annot_id: ObjectId, index: usize) -> Option<&PageObject> {
628 self.annotation_object_at(annot_id, index)
629 }
630
631 pub fn annotation_object_at_mut(
635 &mut self,
636 annot_id: ObjectId,
637 index: usize,
638 ) -> Option<&mut PageObject> {
639 self.ap_objects.get_mut(&annot_id)?.get_mut(index)
640 }
641
642 #[deprecated(note = "use `annotation_object_at_mut()` — no exact upstream counterpart")]
647 #[inline]
648 pub fn get_annotation_object_mut(
649 &mut self,
650 annot_id: ObjectId,
651 index: usize,
652 ) -> Option<&mut PageObject> {
653 self.annotation_object_at_mut(annot_id, index)
654 }
655
656 pub fn remove_annotation_object(
663 &mut self,
664 annot_id: ObjectId,
665 index: usize,
666 ) -> Result<(), EditError> {
667 let list = self
668 .ap_objects
669 .get_mut(&annot_id)
670 .ok_or_else(|| EditError::NotSupported("annotation has no AP objects".into()))?;
671 if index >= list.len() {
672 return Err(EditError::NotSupported("index out of bounds".into()));
673 }
674 list.remove(index);
675 Ok(())
676 }
677
678 #[deprecated(note = "use `doc.annot_objects_mut(annot_id).remove_object(index)` — \
691 namespaced via AnnotObjectCtxMut to avoid collision with FPDFPage_RemoveObject")]
692 #[inline]
693 pub fn remove_object(&mut self, annot_id: ObjectId, index: usize) -> Result<(), EditError> {
694 self.remove_annotation_object(annot_id, index)
695 }
696
697 pub fn update_annotation_ap(&mut self, annot_id: ObjectId) -> Result<(), EditError> {
708 let objects = self
711 .ap_objects
712 .get(&annot_id)
713 .ok_or_else(|| EditError::NotSupported("annotation has no AP objects".into()))?
714 .clone();
715
716 let mut resources = ResourceCollector::new();
718 let content = generate_content_stream(&objects, &mut resources);
719
720 let rect = {
722 let annot = self.resolve(annot_id)?;
723 let dict = annot
724 .as_dict()
725 .ok_or(EditError::AnnotationNotFound(annot_id))?;
726 dict.get(&Name::rect())
727 .and_then(|o| o.as_array())
728 .map(|arr| {
729 let get = |i: usize| arr.get(i).and_then(|v| v.as_f64()).unwrap_or(0.0) as f32;
730 [get(0), get(1), get(2), get(3)]
731 })
732 .unwrap_or([0.0; 4])
733 };
734
735 let w = (rect[2] - rect[0]).abs();
736 let h = (rect[3] - rect[1]).abs();
737
738 let mut ap_stream_dict = HashMap::new();
740 ap_stream_dict.insert(
741 Name::r#type(),
742 Object::Name(Name::from_bytes(b"XObject".to_vec())),
743 );
744 ap_stream_dict.insert(
745 Name::subtype(),
746 Object::Name(Name::from_bytes(b"Form".to_vec())),
747 );
748 ap_stream_dict.insert(
749 Name::from_bytes(b"BBox".to_vec()),
750 Object::Array(vec![
751 Object::Real(0.0),
752 Object::Real(0.0),
753 Object::Real(w as f64),
754 Object::Real(h as f64),
755 ]),
756 );
757
758 let res_obj = build_resource_dict(&resources);
760 let has_resources = matches!(&res_obj, Object::Dictionary(d) if !d.is_empty());
761 if has_resources {
762 ap_stream_dict.insert(Name::from_bytes(b"Resources".to_vec()), res_obj);
763 }
764
765 let ap_stream = Object::Stream {
766 dict: ap_stream_dict,
767 data: StreamData::Decoded { data: content },
768 };
769 let ap_id = self.add_object(ap_stream);
770
771 let annot = self.get_mut(annot_id)?;
774 if let Object::Dictionary(dict) = annot {
775 let ap_key = Name::from_bytes(b"AP".to_vec());
776 let n_key = Name::from_bytes(b"N".to_vec());
777 match dict.get_mut(&ap_key) {
778 Some(Object::Dictionary(existing)) => {
779 existing.insert(n_key, Object::Reference(ap_id));
780 }
781 _ => {
782 let mut ap_dict = HashMap::new();
783 ap_dict.insert(n_key, Object::Reference(ap_id));
784 dict.insert(ap_key, Object::Dictionary(ap_dict));
785 }
786 }
787 }
788
789 Ok(())
790 }
791
792 #[inline]
796 pub fn annot_update_object(&mut self, annot_id: ObjectId) -> Result<(), EditError> {
797 self.update_annotation_ap(annot_id)
798 }
799
800 #[deprecated(note = "use `annot_update_object()` — matches upstream `FPDFAnnot_UpdateObject`")]
802 #[inline]
803 pub fn update_object(&mut self, annot_id: ObjectId) -> Result<(), EditError> {
804 self.update_annotation_ap(annot_id)
805 }
806
807 pub fn add_annotation_file_attachment(
814 &mut self,
815 _annot_id: rpdfium_parser::ObjectId,
816 _name: &str,
817 ) -> Result<(), crate::error::EditError> {
818 Err(crate::error::EditError::NotSupported(
819 "add_annotation_file_attachment: file-attachment annotation mutation not yet implemented"
820 .into(),
821 ))
822 }
823
824 #[inline]
828 pub fn annot_add_file_attachment(
829 &mut self,
830 annot_id: rpdfium_parser::ObjectId,
831 name: &str,
832 ) -> Result<(), crate::error::EditError> {
833 self.add_annotation_file_attachment(annot_id, name)
834 }
835
836 #[deprecated(
838 note = "use `annot_add_file_attachment()` — matches upstream `FPDFAnnot_AddFileAttachment`"
839 )]
840 #[inline]
841 pub fn add_file_attachment(
842 &mut self,
843 annot_id: rpdfium_parser::ObjectId,
844 name: &str,
845 ) -> Result<(), crate::error::EditError> {
846 self.add_annotation_file_attachment(annot_id, name)
847 }
848
849 pub fn annotation_set_ap(
861 &mut self,
862 annot_id: ObjectId,
863 mode: rpdfium_doc::AppearanceMode,
864 content: &[u8],
865 ) -> Result<(), EditError> {
866 let mode_key = ap_mode_name(mode);
867
868 let ap_stream = Object::Stream {
870 dict: HashMap::new(),
871 data: StreamData::Decoded {
872 data: content.to_vec(),
873 },
874 };
875 let ap_id = self.add_object(ap_stream);
876
877 let ap_key = Name::ap();
879 let n_key = Name::from_bytes(mode_key.to_vec());
880 let annot = self.get_mut(annot_id)?;
881 if let Object::Dictionary(dict) = annot {
882 match dict.get_mut(&ap_key) {
883 Some(Object::Dictionary(existing)) => {
884 existing.insert(n_key, Object::Reference(ap_id));
885 }
886 _ => {
887 let mut ap_dict = HashMap::new();
888 ap_dict.insert(n_key, Object::Reference(ap_id));
889 dict.insert(ap_key, Object::Dictionary(ap_dict));
890 }
891 }
892 }
893 Ok(())
894 }
895
896 #[inline]
900 pub fn annot_set_ap(
901 &mut self,
902 annot_id: ObjectId,
903 mode: rpdfium_doc::AppearanceMode,
904 content: &[u8],
905 ) -> Result<(), EditError> {
906 self.annotation_set_ap(annot_id, mode, content)
907 }
908
909 #[deprecated(note = "use `annot_set_ap()` — matches upstream `FPDFAnnot_SetAP`")]
911 #[inline]
912 pub fn set_ap(
913 &mut self,
914 annot_id: ObjectId,
915 mode: rpdfium_doc::AppearanceMode,
916 content: &[u8],
917 ) -> Result<(), EditError> {
918 self.annotation_set_ap(annot_id, mode, content)
919 }
920
921 pub fn annotation_get_ap(
926 &self,
927 annot_id: ObjectId,
928 mode: rpdfium_doc::AppearanceMode,
929 ) -> Result<Option<Vec<u8>>, EditError> {
930 let mode_key = ap_mode_name(mode);
931 let ap_key = Name::ap();
932 let n_key = Name::from_bytes(mode_key.to_vec());
933
934 let ap_ref: ObjectId = {
937 let annot = self.resolve(annot_id)?;
938 let dict = annot
939 .as_dict()
940 .ok_or(EditError::AnnotationNotFound(annot_id))?;
941 let ap_dict = match dict.get(&ap_key) {
942 Some(Object::Dictionary(d)) => d,
943 _ => return Ok(None),
944 };
945 match ap_dict.get(&n_key) {
946 Some(Object::Reference(id)) => *id,
947 _ => return Ok(None),
948 }
949 }; let ap_obj = self.resolve(ap_ref)?.clone();
952 match &ap_obj {
953 Object::Stream { data, .. } => match data {
954 StreamData::Decoded { data } => Ok(Some(data.clone())),
955 StreamData::Raw { .. } => self.decode_stream(&ap_obj).map(Some),
956 },
957 _ => Ok(None),
958 }
959 }
960
961 #[inline]
965 pub fn annot_get_ap(
966 &self,
967 annot_id: ObjectId,
968 mode: rpdfium_doc::AppearanceMode,
969 ) -> Result<Option<Vec<u8>>, EditError> {
970 self.annotation_get_ap(annot_id, mode)
971 }
972
973 #[deprecated(note = "use `annot_get_ap()` — matches upstream `FPDFAnnot_GetAP`")]
975 #[inline]
976 pub fn get_ap(
977 &self,
978 annot_id: ObjectId,
979 mode: rpdfium_doc::AppearanceMode,
980 ) -> Result<Option<Vec<u8>>, EditError> {
981 self.annotation_get_ap(annot_id, mode)
982 }
983}
984
985fn ap_mode_name(mode: rpdfium_doc::AppearanceMode) -> &'static [u8] {
987 match mode {
988 rpdfium_doc::AppearanceMode::Normal => b"N",
989 rpdfium_doc::AppearanceMode::Rollover => b"R",
990 rpdfium_doc::AppearanceMode::Down => b"D",
991 }
992}
993
994fn annotation_type_name(ty: &AnnotationType) -> &'static [u8] {
996 match ty {
997 AnnotationType::Text => b"Text",
998 AnnotationType::Link => b"Link",
999 AnnotationType::FreeText => b"FreeText",
1000 AnnotationType::Line => b"Line",
1001 AnnotationType::Square => b"Square",
1002 AnnotationType::Circle => b"Circle",
1003 AnnotationType::Polygon => b"Polygon",
1004 AnnotationType::PolyLine => b"PolyLine",
1005 AnnotationType::Highlight => b"Highlight",
1006 AnnotationType::Underline => b"Underline",
1007 AnnotationType::Squiggly => b"Squiggly",
1008 AnnotationType::StrikeOut => b"StrikeOut",
1009 AnnotationType::Stamp => b"Stamp",
1010 AnnotationType::Caret => b"Caret",
1011 AnnotationType::Ink => b"Ink",
1012 AnnotationType::Popup => b"Popup",
1013 AnnotationType::FileAttachment => b"FileAttachment",
1014 AnnotationType::Sound => b"Sound",
1015 AnnotationType::Movie => b"Movie",
1016 AnnotationType::Widget => b"Widget",
1017 AnnotationType::Screen => b"Screen",
1018 AnnotationType::PrinterMark => b"PrinterMark",
1019 AnnotationType::TrapNet => b"TrapNet",
1020 AnnotationType::Watermark => b"Watermark",
1021 AnnotationType::ThreeD => b"3D",
1022 AnnotationType::RichMedia => b"RichMedia",
1023 AnnotationType::XFAWidget => b"XFAWidget",
1024 AnnotationType::Redact => b"Redact",
1025 AnnotationType::Other => b"Unknown",
1026 }
1027}
1028
1029fn regenerate_annotation_ap(doc: &mut EditDocument, annot_id: ObjectId) -> Result<(), EditError> {
1034 let (subtype, rect, contents, color, flags) = {
1036 let annot = doc.resolve(annot_id)?;
1037 let dict = annot
1038 .as_dict()
1039 .ok_or(EditError::AnnotationNotFound(annot_id))?;
1040
1041 let subtype = dict
1042 .get(&Name::subtype())
1043 .and_then(|o| o.as_name())
1044 .map(|n| name_to_annotation_type(n.as_bytes()))
1045 .unwrap_or(AnnotationType::Other);
1046
1047 let rect = dict
1048 .get(&Name::rect())
1049 .and_then(|o| o.as_array())
1050 .map(|arr| {
1051 let get = |i: usize| arr.get(i).and_then(|v| v.as_f64()).unwrap_or(0.0) as f32;
1052 [get(0), get(1), get(2), get(3)]
1053 })
1054 .unwrap_or([0.0; 4]);
1055
1056 let contents = dict
1057 .get(&Name::contents())
1058 .and_then(|o| o.as_string())
1059 .map(|s| String::from_utf8_lossy(s.as_bytes()).into_owned());
1060
1061 let color = dict.get(&Name::c()).and_then(|o| o.as_array()).map(|arr| {
1062 arr.iter()
1063 .filter_map(|v| v.as_f64().map(|f| f as f32))
1064 .collect::<Vec<f32>>()
1065 });
1066
1067 let flags_val = dict.get(&Name::f()).and_then(|o| o.as_i64()).unwrap_or(0);
1068 let flags = AnnotationFlags::from_bits(flags_val as u32);
1069
1070 (subtype, rect, contents, color, flags)
1071 };
1072
1073 let temp_annot = Annotation {
1075 subtype,
1076 rect,
1077 contents,
1078 flags,
1079 color,
1080 name: None,
1081 appearance: None,
1082 border: None,
1083 action: None,
1084 destination: None,
1085 subtype_data: Default::default(),
1086 mk: None,
1087 file_spec: None,
1088 parent_ref: None,
1089 object_id: None,
1090 open: None,
1091 ap_n_bytes: None,
1092 ap_r_bytes: None,
1093 ap_d_bytes: None,
1094 irt_ref: None,
1095 field_name: None,
1096 alternate_name: None,
1097 field_value: None,
1098 form_field_flags: None,
1099 additional_actions: None,
1100 form_field_type: None,
1101 options: None,
1102 };
1103
1104 if let Some(ap_bytes) = generate_annotation_appearance(&temp_annot) {
1105 let w = (rect[2] - rect[0]).abs();
1106 let h = (rect[3] - rect[1]).abs();
1107 let mut ap_stream_dict = HashMap::new();
1108 ap_stream_dict.insert(
1109 Name::r#type(),
1110 Object::Name(Name::from_bytes(b"XObject".to_vec())),
1111 );
1112 ap_stream_dict.insert(
1113 Name::subtype(),
1114 Object::Name(Name::from_bytes(b"Form".to_vec())),
1115 );
1116 ap_stream_dict.insert(
1117 Name::from_bytes(b"BBox".to_vec()),
1118 Object::Array(vec![
1119 Object::Real(0.0),
1120 Object::Real(0.0),
1121 Object::Real(w as f64),
1122 Object::Real(h as f64),
1123 ]),
1124 );
1125 let ap_stream = Object::Stream {
1126 dict: ap_stream_dict,
1127 data: StreamData::Decoded { data: ap_bytes },
1128 };
1129 let ap_id = doc.add_object(ap_stream);
1130
1131 let annot = doc.get_mut(annot_id)?;
1133 if let Object::Dictionary(dict) = annot {
1134 let ap_key = Name::from_bytes(b"AP".to_vec());
1135 let n_key = Name::from_bytes(b"N".to_vec());
1136 match dict.get_mut(&ap_key) {
1137 Some(Object::Dictionary(existing)) => {
1138 existing.insert(n_key, Object::Reference(ap_id));
1139 }
1140 _ => {
1141 let mut ap_dict = HashMap::new();
1142 ap_dict.insert(n_key, Object::Reference(ap_id));
1143 dict.insert(ap_key, Object::Dictionary(ap_dict));
1144 }
1145 }
1146 }
1147 }
1148
1149 Ok(())
1150}
1151
1152fn name_to_annotation_type(name: &[u8]) -> AnnotationType {
1154 match name {
1155 b"Text" => AnnotationType::Text,
1156 b"Link" => AnnotationType::Link,
1157 b"FreeText" => AnnotationType::FreeText,
1158 b"Line" => AnnotationType::Line,
1159 b"Square" => AnnotationType::Square,
1160 b"Circle" => AnnotationType::Circle,
1161 b"Polygon" => AnnotationType::Polygon,
1162 b"PolyLine" => AnnotationType::PolyLine,
1163 b"Highlight" => AnnotationType::Highlight,
1164 b"Underline" => AnnotationType::Underline,
1165 b"Squiggly" => AnnotationType::Squiggly,
1166 b"StrikeOut" => AnnotationType::StrikeOut,
1167 b"Stamp" => AnnotationType::Stamp,
1168 b"Caret" => AnnotationType::Caret,
1169 b"Ink" => AnnotationType::Ink,
1170 b"Popup" => AnnotationType::Popup,
1171 b"FileAttachment" => AnnotationType::FileAttachment,
1172 b"Sound" => AnnotationType::Sound,
1173 b"Movie" => AnnotationType::Movie,
1174 b"Widget" => AnnotationType::Widget,
1175 b"Screen" => AnnotationType::Screen,
1176 b"PrinterMark" => AnnotationType::PrinterMark,
1177 b"TrapNet" => AnnotationType::TrapNet,
1178 b"Watermark" => AnnotationType::Watermark,
1179 b"3D" => AnnotationType::ThreeD,
1180 b"RichMedia" => AnnotationType::RichMedia,
1181 b"XFAWidget" => AnnotationType::XFAWidget,
1182 b"Redact" => AnnotationType::Redact,
1183 _ => AnnotationType::Other,
1184 }
1185}
1186
1187#[cfg(test)]
1188mod tests {
1189 use super::*;
1190 use rpdfium_core::Rect;
1191
1192 #[test]
1193 fn test_builder_creates_spec() {
1194 let spec = AnnotationBuilder::new(AnnotationType::Highlight, [10.0, 20.0, 100.0, 50.0])
1195 .contents("Test annotation")
1196 .color(vec![1.0, 0.0, 0.0])
1197 .build();
1198
1199 assert!(matches!(spec.subtype, AnnotationType::Highlight));
1200 assert_eq!(spec.rect, [10.0, 20.0, 100.0, 50.0]);
1201 assert_eq!(spec.contents.as_deref(), Some("Test annotation"));
1202 assert_eq!(spec.color.as_ref().map(|c| c.len()), Some(3));
1203 }
1204
1205 #[test]
1206 fn test_add_annotation_to_page() {
1207 let mut doc = EditDocument::new_blank();
1208 doc.add_page(Rect::new(0.0, 0.0, 612.0, 792.0));
1209
1210 let spec = AnnotationBuilder::new(AnnotationType::Text, [10.0, 10.0, 50.0, 50.0])
1211 .contents("Hello")
1212 .build();
1213
1214 let annot_id = doc.add_annotation(0, spec).unwrap();
1215
1216 let annot = doc.resolve(annot_id).unwrap();
1218 let dict = annot.as_dict().unwrap();
1219 assert!(dict.contains_key(&Name::subtype()));
1220 assert!(dict.contains_key(&Name::rect()));
1221
1222 let page_id = doc.page_id(0).unwrap();
1224 let page = doc.resolve(page_id).unwrap();
1225 let pdict = page.as_dict().unwrap();
1226 let annots = pdict.get(&Name::annots()).unwrap().as_array().unwrap();
1227 assert_eq!(annots.len(), 1);
1228 }
1229
1230 #[test]
1231 fn test_add_annotation_out_of_range() {
1232 let mut doc = EditDocument::new_blank();
1233 let spec = AnnotationBuilder::new(AnnotationType::Text, [0.0; 4]).build();
1234 let result = doc.add_annotation(0, spec);
1235 assert!(result.is_err());
1236 }
1237
1238 #[test]
1239 fn test_delete_annotation() {
1240 let mut doc = EditDocument::new_blank();
1241 doc.add_page(Rect::new(0.0, 0.0, 612.0, 792.0));
1242
1243 let spec = AnnotationBuilder::new(AnnotationType::Text, [10.0, 10.0, 50.0, 50.0]).build();
1244 let annot_id = doc.add_annotation(0, spec).unwrap();
1245
1246 doc.delete_annotation(0, annot_id).unwrap();
1247
1248 assert!(doc.resolve(annot_id).is_err());
1250
1251 let page_id = doc.page_id(0).unwrap();
1253 let page = doc.resolve(page_id).unwrap();
1254 let pdict = page.as_dict().unwrap();
1255 let annots = pdict.get(&Name::annots()).unwrap().as_array().unwrap();
1256 assert_eq!(annots.len(), 0);
1257 }
1258
1259 #[test]
1260 fn test_update_annotation_contents() {
1261 let mut doc = EditDocument::new_blank();
1262 doc.add_page(Rect::new(0.0, 0.0, 612.0, 792.0));
1263
1264 let spec = AnnotationBuilder::new(AnnotationType::Text, [10.0, 10.0, 50.0, 50.0])
1265 .contents("Original")
1266 .build();
1267 let annot_id = doc.add_annotation(0, spec).unwrap();
1268
1269 let updates = AnnotationUpdates {
1270 contents: Some("Updated".to_string()),
1271 ..Default::default()
1272 };
1273 doc.update_annotation(annot_id, updates).unwrap();
1274
1275 let annot = doc.resolve(annot_id).unwrap();
1276 let dict = annot.as_dict().unwrap();
1277 let contents = dict
1278 .get(&Name::contents())
1279 .and_then(|o| o.as_string())
1280 .unwrap();
1281 assert_eq!(contents.as_bytes(), b"Updated");
1282 }
1283
1284 #[test]
1285 fn test_update_annotation_rect() {
1286 let mut doc = EditDocument::new_blank();
1287 doc.add_page(Rect::new(0.0, 0.0, 612.0, 792.0));
1288
1289 let spec = AnnotationBuilder::new(AnnotationType::Text, [10.0, 10.0, 50.0, 50.0]).build();
1290 let annot_id = doc.add_annotation(0, spec).unwrap();
1291
1292 let updates = AnnotationUpdates {
1293 rect: Some([0.0, 0.0, 100.0, 100.0]),
1294 ..Default::default()
1295 };
1296 doc.update_annotation(annot_id, updates).unwrap();
1297
1298 let annot = doc.resolve(annot_id).unwrap();
1299 let dict = annot.as_dict().unwrap();
1300 let rect = dict.get(&Name::rect()).unwrap().as_array().unwrap();
1301 assert_eq!(rect[2].as_f64(), Some(100.0));
1302 }
1303
1304 #[test]
1305 fn test_multiple_annotations_on_page() {
1306 let mut doc = EditDocument::new_blank();
1307 doc.add_page(Rect::new(0.0, 0.0, 612.0, 792.0));
1308
1309 for i in 0..5 {
1310 let r = i as f32 * 10.0;
1311 let spec =
1312 AnnotationBuilder::new(AnnotationType::Text, [r, r, r + 30.0, r + 30.0]).build();
1313 doc.add_annotation(0, spec).unwrap();
1314 }
1315
1316 let page_id = doc.page_id(0).unwrap();
1317 let page = doc.resolve(page_id).unwrap();
1318 let pdict = page.as_dict().unwrap();
1319 let annots = pdict.get(&Name::annots()).unwrap().as_array().unwrap();
1320 assert_eq!(annots.len(), 5);
1321 }
1322
1323 #[test]
1324 fn test_annotation_type_names() {
1325 assert_eq!(
1326 annotation_type_name(&AnnotationType::Highlight),
1327 b"Highlight"
1328 );
1329 assert_eq!(annotation_type_name(&AnnotationType::Link), b"Link");
1330 assert_eq!(annotation_type_name(&AnnotationType::ThreeD), b"3D");
1331 }
1332
1333 #[test]
1338 fn test_update_annotation_subject() {
1339 let mut doc = EditDocument::new_blank();
1340 doc.add_page(Rect::new(0.0, 0.0, 612.0, 792.0));
1341
1342 let spec = AnnotationBuilder::new(AnnotationType::Text, [10.0, 10.0, 50.0, 50.0]).build();
1343 let annot_id = doc.add_annotation(0, spec).unwrap();
1344
1345 let updates = AnnotationUpdates {
1346 subject: Some("Important Note".to_string()),
1347 ..Default::default()
1348 };
1349 doc.update_annotation(annot_id, updates).unwrap();
1350
1351 let annot = doc.resolve(annot_id).unwrap();
1352 let dict = annot.as_dict().unwrap();
1353 let subj = dict.get(&Name::subj()).and_then(|o| o.as_string()).unwrap();
1354 assert_eq!(subj.as_bytes(), b"Important Note");
1355 }
1356
1357 #[test]
1358 fn test_update_annotation_author() {
1359 let mut doc = EditDocument::new_blank();
1360 doc.add_page(Rect::new(0.0, 0.0, 612.0, 792.0));
1361
1362 let spec = AnnotationBuilder::new(AnnotationType::Text, [10.0, 10.0, 50.0, 50.0]).build();
1363 let annot_id = doc.add_annotation(0, spec).unwrap();
1364
1365 let updates = AnnotationUpdates {
1366 author: Some("Jane Doe".to_string()),
1367 ..Default::default()
1368 };
1369 doc.update_annotation(annot_id, updates).unwrap();
1370
1371 let annot = doc.resolve(annot_id).unwrap();
1372 let dict = annot.as_dict().unwrap();
1373 let author = dict.get(&Name::t()).and_then(|o| o.as_string()).unwrap();
1374 assert_eq!(author.as_bytes(), b"Jane Doe");
1375 }
1376
1377 #[test]
1378 fn test_update_annotation_border_width() {
1379 let mut doc = EditDocument::new_blank();
1380 doc.add_page(Rect::new(0.0, 0.0, 612.0, 792.0));
1381
1382 let spec = AnnotationBuilder::new(AnnotationType::Text, [10.0, 10.0, 50.0, 50.0]).build();
1383 let annot_id = doc.add_annotation(0, spec).unwrap();
1384
1385 let updates = AnnotationUpdates {
1386 border_width: Some(2.5),
1387 ..Default::default()
1388 };
1389 doc.update_annotation(annot_id, updates).unwrap();
1390
1391 let annot = doc.resolve(annot_id).unwrap();
1392 let dict = annot.as_dict().unwrap();
1393 let bs_dict = dict.get(&Name::bs()).and_then(|o| o.as_dict()).unwrap();
1394 let w = bs_dict
1395 .get(&Name::from_bytes(b"W".to_vec()))
1396 .and_then(|o| o.as_f64())
1397 .unwrap();
1398 assert!((w - 2.5).abs() < 0.001);
1399 }
1400
1401 #[test]
1407 fn test_update_annotation_border_style_writes_border_array() {
1408 let mut doc = EditDocument::new_blank();
1409 doc.add_page(Rect::new(0.0, 0.0, 612.0, 792.0));
1410 let spec =
1411 AnnotationBuilder::new(AnnotationType::Square, [10.0, 10.0, 100.0, 100.0]).build();
1412 let annot_id = doc.add_annotation(0, spec).unwrap();
1413
1414 let updates = AnnotationUpdates {
1415 border_style: Some((5.0, 10.0, 2.0)), ..Default::default()
1417 };
1418 doc.update_annotation(annot_id, updates).unwrap();
1419
1420 let annot = doc.resolve(annot_id).unwrap();
1421 let dict = annot.as_dict().unwrap();
1422 let border = dict
1423 .get(&Name::border())
1424 .and_then(|o| o.as_array())
1425 .unwrap();
1426 assert_eq!(border.len(), 3);
1427 assert!((border[0].as_f64().unwrap() - 5.0).abs() < 0.001);
1428 assert!((border[1].as_f64().unwrap() - 10.0).abs() < 0.001);
1429 assert!((border[2].as_f64().unwrap() - 2.0).abs() < 0.001);
1430 }
1431
1432 #[test]
1433 fn test_update_annotation_color_rgba_writes_c_array() {
1434 let mut doc = EditDocument::new_blank();
1435 doc.add_page(Rect::new(0.0, 0.0, 612.0, 792.0));
1436 let spec = AnnotationBuilder::new(AnnotationType::Text, [10.0, 10.0, 50.0, 50.0]).build();
1437 let annot_id = doc.add_annotation(0, spec).unwrap();
1438
1439 let updates = AnnotationUpdates {
1441 color_rgba: Some((255, 0, 0, 255)),
1442 ..Default::default()
1443 };
1444 doc.update_annotation(annot_id, updates).unwrap();
1445
1446 let annot = doc.resolve(annot_id).unwrap();
1447 let dict = annot.as_dict().unwrap();
1448 let c = dict.get(&Name::c()).and_then(|o| o.as_array()).unwrap();
1449 assert_eq!(c.len(), 3);
1450 assert!((c[0].as_f64().unwrap() - 1.0).abs() < 0.01); assert!((c[1].as_f64().unwrap() - 0.0).abs() < 0.01); assert!((c[2].as_f64().unwrap() - 0.0).abs() < 0.01); }
1454
1455 #[test]
1456 fn test_update_annotation_attachment_points_writes_quad_points() {
1457 let mut doc = EditDocument::new_blank();
1458 doc.add_page(Rect::new(0.0, 0.0, 612.0, 792.0));
1459 let spec =
1460 AnnotationBuilder::new(AnnotationType::Highlight, [10.0, 10.0, 200.0, 30.0]).build();
1461 let annot_id = doc.add_annotation(0, spec).unwrap();
1462
1463 let updates = AnnotationUpdates {
1464 attachment_points: Some(vec![[10.0, 10.0, 200.0, 10.0, 200.0, 30.0, 10.0, 30.0]]),
1465 ..Default::default()
1466 };
1467 doc.update_annotation(annot_id, updates).unwrap();
1468
1469 let annot = doc.resolve(annot_id).unwrap();
1470 let dict = annot.as_dict().unwrap();
1471 let qp = dict
1472 .get(&Name::quad_points())
1473 .and_then(|o| o.as_array())
1474 .unwrap();
1475 assert_eq!(qp.len(), 8);
1476 assert!((qp[0].as_f64().unwrap() - 10.0).abs() < 0.001);
1477 assert!((qp[2].as_f64().unwrap() - 200.0).abs() < 0.001);
1478 }
1479
1480 #[test]
1481 fn test_update_annotation_ink_list_writes_ink_list() {
1482 let mut doc = EditDocument::new_blank();
1483 doc.add_page(Rect::new(0.0, 0.0, 612.0, 792.0));
1484 let spec = AnnotationBuilder::new(AnnotationType::Ink, [0.0, 0.0, 200.0, 200.0]).build();
1485 let annot_id = doc.add_annotation(0, spec).unwrap();
1486
1487 let updates = AnnotationUpdates {
1488 ink_list: Some(vec![
1489 vec![10.0, 10.0, 20.0, 30.0, 40.0, 50.0], vec![100.0, 100.0, 110.0, 120.0], ]),
1492 ..Default::default()
1493 };
1494 doc.update_annotation(annot_id, updates).unwrap();
1495
1496 let annot = doc.resolve(annot_id).unwrap();
1497 let dict = annot.as_dict().unwrap();
1498 let ink = dict
1499 .get(&Name::ink_list())
1500 .and_then(|o| o.as_array())
1501 .unwrap();
1502 assert_eq!(ink.len(), 2); let stroke0 = ink[0].as_array().unwrap();
1504 assert_eq!(stroke0.len(), 6);
1505 assert!((stroke0[0].as_f64().unwrap() - 10.0).abs() < 0.001);
1506 let stroke1 = ink[1].as_array().unwrap();
1507 assert_eq!(stroke1.len(), 4);
1508 }
1509
1510 #[test]
1511 fn test_update_annotation_extra_string_writes_arbitrary_key() {
1512 let mut doc = EditDocument::new_blank();
1513 doc.add_page(Rect::new(0.0, 0.0, 612.0, 792.0));
1514 let spec = AnnotationBuilder::new(AnnotationType::Text, [10.0, 10.0, 50.0, 50.0]).build();
1515 let annot_id = doc.add_annotation(0, spec).unwrap();
1516
1517 let updates = AnnotationUpdates {
1518 extra_string: Some(("RC".to_string(), "rich text content".to_string())),
1519 ..Default::default()
1520 };
1521 doc.update_annotation(annot_id, updates).unwrap();
1522
1523 let annot = doc.resolve(annot_id).unwrap();
1524 let dict = annot.as_dict().unwrap();
1525 let key = Name::from_bytes(b"RC".to_vec());
1526 let val = dict.get(&key).and_then(|o| o.as_string()).unwrap();
1527 assert_eq!(val.as_bytes(), b"rich text content");
1528 }
1529
1530 #[test]
1535 fn test_ap_object_append_and_count() {
1536 use crate::page_object::{PageObject, PathObject, PathSegment};
1537
1538 let mut doc = EditDocument::new_blank();
1539 doc.add_page(Rect::new(0.0, 0.0, 612.0, 792.0));
1540
1541 let spec = AnnotationBuilder::new(AnnotationType::Ink, [0.0, 0.0, 100.0, 100.0]).build();
1542 let annot_id = doc.add_annotation(0, spec).unwrap();
1543
1544 assert_eq!(doc.annotation_object_count(annot_id), 0);
1546 assert!(doc.annotation_object_at(annot_id, 0).is_none());
1547
1548 let path = PathObject {
1550 segments: vec![
1551 PathSegment::MoveTo(10.0, 10.0),
1552 PathSegment::LineTo(90.0, 90.0),
1553 ],
1554 ..Default::default()
1555 };
1556 doc.append_annotation_object(annot_id, PageObject::Path(path))
1557 .unwrap();
1558
1559 assert_eq!(doc.annotation_object_count(annot_id), 1);
1560 assert!(doc.annotation_object_at(annot_id, 0).is_some());
1561 assert!(doc.annotation_object_at(annot_id, 1).is_none());
1562 }
1563
1564 #[test]
1565 fn test_ap_object_remove() {
1566 use crate::page_object::{PageObject, PathObject, PathSegment};
1567
1568 let mut doc = EditDocument::new_blank();
1569 doc.add_page(Rect::new(0.0, 0.0, 612.0, 792.0));
1570
1571 let spec = AnnotationBuilder::new(AnnotationType::Ink, [0.0, 0.0, 100.0, 100.0]).build();
1572 let annot_id = doc.add_annotation(0, spec).unwrap();
1573
1574 let path = PathObject {
1575 segments: vec![PathSegment::MoveTo(0.0, 0.0)],
1576 ..Default::default()
1577 };
1578 doc.append_annotation_object(annot_id, PageObject::Path(path))
1579 .unwrap();
1580
1581 assert_eq!(doc.annotation_object_count(annot_id), 1);
1582
1583 doc.remove_annotation_object(annot_id, 0).unwrap();
1585 assert_eq!(doc.annotation_object_count(annot_id), 0);
1586
1587 let other_id = ObjectId::new(999, 0);
1589 assert!(doc.remove_annotation_object(other_id, 0).is_err());
1590 }
1591
1592 #[test]
1593 fn test_ap_object_update_generates_ap_stream() {
1594 use crate::page_object::{FillMode, PageObject, PathObject, PathSegment};
1595 use rpdfium_graphics::Color;
1596
1597 let mut doc = EditDocument::new_blank();
1598 doc.add_page(Rect::new(0.0, 0.0, 612.0, 792.0));
1599
1600 let spec = AnnotationBuilder::new(AnnotationType::Ink, [10.0, 10.0, 90.0, 90.0]).build();
1601 let annot_id = doc.add_annotation(0, spec).unwrap();
1602
1603 let path = PathObject {
1604 segments: vec![
1605 PathSegment::MoveTo(10.0, 10.0),
1606 PathSegment::LineTo(90.0, 10.0),
1607 PathSegment::LineTo(90.0, 90.0),
1608 PathSegment::LineTo(10.0, 90.0),
1609 PathSegment::Close,
1610 ],
1611 fill_color: Some(Color::rgb(1.0, 0.0, 0.0)),
1612 stroke_color: Some(Color::rgb(0.0, 0.0, 1.0)),
1613 line_width: 2.0,
1614 fill_mode: FillMode::NonZero,
1615 ..Default::default()
1616 };
1617 doc.append_annotation_object(annot_id, PageObject::Path(path))
1618 .unwrap();
1619
1620 doc.update_annotation_ap(annot_id).unwrap();
1622
1623 let annot = doc.resolve(annot_id).unwrap();
1625 let dict = annot.as_dict().unwrap();
1626 assert!(
1627 dict.contains_key(&Name::from_bytes(b"AP".to_vec())),
1628 "/AP key should be present after update_annotation_ap"
1629 );
1630 let ap_dict = dict
1631 .get(&Name::from_bytes(b"AP".to_vec()))
1632 .and_then(|o| o.as_dict())
1633 .unwrap();
1634 assert!(
1635 ap_dict.contains_key(&Name::from_bytes(b"N".to_vec())),
1636 "/AP /N key should be present"
1637 );
1638 }
1639
1640 #[test]
1641 fn test_ap_object_update_no_objects_returns_error() {
1642 let mut doc = EditDocument::new_blank();
1643 doc.add_page(Rect::new(0.0, 0.0, 612.0, 792.0));
1644
1645 let spec = AnnotationBuilder::new(AnnotationType::Ink, [0.0, 0.0, 100.0, 100.0]).build();
1646 let annot_id = doc.add_annotation(0, spec).unwrap();
1647
1648 let result = doc.update_annotation_ap(annot_id);
1650 assert!(
1651 result.is_err(),
1652 "update_annotation_ap with no objects should return error"
1653 );
1654 }
1655
1656 #[test]
1657 fn test_count_annotation_objects_alias() {
1658 use crate::page_object::{PageObject, PathObject, PathSegment};
1659
1660 let mut doc = EditDocument::new_blank();
1661 doc.add_page(Rect::new(0.0, 0.0, 612.0, 792.0));
1662
1663 let spec = AnnotationBuilder::new(AnnotationType::Stamp, [0.0, 0.0, 200.0, 100.0]).build();
1664 let annot_id = doc.add_annotation(0, spec).unwrap();
1665
1666 let path = PathObject {
1667 segments: vec![PathSegment::MoveTo(0.0, 0.0)],
1668 ..Default::default()
1669 };
1670 doc.append_annotation_object(annot_id, PageObject::Path(path))
1671 .unwrap();
1672
1673 assert_eq!(
1674 doc.annotation_object_count(annot_id),
1675 doc.annot_get_object_count(annot_id)
1676 );
1677 }
1678
1679 #[test]
1684 fn test_update_annotation_vertices_writes_vertices_array() {
1685 let mut doc = EditDocument::new_blank();
1686 doc.add_page(Rect::new(0.0, 0.0, 612.0, 792.0));
1687 let spec =
1688 AnnotationBuilder::new(AnnotationType::Polygon, [0.0, 0.0, 200.0, 200.0]).build();
1689 let annot_id = doc.add_annotation(0, spec).unwrap();
1690
1691 let updates = AnnotationUpdates {
1692 vertices: Some(vec![
1693 [10.0, 20.0],
1694 [100.0, 20.0],
1695 [100.0, 120.0],
1696 [10.0, 120.0],
1697 ]),
1698 ..Default::default()
1699 };
1700 doc.update_annotation(annot_id, updates).unwrap();
1701
1702 let annot = doc.resolve(annot_id).unwrap();
1703 let dict = annot.as_dict().unwrap();
1704 let verts = dict
1705 .get(&Name::vertices())
1706 .and_then(|o| o.as_array())
1707 .unwrap();
1708 assert_eq!(verts.len(), 8);
1710 assert!((verts[0].as_f64().unwrap() - 10.0).abs() < 0.001);
1711 assert!((verts[1].as_f64().unwrap() - 20.0).abs() < 0.001);
1712 assert!((verts[2].as_f64().unwrap() - 100.0).abs() < 0.001);
1713 }
1714
1715 #[test]
1716 fn test_update_annotation_vertices_replaces_existing() {
1717 let mut doc = EditDocument::new_blank();
1718 doc.add_page(Rect::new(0.0, 0.0, 612.0, 792.0));
1719 let spec =
1720 AnnotationBuilder::new(AnnotationType::Polygon, [0.0, 0.0, 200.0, 200.0]).build();
1721 let annot_id = doc.add_annotation(0, spec).unwrap();
1722
1723 let updates1 = AnnotationUpdates {
1725 vertices: Some(vec![[0.0, 0.0], [50.0, 0.0], [50.0, 50.0]]),
1726 ..Default::default()
1727 };
1728 doc.update_annotation(annot_id, updates1).unwrap();
1729
1730 let updates2 = AnnotationUpdates {
1732 vertices: Some(vec![[5.0, 5.0], [15.0, 5.0]]),
1733 ..Default::default()
1734 };
1735 doc.update_annotation(annot_id, updates2).unwrap();
1736
1737 let annot = doc.resolve(annot_id).unwrap();
1738 let dict = annot.as_dict().unwrap();
1739 let verts = dict
1740 .get(&Name::vertices())
1741 .and_then(|o| o.as_array())
1742 .unwrap();
1743 assert_eq!(verts.len(), 4);
1745 assert!((verts[0].as_f64().unwrap() - 5.0).abs() < 0.001);
1746 }
1747
1748 #[test]
1749 fn test_update_annotation_vertices_empty_clears_array() {
1750 let mut doc = EditDocument::new_blank();
1751 doc.add_page(Rect::new(0.0, 0.0, 612.0, 792.0));
1752 let spec =
1753 AnnotationBuilder::new(AnnotationType::Polygon, [0.0, 0.0, 200.0, 200.0]).build();
1754 let annot_id = doc.add_annotation(0, spec).unwrap();
1755
1756 let updates = AnnotationUpdates {
1757 vertices: Some(vec![]),
1758 ..Default::default()
1759 };
1760 doc.update_annotation(annot_id, updates).unwrap();
1761
1762 let annot = doc.resolve(annot_id).unwrap();
1763 let dict = annot.as_dict().unwrap();
1764 let verts = dict
1765 .get(&Name::vertices())
1766 .and_then(|o| o.as_array())
1767 .unwrap();
1768 assert_eq!(verts.len(), 0);
1769 }
1770
1771 #[test]
1776 fn test_update_annotation_sets_uri_action() {
1777 let mut doc = EditDocument::new_blank();
1778 doc.add_page(Rect::new(0.0, 0.0, 612.0, 792.0));
1779 let spec = AnnotationBuilder::new(AnnotationType::Link, [10.0, 10.0, 100.0, 30.0]).build();
1780 let annot_id = doc.add_annotation(0, spec).unwrap();
1781
1782 let updates = AnnotationUpdates {
1783 uri_action: Some("https://example.com".to_string()),
1784 ..Default::default()
1785 };
1786 doc.update_annotation(annot_id, updates).unwrap();
1787
1788 let annot = doc.resolve(annot_id).unwrap();
1789 let dict = annot.as_dict().unwrap();
1790 let action_dict = dict.get(&Name::a()).and_then(|o| o.as_dict()).unwrap();
1791 let s_val = action_dict
1793 .get(&Name::s())
1794 .and_then(|o| o.as_name())
1795 .unwrap();
1796 assert_eq!(s_val.as_bytes(), b"URI");
1797 let uri_val = action_dict
1799 .get(&Name::uri())
1800 .and_then(|o| o.as_string())
1801 .unwrap();
1802 assert_eq!(uri_val.as_bytes(), b"https://example.com");
1803 }
1804
1805 #[test]
1806 fn test_update_annotation_uri_in_correct_dict_structure() {
1807 let mut doc = EditDocument::new_blank();
1809 doc.add_page(Rect::new(0.0, 0.0, 612.0, 792.0));
1810 let spec = AnnotationBuilder::new(AnnotationType::Widget, [0.0, 0.0, 200.0, 50.0]).build();
1811 let annot_id = doc.add_annotation(0, spec).unwrap();
1812
1813 let updates = AnnotationUpdates {
1814 uri_action: Some("https://rust-lang.org/".to_string()),
1815 ..Default::default()
1816 };
1817 doc.update_annotation(annot_id, updates).unwrap();
1818
1819 let annot = doc.resolve(annot_id).unwrap();
1820 let dict = annot.as_dict().unwrap();
1821
1822 let action = dict.get(&Name::a()).expect("/A key missing");
1824 let action_dict = action.as_dict().expect("/A value is not a dictionary");
1825
1826 assert!(
1828 action_dict.contains_key(&Name::s()),
1829 "/S missing from action dict"
1830 );
1831 assert!(
1832 action_dict.contains_key(&Name::uri()),
1833 "/URI missing from action dict"
1834 );
1835
1836 let s = action_dict
1837 .get(&Name::s())
1838 .and_then(|o| o.as_name())
1839 .unwrap();
1840 assert_eq!(s.as_bytes(), b"URI", "/S value should be /URI");
1841
1842 let uri = action_dict
1843 .get(&Name::uri())
1844 .and_then(|o| o.as_string())
1845 .unwrap();
1846 assert_eq!(uri.as_bytes(), b"https://rust-lang.org/");
1847 }
1848
1849 #[test]
1855 fn test_annotation_updates_append_attachment_points() {
1856 let mut updates = AnnotationUpdates::default();
1857 updates.append_attachment_points([1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0]);
1858 updates.append_attachment_points([9.0, 10.0, 11.0, 12.0, 13.0, 14.0, 15.0, 16.0]);
1859 let pts = updates.attachment_points.as_ref().unwrap();
1860 assert_eq!(pts.len(), 2);
1861 assert_eq!(pts[0][0], 1.0);
1862 assert_eq!(pts[1][0], 9.0);
1863 }
1864}