Skip to content

Render Engine

Oliver Drobnik edited this page Nov 12, 2026 · 4 revisions

Cross-platform HTML/CSS → PDF render engine

SwiftTextRender is a from-scratch, cross-platform rendering engine that turns HTML + CSS into a PDF without WebKit — a Swift port of the core WeasyPrint pipeline. It runs anywhere Swift + Foundation run (macOS, iOS, Linux, Windows), so it covers the platforms where the WebKit/NSPrintOperation paginated-PDF path is unavailable, and can be used everywhere as a dependency-light alternative.

Why a port (and what is not ported)

WeasyPrint leans on native C libraries for everything hard: Pango + HarfBuzz + fontconfig for text/fonts, and pydyf for PDF bytes. To stay self-contained and cross-platform, the parts that had no in-house equivalent were re-implemented in pure Swift; the parts SwiftText already had were reused:

Concern WeasyPrint (Python) Here
HTML parsing tinyhtml5 reused SwiftTextHTML → XMLKit HTMLParser (libxml2)
PDF byte output pydyf SwiftTextPDFWriter (pure-Swift port of pydyf)
Fonts / metrics Pango/HarfBuzz/fontconfig SwiftTextOpenType (pure-Swift sfnt reader)
CSS parsing/cascade tinycss2 / cssselect2 SwiftTextCSS (tokenizer, parser, selectors, cascade)
Layout / draw weasyprint/layout,/draw SwiftTextRender (box tree, layout, paint)

Modules

All four are Foundation-only and always available (no package trait required):

  • SwiftTextPDFWriter — low-level PDF object model + writer (dictionaries, arrays, streams, content-stream operators, classic xref).
  • SwiftTextOpenType — TrueType/OpenType reader: head/hhea/maxp/ hmtx/cmap/OS₂/post, glyph advances, Unicode→glyph mapping, and the raw bytes for embedding. Handles .ttc collections.
  • SwiftTextCSS — CSS Syntax Level 3 tokenizer + parser, CSS Color 3, selector matching with specificity, and the cascade producing a typed ComputedStyle (UA stylesheet + author sheets + inline styles).
  • SwiftTextRender — the engine: DOM adapter, box tree, block & inline layout, pagination, and painting to PDF.

Pipeline

HTML string
  → SwiftTextHTML.DomBuilder (libxml2 via XMLKit)   → DOM
  → StyledElement.build (+ StyleResolver cascade)    → styled tree
  → BoxTreeBuilder.build                             → box tree
  → LayoutEngine.layout                              → geometry + line boxes
  → paginate                                         → page slices
  → Painter.paint (per page)                         → PDF content streams
  → FontResourceBuilder.finalize + PDF.write         → PDF bytes

Usage

import SwiftTextRender

let html = "<h1>Hello</h1><p>Rendered with no WebKit.</p>"
let pdf = try await HTMLRenderer.renderPDF(html: html)        // Data
try pdf.write(to: URL(fileURLWithPath: "out.pdf"))

Page geometry (defaults to US Letter, paginated):

var options = RenderOptions()
options.pageWidthPx = 794        // A4 width  @96dpi (210mm)
options.pageHeightPx = 1123      // A4 height @96dpi (297mm); nil = one auto-height page
options.pageMarginPx = 48
let pdf = try await HTMLRenderer.renderPDF(html: html, css: ["body { font-family: serif }"], options: options)

Embedding arbitrary fonts (any family/script, not just the base-14 set):

let fonts = FontBook()
try fonts.register(data: try Data(contentsOf: fontURL), family: "Inter")
let pdf = try await HTMLRenderer.renderPDF(
    html: "<p style=\"font-family: Inter\">Any glyph the font has.</p>",
    fonts: fonts)

Registered fonts are embedded as CIDFontType2 (Type0 / Identity-H) with a FontFile2 program, a width array for the glyphs used, and a ToUnicode CMap so the text stays selectable and searchable. With no fonts registered, the engine uses the PDF base-14 faces (Helvetica, Courier) with their standard metrics — no embedding needed.

