---
title: "Getting started with OneRender"
url: https://recursiveai.org/onerender/docs/getting-started/
description: "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…"
version: "1.0.0"
---

# Getting started with OneRender

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.

```shell
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`:

```csharp
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:

```csharp
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.

```csharp
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:

```csharp
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:

```csharp
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](https://recursiveai.org/onerender/cookbook/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:

```csharp
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](https://recursiveai.org/onerender/docs/display-list/)).

## Next steps

- [HTML to PDF](https://recursiveai.org/onerender/cookbook/html-to-pdf/) and [HTML to PNG](https://recursiveai.org/onerender/cookbook/html-to-png/) cover the options of each renderer.
- [Headers, footers and page breaks](https://recursiveai.org/onerender/cookbook/paged-media/) shows print CSS: page size, running headers, page numbers and forced breaks.
- [Render a Razor component to PDF](https://recursiveai.org/onerender/cookbook/razor-to-pdf/) does it in one call inside ASP.NET Core.
- [Replace a headless browser](https://recursiveai.org/onerender/cookbook/replace-headless-browser/) maps Puppeteer and Playwright calls to OneRender.
- [The overview](https://recursiveai.org/onerender/docs/overview/) lists every package, and [PDF output](https://recursiveai.org/onerender/docs/pdf/) has every rule of the PDF writer.
