Recursive AI

Replace a headless browser for rendering

All onerender recipes

Move HTML-to-PDF and screenshot jobs off Puppeteer, Playwright or wkhtmltopdf onto OneRender: no Chromium download, no browser process, and the same output on every machine. Here is each common call and what replaces it.

C#
using System.Text;
using OneRender;
using OneRender.Image;
using OneRender.Interaction;
using OneRender.Pdf;
 
// What a headless browser would load from the network, the host supplies.
var files = new Dictionary<string, byte[]>
{
    ["https://app.example/css/report.css"] = Encoding.UTF8.GetBytes("""
        body { font: 15px/1.5 sans-serif; margin: 24px; color: #222 }
        .card { border: 1px solid #ddd; border-radius: 8px; padding: 12px 16px; margin: 0 0 12px }
        @media print { .no-print { display: none } }
        @page { size: A4; margin: 20mm; @bottom-center { content: "Page " counter(page) " of " counter(pages); font: 9pt sans-serif } }
        """),
};
byte[]? Resources(string url) => files.GetValueOrDefault(url);
 
var body = new StringBuilder();
for (int i = 1; i <= 12; i++)
    body.Append($"<div class=card><h2>Week {i}</h2><p>{i * 37} orders shipped.</p></div>");
string html = $"""
    <!DOCTYPE html>
    <html lang="en"><head><title>Shipping report</title><link rel="stylesheet" href="/css/report.css"></head>
    <body><button class="no-print">Download</button>{body}</body></html>
    """;
const string baseUrl = "https://app.example/reports/weekly";
 
// page.pdf({ format: 'A4', printBackground: true, displayHeaderFooter: true, ... })
HtmlPdfResult pdf = HtmlPdf.ToPdf(html, File.Create("report.pdf"), new HtmlPdfOptions { BaseUrl = baseUrl, Resources = Resources });
Console.WriteLine($"report.pdf: {pdf.PageCount} pages");
 
// page.setViewport({ width: 800, height: 600, deviceScaleFactor: 2 }); page.screenshot()
RgbaImage shot = HtmlRender.ToImage(html, new HtmlRenderOptions { Width = 800, Height = 600, DevicePixelRatio = 2, BaseUrl = baseUrl, Resources = Resources });
File.WriteAllBytes("viewport.png", shot.ToPng());
Console.WriteLine($"viewport.png: {shot.Width}x{shot.Height}");
 
// page.screenshot({ fullPage: true }): measure the document, then render that tall.
var measure = InteractivePage.Create(html, new HtmlPageOptions(FontProviders.CreateDefault())
    { ViewportWidth = 800, ViewportHeight = 600, BaseUrl = baseUrl, StylesheetBytes = Resources });
int height = measure.Layout.ScrollHeight(measure.Page.Document.DocumentElement);
RgbaImage full = HtmlRender.ToImage(html, new HtmlRenderOptions { Width = 800, Height = height, BaseUrl = baseUrl, Resources = Resources });
File.WriteAllBytes("full.png", full.ToPng());
Console.WriteLine($"full.png: {full.Width}x{full.Height}");
Text
report.pdf: 2 pages
viewport.png: 1600x1200
full.png: 800x1818

The PDF uses print media, so the Download button is gone, and the page numbers come from the @page rule. The screenshots use screen media and keep the button.

Call by call

Headless browserOneRender
page.setContent(html)pass the HTML string
page.goto(url)fetch the HTML yourself, set BaseUrl to its address
network requests, waitUntil: 'networkidle0'the Resources callback returns each style sheet, image and font; nothing is fetched
page.pdf({ format, margin })HtmlPdf.ToPdf with PdfPageSetup, or @page { size; margin } in CSS
printBackground: truealways on: backgrounds are painted
displayHeaderFooter, headerTemplate, footerTemplate@page margin boxes with counter(page), counter(pages) and string()
page.emulateMediaType('print')HtmlPdf uses print media; HtmlPageOptions.PrintMedia for anything else
page.screenshot()HtmlRender.ToImage with Width, Height and DevicePixelRatio
screenshot({ fullPage: true })measure with ScrollHeight, then render at that height, as above
screenshot({ omitBackground: true })PageColor = PaintColor.Transparent
element.boundingBox()Layout.GetBoundingClientRect(node) on an InteractivePage
page.evaluate(...)not available: there is no JavaScript

Full-page screenshots

ScrollHeight of the root element is the height of the whole document at that viewport width, as document.documentElement.scrollHeight is in a browser. HtmlRender.ToImage accepts up to 16,384 CSS pixels on each side, so a longer page belongs in a PDF.

What changes

  • No JavaScript. Scripts are inert text, so a page that builds itself in the browser renders empty. Render the final HTML on the server, for example with Razor (Render a Razor component to PDF).
  • No fonts from the machine. Text uses the bundled fonts and the packages you add, so output doesn’t change between a laptop and a container. See Fonts.
  • No process to manage. Rendering is a function call in your process: no Chromium download, no sandbox flags, no browser pool or crashed tabs, and it works with NativeAOT.
  • Tagged PDFs. Output is tagged from the HTML semantics by default, and PDF/UA-1 or PDF/A-2a is one option away (Accessible PDF).
  • Gaps to check for. <img> of an SVG file isn’t drawn (inline <svg> is), there are no vertical writing modes or CSS footnotes, and repeated table headers don’t show in 1.0.0 (Headers, footers and page breaks).

How close is it to Chrome?

Layout is compared with Chrome for Testing 156 on the same font files, element by element. On the six document templates, 99.967% of the geometry is within tolerance at both 1280 by 800 and 390 by 844. Web Platform Tests reftests gate each CSS module. The method and the numbers are in Geometry against Chrome and Web Platform Tests.