use zpdf_core::{ObjectId, PdfDict, PdfName, PdfObject, PdfString, Rect, Result};
use crate::metadata::encode_text_string;
use crate::{invalid_data, IncrementalWriter};
#[derive(Debug, Clone)]
pub enum AnnotationSpec {
Markup {
kind: MarkupKind,
quads: Vec<[f64; 8]>,
color: (f64, f64, f64),
contents: Option<String>,
},
Note {
x: f64,
y: f64,
contents: String,
color: Option<(f64, f64, f64)>,
icon: Option<String>,
},
FreeText {
rect: Rect,
contents: String,
size: Option<f64>,
color: Option<(f64, f64, f64)>,
},
Square {
rect: Rect,
color: (f64, f64, f64),
interior: Option<(f64, f64, f64)>,
width: f64,
},
Circle {
rect: Rect,
color: (f64, f64, f64),
interior: Option<(f64, f64, f64)>,
width: f64,
},
Line {
x1: f64,
y1: f64,
x2: f64,
y2: f64,
color: (f64, f64, f64),
width: f64,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MarkupKind {
Highlight,
Underline,
StrikeOut,
Squiggly,
}
impl MarkupKind {
fn subtype(self) -> &'static str {
match self {
MarkupKind::Highlight => "Highlight",
MarkupKind::Underline => "Underline",
MarkupKind::StrikeOut => "StrikeOut",
MarkupKind::Squiggly => "Squiggly",
}
}
}
impl AnnotationSpec {
pub fn markup_from_rects(
kind: MarkupKind,
rects: &[Rect],
color: (f64, f64, f64),
contents: Option<String>,
) -> Self {
let quads = rects
.iter()
.map(|r| {
let r = r.normalize();
[r.x0, r.y1, r.x1, r.y1, r.x0, r.y0, r.x1, r.y0]
})
.collect();
AnnotationSpec::Markup {
kind,
quads,
color,
contents,
}
}
}
impl IncrementalWriter {
pub fn add_annotation(&mut self, page_index: usize, spec: &AnnotationSpec) -> Result<ObjectId> {
let mut dict = build_annotation_dict(spec)?;
let page_id = self.page_id(page_index)?;
self.ensure_object_capacity(2)?;
let appearance = subtype_name(&dict).and_then(|subtype| {
let rect = rect_from_dict(&dict)?;
zpdf_document::annot_appearance::generate_annotation_appearance(
self.document().file(),
&dict,
&subtype,
rect,
)
});
if let Some(ap) = appearance {
let mut form = PdfDict::new();
form.insert(
PdfName::new("Type"),
PdfObject::Name(PdfName::new("XObject")),
);
form.insert(
PdfName::new("Subtype"),
PdfObject::Name(PdfName::new("Form")),
);
form.insert(PdfName::new("FormType"), PdfObject::Integer(1));
form.insert(
PdfName::new("BBox"),
PdfObject::Array(vec![
PdfObject::Real(ap.bbox.x0),
PdfObject::Real(ap.bbox.y0),
PdfObject::Real(ap.bbox.x1),
PdfObject::Real(ap.bbox.y1),
]),
);
let m = ap.matrix;
if m != zpdf_core::Matrix::identity() {
form.insert(
PdfName::new("Matrix"),
PdfObject::Array(vec![
PdfObject::Real(m.a),
PdfObject::Real(m.b),
PdfObject::Real(m.c),
PdfObject::Real(m.d),
PdfObject::Real(m.e),
PdfObject::Real(m.f),
]),
);
}
if !ap.resources.0.is_empty() {
form.insert(
PdfName::new("Resources"),
PdfObject::Dict(ap.resources.clone()),
);
}
let (ap_num, ap_gen) = self.try_add_stream(&form, &ap.content)?;
let mut ap_dict = PdfDict::new();
ap_dict.insert(
PdfName::new("N"),
PdfObject::Ref(ObjectId(ap_num, ap_gen as u16)),
);
dict.insert(PdfName::new("AP"), PdfObject::Dict(ap_dict));
}
let (num, gen) = self.try_add_object(&PdfObject::Dict(dict))?;
let annot_id = ObjectId(num, gen as u16);
let page_obj = self.resolve_current(page_id)?;
let mut page_dict = page_obj.as_dict()?.clone();
let mut annots = match page_dict.get("Annots") {
Some(PdfObject::Ref(r)) => match self.resolve_current(*r) {
Ok(obj) => obj.as_array().ok().map(|a| a.to_vec()).unwrap_or_default(),
Err(_) => Vec::new(),
},
Some(PdfObject::Array(arr)) => arr.to_vec(),
_ => Vec::new(),
};
annots.push(PdfObject::Ref(annot_id));
page_dict.insert(PdfName::new("Annots"), PdfObject::Array(annots));
self.overwrite_object(page_id, PdfObject::Dict(page_dict));
Ok(annot_id)
}
}
fn subtype_name(dict: &PdfDict) -> Option<String> {
match dict.get("Subtype") {
Some(PdfObject::Name(n)) => Some(n.as_str().to_string()),
_ => None,
}
}
fn rect_from_dict(dict: &PdfDict) -> Option<Rect> {
match dict.get("Rect") {
Some(PdfObject::Array(a)) if a.len() == 4 => {
let mut v = [0.0f64; 4];
for (i, obj) in a.iter().enumerate() {
v[i] = match obj {
PdfObject::Integer(n) => *n as f64,
PdfObject::Real(f) => *f,
_ => return None,
};
}
Some(Rect::new(v[0], v[1], v[2], v[3]))
}
_ => None,
}
}
const NOTE_ICON_SIZE: f64 = 20.0;
fn build_annotation_dict(spec: &AnnotationSpec) -> Result<PdfDict> {
let mut dict = PdfDict::new();
dict.insert(PdfName::new("Type"), PdfObject::Name(PdfName::new("Annot")));
match spec {
AnnotationSpec::Markup {
kind,
quads,
color,
contents,
} => {
if quads.is_empty() {
return Err(invalid_data("markup annotation needs at least one quad").into());
}
for q in quads {
if q.iter().any(|v| !v.is_finite()) {
return Err(invalid_data("quad coordinates must be finite").into());
}
}
dict.insert(
PdfName::new("Subtype"),
PdfObject::Name(PdfName::new(kind.subtype())),
);
let (mut x0, mut y0) = (f64::INFINITY, f64::INFINITY);
let (mut x1, mut y1) = (f64::NEG_INFINITY, f64::NEG_INFINITY);
let mut qp = Vec::with_capacity(quads.len() * 8);
for q in quads {
for (i, &v) in q.iter().enumerate() {
if i % 2 == 0 {
x0 = x0.min(v);
x1 = x1.max(v);
} else {
y0 = y0.min(v);
y1 = y1.max(v);
}
qp.push(PdfObject::Real(v));
}
}
set_rect(&mut dict, Rect::new(x0, y0, x1, y1));
dict.insert(PdfName::new("QuadPoints"), PdfObject::Array(qp));
set_color(&mut dict, "C", *color);
if let Some(text) = contents {
set_contents(&mut dict, text);
}
}
AnnotationSpec::Note {
x,
y,
contents,
color,
icon,
} => {
dict.insert(
PdfName::new("Subtype"),
PdfObject::Name(PdfName::new("Text")),
);
set_rect(
&mut dict,
Rect::new(*x, *y, x + NOTE_ICON_SIZE, y + NOTE_ICON_SIZE),
);
set_contents(&mut dict, contents);
if let Some(c) = color {
set_color(&mut dict, "C", *c);
}
if let Some(name) = icon {
dict.insert(PdfName::new("Name"), PdfObject::Name(PdfName::new(name)));
}
}
AnnotationSpec::FreeText {
rect,
contents,
size,
color,
} => {
dict.insert(
PdfName::new("Subtype"),
PdfObject::Name(PdfName::new("FreeText")),
);
set_rect(&mut dict, *rect);
set_contents(&mut dict, contents);
let (r, g, b) = color.unwrap_or((0.0, 0.0, 0.0));
let da = format!("/Helv {} Tf {r:.3} {g:.3} {b:.3} rg", size.unwrap_or(12.0));
dict.insert(
PdfName::new("DA"),
PdfObject::String(PdfString(da.into_bytes())),
);
}
AnnotationSpec::Square {
rect,
color,
interior,
width,
}
| AnnotationSpec::Circle {
rect,
color,
interior,
width,
} => {
let subtype = if matches!(spec, AnnotationSpec::Square { .. }) {
"Square"
} else {
"Circle"
};
dict.insert(
PdfName::new("Subtype"),
PdfObject::Name(PdfName::new(subtype)),
);
set_rect(&mut dict, *rect);
set_color(&mut dict, "C", *color);
if let Some(ic) = interior {
set_color(&mut dict, "IC", *ic);
}
set_border_width(&mut dict, *width);
}
AnnotationSpec::Line {
x1,
y1,
x2,
y2,
color,
width,
} => {
dict.insert(
PdfName::new("Subtype"),
PdfObject::Name(PdfName::new("Line")),
);
let pad = width.max(1.0);
set_rect(
&mut dict,
Rect::new(
x1.min(*x2) - pad,
y1.min(*y2) - pad,
x1.max(*x2) + pad,
y1.max(*y2) + pad,
),
);
dict.insert(
PdfName::new("L"),
PdfObject::Array(vec![
PdfObject::Real(*x1),
PdfObject::Real(*y1),
PdfObject::Real(*x2),
PdfObject::Real(*y2),
]),
);
set_color(&mut dict, "C", *color);
set_border_width(&mut dict, *width);
}
}
Ok(dict)
}
fn set_rect(dict: &mut PdfDict, rect: Rect) {
let r = rect.normalize();
dict.insert(
PdfName::new("Rect"),
PdfObject::Array(vec![
PdfObject::Real(r.x0),
PdfObject::Real(r.y0),
PdfObject::Real(r.x1),
PdfObject::Real(r.y1),
]),
);
}
fn set_color(dict: &mut PdfDict, key: &str, (r, g, b): (f64, f64, f64)) {
dict.insert(
PdfName::new(key),
PdfObject::Array(vec![
PdfObject::Real(r.clamp(0.0, 1.0)),
PdfObject::Real(g.clamp(0.0, 1.0)),
PdfObject::Real(b.clamp(0.0, 1.0)),
]),
);
}
fn set_contents(dict: &mut PdfDict, text: &str) {
dict.insert(
PdfName::new("Contents"),
PdfObject::String(encode_text_string(text)),
);
}
fn set_border_width(dict: &mut PdfDict, width: f64) {
let mut bs = PdfDict::new();
bs.insert(PdfName::new("W"), PdfObject::Real(width.max(0.0)));
dict.insert(PdfName::new("BS"), PdfObject::Dict(bs));
}