use crate::annotation_types::{AnnotationColor, AnnotationFlags, TextMarkupType};
use crate::geometry::Rect;
use crate::object::{Object, ObjectRef};
use std::collections::HashMap;
#[derive(Debug, Clone)]
pub struct TextMarkupAnnotation {
pub rect: Rect,
pub markup_type: TextMarkupType,
pub quad_points: Vec<[f64; 8]>,
pub color: Option<AnnotationColor>,
pub opacity: Option<f32>,
pub contents: Option<String>,
pub author: Option<String>,
pub subject: Option<String>,
pub flags: AnnotationFlags,
pub creation_date: Option<String>,
pub modification_date: Option<String>,
}
impl TextMarkupAnnotation {
pub fn new(markup_type: TextMarkupType, rect: Rect, quad_points: Vec<[f64; 8]>) -> Self {
Self {
rect,
markup_type,
quad_points,
color: None,
opacity: None,
contents: None,
author: None,
subject: None,
flags: AnnotationFlags::printable(),
creation_date: None,
modification_date: None,
}
}
pub fn highlight(rect: Rect, quad_points: Vec<[f64; 8]>) -> Self {
let mut annot = Self::new(TextMarkupType::Highlight, rect, quad_points);
annot.color = Some(AnnotationColor::Rgb(1.0, 1.0, 0.0));
annot
}
pub fn underline(rect: Rect, quad_points: Vec<[f64; 8]>) -> Self {
let mut annot = Self::new(TextMarkupType::Underline, rect, quad_points);
annot.color = Some(AnnotationColor::Rgb(1.0, 0.0, 0.0));
annot
}
pub fn strikeout(rect: Rect, quad_points: Vec<[f64; 8]>) -> Self {
let mut annot = Self::new(TextMarkupType::StrikeOut, rect, quad_points);
annot.color = Some(AnnotationColor::Rgb(1.0, 0.0, 0.0));
annot
}
pub fn squiggly(rect: Rect, quad_points: Vec<[f64; 8]>) -> Self {
let mut annot = Self::new(TextMarkupType::Squiggly, rect, quad_points);
annot.color = Some(AnnotationColor::Rgb(1.0, 0.0, 0.0));
annot
}
pub fn from_rect(markup_type: TextMarkupType, rect: Rect) -> Self {
let quad = [
rect.x as f64,
rect.y as f64,
(rect.x + rect.width) as f64,
rect.y as f64,
(rect.x + rect.width) as f64,
(rect.y + rect.height) as f64,
rect.x as f64,
(rect.y + rect.height) as f64,
];
Self::new(markup_type, rect, vec![quad])
}
pub fn with_color(mut self, r: f32, g: f32, b: f32) -> Self {
self.color = Some(AnnotationColor::Rgb(r, g, b));
self
}
pub fn with_annotation_color(mut self, color: AnnotationColor) -> Self {
self.color = Some(color);
self
}
pub fn with_opacity(mut self, opacity: f32) -> Self {
self.opacity = Some(opacity.clamp(0.0, 1.0));
self
}
pub fn with_contents(mut self, contents: impl Into<String>) -> Self {
self.contents = Some(contents.into());
self
}
pub fn with_author(mut self, author: impl Into<String>) -> Self {
self.author = Some(author.into());
self
}
pub fn with_subject(mut self, subject: impl Into<String>) -> Self {
self.subject = Some(subject.into());
self
}
pub fn with_flags(mut self, flags: AnnotationFlags) -> Self {
self.flags = flags;
self
}
pub fn build(&self, _page_refs: &[ObjectRef]) -> HashMap<String, Object> {
let mut dict = HashMap::new();
dict.insert("Type".to_string(), Object::Name("Annot".to_string()));
dict.insert(
"Subtype".to_string(),
Object::Name(self.markup_type.subtype().pdf_name().to_string()),
);
dict.insert(
"Rect".to_string(),
Object::Array(vec![
Object::Real(self.rect.x as f64),
Object::Real(self.rect.y as f64),
Object::Real((self.rect.x + self.rect.width) as f64),
Object::Real((self.rect.y + self.rect.height) as f64),
]),
);
let quad_array: Vec<Object> = self
.quad_points
.iter()
.flat_map(|quad| quad.iter().map(|&v| Object::Real(v)))
.collect();
dict.insert("QuadPoints".to_string(), Object::Array(quad_array));
if self.flags.bits() != 0 {
dict.insert("F".to_string(), Object::Integer(self.flags.bits() as i64));
}
if let Some(ref color) = self.color {
if let Some(color_array) = color.to_array() {
if !color_array.is_empty() {
dict.insert(
"C".to_string(),
Object::Array(
color_array
.into_iter()
.map(|v| Object::Real(v as f64))
.collect(),
),
);
}
}
}
if let Some(opacity) = self.opacity {
dict.insert("CA".to_string(), Object::Real(opacity as f64));
}
if let Some(ref contents) = self.contents {
dict.insert("Contents".to_string(), Object::String(contents.as_bytes().to_vec()));
}
if let Some(ref author) = self.author {
dict.insert("T".to_string(), Object::String(author.as_bytes().to_vec()));
}
if let Some(ref subject) = self.subject {
dict.insert("Subj".to_string(), Object::String(subject.as_bytes().to_vec()));
}
if let Some(ref date) = self.creation_date {
dict.insert("CreationDate".to_string(), Object::String(date.as_bytes().to_vec()));
}
if let Some(ref date) = self.modification_date {
dict.insert("M".to_string(), Object::String(date.as_bytes().to_vec()));
}
dict
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_highlight_annotation() {
let rect = Rect::new(72.0, 720.0, 100.0, 12.0);
let quads = vec![[72.0, 720.0, 172.0, 720.0, 172.0, 732.0, 72.0, 732.0]];
let highlight = TextMarkupAnnotation::highlight(rect, quads);
assert!(matches!(highlight.markup_type, TextMarkupType::Highlight));
assert!(matches!(highlight.color, Some(AnnotationColor::Rgb(1.0, 1.0, 0.0))));
}
#[test]
fn test_underline_annotation() {
let rect = Rect::new(72.0, 720.0, 100.0, 12.0);
let quads = vec![[72.0, 720.0, 172.0, 720.0, 172.0, 732.0, 72.0, 732.0]];
let underline = TextMarkupAnnotation::underline(rect, quads);
assert!(matches!(underline.markup_type, TextMarkupType::Underline));
}
#[test]
fn test_strikeout_annotation() {
let rect = Rect::new(72.0, 720.0, 100.0, 12.0);
let quads = vec![[72.0, 720.0, 172.0, 720.0, 172.0, 732.0, 72.0, 732.0]];
let strikeout = TextMarkupAnnotation::strikeout(rect, quads);
assert!(matches!(strikeout.markup_type, TextMarkupType::StrikeOut));
}
#[test]
fn test_squiggly_annotation() {
let rect = Rect::new(72.0, 720.0, 100.0, 12.0);
let quads = vec![[72.0, 720.0, 172.0, 720.0, 172.0, 732.0, 72.0, 732.0]];
let squiggly = TextMarkupAnnotation::squiggly(rect, quads);
assert!(matches!(squiggly.markup_type, TextMarkupType::Squiggly));
}
#[test]
fn test_from_rect() {
let rect = Rect::new(100.0, 200.0, 50.0, 20.0);
let highlight = TextMarkupAnnotation::from_rect(TextMarkupType::Highlight, rect);
assert_eq!(highlight.quad_points.len(), 1);
let quad = &highlight.quad_points[0];
assert_eq!(quad[0], 100.0); assert_eq!(quad[1], 200.0); assert_eq!(quad[2], 150.0); }
#[test]
fn test_build_highlight() {
let rect = Rect::new(72.0, 720.0, 100.0, 12.0);
let quads = vec![[72.0, 720.0, 172.0, 720.0, 172.0, 732.0, 72.0, 732.0]];
let highlight = TextMarkupAnnotation::highlight(rect, quads)
.with_opacity(0.5)
.with_contents("Important text")
.with_author("Test User");
let dict = highlight.build(&[]);
assert_eq!(dict.get("Type"), Some(&Object::Name("Annot".to_string())));
assert_eq!(dict.get("Subtype"), Some(&Object::Name("Highlight".to_string())));
assert!(dict.contains_key("QuadPoints"));
assert!(dict.contains_key("C")); assert!(dict.contains_key("CA")); assert!(dict.contains_key("Contents"));
assert!(dict.contains_key("T")); }
#[test]
fn test_quad_points_serialization() {
let rect = Rect::new(72.0, 720.0, 100.0, 12.0);
let quads = vec![
[72.0, 720.0, 172.0, 720.0, 172.0, 732.0, 72.0, 732.0],
[72.0, 700.0, 172.0, 700.0, 172.0, 712.0, 72.0, 712.0],
];
let highlight = TextMarkupAnnotation::highlight(rect, quads);
let dict = highlight.build(&[]);
if let Some(Object::Array(quad_array)) = dict.get("QuadPoints") {
assert_eq!(quad_array.len(), 16); } else {
panic!("QuadPoints should be an array");
}
}
#[test]
fn test_fluent_builder() {
let rect = Rect::new(72.0, 720.0, 100.0, 12.0);
let quads = vec![[72.0, 720.0, 172.0, 720.0, 172.0, 732.0, 72.0, 732.0]];
let highlight = TextMarkupAnnotation::highlight(rect, quads)
.with_color(0.5, 0.8, 0.2)
.with_opacity(0.7)
.with_contents("Note")
.with_author("Reviewer")
.with_subject("Important");
assert!(matches!(highlight.color, Some(AnnotationColor::Rgb(0.5, 0.8, 0.2))));
assert_eq!(highlight.opacity, Some(0.7));
assert_eq!(highlight.contents, Some("Note".to_string()));
assert_eq!(highlight.author, Some("Reviewer".to_string()));
assert_eq!(highlight.subject, Some("Important".to_string()));
}
}