use crate::annotation_types::{AnnotationColor, AnnotationFlags};
use crate::geometry::Rect;
use crate::object::{Object, ObjectRef};
use std::collections::HashMap;
#[derive(Debug, Clone)]
pub struct PopupAnnotation {
pub rect: Rect,
pub open: bool,
pub flags: AnnotationFlags,
}
impl PopupAnnotation {
pub fn new(rect: Rect) -> Self {
Self {
rect,
open: false,
flags: AnnotationFlags::empty(),
}
}
pub fn open(rect: Rect) -> Self {
Self {
rect,
open: true,
flags: AnnotationFlags::empty(),
}
}
pub fn with_open(mut self, open: bool) -> Self {
self.open = open;
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("Popup".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),
]),
);
dict.insert("Open".to_string(), Object::Boolean(self.open));
if self.flags.bits() != 0 {
dict.insert("F".to_string(), Object::Integer(self.flags.bits() as i64));
}
dict
}
pub fn rect(&self) -> Rect {
self.rect
}
}
impl Default for PopupAnnotation {
fn default() -> Self {
Self::new(Rect::new(0.0, 0.0, 200.0, 150.0))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub enum CaretSymbol {
#[default]
None,
Paragraph,
}
impl CaretSymbol {
pub fn pdf_name(&self) -> &'static str {
match self {
CaretSymbol::None => "None",
CaretSymbol::Paragraph => "P",
}
}
}
#[derive(Debug, Clone)]
pub struct CaretAnnotation {
pub rect: Rect,
pub symbol: CaretSymbol,
pub rd: Option<(f64, f64, f64, f64)>,
pub contents: Option<String>,
pub author: Option<String>,
pub subject: Option<String>,
pub color: Option<AnnotationColor>,
pub flags: AnnotationFlags,
}
impl CaretAnnotation {
pub fn new(rect: Rect) -> Self {
Self {
rect,
symbol: CaretSymbol::None,
rd: None,
contents: None,
author: None,
subject: None,
color: Some(AnnotationColor::Rgb(0.0, 0.0, 1.0)), flags: AnnotationFlags::printable(),
}
}
pub fn paragraph(rect: Rect) -> Self {
Self {
symbol: CaretSymbol::Paragraph,
..Self::new(rect)
}
}
pub fn with_symbol(mut self, symbol: CaretSymbol) -> Self {
self.symbol = symbol;
self
}
pub fn with_rd(mut self, left: f64, bottom: f64, right: f64, top: f64) -> Self {
self.rd = Some((left, bottom, right, top));
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_color(mut self, color: AnnotationColor) -> Self {
self.color = Some(color);
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("Caret".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),
]),
);
dict.insert("Sy".to_string(), Object::Name(self.symbol.pdf_name().to_string()));
if let Some((l, b, r, t)) = self.rd {
dict.insert(
"RD".to_string(),
Object::Array(vec![
Object::Real(l),
Object::Real(b),
Object::Real(r),
Object::Real(t),
]),
);
}
if let Some(ref contents) = self.contents {
dict.insert("Contents".to_string(), Object::String(contents.as_bytes().to_vec()));
}
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 self.flags.bits() != 0 {
dict.insert("F".to_string(), Object::Integer(self.flags.bits() as i64));
}
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()));
}
dict
}
pub fn rect(&self) -> Rect {
self.rect
}
}
impl Default for CaretAnnotation {
fn default() -> Self {
Self::new(Rect::new(0.0, 0.0, 10.0, 10.0))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub enum FileAttachmentIcon {
#[default]
PushPin,
Graph,
Paperclip,
Tag,
}
impl FileAttachmentIcon {
pub fn pdf_name(&self) -> &'static str {
match self {
FileAttachmentIcon::PushPin => "PushPin",
FileAttachmentIcon::Graph => "Graph",
FileAttachmentIcon::Paperclip => "Paperclip",
FileAttachmentIcon::Tag => "Tag",
}
}
}
#[derive(Debug, Clone)]
pub struct FileAttachmentAnnotation {
pub rect: Rect,
pub icon: FileAttachmentIcon,
pub file_name: String,
pub description: Option<String>,
pub contents: Option<String>,
pub author: Option<String>,
pub color: Option<AnnotationColor>,
pub flags: AnnotationFlags,
}
impl FileAttachmentAnnotation {
pub fn new(rect: Rect, file_name: impl Into<String>) -> Self {
Self {
rect,
icon: FileAttachmentIcon::PushPin,
file_name: file_name.into(),
description: None,
contents: None,
author: None,
color: None,
flags: AnnotationFlags::printable(),
}
}
pub fn with_icon(mut self, icon: FileAttachmentIcon) -> Self {
self.icon = icon;
self
}
pub fn with_description(mut self, description: impl Into<String>) -> Self {
self.description = Some(description.into());
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_color(mut self, color: AnnotationColor) -> Self {
self.color = Some(color);
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("FileAttachment".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),
]),
);
dict.insert("Name".to_string(), Object::Name(self.icon.pdf_name().to_string()));
let mut fs_dict = HashMap::new();
fs_dict.insert("Type".to_string(), Object::Name("Filespec".to_string()));
fs_dict.insert("F".to_string(), Object::String(self.file_name.as_bytes().to_vec()));
if let Some(ref desc) = self.description {
fs_dict.insert("Desc".to_string(), Object::String(desc.as_bytes().to_vec()));
}
dict.insert("FS".to_string(), Object::Dictionary(fs_dict));
if let Some(ref contents) = self.contents {
dict.insert("Contents".to_string(), Object::String(contents.as_bytes().to_vec()));
}
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 self.flags.bits() != 0 {
dict.insert("F".to_string(), Object::Integer(self.flags.bits() as i64));
}
if let Some(ref author) = self.author {
dict.insert("T".to_string(), Object::String(author.as_bytes().to_vec()));
}
dict
}
pub fn rect(&self) -> Rect {
self.rect
}
}
#[derive(Debug, Clone)]
pub struct RedactAnnotation {
pub rect: Rect,
pub quad_points: Option<Vec<[f64; 8]>>,
pub interior_color: Option<AnnotationColor>,
pub overlay_text: Option<String>,
pub overlay_text_alignment: i32,
pub default_appearance: Option<String>,
pub contents: Option<String>,
pub author: Option<String>,
pub subject: Option<String>,
pub flags: AnnotationFlags,
}
impl RedactAnnotation {
pub fn new(rect: Rect) -> Self {
Self {
rect,
quad_points: None,
interior_color: Some(AnnotationColor::black()),
overlay_text: None,
overlay_text_alignment: 0,
default_appearance: None,
contents: None,
author: None,
subject: None,
flags: AnnotationFlags::new(AnnotationFlags::PRINT | AnnotationFlags::LOCKED),
}
}
pub fn with_quads(rect: Rect, quad_points: Vec<[f64; 8]>) -> Self {
Self {
quad_points: Some(quad_points),
..Self::new(rect)
}
}
pub fn with_interior_color(mut self, color: AnnotationColor) -> Self {
self.interior_color = Some(color);
self
}
pub fn with_overlay_text(mut self, text: impl Into<String>) -> Self {
self.overlay_text = Some(text.into());
self
}
pub fn with_overlay_text_alignment(mut self, alignment: i32) -> Self {
self.overlay_text_alignment = alignment.clamp(0, 2);
self
}
pub fn with_default_appearance(mut self, da: impl Into<String>) -> Self {
self.default_appearance = Some(da.into());
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("Redact".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),
]),
);
if let Some(ref quads) = self.quad_points {
let mut all_points = Vec::new();
for quad in quads {
for point in quad {
all_points.push(Object::Real(*point));
}
}
dict.insert("QuadPoints".to_string(), Object::Array(all_points));
}
if let Some(ref color) = self.interior_color {
if let Some(color_array) = color.to_array() {
dict.insert(
"IC".to_string(),
Object::Array(
color_array
.into_iter()
.map(|v| Object::Real(v as f64))
.collect(),
),
);
}
}
if let Some(ref text) = self.overlay_text {
dict.insert("OverlayText".to_string(), Object::String(text.as_bytes().to_vec()));
}
if self.overlay_text_alignment != 0 {
dict.insert("Q".to_string(), Object::Integer(self.overlay_text_alignment as i64));
}
if let Some(ref da) = self.default_appearance {
dict.insert("DA".to_string(), Object::String(da.as_bytes().to_vec()));
}
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 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()));
}
dict
}
pub fn rect(&self) -> Rect {
self.rect
}
}
impl Default for RedactAnnotation {
fn default() -> Self {
Self::new(Rect::new(0.0, 0.0, 100.0, 20.0))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_popup_new() {
let rect = Rect::new(300.0, 500.0, 200.0, 150.0);
let popup = PopupAnnotation::new(rect);
assert_eq!(popup.rect, rect);
assert!(!popup.open);
}
#[test]
fn test_popup_open() {
let rect = Rect::new(300.0, 500.0, 200.0, 150.0);
let popup = PopupAnnotation::open(rect);
assert!(popup.open);
}
#[test]
fn test_popup_build() {
let popup = PopupAnnotation::new(Rect::new(100.0, 100.0, 200.0, 150.0));
let dict = popup.build(&[]);
assert_eq!(dict.get("Subtype"), Some(&Object::Name("Popup".to_string())));
assert_eq!(dict.get("Open"), Some(&Object::Boolean(false)));
}
#[test]
fn test_caret_new() {
let rect = Rect::new(100.0, 700.0, 10.0, 10.0);
let caret = CaretAnnotation::new(rect);
assert_eq!(caret.rect, rect);
assert_eq!(caret.symbol, CaretSymbol::None);
}
#[test]
fn test_caret_paragraph() {
let rect = Rect::new(100.0, 700.0, 10.0, 10.0);
let caret = CaretAnnotation::paragraph(rect);
assert_eq!(caret.symbol, CaretSymbol::Paragraph);
}
#[test]
fn test_caret_symbol_names() {
assert_eq!(CaretSymbol::None.pdf_name(), "None");
assert_eq!(CaretSymbol::Paragraph.pdf_name(), "P");
}
#[test]
fn test_caret_build() {
let caret =
CaretAnnotation::new(Rect::new(100.0, 700.0, 10.0, 10.0)).with_contents("Insert here");
let dict = caret.build(&[]);
assert_eq!(dict.get("Subtype"), Some(&Object::Name("Caret".to_string())));
assert_eq!(dict.get("Sy"), Some(&Object::Name("None".to_string())));
assert!(dict.contains_key("Contents"));
}
#[test]
fn test_caret_fluent_builder() {
let caret = CaretAnnotation::new(Rect::new(100.0, 700.0, 10.0, 10.0))
.with_symbol(CaretSymbol::Paragraph)
.with_contents("Insert paragraph")
.with_author("Editor");
assert_eq!(caret.symbol, CaretSymbol::Paragraph);
assert_eq!(caret.contents, Some("Insert paragraph".to_string()));
assert_eq!(caret.author, Some("Editor".to_string()));
}
#[test]
fn test_file_attachment_new() {
let rect = Rect::new(72.0, 600.0, 24.0, 24.0);
let file = FileAttachmentAnnotation::new(rect, "document.pdf");
assert_eq!(file.rect, rect);
assert_eq!(file.file_name, "document.pdf");
assert_eq!(file.icon, FileAttachmentIcon::PushPin);
}
#[test]
fn test_file_attachment_icons() {
assert_eq!(FileAttachmentIcon::PushPin.pdf_name(), "PushPin");
assert_eq!(FileAttachmentIcon::Graph.pdf_name(), "Graph");
assert_eq!(FileAttachmentIcon::Paperclip.pdf_name(), "Paperclip");
assert_eq!(FileAttachmentIcon::Tag.pdf_name(), "Tag");
}
#[test]
fn test_file_attachment_build() {
let file = FileAttachmentAnnotation::new(Rect::new(72.0, 600.0, 24.0, 24.0), "report.xlsx")
.with_icon(FileAttachmentIcon::Paperclip)
.with_description("Monthly report");
let dict = file.build(&[]);
assert_eq!(dict.get("Subtype"), Some(&Object::Name("FileAttachment".to_string())));
assert_eq!(dict.get("Name"), Some(&Object::Name("Paperclip".to_string())));
assert!(dict.contains_key("FS"));
}
#[test]
fn test_redact_new() {
let rect = Rect::new(72.0, 500.0, 200.0, 20.0);
let redact = RedactAnnotation::new(rect);
assert_eq!(redact.rect, rect);
assert!(redact.interior_color.is_some());
}
#[test]
fn test_redact_with_overlay() {
let redact = RedactAnnotation::new(Rect::new(72.0, 500.0, 200.0, 20.0))
.with_overlay_text("REDACTED")
.with_overlay_text_alignment(1);
assert_eq!(redact.overlay_text, Some("REDACTED".to_string()));
assert_eq!(redact.overlay_text_alignment, 1);
}
#[test]
fn test_redact_build() {
let redact = RedactAnnotation::new(Rect::new(72.0, 500.0, 200.0, 20.0))
.with_overlay_text("CONFIDENTIAL")
.with_interior_color(AnnotationColor::black());
let dict = redact.build(&[]);
assert_eq!(dict.get("Subtype"), Some(&Object::Name("Redact".to_string())));
assert!(dict.contains_key("IC"));
assert!(dict.contains_key("OverlayText"));
}
#[test]
fn test_redact_with_quads() {
let rect = Rect::new(72.0, 500.0, 200.0, 20.0);
let quads = vec![[72.0, 500.0, 272.0, 500.0, 272.0, 520.0, 72.0, 520.0]];
let redact = RedactAnnotation::with_quads(rect, quads);
assert!(redact.quad_points.is_some());
let dict = redact.build(&[]);
assert!(dict.contains_key("QuadPoints"));
}
}