1use crate::annotations::Annotation;
4use crate::geometry::Rectangle;
5use crate::graphics::Color;
6use crate::objects::Object;
7
8#[derive(Debug, Clone, Copy)]
10pub enum MarkupType {
11 Highlight,
13 Underline,
15 StrikeOut,
17 Squiggly,
19}
20
21impl MarkupType {
22 pub fn annotation_type(&self) -> crate::annotations::AnnotationType {
24 match self {
25 MarkupType::Highlight => crate::annotations::AnnotationType::Highlight,
26 MarkupType::Underline => crate::annotations::AnnotationType::Underline,
27 MarkupType::StrikeOut => crate::annotations::AnnotationType::StrikeOut,
28 MarkupType::Squiggly => crate::annotations::AnnotationType::Squiggly,
29 }
30 }
31}
32
33#[derive(Debug, Clone)]
35pub struct QuadPoints {
36 pub points: Vec<f64>,
38}
39
40impl QuadPoints {
41 pub fn from_rect(rect: &Rectangle) -> Self {
43 let points = vec![
45 rect.lower_left.x,
46 rect.lower_left.y, rect.upper_right.x,
48 rect.lower_left.y, rect.upper_right.x,
50 rect.upper_right.y, rect.lower_left.x,
52 rect.upper_right.y, ];
54
55 Self { points }
56 }
57
58 pub fn from_rects(rects: &[Rectangle]) -> Self {
60 let mut points = Vec::new();
61
62 for rect in rects {
63 points.extend_from_slice(&[
64 rect.lower_left.x,
65 rect.lower_left.y,
66 rect.upper_right.x,
67 rect.lower_left.y,
68 rect.upper_right.x,
69 rect.upper_right.y,
70 rect.lower_left.x,
71 rect.upper_right.y,
72 ]);
73 }
74
75 Self { points }
76 }
77
78 pub fn to_array(&self) -> Object {
80 let objects: Vec<Object> = self.points.iter().map(|&p| Object::Real(p)).collect();
81 Object::Array(objects)
82 }
83}
84
85#[derive(Debug, Clone)]
87pub struct MarkupAnnotation {
88 pub annotation: Annotation,
90 pub markup_type: MarkupType,
92 pub quad_points: QuadPoints,
94 pub author: Option<String>,
96 pub subject: Option<String>,
98}
99
100impl MarkupAnnotation {
101 pub fn new(markup_type: MarkupType, rect: Rectangle, quad_points: QuadPoints) -> Self {
103 let annotation_type = markup_type.annotation_type();
104 let mut annotation = Annotation::new(annotation_type, rect);
105
106 annotation.color = Some(match markup_type {
108 MarkupType::Highlight => Color::Rgb(1.0, 1.0, 0.0), MarkupType::Underline => Color::Rgb(0.0, 0.0, 1.0), MarkupType::StrikeOut => Color::Rgb(1.0, 0.0, 0.0), MarkupType::Squiggly => Color::Rgb(0.0, 1.0, 0.0), });
113
114 Self {
115 annotation,
116 markup_type,
117 quad_points,
118 author: None,
119 subject: None,
120 }
121 }
122
123 pub fn highlight(rect: Rectangle) -> Self {
125 let quad_points = QuadPoints::from_rect(&rect);
126 Self::new(MarkupType::Highlight, rect, quad_points)
127 }
128
129 pub fn underline(rect: Rectangle) -> Self {
131 let quad_points = QuadPoints::from_rect(&rect);
132 Self::new(MarkupType::Underline, rect, quad_points)
133 }
134
135 pub fn strikeout(rect: Rectangle) -> Self {
137 let quad_points = QuadPoints::from_rect(&rect);
138 Self::new(MarkupType::StrikeOut, rect, quad_points)
139 }
140
141 pub fn squiggly(rect: Rectangle) -> Self {
143 let quad_points = QuadPoints::from_rect(&rect);
144 Self::new(MarkupType::Squiggly, rect, quad_points)
145 }
146
147 pub fn with_author(mut self, author: impl Into<String>) -> Self {
149 self.author = Some(author.into());
150 self
151 }
152
153 pub fn with_subject(mut self, subject: impl Into<String>) -> Self {
155 self.subject = Some(subject.into());
156 self
157 }
158
159 pub fn with_contents(mut self, contents: impl Into<String>) -> Self {
161 self.annotation.contents = Some(contents.into());
162 self
163 }
164
165 pub fn with_color(mut self, color: Color) -> Self {
167 self.annotation.color = Some(color);
168 self
169 }
170
171 pub fn to_annotation(self) -> Annotation {
173 let mut annotation = self.annotation;
174
175 annotation
177 .properties
178 .set("QuadPoints", self.quad_points.to_array());
179
180 if let Some(author) = self.author {
182 annotation.properties.set("T", Object::String(author));
183 }
184
185 if let Some(subject) = self.subject {
187 annotation.properties.set("Subj", Object::String(subject));
188 }
189
190 annotation
191 }
192}
193
194#[cfg(test)]
195mod tests {
196 use super::*;
197 use crate::geometry::Point;
198
199 #[test]
200 fn test_markup_type() {
201 assert!(matches!(
202 MarkupType::Highlight.annotation_type(),
203 crate::annotations::AnnotationType::Highlight
204 ));
205 assert!(matches!(
206 MarkupType::Underline.annotation_type(),
207 crate::annotations::AnnotationType::Underline
208 ));
209 }
210
211 #[test]
212 fn test_quad_points_from_rect() {
213 let rect = Rectangle::new(Point::new(100.0, 100.0), Point::new(200.0, 120.0));
214
215 let quad = QuadPoints::from_rect(&rect);
216 assert_eq!(quad.points.len(), 8);
217 assert_eq!(quad.points[0], 100.0); assert_eq!(quad.points[1], 100.0); assert_eq!(quad.points[2], 200.0); assert_eq!(quad.points[3], 100.0); }
222
223 #[test]
224 fn test_highlight_annotation() {
225 let rect = Rectangle::new(Point::new(50.0, 500.0), Point::new(250.0, 515.0));
226
227 let highlight = MarkupAnnotation::highlight(rect)
228 .with_author("John Doe")
229 .with_contents("Important text");
230
231 assert!(matches!(highlight.markup_type, MarkupType::Highlight));
232 assert_eq!(highlight.author, Some("John Doe".to_string()));
233 assert_eq!(
234 highlight.annotation.contents,
235 Some("Important text".to_string())
236 );
237 }
238
239 #[test]
240 fn test_markup_default_colors() {
241 let rect = Rectangle::new(Point::new(0.0, 0.0), Point::new(100.0, 20.0));
242
243 let highlight = MarkupAnnotation::highlight(rect);
244 assert!(matches!(
245 highlight.annotation.color,
246 Some(Color::Rgb(1.0, 1.0, 0.0))
247 ));
248
249 let underline = MarkupAnnotation::underline(rect);
250 assert!(matches!(
251 underline.annotation.color,
252 Some(Color::Rgb(0.0, 0.0, 1.0))
253 ));
254
255 let strikeout = MarkupAnnotation::strikeout(rect);
256 assert!(matches!(
257 strikeout.annotation.color,
258 Some(Color::Rgb(1.0, 0.0, 0.0))
259 ));
260
261 let squiggly = MarkupAnnotation::squiggly(rect);
262 assert!(matches!(
263 squiggly.annotation.color,
264 Some(Color::Rgb(0.0, 1.0, 0.0))
265 ));
266 }
267
268 #[test]
269 fn test_quad_points_from_multiple_rects() {
270 let rects = vec![
271 Rectangle::new(Point::new(100.0, 100.0), Point::new(200.0, 120.0)),
272 Rectangle::new(Point::new(100.0, 130.0), Point::new(180.0, 150.0)),
273 Rectangle::new(Point::new(100.0, 160.0), Point::new(220.0, 180.0)),
274 ];
275
276 let quad_points = QuadPoints::from_rects(&rects);
277
278 assert_eq!(quad_points.points.len(), 24);
280
281 assert_eq!(quad_points.points[0], 100.0); assert_eq!(quad_points.points[1], 100.0); assert_eq!(quad_points.points[2], 200.0); assert_eq!(quad_points.points[3], 100.0); assert_eq!(quad_points.points[4], 200.0); assert_eq!(quad_points.points[5], 120.0); assert_eq!(quad_points.points[6], 100.0); assert_eq!(quad_points.points[7], 120.0); assert_eq!(quad_points.points[8], 100.0);
293 assert_eq!(quad_points.points[9], 130.0);
294 }
295
296 #[test]
297 fn test_quad_points_to_array() {
298 let points = vec![10.0, 20.0, 30.0, 40.0, 50.0, 60.0, 70.0, 80.0];
299 let quad_points = QuadPoints {
300 points: points.clone(),
301 };
302
303 if let Object::Array(array) = quad_points.to_array() {
304 assert_eq!(array.len(), 8);
305 for (i, point) in points.iter().enumerate() {
306 assert_eq!(array[i], Object::Real(*point));
307 }
308 } else {
309 panic!("Expected array");
310 }
311 }
312
313 #[test]
314 fn test_markup_type_annotation_types() {
315 assert!(matches!(
316 MarkupType::Highlight.annotation_type(),
317 crate::annotations::AnnotationType::Highlight
318 ));
319 assert!(matches!(
320 MarkupType::Underline.annotation_type(),
321 crate::annotations::AnnotationType::Underline
322 ));
323 assert!(matches!(
324 MarkupType::StrikeOut.annotation_type(),
325 crate::annotations::AnnotationType::StrikeOut
326 ));
327 assert!(matches!(
328 MarkupType::Squiggly.annotation_type(),
329 crate::annotations::AnnotationType::Squiggly
330 ));
331 }
332
333 #[test]
334 fn test_markup_annotation_complete_workflow() {
335 let rect = Rectangle::new(Point::new(100.0, 400.0), Point::new(500.0, 420.0));
336 let quad_points = QuadPoints::from_rect(&rect);
337
338 let markup = MarkupAnnotation::new(MarkupType::Highlight, rect, quad_points)
339 .with_author("Jane Smith")
340 .with_subject("Important passage")
341 .with_contents("This section explains the key concept")
342 .with_color(Color::Rgb(1.0, 0.8, 0.0));
343
344 assert_eq!(markup.author, Some("Jane Smith".to_string()));
346 assert_eq!(markup.subject, Some("Important passage".to_string()));
347 assert_eq!(
348 markup.annotation.contents,
349 Some("This section explains the key concept".to_string())
350 );
351 assert!(matches!(
352 markup.annotation.color,
353 Some(Color::Rgb(1.0, 0.8, 0.0))
354 ));
355
356 let annotation = markup.to_annotation();
358 let dict = annotation.to_dict();
359
360 assert_eq!(dict.get("Type"), Some(&Object::Name("Annot".to_string())));
361 assert_eq!(
362 dict.get("Subtype"),
363 Some(&Object::Name("Highlight".to_string()))
364 );
365 assert!(dict.get("QuadPoints").is_some());
366 assert_eq!(
367 dict.get("T"),
368 Some(&Object::String("Jane Smith".to_string()))
369 );
370 assert_eq!(
371 dict.get("Subj"),
372 Some(&Object::String("Important passage".to_string()))
373 );
374 }
375
376 #[test]
377 fn test_markup_with_empty_metadata() {
378 let rect = Rectangle::new(Point::new(0.0, 0.0), Point::new(100.0, 20.0));
379
380 let markup = MarkupAnnotation::underline(rect)
381 .with_author("")
382 .with_subject("")
383 .with_contents("");
384
385 assert_eq!(markup.author, Some("".to_string()));
386 assert_eq!(markup.subject, Some("".to_string()));
387 assert_eq!(markup.annotation.contents, Some("".to_string()));
388
389 let annotation = markup.to_annotation();
390 let dict = annotation.to_dict();
391
392 assert_eq!(dict.get("T"), Some(&Object::String("".to_string())));
394 assert_eq!(dict.get("Subj"), Some(&Object::String("".to_string())));
395 assert_eq!(dict.get("Contents"), Some(&Object::String("".to_string())));
396 }
397
398 #[test]
399 fn test_markup_with_unicode_metadata() {
400 let rect = Rectangle::new(Point::new(50.0, 50.0), Point::new(150.0, 70.0));
401
402 let markup = MarkupAnnotation::strikeout(rect)
403 .with_author("作者名")
404 .with_subject("Тема аннотации")
405 .with_contents("محتوى التعليق التوضيحي");
406
407 assert_eq!(markup.author, Some("作者名".to_string()));
408 assert_eq!(markup.subject, Some("Тема аннотации".to_string()));
409 assert_eq!(
410 markup.annotation.contents,
411 Some("محتوى التعليق التوضيحي".to_string())
412 );
413 }
414
415 #[test]
416 fn test_markup_convenience_methods() {
417 let rect = Rectangle::new(Point::new(100.0, 100.0), Point::new(300.0, 120.0));
418
419 let highlight = MarkupAnnotation::highlight(rect);
421 assert!(matches!(highlight.markup_type, MarkupType::Highlight));
422 assert_eq!(
423 highlight.annotation.annotation_type,
424 crate::annotations::AnnotationType::Highlight
425 );
426
427 let underline = MarkupAnnotation::underline(rect);
429 assert!(matches!(underline.markup_type, MarkupType::Underline));
430 assert_eq!(
431 underline.annotation.annotation_type,
432 crate::annotations::AnnotationType::Underline
433 );
434
435 let strikeout = MarkupAnnotation::strikeout(rect);
437 assert!(matches!(strikeout.markup_type, MarkupType::StrikeOut));
438 assert_eq!(
439 strikeout.annotation.annotation_type,
440 crate::annotations::AnnotationType::StrikeOut
441 );
442
443 let squiggly = MarkupAnnotation::squiggly(rect);
445 assert!(matches!(squiggly.markup_type, MarkupType::Squiggly));
446 assert_eq!(
447 squiggly.annotation.annotation_type,
448 crate::annotations::AnnotationType::Squiggly
449 );
450 }
451
452 #[test]
453 fn test_quad_points_edge_cases() {
454 let empty_rects: Vec<Rectangle> = vec![];
456 let empty_quad = QuadPoints::from_rects(&empty_rects);
457 assert!(empty_quad.points.is_empty());
458
459 let single_rect = Rectangle::new(Point::new(0.0, 0.0), Point::new(10.0, 10.0));
461 let single_quad = QuadPoints::from_rect(&single_rect);
462 assert_eq!(single_quad.points.len(), 8);
463
464 let extreme_rect = Rectangle::new(
466 Point::new(f64::MIN, f64::MIN),
467 Point::new(f64::MAX, f64::MAX),
468 );
469 let extreme_quad = QuadPoints::from_rect(&extreme_rect);
470 assert_eq!(extreme_quad.points.len(), 8);
471 assert_eq!(extreme_quad.points[0], f64::MIN);
472 assert_eq!(extreme_quad.points[4], f64::MAX);
473 }
474
475 #[test]
476 fn test_markup_type_debug_clone_copy() {
477 let markup_type = MarkupType::Highlight;
478
479 let debug_str = format!("{markup_type:?}");
481 assert!(debug_str.contains("Highlight"));
482
483 let cloned = markup_type;
485 assert!(matches!(cloned, MarkupType::Highlight));
486
487 let copied: MarkupType = markup_type;
489 assert!(matches!(copied, MarkupType::Highlight));
490 }
491
492 #[test]
493 fn test_quad_points_debug_clone() {
494 let quad_points = QuadPoints {
495 points: vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0],
496 };
497
498 let debug_str = format!("{quad_points:?}");
500 assert!(debug_str.contains("QuadPoints"));
501 assert!(debug_str.contains("1.0"));
502
503 let cloned = quad_points.clone();
505 assert_eq!(cloned.points, quad_points.points);
506 }
507
508 #[test]
509 fn test_markup_annotation_debug_clone() {
510 let rect = Rectangle::new(Point::new(100.0, 100.0), Point::new(200.0, 120.0));
511 let markup = MarkupAnnotation::highlight(rect).with_author("Test Author");
512
513 let debug_str = format!("{markup:?}");
515 assert!(debug_str.contains("MarkupAnnotation"));
516 assert!(debug_str.contains("Highlight"));
517
518 let cloned = markup;
520 assert_eq!(cloned.author, Some("Test Author".to_string()));
521 assert!(matches!(cloned.markup_type, MarkupType::Highlight));
522 }
523
524 #[test]
525 fn test_markup_color_customization() {
526 let rect = Rectangle::new(Point::new(0.0, 0.0), Point::new(100.0, 20.0));
527
528 let colors = vec![
530 Color::Gray(0.5),
531 Color::Rgb(0.1, 0.2, 0.3),
532 Color::Cmyk(0.1, 0.2, 0.3, 0.4),
533 ];
534
535 for color in colors {
536 let markup = MarkupAnnotation::highlight(rect).with_color(color);
537
538 let annotation = markup.to_annotation();
539 let dict = annotation.to_dict();
540
541 assert!(dict.get("C").is_some());
543
544 if let Some(Object::Array(color_array)) = dict.get("C") {
545 match color {
546 Color::Gray(_) => assert_eq!(color_array.len(), 1),
547 Color::Rgb(_, _, _) => assert_eq!(color_array.len(), 3),
548 Color::Cmyk(_, _, _, _) => assert_eq!(color_array.len(), 4),
549 }
550 }
551 }
552 }
553
554 #[test]
555 fn test_markup_without_optional_fields() {
556 let rect = Rectangle::new(Point::new(100.0, 100.0), Point::new(200.0, 120.0));
557 let quad_points = QuadPoints::from_rect(&rect);
558
559 let markup = MarkupAnnotation::new(MarkupType::Underline, rect, quad_points);
560
561 assert!(markup.author.is_none());
563 assert!(markup.subject.is_none());
564
565 let annotation = markup.to_annotation();
566 let dict = annotation.to_dict();
567
568 assert!(!dict.contains_key("T"));
570 assert!(!dict.contains_key("Subj"));
571
572 assert!(dict.contains_key("QuadPoints"));
574 }
575
576 #[test]
577 fn test_multiple_line_highlight() {
578 let line_height = 15.0;
580 let lines = 5;
581 let mut rects = Vec::new();
582
583 for i in 0..lines {
584 let y_base = 700.0 - (i as f64 * line_height);
585 let rect = Rectangle::new(
586 Point::new(100.0, y_base),
587 Point::new(500.0 - (i as f64 * 20.0), y_base + 12.0),
588 );
589 rects.push(rect);
590 }
591
592 let bounding_rect = Rectangle::new(
594 Point::new(100.0, 700.0 - ((lines - 1) as f64 * line_height)),
595 Point::new(500.0, 700.0 + 12.0),
596 );
597
598 let quad_points = QuadPoints::from_rects(&rects);
599 let expected_points_len = quad_points.points.len();
600 let markup = MarkupAnnotation::new(MarkupType::Highlight, bounding_rect, quad_points)
601 .with_contents("Multi-line highlight example")
602 .with_subject("Code section");
603
604 assert_eq!(expected_points_len, lines * 8);
605
606 let annotation = markup.to_annotation();
607 let dict = annotation.to_dict();
608
609 if let Some(Object::Array(points_array)) = dict.get("QuadPoints") {
610 assert_eq!(points_array.len(), lines * 8);
611 }
612 }
613
614 #[test]
615 fn test_markup_builder_pattern() {
616 let rect = Rectangle::new(Point::new(50.0, 50.0), Point::new(250.0, 70.0));
617
618 let markup = MarkupAnnotation::squiggly(rect)
620 .with_author("Reviewer")
621 .with_subject("Grammar")
622 .with_contents("Incorrect grammar in this sentence")
623 .with_color(Color::Rgb(1.0, 0.0, 0.5));
624
625 assert_eq!(markup.author, Some("Reviewer".to_string()));
627 assert_eq!(markup.subject, Some("Grammar".to_string()));
628 assert_eq!(
629 markup.annotation.contents,
630 Some("Incorrect grammar in this sentence".to_string())
631 );
632 assert!(matches!(
633 markup.annotation.color,
634 Some(Color::Rgb(1.0, 0.0, 0.5))
635 ));
636 assert!(matches!(markup.markup_type, MarkupType::Squiggly));
637 }
638}