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.
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:
<!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:
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:
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:
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}");
first.png: 640 x 360This 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.
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}");
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:
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"); }
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
alttext become the PDF’s structure tree, and the<title>andlangbecome the document title and language. Text is real text with the fonts subsetted, so it can be selected, searched and extracted. - Page setup.
PdfPageSetup.LetterandPdfPageSetup.A4have half-inch margins;new PdfPageSetup(width, height, top, right, bottom, left)takes CSS pixels (96 per inch). A@pagerule in the document overrides it. - Bytes instead of a stream.
HtmlPdf.ToPdf(html, options)returns abyte[].
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:
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}");
MissingResource: an image was not supplied by the hostThe 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:
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());
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, soQuerySelectortakes any CSS selector. page.Layouthas the CSSOM View queries: bounding and client rectangles, offset and scroll sizes.ElementFromPointis 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
- HTML to PDF and HTML to PNG cover the options of each renderer.
- Headers, footers and page breaks shows print CSS: page size, running headers, page numbers and forced breaks.
- Render a Razor component to PDF does it in one call inside ASP.NET Core.
- Replace a headless browser maps Puppeteer and Playwright calls to OneRender.
- The overview lists every package, and PDF output has every rule of the PDF writer.