Repository navigation
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.
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) |
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.ttccollections. -
SwiftTextCSS— CSS Syntax Level 3 tokenizer + parser, CSS Color 3, selector matching with specificity, and the cascade producing a typedComputedStyle(UA stylesheet + author sheets + inline styles). -
SwiftTextRender— the engine: DOM adapter, box tree, block & inline layout, pagination, and painting to PDF.
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
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.
- 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-alignleft/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-decorationunderline / 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-afterandbreak-inside(plus the legacypage-break-*aliases). Forced breaks are mandatory;avoidis 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/-rightand@bottom-left/-center/-rightfor running headers/ footers, withcontentas a literal string orcounter(page)/counter(pages)(optionally styled, e.g.counter(page, upper-roman)), and the:first/:left/:rightpage 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 (withvertical-align) unless it is styled block-level, andmax-widthscales 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: autocentring, and HTML's legacy alignment (<center>,align="center"), which centres a nested table without centring its text.
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'sborder,hspace/vspaceand<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, throughRenderOptions.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 insidedisplay: noneare skipped. Set it tonilto 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'scid: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.allowsLocalFilesisfalse:<img>paths andfile:URLs draw as placeholders, and the@font-facesources 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 pdfturns it off for.emlinput. -
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.
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.