Text Measurement

Pagination needs to know how tall a block of text will be before it’s rendered - which means measuring how it wraps at a given width. This is the single seam, ITextMeasurer, that every height calculation in the layout engine ultimately depends on.

ITextMeasurer

ITextMeasurer is a one-method interface:

public interface ITextMeasurer
{
    TextMeasurement Measure(string text, FontSpecification font, double maxWidthPx);
}
  • text may contain explicit newlines, which must be treated as hard breaks (not wrapped through).
  • font is a FontSpecification - family, size, bold, italic, and line-height multiplier - deliberately decoupled from Model.Styling.TextStyle so an ITextMeasurer implementation (including one shipped in a separate package) doesn’t need to depend on the document model at all.
  • maxWidthPx is the width a single line may occupy.

The return type, TextMeasurement, holds the resulting Lines (each one guaranteed to fit within the measured width), the resolved LineHeightPx, and WidestLineWidthPx. TotalHeightPx (Lines.Count * LineHeightPx) is what the layout engine actually sums for pagination; elements that support splitting (Paragraph, table cells) slice Lines at a line boundary to build the head/tail fragments for IReportElement.Split.

The default: ApproximateTextMeasurer

ApproximateTextMeasurer (exposed as the stateless singleton ApproximateTextMeasurer.Instance, and used automatically unless you override it) estimates wrapping using per-character average-width tables for Helvetica (HelveticaCharacterWidths), scaled by font size, with a flat 1.08x multiplier approximating bold’s extra width. It has zero runtime dependencies - no System.Drawing, no native text-shaping library - which is what keeps the whole core package usable on netstandard2.0 and trimmable/AOT-compatible on modern .NET.

What this buys you, and what it doesn’t:

  • It will not match a browser’s actual layout engine pixel-for-pixel - particularly for proportional fonts other than a generic sans-serif, text with heavy kerning/ligatures, or any font where Helvetica’s metrics are a poor stand-in.
  • It does not hyphenate: a single word wider than the available width is placed alone on its own (overflowing) line rather than broken mid-word.
  • FontFamily/Bold/Italic on a TextStyle still affect the rendered output normally (the CSS faithfully sets font-family, font-weight, font-style) - they just don’t change which width table ApproximateTextMeasurer consults, since it only ever measures against the one Helvetica table.

In practice this means: page breaks chosen with the default measurer are close to where a real browser would wrap the same text, but not guaranteed exact - acceptable for most reports, but worth knowing about if your report’s pagination needs to be pixel-exact (e.g. a legally significant multi-page contract where a line must never silently shift to the next page in print).

Supplying a precise measurer

ReportDocument.Create(PageSize.A4)
    .UseTextMeasurer(myPreciseMeasurer)
    .Content(c => { ... })
    .Build();

ReportDocumentBuilder.UseTextMeasurer(ITextMeasurer measurer) overrides the default for the whole document; it throws ArgumentNullException if you pass null. There’s no per-element override - one document, one measurer, used consistently for every element so pagination is internally coherent.

Implement ITextMeasurer against a real rendering engine - a headless browser (e.g. Playwright/Puppeteer measuring actual DOM layout), System.Drawing/GDI+ on Windows, or any text-shaping library that can report glyph advances for your target font - to get pagination that matches actual rendering. No ready-made precise measurer ships with the core package today - this is a documented extension point, not a plug-in registry. The intended pattern is a separate companion package (e.g. a hypothetical TerraFluent.Html.Reporting.Measurement.Playwright) that depends on the core package and supplies one, keeping the core package itself free of native/runtime dependencies.

A working reference implementation of exactly that pattern lives at samples/TerraFluent.Html.Reporting.Sample.PlaywrightMeasurer - it measures word widths with a real, headless Chromium instance’s Canvas 2D measureText API rather than ApproximateTextMeasurer’s static Helvetica table. Its README spells out exactly what it improves on and what it still doesn’t do (it’s closer to a real browser, not a full DOM layout pass) - read that before assuming it gives you pixel-exact pagination. See Extending the Library for the ITextMeasurer contract sketch this sample implements.

Where to go next

View this page's source on GitHub →