Recursive AI

Getting started with OneRender

All onerender docs

Install OneRender for .NET 10, render an HTML invoice to a PNG and to a tagged PDF without a browser, supply its style sheet and images from your own code, read the diagnostics, and query the layout.

Install

OneRender targets net10.0, so you need the .NET 10 SDK. Every package shares one version; this guide uses 1.0.0.

Terminal
dotnet new console -o OneRenderStart
cd OneRenderStart
dotnet add package RecursiveAI.OneRender.Pdf --version 1.0.0

RecursiveAI.OneRender.Pdf brings in everything this guide needs: the engine (RecursiveAI.OneRender), the rasterizer (RecursiveAI.OneRender.Image) and the default fonts (RecursiveAI.OneRender.Fonts). If you only want images, install RecursiveAI.OneRender.Image instead. The libraries are NativeAOT-, trim- and WebAssembly-clean and contain no unsafe code.

OneRender has no JavaScript, no networking and no clock. It never fetches anything: your code hands it the HTML, and the style sheets, images and fonts the HTML refers to.

The sample page

The steps below render site/invoice.html, a one-page invoice that links a style sheet and a logo:

HTML
<!DOCTYPE html>
<html lang="en">
<head>
<title>Invoice INV-1042</title>
<link rel="stylesheet" href="invoice.css">
</head>
<body>
<header>
  <img src="logo.png" width="48" height="48" alt="Harbor Street Books">
  <div><h1>Invoice INV-1042</h1><p class="to">Billed to Harbor Street Books, October 14, 2026</p></div>
</header>
<table>
  <thead><tr><th>Item</th><th>Qty</th><th>Amount</th></tr></thead>
  <tbody>
    <tr><td>Shelf labels</td><td>400</td><td>$120.00</td></tr>
    <tr><td>Reading lamps</td><td>6</td><td>$354.00</td></tr>
  </tbody>
  <tfoot><tr><td colspan="2">Total</td><td>$474.00</td></tr></tfoot>
</table>
</body>
</html>

site/invoice.css sits next to it, along with site/logo.png:

