use crate::annotation_types::{AnnotationColor, AnnotationFlags, BorderStyleType};
use crate::geometry::Rect;
use crate::object::{Object, ObjectRef};
use std::collections::HashMap;
#[derive(Debug, Clone)]
pub struct InkAnnotation {
pub strokes: Vec<Vec<(f64, f64)>>,
pub color: Option<AnnotationColor>,
pub opacity: Option<f32>,
pub border_style: Option<BorderStyleType>,
pub line_width: Option<f32>,
pub dash_pattern: Option<Vec<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 InkAnnotation {
pub fn new() -> Self {
Self {
strokes: Vec::new(),
color: Some(AnnotationColor::black()),
opacity: None,
border_style: None,
line_width: Some(1.0),
dash_pattern: None,
contents: None,
author: None,
subject: None,
flags: AnnotationFlags::printable(),
creation_date: None,
modification_date: None,
}
}
pub fn with_stroke(stroke: Vec<(f64, f64)>) -> Self {
let mut ink = Self::new();
ink.strokes.push(stroke);
ink
}
pub fn with_strokes(strokes: Vec<Vec<(f64, f64)>>) -> Self {
Self {
strokes,
..Self::new()
}
}
pub fn add_stroke(mut self, stroke: Vec<(f64, f64)>) -> Self {
self.strokes.push(stroke);
self
}
pub fn with_stroke_color(mut self, r: f32, g: f32, b: f32) -> Self {
self.color = Some(AnnotationColor::Rgb(r, g, b));
self
}
pub fn with_color(mut self, color: AnnotationColor) -> Self {
self.color = Some(color);
self
}
pub fn with_line_width(mut self, width: f32) -> Self {
self.line_width = Some(width);
self
}
pub fn with_border_style(mut self, style: BorderStyleType) -> Self {
self.border_style = Some(style);
self
}
pub fn with_dash_pattern(mut self, pattern: Vec<f32>) -> Self {
self.dash_pattern = Some(pattern);
self.border_style = Some(BorderStyleType::Dashed);
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 calculate_rect(&self) -> Rect {
if self.strokes.is_empty() {
return Rect::new(0.0, 0.0, 0.0, 0.0);
}
let mut min_x = f64::MAX;
let mut max_x = f64::MIN;
let mut min_y = f64::MAX;
let mut max_y = f64::MIN;
for stroke in &self.strokes {
for (x, y) in stroke {
min_x = min_x.min(*x);
max_x = max_x.max(*x);
min_y = min_y.min(*y);
max_y = max_y.max(*y);
}
}
let margin = self.line_width.unwrap_or(1.0) as f64 + 2.0;
Rect::new(
(min_x - margin) as f32,
(min_y - margin) as f32,
(max_x - min_x + 2.0 * margin) as f32,
(max_y - min_y + 2.0 * margin) as f32,
)
}
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("Ink".to_string()));
let rect = self.calculate_rect();
dict.insert(
"Rect".to_string(),
Object::Array(vec![
Object::Real(rect.x as f64),
Object::Real(rect.y as f64),
Object::Real((rect.x + rect.width) as f64),
Object::Real((rect.y + rect.height) as f64),
]),
);
let ink_list: Vec<Object> = self
.strokes
.iter()
.map(|stroke| {
let points: Vec<Object> = stroke
.iter()
.flat_map(|(x, y)| vec![Object::Real(*x), Object::Real(*y)])
.collect();
Object::Array(points)
})
.collect();
dict.insert("InkList".to_string(), Object::Array(ink_list));
if let Some(ref contents) = self.contents {
dict.insert("Contents".to_string(), Object::String(contents.as_bytes().to_vec()));
}
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 self.border_style.is_some() || self.line_width.is_some() || self.dash_pattern.is_some() {
let mut bs = HashMap::new();
bs.insert("Type".to_string(), Object::Name("Border".to_string()));
if let Some(width) = self.line_width {
bs.insert("W".to_string(), Object::Real(width as f64));
}
if let Some(ref style) = self.border_style {
let style_char = match style {
BorderStyleType::Solid => "S",
BorderStyleType::Dashed => "D",
BorderStyleType::Beveled => "B",
BorderStyleType::Inset => "I",
BorderStyleType::Underline => "U",
};
bs.insert("S".to_string(), Object::Name(style_char.to_string()));
}
if let Some(ref pattern) = self.dash_pattern {
bs.insert(
"D".to_string(),
Object::Array(pattern.iter().map(|&v| Object::Real(v as f64)).collect()),
);
}
dict.insert("BS".to_string(), Object::Dictionary(bs));
}
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
}
}
impl Default for InkAnnotation {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_ink_annotation_new() {
let ink = InkAnnotation::new();
assert!(ink.strokes.is_empty());
assert!(ink.color.is_some());
assert_eq!(ink.line_width, Some(1.0));
}
#[test]
fn test_ink_with_single_stroke() {
let stroke = vec![(100.0, 100.0), (150.0, 120.0), (200.0, 100.0)];
let ink = InkAnnotation::with_stroke(stroke.clone());
assert_eq!(ink.strokes.len(), 1);
assert_eq!(ink.strokes[0], stroke);
}
#[test]
fn test_ink_with_multiple_strokes() {
let strokes = vec![
vec![(100.0, 100.0), (150.0, 120.0)],
vec![(200.0, 200.0), (250.0, 220.0)],
];
let ink = InkAnnotation::with_strokes(strokes.clone());
assert_eq!(ink.strokes.len(), 2);
}
#[test]
fn test_ink_add_stroke() {
let ink = InkAnnotation::new()
.add_stroke(vec![(100.0, 100.0), (150.0, 120.0)])
.add_stroke(vec![(200.0, 200.0), (250.0, 220.0)]);
assert_eq!(ink.strokes.len(), 2);
}
#[test]
fn test_ink_calculate_rect() {
let ink = InkAnnotation::new()
.add_stroke(vec![(100.0, 100.0), (200.0, 200.0)])
.with_line_width(2.0);
let rect = ink.calculate_rect();
assert!(rect.x < 100.0);
assert!(rect.y < 100.0);
assert!(rect.x + rect.width > 200.0);
assert!(rect.y + rect.height > 200.0);
}
#[test]
fn test_ink_build() {
let ink = InkAnnotation::new()
.add_stroke(vec![(100.0, 100.0), (150.0, 120.0), (200.0, 100.0)])
.with_stroke_color(1.0, 0.0, 0.0)
.with_line_width(2.0);
let dict = ink.build(&[]);
assert_eq!(dict.get("Type"), Some(&Object::Name("Annot".to_string())));
assert_eq!(dict.get("Subtype"), Some(&Object::Name("Ink".to_string())));
assert!(dict.contains_key("InkList"));
assert!(dict.contains_key("Rect"));
assert!(dict.contains_key("C")); assert!(dict.contains_key("BS")); }
#[test]
fn test_ink_with_dashed_stroke() {
let ink = InkAnnotation::new()
.add_stroke(vec![(100.0, 100.0), (200.0, 100.0)])
.with_dash_pattern(vec![3.0, 2.0]);
assert_eq!(ink.dash_pattern, Some(vec![3.0, 2.0]));
assert!(matches!(ink.border_style, Some(BorderStyleType::Dashed)));
let dict = ink.build(&[]);
assert!(dict.contains_key("BS"));
}
#[test]
fn test_ink_fluent_builder() {
let ink = InkAnnotation::new()
.add_stroke(vec![(100.0, 100.0), (200.0, 200.0)])
.with_stroke_color(0.0, 0.0, 1.0)
.with_line_width(3.0)
.with_opacity(0.8)
.with_author("Artist")
.with_subject("Drawing")
.with_contents("A simple line");
assert_eq!(ink.line_width, Some(3.0));
assert_eq!(ink.opacity, Some(0.8));
assert_eq!(ink.author, Some("Artist".to_string()));
assert_eq!(ink.subject, Some("Drawing".to_string()));
assert_eq!(ink.contents, Some("A simple line".to_string()));
}
#[test]
fn test_ink_empty_strokes() {
let ink = InkAnnotation::new();
let rect = ink.calculate_rect();
assert_eq!(rect.width, 0.0);
assert_eq!(rect.height, 0.0);
}
}