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
6// Copyright (c) 2026 Innovation Trigger B.V.
7//
8// PDFluent is available under two licences, at your option: the GNU AGPLv3, or
9// the PDFluent Commercial Licence. See the LICENSE file in this repository --
10// that file travels with the copy you received, which a URL does not.
11
12mod 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// Re-export the unified StampName from the stamp module so that
25// `pdf_annot::builder::StampName` keeps working for existing call sites.
26#[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/// Type alias for a custom appearance builder closure.
36#[cfg(feature = "write")]
37type AppearanceFn = Box<dyn FnOnce(&mut AppearanceStreamBuilder)>;
38
39/// Builder for creating a PDF annotation and adding it to a document.
40///
41/// # Example
42/// ```no_run
43/// use pdf_annot::builder::{AnnotationBuilder, AnnotSubtype, AnnotRect};
44///
45/// let mut doc = lopdf::Document::with_version("1.7");
46/// // ... add pages ...
47/// let annot_id = AnnotationBuilder::new(AnnotSubtype::Square, AnnotRect::new(100.0, 200.0, 300.0, 400.0))
48///     .color(1.0, 0.0, 0.0)
49///     .border_width(2.0)
50///     .contents("A red square")
51///     .build(&mut doc)
52///     .unwrap();
53/// ```
54#[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    /// QuadPoints for markup annotations (Highlight, Underline, StrikeOut, Squiggly).
65    quad_points: Option<Vec<f64>>,
66    /// Line endpoints /L [x1 y1 x2 y2] for Line annotations.
67    line_endpoints: Option<[f64; 4]>,
68    /// Line endings /LE for Line annotations.
69    line_endings: Option<[LineEnding; 2]>,
70    /// InkList for Ink annotations: list of stroke paths.
71    ink_list: Option<Vec<Vec<f64>>>,
72    /// Vertices for Polygon/PolyLine annotations.
73    vertices: Option<Vec<f64>>,
74    /// Dash pattern for stroked annotations.
75    dash_pattern: Option<Vec<f64>>,
76    /// Default appearance string (/DA) for FreeText annotations.
77    default_appearance_str: Option<String>,
78    /// Text alignment (/Q) for FreeText: 0=left, 1=center, 2=right.
79    text_alignment: Option<i64>,
80    /// Icon name (/Name) for Text (sticky note) and Stamp annotations.
81    icon_name: Option<String>,
82    /// URI action (/A) for Link annotations.
83    uri_action: Option<String>,
84    /// Named destination (/Dest) for Link annotations.
85    destination: Option<String>,
86    /// Custom appearance builder function. If None, a default appearance is generated.
87    custom_appearance: Option<AppearanceFn>,
88}
89
90#[cfg(feature = "write")]
91impl AnnotationBuilder {
92    /// Create a new annotation builder for the given subtype and rectangle.
93    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, // Print flag set by default
103            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    /// Create a FreeText annotation with the given text and font size.
119    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    /// Create a Text (sticky note) annotation.
127    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    /// Create a Stamp annotation with a standard stamp name.
134    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    /// Create a Stamp annotation with a custom name.
141    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    /// Create a Link annotation with a URI action.
148    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; // Links typically have no border.
152        b
153    }
154
155    /// Create a Link annotation with a named destination.
156    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    /// Create a Square annotation.
164    pub fn square(rect: AnnotRect) -> Self {
165        Self::new(AnnotSubtype::Square, rect)
166    }
167
168    /// Create a Circle annotation.
169    pub fn circle(rect: AnnotRect) -> Self {
170        Self::new(AnnotSubtype::Circle, rect)
171    }
172
173    /// Create a Line annotation between two points.
174    ///
175    /// Automatically pads the bounding rect so it is never zero-area.
176    pub fn line(x1: f64, y1: f64, x2: f64, y2: f64) -> Self {
177        let pad = 1.0; // Minimum 1pt padding to prevent zero-area rect.
178        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    /// Create an Ink annotation from stroke paths.
197    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    /// Create a Polygon annotation from vertices.
204    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    /// Create a PolyLine annotation from vertices.
211    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    /// Create a Highlight markup annotation.
218    pub fn highlight(rect: AnnotRect) -> Self {
219        Self::new(AnnotSubtype::Highlight, rect)
220            .color(1.0, 1.0, 0.0) // Yellow
221            .opacity(0.4)
222    }
223
224    /// Create an Underline markup annotation.
225    pub fn underline(rect: AnnotRect) -> Self {
226        Self::new(AnnotSubtype::Underline, rect).color(0.0, 0.0, 1.0)
227    }
228
229    /// Create a StrikeOut markup annotation.
230    pub fn strikeout(rect: AnnotRect) -> Self {
231        Self::new(AnnotSubtype::StrikeOut, rect).color(1.0, 0.0, 0.0)
232    }
233
234    /// Create a Squiggly markup annotation.
235    pub fn squiggly(rect: AnnotRect) -> Self {
236        Self::new(AnnotSubtype::Squiggly, rect).color(0.0, 0.8, 0.0)
237    }
238
239    /// Set the annotation color (RGB, 0.0–1.0).
240    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    /// Set the interior (fill) color for annotations that support it.
246    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    /// Set the opacity (CA/ca, 0.0–1.0).
252    pub fn opacity(mut self, alpha: f64) -> Self {
253        self.opacity = Some(alpha.clamp(0.0, 1.0));
254        self
255    }
256
257    /// Set the border/stroke width.
258    pub fn border_width(mut self, width: f64) -> Self {
259        self.border_width = width;
260        self
261    }
262
263    /// Set the /Contents text.
264    pub fn contents(mut self, text: impl Into<String>) -> Self {
265        self.contents = Some(text.into());
266        self
267    }
268
269    /// Set the annotation flags (raw u32).
270    pub fn flags(mut self, flags: u32) -> Self {
271        self.flags = flags;
272        self
273    }
274
275    /// Set text alignment for FreeText annotations (0=left, 1=center, 2=right).
276    pub fn alignment(mut self, q: i64) -> Self {
277        self.text_alignment = Some(q);
278        self
279    }
280
281    /// Set line endings for Line annotations.
282    pub fn line_endings(mut self, start: LineEnding, end: LineEnding) -> Self {
283        self.line_endings = Some([start, end]);
284        self
285    }
286
287    /// Set a dash pattern for stroked annotations.
288    pub fn dash(mut self, pattern: Vec<f64>) -> Self {
289        self.dash_pattern = Some(pattern);
290        self
291    }
292
293    /// Set QuadPoints for text markup annotations.
294    ///
295    /// Each quadrilateral is defined by 8 values in page coordinates:
296    /// `[x1,y1, x2,y2, x3,y3, x4,y4]` where the points define the
297    /// corners of the marked text region. Multiple quads can be
298    /// concatenated for multi-line selections.
299    pub fn quad_points(mut self, points: Vec<f64>) -> Self {
300        self.quad_points = Some(points);
301        self
302    }
303
304    /// Set QuadPoints from a simple rectangle (single quad).
305    pub fn quad_points_from_rect(self, rect: &AnnotRect) -> Self {
306        // PDF QuadPoints order: top-left, top-right, bottom-left, bottom-right
307        self.quad_points(vec![
308            rect.x0, rect.y1, // top-left
309            rect.x1, rect.y1, // top-right
310            rect.x0, rect.y0, // bottom-left
311            rect.x1, rect.y0, // bottom-right
312        ])
313    }
314
315    /// Provide a custom appearance builder closure.
316    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    /// Set `/L` line endpoints from an existing dictionary.
322    ///
323    /// Crate-visible: the constructors cover new annotations; this covers
324    /// appearance regeneration for annotations parsed from a file.
325    pub(crate) fn set_line_endpoints(&mut self, endpoints: [f64; 4]) {
326        self.line_endpoints = Some(endpoints);
327    }
328
329    /// Set the icon name from an existing dictionary (`/Name` on text and
330    /// stamp annotations). Stamp appearances render the name as a label.
331    pub(crate) fn set_icon_name(&mut self, name: impl Into<String>) {
332        self.icon_name = Some(name.into());
333    }
334
335    /// Build only the appearance Form XObject, without an annotation dict.
336    ///
337    /// `build` uses this for new annotations; appearance regeneration for
338    /// annotations parsed from a file uses it to backfill a missing `/AP`.
339    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        // Extract custom appearance before building (avoids borrow issues).
347        let custom_appearance = self.custom_appearance.take();
348
349        self.build_appearance(doc, w, h, custom_appearance)
350    }
351
352    /// Build the annotation, add it to the document, and return the annotation object ID.
353    ///
354    /// This creates the annotation dictionary, generates the appearance stream,
355    /// and adds both as objects to the document. The annotation is NOT automatically
356    /// added to any page's /Annots array — use `add_to_page` for that.
357    pub fn build(mut self, doc: &mut Document) -> Result<ObjectId, AnnotBuildError> {
358        // Build appearance stream.
359        let ap_stream_id = self.appearance_stream(doc)?;
360
361        // Build the annotation dictionary.
362        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        // Color (/C).
370        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        // Interior color (/IC) — for Square, Circle.
382        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        // Opacity.
394        if let Some(alpha) = self.opacity {
395            annot_dict.set("CA", Object::Real(alpha as f32));
396        }
397
398        // Contents.
399        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        // QuadPoints for markup annotations.
407        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        // Line endpoints (/L) for Line annotations.
413        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        // Line endings (/LE).
426        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        // InkList for Ink annotations.
437        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        // Vertices for Polygon/PolyLine annotations.
448        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        // Border style.
454        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        // Default appearance string (/DA) for FreeText.
476        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        // Text alignment (/Q) for FreeText.
484        if let Some(q) = self.text_alignment {
485            annot_dict.set("Q", Object::Integer(q));
486        }
487
488        // Icon name (/Name) for Text, Stamp.
489        if let Some(ref name) = self.icon_name {
490            annot_dict.set("Name", Object::Name(name.as_bytes().to_vec()));
491        }
492
493        // URI action (/A) for Link.
494        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        // Named destination (/Dest) for Link.
503        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        // Normal appearance.
511        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    /// Build appearance stream and add it as a Form XObject to the document.
520    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        // Build resources for the form XObject.
551        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;