Skip to content

2. Styling & typography

Fonts, colors, spacing, and the styling cascade.

In Rivoli PDF, appearance lives in a single type: Style. You attach a Style to a run of text, a paragraph, or the whole document, and the layout engine resolves the effective look from those layers. This chapter walks through the fluent Style API, then shows where to attach styles for per-run, per-paragraph, and document-wide effects.

Style, Color, and the styling enums live in Rivoli.Pdf.Model and Rivoli.Pdf.Styling:

using Rivoli.Pdf.Model;
using Rivoli.Pdf.Styling; // TextAlignment, TextDecoration, FontWeight, FontStyle

A Style is a mutable bag of formatting properties with a fluent configuration API. Every With… method sets one property and returns the same Style, so they chain:

var heading = new Style()
.WithFontFamily("Helvetica")
.WithFontSize(18)
.Bold()
.WithForegroundColor(Color.FromHex("#1a1a2e"))
.WithTextAlignment(TextAlignment.Center)
.WithParagraphSpacing(before: 0, after: 12);

The methods you’ll reach for most:

MethodSets
WithFontFamily(string)Font family, e.g. "Helvetica", "Times-Roman", "Courier"
WithFontSize(double)Size in points
Bold() / Italic()Shortcuts for FontWeight.Bold / FontStyle.Italic
WithForegroundColor(Color)Text color
WithBackgroundColor(Color)Fill behind the text or block
WithTextAlignment(TextAlignment)Left, Center, Right, Justify
WithTextDecoration(TextDecoration)Underline, LineThrough, Overline (flags)
WithLineHeight(double)Line height (multiplier if < 10, absolute points if >= 10)
WithLetterSpacing(double)Tracking in points; negative tightens
WithMargin(double) / WithMargin(h, v)Space outside the block
WithPadding(double)Space inside the block, between border and content
WithParagraphSpacing(before, after)Space above and below a paragraph
WithBorder(double width, Color)A uniform border on all four sides

For finer control you can also set the underlying properties directly ( FontWeight, FontStyle, MarginLeft, PaddingTop, BorderColor, and so on), since Style is a plain object.

Color is an RGB value. There are named statics for the common ones, plus three factory methods:

Color.Black; Color.White; Color.Red; Color.Green; Color.Blue;
Color.Yellow; Color.Cyan; Color.Magenta; Color.Gray;
Color.FromRgb(255, 128, 0); // bytes, 0–255
Color.FromRgb(1.0, 0.5, 0.0); // doubles, 0.0–1.0
Color.FromHex("#FF8000"); // "#RRGGBB" or "RRGGBB"

Use them anywhere a color is expected:

var warning = new Style()
.WithForegroundColor(Color.FromHex("#b00020"))
.WithBackgroundColor(Color.FromRgb(255, 240, 240))
.Bold();

TextDecoration is a [Flags] enum, so you can combine decorations:

var struckLink = new Style()
.WithTextDecoration(TextDecoration.Underline | TextDecoration.LineThrough);

The same Style type attaches at three levels. Pick the level that matches the scope you want to affect.

A Run is a span of text inside a paragraph. Give a single run its own style to format just those words, leaving the rest of the line alone:

var emphasis = new Style().Bold().WithForegroundColor(Color.Red);
var para = new Paragraph();
para.Inlines.Add(new Run("Status: "));
para.Inlines.Add(new Run("OVERDUE") { Style = emphasis });
section.Add(para);

Only the second run is bold and red; the label stays plain. This is the level for inline emphasis, highlighted terms, and mixed formatting within a sentence.

Attach a style to a Paragraph (or any Block) to format the entire block: alignment, block-level spacing, borders, and the default font for the text inside it. The AddParagraph overload that takes an Action<Style> is the concise way to do this:

using Rivoli.Pdf.Builders;
section.AddParagraph("A centered, spaced lead paragraph.", style => style
.WithFontSize(13)
.WithTextAlignment(TextAlignment.Center)
.WithParagraphSpacing(before: 0, after: 18));

Some properties only make sense at the block level: TextAlignment, WithParagraphSpacing, WithBorder, and WithPadding describe the box, not individual characters. Set those on the paragraph, not on a run.

Document.DefaultStyle is the base every node falls back to. Set the body font and size once, and every paragraph inherits it unless it overrides:

var doc = Document.Create("Styled Report");
doc.DefaultStyle = new Style()
.WithFontFamily("Helvetica")
.WithFontSize(11)
.WithLineHeight(1.4)
.WithForegroundColor(Color.FromHex("#222222"));

With DocumentBuilder, the same thing reads as one expression:

using Rivoli.Pdf.Builders;
var doc = DocumentBuilder.Create("Styled Report")
.WithDefaultStyle(s => s.WithFontFamily("Helvetica").WithFontSize(11))
.Build();

When a node needs a property (say the font size of a word in a run), the layout engine looks up the tree until it finds a value: the run’s own style first, then the paragraph’s, then the document default, then the built-in fallback. The closest setting wins, and unset properties fall through.

This is why the three levels compose so cleanly: set the body font on the document, alignment and spacing on the paragraph, and only the truly local emphasis on the run. For the precise resolution rules (including how the stylesheet and inheritance interact). Read The styling cascade.

using Rivoli.Pdf;
using Rivoli.Pdf.Builders;
using Rivoli.Pdf.Model;
using Rivoli.Pdf.Styling;
var doc = Document.Create("Styled Report");
doc.DefaultPageSize = PageSize.A4;
doc.DefaultMargin = Margin.Standard;
doc.DefaultStyle = new Style()
.WithFontFamily("Helvetica")
.WithFontSize(11)
.WithLineHeight(1.4);
var section = doc.AddSection();
section.AddParagraph("Styled Report", style => style
.WithFontSize(22)
.Bold()
.WithTextAlignment(TextAlignment.Center)
.WithForegroundColor(Color.FromHex("#1a1a2e")));
var note = new Paragraph();
note.Inlines.Add(new Run("Note: "));
note.Inlines.Add(new Run("figures are provisional.")
{ Style = new Style().Italic().WithForegroundColor(Color.Gray) });
section.Add(note);
doc.SavePdf("styled-report.pdf");