Rendering

Rendering is the second pass: it turns an already-paginated LayoutResult (see Pagination and Layout) into HTML. By the time a renderer runs, every page’s geometry - and therefore the total page count - is already known, which is what lets header/footer elements resolve {page}/{totalPages} tokens correctly.

The APIs on ReportDocument

You usually don’t call the layout/render stages separately - ReportDocument exposes five convenience methods that do both:

Method Output Use when…
RenderHtml(renderer?, ct?) string (full document) You want the complete HTML in memory, e.g. to return from a web endpoint.
RenderHtmlDocument(filePath, renderer?, ct?) writes to a file You want to stream straight to disk without holding the full string in memory.
RenderHtmlDocumentAsync(filePath, renderer?, ct?) Task, writes to a file Same as above, but doesn’t block a thread-pool thread on the final flush/dispose - see below.
RenderFragment(renderer?, ct?) string (page <div>s only) You’re embedding the report inside an existing HTML page.

All five accept an optional IHtmlReportRenderer (defaulting to HtmlReportRenderer.Default) and a CancellationToken, checked periodically during both pagination and rendering so a very large document can be aborted - e.g. when the originating web request is canceled.

string html = report.RenderHtml();
await report.RenderHtmlDocumentAsync("report.html", cancellationToken: ct);
string fragment = report.RenderFragment(); // for embedding

Why is RenderHtmlDocumentAsync async at all if pagination/rendering are synchronous CPU work? Because there’s no I/O to await until the final flush to disk - the method exists so the flush, and the writer’s disposal, do not block a thread-pool thread in an async call chain (e.g. inside an ASP.NET request handler), not because layout or HTML generation themselves are asynchronous.

IHtmlReportRenderer

IHtmlReportRenderer is the renderer contract:

public interface IHtmlReportRenderer
{
    string RenderDocument(LayoutResult layout);
    string RenderFragment(LayoutResult layout);
    void RenderDocumentTo(TextWriter writer, LayoutResult layout, CancellationToken cancellationToken = default);
    void RenderFragmentTo(TextWriter writer, LayoutResult layout, CancellationToken cancellationToken = default);
}

HtmlReportRenderer (accessible as the shared, stateless HtmlReportRenderer.Default) is the only implementation shipped today. The ...To(TextWriter, ...) overloads write one page at a time rather than building the whole document as a single in-memory string first - for a report with many thousands of pages, this bounds peak memory to roughly one page’s HTML rather than the entire document’s. The non-streaming RenderDocument/RenderFragment overloads are implemented as a thin wrapper: write to a StringWriter, then return its contents.

See Extending the Library: Custom renderer if you want to implement this interface yourself (e.g. to emit a different page-wrapper structure, or PDF-specific markup).

What the HTML looks like

RenderDocumentTo produces a single self-contained document - inline <style>, no external CSS or JS, no external image references (every ReportImage is base64-encoded inline). The example below is for PageSize.A4 (210mm x 297mm, converted to pixels at 96px/inch):

<!DOCTYPE html><html><head><meta charset="utf-8" /><title>Report</title>
<style>
  @page { size: 793.701px 1122.52px; margin: 0; }
  html, body { margin: 0; padding: 0; }
  body { background: #e8e8e8; font-family: Segoe UI, Arial, sans-serif; }
  .fhr-page { position: relative; width: 793.7px; height: 1122.52px; background: #ffffff;
    overflow: hidden; margin: 0 auto 16px auto; box-shadow: 0 0 6px rgba(0,0,0,0.25);
    page-break-after: always; break-after: page; }
  .fhr-page:last-child { page-break-after: auto; break-after: auto; margin-bottom: 0; }
  @media print { body { background: none; } .fhr-page { margin: 0; box-shadow: none; } }
</style>
</head><body>
  <div class="fhr-page"> ... one absolutely-positioned element per placed fragment ... </div>
  <div class="fhr-page"> ... </div>
</body></html>

Key points:

  • @page { size: ...; margin: 0; } sets the print page size to exactly match the document’s PageSize. Page margins are not expressed as @page margin - they’re baked into each element’s absolute left/top position instead, so what you see on screen (a white page with a drop shadow) is pixel-identical to what prints.
  • One .fhr-page <div> per page, sized exactly to PageSize, with overflow:hidden so any force-placed, overflowing content (see LayoutWarning) is clipped visually rather than spilling into the next page’s div.
  • page-break-after: always (plus the modern break-after: page) on every page except the last is a redundant signal for browsers that don’t fully honor @page sizing during print - belt-and-suspenders, since the exact-pixel page divs are normally enough on their own.
  • Every element renders as its own absolutely-positioned tag (<p>, <h1>-<h6>, <img>, <table>, <ul>/<ol>, or a styled <div>) at the left/top/width/height the layout engine computed - the renderer has no pagination logic of its own; it only translates each ElementPlacement from section-relative to page-absolute coordinates (see Pagination and Layout: The output) and calls IReportElement.RenderHtml.
  • All CSS pixel values use the invariant culture (123.45px, never 123,45px) - see CssFormat - so generated reports are correct regardless of the server’s locale.
  • All user-supplied text is HTML-encoded before being written (again via CssFormat.Encode, i.e. WebUtility.HtmlEncode) - the one exception is RawHtml, which is emitted verbatim by design (see Content Elements: Raw HTML).
  • <title> defaults to Report, or the document’s own title (HTML-encoded) when ReportDocumentBuilder.Title(...) was called - a custom IHtmlReportRenderer can read it via LayoutResult.Title.

RenderFragmentTo emits the page styles and page <div>s without the <html>/<head>/<body> wrapper. Its stylesheet omits the document-level html/body reset and background rules, so embedding a report does not change the host page’s margins, background, or font.

Printing to PDF

Because the HTML is built around exact-pixel @page sizing and one <div> per page, a browser’s native “Print to PDF” (or a headless-browser print API, e.g. Playwright/Puppeteer) reproduces the same page breaks you see when viewing the HTML directly - there’s no separate PDF-specific code path in this library. If you need exact pixel-perfect text wrapping in that PDF, see Text Measurement for why the bundled measurer alone may not guarantee that.

Tested against real browsers

The pagination and rendering logic isn’t only checked with plain unit tests - CI also runs PrintLayoutBrowserTests against real, headless Chromium, Firefox, and WebKit via Playwright, rendering a generated report and asserting - under @media print, the same media browsers use for “Print to PDF” - that every page’s getBoundingClientRect() matches the requested PageSize exactly and that no element’s box overflows its page’s bounds. See .github/workflows/ci.yml for the CI job that installs and runs all three engines on every push and pull request. This is in addition to, not instead of, the unit tests that exercise the pagination math itself (see Pagination and Layout) - the browser tests specifically guard the rendered HTML/CSS against real browsers’ print-layout behavior, which a markup-only assertion cannot.

Where to go next

View this page's source on GitHub →