1use crate::annotations::Annotation;
4use crate::geometry::{Point, Rectangle};
5use crate::objects::Object;
6
7#[derive(Debug, Clone, Copy, Default)]
9pub enum Icon {
10 Comment,
12 Key,
14 #[default]
16 Note,
17 Help,
19 NewParagraph,
21 Paragraph,
23 Insert,
25}
26
27impl Icon {
28 pub fn pdf_name(&self) -> &'static str {
30 match self {
31 Icon::Comment => "Comment",
32 Icon::Key => "Key",
33 Icon::Note => "Note",
34 Icon::Help => "Help",
35 Icon::NewParagraph => "NewParagraph",
36 Icon::Paragraph => "Paragraph",
37 Icon::Insert => "Insert",
38 }
39 }
40}
41
42#[derive(Debug, Clone)]
44pub struct TextAnnotation {
45 pub annotation: Annotation,
47 pub icon: Icon,
49 pub open: bool,
51 pub state_model: Option<String>,
53 pub state: Option<String>,
55}
56
57impl TextAnnotation {
58 pub fn new(position: Point) -> Self {
60 let rect = Rectangle::new(position, Point::new(position.x + 20.0, position.y + 20.0));
62
63 let annotation = Annotation::new(crate::annotations::AnnotationType::Text, rect);
64
65 Self {
66 annotation,
67 icon: Icon::default(),
68 open: false,
69 state_model: None,
70 state: None,
71 }
72 }
73
74 pub fn with_icon(mut self, icon: Icon) -> Self {
76 self.icon = icon;
77 self
78 }
79
80 pub fn open(mut self) -> Self {
82 self.open = true;
83 self
84 }
85
86 pub fn with_contents(mut self, contents: impl Into<String>) -> Self {
88 self.annotation.contents = Some(contents.into());
89 self
90 }
91
92 pub fn with_state(mut self, state_model: impl Into<String>, state: impl Into<String>) -> Self {
94 self.state_model = Some(state_model.into());
95 self.state = Some(state.into());
96 self
97 }
98
99 pub fn to_annotation(self) -> Annotation {
101 let mut annotation = self.annotation;
102
103 annotation
105 .properties
106 .set("Name", Object::Name(self.icon.pdf_name().to_string()));
107
108 annotation
110 .properties
111 .set("Open", Object::Boolean(self.open));
112
113 if let Some(state_model) = self.state_model {
115 annotation
116 .properties
117 .set("StateModel", Object::String(state_model));
118 }
119
120 if let Some(state) = self.state {
121 annotation.properties.set("State", Object::String(state));
122 }
123
124 annotation
125 }
126}
127
128#[cfg(test)]
129mod tests {
130 use super::*;
131 use crate::geometry::Point;
132
133 #[test]
134 fn test_icon_names() {
135 assert_eq!(Icon::Comment.pdf_name(), "Comment");
136 assert_eq!(Icon::Note.pdf_name(), "Note");
137 assert_eq!(Icon::Help.pdf_name(), "Help");
138 }
139
140 #[test]
141 fn test_text_annotation_creation() {
142 let position = Point::new(100.0, 700.0);
143 let text_annot = TextAnnotation::new(position)
144 .with_contents("This is a note")
145 .with_icon(Icon::Comment)
146 .open();
147
148 assert_eq!(text_annot.icon.pdf_name(), "Comment");
149 assert!(text_annot.open);
150 assert_eq!(
151 text_annot.annotation.contents,
152 Some("This is a note".to_string())
153 );
154 }
155
156 #[test]
157 fn test_text_annotation_to_annotation() {
158 let position = Point::new(50.0, 650.0);
159 let text_annot = TextAnnotation::new(position)
160 .with_contents("Review this section")
161 .with_state("Review", "Accepted");
162
163 let annotation = text_annot.to_annotation();
164 assert!(annotation.properties.get("Name").is_some());
165 assert_eq!(
166 annotation.properties.get("Open"),
167 Some(&Object::Boolean(false))
168 );
169 assert_eq!(
170 annotation.properties.get("StateModel"),
171 Some(&Object::String("Review".to_string()))
172 );
173 assert_eq!(
174 annotation.properties.get("State"),
175 Some(&Object::String("Accepted".to_string()))
176 );
177 }
178
179 #[test]
180 fn test_text_annotation_rect() {
181 let position = Point::new(200.0, 500.0);
182 let text_annot = TextAnnotation::new(position);
183
184 let rect = text_annot.annotation.rect;
185 assert_eq!(rect.lower_left.x, 200.0);
186 assert_eq!(rect.lower_left.y, 500.0);
187 assert_eq!(rect.upper_right.x, 220.0); assert_eq!(rect.upper_right.y, 520.0); }
190
191 #[test]
192 fn test_all_icon_types() {
193 let icons = [
194 Icon::Comment,
195 Icon::Key,
196 Icon::Note,
197 Icon::Help,
198 Icon::NewParagraph,
199 Icon::Paragraph,
200 Icon::Insert,
201 ];
202
203 let expected_names = [
204 "Comment",
205 "Key",
206 "Note",
207 "Help",
208 "NewParagraph",
209 "Paragraph",
210 "Insert",
211 ];
212
213 for (icon, expected) in icons.iter().zip(expected_names.iter()) {
214 assert_eq!(icon.pdf_name(), *expected);
215 }
216 }
217
218 #[test]
219 fn test_icon_default() {
220 let default_icon = Icon::default();
221 assert!(matches!(default_icon, Icon::Note));
222 assert_eq!(default_icon.pdf_name(), "Note");
223 }
224
225 #[test]
226 fn test_icon_debug_clone_copy() {
227 let icon = Icon::Help;
228
229 let debug_str = format!("{icon:?}");
231 assert_eq!(debug_str, "Help");
232
233 let cloned = icon;
235 assert!(matches!(cloned, Icon::Help));
236
237 let copied: Icon = icon; assert!(matches!(copied, Icon::Help));
240 assert!(matches!(icon, Icon::Help)); }
242
243 #[test]
244 fn test_text_annotation_default_values() {
245 let position = Point::new(0.0, 0.0);
246 let text_annot = TextAnnotation::new(position);
247
248 assert!(matches!(text_annot.icon, Icon::Note));
249 assert!(!text_annot.open);
250 assert!(text_annot.state_model.is_none());
251 assert!(text_annot.state.is_none());
252 assert!(text_annot.annotation.contents.is_none());
253 }
254
255 #[test]
256 fn test_text_annotation_builder_chain() {
257 let position = Point::new(100.0, 200.0);
258 let text_annot = TextAnnotation::new(position)
259 .with_icon(Icon::Paragraph)
260 .open()
261 .with_contents("Important paragraph")
262 .with_state("Marked", "Completed");
263
264 assert!(matches!(text_annot.icon, Icon::Paragraph));
265 assert!(text_annot.open);
266 assert_eq!(
267 text_annot.annotation.contents,
268 Some("Important paragraph".to_string())
269 );
270 assert_eq!(text_annot.state_model, Some("Marked".to_string()));
271 assert_eq!(text_annot.state, Some("Completed".to_string()));
272 }
273
274 #[test]
275 fn test_text_annotation_with_empty_contents() {
276 let position = Point::new(50.0, 50.0);
277 let text_annot = TextAnnotation::new(position).with_contents("");
278
279 assert_eq!(text_annot.annotation.contents, Some("".to_string()));
280 }
281
282 #[test]
283 fn test_text_annotation_with_long_contents() {
284 let position = Point::new(0.0, 0.0);
285 let long_text = "a".repeat(1000);
286 let text_annot = TextAnnotation::new(position).with_contents(long_text.clone());
287
288 assert_eq!(text_annot.annotation.contents, Some(long_text));
289 }
290
291 #[test]
292 fn test_text_annotation_state_variations() {
293 let position = Point::new(100.0, 100.0);
294
295 let review_annot = TextAnnotation::new(position).with_state("Review", "Accepted");
297 assert_eq!(review_annot.state_model, Some("Review".to_string()));
298 assert_eq!(review_annot.state, Some("Accepted".to_string()));
299
300 let marked_annot = TextAnnotation::new(position).with_state("Marked", "Completed");
302 assert_eq!(marked_annot.state_model, Some("Marked".to_string()));
303 assert_eq!(marked_annot.state, Some("Completed".to_string()));
304 }
305
306 #[test]
307 fn test_text_annotation_different_positions() {
308 let positions = vec![
309 Point::new(0.0, 0.0),
310 Point::new(-100.0, -100.0),
311 Point::new(1000.0, 2000.0),
312 Point::new(0.5, 0.5),
313 ];
314
315 for pos in positions {
316 let text_annot = TextAnnotation::new(pos);
317 assert_eq!(text_annot.annotation.rect.lower_left.x, pos.x);
318 assert_eq!(text_annot.annotation.rect.lower_left.y, pos.y);
319 assert_eq!(text_annot.annotation.rect.upper_right.x, pos.x + 20.0);
320 assert_eq!(text_annot.annotation.rect.upper_right.y, pos.y + 20.0);
321 }
322 }
323
324 #[test]
325 fn test_to_annotation_without_state() {
326 let position = Point::new(150.0, 350.0);
327 let text_annot = TextAnnotation::new(position)
328 .with_icon(Icon::Key)
329 .with_contents("Key information");
330
331 let annotation = text_annot.to_annotation();
332
333 assert_eq!(
334 annotation.properties.get("Name"),
335 Some(&Object::Name("Key".to_string()))
336 );
337 assert_eq!(
338 annotation.properties.get("Open"),
339 Some(&Object::Boolean(false))
340 );
341 assert!(annotation.properties.get("StateModel").is_none());
342 assert!(annotation.properties.get("State").is_none());
343 }
344
345 #[test]
346 fn test_to_annotation_open_state() {
347 let position = Point::new(75.0, 125.0);
348 let text_annot = TextAnnotation::new(position).open();
349
350 let annotation = text_annot.to_annotation();
351
352 assert_eq!(
353 annotation.properties.get("Open"),
354 Some(&Object::Boolean(true))
355 );
356 }
357
358 #[test]
359 fn test_text_annotation_clone() {
360 let position = Point::new(25.0, 75.0);
361 let text_annot = TextAnnotation::new(position)
362 .with_icon(Icon::Insert)
363 .open()
364 .with_contents("Insert here")
365 .with_state("Review", "Rejected");
366
367 let cloned = text_annot.clone();
368
369 assert!(matches!(cloned.icon, Icon::Insert));
370 assert_eq!(cloned.open, text_annot.open);
371 assert_eq!(cloned.annotation.contents, text_annot.annotation.contents);
372 assert_eq!(cloned.state_model, text_annot.state_model);
373 assert_eq!(cloned.state, text_annot.state);
374 }
375
376 #[test]
377 fn test_text_annotation_debug() {
378 let position = Point::new(300.0, 400.0);
379 let text_annot = TextAnnotation::new(position).with_icon(Icon::NewParagraph);
380
381 let debug_str = format!("{text_annot:?}");
382 assert!(debug_str.contains("TextAnnotation"));
383 assert!(debug_str.contains("NewParagraph"));
384 }
385
386 #[test]
387 fn test_annotation_type_consistency() {
388 let position = Point::new(10.0, 20.0);
389 let text_annot = TextAnnotation::new(position);
390
391 assert_eq!(
393 text_annot.annotation.annotation_type,
394 crate::annotations::AnnotationType::Text
395 );
396 }
397
398 #[test]
399 fn test_with_contents_string_types() {
400 let position = Point::new(0.0, 0.0);
401
402 let annot1 = TextAnnotation::new(position).with_contents("string slice");
404 assert_eq!(annot1.annotation.contents, Some("string slice".to_string()));
405
406 let annot2 = TextAnnotation::new(position).with_contents(String::from("owned string"));
408 assert_eq!(annot2.annotation.contents, Some("owned string".to_string()));
409
410 let content = String::from("ref string");
412 let annot3 = TextAnnotation::new(position).with_contents(&content);
413 assert_eq!(annot3.annotation.contents, Some("ref string".to_string()));
414 }
415
416 #[test]
417 fn test_with_state_string_types() {
418 let position = Point::new(0.0, 0.0);
419
420 let annot1 = TextAnnotation::new(position).with_state("Review", "Accepted");
422 assert_eq!(annot1.state_model, Some("Review".to_string()));
423 assert_eq!(annot1.state, Some("Accepted".to_string()));
424
425 let annot2 =
427 TextAnnotation::new(position).with_state(String::from("Marked"), String::from("None"));
428 assert_eq!(annot2.state_model, Some("Marked".to_string()));
429 assert_eq!(annot2.state, Some("None".to_string()));
430 }
431
432 #[test]
433 fn test_special_characters_in_contents() {
434 let position = Point::new(0.0, 0.0);
435 let special_content = "Line 1\nLine 2\tTabbed\r\nSpecial chars: ()[]{}\\";
436
437 let text_annot = TextAnnotation::new(position).with_contents(special_content);
438
439 assert_eq!(
440 text_annot.annotation.contents,
441 Some(special_content.to_string())
442 );
443 }
444
445 #[test]
446 fn test_unicode_in_contents() {
447 let position = Point::new(0.0, 0.0);
448 let unicode_content = "Unicode: 你好世界 🌍 Ñoño";
449
450 let text_annot = TextAnnotation::new(position).with_contents(unicode_content);
451
452 assert_eq!(
453 text_annot.annotation.contents,
454 Some(unicode_content.to_string())
455 );
456 }
457
458 #[test]
459 fn test_all_state_combinations() {
460 let position = Point::new(0.0, 0.0);
461
462 let state_combinations = vec![
463 (
464 "Review",
465 vec!["Accepted", "Rejected", "Cancelled", "Completed", "None"],
466 ),
467 ("Marked", vec!["Marked", "Unmarked"]),
468 ];
469
470 for (model, states) in state_combinations {
471 for state in states {
472 let text_annot = TextAnnotation::new(position).with_state(model, state);
473
474 let annotation = text_annot.to_annotation();
475 assert_eq!(
476 annotation.properties.get("StateModel"),
477 Some(&Object::String(model.to_string()))
478 );
479 assert_eq!(
480 annotation.properties.get("State"),
481 Some(&Object::String(state.to_string()))
482 );
483 }
484 }
485 }
486
487 #[test]
488 fn test_extreme_positions() {
489 let extreme_positions = vec![
490 Point::new(f64::MIN, f64::MIN),
491 Point::new(f64::MAX, f64::MAX),
492 Point::new(0.0, f64::MAX),
493 Point::new(f64::MAX, 0.0),
494 Point::new(-1e10, -1e10),
495 Point::new(1e10, 1e10),
496 ];
497
498 for pos in extreme_positions {
499 let text_annot = TextAnnotation::new(pos);
500 assert_eq!(text_annot.annotation.rect.lower_left.x, pos.x);
501 assert_eq!(text_annot.annotation.rect.lower_left.y, pos.y);
502 assert_eq!(text_annot.annotation.rect.upper_right.x, pos.x + 20.0);
504 assert_eq!(text_annot.annotation.rect.upper_right.y, pos.y + 20.0);
505 }
506 }
507
508 #[test]
509 fn test_pdf_properties_structure() {
510 let position = Point::new(100.0, 100.0);
511 let text_annot = TextAnnotation::new(position)
512 .with_icon(Icon::Comment)
513 .open()
514 .with_contents("Test comment")
515 .with_state("Review", "Accepted");
516
517 let annotation = text_annot.to_annotation();
518
519 assert!(annotation.properties.get("Name").is_some());
521 assert!(annotation.properties.get("Open").is_some());
522 assert!(annotation.properties.get("StateModel").is_some());
523 assert!(annotation.properties.get("State").is_some());
524
525 assert!(matches!(
527 annotation.properties.get("Name"),
528 Some(Object::Name(_))
529 ));
530 assert!(matches!(
531 annotation.properties.get("Open"),
532 Some(Object::Boolean(_))
533 ));
534 assert!(matches!(
535 annotation.properties.get("StateModel"),
536 Some(Object::String(_))
537 ));
538 assert!(matches!(
539 annotation.properties.get("State"),
540 Some(Object::String(_))
541 ));
542 }
543
544 #[test]
545 fn test_repeated_builder_calls() {
546 let position = Point::new(50.0, 50.0);
547
548 let text_annot = TextAnnotation::new(position)
550 .with_icon(Icon::Note)
551 .with_icon(Icon::Help) .with_contents("First")
553 .with_contents("Second") .with_state("Review", "Accepted")
555 .with_state("Marked", "Completed"); assert!(matches!(text_annot.icon, Icon::Help));
558 assert_eq!(text_annot.annotation.contents, Some("Second".to_string()));
559 assert_eq!(text_annot.state_model, Some("Marked".to_string()));
560 assert_eq!(text_annot.state, Some("Completed".to_string()));
561 }
562}