1use crate::error::{JustPdfError, Result};
2use crate::object::{IndirectRef, PdfDict, PdfObject};
3use crate::page::Rect;
4use crate::writer::modify::DocumentModifier;
5
6use super::types::*;
7
8pub struct AnnotationBuilder {
10 annot_type: AnnotationType,
11 rect: Rect,
12 contents: Option<String>,
13 color: Option<AnnotColor>,
14 border: Option<BorderStyle>,
15 flags: AnnotationFlags,
16 data: AnnotationData,
17}
18
19impl AnnotationBuilder {
20 fn new(annot_type: AnnotationType, rect: Rect, data: AnnotationData) -> Self {
21 Self {
22 annot_type,
23 rect,
24 contents: None,
25 color: None,
26 border: None,
27 flags: AnnotationFlags(AnnotationFlags::PRINT),
28 data,
29 }
30 }
31
32 pub fn highlight(rect: Rect, quad_points: Vec<f64>, color: AnnotColor) -> Self {
35 let mut b = Self::new(
36 AnnotationType::Highlight,
37 rect,
38 AnnotationData::Markup { quad_points },
39 );
40 b.color = Some(color);
41 b
42 }
43
44 pub fn underline(rect: Rect, quad_points: Vec<f64>, color: AnnotColor) -> Self {
45 let mut b = Self::new(
46 AnnotationType::Underline,
47 rect,
48 AnnotationData::Markup { quad_points },
49 );
50 b.color = Some(color);
51 b
52 }
53
54 pub fn strike_out(rect: Rect, quad_points: Vec<f64>, color: AnnotColor) -> Self {
55 let mut b = Self::new(
56 AnnotationType::StrikeOut,
57 rect,
58 AnnotationData::Markup { quad_points },
59 );
60 b.color = Some(color);
61 b
62 }
63
64 pub fn squiggly(rect: Rect, quad_points: Vec<f64>, color: AnnotColor) -> Self {
65 let mut b = Self::new(
66 AnnotationType::Squiggly,
67 rect,
68 AnnotationData::Markup { quad_points },
69 );
70 b.color = Some(color);
71 b
72 }
73
74 pub fn text(rect: Rect, contents: &str) -> Self {
75 let mut b = Self::new(AnnotationType::Text, rect, AnnotationData::None);
76 b.contents = Some(contents.to_string());
77 b
78 }
79
80 pub fn free_text(rect: Rect, text: &str, da: &str) -> Self {
81 let mut b = Self::new(
82 AnnotationType::FreeText,
83 rect,
84 AnnotationData::FreeText {
85 da: da.to_string(),
86 justification: 0,
87 },
88 );
89 b.contents = Some(text.to_string());
90 b
91 }
92
93 pub fn line(start: (f64, f64), end: (f64, f64)) -> Self {
94 let rect = Rect {
95 llx: start.0.min(end.0),
96 lly: start.1.min(end.1),
97 urx: start.0.max(end.0),
98 ury: start.1.max(end.1),
99 };
100 Self::new(
101 AnnotationType::Line,
102 rect,
103 AnnotationData::Line {
104 start,
105 end,
106 line_endings: (LineEndingStyle::None, LineEndingStyle::None),
107 leader_line_length: 0.0,
108 leader_line_extension: 0.0,
109 caption: false,
110 interior_color: None,
111 },
112 )
113 }
114
115 pub fn square(rect: Rect) -> Self {
116 Self::new(
117 AnnotationType::Square,
118 rect,
119 AnnotationData::Shape {
120 vertices: Vec::new(),
121 interior_color: None,
122 },
123 )
124 }
125
126 pub fn circle(rect: Rect) -> Self {
127 Self::new(
128 AnnotationType::Circle,
129 rect,
130 AnnotationData::Shape {
131 vertices: Vec::new(),
132 interior_color: None,
133 },
134 )
135 }
136
137 pub fn polygon(rect: Rect, vertices: Vec<(f64, f64)>) -> Self {
138 Self::new(
139 AnnotationType::Polygon,
140 rect,
141 AnnotationData::Shape {
142 vertices,
143 interior_color: None,
144 },
145 )
146 }
147
148 pub fn polyline(rect: Rect, vertices: Vec<(f64, f64)>) -> Self {
149 Self::new(
150 AnnotationType::PolyLine,
151 rect,
152 AnnotationData::Shape {
153 vertices,
154 interior_color: None,
155 },
156 )
157 }
158
159 pub fn ink(rect: Rect, ink_list: Vec<Vec<(f64, f64)>>) -> Self {
160 Self::new(AnnotationType::Ink, rect, AnnotationData::Ink { ink_list })
161 }
162
163 pub fn stamp(rect: Rect, stamp_name: &str) -> Self {
164 Self::new(
165 AnnotationType::Stamp,
166 rect,
167 AnnotationData::Stamp {
168 icon_name: stamp_name.to_string(),
169 },
170 )
171 }
172
173 pub fn link_uri(rect: Rect, uri: &str) -> Self {
174 Self::new(
175 AnnotationType::Link,
176 rect,
177 AnnotationData::Link {
178 uri: Some(uri.to_string()),
179 dest: None,
180 },
181 )
182 }
183
184 pub fn link_goto(rect: Rect, dest: PdfObject) -> Self {
185 Self::new(
186 AnnotationType::Link,
187 rect,
188 AnnotationData::Link {
189 uri: None,
190 dest: Some(dest),
191 },
192 )
193 }
194
195 pub fn file_attachment(rect: Rect, fs_ref: IndirectRef, icon_name: &str) -> Self {
196 Self::new(
197 AnnotationType::FileAttachment,
198 rect,
199 AnnotationData::FileAttachment {
200 fs_ref: Some(fs_ref),
201 icon_name: icon_name.to_string(),
202 },
203 )
204 }
205
206 pub fn redact(rect: Rect) -> Self {
207 Self::new(
208 AnnotationType::Redact,
209 rect,
210 AnnotationData::Redact {
211 overlay_text: None,
212 repeat: false,
213 interior_color: None,
214 },
215 )
216 }
217
218 pub fn contents(mut self, contents: &str) -> Self {
221 self.contents = Some(contents.to_string());
222 self
223 }
224
225 pub fn color(mut self, color: AnnotColor) -> Self {
226 self.color = Some(color);
227 self
228 }
229
230 pub fn border(mut self, border: BorderStyle) -> Self {
231 self.border = Some(border);
232 self
233 }
234
235 pub fn flags(mut self, flags: AnnotationFlags) -> Self {
236 self.flags = flags;
237 self
238 }
239
240 pub fn line_endings(mut self, start: LineEndingStyle, end: LineEndingStyle) -> Self {
241 if let AnnotationData::Line {
242 ref mut line_endings,
243 ..
244 } = self.data
245 {
246 *line_endings = (start, end);
247 }
248 self
249 }
250
251 pub fn interior_color(mut self, color: AnnotColor) -> Self {
252 match &mut self.data {
253 AnnotationData::Line {
254 interior_color,
255 ..
256 }
257 | AnnotationData::Shape {
258 interior_color,
259 ..
260 }
261 | AnnotationData::Redact {
262 interior_color,
263 ..
264 } => {
265 *interior_color = Some(color);
266 }
267 _ => {}
268 }
269 self
270 }
271
272 pub fn overlay_text(mut self, text: &str) -> Self {
273 if let AnnotationData::Redact {
274 overlay_text,
275 ..
276 } = &mut self.data
277 {
278 *overlay_text = Some(text.to_string());
279 }
280 self
281 }
282
283 pub fn justification(mut self, q: i64) -> Self {
284 if let AnnotationData::FreeText {
285 justification,
286 ..
287 } = &mut self.data
288 {
289 *justification = q;
290 }
291 self
292 }
293
294 pub fn build_dict(&self) -> PdfDict {
296 let mut dict = PdfDict::new();
297 dict.insert(b"Type".to_vec(), PdfObject::Name(b"Annot".to_vec()));
298 dict.insert(
299 b"Subtype".to_vec(),
300 PdfObject::Name(self.annot_type.to_name().to_vec()),
301 );
302 dict.insert(
303 b"Rect".to_vec(),
304 PdfObject::Array(vec![
305 PdfObject::Real(self.rect.llx),
306 PdfObject::Real(self.rect.lly),
307 PdfObject::Real(self.rect.urx),
308 PdfObject::Real(self.rect.ury),
309 ]),
310 );
311
312 if self.flags.0 != 0 {
313 dict.insert(b"F".to_vec(), PdfObject::Integer(self.flags.0 as i64));
314 }
315
316 if let Some(ref contents) = self.contents {
317 dict.insert(
318 b"Contents".to_vec(),
319 PdfObject::String(contents.as_bytes().to_vec()),
320 );
321 }
322
323 if let Some(ref color) = self.color {
324 dict.insert(b"C".to_vec(), PdfObject::Array(color.to_pdf_array()));
325 }
326
327 if let Some(ref border) = self.border {
328 let mut bs = PdfDict::new();
329 bs.insert(b"W".to_vec(), PdfObject::Real(border.width));
330 bs.insert(
331 b"S".to_vec(),
332 PdfObject::Name(border.style.to_name().to_vec()),
333 );
334 if !border.dash_pattern.is_empty() {
335 bs.insert(
336 b"D".to_vec(),
337 PdfObject::Array(
338 border
339 .dash_pattern
340 .iter()
341 .map(|&v| PdfObject::Real(v))
342 .collect(),
343 ),
344 );
345 }
346 dict.insert(b"BS".to_vec(), PdfObject::Dict(bs));
347 }
348
349 match &self.data {
351 AnnotationData::Markup { quad_points } => {
352 if !quad_points.is_empty() {
353 dict.insert(
354 b"QuadPoints".to_vec(),
355 PdfObject::Array(
356 quad_points.iter().map(|&v| PdfObject::Real(v)).collect(),
357 ),
358 );
359 }
360 }
361 AnnotationData::Line {
362 start,
363 end,
364 line_endings,
365 leader_line_length,
366 leader_line_extension,
367 caption,
368 interior_color,
369 } => {
370 dict.insert(
371 b"L".to_vec(),
372 PdfObject::Array(vec![
373 PdfObject::Real(start.0),
374 PdfObject::Real(start.1),
375 PdfObject::Real(end.0),
376 PdfObject::Real(end.1),
377 ]),
378 );
379 dict.insert(
380 b"LE".to_vec(),
381 PdfObject::Array(vec![
382 PdfObject::Name(line_endings.0.to_name().to_vec()),
383 PdfObject::Name(line_endings.1.to_name().to_vec()),
384 ]),
385 );
386 if *leader_line_length != 0.0 {
387 dict.insert(b"LL".to_vec(), PdfObject::Real(*leader_line_length));
388 }
389 if *leader_line_extension != 0.0 {
390 dict.insert(b"LLE".to_vec(), PdfObject::Real(*leader_line_extension));
391 }
392 if *caption {
393 dict.insert(b"Cap".to_vec(), PdfObject::Bool(true));
394 }
395 if let Some(ic) = interior_color {
396 dict.insert(b"IC".to_vec(), PdfObject::Array(ic.to_pdf_array()));
397 }
398 }
399 AnnotationData::Ink { ink_list } => {
400 let ink_arr: Vec<PdfObject> = ink_list
401 .iter()
402 .map(|stroke| {
403 let coords: Vec<PdfObject> = stroke
404 .iter()
405 .flat_map(|&(x, y)| vec![PdfObject::Real(x), PdfObject::Real(y)])
406 .collect();
407 PdfObject::Array(coords)
408 })
409 .collect();
410 dict.insert(b"InkList".to_vec(), PdfObject::Array(ink_arr));
411 }
412 AnnotationData::Link { uri, dest } => {
413 if let Some(uri) = uri {
414 let mut action = PdfDict::new();
415 action.insert(b"S".to_vec(), PdfObject::Name(b"URI".to_vec()));
416 action.insert(
417 b"URI".to_vec(),
418 PdfObject::String(uri.as_bytes().to_vec()),
419 );
420 dict.insert(b"A".to_vec(), PdfObject::Dict(action));
421 } else if let Some(dest) = dest {
422 dict.insert(b"Dest".to_vec(), dest.clone());
423 }
424 }
425 AnnotationData::FreeText { da, justification } => {
426 dict.insert(b"DA".to_vec(), PdfObject::String(da.as_bytes().to_vec()));
427 if *justification != 0 {
428 dict.insert(b"Q".to_vec(), PdfObject::Integer(*justification));
429 }
430 }
431 AnnotationData::FileAttachment { fs_ref, icon_name } => {
432 if let Some(r) = fs_ref {
433 dict.insert(b"FS".to_vec(), PdfObject::Reference(r.clone()));
434 }
435 dict.insert(
436 b"Name".to_vec(),
437 PdfObject::Name(icon_name.as_bytes().to_vec()),
438 );
439 }
440 AnnotationData::Stamp { icon_name } => {
441 dict.insert(
442 b"Name".to_vec(),
443 PdfObject::Name(icon_name.as_bytes().to_vec()),
444 );
445 }
446 AnnotationData::Shape {
447 vertices,
448 interior_color,
449 } => {
450 if !vertices.is_empty() {
451 let coords: Vec<PdfObject> = vertices
452 .iter()
453 .flat_map(|&(x, y)| vec![PdfObject::Real(x), PdfObject::Real(y)])
454 .collect();
455 dict.insert(b"Vertices".to_vec(), PdfObject::Array(coords));
456 }
457 if let Some(ic) = interior_color {
458 dict.insert(b"IC".to_vec(), PdfObject::Array(ic.to_pdf_array()));
459 }
460 }
461 AnnotationData::Redact {
462 overlay_text,
463 repeat,
464 interior_color,
465 } => {
466 if let Some(text) = overlay_text {
467 dict.insert(
468 b"OverlayText".to_vec(),
469 PdfObject::String(text.as_bytes().to_vec()),
470 );
471 }
472 if *repeat {
473 dict.insert(b"Repeat".to_vec(), PdfObject::Bool(true));
474 }
475 if let Some(ic) = interior_color {
476 dict.insert(b"IC".to_vec(), PdfObject::Array(ic.to_pdf_array()));
477 }
478 }
479 AnnotationData::None => {}
480 }
481
482 dict
483 }
484}
485
486pub fn add_annotation(
488 modifier: &mut DocumentModifier,
489 page_obj_num: u32,
490 builder: AnnotationBuilder,
491) -> Result<IndirectRef> {
492 let annot_dict = builder.build_dict();
493
494 let ap_ref = super::appearance::generate_appearance(&annot_dict, modifier)?;
496
497 let mut final_dict = annot_dict;
498 if let Some(ap_ref) = ap_ref {
499 let mut ap_dict = PdfDict::new();
500 ap_dict.insert(b"N".to_vec(), PdfObject::Reference(ap_ref));
501 final_dict.insert(b"AP".to_vec(), PdfObject::Dict(ap_dict));
502 }
503
504 let annot_ref = modifier.add_object(PdfObject::Dict(final_dict));
505
506 let page_obj = modifier
508 .find_object_pub(page_obj_num)
509 .cloned()
510 .unwrap_or(PdfObject::Null);
511
512 if let PdfObject::Dict(mut page_dict) = page_obj {
513 let mut annots = match page_dict.remove(b"Annots") {
514 Some(PdfObject::Array(arr)) => arr,
515 _ => Vec::new(),
516 };
517 annots.push(PdfObject::Reference(annot_ref.clone()));
518 page_dict.insert(b"Annots".to_vec(), PdfObject::Array(annots));
519 modifier.set_object(page_obj_num, PdfObject::Dict(page_dict));
520 }
521
522 Ok(annot_ref)
523}
524
525pub fn delete_annotation(
527 modifier: &mut DocumentModifier,
528 page_obj_num: u32,
529 annot_index: usize,
530) -> Result<()> {
531 let page_obj = modifier
532 .find_object_pub(page_obj_num)
533 .cloned()
534 .unwrap_or(PdfObject::Null);
535
536 if let PdfObject::Dict(mut page_dict) = page_obj {
537 if let Some(PdfObject::Array(mut annots)) = page_dict.remove(b"Annots") {
538 if annot_index >= annots.len() {
539 return Err(JustPdfError::AnnotationError {
540 detail: format!(
541 "annotation index {annot_index} out of range ({})",
542 annots.len()
543 ),
544 });
545 }
546 annots.remove(annot_index);
547 if !annots.is_empty() {
548 page_dict.insert(b"Annots".to_vec(), PdfObject::Array(annots));
549 }
550 modifier.set_object(page_obj_num, PdfObject::Dict(page_dict));
551 Ok(())
552 } else {
553 Err(JustPdfError::AnnotationError {
554 detail: "page has no annotations".into(),
555 })
556 }
557 } else {
558 Err(JustPdfError::AnnotationError {
559 detail: "invalid page object".into(),
560 })
561 }
562}
563
564#[cfg(test)]
565mod tests {
566 use super::*;
567
568 #[test]
569 fn test_build_highlight_dict() {
570 let rect = Rect {
571 llx: 100.0,
572 lly: 200.0,
573 urx: 300.0,
574 ury: 220.0,
575 };
576 let qp = vec![100.0, 220.0, 300.0, 220.0, 100.0, 200.0, 300.0, 200.0];
577 let builder =
578 AnnotationBuilder::highlight(rect, qp, AnnotColor::Rgb(1.0, 1.0, 0.0));
579 let dict = builder.build_dict();
580
581 assert_eq!(dict.get_name(b"Subtype"), Some(b"Highlight".as_slice()));
582 assert!(dict.get_array(b"Rect").is_some());
583 assert!(dict.get_array(b"QuadPoints").is_some());
584 assert!(dict.get_array(b"C").is_some());
585 }
586
587 #[test]
588 fn test_build_ink_dict() {
589 let rect = Rect {
590 llx: 0.0,
591 lly: 0.0,
592 urx: 100.0,
593 ury: 100.0,
594 };
595 let ink_list = vec![vec![(10.0, 20.0), (30.0, 40.0), (50.0, 60.0)]];
596 let builder = AnnotationBuilder::ink(rect, ink_list)
597 .color(AnnotColor::Rgb(1.0, 0.0, 0.0))
598 .contents("Test ink");
599 let dict = builder.build_dict();
600
601 assert_eq!(dict.get_name(b"Subtype"), Some(b"Ink".as_slice()));
602 assert!(dict.get_array(b"InkList").is_some());
603 assert!(dict.get_array(b"C").is_some());
604 assert!(dict.get(b"Contents").is_some());
605 }
606
607 #[test]
608 fn test_build_line_dict() {
609 let builder = AnnotationBuilder::line((100.0, 100.0), (300.0, 300.0))
610 .line_endings(LineEndingStyle::OpenArrow, LineEndingStyle::ClosedArrow)
611 .color(AnnotColor::Rgb(0.0, 0.0, 1.0));
612 let dict = builder.build_dict();
613
614 assert_eq!(dict.get_name(b"Subtype"), Some(b"Line".as_slice()));
615 assert!(dict.get_array(b"L").is_some());
616 assert!(dict.get_array(b"LE").is_some());
617 }
618
619 #[test]
620 fn test_build_link_uri_dict() {
621 let rect = Rect {
622 llx: 72.0,
623 lly: 700.0,
624 urx: 200.0,
625 ury: 720.0,
626 };
627 let builder = AnnotationBuilder::link_uri(rect, "https://example.com");
628 let dict = builder.build_dict();
629
630 assert_eq!(dict.get_name(b"Subtype"), Some(b"Link".as_slice()));
631 let action = dict.get_dict(b"A").unwrap();
632 assert_eq!(action.get_name(b"S"), Some(b"URI".as_slice()));
633 }
634
635 #[test]
636 fn test_build_redact_dict() {
637 let rect = Rect {
638 llx: 100.0,
639 lly: 200.0,
640 urx: 400.0,
641 ury: 220.0,
642 };
643 let builder = AnnotationBuilder::redact(rect)
644 .overlay_text("REDACTED")
645 .interior_color(AnnotColor::Rgb(0.0, 0.0, 0.0));
646 let dict = builder.build_dict();
647
648 assert_eq!(dict.get_name(b"Subtype"), Some(b"Redact".as_slice()));
649 assert!(dict.get(b"OverlayText").is_some());
650 assert!(dict.get_array(b"IC").is_some());
651 }
652
653 #[test]
654 fn test_build_stamp_dict() {
655 let rect = Rect {
656 llx: 100.0,
657 lly: 600.0,
658 urx: 250.0,
659 ury: 650.0,
660 };
661 let builder = AnnotationBuilder::stamp(rect, "Approved");
662 let dict = builder.build_dict();
663
664 assert_eq!(dict.get_name(b"Subtype"), Some(b"Stamp".as_slice()));
665 assert_eq!(dict.get_name(b"Name"), Some(b"Approved".as_slice()));
666 }
667}