Skip to main content

pdfrum_edit/
annot_spec.rs

1//! Per-subtype builders for [`AnnotSpec`].
2//!
3//! [`AnnotSpec`] is one enum because the write path matches on it, but its
4//! options are not shared: `/IC` belongs to Line, `/H` to Link, `/Open` to
5//! Text. Setting them through the enum means every setter has to accept every
6//! variant and do nothing on the ones it does not apply to, which turns a
7//! misuse into silence rather than an error.
8//!
9//! The builders here carry one subtype each, so only the options that subtype
10//! has exist as methods. Asking a [`TextSpec`] for an interior colour does not
11//! compile, where the removed `AnnotSpec::with_interior` accepted a `Text`
12//! and dropped the call.
13//!
14//! An option that does not belong to a subtype is not a method on that
15//! subtype's builder, so the mistake is a compile error rather than a
16//! silently dropped call:
17//!
18//! ```compile_fail
19//! use kurbo::Rect;
20//! use peniko::Color;
21//! use pdfrum_edit::TextSpec;
22//!
23//! // `/IC` belongs to Line, not Text.
24//! let _ = TextSpec::new(Rect::new(0.0, 0.0, 1.0, 1.0), Color::from_rgb8(0, 0, 0))
25//!     .interior(Color::from_rgb8(0, 0, 255));
26//! ```
27//!
28//! ```compile_fail
29//! use kurbo::Rect;
30//! use peniko::Color;
31//! use pdfrum_edit::{AnnotLinkHighlight, CaretSpec};
32//!
33//! // `/H` belongs to Link, not Caret.
34//! let _ = CaretSpec::new(Rect::new(0.0, 0.0, 1.0, 1.0), Color::from_rgb8(0, 0, 0))
35//!     .highlight(AnnotLinkHighlight::Outline);
36//! ```
37//!
38//! Each builder converts with [`Into<AnnotSpec>`], so they are accepted
39//! wherever a spec is:
40//!
41//! ```
42//! use kurbo::{Point, Rect};
43//! use peniko::Color;
44//! use pdfrum_edit::{LineEndingStyle, LineSpec};
45//!
46//! let spec = LineSpec::new(
47//!     Rect::new(0.0, 0.0, 100.0, 20.0),
48//!     Color::from_rgb8(255, 0, 0),
49//!     Point::new(0.0, 0.0),
50//!     Point::new(100.0, 20.0),
51//! )
52//! .endings(LineEndingStyle::OpenArrow, LineEndingStyle::ClosedArrow)
53//! .interior(Color::from_rgb8(0, 0, 255));
54//! ```
55
56use kurbo::{Point, Rect};
57use pdfrum_object::{Name, ObjRef};
58use peniko::Color;
59
60use crate::annot::{
61    AnnotBorder, AnnotGoToView, AnnotLinkAction, AnnotLinkHighlight, AnnotMeta, AnnotSpec,
62    AnnotWrite, LineEndingStyle, Quad,
63};
64
65/// Declares the setters every subtype shares.
66///
67/// `contents` stays on the builder, since `/Contents` is part of the spec.
68/// The rest are annotation *metadata* rather than subtype options, so they
69/// answer [`AnnotWrite`] — the same shape `AnnotSpec`'s own metadata setters
70/// have, and the reason a typed builder no longer has to fall back to the
71/// enum to name an author.
72macro_rules! spec_builder {
73    ($builder:ident, $doc:literal) => {
74        impl $builder {
75            #[doc = $doc]
76            ///
77            /// Sets `/Contents`.
78            #[must_use]
79            pub fn contents(mut self, contents: impl Into<String>) -> Self {
80                self.contents = Some(contents.into());
81                self
82            }
83
84            #[doc = $doc]
85            ///
86            /// Sets `/T`, the author.
87            #[must_use]
88            pub fn author(self, author: impl Into<String>) -> AnnotWrite {
89                AnnotSpec::from(self).with_author(author)
90            }
91
92            #[doc = $doc]
93            ///
94            /// Sets `/NM`, the annotation's unique name.
95            #[must_use]
96            pub fn name(self, name: impl Into<String>) -> AnnotWrite {
97                AnnotSpec::from(self).with_name(name)
98            }
99
100            #[doc = $doc]
101            ///
102            /// Sets `/M`, the modification date.
103            #[must_use]
104            pub fn modified(self, modified: impl Into<String>) -> AnnotWrite {
105                AnnotSpec::from(self).with_modified(modified)
106            }
107
108            #[doc = $doc]
109            ///
110            /// Sets `/F`, the annotation flags.
111            #[must_use]
112            pub fn flags(self, flags: pdfrum_doc::AnnotFlags) -> AnnotWrite {
113                AnnotSpec::from(self).with_flags(flags)
114            }
115
116            #[doc = $doc]
117            ///
118            /// Sets every metadata field at once.
119            #[must_use]
120            pub fn meta(self, meta: AnnotMeta) -> AnnotWrite {
121                AnnotSpec::from(self).with_meta(meta)
122            }
123        }
124
125        // So a builder reaches `add_annotation` directly, the way an
126        // `AnnotSpec` does — without it every call site would need an
127        // `.into()` naming a type the caller never mentions.
128        impl From<$builder> for AnnotWrite {
129            fn from(builder: $builder) -> Self {
130                AnnotSpec::from(builder).into()
131            }
132        }
133    };
134}
135
136/// A text-markup annotation: Highlight, Underline, `StrikeOut`, or Squiggly.
137///
138/// The four share a shape — a colour and a set of `/QuadPoints` — so one
139/// builder covers them and [`MarkupKind`] picks the subtype.
140///
141/// ```
142/// use kurbo::Rect;
143/// use peniko::Color;
144/// use pdfrum_edit::{AnnotSpec, MarkupKind, MarkupSpec};
145///
146/// let rect = Rect::new(0.0, 0.0, 80.0, 12.0);
147/// let spec: AnnotSpec = MarkupSpec::new(MarkupKind::Highlight, rect, Color::from_rgb8(255, 255, 0))
148///     .quads([rect.into()])
149///     .contents("note")
150///     .into();
151/// ```
152#[derive(Debug, Clone, PartialEq)]
153pub struct MarkupSpec {
154    kind: MarkupKind,
155    rect: Rect,
156    color: Color,
157    quads: Vec<Quad>,
158    contents: Option<String>,
159}
160
161/// Which text-markup subtype a [`MarkupSpec`] writes.
162#[derive(Debug, Clone, Copy, PartialEq, Eq)]
163#[non_exhaustive]
164pub enum MarkupKind {
165    /// `/Subtype /Highlight`.
166    Highlight,
167    /// `/Subtype /Underline`.
168    Underline,
169    /// `/Subtype /StrikeOut`.
170    StrikeOut,
171    /// `/Subtype /Squiggly`.
172    Squiggly,
173}
174
175impl MarkupSpec {
176    /// A markup annotation covering `rect` as a single quadrilateral.
177    ///
178    /// Replace the geometry with [`MarkupSpec::quads`] to mark up a
179    /// selection of several runs.
180    #[must_use]
181    pub fn new(kind: MarkupKind, rect: Rect, color: Color) -> Self {
182        Self {
183            kind,
184            rect,
185            color,
186            quads: vec![Quad::from(rect)],
187            contents: None,
188        }
189    }
190
191    /// Replaces the `/QuadPoints`, one quadrilateral per marked run.
192    ///
193    /// Writing a spec whose quads are empty fails with
194    /// [`crate::Error::EmptyQuadPoints`]. `pdfrum_text::rects_loose` gives
195    /// the em-box rectangles Acrobat marks up.
196    #[must_use]
197    pub fn quads(mut self, quads: impl IntoIterator<Item = Quad>) -> Self {
198        self.quads = quads.into_iter().collect();
199        self
200    }
201}
202
203spec_builder!(MarkupSpec, "A text-markup annotation.");
204
205impl From<MarkupSpec> for AnnotSpec {
206    fn from(b: MarkupSpec) -> Self {
207        let MarkupSpec {
208            kind,
209            rect,
210            color,
211            quads,
212            contents,
213        } = b;
214        match kind {
215            MarkupKind::Highlight => Self::Highlight {
216                rect,
217                color,
218                quads,
219                contents,
220            },
221            MarkupKind::Underline => Self::Underline {
222                rect,
223                color,
224                quads,
225                contents,
226            },
227            MarkupKind::StrikeOut => Self::StrikeOut {
228                rect,
229                color,
230                quads,
231                contents,
232            },
233            MarkupKind::Squiggly => Self::Squiggly {
234                rect,
235                color,
236                quads,
237                contents,
238            },
239        }
240    }
241}
242
243/// A sticky-note text annotation (`/Subtype /Text`).
244///
245/// ```
246/// use kurbo::Rect;
247/// use peniko::Color;
248/// use pdfrum_edit::{AnnotSpec, TextSpec};
249///
250/// let spec: AnnotSpec = TextSpec::new(Rect::new(0.0, 0.0, 20.0, 20.0), Color::from_rgb8(255, 255, 0))
251///     .icon("Note")
252///     .open(true)
253///     .into();
254/// ```
255#[derive(Debug, Clone, PartialEq)]
256pub struct TextSpec {
257    rect: Rect,
258    color: Color,
259    contents: Option<String>,
260    icon: Option<Name>,
261    open: bool,
262}
263
264impl TextSpec {
265    /// A sticky note at `rect`, `/Comment` icon, pop-up closed.
266    #[must_use]
267    pub fn new(rect: Rect, color: Color) -> Self {
268        Self {
269            rect,
270            color,
271            contents: None,
272            icon: None,
273            open: false,
274        }
275    }
276
277    /// Sets the icon name (`/Name`), such as `Note` or `Help`.
278    #[must_use]
279    pub fn icon(mut self, icon: impl Into<Name>) -> Self {
280        self.icon = Some(icon.into());
281        self
282    }
283
284    /// Sets whether the pop-up starts open (`/Open`).
285    #[must_use]
286    pub fn open(mut self, open: bool) -> Self {
287        self.open = open;
288        self
289    }
290}
291
292spec_builder!(TextSpec, "A sticky-note annotation.");
293
294impl From<TextSpec> for AnnotSpec {
295    fn from(b: TextSpec) -> Self {
296        let TextSpec {
297            rect,
298            color,
299            contents,
300            icon,
301            open,
302        } = b;
303        Self::Text {
304            rect,
305            color,
306            contents,
307            icon: icon.unwrap_or_else(|| crate::names::COMMENT.clone()),
308            open,
309        }
310    }
311}
312
313/// A square / area annotation (`/Subtype /Square`).
314#[derive(Debug, Clone, PartialEq)]
315pub struct SquareSpec {
316    rect: Rect,
317    color: Color,
318    contents: Option<String>,
319    border: Option<AnnotBorder>,
320    interior: Option<Color>,
321}
322
323impl SquareSpec {
324    /// A square over `rect` with the default border (width 2, solid).
325    #[must_use]
326    pub fn new(rect: Rect, color: Color) -> Self {
327        Self {
328            rect,
329            color,
330            contents: None,
331            border: None,
332            interior: None,
333        }
334    }
335
336    /// Sets `/BS` width and style.
337    #[must_use]
338    pub fn border(mut self, border: AnnotBorder) -> Self {
339        self.border = Some(border);
340        self
341    }
342
343    /// Sets `/IC`, the colour filling the shape.
344    ///
345    /// Left unset the shape is an outline, which is what an absent `/IC`
346    /// means to a reader.
347    #[must_use]
348    pub fn interior(mut self, color: Color) -> Self {
349        self.interior = Some(color);
350        self
351    }
352}
353
354spec_builder!(SquareSpec, "A square annotation.");
355
356impl From<SquareSpec> for AnnotSpec {
357    fn from(b: SquareSpec) -> Self {
358        let SquareSpec {
359            rect,
360            color,
361            contents,
362            border,
363            interior,
364        } = b;
365        Self::Square {
366            rect,
367            color,
368            contents,
369            border: border.unwrap_or_default(),
370            interior,
371        }
372    }
373}
374
375/// A circle / ellipse annotation (`/Subtype /Circle`).
376#[derive(Debug, Clone, PartialEq)]
377pub struct CircleSpec {
378    rect: Rect,
379    color: Color,
380    contents: Option<String>,
381    border: Option<AnnotBorder>,
382    interior: Option<Color>,
383}
384
385impl CircleSpec {
386    /// An ellipse inscribed in `rect` with the default border.
387    #[must_use]
388    pub fn new(rect: Rect, color: Color) -> Self {
389        Self {
390            rect,
391            color,
392            contents: None,
393            border: None,
394            interior: None,
395        }
396    }
397
398    /// Sets `/BS` width and style.
399    #[must_use]
400    pub fn border(mut self, border: AnnotBorder) -> Self {
401        self.border = Some(border);
402        self
403    }
404
405    /// Sets `/IC`, the colour filling the shape.
406    ///
407    /// Left unset the shape is an outline, which is what an absent `/IC`
408    /// means to a reader.
409    #[must_use]
410    pub fn interior(mut self, color: Color) -> Self {
411        self.interior = Some(color);
412        self
413    }
414}
415
416spec_builder!(CircleSpec, "A circle annotation.");
417
418impl From<CircleSpec> for AnnotSpec {
419    fn from(b: CircleSpec) -> Self {
420        let CircleSpec {
421            rect,
422            color,
423            contents,
424            border,
425            interior,
426        } = b;
427        Self::Circle {
428            rect,
429            color,
430            contents,
431            border: border.unwrap_or_default(),
432            interior,
433        }
434    }
435}
436
437/// A freehand ink annotation (`/Subtype /Ink`).
438#[derive(Debug, Clone, PartialEq)]
439pub struct InkSpec {
440    rect: Rect,
441    color: Color,
442    strokes: Vec<Vec<Point>>,
443    contents: Option<String>,
444    border: Option<AnnotBorder>,
445}
446
447impl InkSpec {
448    /// Ink over `rect`, one polyline per stroke.
449    #[must_use]
450    pub fn new(rect: Rect, color: Color, strokes: Vec<Vec<Point>>) -> Self {
451        Self {
452            rect,
453            color,
454            strokes,
455            contents: None,
456            border: None,
457        }
458    }
459
460    /// Sets `/BS` width and style.
461    #[must_use]
462    pub fn border(mut self, border: AnnotBorder) -> Self {
463        self.border = Some(border);
464        self
465    }
466}
467
468spec_builder!(InkSpec, "An ink annotation.");
469
470impl From<InkSpec> for AnnotSpec {
471    fn from(b: InkSpec) -> Self {
472        let InkSpec {
473            rect,
474            color,
475            strokes,
476            contents,
477            border,
478        } = b;
479        Self::Ink {
480            rect,
481            color,
482            strokes,
483            contents,
484            border: border.unwrap_or_default(),
485        }
486    }
487}
488
489/// A line annotation (`/Subtype /Line`).
490///
491/// ```
492/// use kurbo::{Point, Rect};
493/// use peniko::Color;
494/// use pdfrum_edit::{AnnotSpec, LineEndingStyle, LineSpec};
495///
496/// let spec: AnnotSpec = LineSpec::new(
497///     Rect::new(0.0, 0.0, 100.0, 20.0),
498///     Color::from_rgb8(255, 0, 0),
499///     Point::new(0.0, 0.0),
500///     Point::new(100.0, 20.0),
501/// )
502/// .endings(LineEndingStyle::OpenArrow, LineEndingStyle::ClosedArrow)
503/// .into();
504/// ```
505#[derive(Debug, Clone, PartialEq)]
506pub struct LineSpec {
507    rect: Rect,
508    color: Color,
509    start: Point,
510    end: Point,
511    contents: Option<String>,
512    border: Option<AnnotBorder>,
513    endings: Option<(LineEndingStyle, LineEndingStyle)>,
514    interior: Option<Color>,
515}
516
517impl LineSpec {
518    /// A line from `start` to `end`, with `/Rect` as its bounding box.
519    #[must_use]
520    pub fn new(rect: Rect, color: Color, start: Point, end: Point) -> Self {
521        Self {
522            rect,
523            color,
524            start,
525            end,
526            contents: None,
527            border: None,
528            endings: None,
529            interior: None,
530        }
531    }
532
533    /// Sets `/BS` width and style. Ending decorations scale with the width.
534    #[must_use]
535    pub fn border(mut self, border: AnnotBorder) -> Self {
536        self.border = Some(border);
537        self
538    }
539
540    /// Sets the `/LE` ending styles. Omitted by default.
541    #[must_use]
542    pub fn endings(mut self, start: LineEndingStyle, end: LineEndingStyle) -> Self {
543        self.endings = Some((start, end));
544        self
545    }
546
547    /// Sets `/IC`, the fill for closed endings. Unfilled without it.
548    #[must_use]
549    pub fn interior(mut self, color: Color) -> Self {
550        self.interior = Some(color);
551        self
552    }
553}
554
555spec_builder!(LineSpec, "A line annotation.");
556
557impl From<LineSpec> for AnnotSpec {
558    fn from(b: LineSpec) -> Self {
559        let LineSpec {
560            rect,
561            color,
562            start,
563            end,
564            contents,
565            border,
566            endings,
567            interior,
568        } = b;
569        Self::Line {
570            rect,
571            color,
572            start,
573            end,
574            contents,
575            border: border.unwrap_or_default(),
576            line_endings: endings,
577            interior,
578        }
579    }
580}
581
582/// A link annotation (`/Subtype /Link`).
583///
584/// The action is required, so it is taken at construction; the constructors
585/// mirror [`AnnotSpec`]'s link family.
586///
587/// ```
588/// use kurbo::Rect;
589/// use pdfrum_edit::{AnnotLinkHighlight, AnnotSpec, LinkSpec};
590///
591/// let spec: AnnotSpec = LinkSpec::uri(Rect::new(0.0, 0.0, 50.0, 10.0), "https://example.com")
592///     .highlight(AnnotLinkHighlight::Outline)
593///     .into();
594/// ```
595#[derive(Debug, Clone, PartialEq)]
596pub struct LinkSpec {
597    rect: Rect,
598    action: AnnotLinkAction,
599    contents: Option<String>,
600    color: Option<Color>,
601    border: Option<AnnotBorder>,
602    highlight: Option<AnnotLinkHighlight>,
603}
604
605impl LinkSpec {
606    /// A link at `rect` running `action`.
607    #[must_use]
608    pub fn new(rect: Rect, action: AnnotLinkAction) -> Self {
609        Self {
610            rect,
611            action,
612            contents: None,
613            color: None,
614            border: None,
615            highlight: None,
616        }
617    }
618
619    /// A link opening `uri`.
620    #[must_use]
621    pub fn uri(rect: Rect, uri: impl Into<String>) -> Self {
622        Self::new(rect, AnnotLinkAction::Uri(uri.into()))
623    }
624
625    /// A link jumping to `page` in this document.
626    #[must_use]
627    pub fn goto(rect: Rect, page: ObjRef, view: AnnotGoToView) -> Self {
628        Self::new(rect, AnnotLinkAction::GoTo { page, view })
629    }
630
631    /// A link to a named destination, registering `name` on write.
632    #[must_use]
633    pub fn named(rect: Rect, name: impl Into<String>, page: ObjRef, view: AnnotGoToView) -> Self {
634        Self::new(
635            rect,
636            AnnotLinkAction::Named {
637                name: name.into(),
638                page,
639                view,
640            },
641        )
642    }
643
644    /// A link to a destination the document already names.
645    #[must_use]
646    pub fn named_existing(rect: Rect, name: impl Into<String>) -> Self {
647        Self::new(rect, AnnotLinkAction::NamedExisting { name: name.into() })
648    }
649
650    /// A link into another file, at a page number and view.
651    #[must_use]
652    pub fn goto_r(rect: Rect, file: impl Into<String>, page: i64, view: AnnotGoToView) -> Self {
653        Self::new(
654            rect,
655            AnnotLinkAction::GoToR {
656                file: file.into(),
657                dest: crate::AnnotRemoteDest::Page { page, view },
658                new_window: None,
659            },
660        )
661    }
662
663    /// A link into another file, at a destination that file names.
664    #[must_use]
665    pub fn goto_r_named(rect: Rect, file: impl Into<String>, name: impl Into<String>) -> Self {
666        Self::new(
667            rect,
668            AnnotLinkAction::GoToR {
669                file: file.into(),
670                dest: crate::AnnotRemoteDest::Named(name.into()),
671                new_window: None,
672            },
673        )
674    }
675
676    /// A link that launches a file or application.
677    #[must_use]
678    pub fn launch(rect: Rect, file: impl Into<String>) -> Self {
679        Self::new(rect, AnnotLinkAction::Launch { file: file.into() })
680    }
681
682    /// Sets `/C`, the colour of the link's border chrome.
683    #[must_use]
684    pub fn color(mut self, color: Color) -> Self {
685        self.color = Some(color);
686        self
687    }
688
689    /// Sets `/BS` width and style.
690    #[must_use]
691    pub fn border(mut self, border: AnnotBorder) -> Self {
692        self.border = Some(border);
693        self
694    }
695
696    /// Sets `/H`, the viewer's click feedback. Defaults to Invert.
697    #[must_use]
698    pub fn highlight(mut self, highlight: AnnotLinkHighlight) -> Self {
699        self.highlight = Some(highlight);
700        self
701    }
702}
703
704spec_builder!(LinkSpec, "A link annotation.");
705
706impl From<LinkSpec> for AnnotSpec {
707    fn from(b: LinkSpec) -> Self {
708        let LinkSpec {
709            rect,
710            action,
711            contents,
712            color,
713            border,
714            highlight,
715        } = b;
716        Self::Link {
717            rect,
718            action,
719            contents,
720            color,
721            // A link's border defaults to a hairline, not the width-2 the
722            // drawn shapes take: a link boxed as thickly as a square would be
723            // wrong over text.
724            border: border.unwrap_or_else(|| AnnotBorder::solid(1.0)),
725            highlight: highlight.unwrap_or_default(),
726        }
727    }
728}
729
730/// A caret annotation (`/Subtype /Caret`).
731#[derive(Debug, Clone, PartialEq)]
732pub struct CaretSpec {
733    rect: Rect,
734    color: Color,
735    contents: Option<String>,
736}
737
738impl CaretSpec {
739    /// A caret over `rect`.
740    #[must_use]
741    pub fn new(rect: Rect, color: Color) -> Self {
742        Self {
743            rect,
744            color,
745            contents: None,
746        }
747    }
748}
749
750spec_builder!(CaretSpec, "A caret annotation.");
751
752impl From<CaretSpec> for AnnotSpec {
753    fn from(b: CaretSpec) -> Self {
754        let CaretSpec {
755            rect,
756            color,
757            contents,
758        } = b;
759        Self::Caret {
760            rect,
761            color,
762            contents,
763        }
764    }
765}
766
767/// A free-text annotation (`/Subtype /FreeText`): text drawn on the page
768/// rather than held behind an icon.
769///
770/// The one subtype whose text is *required* — a free-text annotation with no
771/// `/Contents` has nothing to draw — so it is a constructor argument here
772/// rather than the optional `contents` every other builder has, and this
773/// builder declares the shared metadata setters itself instead of taking them
774/// from `spec_builder!`.
775#[derive(Debug, Clone, PartialEq)]
776pub struct FreeTextSpec {
777    rect: Rect,
778    color: Color,
779    contents: String,
780    da: Option<String>,
781    align: Option<pdfrum_doc::vt::Alignment>,
782}
783
784impl FreeTextSpec {
785    /// Free text over `rect`, with [`DEFAULT_DA`](crate::DEFAULT_DA) as the
786    /// default appearance.
787    #[must_use]
788    pub fn new(rect: Rect, color: Color, contents: impl Into<String>) -> Self {
789        Self {
790            rect,
791            color,
792            contents: contents.into(),
793            da: None,
794            align: None,
795        }
796    }
797
798    /// Sets `/DA`, the default-appearance string naming the font and size the
799    /// body is laid out with.
800    ///
801    /// Left unset this is [`DEFAULT_DA`](crate::DEFAULT_DA).
802    #[must_use]
803    pub fn da(mut self, da: impl Into<String>) -> Self {
804        self.da = Some(da.into());
805        self
806    }
807
808    /// Sets `/Q`, the text alignment.
809    ///
810    /// Unset leaves the key out, which a reader takes as flush left.
811    #[must_use]
812    pub fn align(mut self, align: pdfrum_doc::vt::Alignment) -> Self {
813        self.align = Some(align);
814        self
815    }
816
817    /// Sets `/T`, the author.
818    #[must_use]
819    pub fn author(self, author: impl Into<String>) -> AnnotWrite {
820        AnnotSpec::from(self).with_author(author)
821    }
822
823    /// Sets `/NM`, the annotation's unique name.
824    #[must_use]
825    pub fn name(self, name: impl Into<String>) -> AnnotWrite {
826        AnnotSpec::from(self).with_name(name)
827    }
828
829    /// Sets `/M`, the modification date.
830    #[must_use]
831    pub fn modified(self, modified: impl Into<String>) -> AnnotWrite {
832        AnnotSpec::from(self).with_modified(modified)
833    }
834
835    /// Sets `/F`, the annotation flags.
836    #[must_use]
837    pub fn flags(self, flags: pdfrum_doc::AnnotFlags) -> AnnotWrite {
838        AnnotSpec::from(self).with_flags(flags)
839    }
840
841    /// Sets every metadata field at once.
842    #[must_use]
843    pub fn meta(self, meta: AnnotMeta) -> AnnotWrite {
844        AnnotSpec::from(self).with_meta(meta)
845    }
846}
847
848impl From<FreeTextSpec> for AnnotSpec {
849    fn from(b: FreeTextSpec) -> Self {
850        let FreeTextSpec {
851            rect,
852            color,
853            contents,
854            da,
855            align,
856        } = b;
857        Self::FreeText {
858            rect,
859            color,
860            contents,
861            da: da.unwrap_or_else(|| crate::DEFAULT_DA.to_owned()),
862            align,
863        }
864    }
865}
866
867impl From<FreeTextSpec> for AnnotWrite {
868    fn from(builder: FreeTextSpec) -> Self {
869        AnnotSpec::from(builder).into()
870    }
871}