Signature Appearance
Everything the visible signature can show — render modes, graphics, layout, colours, fonts, Unicode shaping, borders, fields, labels and templates
The Appearance struct controls what a visible signature draws inside its
placement box: the render mode, any graphics, the layout, colours, fonts,
and the viewer's validity icon. It is built fluently, and its defaults reproduce the classic iText-style
text block, so you set only what you want to change.
use mudrit_pdfsign::prelude::*;
let ap = Appearance::new()
.mode(RenderMode::GraphicAndText)
.layout(Layout::GraphicLeft)
.graphic(Image::Png(std::fs::read("logo.png")?));
let cfg = SignConfig::builder()
.place("F", [350, 60, 560, 160])?
.appearance(ap) // attach the whole Appearance
.build();Attach an appearance with .appearance(Appearance). The builder also exposes .trust_icon(bool) as a
shortcut for the single most common tweak. The default appearance is DescriptionOnly with the
validity icon on — so a bare SignConfig::builder() already gives you the familiar signer text block.
Render modes
RenderMode decides which of the text block and the graphics are drawn, and how.
The default. Only the signer text block — "Digitally Signed by: / <name> / <org> / Date: …". No graphic needed.
Appearance::new() // mode defaults to RenderMode::DescriptionOnlyThe signer's name large and bold on one side (filling ~half the box), the other details (org /
date / reason / …) on the other side — iText's "name and description" stamp. The name takes the
Layout graphic position (default left) and
graphic_fraction sizes its region. The detail side defaults to the
other fields (the name is not repeated) unless you set fields or
template. With a std-14 font the name uses the family's bold
weight; with an embedded font it uses that font, just larger.
Appearance::new().mode(RenderMode::NameAndText)Graphic(s) and the text block side by side, positioned per Layout.
Needs at least one graphic.
Appearance::new()
.mode(RenderMode::GraphicAndText)
.layout(Layout::GraphicLeft)
.graphic(Image::Png(logo));Only the graphics (a logo or handwritten-signature image), no text. Needs at least one graphic.
Appearance::new().mode(RenderMode::GraphicOnly).graphic(Image::Png(sig));A single graphic drawn faded behind the text block. Uses only the first graphic, and its
opacity is set by watermark_opacity. Flat-only — it cannot combine with
trust_icon (see validate rules).
Appearance::new()
.mode(RenderMode::Watermark)
.trust_icon(false) // required: watermark is flat-only
.watermark_opacity(0.15)
.graphic(Image::Png(seal));No visible appearance at all — the document is signed but nothing is drawn (zero-size widget, no /AP,
no validity icon). Prefer the Appearance::invisible() constructor.
Appearance::invisible()The trust icon (Acro6 layered validity icon)
trust_icon (default true) builds the iText-style Acro6 layered appearance, so the viewer
overlays its own dynamic validity icon on top of your content — a green check when the certificate is
trusted, a "?" otherwise. Set it false for a flat, static appearance that never changes.
Appearance::new().trust_icon(false); // flat appearance, no viewer overlay
// or, as a builder shortcut:
SignConfig::builder().trust_icon(true) /* … */;The green check depends on the certificate being trusted by the viewer (Adobe AATL, the Windows
store, a CCA root, …). The bundled test cert shows "validity unknown" with a "?" — that is expected.
Mudrit builds the appearance; the viewer decides the icon. trust_icon is illegal with
RenderMode::Invisible (nothing to overlay) and RenderMode::Watermark (flat-only).
Graphics
Up to four images can be drawn (logo, handwritten signature, seal, …); they are auto-arranged —
a wide region becomes a row, a tall region becomes a column. RenderMode::Watermark uses only the
first. An Image is PNG or JPEG bytes, or a path read at sign time:
Image::Png(bytes) // decoded to RGB/Gray (+ alpha /SMask), Flate-compressed
Image::Jpeg(bytes) // embedded as-is via /DCTDecode (no re-encode): RGB, gray, or CMYK
Image::Path("logo.png".into()) // format auto-detected from the file's magic bytes, read at sign timeAdd them with the builder methods:
| Method | Effect |
|---|---|
.graphic(Image) | Append one graphic (up to 4 are drawn). |
.graphic_path(path) | Append a graphic loaded from a path at sign time (PNG/JPEG auto-detected). |
.graphics(iter) | Replace the graphics with an iterator of Image. |
Appearance::new()
.mode(RenderMode::GraphicAndText)
.graphic(Image::Png(logo))
.graphic_path("handwritten.png"); // a missing/invalid file fails loudly during sign_pdf, not hereImage::Path and .graphic_path (and .font_path) defer file reads to sign_pdf, so a bad path
surfaces there rather than at build time. More than 4 graphics is rejected by
validate().
Layout and graphic fraction
For RenderMode::GraphicAndText (and the name region of NameAndText), Layout places the graphic
relative to the text, and graphic_fraction sets how much of the box the graphic region gets.
Layout | Graphic position |
|---|---|
GraphicLeft (default) | graphic left, text right — the common enterprise layout |
GraphicRight | graphic right, text left |
GraphicTop | graphic on top, text below |
GraphicBottom | graphic on the bottom, text above |
Appearance::new()
.mode(RenderMode::GraphicAndText)
.layout(Layout::GraphicRight)
.graphic_fraction(0.35) // graphic region = 35% of the box; must be 0.1..=0.9 (default 0.4)
.graphic(Image::Png(logo));Watermark opacity
watermark_opacity (default 0.2, range 0.0..=1.0) sets how faded the background graphic is in
RenderMode::Watermark. Lower is subtler.
Appearance::new()
.mode(RenderMode::Watermark)
.trust_icon(false)
.watermark_opacity(0.12)
.graphic(Image::Png(seal));Colours
| Option | Effect | Default |
|---|---|---|
.text_color([r, g, b]) | Text colour, RGB in 0.0..=1.0 | black [0, 0, 0] |
.background([r, g, b]) | Optional background fill, RGB in 0.0..=1.0 | None (transparent) |
Appearance::new()
.text_color([0.10, 0.20, 0.45]) // deep blue text
.background([0.96, 0.97, 1.0]); // pale panel behind itFont size and line spacing
| Option | Effect | Default |
|---|---|---|
.font_size(points) | Explicit font size, disabling auto-fit | None → 9.94pt with auto-shrink to fit |
.line_spacing(factor) | Leading, as a multiple of the font size (must be > 0) | 1.2 |
Leaving font_size unset lets Mudrit auto-shrink the text to fit the box; setting it pins the size.
line_spacing 1.0 packs lines tight (baselines one em apart); 1.2 leaves a little air. Both apply
to the std-14 and embedded-font paths.
Appearance::new()
.font_size(8.0) // pin the size (no auto-shrink)
.line_spacing(1.15);Fonts — std-14 vs. embedded Unicode
By default the text is drawn with a PDF standard-14 font: no font program is embedded, it renders
in every viewer, but it covers only WinAnsi (Latin-1). Choose the family with .std_font(Std14):
Appearance::new().std_font(Std14::TimesBold);The Std14 families are the three Adobe base faces in their four styles: Helvetica (default) /
HelveticaBold / HelveticaOblique / HelveticaBoldOblique; Times / TimesBold / TimesItalic /
TimesBoldItalic; Courier / CourierBold / CourierOblique / CourierBoldOblique.
For Unicode text — Devanagari, CJK, Arabic, and other complex scripts — embed a TrueType/OpenType
font. Mudrit renders it as a Type0 / Identity-H font with full HarfBuzz shaping (conjuncts,
joining forms, marks). An embedded font overrides std_font.
Appearance::new().font(std::fs::read("NotoSansDevanagari.ttf")?); // bytes in memory
Appearance::new().font_path("fonts/NotoSansDevanagari.ttf"); // read at sign timeComplex scripts require an embedded font
The std-14 fallback (WinAnsi, no GSUB/GPOS) cannot map or shape Hebrew, Arabic, the Indic block
(Devanagari … Malayalam, Sinhala), Thai/Lao, Tibetan, Myanmar, or Khmer. When such characters are
actually present, an embedded font is mandatory — otherwise the sign fails loudly. (A plain-Latin
string in any script direction is fine on std-14.)
Text direction (bidi / RTL)
Direction sets the base paragraph direction (Ltr default). Direction::Rtl (or the .rtl()
shorthand) bidi-reorders the text right-to-left and right-aligns it, per the Unicode bidi algorithm —
inherently RTL runs (Arabic, Hebrew) flow RTL, while embedded LTR runs (Latin, digits, Devanagari) keep
their natural order. Pair RTL with an embedded font so the non-Latin glyphs are shaped.
Appearance::new()
.font(std::fs::read("NotoNaskhArabic.ttf")?)
.rtl(); // == .direction(Direction::Rtl)Word wrapping
wrap (default true) wraps the signer text to the box width — long values break onto multiple
lines (word-wrap; an over-wide token character-wraps). Set false for the classic one-line-per-field
auto-shrink behaviour.
Appearance::new().wrap(false); // one line per field, auto-shrunk to fitBorder
Stroke a Border around the signature box (drawn inside the signed appearance, so it renders in
every viewer). Build it solid or dashed; the width must be > 0.
Appearance::new().border(Border::solid([0.0, 0.0, 0.0], 1.0)); // 1pt black solid
Appearance::new().border(Border::dashed([0.2, 0.2, 0.2], 0.75)); // 0.75pt grey dashed| Constructor | Border |
|---|---|
Border::solid([r, g, b], width) | Solid stroke, RGB 0.0..=1.0, width in points |
Border::dashed([r, g, b], width) | Dashed stroke, same parameters |
Fields, labels, and templates
Three layers control which text appears and in what order, from most structured to most free-form:
fields — pick and order the signer fields
SigField chooses which fields show and in what order; empty values are skipped (e.g. an absent
organisation). None (the default) draws the classic block; Some(list) drives it explicitly.
Appearance::new().fields([
SigField::Name, SigField::Org, SigField::Reason, SigField::Location, SigField::Date,
]);The variants are Name, Org, Date, Reason, Location, Contact. Name uses the certificate
CN (or the .name(...) override); the others come from the signing metadata.
labels — localize the field prefixes
Labels sets the prefix for each field, so the block translates. Override any; the rest stay English.
Appearance::new()
.fields([SigField::Name, SigField::Date])
.labels(Labels::default()
.signed_by("Hastakshar:") // was "Digitally Signed by:"
.date("Dinank:")); // was "Date:"Defaults: signed_by = "Digitally Signed by:", org = "Org:", date = "Date:",
reason = "Reason:", location = "Location:", contact = "Contact:". Set org to "" for the
bare organization value with no prefix.
template — a free-form layout
template overrides fields entirely: newlines split lines, and the placeholders {name}, {org},
{date}, {reason}, {location}, {contact} interpolate.
Appearance::new().template("Signed by {name}\n{org}\n{date} · {location}");Precedence: template (if set) wins over fields, which wins over the default block. A template that
uses a complex/RTL script is validated at build time — it errors unless you also set an embedded font.
Invisible signatures
For a legally-signed document with no visible mark, use the Appearance::invisible() constructor —
it sets RenderMode::Invisible and turns the trust icon off for you.
let cfg = SignConfig::builder()
.appearance(Appearance::invisible())
.build();An invisible signature still carries the full CMS, timestamp, LTV, etc. — only the on-page widget is omitted. Placements are irrelevant to it.
Validation
Appearance::validate() rejects impossible combinations early (they are re-checked at render time too),
returning Error::InvalidInput:
| Rule | Requirement |
|---|---|
| Graphics count | at most 4 graphics |
graphic_fraction | within 0.1..=0.9 |
watermark_opacity | within 0.0..=1.0 |
font_size | a positive, finite number of points (when set) |
line_spacing | a positive, finite multiple |
border.width | positive and finite |
template on std-14 | no complex/RTL script chars unless an embedded font is set |
Invisible mode | trust_icon must be off and no graphics |
Watermark mode | trust_icon must be off and exactly one (the first) graphic present |
GraphicOnly / GraphicAndText | at least one graphic |
Full option reference
Prop
Type