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);
}
textmay contain explicit newlines, which must be treated as hard breaks (not wrapped through).fontis aFontSpecification- family, size, bold, italic, and line-height multiplier - deliberately decoupled fromModel.Styling.TextStyleso anITextMeasurerimplementation (including one shipped in a separate package) doesn’t need to depend on the document model at all.maxWidthPxis 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/Italicon aTextStylestill affect the rendered output normally (the CSS faithfully setsfont-family,font-weight,font-style) - they just don’t change which width tableApproximateTextMeasurerconsults, 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
- Pagination and Layout for how
Measureresults feed into page-break decisions. - Extending the Library for a concrete starting point if you’re writing a custom measurer.
- FAQ: Why didn’t my page break where I expected?