1mod appearance;
13mod page_attach;
14pub mod types;
15
16#[cfg(feature = "write")]
17use lopdf::{dictionary, Document, Object, ObjectId, Stream};
18
19#[cfg(feature = "write")]
20use crate::appearance_writer::{AppearanceColor, AppearanceStreamBuilder};
21#[cfg(feature = "write")]
22use crate::error::AnnotBuildError;
23
24#[cfg(feature = "write")]
27pub use crate::StampName;
28
29#[cfg(feature = "write")]
30pub use page_attach::add_annotation_to_page;
31
32#[cfg(feature = "write")]
33pub use types::*;
34
35#[cfg(feature = "write")]
37type AppearanceFn = Box<dyn FnOnce(&mut AppearanceStreamBuilder)>;
38
39#[cfg(feature = "write")]
55pub struct AnnotationBuilder {
56 subtype: AnnotSubtype,
57 rect: AnnotRect,
58 color: Option<AppearanceColor>,
59 interior_color: Option<AppearanceColor>,
60 opacity: Option<f64>,
61 border_width: f64,
62 contents: Option<String>,
63 flags: u32,
64 quad_points: Option<Vec<f64>>,
66 line_endpoints: Option<[f64; 4]>,
68 line_endings: Option<[LineEnding; 2]>,
70 ink_list: Option<Vec<Vec<f64>>>,
72 vertices: Option<Vec<f64>>,
74 dash_pattern: Option<Vec<f64>>,
76 default_appearance_str: Option<String>,
78 text_alignment: Option<i64>,
80 icon_name: Option<String>,
82 uri_action: Option<String>,
84 destination: Option<String>,
86 custom_appearance: Option<AppearanceFn>,
88}
89
90#[cfg(feature = "write")]
91impl AnnotationBuilder {
92 pub fn new(subtype: AnnotSubtype, rect: AnnotRect) -> Self {
94 Self {
95 subtype,
96 rect,
97 color: None,
98 interior_color: None,
99 opacity: None,
100 border_width: 1.0,
101 contents: None,
102 flags: 4, quad_points: None,
104 line_endpoints: None,
105 line_endings: None,
106 ink_list: None,
107 vertices: None,
108 dash_pattern: None,
109 default_appearance_str: None,
110 text_alignment: None,
111 icon_name: None,
112 uri_action: None,
113 destination: None,
114 custom_appearance: None,
115 }
116 }
117
118 pub fn free_text(rect: AnnotRect, text: &str, font_size: f64) -> Self {
120 let da = format!("/Helv {font_size} Tf 0 g");
121 let mut b = Self::new(AnnotSubtype::FreeText, rect).contents(text);
122 b.default_appearance_str = Some(da);
123 b
124 }
125
126 pub fn sticky_note(rect: AnnotRect, icon: TextIcon) -> Self {
128 let mut b = Self::new(AnnotSubtype::Text, rect);
129 b.icon_name = Some(icon.as_str().to_string());
130 b
131 }
132
133 pub fn stamp(rect: AnnotRect, name: StampName) -> Self {
135 let mut b = Self::new(AnnotSubtype::Stamp, rect);
136 b.icon_name = Some(name.to_pdf_name().to_string());
137 b
138 }
139
140 pub fn stamp_custom(rect: AnnotRect, name: &str) -> Self {
142 let mut b = Self::new(AnnotSubtype::Stamp, rect);
143 b.icon_name = Some(name.to_string());
144 b
145 }
146
147 pub fn link_uri(rect: AnnotRect, uri: &str) -> Self {
149 let mut b = Self::new(AnnotSubtype::Link, rect);
150 b.uri_action = Some(uri.to_string());
151 b.border_width = 0.0; b
153 }
154
155 pub fn link_dest(rect: AnnotRect, dest: &str) -> Self {
157 let mut b = Self::new(AnnotSubtype::Link, rect);
158 b.destination = Some(dest.to_string());
159 b.border_width = 0.0;
160 b
161 }
162
163 pub fn square(rect: AnnotRect) -> Self {
165 Self::new(AnnotSubtype::Square, rect)
166 }
167
168 pub fn circle(rect: AnnotRect) -> Self {
170 Self::new(AnnotSubtype::Circle, rect)
171 }
172
173 pub fn line(x1: f64, y1: f64, x2: f64, y2: f64) -> Self {
177 let pad = 1.0; let mut min_x = x1.min(x2);
179 let mut min_y = y1.min(y2);
180 let mut max_x = x1.max(x2);
181 let mut max_y = y1.max(y2);
182 if (max_x - min_x).abs() < f64::EPSILON {
183 min_x -= pad;
184 max_x += pad;
185 }
186 if (max_y - min_y).abs() < f64::EPSILON {
187 min_y -= pad;
188 max_y += pad;
189 }
190 let rect = AnnotRect::new(min_x, min_y, max_x, max_y);
191 let mut b = Self::new(AnnotSubtype::Line, rect);
192 b.line_endpoints = Some([x1, y1, x2, y2]);
193 b
194 }
195
196 pub fn ink(rect: AnnotRect, strokes: Vec<Vec<f64>>) -> Self {
198 let mut b = Self::new(AnnotSubtype::Ink, rect);
199 b.ink_list = Some(strokes);
200 b
201 }
202
203 pub fn polygon(rect: AnnotRect, vertices: Vec<f64>) -> Self {
205 let mut b = Self::new(AnnotSubtype::Polygon, rect);
206 b.vertices = Some(vertices);
207 b
208 }
209
210 pub fn polyline(rect: AnnotRect, vertices: Vec<f64>) -> Self {
212 let mut b = Self::new(AnnotSubtype::PolyLine, rect);
213 b.vertices = Some(vertices);
214 b
215 }
216
217 pub fn highlight(rect: AnnotRect) -> Self {
219 Self::new(AnnotSubtype::Highlight, rect)
220 .color(1.0, 1.0, 0.0) .opacity(0.4)
222 }
223
224 pub fn underline(rect: AnnotRect) -> Self {
226 Self::new(AnnotSubtype::Underline, rect).color(0.0, 0.0, 1.0)
227 }
228
229 pub fn strikeout(rect: AnnotRect) -> Self {
231 Self::new(AnnotSubtype::StrikeOut, rect).color(1.0, 0.0, 0.0)
232 }
233
234 pub fn squiggly(rect: AnnotRect) -> Self {
236 Self::new(AnnotSubtype::Squiggly, rect).color(0.0, 0.8, 0.0)
237 }
238
239 pub fn color(mut self, r: f64, g: f64, b: f64) -> Self {
241 self.color = Some(AppearanceColor::new(r, g, b));
242 self
243 }
244
245 pub fn interior_color(mut self, r: f64, g: f64, b: f64) -> Self {
247 self.interior_color = Some(AppearanceColor::new(r, g, b));
248 self
249 }
250
251 pub fn opacity(mut self, alpha: f64) -> Self {
253 self.opacity = Some(alpha.clamp(0.0, 1.0));
254 self
255 }
256
257 pub fn border_width(mut self, width: f64) -> Self {
259 self.border_width = width;
260 self
261 }
262
263 pub fn contents(mut self, text: impl Into<String>) -> Self {
265 self.contents = Some(text.into());
266 self
267 }
268
269 pub fn flags(mut self, flags: u32) -> Self {
271 self.flags = flags;
272 self
273 }
274
275 pub fn alignment(mut self, q: i64) -> Self {
277 self.text_alignment = Some(q);
278 self
279 }
280
281 pub fn line_endings(mut self, start: LineEnding, end: LineEnding) -> Self {
283 self.line_endings = Some([start, end]);
284 self
285 }
286
287 pub fn dash(mut self, pattern: Vec<f64>) -> Self {
289 self.dash_pattern = Some(pattern);
290 self
291 }
292
293 pub fn quad_points(mut self, points: Vec<f64>) -> Self {
300 self.quad_points = Some(points);
301 self
302 }
303
304 pub fn quad_points_from_rect(self, rect: &AnnotRect) -> Self {
306 self.quad_points(vec![
308 rect.x0, rect.y1, rect.x1, rect.y1, rect.x0, rect.y0, rect.x1, rect.y0, ])
313 }
314
315 pub fn appearance(mut self, f: impl FnOnce(&mut AppearanceStreamBuilder) + 'static) -> Self {
317 self.custom_appearance = Some(Box::new(f));
318 self
319 }
320
321 pub(crate) fn set_line_endpoints(&mut self, endpoints: [f64; 4]) {
326 self.line_endpoints = Some(endpoints);
327 }
328
329 pub(crate) fn set_icon_name(&mut self, name: impl Into<String>) {
332 self.icon_name = Some(name.into());
333 }
334
335 pub fn appearance_stream(&mut self, doc: &mut Document) -> Result<ObjectId, AnnotBuildError> {
340 let w = self.rect.width();
341 let h = self.rect.height();
342 if w < f64::EPSILON || h < f64::EPSILON {
343 return Err(AnnotBuildError::InvalidRect);
344 }
345
346 let custom_appearance = self.custom_appearance.take();
348
349 self.build_appearance(doc, w, h, custom_appearance)
350 }
351
352 pub fn build(mut self, doc: &mut Document) -> Result<ObjectId, AnnotBuildError> {
358 let ap_stream_id = self.appearance_stream(doc)?;
360
361 let mut annot_dict = dictionary! {
363 "Type" => "Annot",
364 "Subtype" => Object::Name(self.subtype.as_str().as_bytes().to_vec()),
365 "Rect" => self.rect.as_array(),
366 "F" => Object::Integer(self.flags as i64),
367 };
368
369 if let Some(ref c) = self.color {
371 annot_dict.set(
372 "C",
373 Object::Array(vec![
374 Object::Real(c.r as f32),
375 Object::Real(c.g as f32),
376 Object::Real(c.b as f32),
377 ]),
378 );
379 }
380
381 if let Some(ref ic) = self.interior_color {
383 annot_dict.set(
384 "IC",
385 Object::Array(vec![
386 Object::Real(ic.r as f32),
387 Object::Real(ic.g as f32),
388 Object::Real(ic.b as f32),
389 ]),
390 );
391 }
392
393 if let Some(alpha) = self.opacity {
395 annot_dict.set("CA", Object::Real(alpha as f32));
396 }
397
398 if let Some(ref text) = self.contents {
400 annot_dict.set(
401 "Contents",
402 Object::String(text.as_bytes().to_vec(), lopdf::StringFormat::Literal),
403 );
404 }
405
406 if let Some(ref qp) = self.quad_points {
408 let arr: Vec<Object> = qp.iter().map(|&v| Object::Real(v as f32)).collect();
409 annot_dict.set("QuadPoints", Object::Array(arr));
410 }
411
412 if let Some(ref l) = self.line_endpoints {
414 annot_dict.set(
415 "L",
416 Object::Array(vec![
417 Object::Real(l[0] as f32),
418 Object::Real(l[1] as f32),
419 Object::Real(l[2] as f32),
420 Object::Real(l[3] as f32),
421 ]),
422 );
423 }
424
425 if let Some(ref le) = self.line_endings {
427 annot_dict.set(
428 "LE",
429 Object::Array(vec![
430 Object::Name(le[0].as_str().as_bytes().to_vec()),
431 Object::Name(le[1].as_str().as_bytes().to_vec()),
432 ]),
433 );
434 }
435
436 if let Some(ref ink) = self.ink_list {
438 let ink_arr: Vec<Object> = ink
439 .iter()
440 .map(|stroke| {
441 Object::Array(stroke.iter().map(|&v| Object::Real(v as f32)).collect())
442 })
443 .collect();
444 annot_dict.set("InkList", Object::Array(ink_arr));
445 }
446
447 if let Some(ref verts) = self.vertices {
449 let arr: Vec<Object> = verts.iter().map(|&v| Object::Real(v as f32)).collect();
450 annot_dict.set("Vertices", Object::Array(arr));
451 }
452
453 let has_dash = self.dash_pattern.is_some();
455 if (self.border_width - 1.0).abs() > f64::EPSILON || has_dash {
456 let mut bs = dictionary! {
457 "W" => Object::Real(self.border_width as f32),
458 };
459 if has_dash {
460 bs.set("S", Object::Name(b"D".to_vec()));
461 let d_arr: Vec<Object> = self
462 .dash_pattern
463 .as_ref()
464 .expect("guarded by has_dash which checks is_some()")
465 .iter()
466 .map(|&v| Object::Real(v as f32))
467 .collect();
468 bs.set("D", Object::Array(d_arr));
469 } else {
470 bs.set("S", Object::Name(b"S".to_vec()));
471 }
472 annot_dict.set("BS", Object::Dictionary(bs));
473 }
474
475 if let Some(ref da) = self.default_appearance_str {
477 annot_dict.set(
478 "DA",
479 Object::String(da.as_bytes().to_vec(), lopdf::StringFormat::Literal),
480 );
481 }
482
483 if let Some(q) = self.text_alignment {
485 annot_dict.set("Q", Object::Integer(q));
486 }
487
488 if let Some(ref name) = self.icon_name {
490 annot_dict.set("Name", Object::Name(name.as_bytes().to_vec()));
491 }
492
493 if let Some(ref uri) = self.uri_action {
495 let action = dictionary! {
496 "S" => "URI",
497 "URI" => Object::String(uri.as_bytes().to_vec(), lopdf::StringFormat::Literal),
498 };
499 annot_dict.set("A", Object::Dictionary(action));
500 }
501
502 if let Some(ref dest) = self.destination {
504 annot_dict.set(
505 "Dest",
506 Object::String(dest.as_bytes().to_vec(), lopdf::StringFormat::Literal),
507 );
508 }
509
510 let ap = dictionary! {
512 "N" => Object::Reference(ap_stream_id),
513 };
514 annot_dict.set("AP", Object::Dictionary(ap));
515
516 Ok(doc.add_object(Object::Dictionary(annot_dict)))
517 }
518
519 fn build_appearance(
521 &self,
522 doc: &mut Document,
523 w: f64,
524 h: f64,
525 custom_appearance: Option<AppearanceFn>,
526 ) -> Result<ObjectId, AnnotBuildError> {
527 let mut builder = AppearanceStreamBuilder::new(w, h);
528
529 if let Some(custom) = custom_appearance {
530 custom(&mut builder);
531 } else {
532 self.default_appearance(&mut builder, w, h);
533 }
534
535 let content_bytes = builder
536 .encode()
537 .map_err(AnnotBuildError::AppearanceEncode)?;
538
539 let mut stream_dict = dictionary! {
540 "Type" => "XObject",
541 "Subtype" => "Form",
542 "BBox" => Object::Array(vec![
543 Object::Real(0.0),
544 Object::Real(0.0),
545 Object::Real(w as f32),
546 Object::Real(h as f32),
547 ]),
548 };
549
550 let needs_multiply = matches!(self.subtype, AnnotSubtype::Highlight);
552 let needs_gs = self.opacity.is_some() || needs_multiply;
553 let needs_font = matches!(self.subtype, AnnotSubtype::FreeText | AnnotSubtype::Stamp);
554
555 if needs_gs || needs_font {
556 let mut resources = lopdf::Dictionary::new();
557
558 if needs_gs {
559 let mut gs_dict = dictionary! {
560 "Type" => "ExtGState",
561 };
562 if let Some(alpha) = self.opacity {
563 gs_dict.set("ca", Object::Real(alpha as f32));
564 gs_dict.set("CA", Object::Real(alpha as f32));
565 }
566 if needs_multiply {
567 gs_dict.set("BM", Object::Name(b"Multiply".to_vec()));
568 }
569 let gs_id = doc.add_object(Object::Dictionary(gs_dict));
570 let mut gs_res = lopdf::Dictionary::new();
571 gs_res.set("GS0", Object::Reference(gs_id));
572 resources.set("ExtGState", Object::Dictionary(gs_res));
573 }
574
575 if needs_font {
576 let font_dict = dictionary! {
577 "Type" => "Font",
578 "Subtype" => "Type1",
579 "BaseFont" => "Helvetica",
580 };
581 let font_id = doc.add_object(Object::Dictionary(font_dict));
582 let mut font_res = lopdf::Dictionary::new();
583 font_res.set("Helv", Object::Reference(font_id));
584 resources.set("Font", Object::Dictionary(font_res));
585 }
586
587 stream_dict.set("Resources", Object::Dictionary(resources));
588 }
589
590 let stream = Stream::new(stream_dict, content_bytes);
591 Ok(doc.add_object(Object::Stream(stream)))
592 }
593}
594
595#[cfg(all(test, feature = "write"))]
596mod tests;