use super::freetext::FreeTextAnnotation;
use super::ink::InkAnnotation;
use super::movie::MovieAnnotation;
use super::richmedia::RichMediaAnnotation;
use super::screen::ScreenAnnotation;
use super::shape_annotations::{LineAnnotation, PolygonAnnotation, ShapeAnnotation};
use super::sound::SoundAnnotation;
use super::special_annotations::{
CaretAnnotation, FileAttachmentAnnotation, PopupAnnotation, RedactAnnotation,
};
use super::stamp::StampAnnotation;
use super::text_annotations::TextAnnotation;
use super::text_markup::TextMarkupAnnotation;
use super::threed::ThreeDAnnotation;
use super::watermark::WatermarkAnnotation;
use crate::geometry::Rect;
use crate::object::{Object, ObjectRef};
use std::collections::HashMap;
#[derive(Debug, Clone, Copy)]
pub struct BorderStyle {
pub horizontal_radius: f32,
pub vertical_radius: f32,
pub width: f32,
pub dash: Option<(f32, f32)>,
}
impl Default for BorderStyle {
fn default() -> Self {
Self {
horizontal_radius: 0.0,
vertical_radius: 0.0,
width: 0.0, dash: None,
}
}
}
impl BorderStyle {
pub fn none() -> Self {
Self::default()
}
pub fn solid(width: f32) -> Self {
Self {
width,
..Default::default()
}
}
pub fn dashed(width: f32, dash_length: f32, gap_length: f32) -> Self {
Self {
width,
dash: Some((dash_length, gap_length)),
..Default::default()
}
}
pub fn with_radius(mut self, radius: f32) -> Self {
self.horizontal_radius = radius;
self.vertical_radius = radius;
self
}
pub fn to_border_array(&self) -> Object {
let mut arr = vec![
Object::Real(self.horizontal_radius as f64),
Object::Real(self.vertical_radius as f64),
Object::Real(self.width as f64),
];
if let Some((dash, gap)) = self.dash {
arr.push(Object::Array(vec![Object::Real(dash as f64), Object::Real(gap as f64)]));
}
Object::Array(arr)
}
}
#[derive(Debug, Clone, Copy, Default)]
pub enum HighlightMode {
None,
#[default]
Invert,
Outline,
Push,
}
impl HighlightMode {
pub fn pdf_name(&self) -> &'static str {
match self {
HighlightMode::None => "N",
HighlightMode::Invert => "I",
HighlightMode::Outline => "O",
HighlightMode::Push => "P",
}
}
}
#[derive(Debug, Clone)]
pub enum LinkAction {
Uri(String),
GoTo {
page: usize,
fit: Option<super::outline_builder::FitMode>,
},
GoToNamed(String),
GoToRemote {
file: String,
page: usize,
},
Launch(String),
JavaScript(String),
}
#[derive(Debug, Clone)]
pub struct LinkAnnotation {
pub rect: Rect,
pub action: LinkAction,
pub border: BorderStyle,
pub highlight: HighlightMode,
pub color: Option<(f32, f32, f32)>,
pub quad_points: Option<Vec<f32>>,
}
impl LinkAnnotation {
pub fn uri(rect: Rect, uri: impl Into<String>) -> Self {
Self {
rect,
action: LinkAction::Uri(uri.into()),
border: BorderStyle::none(),
highlight: HighlightMode::default(),
color: None,
quad_points: None,
}
}
pub fn goto_page(rect: Rect, page: usize) -> Self {
Self {
rect,
action: LinkAction::GoTo { page, fit: None },
border: BorderStyle::none(),
highlight: HighlightMode::default(),
color: None,
quad_points: None,
}
}
pub fn goto_named(rect: Rect, name: impl Into<String>) -> Self {
Self {
rect,
action: LinkAction::GoToNamed(name.into()),
border: BorderStyle::none(),
highlight: HighlightMode::default(),
color: None,
quad_points: None,
}
}
pub fn with_border(mut self, border: BorderStyle) -> Self {
self.border = border;
self
}
pub fn with_highlight(mut self, highlight: HighlightMode) -> Self {
self.highlight = highlight;
self
}
pub fn with_color(mut self, r: f32, g: f32, b: f32) -> Self {
self.color = Some((r, g, b));
self
}
pub fn with_quad_points(mut self, points: Vec<f32>) -> Self {
self.quad_points = Some(points);
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("Link".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("Border".to_string(), self.border.to_border_array());
dict.insert("H".to_string(), Object::Name(self.highlight.pdf_name().to_string()));
if let Some((r, g, b)) = self.color {
dict.insert(
"C".to_string(),
Object::Array(vec![
Object::Real(r as f64),
Object::Real(g as f64),
Object::Real(b as f64),
]),
);
}
if let Some(ref points) = self.quad_points {
dict.insert(
"QuadPoints".to_string(),
Object::Array(points.iter().map(|&p| Object::Real(p as f64)).collect()),
);
}
match &self.action {
LinkAction::Uri(uri) => {
let mut action = HashMap::new();
action.insert("S".to_string(), Object::Name("URI".to_string()));
action.insert("URI".to_string(), Object::String(uri.as_bytes().to_vec()));
dict.insert("A".to_string(), Object::Dictionary(action));
},
LinkAction::GoTo { page, fit } => {
if let Some(page_ref) = page_refs.get(*page) {
let dest = if let Some(fit_mode) = fit {
self.build_destination(*page_ref, fit_mode)
} else {
Object::Array(vec![
Object::Reference(*page_ref),
Object::Name("Fit".to_string()),
])
};
dict.insert("Dest".to_string(), dest);
}
},
LinkAction::GoToNamed(name) => {
dict.insert("Dest".to_string(), Object::String(name.as_bytes().to_vec()));
},
LinkAction::GoToRemote { file, page } => {
let mut action = HashMap::new();
action.insert("S".to_string(), Object::Name("GoToR".to_string()));
action.insert("F".to_string(), Object::String(file.as_bytes().to_vec()));
action.insert(
"D".to_string(),
Object::Array(vec![
Object::Integer(*page as i64),
Object::Name("Fit".to_string()),
]),
);
dict.insert("A".to_string(), Object::Dictionary(action));
},
LinkAction::Launch(app) => {
let mut action = HashMap::new();
action.insert("S".to_string(), Object::Name("Launch".to_string()));
action.insert("F".to_string(), Object::String(app.as_bytes().to_vec()));
dict.insert("A".to_string(), Object::Dictionary(action));
},
LinkAction::JavaScript(script) => {
let mut action = HashMap::new();
action.insert("S".to_string(), Object::Name("JavaScript".to_string()));
action.insert("JS".to_string(), Object::String(script.as_bytes().to_vec()));
dict.insert("A".to_string(), Object::Dictionary(action));
},
}
dict
}
fn build_destination(
&self,
page_ref: ObjectRef,
fit: &super::outline_builder::FitMode,
) -> Object {
use super::outline_builder::FitMode;
let mut arr = vec![Object::Reference(page_ref)];
match fit {
FitMode::Fit => {
arr.push(Object::Name("Fit".to_string()));
},
FitMode::FitH(top) => {
arr.push(Object::Name("FitH".to_string()));
arr.push(top.map(|t| Object::Real(t as f64)).unwrap_or(Object::Null));
},
FitMode::FitV(left) => {
arr.push(Object::Name("FitV".to_string()));
arr.push(left.map(|l| Object::Real(l as f64)).unwrap_or(Object::Null));
},
FitMode::FitR {
left,
bottom,
right,
top,
} => {
arr.push(Object::Name("FitR".to_string()));
arr.push(Object::Real(*left as f64));
arr.push(Object::Real(*bottom as f64));
arr.push(Object::Real(*right as f64));
arr.push(Object::Real(*top as f64));
},
FitMode::FitB => {
arr.push(Object::Name("FitB".to_string()));
},
FitMode::FitBH(top) => {
arr.push(Object::Name("FitBH".to_string()));
arr.push(top.map(|t| Object::Real(t as f64)).unwrap_or(Object::Null));
},
FitMode::FitBV(left) => {
arr.push(Object::Name("FitBV".to_string()));
arr.push(left.map(|l| Object::Real(l as f64)).unwrap_or(Object::Null));
},
FitMode::XYZ { left, top, zoom } => {
arr.push(Object::Name("XYZ".to_string()));
arr.push(left.map(|l| Object::Real(l as f64)).unwrap_or(Object::Null));
arr.push(top.map(|t| Object::Real(t as f64)).unwrap_or(Object::Null));
arr.push(zoom.map(|z| Object::Real(z as f64)).unwrap_or(Object::Null));
},
}
Object::Array(arr)
}
}
#[derive(Debug, Clone)]
pub enum Annotation {
Link(LinkAnnotation),
TextMarkup(TextMarkupAnnotation),
Text(TextAnnotation),
FreeText(FreeTextAnnotation),
Line(LineAnnotation),
Shape(ShapeAnnotation),
Polygon(PolygonAnnotation),
Ink(InkAnnotation),
Stamp(StampAnnotation),
Popup(PopupAnnotation),
Caret(CaretAnnotation),
FileAttachment(FileAttachmentAnnotation),
Redact(RedactAnnotation),
Watermark(WatermarkAnnotation),
Sound(SoundAnnotation),
Movie(MovieAnnotation),
Screen(ScreenAnnotation),
ThreeD(ThreeDAnnotation),
RichMedia(RichMediaAnnotation),
}
impl Annotation {
pub fn build(&self, page_refs: &[ObjectRef]) -> HashMap<String, Object> {
match self {
Annotation::Link(link) => link.build(page_refs),
Annotation::TextMarkup(markup) => markup.build(page_refs),
Annotation::Text(text) => text.build(page_refs),
Annotation::FreeText(freetext) => freetext.build(page_refs),
Annotation::Line(line) => line.build(page_refs),
Annotation::Shape(shape) => shape.build(page_refs),
Annotation::Polygon(polygon) => polygon.build(page_refs),
Annotation::Ink(ink) => ink.build(page_refs),
Annotation::Stamp(stamp) => stamp.build(page_refs),
Annotation::Popup(popup) => popup.build(page_refs),
Annotation::Caret(caret) => caret.build(page_refs),
Annotation::FileAttachment(file) => file.build(page_refs),
Annotation::Redact(redact) => redact.build(page_refs),
Annotation::Watermark(watermark) => {
let page_ref = page_refs.first().copied().unwrap_or(ObjectRef::new(0, 0));
watermark.build(page_ref)
},
Annotation::Sound(sound) => sound.build(page_refs),
Annotation::Movie(movie) => movie.build(page_refs),
Annotation::Screen(screen) => screen.build(page_refs),
Annotation::ThreeD(threed) => threed.build(page_refs),
Annotation::RichMedia(richmedia) => richmedia.build(page_refs),
}
}
pub fn rect(&self) -> Rect {
match self {
Annotation::Link(link) => link.rect,
Annotation::TextMarkup(markup) => markup.rect,
Annotation::Text(text) => text.rect,
Annotation::FreeText(freetext) => freetext.rect,
Annotation::Line(line) => line.calculate_rect(),
Annotation::Shape(shape) => shape.rect,
Annotation::Polygon(polygon) => polygon.calculate_rect(),
Annotation::Ink(ink) => ink.calculate_rect(),
Annotation::Stamp(stamp) => stamp.rect(),
Annotation::Popup(popup) => popup.rect(),
Annotation::Caret(caret) => caret.rect(),
Annotation::FileAttachment(file) => file.rect(),
Annotation::Redact(redact) => redact.rect(),
Annotation::Watermark(watermark) => watermark.rect(),
Annotation::Sound(sound) => sound.rect,
Annotation::Movie(movie) => movie.rect,
Annotation::Screen(screen) => screen.rect,
Annotation::ThreeD(threed) => threed.rect,
Annotation::RichMedia(richmedia) => richmedia.rect,
}
}
}
impl From<LinkAnnotation> for Annotation {
fn from(link: LinkAnnotation) -> Self {
Annotation::Link(link)
}
}
impl From<TextMarkupAnnotation> for Annotation {
fn from(markup: TextMarkupAnnotation) -> Self {
Annotation::TextMarkup(markup)
}
}
impl From<TextAnnotation> for Annotation {
fn from(text: TextAnnotation) -> Self {
Annotation::Text(text)
}
}
impl From<FreeTextAnnotation> for Annotation {
fn from(freetext: FreeTextAnnotation) -> Self {
Annotation::FreeText(freetext)
}
}
impl From<LineAnnotation> for Annotation {
fn from(line: LineAnnotation) -> Self {
Annotation::Line(line)
}
}
impl From<ShapeAnnotation> for Annotation {
fn from(shape: ShapeAnnotation) -> Self {
Annotation::Shape(shape)
}
}
impl From<PolygonAnnotation> for Annotation {
fn from(polygon: PolygonAnnotation) -> Self {
Annotation::Polygon(polygon)
}
}
impl From<InkAnnotation> for Annotation {
fn from(ink: InkAnnotation) -> Self {
Annotation::Ink(ink)
}
}
impl From<StampAnnotation> for Annotation {
fn from(stamp: StampAnnotation) -> Self {
Annotation::Stamp(stamp)
}
}
impl From<PopupAnnotation> for Annotation {
fn from(popup: PopupAnnotation) -> Self {
Annotation::Popup(popup)
}
}
impl From<CaretAnnotation> for Annotation {
fn from(caret: CaretAnnotation) -> Self {
Annotation::Caret(caret)
}
}
impl From<FileAttachmentAnnotation> for Annotation {
fn from(file: FileAttachmentAnnotation) -> Self {
Annotation::FileAttachment(file)
}
}
impl From<RedactAnnotation> for Annotation {
fn from(redact: RedactAnnotation) -> Self {
Annotation::Redact(redact)
}
}
impl From<WatermarkAnnotation> for Annotation {
fn from(watermark: WatermarkAnnotation) -> Self {
Annotation::Watermark(watermark)
}
}
impl From<SoundAnnotation> for Annotation {
fn from(sound: SoundAnnotation) -> Self {
Annotation::Sound(sound)
}
}
impl From<MovieAnnotation> for Annotation {
fn from(movie: MovieAnnotation) -> Self {
Annotation::Movie(movie)
}
}
impl From<ScreenAnnotation> for Annotation {
fn from(screen: ScreenAnnotation) -> Self {
Annotation::Screen(screen)
}
}
impl From<ThreeDAnnotation> for Annotation {
fn from(threed: ThreeDAnnotation) -> Self {
Annotation::ThreeD(threed)
}
}
impl From<RichMediaAnnotation> for Annotation {
fn from(richmedia: RichMediaAnnotation) -> Self {
Annotation::RichMedia(richmedia)
}
}
#[derive(Debug, Default, Clone)]
pub struct AnnotationBuilder {
annotations: Vec<Annotation>,
}
impl AnnotationBuilder {
pub fn new() -> Self {
Self {
annotations: Vec::new(),
}
}
pub fn add_annotation(&mut self, annotation: impl Into<Annotation>) -> &mut Self {
self.annotations.push(annotation.into());
self
}
pub fn add_link(&mut self, link: LinkAnnotation) -> &mut Self {
self.add_annotation(link)
}
pub fn uri(&mut self, rect: Rect, uri: impl Into<String>) -> &mut Self {
self.add_link(LinkAnnotation::uri(rect, uri))
}
pub fn goto(&mut self, rect: Rect, page: usize) -> &mut Self {
self.add_link(LinkAnnotation::goto_page(rect, page))
}
pub fn annotations(&self) -> &[Annotation] {
&self.annotations
}
pub fn links(&self) -> Vec<&LinkAnnotation> {
self.annotations
.iter()
.filter_map(|a| match a {
Annotation::Link(link) => Some(link),
_ => None,
})
.collect()
}
pub fn text_markups(&self) -> Vec<&TextMarkupAnnotation> {
self.annotations
.iter()
.filter_map(|a| match a {
Annotation::TextMarkup(markup) => Some(markup),
_ => None,
})
.collect()
}
pub fn text_notes(&self) -> Vec<&TextAnnotation> {
self.annotations
.iter()
.filter_map(|a| match a {
Annotation::Text(text) => Some(text),
_ => None,
})
.collect()
}
pub fn add_text_markup(&mut self, markup: TextMarkupAnnotation) -> &mut Self {
self.add_annotation(markup)
}
pub fn add_text_note(&mut self, note: TextAnnotation) -> &mut Self {
self.add_annotation(note)
}
pub fn free_texts(&self) -> Vec<&FreeTextAnnotation> {
self.annotations
.iter()
.filter_map(|a| match a {
Annotation::FreeText(ft) => Some(ft),
_ => None,
})
.collect()
}
pub fn add_freetext(&mut self, freetext: FreeTextAnnotation) -> &mut Self {
self.add_annotation(freetext)
}
pub fn lines(&self) -> Vec<&LineAnnotation> {
self.annotations
.iter()
.filter_map(|a| match a {
Annotation::Line(line) => Some(line),
_ => None,
})
.collect()
}
pub fn add_line(&mut self, line: LineAnnotation) -> &mut Self {
self.add_annotation(line)
}
pub fn shapes(&self) -> Vec<&ShapeAnnotation> {
self.annotations
.iter()
.filter_map(|a| match a {
Annotation::Shape(shape) => Some(shape),
_ => None,
})
.collect()
}
pub fn add_shape(&mut self, shape: ShapeAnnotation) -> &mut Self {
self.add_annotation(shape)
}
pub fn polygons(&self) -> Vec<&PolygonAnnotation> {
self.annotations
.iter()
.filter_map(|a| match a {
Annotation::Polygon(polygon) => Some(polygon),
_ => None,
})
.collect()
}
pub fn add_polygon(&mut self, polygon: PolygonAnnotation) -> &mut Self {
self.add_annotation(polygon)
}
pub fn inks(&self) -> Vec<&InkAnnotation> {
self.annotations
.iter()
.filter_map(|a| match a {
Annotation::Ink(ink) => Some(ink),
_ => None,
})
.collect()
}
pub fn add_ink(&mut self, ink: InkAnnotation) -> &mut Self {
self.add_annotation(ink)
}
pub fn stamps(&self) -> Vec<&StampAnnotation> {
self.annotations
.iter()
.filter_map(|a| match a {
Annotation::Stamp(stamp) => Some(stamp),
_ => None,
})
.collect()
}
pub fn add_stamp(&mut self, stamp: StampAnnotation) -> &mut Self {
self.add_annotation(stamp)
}
pub fn add_popup(&mut self, popup: PopupAnnotation) -> &mut Self {
self.add_annotation(popup)
}
pub fn add_caret(&mut self, caret: CaretAnnotation) -> &mut Self {
self.add_annotation(caret)
}
pub fn add_file_attachment(&mut self, file: FileAttachmentAnnotation) -> &mut Self {
self.add_annotation(file)
}
pub fn add_redact(&mut self, redact: RedactAnnotation) -> &mut Self {
self.add_annotation(redact)
}
pub fn is_empty(&self) -> bool {
self.annotations.is_empty()
}
pub fn len(&self) -> usize {
self.annotations.len()
}
pub fn build(&self, page_refs: &[ObjectRef]) -> Vec<HashMap<String, Object>> {
self.annotations
.iter()
.map(|annot| annot.build(page_refs))
.collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_border_style_none() {
let border = BorderStyle::none();
assert_eq!(border.width, 0.0);
}
#[test]
fn test_border_style_solid() {
let border = BorderStyle::solid(1.0);
assert_eq!(border.width, 1.0);
assert!(border.dash.is_none());
}
#[test]
fn test_border_style_dashed() {
let border = BorderStyle::dashed(2.0, 3.0, 1.0);
assert_eq!(border.width, 2.0);
assert_eq!(border.dash, Some((3.0, 1.0)));
}
#[test]
fn test_highlight_modes() {
assert_eq!(HighlightMode::None.pdf_name(), "N");
assert_eq!(HighlightMode::Invert.pdf_name(), "I");
assert_eq!(HighlightMode::Outline.pdf_name(), "O");
assert_eq!(HighlightMode::Push.pdf_name(), "P");
}
#[test]
fn test_link_annotation_uri() {
let link = LinkAnnotation::uri(Rect::new(72.0, 720.0, 100.0, 12.0), "https://example.com");
assert!(matches!(link.action, LinkAction::Uri(_)));
assert_eq!(link.rect.x, 72.0);
}
#[test]
fn test_link_annotation_goto() {
let link = LinkAnnotation::goto_page(Rect::new(72.0, 720.0, 50.0, 12.0), 5);
assert!(matches!(link.action, LinkAction::GoTo { page: 5, .. }));
}
#[test]
fn test_link_annotation_build_uri() {
let link = LinkAnnotation::uri(Rect::new(0.0, 0.0, 100.0, 20.0), "https://rust-lang.org");
let page_refs = vec![ObjectRef::new(10, 0)];
let dict = link.build(&page_refs);
assert_eq!(dict.get("Type"), Some(&Object::Name("Annot".to_string())));
assert_eq!(dict.get("Subtype"), Some(&Object::Name("Link".to_string())));
assert!(dict.contains_key("A")); }
#[test]
fn test_link_annotation_build_goto() {
let link = LinkAnnotation::goto_page(Rect::new(0.0, 0.0, 100.0, 20.0), 0);
let page_refs = vec![ObjectRef::new(10, 0), ObjectRef::new(11, 0)];
let dict = link.build(&page_refs);
assert!(dict.contains_key("Dest")); }
#[test]
fn test_annotation_builder() {
let mut builder = AnnotationBuilder::new();
assert!(builder.is_empty());
builder.uri(Rect::new(0.0, 0.0, 50.0, 10.0), "https://example.com");
builder.goto(Rect::new(100.0, 0.0, 50.0, 10.0), 1);
assert_eq!(builder.len(), 2);
assert!(!builder.is_empty());
}
#[test]
fn test_link_with_styling() {
let link = LinkAnnotation::uri(Rect::new(0.0, 0.0, 100.0, 20.0), "https://example.com")
.with_border(BorderStyle::solid(1.0))
.with_color(0.0, 0.0, 1.0)
.with_highlight(HighlightMode::Push);
assert_eq!(link.border.width, 1.0);
assert_eq!(link.color, Some((0.0, 0.0, 1.0)));
assert!(matches!(link.highlight, HighlightMode::Push));
}
#[test]
fn test_border_style_with_radius() {
let border = BorderStyle::solid(2.0).with_radius(5.0);
assert_eq!(border.horizontal_radius, 5.0);
assert_eq!(border.vertical_radius, 5.0);
assert_eq!(border.width, 2.0);
}
#[test]
fn test_border_style_default() {
let border = BorderStyle::default();
assert_eq!(border.horizontal_radius, 0.0);
assert_eq!(border.vertical_radius, 0.0);
assert_eq!(border.width, 0.0);
assert!(border.dash.is_none());
}
#[test]
fn test_border_style_to_border_array_solid() {
let border = BorderStyle::solid(1.5);
let obj = border.to_border_array();
match obj {
Object::Array(arr) => {
assert_eq!(arr.len(), 3);
},
_ => panic!("Expected array"),
}
}
#[test]
fn test_border_style_to_border_array_dashed() {
let border = BorderStyle::dashed(2.0, 3.0, 1.0);
let obj = border.to_border_array();
match obj {
Object::Array(arr) => {
assert_eq!(arr.len(), 4); match &arr[3] {
Object::Array(dash) => assert_eq!(dash.len(), 2),
_ => panic!("Expected dash array"),
}
},
_ => panic!("Expected array"),
}
}
#[test]
fn test_border_style_to_border_array_with_radius() {
let border = BorderStyle::solid(1.0).with_radius(3.0);
let obj = border.to_border_array();
match obj {
Object::Array(arr) => {
assert_eq!(arr.len(), 3);
assert_eq!(arr[0], Object::Real(3.0));
assert_eq!(arr[1], Object::Real(3.0));
},
_ => panic!("Expected array"),
}
}
#[test]
fn test_highlight_mode_default() {
let mode = HighlightMode::default();
assert_eq!(mode.pdf_name(), "I"); }
#[test]
fn test_link_annotation_goto_named() {
let link = LinkAnnotation::goto_named(Rect::new(0.0, 0.0, 50.0, 10.0), "chapter1");
assert!(matches!(link.action, LinkAction::GoToNamed(_)));
}
#[test]
fn test_link_annotation_build_goto_named() {
let link = LinkAnnotation::goto_named(Rect::new(0.0, 0.0, 50.0, 10.0), "chapter1");
let page_refs = vec![ObjectRef::new(10, 0)];
let dict = link.build(&page_refs);
assert!(dict.contains_key("Dest"));
match dict.get("Dest") {
Some(Object::String(s)) => {
assert_eq!(s, b"chapter1");
},
_ => panic!("Expected string Dest"),
}
}
#[test]
fn test_link_annotation_build_goto_remote() {
let link = LinkAnnotation {
rect: Rect::new(0.0, 0.0, 100.0, 20.0),
action: LinkAction::GoToRemote {
file: "other.pdf".to_string(),
page: 3,
},
border: BorderStyle::none(),
highlight: HighlightMode::default(),
color: None,
quad_points: None,
};
let page_refs = vec![ObjectRef::new(10, 0)];
let dict = link.build(&page_refs);
assert!(dict.contains_key("A"));
match dict.get("A") {
Some(Object::Dictionary(action)) => {
assert_eq!(action.get("S"), Some(&Object::Name("GoToR".to_string())));
},
_ => panic!("Expected action dict"),
}
}
#[test]
fn test_link_annotation_build_launch() {
let link = LinkAnnotation {
rect: Rect::new(0.0, 0.0, 100.0, 20.0),
action: LinkAction::Launch("/usr/bin/app".to_string()),
border: BorderStyle::none(),
highlight: HighlightMode::default(),
color: None,
quad_points: None,
};
let page_refs = vec![];
let dict = link.build(&page_refs);
match dict.get("A") {
Some(Object::Dictionary(action)) => {
assert_eq!(action.get("S"), Some(&Object::Name("Launch".to_string())));
},
_ => panic!("Expected action dict"),
}
}
#[test]
fn test_link_annotation_build_javascript() {
let link = LinkAnnotation {
rect: Rect::new(0.0, 0.0, 100.0, 20.0),
action: LinkAction::JavaScript("alert('hello')".to_string()),
border: BorderStyle::none(),
highlight: HighlightMode::default(),
color: None,
quad_points: None,
};
let page_refs = vec![];
let dict = link.build(&page_refs);
match dict.get("A") {
Some(Object::Dictionary(action)) => {
assert_eq!(action.get("S"), Some(&Object::Name("JavaScript".to_string())));
},
_ => panic!("Expected action dict"),
}
}
#[test]
fn test_link_annotation_build_with_color() {
let link = LinkAnnotation::uri(Rect::new(0.0, 0.0, 100.0, 20.0), "https://example.com")
.with_color(1.0, 0.0, 0.0);
let dict = link.build(&[]);
assert!(dict.contains_key("C"));
match dict.get("C") {
Some(Object::Array(arr)) => {
assert_eq!(arr.len(), 3);
assert_eq!(arr[0], Object::Real(1.0));
},
_ => panic!("Expected color array"),
}
}
#[test]
fn test_link_annotation_build_no_color() {
let link = LinkAnnotation::uri(Rect::new(0.0, 0.0, 100.0, 20.0), "https://example.com");
let dict = link.build(&[]);
assert!(!dict.contains_key("C"));
}
#[test]
fn test_link_annotation_with_quad_points() {
let points = vec![0.0, 0.0, 100.0, 0.0, 100.0, 20.0, 0.0, 20.0];
let link = LinkAnnotation::uri(Rect::new(0.0, 0.0, 100.0, 20.0), "https://example.com")
.with_quad_points(points.clone());
assert!(link.quad_points.is_some());
let dict = link.build(&[]);
assert!(dict.contains_key("QuadPoints"));
match dict.get("QuadPoints") {
Some(Object::Array(arr)) => assert_eq!(arr.len(), 8),
_ => panic!("Expected quad points array"),
}
}
#[test]
fn test_link_annotation_build_goto_out_of_range() {
let link = LinkAnnotation::goto_page(Rect::new(0.0, 0.0, 100.0, 20.0), 99);
let page_refs = vec![ObjectRef::new(10, 0)]; let dict = link.build(&page_refs);
assert!(!dict.contains_key("Dest"));
}
#[test]
fn test_link_build_goto_with_fit_mode() {
use super::super::outline_builder::FitMode;
let link = LinkAnnotation {
rect: Rect::new(0.0, 0.0, 100.0, 20.0),
action: LinkAction::GoTo {
page: 0,
fit: Some(FitMode::FitH(Some(500.0))),
},
border: BorderStyle::none(),
highlight: HighlightMode::default(),
color: None,
quad_points: None,
};
let page_refs = vec![ObjectRef::new(10, 0)];
let dict = link.build(&page_refs);
assert!(dict.contains_key("Dest"));
}
#[test]
fn test_link_build_goto_with_fit_mode_fit() {
use super::super::outline_builder::FitMode;
let link = LinkAnnotation {
rect: Rect::new(0.0, 0.0, 100.0, 20.0),
action: LinkAction::GoTo {
page: 0,
fit: Some(FitMode::Fit),
},
border: BorderStyle::none(),
highlight: HighlightMode::default(),
color: None,
quad_points: None,
};
let page_refs = vec![ObjectRef::new(10, 0)];
let dict = link.build(&page_refs);
assert!(dict.contains_key("Dest"));
}
#[test]
fn test_link_build_goto_with_fit_mode_fitv() {
use super::super::outline_builder::FitMode;
let link = LinkAnnotation {
rect: Rect::new(0.0, 0.0, 100.0, 20.0),
action: LinkAction::GoTo {
page: 0,
fit: Some(FitMode::FitV(Some(100.0))),
},
border: BorderStyle::none(),
highlight: HighlightMode::default(),
color: None,
quad_points: None,
};
let page_refs = vec![ObjectRef::new(10, 0)];
let dict = link.build(&page_refs);
assert!(dict.contains_key("Dest"));
}
#[test]
fn test_link_build_goto_with_fit_mode_fitr() {
use super::super::outline_builder::FitMode;
let link = LinkAnnotation {
rect: Rect::new(0.0, 0.0, 100.0, 20.0),
action: LinkAction::GoTo {
page: 0,
fit: Some(FitMode::FitR {
left: 0.0,
bottom: 0.0,
right: 612.0,
top: 792.0,
}),
},
border: BorderStyle::none(),
highlight: HighlightMode::default(),
color: None,
quad_points: None,
};
let page_refs = vec![ObjectRef::new(10, 0)];
let dict = link.build(&page_refs);
assert!(dict.contains_key("Dest"));
}
#[test]
fn test_link_build_goto_with_fit_mode_fitb() {
use super::super::outline_builder::FitMode;
let link = LinkAnnotation {
rect: Rect::new(0.0, 0.0, 100.0, 20.0),
action: LinkAction::GoTo {
page: 0,
fit: Some(FitMode::FitB),
},
border: BorderStyle::none(),
highlight: HighlightMode::default(),
color: None,
quad_points: None,
};
let page_refs = vec![ObjectRef::new(10, 0)];
let dict = link.build(&page_refs);
assert!(dict.contains_key("Dest"));
}
#[test]
fn test_link_build_goto_with_fit_mode_fitbh() {
use super::super::outline_builder::FitMode;
let link = LinkAnnotation {
rect: Rect::new(0.0, 0.0, 100.0, 20.0),
action: LinkAction::GoTo {
page: 0,
fit: Some(FitMode::FitBH(Some(700.0))),
},
border: BorderStyle::none(),
highlight: HighlightMode::default(),
color: None,
quad_points: None,
};
let page_refs = vec![ObjectRef::new(10, 0)];
let dict = link.build(&page_refs);
assert!(dict.contains_key("Dest"));
}
#[test]
fn test_link_build_goto_with_fit_mode_fitbv() {
use super::super::outline_builder::FitMode;
let link = LinkAnnotation {
rect: Rect::new(0.0, 0.0, 100.0, 20.0),
action: LinkAction::GoTo {
page: 0,
fit: Some(FitMode::FitBV(None)),
},
border: BorderStyle::none(),
highlight: HighlightMode::default(),
color: None,
quad_points: None,
};
let page_refs = vec![ObjectRef::new(10, 0)];
let dict = link.build(&page_refs);
assert!(dict.contains_key("Dest"));
}
#[test]
fn test_link_build_goto_with_xyz() {
use super::super::outline_builder::FitMode;
let link = LinkAnnotation {
rect: Rect::new(0.0, 0.0, 100.0, 20.0),
action: LinkAction::GoTo {
page: 0,
fit: Some(FitMode::XYZ {
left: Some(72.0),
top: Some(720.0),
zoom: Some(1.5),
}),
},
border: BorderStyle::none(),
highlight: HighlightMode::default(),
color: None,
quad_points: None,
};
let page_refs = vec![ObjectRef::new(10, 0)];
let dict = link.build(&page_refs);
assert!(dict.contains_key("Dest"));
}
#[test]
fn test_link_build_goto_with_fith_none() {
use super::super::outline_builder::FitMode;
let link = LinkAnnotation {
rect: Rect::new(0.0, 0.0, 100.0, 20.0),
action: LinkAction::GoTo {
page: 0,
fit: Some(FitMode::FitH(None)),
},
border: BorderStyle::none(),
highlight: HighlightMode::default(),
color: None,
quad_points: None,
};
let page_refs = vec![ObjectRef::new(10, 0)];
let dict = link.build(&page_refs);
assert!(dict.contains_key("Dest"));
}
#[test]
fn test_annotation_builder_default() {
let builder = AnnotationBuilder::default();
assert!(builder.is_empty());
assert_eq!(builder.len(), 0);
}
#[test]
fn test_annotation_builder_add_annotation() {
let mut builder = AnnotationBuilder::new();
let link = LinkAnnotation::uri(Rect::new(0.0, 0.0, 50.0, 10.0), "https://example.com");
builder.add_annotation(link);
assert_eq!(builder.len(), 1);
assert!(!builder.is_empty());
}
#[test]
fn test_annotation_builder_add_link() {
let mut builder = AnnotationBuilder::new();
let link = LinkAnnotation::uri(Rect::new(0.0, 0.0, 50.0, 10.0), "https://example.com");
builder.add_link(link);
assert_eq!(builder.len(), 1);
assert_eq!(builder.links().len(), 1);
}
#[test]
fn test_annotation_builder_links_filter() {
let mut builder = AnnotationBuilder::new();
builder.uri(Rect::new(0.0, 0.0, 50.0, 10.0), "https://example.com");
builder.goto(Rect::new(100.0, 0.0, 50.0, 10.0), 1);
let links = builder.links();
assert_eq!(links.len(), 2);
}
#[test]
fn test_annotation_builder_text_markups_filter() {
let builder = AnnotationBuilder::new();
assert!(builder.text_markups().is_empty());
}
#[test]
fn test_annotation_builder_text_notes_filter() {
let builder = AnnotationBuilder::new();
assert!(builder.text_notes().is_empty());
}
#[test]
fn test_annotation_builder_free_texts_filter() {
let builder = AnnotationBuilder::new();
assert!(builder.free_texts().is_empty());
}
#[test]
fn test_annotation_builder_lines_filter() {
let builder = AnnotationBuilder::new();
assert!(builder.lines().is_empty());
}
#[test]
fn test_annotation_builder_shapes_filter() {
let builder = AnnotationBuilder::new();
assert!(builder.shapes().is_empty());
}
#[test]
fn test_annotation_builder_polygons_filter() {
let builder = AnnotationBuilder::new();
assert!(builder.polygons().is_empty());
}
#[test]
fn test_annotation_builder_inks_filter() {
let builder = AnnotationBuilder::new();
assert!(builder.inks().is_empty());
}
#[test]
fn test_annotation_builder_stamps_filter() {
let builder = AnnotationBuilder::new();
assert!(builder.stamps().is_empty());
}
#[test]
fn test_annotation_builder_annotations_accessor() {
let mut builder = AnnotationBuilder::new();
builder.uri(Rect::new(0.0, 0.0, 50.0, 10.0), "https://example.com");
assert_eq!(builder.annotations().len(), 1);
}
#[test]
fn test_annotation_builder_build() {
let mut builder = AnnotationBuilder::new();
builder.uri(Rect::new(0.0, 0.0, 100.0, 20.0), "https://example.com");
builder.goto(Rect::new(0.0, 0.0, 100.0, 20.0), 0);
let page_refs = vec![ObjectRef::new(10, 0)];
let dicts = builder.build(&page_refs);
assert_eq!(dicts.len(), 2);
for dict in &dicts {
assert_eq!(dict.get("Type"), Some(&Object::Name("Annot".to_string())));
}
}
#[test]
fn test_annotation_from_link() {
let link = LinkAnnotation::uri(Rect::new(0.0, 0.0, 50.0, 10.0), "https://example.com");
let annot: Annotation = link.into();
assert!(matches!(annot, Annotation::Link(_)));
}
#[test]
fn test_annotation_rect_for_link() {
let link = LinkAnnotation::uri(Rect::new(10.0, 20.0, 50.0, 10.0), "https://example.com");
let annot: Annotation = link.into();
let r = annot.rect();
assert_eq!(r.x, 10.0);
assert_eq!(r.y, 20.0);
}
#[test]
fn test_link_build_rect_values() {
let link = LinkAnnotation::uri(Rect::new(72.0, 720.0, 100.0, 12.0), "https://example.com");
let dict = link.build(&[]);
match dict.get("Rect") {
Some(Object::Array(arr)) => {
assert_eq!(arr.len(), 4);
assert_eq!(arr[0], Object::Real(72.0));
assert_eq!(arr[1], Object::Real(720.0));
assert_eq!(arr[2], Object::Real(172.0)); assert_eq!(arr[3], Object::Real(732.0)); },
_ => panic!("Expected Rect array"),
}
}
#[test]
fn test_link_build_highlight_mode_in_dict() {
let link = LinkAnnotation::uri(Rect::new(0.0, 0.0, 100.0, 20.0), "https://example.com")
.with_highlight(HighlightMode::Outline);
let dict = link.build(&[]);
assert_eq!(dict.get("H"), Some(&Object::Name("O".to_string())));
}
#[test]
fn test_link_build_highlight_mode_none_in_dict() {
let link = LinkAnnotation::uri(Rect::new(0.0, 0.0, 100.0, 20.0), "https://example.com")
.with_highlight(HighlightMode::None);
let dict = link.build(&[]);
assert_eq!(dict.get("H"), Some(&Object::Name("N".to_string())));
}
#[test]
fn test_link_build_border_in_dict() {
let link = LinkAnnotation::uri(Rect::new(0.0, 0.0, 100.0, 20.0), "https://example.com")
.with_border(BorderStyle::dashed(2.0, 3.0, 1.0));
let dict = link.build(&[]);
assert!(dict.contains_key("Border"));
}
}