1#[cfg(feature = "write")]
7use lopdf::{dictionary, Document, Object, ObjectId, Stream};
8
9#[cfg(feature = "write")]
10use crate::appearance_writer::{AppearanceColor, AppearanceStreamBuilder};
11#[cfg(feature = "write")]
12use crate::error::AnnotBuildError;
13
14#[cfg(feature = "write")]
16type AppearanceFn = Box<dyn FnOnce(&mut AppearanceStreamBuilder)>;
17
18#[cfg(feature = "write")]
20#[derive(Debug, Clone, Copy)]
21pub struct AnnotRect {
22 pub x0: f64,
23 pub y0: f64,
24 pub x1: f64,
25 pub y1: f64,
26}
27
28#[cfg(feature = "write")]
29impl AnnotRect {
30 pub fn new(x0: f64, y0: f64, x1: f64, y1: f64) -> Self {
31 Self { x0, y0, x1, y1 }
32 }
33
34 pub fn width(&self) -> f64 {
35 (self.x1 - self.x0).abs()
36 }
37
38 pub fn height(&self) -> f64 {
39 (self.y1 - self.y0).abs()
40 }
41
42 fn as_array(&self) -> Object {
43 Object::Array(vec![
44 Object::Real(self.x0 as f32),
45 Object::Real(self.y0 as f32),
46 Object::Real(self.x1 as f32),
47 Object::Real(self.y1 as f32),
48 ])
49 }
50}
51
52#[cfg(feature = "write")]
54#[derive(Debug, Clone, Copy)]
55pub enum AnnotSubtype {
56 Square,
57 Circle,
58 Line,
59 Highlight,
60 Underline,
61 StrikeOut,
62 Squiggly,
63 FreeText,
64 Text,
65 Stamp,
66 Ink,
67 Polygon,
68 PolyLine,
69 Link,
70}
71
72#[cfg(feature = "write")]
73impl AnnotSubtype {
74 fn as_str(&self) -> &'static str {
75 match self {
76 Self::Square => "Square",
77 Self::Circle => "Circle",
78 Self::Line => "Line",
79 Self::Highlight => "Highlight",
80 Self::Underline => "Underline",
81 Self::StrikeOut => "StrikeOut",
82 Self::Squiggly => "Squiggly",
83 Self::FreeText => "FreeText",
84 Self::Text => "Text",
85 Self::Stamp => "Stamp",
86 Self::Ink => "Ink",
87 Self::Polygon => "Polygon",
88 Self::PolyLine => "PolyLine",
89 Self::Link => "Link",
90 }
91 }
92}
93
94#[cfg(feature = "write")]
110pub struct AnnotationBuilder {
111 subtype: AnnotSubtype,
112 rect: AnnotRect,
113 color: Option<AppearanceColor>,
114 interior_color: Option<AppearanceColor>,
115 opacity: Option<f64>,
116 border_width: f64,
117 contents: Option<String>,
118 flags: u32,
119 quad_points: Option<Vec<f64>>,
121 line_endpoints: Option<[f64; 4]>,
123 line_endings: Option<[LineEnding; 2]>,
125 ink_list: Option<Vec<Vec<f64>>>,
127 vertices: Option<Vec<f64>>,
129 dash_pattern: Option<Vec<f64>>,
131 default_appearance_str: Option<String>,
133 text_alignment: Option<i64>,
135 icon_name: Option<String>,
137 uri_action: Option<String>,
139 destination: Option<String>,
141 custom_appearance: Option<AppearanceFn>,
143}
144
145#[cfg(feature = "write")]
147#[derive(Debug, Clone, Copy)]
148pub enum StampName {
149 Approved,
150 Experimental,
151 NotApproved,
152 AsIs,
153 Expired,
154 NotForPublicRelease,
155 Confidential,
156 Final,
157 Sold,
158 Departmental,
159 ForComment,
160 TopSecret,
161 Draft,
162 ForPublicRelease,
163}
164
165#[cfg(feature = "write")]
166impl StampName {
167 fn as_str(&self) -> &'static str {
168 match self {
169 Self::Approved => "Approved",
170 Self::Experimental => "Experimental",
171 Self::NotApproved => "NotApproved",
172 Self::AsIs => "AsIs",
173 Self::Expired => "Expired",
174 Self::NotForPublicRelease => "NotForPublicRelease",
175 Self::Confidential => "Confidential",
176 Self::Final => "Final",
177 Self::Sold => "Sold",
178 Self::Departmental => "Departmental",
179 Self::ForComment => "ForComment",
180 Self::TopSecret => "TopSecret",
181 Self::Draft => "Draft",
182 Self::ForPublicRelease => "ForPublicRelease",
183 }
184 }
185}
186
187#[cfg(feature = "write")]
189#[derive(Debug, Clone, Copy)]
190pub enum TextIcon {
191 Comment,
192 Key,
193 Note,
194 Help,
195 NewParagraph,
196 Paragraph,
197 Insert,
198}
199
200#[cfg(feature = "write")]
201impl TextIcon {
202 fn as_str(&self) -> &'static str {
203 match self {
204 Self::Comment => "Comment",
205 Self::Key => "Key",
206 Self::Note => "Note",
207 Self::Help => "Help",
208 Self::NewParagraph => "NewParagraph",
209 Self::Paragraph => "Paragraph",
210 Self::Insert => "Insert",
211 }
212 }
213}
214
215#[cfg(feature = "write")]
217#[derive(Debug, Clone, Copy)]
218pub enum LineEnding {
219 None,
220 Square,
221 Circle,
222 Diamond,
223 OpenArrow,
224 ClosedArrow,
225 Butt,
226 ROpenArrow,
227 RClosedArrow,
228 Slash,
229}
230
231#[cfg(feature = "write")]
232impl LineEnding {
233 fn as_str(&self) -> &'static str {
234 match self {
235 Self::None => "None",
236 Self::Square => "Square",
237 Self::Circle => "Circle",
238 Self::Diamond => "Diamond",
239 Self::OpenArrow => "OpenArrow",
240 Self::ClosedArrow => "ClosedArrow",
241 Self::Butt => "Butt",
242 Self::ROpenArrow => "ROpenArrow",
243 Self::RClosedArrow => "RClosedArrow",
244 Self::Slash => "Slash",
245 }
246 }
247}
248
249#[cfg(feature = "write")]
250impl AnnotationBuilder {
251 pub fn new(subtype: AnnotSubtype, rect: AnnotRect) -> Self {
253 Self {
254 subtype,
255 rect,
256 color: None,
257 interior_color: None,
258 opacity: None,
259 border_width: 1.0,
260 contents: None,
261 flags: 4, quad_points: None,
263 line_endpoints: None,
264 line_endings: None,
265 ink_list: None,
266 vertices: None,
267 dash_pattern: None,
268 default_appearance_str: None,
269 text_alignment: None,
270 icon_name: None,
271 uri_action: None,
272 destination: None,
273 custom_appearance: None,
274 }
275 }
276
277 pub fn free_text(rect: AnnotRect, text: &str, font_size: f64) -> Self {
279 let da = format!("/Helv {font_size} Tf 0 g");
280 let mut b = Self::new(AnnotSubtype::FreeText, rect).contents(text);
281 b.default_appearance_str = Some(da);
282 b
283 }
284
285 pub fn sticky_note(rect: AnnotRect, icon: TextIcon) -> Self {
287 let mut b = Self::new(AnnotSubtype::Text, rect);
288 b.icon_name = Some(icon.as_str().to_string());
289 b
290 }
291
292 pub fn stamp(rect: AnnotRect, name: StampName) -> Self {
294 let mut b = Self::new(AnnotSubtype::Stamp, rect);
295 b.icon_name = Some(name.as_str().to_string());
296 b
297 }
298
299 pub fn stamp_custom(rect: AnnotRect, name: &str) -> Self {
301 let mut b = Self::new(AnnotSubtype::Stamp, rect);
302 b.icon_name = Some(name.to_string());
303 b
304 }
305
306 pub fn link_uri(rect: AnnotRect, uri: &str) -> Self {
308 let mut b = Self::new(AnnotSubtype::Link, rect);
309 b.uri_action = Some(uri.to_string());
310 b.border_width = 0.0; b
312 }
313
314 pub fn link_dest(rect: AnnotRect, dest: &str) -> Self {
316 let mut b = Self::new(AnnotSubtype::Link, rect);
317 b.destination = Some(dest.to_string());
318 b.border_width = 0.0;
319 b
320 }
321
322 pub fn square(rect: AnnotRect) -> Self {
324 Self::new(AnnotSubtype::Square, rect)
325 }
326
327 pub fn circle(rect: AnnotRect) -> Self {
329 Self::new(AnnotSubtype::Circle, rect)
330 }
331
332 pub fn line(x1: f64, y1: f64, x2: f64, y2: f64) -> Self {
336 let pad = 1.0; let mut min_x = x1.min(x2);
338 let mut min_y = y1.min(y2);
339 let mut max_x = x1.max(x2);
340 let mut max_y = y1.max(y2);
341 if (max_x - min_x).abs() < f64::EPSILON {
342 min_x -= pad;
343 max_x += pad;
344 }
345 if (max_y - min_y).abs() < f64::EPSILON {
346 min_y -= pad;
347 max_y += pad;
348 }
349 let rect = AnnotRect::new(min_x, min_y, max_x, max_y);
350 let mut b = Self::new(AnnotSubtype::Line, rect);
351 b.line_endpoints = Some([x1, y1, x2, y2]);
352 b
353 }
354
355 pub fn ink(rect: AnnotRect, strokes: Vec<Vec<f64>>) -> Self {
357 let mut b = Self::new(AnnotSubtype::Ink, rect);
358 b.ink_list = Some(strokes);
359 b
360 }
361
362 pub fn polygon(rect: AnnotRect, vertices: Vec<f64>) -> Self {
364 let mut b = Self::new(AnnotSubtype::Polygon, rect);
365 b.vertices = Some(vertices);
366 b
367 }
368
369 pub fn polyline(rect: AnnotRect, vertices: Vec<f64>) -> Self {
371 let mut b = Self::new(AnnotSubtype::PolyLine, rect);
372 b.vertices = Some(vertices);
373 b
374 }
375
376 pub fn highlight(rect: AnnotRect) -> Self {
378 Self::new(AnnotSubtype::Highlight, rect)
379 .color(1.0, 1.0, 0.0) .opacity(0.4)
381 }
382
383 pub fn underline(rect: AnnotRect) -> Self {
385 Self::new(AnnotSubtype::Underline, rect).color(0.0, 0.0, 1.0)
386 }
387
388 pub fn strikeout(rect: AnnotRect) -> Self {
390 Self::new(AnnotSubtype::StrikeOut, rect).color(1.0, 0.0, 0.0)
391 }
392
393 pub fn squiggly(rect: AnnotRect) -> Self {
395 Self::new(AnnotSubtype::Squiggly, rect).color(0.0, 0.8, 0.0)
396 }
397
398 pub fn color(mut self, r: f64, g: f64, b: f64) -> Self {
400 self.color = Some(AppearanceColor::new(r, g, b));
401 self
402 }
403
404 pub fn interior_color(mut self, r: f64, g: f64, b: f64) -> Self {
406 self.interior_color = Some(AppearanceColor::new(r, g, b));
407 self
408 }
409
410 pub fn opacity(mut self, alpha: f64) -> Self {
412 self.opacity = Some(alpha.clamp(0.0, 1.0));
413 self
414 }
415
416 pub fn border_width(mut self, width: f64) -> Self {
418 self.border_width = width;
419 self
420 }
421
422 pub fn contents(mut self, text: impl Into<String>) -> Self {
424 self.contents = Some(text.into());
425 self
426 }
427
428 pub fn flags(mut self, flags: u32) -> Self {
430 self.flags = flags;
431 self
432 }
433
434 pub fn alignment(mut self, q: i64) -> Self {
436 self.text_alignment = Some(q);
437 self
438 }
439
440 pub fn line_endings(mut self, start: LineEnding, end: LineEnding) -> Self {
442 self.line_endings = Some([start, end]);
443 self
444 }
445
446 pub fn dash(mut self, pattern: Vec<f64>) -> Self {
448 self.dash_pattern = Some(pattern);
449 self
450 }
451
452 pub fn quad_points(mut self, points: Vec<f64>) -> Self {
459 self.quad_points = Some(points);
460 self
461 }
462
463 pub fn quad_points_from_rect(self, rect: &AnnotRect) -> Self {
465 self.quad_points(vec![
467 rect.x0, rect.y1, rect.x1, rect.y1, rect.x0, rect.y0, rect.x1, rect.y0, ])
472 }
473
474 pub fn appearance(mut self, f: impl FnOnce(&mut AppearanceStreamBuilder) + 'static) -> Self {
476 self.custom_appearance = Some(Box::new(f));
477 self
478 }
479
480 pub fn build(mut self, doc: &mut Document) -> Result<ObjectId, AnnotBuildError> {
486 let w = self.rect.width();
487 let h = self.rect.height();
488 if w < f64::EPSILON || h < f64::EPSILON {
489 return Err(AnnotBuildError::InvalidRect);
490 }
491
492 let custom_appearance = self.custom_appearance.take();
494
495 let ap_stream_id = self.build_appearance(doc, w, h, custom_appearance)?;
497
498 let mut annot_dict = dictionary! {
500 "Type" => "Annot",
501 "Subtype" => Object::Name(self.subtype.as_str().as_bytes().to_vec()),
502 "Rect" => self.rect.as_array(),
503 "F" => Object::Integer(self.flags as i64),
504 };
505
506 if let Some(ref c) = self.color {
508 annot_dict.set(
509 "C",
510 Object::Array(vec![
511 Object::Real(c.r as f32),
512 Object::Real(c.g as f32),
513 Object::Real(c.b as f32),
514 ]),
515 );
516 }
517
518 if let Some(ref ic) = self.interior_color {
520 annot_dict.set(
521 "IC",
522 Object::Array(vec![
523 Object::Real(ic.r as f32),
524 Object::Real(ic.g as f32),
525 Object::Real(ic.b as f32),
526 ]),
527 );
528 }
529
530 if let Some(alpha) = self.opacity {
532 annot_dict.set("CA", Object::Real(alpha as f32));
533 }
534
535 if let Some(ref text) = self.contents {
537 annot_dict.set(
538 "Contents",
539 Object::String(text.as_bytes().to_vec(), lopdf::StringFormat::Literal),
540 );
541 }
542
543 if let Some(ref qp) = self.quad_points {
545 let arr: Vec<Object> = qp.iter().map(|&v| Object::Real(v as f32)).collect();
546 annot_dict.set("QuadPoints", Object::Array(arr));
547 }
548
549 if let Some(ref l) = self.line_endpoints {
551 annot_dict.set(
552 "L",
553 Object::Array(vec![
554 Object::Real(l[0] as f32),
555 Object::Real(l[1] as f32),
556 Object::Real(l[2] as f32),
557 Object::Real(l[3] as f32),
558 ]),
559 );
560 }
561
562 if let Some(ref le) = self.line_endings {
564 annot_dict.set(
565 "LE",
566 Object::Array(vec![
567 Object::Name(le[0].as_str().as_bytes().to_vec()),
568 Object::Name(le[1].as_str().as_bytes().to_vec()),
569 ]),
570 );
571 }
572
573 if let Some(ref ink) = self.ink_list {
575 let ink_arr: Vec<Object> = ink
576 .iter()
577 .map(|stroke| {
578 Object::Array(stroke.iter().map(|&v| Object::Real(v as f32)).collect())
579 })
580 .collect();
581 annot_dict.set("InkList", Object::Array(ink_arr));
582 }
583
584 if let Some(ref verts) = self.vertices {
586 let arr: Vec<Object> = verts.iter().map(|&v| Object::Real(v as f32)).collect();
587 annot_dict.set("Vertices", Object::Array(arr));
588 }
589
590 let has_dash = self.dash_pattern.is_some();
592 if (self.border_width - 1.0).abs() > f64::EPSILON || has_dash {
593 let mut bs = dictionary! {
594 "W" => Object::Real(self.border_width as f32),
595 };
596 if has_dash {
597 bs.set("S", Object::Name(b"D".to_vec()));
598 let d_arr: Vec<Object> = self
599 .dash_pattern
600 .as_ref()
601 .expect("guarded by has_dash which checks is_some()")
602 .iter()
603 .map(|&v| Object::Real(v as f32))
604 .collect();
605 bs.set("D", Object::Array(d_arr));
606 } else {
607 bs.set("S", Object::Name(b"S".to_vec()));
608 }
609 annot_dict.set("BS", Object::Dictionary(bs));
610 }
611
612 if let Some(ref da) = self.default_appearance_str {
614 annot_dict.set(
615 "DA",
616 Object::String(da.as_bytes().to_vec(), lopdf::StringFormat::Literal),
617 );
618 }
619
620 if let Some(q) = self.text_alignment {
622 annot_dict.set("Q", Object::Integer(q));
623 }
624
625 if let Some(ref name) = self.icon_name {
627 annot_dict.set("Name", Object::Name(name.as_bytes().to_vec()));
628 }
629
630 if let Some(ref uri) = self.uri_action {
632 let action = dictionary! {
633 "S" => "URI",
634 "URI" => Object::String(uri.as_bytes().to_vec(), lopdf::StringFormat::Literal),
635 };
636 annot_dict.set("A", Object::Dictionary(action));
637 }
638
639 if let Some(ref dest) = self.destination {
641 annot_dict.set(
642 "Dest",
643 Object::String(dest.as_bytes().to_vec(), lopdf::StringFormat::Literal),
644 );
645 }
646
647 let ap = dictionary! {
649 "N" => Object::Reference(ap_stream_id),
650 };
651 annot_dict.set("AP", Object::Dictionary(ap));
652
653 Ok(doc.add_object(Object::Dictionary(annot_dict)))
654 }
655
656 fn build_appearance(
658 &self,
659 doc: &mut Document,
660 w: f64,
661 h: f64,
662 custom_appearance: Option<AppearanceFn>,
663 ) -> Result<ObjectId, AnnotBuildError> {
664 let mut builder = AppearanceStreamBuilder::new(w, h);
665
666 if let Some(custom) = custom_appearance {
667 custom(&mut builder);
668 } else {
669 self.default_appearance(&mut builder, w, h);
670 }
671
672 let content_bytes = builder
673 .encode()
674 .map_err(AnnotBuildError::AppearanceEncode)?;
675
676 let mut stream_dict = dictionary! {
677 "Type" => "XObject",
678 "Subtype" => "Form",
679 "BBox" => Object::Array(vec![
680 Object::Real(0.0),
681 Object::Real(0.0),
682 Object::Real(w as f32),
683 Object::Real(h as f32),
684 ]),
685 };
686
687 let needs_multiply = matches!(self.subtype, AnnotSubtype::Highlight);
689 let needs_gs = self.opacity.is_some() || needs_multiply;
690 let needs_font = matches!(self.subtype, AnnotSubtype::FreeText | AnnotSubtype::Stamp);
691
692 if needs_gs || needs_font {
693 let mut resources = lopdf::Dictionary::new();
694
695 if needs_gs {
696 let mut gs_dict = dictionary! {
697 "Type" => "ExtGState",
698 };
699 if let Some(alpha) = self.opacity {
700 gs_dict.set("ca", Object::Real(alpha as f32));
701 gs_dict.set("CA", Object::Real(alpha as f32));
702 }
703 if needs_multiply {
704 gs_dict.set("BM", Object::Name(b"Multiply".to_vec()));
705 }
706 let gs_id = doc.add_object(Object::Dictionary(gs_dict));
707 let mut gs_res = lopdf::Dictionary::new();
708 gs_res.set("GS0", Object::Reference(gs_id));
709 resources.set("ExtGState", Object::Dictionary(gs_res));
710 }
711
712 if needs_font {
713 let font_dict = dictionary! {
714 "Type" => "Font",
715 "Subtype" => "Type1",
716 "BaseFont" => "Helvetica",
717 };
718 let font_id = doc.add_object(Object::Dictionary(font_dict));
719 let mut font_res = lopdf::Dictionary::new();
720 font_res.set("Helv", Object::Reference(font_id));
721 resources.set("Font", Object::Dictionary(font_res));
722 }
723
724 stream_dict.set("Resources", Object::Dictionary(resources));
725 }
726
727 let stream = Stream::new(stream_dict, content_bytes);
728 Ok(doc.add_object(Object::Stream(stream)))
729 }
730
731 fn default_appearance(&self, builder: &mut AppearanceStreamBuilder, w: f64, h: f64) {
733 let stroke = self.color.unwrap_or(AppearanceColor::new(0.0, 0.0, 0.0));
734 let needs_gs = self.opacity.is_some() || matches!(self.subtype, AnnotSubtype::Highlight);
735
736 if needs_gs {
737 builder.save_state();
738 builder.ops_push_raw(lopdf::content::Operation::new(
739 "gs",
740 vec![Object::Name(b"GS0".to_vec())],
741 ));
742 }
743
744 match self.subtype {
745 AnnotSubtype::Square => {
746 if let Some(ref fill) = self.interior_color {
747 builder.filled_stroked_rect(fill, &stroke, self.border_width);
748 } else {
749 builder.stroked_rect(&stroke, self.border_width);
750 }
751 }
752 AnnotSubtype::Circle => {
753 builder.save_state();
754 if let Some(ref fill) = self.interior_color {
755 builder.set_fill_color(fill);
756 }
757 builder.set_stroke_color(&stroke);
758 builder.set_line_width(self.border_width);
759 builder.ellipse();
760 if self.interior_color.is_some() {
761 builder.fill_and_stroke();
762 } else {
763 builder.stroke();
764 }
765 builder.restore_state();
766 }
767 AnnotSubtype::Line => {
768 builder.save_state();
769 builder.set_stroke_color(&stroke);
770 builder.set_line_width(self.border_width);
771 if let Some(ref dash) = self.dash_pattern {
772 builder.set_dash_pattern(dash, 0.0);
773 }
774 if let Some(ref l) = self.line_endpoints {
776 let lx1 = l[0] - self.rect.x0;
777 let ly1 = l[1] - self.rect.y0;
778 let lx2 = l[2] - self.rect.x0;
779 let ly2 = l[3] - self.rect.y0;
780 builder.line(lx1, ly1, lx2, ly2);
781 } else {
782 builder.line(0.0, h / 2.0, w, h / 2.0);
783 }
784 builder.stroke();
785 builder.restore_state();
786 }
787 AnnotSubtype::Highlight => {
788 let fill = self.color.unwrap_or(AppearanceColor::new(1.0, 1.0, 0.0));
789 builder.filled_rect(&fill);
790 }
791 AnnotSubtype::Underline => {
792 builder.save_state();
793 builder.set_stroke_color(&stroke);
794 builder.set_line_width(self.border_width.max(0.5));
795 builder.line(0.0, 0.0, w, 0.0);
796 builder.stroke();
797 builder.restore_state();
798 }
799 AnnotSubtype::StrikeOut => {
800 builder.save_state();
801 builder.set_stroke_color(&stroke);
802 builder.set_line_width(self.border_width.max(0.5));
803 builder.line(0.0, h / 2.0, w, h / 2.0);
804 builder.stroke();
805 builder.restore_state();
806 }
807 AnnotSubtype::Squiggly => {
808 builder.save_state();
810 builder.set_stroke_color(&stroke);
811 builder.set_line_width(self.border_width.max(0.5));
812 let step = 4.0;
813 let amp = 2.0;
814 builder.move_to(0.0, amp);
815 let mut x = 0.0;
816 let mut up = false;
817 while x < w {
818 x += step;
819 let y = if up { amp } else { 0.0 };
820 builder.line_to(x.min(w), y);
821 up = !up;
822 }
823 builder.stroke();
824 builder.restore_state();
825 }
826 AnnotSubtype::Ink => {
827 builder.save_state();
828 builder.set_stroke_color(&stroke);
829 builder.set_line_width(self.border_width);
830 if let Some(ref ink) = self.ink_list {
831 for path in ink {
832 if path.len() >= 2 {
833 let x0 = path[0] - self.rect.x0;
834 let y0 = path[1] - self.rect.y0;
835 builder.move_to(x0, y0);
836 let mut i = 2;
837 while i + 1 < path.len() {
838 let x = path[i] - self.rect.x0;
839 let y = path[i + 1] - self.rect.y0;
840 builder.line_to(x, y);
841 i += 2;
842 }
843 builder.stroke();
844 }
845 }
846 }
847 builder.restore_state();
848 }
849 AnnotSubtype::Polygon | AnnotSubtype::PolyLine => {
850 builder.save_state();
851 if let Some(ref fill) = self.interior_color {
852 builder.set_fill_color(fill);
853 }
854 builder.set_stroke_color(&stroke);
855 builder.set_line_width(self.border_width);
856 if let Some(ref dash) = self.dash_pattern {
857 builder.set_dash_pattern(dash, 0.0);
858 }
859 if let Some(ref verts) = self.vertices {
860 if verts.len() >= 2 {
861 let x0 = verts[0] - self.rect.x0;
862 let y0 = verts[1] - self.rect.y0;
863 builder.move_to(x0, y0);
864 let mut i = 2;
865 while i + 1 < verts.len() {
866 let x = verts[i] - self.rect.x0;
867 let y = verts[i + 1] - self.rect.y0;
868 builder.line_to(x, y);
869 i += 2;
870 }
871 }
872 }
873 let is_polygon = matches!(self.subtype, AnnotSubtype::Polygon);
874 if is_polygon {
875 builder.close_path();
876 if self.interior_color.is_some() {
877 builder.fill_and_stroke();
878 } else {
879 builder.stroke();
880 }
881 } else {
882 builder.stroke();
883 }
884 builder.restore_state();
885 }
886 AnnotSubtype::FreeText => {
887 let white = AppearanceColor::new(1.0, 1.0, 1.0);
889 builder.filled_stroked_rect(&white, &stroke, self.border_width);
890 if let Some(ref text) = self.contents {
893 let text_color = self.color.unwrap_or(AppearanceColor::new(0.0, 0.0, 0.0));
894 let margin = self.border_width + 2.0;
895 builder.text(text, "Helv", 12.0, margin, h - margin - 12.0, &text_color);
896 }
897 }
898 AnnotSubtype::Text => {
899 let fill = AppearanceColor::new(1.0, 1.0, 0.6); builder.filled_stroked_rect(&fill, &stroke, self.border_width);
902 }
903 AnnotSubtype::Stamp => {
904 let red = AppearanceColor::new(1.0, 0.0, 0.0);
906 builder.stroked_rect(&red, 2.0);
907 if let Some(ref name) = self.icon_name {
908 builder.text(name, "Helv", 18.0, 4.0, h / 2.0 - 9.0, &red);
909 }
910 }
911 AnnotSubtype::Link => {
912 }
915 }
916
917 if needs_gs {
918 builder.restore_state();
919 }
920 }
921}
922
923#[cfg(feature = "write")]
924enum AnnotsAction {
925 SetArray(Vec<Object>),
926 AppendIndirect(ObjectId),
927}
928
929#[cfg(feature = "write")]
936pub fn add_annotation_to_page(
937 doc: &mut Document,
938 page_num: u32,
939 annot_id: ObjectId,
940) -> Result<(), AnnotBuildError> {
941 let pages = doc.get_pages();
942 let page_count = pages.len();
943 let page_id = *pages
944 .get(&page_num)
945 .ok_or(AnnotBuildError::PageOutOfRange(page_num, page_count))?;
946
947 let annots_action = {
950 match doc.get_dictionary(page_id) {
951 Ok(page_dict) => match page_dict.get(b"Annots").ok() {
952 Some(Object::Array(arr)) => {
953 let mut new_arr = arr.clone();
954 new_arr.push(Object::Reference(annot_id));
955 AnnotsAction::SetArray(new_arr)
956 }
957 Some(Object::Reference(r)) => AnnotsAction::AppendIndirect(*r),
958 _ => AnnotsAction::SetArray(vec![Object::Reference(annot_id)]),
959 },
960 Err(_) => AnnotsAction::SetArray(vec![Object::Reference(annot_id)]),
962 }
963 };
964
965 match annots_action {
966 AnnotsAction::SetArray(arr) => {
967 if let Ok(page_dict) = doc.get_dictionary_mut(page_id) {
972 page_dict.set("Annots", Object::Array(arr));
973 } else {
974 return Err(AnnotBuildError::PageMutationFailed);
975 }
976 }
977 AnnotsAction::AppendIndirect(annots_ref) => {
978 let appended = {
980 if let Ok(Object::Array(ref mut arr)) = doc.get_object_mut(annots_ref) {
981 arr.push(Object::Reference(annot_id));
982 true
983 } else {
984 false
985 }
986 };
987
988 if !appended {
989 let existing: Vec<Object> = match doc.get_object(annots_ref) {
995 Ok(Object::Array(ref arr)) => arr.clone(),
996 _ => Vec::new(),
997 };
998 let mut new_annots = existing;
999 new_annots.push(Object::Reference(annot_id));
1000 if let Ok(page_dict) = doc.get_dictionary_mut(page_id) {
1001 page_dict.set("Annots", Object::Array(new_annots));
1002 } else {
1003 return Err(AnnotBuildError::PageMutationFailed);
1004 }
1005 }
1006 }
1007 }
1008
1009 Ok(())
1010}
1011
1012#[cfg(all(test, feature = "write"))]
1013mod tests {
1014 use super::*;
1015
1016 fn make_test_doc() -> Document {
1017 let mut doc = Document::with_version("1.7");
1018 let pages_id = doc.new_object_id();
1019
1020 let content_data = b"BT /F1 12 Tf (Test) Tj ET".to_vec();
1021 let content_stream = Stream::new(dictionary! {}, content_data);
1022 let content_id = doc.add_object(Object::Stream(content_stream));
1023
1024 let page_dict = dictionary! {
1025 "Type" => "Page",
1026 "Parent" => Object::Reference(pages_id),
1027 "MediaBox" => Object::Array(vec![
1028 Object::Integer(0), Object::Integer(0),
1029 Object::Integer(612), Object::Integer(792),
1030 ]),
1031 "Contents" => Object::Reference(content_id),
1032 "Resources" => Object::Dictionary(lopdf::Dictionary::new()),
1033 };
1034 let page_id = doc.add_object(Object::Dictionary(page_dict));
1035
1036 let pages_dict = dictionary! {
1037 "Type" => "Pages",
1038 "Count" => Object::Integer(1),
1039 "Kids" => Object::Array(vec![Object::Reference(page_id)]),
1040 };
1041 doc.objects.insert(pages_id, Object::Dictionary(pages_dict));
1042
1043 let catalog = dictionary! {
1044 "Type" => "Catalog",
1045 "Pages" => Object::Reference(pages_id),
1046 };
1047 let catalog_id = doc.add_object(Object::Dictionary(catalog));
1048 doc.trailer.set("Root", Object::Reference(catalog_id));
1049
1050 doc
1051 }
1052
1053 #[test]
1054 fn build_square_annotation() {
1055 let mut doc = make_test_doc();
1056 let rect = AnnotRect::new(100.0, 200.0, 300.0, 400.0);
1057 let annot_id = AnnotationBuilder::new(AnnotSubtype::Square, rect)
1058 .color(1.0, 0.0, 0.0)
1059 .border_width(2.0)
1060 .contents("Red square")
1061 .build(&mut doc)
1062 .unwrap();
1063
1064 let annot = doc.get_object(annot_id).unwrap();
1065 if let Object::Dictionary(d) = annot {
1066 assert_eq!(
1067 d.get(b"Subtype").unwrap(),
1068 &Object::Name(b"Square".to_vec())
1069 );
1070 assert!(d.get(b"AP").is_ok());
1071 assert!(d.get(b"C").is_ok());
1072 } else {
1073 panic!("Expected dictionary");
1074 }
1075 }
1076
1077 #[test]
1078 fn build_circle_annotation() {
1079 let mut doc = make_test_doc();
1080 let rect = AnnotRect::new(50.0, 50.0, 150.0, 150.0);
1081 let annot_id = AnnotationBuilder::new(AnnotSubtype::Circle, rect)
1082 .color(0.0, 0.0, 1.0)
1083 .interior_color(0.8, 0.8, 1.0)
1084 .build(&mut doc)
1085 .unwrap();
1086
1087 let annot = doc.get_object(annot_id).unwrap();
1088 if let Object::Dictionary(d) = annot {
1089 assert_eq!(
1090 d.get(b"Subtype").unwrap(),
1091 &Object::Name(b"Circle".to_vec())
1092 );
1093 assert!(d.get(b"IC").is_ok());
1094 } else {
1095 panic!("Expected dictionary");
1096 }
1097 }
1098
1099 #[test]
1100 fn build_with_opacity() {
1101 let mut doc = make_test_doc();
1102 let rect = AnnotRect::new(0.0, 0.0, 100.0, 100.0);
1103 let annot_id = AnnotationBuilder::new(AnnotSubtype::Square, rect)
1104 .opacity(0.5)
1105 .build(&mut doc)
1106 .unwrap();
1107
1108 let annot = doc.get_object(annot_id).unwrap();
1109 if let Object::Dictionary(d) = annot {
1110 let ca = d.get(b"CA").unwrap();
1111 assert_eq!(ca, &Object::Real(0.5));
1112 } else {
1113 panic!("Expected dictionary");
1114 }
1115 }
1116
1117 #[test]
1118 fn reject_zero_area_rect() {
1119 let mut doc = make_test_doc();
1120 let rect = AnnotRect::new(100.0, 200.0, 100.0, 400.0); let result = AnnotationBuilder::new(AnnotSubtype::Square, rect).build(&mut doc);
1122 assert!(result.is_err());
1123 }
1124
1125 #[test]
1126 fn add_annotation_to_page_creates_annots() {
1127 let mut doc = make_test_doc();
1128 let rect = AnnotRect::new(10.0, 10.0, 50.0, 50.0);
1129 let annot_id = AnnotationBuilder::new(AnnotSubtype::Square, rect)
1130 .build(&mut doc)
1131 .unwrap();
1132
1133 add_annotation_to_page(&mut doc, 1, annot_id).unwrap();
1134
1135 let pages = doc.get_pages();
1137 let page_id = pages[&1];
1138 if let Object::Dictionary(d) = doc.get_object(page_id).unwrap() {
1139 let annots = d.get(b"Annots").unwrap();
1140 if let Object::Array(arr) = annots {
1141 assert_eq!(arr.len(), 1);
1142 assert_eq!(arr[0], Object::Reference(annot_id));
1143 } else {
1144 panic!("Expected array");
1145 }
1146 }
1147 }
1148
1149 #[test]
1150 fn add_annotation_appends_to_existing_annots() {
1151 let mut doc = make_test_doc();
1152 let rect1 = AnnotRect::new(10.0, 10.0, 50.0, 50.0);
1153 let rect2 = AnnotRect::new(60.0, 60.0, 100.0, 100.0);
1154
1155 let id1 = AnnotationBuilder::new(AnnotSubtype::Square, rect1)
1156 .build(&mut doc)
1157 .unwrap();
1158 let id2 = AnnotationBuilder::new(AnnotSubtype::Circle, rect2)
1159 .build(&mut doc)
1160 .unwrap();
1161
1162 add_annotation_to_page(&mut doc, 1, id1).unwrap();
1163 add_annotation_to_page(&mut doc, 1, id2).unwrap();
1164
1165 let pages = doc.get_pages();
1166 let page_id = pages[&1];
1167 if let Object::Dictionary(d) = doc.get_object(page_id).unwrap() {
1168 if let Object::Array(arr) = d.get(b"Annots").unwrap() {
1169 assert_eq!(arr.len(), 2);
1170 } else {
1171 panic!("Expected array");
1172 }
1173 }
1174 }
1175
1176 #[test]
1177 fn invalid_page_returns_error() {
1178 let mut doc = make_test_doc();
1179 let rect = AnnotRect::new(10.0, 10.0, 50.0, 50.0);
1180 let annot_id = AnnotationBuilder::new(AnnotSubtype::Square, rect)
1181 .build(&mut doc)
1182 .unwrap();
1183
1184 let result = add_annotation_to_page(&mut doc, 99, annot_id);
1185 assert!(result.is_err());
1186 }
1187
1188 #[test]
1189 fn highlight_annotation() {
1190 let mut doc = make_test_doc();
1191 let rect = AnnotRect::new(72.0, 700.0, 400.0, 712.0);
1192 let annot_id = AnnotationBuilder::new(AnnotSubtype::Highlight, rect)
1193 .color(1.0, 1.0, 0.0)
1194 .opacity(0.4)
1195 .build(&mut doc)
1196 .unwrap();
1197
1198 let annot = doc.get_object(annot_id).unwrap();
1199 if let Object::Dictionary(d) = annot {
1200 assert_eq!(
1201 d.get(b"Subtype").unwrap(),
1202 &Object::Name(b"Highlight".to_vec())
1203 );
1204 } else {
1205 panic!("Expected dictionary");
1206 }
1207 }
1208
1209 #[test]
1210 fn custom_appearance() {
1211 let mut doc = make_test_doc();
1212 let rect = AnnotRect::new(0.0, 0.0, 100.0, 100.0);
1213 let annot_id = AnnotationBuilder::new(AnnotSubtype::Square, rect)
1214 .appearance(|b| {
1215 let red = AppearanceColor::new(1.0, 0.0, 0.0);
1216 b.filled_rect(&red);
1217 })
1218 .build(&mut doc)
1219 .unwrap();
1220
1221 assert!(doc.get_object(annot_id).is_ok());
1222 }
1223
1224 #[test]
1227 fn highlight_with_quad_points() {
1228 let mut doc = make_test_doc();
1229 let rect = AnnotRect::new(72.0, 700.0, 400.0, 712.0);
1230 let annot_id = AnnotationBuilder::highlight(rect)
1231 .quad_points_from_rect(&rect)
1232 .build(&mut doc)
1233 .unwrap();
1234
1235 let annot = doc.get_object(annot_id).unwrap();
1236 if let Object::Dictionary(d) = annot {
1237 assert_eq!(
1238 d.get(b"Subtype").unwrap(),
1239 &Object::Name(b"Highlight".to_vec())
1240 );
1241 let qp = d.get(b"QuadPoints").unwrap();
1243 if let Object::Array(arr) = qp {
1244 assert_eq!(arr.len(), 8); } else {
1246 panic!("Expected QuadPoints array");
1247 }
1248 assert!(d.get(b"CA").is_ok());
1250 } else {
1251 panic!("Expected dictionary");
1252 }
1253 }
1254
1255 #[test]
1256 fn highlight_has_multiply_blend() {
1257 let mut doc = make_test_doc();
1258 let rect = AnnotRect::new(72.0, 700.0, 400.0, 712.0);
1259 let annot_id = AnnotationBuilder::highlight(rect).build(&mut doc).unwrap();
1260
1261 let annot = doc.get_object(annot_id).unwrap();
1263 if let Object::Dictionary(d) = annot {
1264 let ap = d.get(b"AP").unwrap();
1265 if let Object::Dictionary(ap_dict) = ap {
1266 let n_ref = ap_dict.get(b"N").unwrap();
1267 if let Object::Reference(stream_id) = n_ref {
1268 let stream = doc.get_object(*stream_id).unwrap();
1269 if let Object::Stream(s) = stream {
1270 let res = s.dict.get(b"Resources").unwrap();
1271 if let Object::Dictionary(res_dict) = res {
1272 let gs = res_dict.get(b"ExtGState").unwrap();
1273 if let Object::Dictionary(gs_dict) = gs {
1274 let gs0_ref = gs_dict.get(b"GS0").unwrap();
1275 if let Object::Reference(gs0_id) = gs0_ref {
1276 let gs0 = doc.get_object(*gs0_id).unwrap();
1277 if let Object::Dictionary(gs0_dict) = gs0 {
1278 assert_eq!(
1279 gs0_dict.get(b"BM").unwrap(),
1280 &Object::Name(b"Multiply".to_vec())
1281 );
1282 return;
1283 }
1284 }
1285 }
1286 }
1287 }
1288 }
1289 }
1290 }
1291 panic!("Could not find BM /Multiply in ExtGState");
1292 }
1293
1294 #[test]
1295 fn underline_convenience() {
1296 let mut doc = make_test_doc();
1297 let rect = AnnotRect::new(72.0, 700.0, 400.0, 712.0);
1298 let annot_id = AnnotationBuilder::underline(rect)
1299 .quad_points_from_rect(&rect)
1300 .build(&mut doc)
1301 .unwrap();
1302
1303 let annot = doc.get_object(annot_id).unwrap();
1304 if let Object::Dictionary(d) = annot {
1305 assert_eq!(
1306 d.get(b"Subtype").unwrap(),
1307 &Object::Name(b"Underline".to_vec())
1308 );
1309 assert!(d.get(b"QuadPoints").is_ok());
1310 } else {
1311 panic!("Expected dictionary");
1312 }
1313 }
1314
1315 #[test]
1316 fn strikeout_convenience() {
1317 let mut doc = make_test_doc();
1318 let rect = AnnotRect::new(72.0, 700.0, 400.0, 712.0);
1319 let annot_id = AnnotationBuilder::strikeout(rect).build(&mut doc).unwrap();
1320
1321 let annot = doc.get_object(annot_id).unwrap();
1322 if let Object::Dictionary(d) = annot {
1323 assert_eq!(
1324 d.get(b"Subtype").unwrap(),
1325 &Object::Name(b"StrikeOut".to_vec())
1326 );
1327 } else {
1328 panic!("Expected dictionary");
1329 }
1330 }
1331
1332 #[test]
1333 fn squiggly_convenience() {
1334 let mut doc = make_test_doc();
1335 let rect = AnnotRect::new(72.0, 700.0, 400.0, 712.0);
1336 let annot_id = AnnotationBuilder::squiggly(rect).build(&mut doc).unwrap();
1337
1338 let annot = doc.get_object(annot_id).unwrap();
1339 if let Object::Dictionary(d) = annot {
1340 assert_eq!(
1341 d.get(b"Subtype").unwrap(),
1342 &Object::Name(b"Squiggly".to_vec())
1343 );
1344 } else {
1345 panic!("Expected dictionary");
1346 }
1347 }
1348
1349 #[test]
1350 fn multi_quad_points() {
1351 let mut doc = make_test_doc();
1352 let rect = AnnotRect::new(72.0, 688.0, 400.0, 712.0);
1353 let qp = vec![
1355 72.0, 712.0, 400.0, 712.0, 72.0, 700.0, 400.0, 700.0, 72.0, 700.0, 300.0, 700.0, 72.0, 688.0, 300.0, 688.0, ];
1358 let annot_id = AnnotationBuilder::highlight(rect)
1359 .quad_points(qp)
1360 .build(&mut doc)
1361 .unwrap();
1362
1363 let annot = doc.get_object(annot_id).unwrap();
1364 if let Object::Dictionary(d) = annot {
1365 if let Object::Array(arr) = d.get(b"QuadPoints").unwrap() {
1366 assert_eq!(arr.len(), 16); } else {
1368 panic!("Expected QuadPoints array");
1369 }
1370 }
1371 }
1372
1373 #[test]
1376 fn square_convenience() {
1377 let mut doc = make_test_doc();
1378 let rect = AnnotRect::new(100.0, 100.0, 200.0, 200.0);
1379 let annot_id = AnnotationBuilder::square(rect)
1380 .color(0.0, 0.0, 1.0)
1381 .interior_color(0.9, 0.9, 1.0)
1382 .border_width(2.0)
1383 .build(&mut doc)
1384 .unwrap();
1385
1386 let annot = doc.get_object(annot_id).unwrap();
1387 if let Object::Dictionary(d) = annot {
1388 assert_eq!(
1389 d.get(b"Subtype").unwrap(),
1390 &Object::Name(b"Square".to_vec())
1391 );
1392 assert!(d.get(b"IC").is_ok());
1393 } else {
1394 panic!("Expected dictionary");
1395 }
1396 }
1397
1398 #[test]
1399 fn circle_convenience() {
1400 let mut doc = make_test_doc();
1401 let rect = AnnotRect::new(50.0, 50.0, 150.0, 150.0);
1402 let annot_id = AnnotationBuilder::circle(rect)
1403 .color(1.0, 0.0, 0.0)
1404 .build(&mut doc)
1405 .unwrap();
1406
1407 let annot = doc.get_object(annot_id).unwrap();
1408 if let Object::Dictionary(d) = annot {
1409 assert_eq!(
1410 d.get(b"Subtype").unwrap(),
1411 &Object::Name(b"Circle".to_vec())
1412 );
1413 } else {
1414 panic!("Expected dictionary");
1415 }
1416 }
1417
1418 #[test]
1419 fn line_annotation_with_endpoints() {
1420 let mut doc = make_test_doc();
1421 let annot_id = AnnotationBuilder::line(100.0, 200.0, 400.0, 600.0)
1422 .color(1.0, 0.0, 0.0)
1423 .border_width(2.0)
1424 .build(&mut doc)
1425 .unwrap();
1426
1427 let annot = doc.get_object(annot_id).unwrap();
1428 if let Object::Dictionary(d) = annot {
1429 assert_eq!(d.get(b"Subtype").unwrap(), &Object::Name(b"Line".to_vec()));
1430 let l = d.get(b"L").unwrap();
1432 if let Object::Array(arr) = l {
1433 assert_eq!(arr.len(), 4);
1434 } else {
1435 panic!("Expected /L array");
1436 }
1437 } else {
1438 panic!("Expected dictionary");
1439 }
1440 }
1441
1442 #[test]
1443 fn line_with_endings() {
1444 let mut doc = make_test_doc();
1445 let annot_id = AnnotationBuilder::line(100.0, 300.0, 500.0, 300.0)
1446 .line_endings(LineEnding::ClosedArrow, LineEnding::OpenArrow)
1447 .build(&mut doc)
1448 .unwrap();
1449
1450 let annot = doc.get_object(annot_id).unwrap();
1451 if let Object::Dictionary(d) = annot {
1452 let le = d.get(b"LE").unwrap();
1453 if let Object::Array(arr) = le {
1454 assert_eq!(arr.len(), 2);
1455 assert_eq!(arr[0], Object::Name(b"ClosedArrow".to_vec()));
1456 assert_eq!(arr[1], Object::Name(b"OpenArrow".to_vec()));
1457 } else {
1458 panic!("Expected /LE array");
1459 }
1460 } else {
1461 panic!("Expected dictionary");
1462 }
1463 }
1464
1465 #[test]
1466 fn ink_annotation() {
1467 let mut doc = make_test_doc();
1468 let rect = AnnotRect::new(50.0, 50.0, 200.0, 200.0);
1469 let strokes = vec![
1470 vec![60.0, 60.0, 100.0, 150.0, 180.0, 80.0],
1471 vec![70.0, 70.0, 120.0, 160.0],
1472 ];
1473 let annot_id = AnnotationBuilder::ink(rect, strokes)
1474 .color(0.0, 0.5, 0.0)
1475 .border_width(3.0)
1476 .build(&mut doc)
1477 .unwrap();
1478
1479 let annot = doc.get_object(annot_id).unwrap();
1480 if let Object::Dictionary(d) = annot {
1481 assert_eq!(d.get(b"Subtype").unwrap(), &Object::Name(b"Ink".to_vec()));
1482 let ink = d.get(b"InkList").unwrap();
1483 if let Object::Array(arr) = ink {
1484 assert_eq!(arr.len(), 2); } else {
1486 panic!("Expected InkList array");
1487 }
1488 } else {
1489 panic!("Expected dictionary");
1490 }
1491 }
1492
1493 #[test]
1494 fn polygon_annotation() {
1495 let mut doc = make_test_doc();
1496 let rect = AnnotRect::new(100.0, 100.0, 300.0, 300.0);
1497 let verts = vec![100.0, 100.0, 300.0, 100.0, 200.0, 300.0];
1498 let annot_id = AnnotationBuilder::polygon(rect, verts)
1499 .color(0.0, 0.0, 1.0)
1500 .interior_color(0.8, 0.8, 1.0)
1501 .build(&mut doc)
1502 .unwrap();
1503
1504 let annot = doc.get_object(annot_id).unwrap();
1505 if let Object::Dictionary(d) = annot {
1506 assert_eq!(
1507 d.get(b"Subtype").unwrap(),
1508 &Object::Name(b"Polygon".to_vec())
1509 );
1510 let v = d.get(b"Vertices").unwrap();
1511 if let Object::Array(arr) = v {
1512 assert_eq!(arr.len(), 6);
1513 } else {
1514 panic!("Expected Vertices array");
1515 }
1516 } else {
1517 panic!("Expected dictionary");
1518 }
1519 }
1520
1521 #[test]
1522 fn polyline_annotation() {
1523 let mut doc = make_test_doc();
1524 let rect = AnnotRect::new(50.0, 50.0, 400.0, 200.0);
1525 let verts = vec![50.0, 100.0, 200.0, 180.0, 350.0, 60.0, 400.0, 150.0];
1526 let annot_id = AnnotationBuilder::polyline(rect, verts)
1527 .color(1.0, 0.5, 0.0)
1528 .build(&mut doc)
1529 .unwrap();
1530
1531 let annot = doc.get_object(annot_id).unwrap();
1532 if let Object::Dictionary(d) = annot {
1533 assert_eq!(
1534 d.get(b"Subtype").unwrap(),
1535 &Object::Name(b"PolyLine".to_vec())
1536 );
1537 } else {
1538 panic!("Expected dictionary");
1539 }
1540 }
1541
1542 #[test]
1543 fn dashed_line_annotation() {
1544 let mut doc = make_test_doc();
1545 let annot_id = AnnotationBuilder::line(72.0, 400.0, 540.0, 400.0)
1546 .dash(vec![3.0, 2.0])
1547 .build(&mut doc)
1548 .unwrap();
1549
1550 let annot = doc.get_object(annot_id).unwrap();
1551 if let Object::Dictionary(d) = annot {
1552 let bs = d.get(b"BS").unwrap();
1553 if let Object::Dictionary(bs_dict) = bs {
1554 assert_eq!(bs_dict.get(b"S").unwrap(), &Object::Name(b"D".to_vec()));
1555 assert!(bs_dict.get(b"D").is_ok());
1556 } else {
1557 panic!("Expected BS dictionary");
1558 }
1559 } else {
1560 panic!("Expected dictionary");
1561 }
1562 }
1563
1564 #[test]
1567 fn free_text_annotation() {
1568 let mut doc = make_test_doc();
1569 let rect = AnnotRect::new(72.0, 700.0, 300.0, 730.0);
1570 let annot_id = AnnotationBuilder::free_text(rect, "Hello World", 14.0)
1571 .alignment(1) .build(&mut doc)
1573 .unwrap();
1574
1575 let annot = doc.get_object(annot_id).unwrap();
1576 if let Object::Dictionary(d) = annot {
1577 assert_eq!(
1578 d.get(b"Subtype").unwrap(),
1579 &Object::Name(b"FreeText".to_vec())
1580 );
1581 assert!(d.get(b"DA").is_ok());
1582 assert_eq!(d.get(b"Q").unwrap(), &Object::Integer(1));
1583 } else {
1584 panic!("Expected dictionary");
1585 }
1586 }
1587
1588 #[test]
1589 fn sticky_note_annotation() {
1590 let mut doc = make_test_doc();
1591 let rect = AnnotRect::new(500.0, 700.0, 524.0, 724.0);
1592 let annot_id = AnnotationBuilder::sticky_note(rect, TextIcon::Comment)
1593 .contents("This is a comment")
1594 .color(1.0, 1.0, 0.0)
1595 .build(&mut doc)
1596 .unwrap();
1597
1598 let annot = doc.get_object(annot_id).unwrap();
1599 if let Object::Dictionary(d) = annot {
1600 assert_eq!(d.get(b"Subtype").unwrap(), &Object::Name(b"Text".to_vec()));
1601 assert_eq!(d.get(b"Name").unwrap(), &Object::Name(b"Comment".to_vec()));
1602 assert!(d.get(b"Contents").is_ok());
1603 } else {
1604 panic!("Expected dictionary");
1605 }
1606 }
1607
1608 #[test]
1609 fn stamp_annotation() {
1610 let mut doc = make_test_doc();
1611 let rect = AnnotRect::new(72.0, 600.0, 250.0, 650.0);
1612 let annot_id = AnnotationBuilder::stamp(rect, StampName::Approved)
1613 .build(&mut doc)
1614 .unwrap();
1615
1616 let annot = doc.get_object(annot_id).unwrap();
1617 if let Object::Dictionary(d) = annot {
1618 assert_eq!(d.get(b"Subtype").unwrap(), &Object::Name(b"Stamp".to_vec()));
1619 assert_eq!(d.get(b"Name").unwrap(), &Object::Name(b"Approved".to_vec()));
1620 } else {
1621 panic!("Expected dictionary");
1622 }
1623 }
1624
1625 #[test]
1626 fn stamp_custom_name() {
1627 let mut doc = make_test_doc();
1628 let rect = AnnotRect::new(72.0, 500.0, 250.0, 550.0);
1629 let annot_id = AnnotationBuilder::stamp_custom(rect, "ReviewNeeded")
1630 .build(&mut doc)
1631 .unwrap();
1632
1633 let annot = doc.get_object(annot_id).unwrap();
1634 if let Object::Dictionary(d) = annot {
1635 assert_eq!(
1636 d.get(b"Name").unwrap(),
1637 &Object::Name(b"ReviewNeeded".to_vec())
1638 );
1639 } else {
1640 panic!("Expected dictionary");
1641 }
1642 }
1643
1644 #[test]
1645 fn link_uri_annotation() {
1646 let mut doc = make_test_doc();
1647 let rect = AnnotRect::new(72.0, 700.0, 200.0, 712.0);
1648 let annot_id = AnnotationBuilder::link_uri(rect, "https://example.com")
1649 .color(0.0, 0.0, 1.0)
1650 .build(&mut doc)
1651 .unwrap();
1652
1653 let annot = doc.get_object(annot_id).unwrap();
1654 if let Object::Dictionary(d) = annot {
1655 assert_eq!(d.get(b"Subtype").unwrap(), &Object::Name(b"Link".to_vec()));
1656 let action = d.get(b"A").unwrap();
1657 if let Object::Dictionary(a) = action {
1658 assert_eq!(a.get(b"S").unwrap(), &Object::Name(b"URI".to_vec()));
1659 } else {
1660 panic!("Expected action dictionary");
1661 }
1662 } else {
1663 panic!("Expected dictionary");
1664 }
1665 }
1666
1667 #[test]
1668 fn link_destination_annotation() {
1669 let mut doc = make_test_doc();
1670 let rect = AnnotRect::new(72.0, 650.0, 200.0, 662.0);
1671 let annot_id = AnnotationBuilder::link_dest(rect, "chapter1")
1672 .build(&mut doc)
1673 .unwrap();
1674
1675 let annot = doc.get_object(annot_id).unwrap();
1676 if let Object::Dictionary(d) = annot {
1677 assert!(d.get(b"Dest").is_ok());
1678 } else {
1679 panic!("Expected dictionary");
1680 }
1681 }
1682}