PeachBrackets

PeachBrackets is the HTML, CSS, SVG and MathML engine inside PeachPDF, on its own. It parses a document, resolves the cascade, lays the content out into pages, and paints each page onto any PeachDrawing.Core Canvas: a PDF page from PeachDrawing.PDF, a bitmap from PeachDrawing, or a canvas you implement. It has no PDF dependency and does not know what the canvas writes. It is pure .NET, trimmable and Native AOT compatible, and is versioned in lockstep with PeachPDF: the same version number for every release.

dotnet add package PeachBrackets

Status: pre-1.0. HtmlDocument, HtmlDocumentOptions, DocumentPage and the annotation and tagging types on this page are the package’s public surface today - see PublicApi.txt for the exact, machine-checked snapshot. PeachPDF is the package’s other consumer: it lays HTML out with this same engine and paints it onto PDF pages, so this is not a copy built beside it. Until 1.0, the public API may change between releases, and what PeachPDF reaches through internals today moves onto it step by step.

For what the engine supports - elements, properties, selectors, at-rules - see HTML and CSS support and SVG support. That matrix describes the engine; it applies here exactly as it does to PeachPDF.

Quick start

Render a document to a PNG per page:

using PeachBrackets;
using PeachDrawing;
using PeachImage.Formats.Png;

var context = new RasterRenderContext();
await using (var fontFile = File.OpenRead("SourceSans3-Regular.ttf"))
{
    await context.AddFont(fontFile, "Source Sans");
}

var html = """
    <html><body style="font-family: 'Source Sans'">
      <h1>Hello, PeachBrackets</h1>
      <p style="background: #fde8c8; padding: 6pt">Laid out by the same engine that powers PeachPDF.</p>
    </body></html>
    """;

using var document = await HtmlDocument.LoadAsync(html, context, new HtmlDocumentOptions
{
    PageWidthPoints = 400,
    PageHeightPoints = 300,
});

using (var measuring = context.CreateCanvas(1, 1))
{
    await document.LayoutAsync(measuring);
}

foreach (var page in document.Pages)
{
    using var canvas = context.CreateCanvas((int)page.WidthPoints, (int)page.HeightPoints);
    await document.PaintPageAsync(page.Index, canvas);

    await using var file = File.Create($"page-{page.Index + 1}.png");
    await canvas.SaveAsync(file, "png", new PngEncoderOptions());
}

The three steps

  1. HtmlDocument.LoadAsync(html, renderContext, options) parses the HTML and cascades every style sheet that applies. The RenderContext supplies the fonts, images and resource loading the document uses; fonts the document names must be registered on it (or installed) before loading.
  2. LayoutAsync(measuringCanvas) breaks the content into pages. The canvas is only used to measure text, so any canvas made by the same render context will do; nothing is drawn on it. A document is laid out once.
  3. PaintPageAsync(pageIndex, canvas) draws one page onto a canvas you provide. The page is painted in the layer order css-page-3 defines: the @page box’s background, the document canvas background, the page border, the content, footnote areas, and the margin boxes. Pages can be painted in any order, any number of times.

Pages lists the laid-out pages: each has its Index and the size of its sheet in points. A page’s size can differ from its neighbours’ when @page rules say so, which is why you read it per page rather than assuming one size.

Units

Every length the API reports or accepts is in points (1/72 inch). The canvases are the exception: a canvas is in the layout units of the render context, RenderContext.LayoutUnitsPerPoint units to the point, with its origin at the top-left corner of the sheet and y growing downward. With the default of one unit per point, a canvas for a 400 x 300 point page is 400 x 300 units. Set LayoutUnitsPerPoint before loading a document: fonts are sized in it.

HtmlDocumentOptions

