Skip to main content

pdf_annot/builder/
mod.rs

1//! Annotation builder for creating PDF annotations via lopdf.
2//!
3//! Provides `AnnotationBuilder` for constructing annotation dictionaries
4//! with appearance streams and adding them to a PDF document.
5
6mod 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// Re-export the unified StampName from the stamp module so that
19// `pdf_annot::builder::StampName` keeps working for existing call sites.
20#[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/// Type alias for a custom appearance builder closure.
30#[cfg(feature = "write")]
31type AppearanceFn = Box<dyn FnOnce(&mut AppearanceStreamBuilder)>;
32
33/// Builder for creating a PDF annotation and adding it to a document.
34///
35/// # Example
36/// ```no_run
37/// use pdf_annot::builder::{AnnotationBuilder, AnnotSubtype, AnnotRect};
38///
39/// let mut doc = lopdf::Document::with_version("1.7");
40/// // ... add pages ...
41/// let annot_id = AnnotationBuilder::new(AnnotSubtype::Square, AnnotRect::new(100.0, 200.0, 300.0, 400.0))
42///     .color(1.0, 0.0, 0.0)
43///     .border_width(2.0)
44///     .contents("A red square")
45///     .build(&mut doc)
46///     .unwrap();
47/// ```
48#[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    /// QuadPoints for markup annotations (Highlight, Underline, StrikeOut, Squiggly).
59    quad_points: Option<Vec<f64>>,
60    /// Line endpoints /L [x1 y1 x2 y2] for Line annotations.
61    line_endpoints: Option<[f64; 4]>,
62    /// Line endings /LE for Line annotations.
63    line_endings: Option<[LineEnding; 2]>,
64    /// InkList for Ink annotations: list of stroke paths.
65    ink_list: Option<Vec<Vec<f64>>>,
66    /// Vertices for Polygon/PolyLine annotations.
67    vertices: Option<Vec<f64>>,
68    /// Dash pattern for stroked annotations.
69    dash_pattern: Option<Vec<f64>>,
70    /// Default appearance string (/DA) for FreeText annotations.
71    default_appearance_str: Option<String>,
72    /// Text alignment (/Q) for FreeText: 0=left, 1=center, 2=right.
73    text_alignment: Option<i64>,
74    /// Icon name (/Name) for Text (sticky note) and Stamp annotations.
75    icon_name: Option<String>,
76    /// URI action (/A) for Link annotations.
77    uri_action: Option<String>,
78    /// Named destination (/Dest) for Link annotations.
79    destination: Option<String>,
80    /// Custom appearance builder function. If None, a default appearance is generated.
81    custom_appearance: Option<AppearanceFn>,
82}
83
84#[cfg(feature = "write")]
85impl AnnotationBuilder {
86    /// Create a new annotation builder for the given subtype and rectangle.
87    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, // Print flag set by default
97            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    /// Create a FreeText annotation with the given text and font size.
113    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    /// Create a Text (sticky note) annotation.
121    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    /// Create a Stamp annotation with a standard stamp name.
128    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    /// Create a Stamp annotation with a custom name.
135    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    /// Create a Link annotation with a URI action.
142    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; // Links typically have no border.
146        b
147    }
148
149    /// Create a Link annotation with a named destination.
150    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    /// Create a Square annotation.
158    pub fn square(rect: AnnotRect) -> Self {
159        Self::new(AnnotSubtype::Square, rect)
160    }
161
162    /// Create a Circle annotation.
163    pub fn circle(rect: AnnotRect) -> Self {
164        Self::new(AnnotSubtype::Circle, rect)
165    }
166
167    /// Create a Line annotation between two points.
168    ///
169    /// Automatically pads the bounding rect so it is never zero-area.
170    pub fn line(x1: f64, y1: f64, x2: f64, y2: f64) -> Self {
171        let pad = 1.0; // Minimum 1pt padding to prevent zero-area rect.
172        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    /// Create an Ink annotation from stroke paths.
191    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    /// Create a Polygon annotation from vertices.
198    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    /// Create a PolyLine annotation from vertices.
205    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    /// Create a Highlight markup annotation.
212    pub fn highlight(rect: AnnotRect) -> Self {
213        Self::new(AnnotSubtype::Highlight, rect)
214            .color(1.0, 1.0, 0.0) // Yellow
215            .opacity(0.4)
216    }
217
218    /// Create an Underline markup annotation.
219    pub fn underline(rect: AnnotRect) -> Self {
220        Self::new(AnnotSubtype::Underline, rect).color(0.0, 0.0, 1.0)
221    }
222
223    /// Create a StrikeOut markup annotation.
224    pub fn strikeout(rect: AnnotRect) -> Self {
225        Self::new(AnnotSubtype::StrikeOut, rect).color(1.0, 0.0, 0.0)
226    }
227
228    /// Create a Squiggly markup annotation.
229    pub fn squiggly(rect: AnnotRect) -> Self {
230        Self::new(AnnotSubtype::Squiggly, rect).color(0.0, 0.8, 0.0)
231    }
232
233    /// Set the annotation color (RGB, 0.0–1.0).
234    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    /// Set the interior (fill) color for annotations that support it.
240    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    /// Set the opacity (CA/ca, 0.0–1.0).
246    pub fn opacity(mut self, alpha: f64) -> Self {
247        self.opacity = Some(alpha.clamp(0.0, 1.0));
248        self
249    }
250
251    /// Set the border/stroke width.
252    pub fn border_width(mut self, width: f64) -> Self {
253        self.border_width = width;
254        self
255    }
256
257    /// Set the /Contents text.
258    pub fn contents(mut self, text: impl Into<String>) -> Self {
259        self.contents = Some(text.into());
260        self
261    }
262
263    /// Set the annotation flags (raw u32).
264    pub fn flags(mut self, flags: u32) -> Self {
265        self.flags = flags;
266        self
267    }
268
269    /// Set text alignment for FreeText annotations (0=left, 1=center, 2=right).
270    pub fn alignment(mut self, q: i64) -> Self {
271        self.text_alignment = Some(q);
272        self
273    }
274
275    /// Set line endings for Line annotations.
276    pub fn line_endings(mut self, start: LineEnding, end: LineEnding) -> Self {
277        self.line_endings = Some([start, end]);
278        self
279    }
280
281    /// Set a dash pattern for stroked annotations.
282    pub fn dash(mut self, pattern: Vec<f64>) -> Self {
283        self.dash_pattern = Some(pattern);
284        self
285    }
286
287    /// Set QuadPoints for text markup annotations.
288    ///
289    /// Each quadrilateral is defined by 8 values in page coordinates:
290    /// `[x1,y1, x2,y2, x3,y3, x4,y4]` where the points define the
291    /// corners of the marked text region. Multiple quads can be
292    /// concatenated for multi-line selections.
293    pub fn quad_points(mut self, points: Vec<f64>) -> Self {
294        self.quad_points = Some(points);
295        self
296    }
297
298    /// Set QuadPoints from a simple rectangle (single quad).
299    pub fn quad_points_from_rect(self, rect: &AnnotRect) -> Self {
300        // PDF QuadPoints order: top-left, top-right, bottom-left, bottom-right
301        self.quad_points(vec![
302            rect.x0, rect.y1, // top-left
303            rect.x1, rect.y1, // top-right
304            rect.x0, rect.y0, // bottom-left
305            rect.x1, rect.y0, // bottom-right
306        ])
307    }
308
309    /// Provide a custom appearance builder closure.
310    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    /// Build the annotation, add it to the document, and return the annotation object ID.
316    ///
317    /// This creates the annotation dictionary, generates the appearance stream,
318    /// and adds both as objects to the document. The annotation is NOT automatically
319    /// added to any page's /Annots array — use `add_to_page` for that.
320    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        // Extract custom appearance before building (avoids borrow issues).
328        let custom_appearance = self.custom_appearance.take();
329
330        // Build appearance stream.
331        let ap_stream_id = self.build_appearance(doc, w, h, custom_appearance)?;
332
333        // Build the annotation dictionary.
334        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        // Color (/C).
342        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        // Interior color (/IC) — for Square, Circle.
354        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        // Opacity.
366        if let Some(alpha) = self.opacity {
367            annot_dict.set("CA", Object::Real(alpha as f32));
368        }
369
370        // Contents.
371        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        // QuadPoints for markup annotations.
379        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        // Line endpoints (/L) for Line annotations.
385        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        // Line endings (/LE).
398        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        // InkList for Ink annotations.
409        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        // Vertices for Polygon/PolyLine annotations.
420        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        // Border style.
426        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        // Default appearance string (/DA) for FreeText.
448        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        // Text alignment (/Q) for FreeText.
456        if let Some(q) = self.text_alignment {
457            annot_dict.set("Q", Object::Integer(q));
458        }
459
460        // Icon name (/Name) for Text, Stamp.
461        if let Some(ref name) = self.icon_name {
462            annot_dict.set("Name", Object::Name(name.as_bytes().to_vec()));
463        }
464
465        // URI action (/A) for Link.
466        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        // Named destination (/Dest) for Link.
475        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        // Normal appearance.
483        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    /// Build appearance stream and add it as a Form XObject to the document.
492    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        // Build resources for the form XObject.
523        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;