1mod appearance;
7mod page_attach;
8pub mod types;
9
10#[cfg(feature = "write")]
11use lopdf::{dictionary, Document, Object, ObjectId, Stream};
12
13#[cfg(feature = "write")]
14use crate::appearance_writer::{AppearanceColor, AppearanceStreamBuilder};
15#[cfg(feature = "write")]
16use crate::error::AnnotBuildError;
17
18#[cfg(feature = "write")]
21pub use crate::StampName;
22
23#[cfg(feature = "write")]
24pub use page_attach::add_annotation_to_page;
25
26#[cfg(feature = "write")]
27pub use types::*;
28
29#[cfg(feature = "write")]
31type AppearanceFn = Box<dyn FnOnce(&mut AppearanceStreamBuilder)>;
32
33#[cfg(feature = "write")]
49pub struct AnnotationBuilder {
50 subtype: AnnotSubtype,
51 rect: AnnotRect,
52 color: Option<AppearanceColor>,
53 interior_color: Option<AppearanceColor>,
54 opacity: Option<f64>,
55 border_width: f64,
56 contents: Option<String>,
57 flags: u32,
58 quad_points: Option<Vec<f64>>,
60 line_endpoints: Option<[f64; 4]>,
62 line_endings: Option<[LineEnding; 2]>,
64 ink_list: Option<Vec<Vec<f64>>>,
66 vertices: Option<Vec<f64>>,
68 dash_pattern: Option<Vec<f64>>,
70 default_appearance_str: Option<String>,
72 text_alignment: Option<i64>,
74 icon_name: Option<String>,
76 uri_action: Option<String>,
78 destination: Option<String>,
80 custom_appearance: Option<AppearanceFn>,
82}
83
84#[cfg(feature = "write")]
85impl AnnotationBuilder {
86 pub fn new(subtype: AnnotSubtype, rect: AnnotRect) -> Self {
88 Self {
89 subtype,
90 rect,
91 color: None,
92 interior_color: None,
93 opacity: None,
94 border_width: 1.0,
95 contents: None,
96 flags: 4, quad_points: None,
98 line_endpoints: None,
99 line_endings: None,
100 ink_list: None,
101 vertices: None,
102 dash_pattern: None,
103 default_appearance_str: None,
104 text_alignment: None,
105 icon_name: None,
106 uri_action: None,
107 destination: None,
108 custom_appearance: None,
109 }
110 }
111
112 pub fn free_text(rect: AnnotRect, text: &str, font_size: f64) -> Self {
114 let da = format!("/Helv {font_size} Tf 0 g");
115 let mut b = Self::new(AnnotSubtype::FreeText, rect).contents(text);
116 b.default_appearance_str = Some(da);
117 b
118 }
119
120 pub fn sticky_note(rect: AnnotRect, icon: TextIcon) -> Self {
122 let mut b = Self::new(AnnotSubtype::Text, rect);
123 b.icon_name = Some(icon.as_str().to_string());
124 b
125 }
126
127 pub fn stamp(rect: AnnotRect, name: StampName) -> Self {
129 let mut b = Self::new(AnnotSubtype::Stamp, rect);
130 b.icon_name = Some(name.to_pdf_name().to_string());
131 b
132 }
133
134 pub fn stamp_custom(rect: AnnotRect, name: &str) -> Self {
136 let mut b = Self::new(AnnotSubtype::Stamp, rect);
137 b.icon_name = Some(name.to_string());
138 b
139 }
140
141 pub fn link_uri(rect: AnnotRect, uri: &str) -> Self {
143 let mut b = Self::new(AnnotSubtype::Link, rect);
144 b.uri_action = Some(uri.to_string());
145 b.border_width = 0.0; b
147 }
148
149 pub fn link_dest(rect: AnnotRect, dest: &str) -> Self {
151 let mut b = Self::new(AnnotSubtype::Link, rect);
152 b.destination = Some(dest.to_string());
153 b.border_width = 0.0;
154 b
155 }
156
157 pub fn square(rect: AnnotRect) -> Self {
159 Self::new(AnnotSubtype::Square, rect)
160 }
161
162 pub fn circle(rect: AnnotRect) -> Self {
164 Self::new(AnnotSubtype::Circle, rect)
165 }
166
167 pub fn line(x1: f64, y1: f64, x2: f64, y2: f64) -> Self {
171 let pad = 1.0; let mut min_x = x1.min(x2);
173 let mut min_y = y1.min(y2);
174 let mut max_x = x1.max(x2);
175 let mut max_y = y1.max(y2);
176 if (max_x - min_x).abs() < f64::EPSILON {
177 min_x -= pad;
178 max_x += pad;
179 }
180 if (max_y - min_y).abs() < f64::EPSILON {
181 min_y -= pad;
182 max_y += pad;
183 }
184 let rect = AnnotRect::new(min_x, min_y, max_x, max_y);
185 let mut b = Self::new(AnnotSubtype::Line, rect);
186 b.line_endpoints = Some([x1, y1, x2, y2]);
187 b
188 }
189
190 pub fn ink(rect: AnnotRect, strokes: Vec<Vec<f64>>) -> Self {
192 let mut b = Self::new(AnnotSubtype::Ink, rect);
193 b.ink_list = Some(strokes);
194 b
195 }
196
197 pub fn polygon(rect: AnnotRect, vertices: Vec<f64>) -> Self {
199 let mut b = Self::new(AnnotSubtype::Polygon, rect);
200 b.vertices = Some(vertices);
201 b
202 }
203
204 pub fn polyline(rect: AnnotRect, vertices: Vec<f64>) -> Self {
206 let mut b = Self::new(AnnotSubtype::PolyLine, rect);
207 b.vertices = Some(vertices);
208 b
209 }
210
211 pub fn highlight(rect: AnnotRect) -> Self {
213 Self::new(AnnotSubtype::Highlight, rect)
214 .color(1.0, 1.0, 0.0) .opacity(0.4)
216 }
217
218 pub fn underline(rect: AnnotRect) -> Self {
220 Self::new(AnnotSubtype::Underline, rect).color(0.0, 0.0, 1.0)
221 }
222
223 pub fn strikeout(rect: AnnotRect) -> Self {
225 Self::new(AnnotSubtype::StrikeOut, rect).color(1.0, 0.0, 0.0)
226 }
227
228 pub fn squiggly(rect: AnnotRect) -> Self {
230 Self::new(AnnotSubtype::Squiggly, rect).color(0.0, 0.8, 0.0)
231 }
232
233 pub fn color(mut self, r: f64, g: f64, b: f64) -> Self {
235 self.color = Some(AppearanceColor::new(r, g, b));
236 self
237 }
238
239 pub fn interior_color(mut self, r: f64, g: f64, b: f64) -> Self {
241 self.interior_color = Some(AppearanceColor::new(r, g, b));
242 self
243 }
244
245 pub fn opacity(mut self, alpha: f64) -> Self {
247 self.opacity = Some(alpha.clamp(0.0, 1.0));
248 self
249 }
250
251 pub fn border_width(mut self, width: f64) -> Self {
253 self.border_width = width;
254 self
255 }
256
257 pub fn contents(mut self, text: impl Into<String>) -> Self {
259 self.contents = Some(text.into());
260 self
261 }
262
263 pub fn flags(mut self, flags: u32) -> Self {
265 self.flags = flags;
266 self
267 }
268
269 pub fn alignment(mut self, q: i64) -> Self {
271 self.text_alignment = Some(q);
272 self
273 }
274
275 pub fn line_endings(mut self, start: LineEnding, end: LineEnding) -> Self {
277 self.line_endings = Some([start, end]);
278 self
279 }
280
281 pub fn dash(mut self, pattern: Vec<f64>) -> Self {
283 self.dash_pattern = Some(pattern);
284 self
285 }
286
287 pub fn quad_points(mut self, points: Vec<f64>) -> Self {
294 self.quad_points = Some(points);
295 self
296 }
297
298 pub fn quad_points_from_rect(self, rect: &AnnotRect) -> Self {
300 self.quad_points(vec![
302 rect.x0, rect.y1, rect.x1, rect.y1, rect.x0, rect.y0, rect.x1, rect.y0, ])
307 }
308
309 pub fn appearance(mut self, f: impl FnOnce(&mut AppearanceStreamBuilder) + 'static) -> Self {
311 self.custom_appearance = Some(Box::new(f));
312 self
313 }
314
315 pub fn build(mut self, doc: &mut Document) -> Result<ObjectId, AnnotBuildError> {
321 let w = self.rect.width();
322 let h = self.rect.height();
323 if w < f64::EPSILON || h < f64::EPSILON {
324 return Err(AnnotBuildError::InvalidRect);
325 }
326
327 let custom_appearance = self.custom_appearance.take();
329
330 let ap_stream_id = self.build_appearance(doc, w, h, custom_appearance)?;
332
333 let mut annot_dict = dictionary! {
335 "Type" => "Annot",
336 "Subtype" => Object::Name(self.subtype.as_str().as_bytes().to_vec()),
337 "Rect" => self.rect.as_array(),
338 "F" => Object::Integer(self.flags as i64),
339 };
340
341 if let Some(ref c) = self.color {
343 annot_dict.set(
344 "C",
345 Object::Array(vec![
346 Object::Real(c.r as f32),
347 Object::Real(c.g as f32),
348 Object::Real(c.b as f32),
349 ]),
350 );
351 }
352
353 if let Some(ref ic) = self.interior_color {
355 annot_dict.set(
356 "IC",
357 Object::Array(vec![
358 Object::Real(ic.r as f32),
359 Object::Real(ic.g as f32),
360 Object::Real(ic.b as f32),
361 ]),
362 );
363 }
364
365 if let Some(alpha) = self.opacity {
367 annot_dict.set("CA", Object::Real(alpha as f32));
368 }
369
370 if let Some(ref text) = self.contents {
372 annot_dict.set(
373 "Contents",
374 Object::String(text.as_bytes().to_vec(), lopdf::StringFormat::Literal),
375 );
376 }
377
378 if let Some(ref qp) = self.quad_points {
380 let arr: Vec<Object> = qp.iter().map(|&v| Object::Real(v as f32)).collect();
381 annot_dict.set("QuadPoints", Object::Array(arr));
382 }
383
384 if let Some(ref l) = self.line_endpoints {
386 annot_dict.set(
387 "L",
388 Object::Array(vec![
389 Object::Real(l[0] as f32),
390 Object::Real(l[1] as f32),
391 Object::Real(l[2] as f32),
392 Object::Real(l[3] as f32),
393 ]),
394 );
395 }
396
397 if let Some(ref le) = self.line_endings {
399 annot_dict.set(
400 "LE",
401 Object::Array(vec![
402 Object::Name(le[0].as_str().as_bytes().to_vec()),
403 Object::Name(le[1].as_str().as_bytes().to_vec()),
404 ]),
405 );
406 }
407
408 if let Some(ref ink) = self.ink_list {
410 let ink_arr: Vec<Object> = ink
411 .iter()
412 .map(|stroke| {
413 Object::Array(stroke.iter().map(|&v| Object::Real(v as f32)).collect())
414 })
415 .collect();
416 annot_dict.set("InkList", Object::Array(ink_arr));
417 }
418
419 if let Some(ref verts) = self.vertices {
421 let arr: Vec<Object> = verts.iter().map(|&v| Object::Real(v as f32)).collect();
422 annot_dict.set("Vertices", Object::Array(arr));
423 }
424
425 let has_dash = self.dash_pattern.is_some();
427 if (self.border_width - 1.0).abs() > f64::EPSILON || has_dash {
428 let mut bs = dictionary! {
429 "W" => Object::Real(self.border_width as f32),
430 };
431 if has_dash {
432 bs.set("S", Object::Name(b"D".to_vec()));
433 let d_arr: Vec<Object> = self
434 .dash_pattern
435 .as_ref()
436 .expect("guarded by has_dash which checks is_some()")
437 .iter()
438 .map(|&v| Object::Real(v as f32))
439 .collect();
440 bs.set("D", Object::Array(d_arr));
441 } else {
442 bs.set("S", Object::Name(b"S".to_vec()));
443 }
444 annot_dict.set("BS", Object::Dictionary(bs));
445 }
446
447 if let Some(ref da) = self.default_appearance_str {
449 annot_dict.set(
450 "DA",
451 Object::String(da.as_bytes().to_vec(), lopdf::StringFormat::Literal),
452 );
453 }
454
455 if let Some(q) = self.text_alignment {
457 annot_dict.set("Q", Object::Integer(q));
458 }
459
460 if let Some(ref name) = self.icon_name {
462 annot_dict.set("Name", Object::Name(name.as_bytes().to_vec()));
463 }
464
465 if let Some(ref uri) = self.uri_action {
467 let action = dictionary! {
468 "S" => "URI",
469 "URI" => Object::String(uri.as_bytes().to_vec(), lopdf::StringFormat::Literal),
470 };
471 annot_dict.set("A", Object::Dictionary(action));
472 }
473
474 if let Some(ref dest) = self.destination {
476 annot_dict.set(
477 "Dest",
478 Object::String(dest.as_bytes().to_vec(), lopdf::StringFormat::Literal),
479 );
480 }
481
482 let ap = dictionary! {
484 "N" => Object::Reference(ap_stream_id),
485 };
486 annot_dict.set("AP", Object::Dictionary(ap));
487
488 Ok(doc.add_object(Object::Dictionary(annot_dict)))
489 }
490
491 fn build_appearance(
493 &self,
494 doc: &mut Document,
495 w: f64,
496 h: f64,
497 custom_appearance: Option<AppearanceFn>,
498 ) -> Result<ObjectId, AnnotBuildError> {
499 let mut builder = AppearanceStreamBuilder::new(w, h);
500
501 if let Some(custom) = custom_appearance {
502 custom(&mut builder);
503 } else {
504 self.default_appearance(&mut builder, w, h);
505 }
506
507 let content_bytes = builder
508 .encode()
509 .map_err(AnnotBuildError::AppearanceEncode)?;
510
511 let mut stream_dict = dictionary! {
512 "Type" => "XObject",
513 "Subtype" => "Form",
514 "BBox" => Object::Array(vec![
515 Object::Real(0.0),
516 Object::Real(0.0),
517 Object::Real(w as f32),
518 Object::Real(h as f32),
519 ]),
520 };
521
522 let needs_multiply = matches!(self.subtype, AnnotSubtype::Highlight);
524 let needs_gs = self.opacity.is_some() || needs_multiply;
525 let needs_font = matches!(self.subtype, AnnotSubtype::FreeText | AnnotSubtype::Stamp);
526
527 if needs_gs || needs_font {
528 let mut resources = lopdf::Dictionary::new();
529
530 if needs_gs {
531 let mut gs_dict = dictionary! {
532 "Type" => "ExtGState",
533 };
534 if let Some(alpha) = self.opacity {
535 gs_dict.set("ca", Object::Real(alpha as f32));
536 gs_dict.set("CA", Object::Real(alpha as f32));
537 }
538 if needs_multiply {
539 gs_dict.set("BM", Object::Name(b"Multiply".to_vec()));
540 }
541 let gs_id = doc.add_object(Object::Dictionary(gs_dict));
542 let mut gs_res = lopdf::Dictionary::new();
543 gs_res.set("GS0", Object::Reference(gs_id));
544 resources.set("ExtGState", Object::Dictionary(gs_res));
545 }
546
547 if needs_font {
548 let font_dict = dictionary! {
549 "Type" => "Font",
550 "Subtype" => "Type1",
551 "BaseFont" => "Helvetica",
552 };
553 let font_id = doc.add_object(Object::Dictionary(font_dict));
554 let mut font_res = lopdf::Dictionary::new();
555 font_res.set("Helv", Object::Reference(font_id));
556 resources.set("Font", Object::Dictionary(font_res));
557 }
558
559 stream_dict.set("Resources", Object::Dictionary(resources));
560 }
561
562 let stream = Stream::new(stream_dict, content_bytes);
563 Ok(doc.add_object(Object::Stream(stream)))
564 }
565}
566
567#[cfg(all(test, feature = "write"))]
568mod tests;