Supported today

  • Box model: margins (with adjacent-sibling collapsing), borders, padding, width/height; anonymous-block generation.
  • Block stacking and inline layout with greedy line breaking, whitespace collapsing, and white-space: pre/nowrap.
  • text-align left/right/center/justify; <br> and <pre> line breaks.
  • Backgrounds, solid borders, color (color, named/#hex/rgb()/hsl()).
  • Fonts: base-14 + embedded OpenType; bold/italic/family selection.
  • text-decoration underline / line-through.
  • Lists (<ul>/<ol> markers), clickable links (<a href> → PDF annotations).
  • Pagination to a fixed page size; embedded <style> and caller CSS.
  • CSS Fragmentation: break-before, break-after and break-inside (plus the legacy page-break-* aliases). Forced breaks are mandatory; avoid is honoured when an alternative exists — a block taller than the page is split regardless. A forced break with nothing painted before or after it is dropped rather than emitting a blank page, which is how WeasyPrint behaves and is a deliberate difference from the WebKit engine (it emits the leading blank).
  • @page { size; margin }, plus CSS Paged Media margin boxes: @top-left/ -center/-right and @bottom-left/-center/-right for running headers/ footers, with content as a literal string or counter(page)/ counter(pages) (optionally styled, e.g. counter(page, upper-roman)), and the :first/:left/:right page selectors (e.g. to suppress a header on the title page via @page :first { @top-center { content: normal } }).
  • Images: JPEG, and PNG in every colour type — palette, alpha and interlaced images are unpacked into colour plus a soft mask. An <img> sits in the line like a glyph (with vertical-align) unless it is styled block-level, and max-width scales it with its height.
  • Tables with automatic layout: shrink-to-fit without a width, spare width shared among columns that set none, cell widths honoured, colspan/rowspan, separated and collapsed borders, repeated headers across pages.
  • margin: auto centring, and HTML's legacy alignment (<center>, align="center"), which centres a nested table without centring its text.

HTML email

Email HTML lays itself out with nested tables, attributes rather than CSS (Outlook ignores much of CSS) and remote images. The engine renders it the way a browser does:

  • Presentational attributes become CSS as the HTML standard maps them: width/height, bgcolor, align/valign, cellpadding/cellspacing, a table's border, hspace/vspace and <font>. Any CSS rule for the same property wins over them.
  • Remote images (http/https) are fetched before layout, at most six at a time, through RenderOptions.imageLoader — URLSession by default, which gives up after 15 seconds of silence or 30 in all, and stops a body the moment it passes 20 MB. Images inside display: none are skipped. Set it to nil to render without touching the network (a remote image then draws as a placeholder, and the warning handler says why), or supply your own loader to resolve other schemes, such as an email's cid: parts. Loading an email's images tells its sender it was opened, as any mail client does.
  • Untrusted HTML reads no local files when RenderOptions.allowsLocalFiles is false: <img> paths and file: URLs draw as placeholders, and the @font-face sources in the document's own <style> are not read. Otherwise a sender could name /dev/zero, which never finishes reading, or a private file to embed in the PDF. Fonts in the caller's CSS and installed system fonts are still used. swifttext pdf turns it off for .eml input.
  • Blocks where a table or a line does not expect them are kept, as CSS 2.1 keeps them: a <td style="display: block"> (how templates centre a column of fixed width) is wrapped in an anonymous table cell, and an inline box holding a block — <a href="…"><img style="display: block"></a> — is split around it. A block image moves to the next page whole rather than being cut.
  • CR LF line ends, which every mail body has, read as single newlines.
  • The Euro sign is drawn from an embedded, metric-compatible face (Arial, Times New Roman, or Liberation) when one is installed: viewers disagree on the base-14 Euro, and macOS's draws it into the digit that follows.

Not yet (roadmap)

Flex/grid, floats (an align="left" image or table stays in the flow), positioned boxes, inline-block boxes (laid out as inline, or as a block when they hold block-level boxes, so side-by-side columns stack), parent/child margin collapsing, transforms, gradients, background images, GIF/WebP/SVG images, OpenType shaping beyond Arabic presentation forms (GSUB/GPOS), hyphenation. See the layout source for the simplifications called out in comments.


Generated from Docs/RENDER_ENGINE.md in the repo; edit via a merge request.

Clone this wiki locally