CSS
body { font: 15px/1.5 Arial, sans-serif; color: #222; margin: 32px }
header { display: flex; align-items: center; gap: 16px; margin: 0 0 20px }
h1 { font-family: Georgia, serif; font-weight: normal; margin: 0 }
.to { color: #555; margin: 0 }
table { width: 100%; border-collapse: collapse }
th, td { text-align: left; padding: 6px 8px; border-bottom: 1px solid #ddd }
td:last-child, th:last-child { text-align: right }
tfoot td { font-weight: bold; border-bottom: 0 }

The snippets are one program, in order. Put these using directives at the top of Program.cs:

C#
using OneRender;
using OneRender.Image;
using OneRender.Interaction;
using OneRender.Layout;
using OneRender.Painting;
using OneRender.Pdf;
using RecursiveAI.OneHtml;

Render HTML to a PNG

HtmlRender.ToImage lays the page out in a viewport of the size you give, in CSS pixels, and paints it:

C#
string html = File.ReadAllText("site/invoice.html");
 
RgbaImage first = HtmlRender.ToImage(html, new HtmlRenderOptions { Width = 640, Height = 360 });
File.WriteAllBytes("first.png", first.ToPng());
Console.WriteLine($"first.png: {first.Width} x {first.Height}");
Text
first.png: 640 x 360

This first image has no styling and no logo: nothing supplied invoice.css or logo.png, and the engine won’t go looking for them. The text is set in the bundled fonts. Arial, Helvetica, Times New Roman, Courier New, Calibri and Cambria map to metric-compatible Liberation, Carlito and Caladea faces, and a family it doesn’t have, like Georgia here, falls back to the generic family you list after it. The output is the same on every machine.

Supply style sheets and images

Give the page an address with BaseUrl and a Resources callback. The engine resolves each reference against the base address and asks the callback for the bytes. Return null for anything you don’t have.

C#
const string baseUrl = "https://billing.example/invoices/";
byte[]? Resources(string address)
{
    string name = address.StartsWith(baseUrl) ? address[baseUrl.Length..] : "";
    string path = Path.Combine("site", name);
    Console.WriteLine($"  asked for {address}");
    return name.Length > 0 && File.Exists(path) ? File.ReadAllBytes(path) : null;
}
 
RgbaImage image = HtmlRender.ToImage(html, new HtmlRenderOptions
{
    Width = 640,
    Height = 360,
    DevicePixelRatio = 2,
    BaseUrl = baseUrl,
    Resources = Resources,
});
File.WriteAllBytes("invoice.png", image.ToPng());
Console.WriteLine($"invoice.png: {image.Width} x {image.Height}");
Text
  asked for https://billing.example/invoices/invoice.css
  asked for https://billing.example/invoices/logo.png
invoice.png: 1280 x 720

The address is only a name: nothing is fetched from billing.example. Your callback decides what each address means, so it can read files, look in a database or serve embedded resources, and it never exposes anything you didn’t mean to hand over. DevicePixelRatio = 2 renders the same 640 by 360 layout at twice the pixel density. The same callback serves fonts named in @font-face rules.

Render HTML to a PDF

HtmlPdf.ToPdf lays the page out on pages and writes a tagged PDF, page by page, to a stream:

C#
using (FileStream file = File.Create("invoice.pdf"))
{
    HtmlPdfResult result = HtmlPdf.ToPdf(html, file, new HtmlPdfOptions
    {
        Page = PdfPageSetup.Letter,
        BaseUrl = baseUrl,
        Resources = Resources,
    });
    Console.WriteLine($"invoice.pdf: {result.PageCount} page, {result.Diagnostics.Count} diagnostics");
}
Text
  asked for https://billing.example/invoices/invoice.css
  asked for https://billing.example/invoices/logo.png
invoice.pdf: 1 page, 0 diagnostics
  • Tagged by default. The headings, paragraphs, table, header cells and the logo’s alt text become the PDF’s structure tree, and the <title> and lang become the document title and language. Text is real text with the fonts subsetted, so it can be selected, searched and extracted.
  • Page setup. PdfPageSetup.Letter and PdfPageSetup.A4 have half-inch margins; new PdfPageSetup(width, height, top, right, bottom, left) takes CSS pixels (96 per inch). A @page rule in the document overrides it.
  • Bytes instead of a stream. HtmlPdf.ToPdf(html, options) returns a byte[].

Read the diagnostics

Nothing in a document makes the renderers throw. Missing resources, unsupported features and limits become diagnostics. Here the callback supplies nothing at all:

C#
HtmlPdfResult missing = HtmlPdf.ToPdf(html, Stream.Null, new HtmlPdfOptions { BaseUrl = baseUrl, Resources = _ => null });
foreach (Diagnostic d in missing.Diagnostics)
    Console.WriteLine($"{d.Kind}: {d.Message}");
Text
MissingResource: an image was not supplied by the host

The missing logo is reported. A style sheet your callback doesn’t supply is left out without a diagnostic, the way a browser skips a sheet that failed to load, so log what your callback misses if you need to know. Characters that no font covers are recorded on the font provider instead; Fonts shows how to read them and which package to add.

Query the layout

Under the one-call renderers are public types you can use yourself. InteractivePage lays a page out once and answers the questions a browser’s DOM would, synchronously:

C#
var options = new HtmlPageOptions(FontProviders.CreateDefault())
{
    ViewportWidth = 640,
    ViewportHeight = 360,
    BaseUrl = baseUrl,
    StylesheetBytes = Resources,
};
InteractivePage page = InteractivePage.Create(html, options);
Node total = page.Page.Document.Root.QuerySelector("tfoot td:last-child");
Rect box = page.Layout.GetBoundingClientRect(total);
Console.WriteLine($"total cell: x {box.X:F1}, y {box.Y:F1}, {box.Width:F1} x {box.Height:F1}");
Console.WriteLine($"at (100, 150): <{page.ElementFromPoint(100, 150).LocalName}>");
 
var list = new DisplayList();
page.Page.Paint(list);
Console.WriteLine($"display list: {list.Count} items");
RgbaImage again = DisplayListRasterizer.Render(list, 640, 360);
File.WriteAllBytes("low-level.png", again.ToPng());
Text
  asked for https://billing.example/invoices/invoice.css
total cell: x 425.8, y 225.0, 182.2 x 35.5
at (100, 150): <th>
display list: 21 items
  • The document is a OneHtml Document, so QuerySelector takes any CSS selector.
  • page.Layout has the CSSOM View queries: bounding and client rectangles, offset and scroll sizes. ElementFromPoint is a hit test.
  • Painting writes a display list: backgrounds, borders, glyph runs and images with stable ids. The rasterizer turns it into pixels, the PDF writer into pages, and a game engine can draw it with its own GPU code (The display list).

Next steps