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 fn build(mut self, doc: &mut Document) -> Result<ObjectId, AnnotBuildError> {
327 let w = self.rect.width();
328 let h = self.rect.height();
329 if w < f64::EPSILON || h < f64::EPSILON {
330 return Err(AnnotBuildError::InvalidRect);
331 }
332
333 let custom_appearance = self.custom_appearance.take();
335
336 let ap_stream_id = self.build_appearance(doc, w, h, custom_appearance)?;
338
339 let mut annot_dict = dictionary! {
341 "Type" => "Annot",
342 "Subtype" => Object::Name(self.subtype.as_str().as_bytes().to_vec()),
343 "Rect" => self.rect.as_array(),
344 "F" => Object::Integer(self.flags as i64),
345 };
346
347 if let Some(ref c) = self.color {
349 annot_dict.set(
350 "C",
351 Object::Array(vec![
352 Object::Real(c.r as f32),
353 Object::Real(c.g as f32),
354 Object::Real(c.b as f32),
355 ]),
356 );
357 }
358
359 if let Some(ref ic) = self.interior_color {
361 annot_dict.set(
362 "IC",
363 Object::Array(vec![
364 Object::Real(ic.r as f32),
365 Object::Real(ic.g as f32),
366 Object::Real(ic.b as f32),
367 ]),
368 );
369 }
370
371 if let Some(alpha) = self.opacity {
373 annot_dict.set("CA", Object::Real(alpha as f32));
374 }
375
376 if let Some(ref text) = self.contents {
378 annot_dict.set(
379 "Contents",
380 Object::String(text.as_bytes().to_vec(), lopdf::StringFormat::Literal),
381 );
382 }
383
384 if let Some(ref qp) = self.quad_points {
386 let arr: Vec<Object> = qp.iter().map(|&v| Object::Real(v as f32)).collect();
387 annot_dict.set("QuadPoints", Object::Array(arr));
388 }
389
390 if let Some(ref l) = self.line_endpoints {
392 annot_dict.set(
393 "L",
394 Object::Array(vec![
395 Object::Real(l[0] as f32),
396 Object::Real(l[1] as f32),
397 Object::Real(l[2] as f32),
398 Object::Real(l[3] as f32),
399 ]),
400 );
401 }
402
403 if let Some(ref le) = self.line_endings {
405 annot_dict.set(
406 "LE",
407 Object::Array(vec![
408 Object::Name(le[0].as_str().as_bytes().to_vec()),
409 Object::Name(le[1].as_str().as_bytes().to_vec()),
410 ]),
411 );
412 }
413
414 if let Some(ref ink) = self.ink_list {
416 let ink_arr: Vec<Object> = ink
417 .iter()
418 .map(|stroke| {
419 Object::Array(stroke.iter().map(|&v| Object::Real(v as f32)).collect())
420 })
421 .collect();
422 annot_dict.set("InkList", Object::Array(ink_arr));
423 }
424
425 if let Some(ref verts) = self.vertices {
427 let arr: Vec<Object> = verts.iter().map(|&v| Object::Real(v as f32)).collect();
428 annot_dict.set("Vertices", Object::Array(arr));
429 }
430
431 let has_dash = self.dash_pattern.is_some();
433 if (self.border_width - 1.0).abs() > f64::EPSILON || has_dash {
434 let mut bs = dictionary! {
435 "W" => Object::Real(self.border_width as f32),
436 };
437 if has_dash {
438 bs.set("S", Object::Name(b"D".to_vec()));
439 let d_arr: Vec<Object> = self
440 .dash_pattern
441 .as_ref()
442 .expect("guarded by has_dash which checks is_some()")
443 .iter()
444 .map(|&v| Object::Real(v as f32))
445 .collect();
446 bs.set("D", Object::Array(d_arr));
447 } else {
448 bs.set("S", Object::Name(b"S".to_vec()));
449 }
450 annot_dict.set("BS", Object::Dictionary(bs));
451 }
452
453 if let Some(ref da) = self.default_appearance_str {
455 annot_dict.set(
456 "DA",
457 Object::String(da.as_bytes().to_vec(), lopdf::StringFormat::Literal),
458 );
459 }
460
461 if let Some(q) = self.text_alignment {
463 annot_dict.set("Q", Object::Integer(q));
464 }
465
466 if let Some(ref name) = self.icon_name {
468 annot_dict.set("Name", Object::Name(name.as_bytes().to_vec()));
469 }
470
471 if let Some(ref uri) = self.uri_action {
473 let action = dictionary! {
474 "S" => "URI",
475 "URI" => Object::String(uri.as_bytes().to_vec(), lopdf::StringFormat::Literal),
476 };
477 annot_dict.set("A", Object::Dictionary(action));
478 }
479
480 if let Some(ref dest) = self.destination {
482 annot_dict.set(
483 "Dest",
484 Object::String(dest.as_bytes().to_vec(), lopdf::StringFormat::Literal),
485 );
486 }
487
488 let ap = dictionary! {
490 "N" => Object::Reference(ap_stream_id),
491 };
492 annot_dict.set("AP", Object::Dictionary(ap));
493
494 Ok(doc.add_object(Object::Dictionary(annot_dict)))
495 }
496
497 fn build_appearance(
499 &self,
500 doc: &mut Document,
501 w: f64,
502 h: f64,
503 custom_appearance: Option<AppearanceFn>,
504 ) -> Result<ObjectId, AnnotBuildError> {
505 let mut builder = AppearanceStreamBuilder::new(w, h);
506
507 if let Some(custom) = custom_appearance {
508 custom(&mut builder);
509 } else {
510 self.default_appearance(&mut builder, w, h);
511 }
512
513 let content_bytes = builder
514 .encode()
515 .map_err(AnnotBuildError::AppearanceEncode)?;
516
517 let mut stream_dict = dictionary! {
518 "Type" => "XObject",
519 "Subtype" => "Form",
520 "BBox" => Object::Array(vec![
521 Object::Real(0.0),
522 Object::Real(0.0),
523 Object::Real(w as f32),
524 Object::Real(h as f32),
525 ]),
526 };
527
528 let needs_multiply = matches!(self.subtype, AnnotSubtype::Highlight);
530 let needs_gs = self.opacity.is_some() || needs_multiply;
531 let needs_font = matches!(self.subtype, AnnotSubtype::FreeText | AnnotSubtype::Stamp);
532
533 if needs_gs || needs_font {
534 let mut resources = lopdf::Dictionary::new();
535
536 if needs_gs {
537 let mut gs_dict = dictionary! {
538 "Type" => "ExtGState",
539 };
540 if let Some(alpha) = self.opacity {
541 gs_dict.set("ca", Object::Real(alpha as f32));
542 gs_dict.set("CA", Object::Real(alpha as f32));
543 }
544 if needs_multiply {
545 gs_dict.set("BM", Object::Name(b"Multiply".to_vec()));
546 }
547 let gs_id = doc.add_object(Object::Dictionary(gs_dict));
548 let mut gs_res = lopdf::Dictionary::new();
549 gs_res.set("GS0", Object::Reference(gs_id));
550 resources.set("ExtGState", Object::Dictionary(gs_res));
551 }
552
553 if needs_font {
554 let font_dict = dictionary! {
555 "Type" => "Font",
556 "Subtype" => "Type1",
557 "BaseFont" => "Helvetica",
558 };
559 let font_id = doc.add_object(Object::Dictionary(font_dict));
560 let mut font_res = lopdf::Dictionary::new();
561 font_res.set("Helv", Object::Reference(font_id));
562 resources.set("Font", Object::Dictionary(font_res));
563 }
564
565 stream_dict.set("Resources", Object::Dictionary(resources));
566 }
567
568 let stream = Stream::new(stream_dict, content_bytes);
569 Ok(doc.add_object(Object::Stream(stream)))
570 }
571}
572
573#[cfg(all(test, feature = "write"))]
574mod tests;