1use zpdf_core::{ObjectId, PdfDict, PdfName, PdfObject, PdfString, Rect, Result};
14
15use crate::metadata::encode_text_string;
16use crate::{invalid_data, IncrementalWriter};
17
18#[derive(Debug, Clone)]
20pub enum AnnotationSpec {
21 Markup {
25 kind: MarkupKind,
26 quads: Vec<[f64; 8]>,
27 color: (f64, f64, f64),
28 contents: Option<String>,
30 },
31 Note {
33 x: f64,
35 y: f64,
36 contents: String,
37 color: Option<(f64, f64, f64)>,
38 icon: Option<String>,
40 },
41 FreeText {
43 rect: Rect,
44 contents: String,
45 size: Option<f64>,
47 color: Option<(f64, f64, f64)>,
48 },
49 Square {
51 rect: Rect,
52 color: (f64, f64, f64),
53 interior: Option<(f64, f64, f64)>,
54 width: f64,
55 },
56 Circle {
58 rect: Rect,
59 color: (f64, f64, f64),
60 interior: Option<(f64, f64, f64)>,
61 width: f64,
62 },
63 Line {
65 x1: f64,
66 y1: f64,
67 x2: f64,
68 y2: f64,
69 color: (f64, f64, f64),
70 width: f64,
71 },
72}
73
74#[derive(Debug, Clone, Copy, PartialEq, Eq)]
76pub enum MarkupKind {
77 Highlight,
78 Underline,
79 StrikeOut,
80 Squiggly,
81}
82
83impl MarkupKind {
84 fn subtype(self) -> &'static str {
85 match self {
86 MarkupKind::Highlight => "Highlight",
87 MarkupKind::Underline => "Underline",
88 MarkupKind::StrikeOut => "StrikeOut",
89 MarkupKind::Squiggly => "Squiggly",
90 }
91 }
92}
93
94impl AnnotationSpec {
95 pub fn markup_from_rects(
98 kind: MarkupKind,
99 rects: &[Rect],
100 color: (f64, f64, f64),
101 contents: Option<String>,
102 ) -> Self {
103 let quads = rects
104 .iter()
105 .map(|r| {
106 let r = r.normalize();
107 [r.x0, r.y1, r.x1, r.y1, r.x0, r.y0, r.x1, r.y0]
110 })
111 .collect();
112 AnnotationSpec::Markup {
113 kind,
114 quads,
115 color,
116 contents,
117 }
118 }
119}
120
121impl IncrementalWriter {
122 pub fn add_annotation(&mut self, page_index: usize, spec: &AnnotationSpec) -> Result<ObjectId> {
130 let mut dict = build_annotation_dict(spec)?;
131 let page_id = self.page_id(page_index)?;
132 self.ensure_object_capacity(2)?;
133
134 let appearance = subtype_name(&dict).and_then(|subtype| {
137 let rect = rect_from_dict(&dict)?;
138 zpdf_document::annot_appearance::generate_annotation_appearance(
139 self.document().file(),
140 &dict,
141 &subtype,
142 rect,
143 )
144 });
145
146 if let Some(ap) = appearance {
147 let mut form = PdfDict::new();
148 form.insert(
149 PdfName::new("Type"),
150 PdfObject::Name(PdfName::new("XObject")),
151 );
152 form.insert(
153 PdfName::new("Subtype"),
154 PdfObject::Name(PdfName::new("Form")),
155 );
156 form.insert(PdfName::new("FormType"), PdfObject::Integer(1));
157 form.insert(
158 PdfName::new("BBox"),
159 PdfObject::Array(vec![
160 PdfObject::Real(ap.bbox.x0),
161 PdfObject::Real(ap.bbox.y0),
162 PdfObject::Real(ap.bbox.x1),
163 PdfObject::Real(ap.bbox.y1),
164 ]),
165 );
166 let m = ap.matrix;
167 if m != zpdf_core::Matrix::identity() {
168 form.insert(
169 PdfName::new("Matrix"),
170 PdfObject::Array(vec![
171 PdfObject::Real(m.a),
172 PdfObject::Real(m.b),
173 PdfObject::Real(m.c),
174 PdfObject::Real(m.d),
175 PdfObject::Real(m.e),
176 PdfObject::Real(m.f),
177 ]),
178 );
179 }
180 if !ap.resources.0.is_empty() {
181 form.insert(
182 PdfName::new("Resources"),
183 PdfObject::Dict(ap.resources.clone()),
184 );
185 }
186 let (ap_num, ap_gen) = self.try_add_stream(&form, &ap.content)?;
187 let mut ap_dict = PdfDict::new();
188 ap_dict.insert(
189 PdfName::new("N"),
190 PdfObject::Ref(ObjectId(ap_num, ap_gen as u16)),
191 );
192 dict.insert(PdfName::new("AP"), PdfObject::Dict(ap_dict));
193 }
194
195 let (num, gen) = self.try_add_object(&PdfObject::Dict(dict))?;
196 let annot_id = ObjectId(num, gen as u16);
197
198 let page_obj = self.resolve_current(page_id)?;
200 let mut page_dict = page_obj.as_dict()?.clone();
201 let mut annots = match page_dict.get("Annots") {
202 Some(PdfObject::Ref(r)) => match self.resolve_current(*r) {
203 Ok(obj) => obj.as_array().ok().map(|a| a.to_vec()).unwrap_or_default(),
204 Err(_) => Vec::new(),
205 },
206 Some(PdfObject::Array(arr)) => arr.to_vec(),
207 _ => Vec::new(),
208 };
209 annots.push(PdfObject::Ref(annot_id));
210 page_dict.insert(PdfName::new("Annots"), PdfObject::Array(annots));
211 self.overwrite_object(page_id, PdfObject::Dict(page_dict));
212 Ok(annot_id)
213 }
214}
215
216fn subtype_name(dict: &PdfDict) -> Option<String> {
218 match dict.get("Subtype") {
219 Some(PdfObject::Name(n)) => Some(n.as_str().to_string()),
220 _ => None,
221 }
222}
223
224fn rect_from_dict(dict: &PdfDict) -> Option<Rect> {
226 match dict.get("Rect") {
227 Some(PdfObject::Array(a)) if a.len() == 4 => {
228 let mut v = [0.0f64; 4];
229 for (i, obj) in a.iter().enumerate() {
230 v[i] = match obj {
231 PdfObject::Integer(n) => *n as f64,
232 PdfObject::Real(f) => *f,
233 _ => return None,
234 };
235 }
236 Some(Rect::new(v[0], v[1], v[2], v[3]))
237 }
238 _ => None,
239 }
240}
241
242const NOTE_ICON_SIZE: f64 = 20.0;
244
245fn build_annotation_dict(spec: &AnnotationSpec) -> Result<PdfDict> {
246 let mut dict = PdfDict::new();
247 dict.insert(PdfName::new("Type"), PdfObject::Name(PdfName::new("Annot")));
248
249 match spec {
250 AnnotationSpec::Markup {
251 kind,
252 quads,
253 color,
254 contents,
255 } => {
256 if quads.is_empty() {
257 return Err(invalid_data("markup annotation needs at least one quad").into());
258 }
259 for q in quads {
260 if q.iter().any(|v| !v.is_finite()) {
261 return Err(invalid_data("quad coordinates must be finite").into());
262 }
263 }
264 dict.insert(
265 PdfName::new("Subtype"),
266 PdfObject::Name(PdfName::new(kind.subtype())),
267 );
268 let (mut x0, mut y0) = (f64::INFINITY, f64::INFINITY);
270 let (mut x1, mut y1) = (f64::NEG_INFINITY, f64::NEG_INFINITY);
271 let mut qp = Vec::with_capacity(quads.len() * 8);
272 for q in quads {
273 for (i, &v) in q.iter().enumerate() {
274 if i % 2 == 0 {
275 x0 = x0.min(v);
276 x1 = x1.max(v);
277 } else {
278 y0 = y0.min(v);
279 y1 = y1.max(v);
280 }
281 qp.push(PdfObject::Real(v));
282 }
283 }
284 set_rect(&mut dict, Rect::new(x0, y0, x1, y1));
285 dict.insert(PdfName::new("QuadPoints"), PdfObject::Array(qp));
286 set_color(&mut dict, "C", *color);
287 if let Some(text) = contents {
288 set_contents(&mut dict, text);
289 }
290 }
291 AnnotationSpec::Note {
292 x,
293 y,
294 contents,
295 color,
296 icon,
297 } => {
298 dict.insert(
299 PdfName::new("Subtype"),
300 PdfObject::Name(PdfName::new("Text")),
301 );
302 set_rect(
303 &mut dict,
304 Rect::new(*x, *y, x + NOTE_ICON_SIZE, y + NOTE_ICON_SIZE),
305 );
306 set_contents(&mut dict, contents);
307 if let Some(c) = color {
308 set_color(&mut dict, "C", *c);
309 }
310 if let Some(name) = icon {
311 dict.insert(PdfName::new("Name"), PdfObject::Name(PdfName::new(name)));
312 }
313 }
314 AnnotationSpec::FreeText {
315 rect,
316 contents,
317 size,
318 color,
319 } => {
320 dict.insert(
321 PdfName::new("Subtype"),
322 PdfObject::Name(PdfName::new("FreeText")),
323 );
324 set_rect(&mut dict, *rect);
325 set_contents(&mut dict, contents);
326 let (r, g, b) = color.unwrap_or((0.0, 0.0, 0.0));
329 let da = format!("/Helv {} Tf {r:.3} {g:.3} {b:.3} rg", size.unwrap_or(12.0));
330 dict.insert(
331 PdfName::new("DA"),
332 PdfObject::String(PdfString(da.into_bytes())),
333 );
334 }
335 AnnotationSpec::Square {
336 rect,
337 color,
338 interior,
339 width,
340 }
341 | AnnotationSpec::Circle {
342 rect,
343 color,
344 interior,
345 width,
346 } => {
347 let subtype = if matches!(spec, AnnotationSpec::Square { .. }) {
348 "Square"
349 } else {
350 "Circle"
351 };
352 dict.insert(
353 PdfName::new("Subtype"),
354 PdfObject::Name(PdfName::new(subtype)),
355 );
356 set_rect(&mut dict, *rect);
357 set_color(&mut dict, "C", *color);
358 if let Some(ic) = interior {
359 set_color(&mut dict, "IC", *ic);
360 }
361 set_border_width(&mut dict, *width);
362 }
363 AnnotationSpec::Line {
364 x1,
365 y1,
366 x2,
367 y2,
368 color,
369 width,
370 } => {
371 dict.insert(
372 PdfName::new("Subtype"),
373 PdfObject::Name(PdfName::new("Line")),
374 );
375 let pad = width.max(1.0);
376 set_rect(
377 &mut dict,
378 Rect::new(
379 x1.min(*x2) - pad,
380 y1.min(*y2) - pad,
381 x1.max(*x2) + pad,
382 y1.max(*y2) + pad,
383 ),
384 );
385 dict.insert(
386 PdfName::new("L"),
387 PdfObject::Array(vec![
388 PdfObject::Real(*x1),
389 PdfObject::Real(*y1),
390 PdfObject::Real(*x2),
391 PdfObject::Real(*y2),
392 ]),
393 );
394 set_color(&mut dict, "C", *color);
395 set_border_width(&mut dict, *width);
396 }
397 }
398 Ok(dict)
399}
400
401fn set_rect(dict: &mut PdfDict, rect: Rect) {
402 let r = rect.normalize();
403 dict.insert(
404 PdfName::new("Rect"),
405 PdfObject::Array(vec![
406 PdfObject::Real(r.x0),
407 PdfObject::Real(r.y0),
408 PdfObject::Real(r.x1),
409 PdfObject::Real(r.y1),
410 ]),
411 );
412}
413
414fn set_color(dict: &mut PdfDict, key: &str, (r, g, b): (f64, f64, f64)) {
415 dict.insert(
416 PdfName::new(key),
417 PdfObject::Array(vec![
418 PdfObject::Real(r.clamp(0.0, 1.0)),
419 PdfObject::Real(g.clamp(0.0, 1.0)),
420 PdfObject::Real(b.clamp(0.0, 1.0)),
421 ]),
422 );
423}
424
425fn set_contents(dict: &mut PdfDict, text: &str) {
426 dict.insert(
427 PdfName::new("Contents"),
428 PdfObject::String(encode_text_string(text)),
429 );
430}
431
432fn set_border_width(dict: &mut PdfDict, width: f64) {
433 let mut bs = PdfDict::new();
434 bs.insert(PdfName::new("W"), PdfObject::Real(width.max(0.0)));
435 dict.insert(PdfName::new("BS"), PdfObject::Dict(bs));
436}