Option Meaning
PageWidthPoints, PageHeightPoints The sheet. A4 (595 x 842) by default. A @page { size } rule in the document overrides it.
MarginTopPoints, MarginRightPoints, MarginBottomPoints, MarginLeftPoints, SetMargins(points) The page margins, 20 points each by default. @page { margin } overrides them.
Media The media type @media queries see. print by default.
ColorScheme What prefers-color-scheme reports: Light (the default) or Dark.
UserStyleSheet CSS applied after the built-in defaults and before the document’s own style sheets.
IgnoreAuthorStyleSheets Drop the document’s own <style> and <link rel=stylesheet> rules, leaving the defaults and UserStyleSheet.
DefaultLanguage The language to assume when the document declares none; it drives hyphenation and language-dependent line breaking.
AnimationProgress Paint the document at a point in its animations, from 0 to 1, instead of its static look.
SnapBoxDecorationsToCssPixels, SnapBorderWidthsToCssPixels Snap backgrounds, borders and border widths to whole CSS pixels, as a browser does.
CollectFormFields Collect form controls into Annotations.FormFields.

The options are read when the document is loaded.

What the pages hold besides pixels

document.Annotations describes the parts of the document a reader of the pages can act on, positioned on their pages in points from the top-left corner of the sheet. A consumer that writes a format with these concepts carries them over; one that only draws can ignore them.

document.Language and document.Metadata (title, author, subject, keywords, date, generator) report what the document says about itself. GetElementBounds(id) gives the rectangle of an element by its id.

Structure for tagged output

Set document.StructureTagSink to an IStructureTagSink before painting, and the engine reports the logical structure of what it paints: a grouping element for a list or a section, a content element around the text and images of a paragraph, an artifact for decoration that is not part of the structure. Each element is identified by an opaque StructureNode, equal for the same source element on every page, so a paragraph that continues across a page break is one element. This is the seam a tagged PDF is built from: PeachPDF implements the sink with PdfTagRecorder from PeachDrawing.PDF. Which tag a box maps to is governed by the -peachpdf-pdf-tag-type property described in HTML and CSS support.

Text that was cut off

document.ClippedText lists the words that were painted but cut by an ancestor’s overflow clip, with how much of each showed. Read it after painting to learn whether content was silently lost, rather than discovering it in the output.

Building a document in code

HtmlDocument.LoadAsync parses HTML. When your tree is not HTML - it comes from a template, a model or a fluent builder - build it directly instead, with the types in PeachBrackets.Building:

var context = new DocumentBuildContext(renderContext);
var page = context.CreatePage();

var heading = page.Root.AppendBlock();
heading.SetStyle("font-size", "24pt");
heading.AppendText("Quarterly report");

var sheet = await StyleSheet.ParseAsync(renderContext, ".note { color: #555 }");
using var document = await HtmlDocument.CreateAsync(page, new HtmlDocumentOptions(), sheet);

A DocumentNode is one box of the tree. AppendBlock, AppendText, AppendElement, AppendListItem, AppendImage and AppendHtmlFragment add children; SetStyle writes a CSS property, SetAttribute/AddClass give a node the id and classes a style sheet selects on, and SetImageContent/SetSvgContent supply a decoded image, an SvgImage, or a function that produces either at the size the layout decides. DeclarativePage.AddMarginBox fills one of the page’s margin boxes, and Overrides records which of the page size and margins your code fixed explicitly, so a style sheet’s @page rule does not replace them. The result lays out and paints exactly like a loaded document.

Font and URI helpers for a host

A RenderContext that wants the HTML engine’s font behaviour (the CSS generic families, system-ui, font-variation-settings) needs three small things, which FontDefaults provides: DefaultFamily and DefaultSizePoints, GenericFamilies (each CSS keyword with the generic family it stands for) and ToAxisSettings (the encoded font-variation-settings as font axes). DataUri.TryDecode decodes a data: URI, percent-decoding before base64-decoding as the URL standard does, and PeachBrackets.Svg.SvgGlyphPainter is the ISvgGlyphPainter that paints SVG-in-OpenType glyphs onto the canvas a RenderContext hands out.

Using it from PeachPDF

You do not need this package to render HTML to PDF - PeachPDF depends on it and drives it for you, writing the pages through PeachDrawing.PDF. Reach for PeachBrackets directly to render HTML to something else (a preview bitmap, a different document format, your own canvas), or to inspect how a document paginates and what its links and outline are.

Licences

PeachBrackets is BSD 3-Clause. The CSS parser descends from ExCSS and the layout engine from HTML Renderer; their notices ship in the package. See License for the whole list.