The styling cascade
Formatting in Rivoli PDF lives in one place: the Style class. A Style describes
fonts, colors, spacing, borders, and text properties. You can attach a style to a
block or inline, set a document-wide default, or register named styles in a
StyleSheet and reference them by name. Where a style leaves a property unset, that
property is inherited: this is what makes the system feel like CSS.
Style, StyleSheet, and the styling enums live in the Rivoli.Pdf.Styling
namespace.
Everything is nullable
Section titled “Everything is nullable”The key design decision: every property on Style is nullable, and null means
“not set: inherit this.” A style only carries the properties it actually overrides.
using Rivoli.Pdf.Styling;
var baseStyle = new Style { FontFamily = "Helvetica", FontSize = 12 };var derivedStyle = new Style { FontSize = 16 }; // FontFamily is null
var merged = derivedStyle.MergeWith(baseStyle);// merged.FontFamily == "Helvetica" (inherited)// merged.FontSize == 16 (overridden)MergeWith(baseStyle) returns a new style where each unset property falls back to
the base. This is the single primitive the whole cascade is built on.
What a Style carries
Section titled “What a Style carries”var style = new Style() .WithFontFamily("Helvetica") .WithFontSize(14) .Bold() .WithForegroundColor(Color.FromRgb(44, 62, 80)) .WithMargin(10) .WithPadding(5) .WithLineHeight(1.5) .WithTextAlignment(TextAlignment.Justify);The fluent setters mirror the underlying properties, which fall into groups:
- Font:
FontFamily,FontSize(points),FontWeight,FontStyle. - Color:
ForegroundColor(text),BackgroundColor. - Spacing: per-side
Margin*andPadding*, plusSpaceBefore/SpaceAfterfor paragraph spacing andLineHeight. - Borders: per-side
Border*Widthand aBorderColor. - Text:
TextDecoration(flags: underline, line-through, overline),TextAlignment,VerticalAlignment,LetterSpacing.
Two conventions worth remembering:
style.LineHeight = 1.5; // values < 10 are a MULTIPLE of font size (150%)style.LineHeight = 18; // values >= 10 are ABSOLUTE pointsBold() and Italic() are shorthands for FontWeight = FontWeight.Bold and
FontStyle = FontStyle.Italic. Font weight is a full numeric scale:
public enum FontWeight{ Thin = 100, ExtraLight = 200, Light = 300, Normal = 400, Medium = 500, SemiBold = 600, Bold = 700, ExtraBold = 800, Black = 900}Color is a small RGB value type with named constants and factory methods:
Color.Black; Color.White; Color.Red; Color.Blue; // namedColor.FromRgb(52, 152, 219); // bytes 0–255Color.FromRgb(0.2, 0.6, 0.86); // doubles 0.0–1.0Color.FromHex("#3498DB"); // hex stringInheritance with MergeWith and Clone
Section titled “Inheritance with MergeWith and Clone”Because styles compose, you build a hierarchy by merging up a chain. A body style, a heading base derived from it, and concrete heading levels derived from that:
var body = new Style() .WithFontFamily("Times-Roman") .WithFontSize(12) .WithLineHeight(1.6);
var headingBase = new Style() .WithFontFamily("Helvetica") .Bold() .MergeWith(body); // inherits size & line height from body
var h1 = headingBase.Clone().WithFontSize(24).WithParagraphSpacing(20, 12);var h2 = headingBase.Clone().WithFontSize(20).WithParagraphSpacing(16, 10);Named styles: StyleSheet
Section titled “Named styles: StyleSheet”Merging by hand is fine for a few styles, but for a whole document you want a named
registry. StyleSheet maps names to styles and resolves inheritance for you. Each
style can name a ParentStyleName, and ResolveStyle walks that chain to produce
the fully-resolved style:
var sheet = new StyleSheet();
sheet.SetStyle("Normal", new Style{ FontFamily = "Times-Roman", FontSize = 12, ForegroundColor = Color.Black});
sheet.SetStyle("Heading", new Style{ ParentStyleName = "Normal", // inherit from Normal FontFamily = "Helvetica", FontWeight = FontWeight.Bold});
sheet.SetStyle("Heading1", new Style{ ParentStyleName = "Heading", // inherit from Heading (and so Normal) FontSize = 24});
var h1 = sheet.ResolveStyle("Heading1");// FontFamily "Helvetica" (Heading), FontSize 24 (Heading1),// FontWeight Bold (Heading), ForegroundColor Black (Normal)Resolution order is local first, then parent, then grandparent, and so on up the chain. The first style that sets a property wins.
ResolveStyle returns a clone, so mutating the result never touches the
registered style. Use GetStyle(name) to read back the registered (unresolved)
style.
A ready-made stylesheet
Section titled “A ready-made stylesheet”StyleSheet.CreateDefault() gives you a sensible starting point: Normal,
Heading, Heading1–Heading4, Code, and Quote, already wired with
inheritance.
var doc = Document.Create("My Document");doc.StyleSheet = StyleSheet.CreateDefault();Validation and cycles
Section titled “Validation and cycles”A stylesheet can detect problems before you rely on it. Validate() returns false
if any style names a missing parent or if there is a cycle. Resolving a style that is
part of a cycle throws:
sheet.SetStyle("A", new Style { ParentStyleName = "B" });sheet.SetStyle("B", new Style { ParentStyleName = "A" });
if (!sheet.Validate()) throw new InvalidOperationException("StyleSheet has invalid references");Where styles attach
Section titled “Where styles attach”Styles flow through the document at several levels:
Document.DefaultStyle: the document-wide baseline.Document.StyleSheet: the named-style registry for the document.Section.DefaultStyle: a per-section default.Block.Style/Block.StyleName: an explicit style and/or a named style on individual blocks.Inline.Style: overrides on individual runs/spans.
Layout and rendering resolve each node’s effective formatting through the full cascade, closest setting first:
inline (Run/Span) style → block style → named style (Block.StyleName) → section default → document defaultEach level only contributes the properties the levels above it leave unset, so a node’s effective formatting is its own style merged over the defaults that apply to it. Set a small document default and only override what changes locally: the same philosophy as a CSS reset plus targeted rules:
var doc = Document.Create("Annual Report");doc.DefaultStyle = new Style() // document-wide baseline .WithFontSize(11) .WithForegroundColor(Color.FromHex("#222222"));
var appendix = doc.AddSection();appendix.DefaultStyle = new Style() // this section runs smaller .WithFontSize(9);
// FontSize 9 (section default), color #222222 (document default).appendix.Add(new Paragraph("Appendix body text."));
// FontSize 14 (own style wins), color #222222 (still inherited).appendix.Add(new Paragraph("Appendix heading") { Style = new Style { FontSize = 14 } });Named styles join the cascade through Block.StyleName: set it to the name of a
registered style and the renderer resolves it against Document.StyleSheet at
layout time (flattening the ParentStyleName chain). The block’s explicit
Style still wins property-by-property, and the named style overrides the
section/document defaults:
doc.StyleSheet = new StyleSheet{ // collection-initializer syntax; SetStyle(name, style) works too new("Heading1", new Style { FontSize = 24, FontWeight = FontWeight.Bold }),};
appendix.Add(new Paragraph("Named heading") { StyleName = "Heading1" });An unknown StyleName never throws: it emits a diagnostics warning (see
PdfSaveOptions.Diagnostics) and the block falls back to the defaults. Manual
resolution (sheet.ResolveStyle("Heading1")) remains available when you want the
flattened style as an object.
The cascade applies to paragraphs (and their runs), tables (and their cells), lists, and blocks nested inside containers. One boundary to keep in mind: header/footer bands are laid out independently of the section body, so they do not inherit the default styles.
- Lean on inheritance. Set common properties once on a base or
Normalstyle; override only what differs. - Name styles semantically (
Heading1,Code,Quote) rather than by appearance (BigBoldText). - Keep a palette. Define brand colors as
static readonly Colorconstants and reference them, instead of repeatingFromRgb(...).
- The layout engine: how resolved styles affect measurement and arrangement.
- Styling via extension methods: packaging style application as reusable helpers.