How font resolution works

EggPdf resolves fonts in this order:

  1. @font-face declarations in CSS — from http(s) URLs (Google Fonts and other CDNs), data: URIs, or files (relative paths resolve against basePath)
  2. System-installed TrueType/OpenType fonts matched by name (including .otf fonts with CFF/CFF2 outlines, which are converted to TrueType outlines, and variable fonts of either flavour)
  3. Built-in PDF standard fonts: Helvetica, Times, Courier

Remote <link rel="stylesheet"> tags are fetched too, so a plain Google Fonts <link> works out of the box — every declared weight (300–900) selects its own face, and characters missing from the chosen font (Hangul in Arial, symbols like ⚠, ...) fall back to another installed font automatically — see Per-character font fallback.

Any matched TrueType font is automatically subset — only the glyphs actually used in the document are embedded, keeping PDF size small.

Per-character font fallback

No single font covers every script, so a browser draws each character it cannot find in your font-family from some other installed font. EggPdf does the same, for every script: when the chosen font has no glyph for a character, the glyph comes from an installed font that actually maps it, so font-family: Arial with Korean, Japanese, Chinese, Hebrew, Arabic, Thai, Indic, Armenian, Georgian, symbol or math text no longer shows empty boxes. The fallback is chosen by each installed font's real character coverage, never by language, so any script works with whatever fonts the host has.

  1. A per-platform, per-script candidate list is tried in order (macOS system fonts, Windows fonts, and on Linux the Noto, Droid, WenQuanYi, Nanum, Takao, Lohit and TLWG families).
  2. Then every installed font is considered, broadest coverage first. This covers .ttc collections (every face is checked) and CFF/OpenType fonts, and finds fonts installed under names no list mentions.
  3. Bitmap-only colour fonts (such as Apple Color Emoji or Noto Color Emoji) are skipped because they have no vector outlines to embed; colour emoji need a COLR font (Segoe UI Emoji, Twemoji).

Only the glyphs actually used are embedded (several fonts per document are fine), and layout measures the fallback glyphs' real widths, so wrapping and box sizes match the painted text. Ideographs follow the regional variant of the root lang attribute (ja, ko, zh-Hans, zh-Hant, ...) and otherwise the text (kana implies Japanese, Hangul implies Korean).

When no installed font covers a character it is still drawn as the missing-glyph box, but it is no longer silent: a FONT_GLYPH_MISSING warning names the script, the characters and a font to install (for example Noto Sans CJK). Collect warnings with PdfRenderOptions.Warnings, the Fluent CollectWarnings(...), the CLI (printed to stderr) or the REST X-EggPdf-Warnings response header. In a container, install a font family for the scripts you render (the Docker images include Noto and Noto CJK) or load one with @font-face.

To prefer your own fonts for a script, register them ahead of the built-in candidates: FontFallbackCatalog.Default.Register(UnicodeScript.Hangul, "Corporate Sans KR");

CSS font-family

Use any installed font name in CSS:

body { font-family: Arial, sans-serif; }
h1   { font-family: 'Georgia', serif; }
code { font-family: 'Courier New', monospace; }

If the named font is installed on the machine running EggPdf, it will be found, subset, and embedded. If not found, EggPdf falls back to the next font in the stack, and ultimately to Helvetica.

Built-in PDF fonts

These 14 fonts are always available without embedding — zero overhead:

  • Helvetica, Helvetica-Bold, Helvetica-Oblique, Helvetica-BoldOblique
  • Times-Roman, Times-Bold, Times-Italic, Times-BoldItalic
  • Courier, Courier-Bold, Courier-Oblique, Courier-BoldOblique
  • Symbol, ZapfDingbats

These are the 14 standard PDF fonts defined in the PDF specification. Every PDF viewer renders them without needing a font file embedded in the document.

Webfonts — Google Fonts and remote URLs

Reference a hosted stylesheet exactly like on the web — EggPdf fetches the CSS, downloads the declared faces, and embeds subsets with the document's real glyphs (full Unicode, including Vietnamese and other extended Latin):

<link href="https://fonts.googleapis.com/css2?family=Be+Vietnam+Pro:wght@300;400;600;700;800" rel="stylesheet">
<style>
body { font-family: 'Be Vietnam Pro', Arial, sans-serif; }
</style>

Intermediate weights (font-weight: 500/600/800…) embed the matching variant instead of snapping to regular/bold, and text is measured with the real font metrics so line breaks match the browser's.

@font-face — load from file

Load fonts that aren't installed system-wide by providing a file path in @font-face and setting basePath when calling the API:

<style>
@font-face {
    font-family: 'Inter';
    src: url('fonts/Inter-Regular.ttf');
    font-weight: 400;
}
@font-face {
    font-family: 'Inter';
    src: url('fonts/Inter-Bold.ttf');
    font-weight: 700;
}
body { font-family: 'Inter', Helvetica, sans-serif; }
</style>

Then render with basePath:

byte[] pdf = HtmlToPdf.Render(html, basePath: "./assets/");

The url('fonts/Inter-Regular.ttf') path resolves relative to basePath. Use absolute paths in src: url(...) if you prefer not to set basePath.

Weight and style

Standard CSS values are supported: font-weight: 400, font-weight: 700, font-weight: bold, font-style: italic, font-style: normal. EggPdf selects the best matching font file from the system — for example, Arial-Bold.ttf for font-family: Arial; font-weight: bold.

When an exact weight/style variant is not found, EggPdf applies the CSS font-weight matching algorithm to pick the closest available variant rather than substituting a different typeface.

Arabic text

Arabic letters are shaped into their contextual isolated / initial / medial / final forms, including lam-alef ligatures, and laid out right-to-left. Supported letters are the standard Arabic block plus Persian peh, tcheh, jeh, keheh, gaf and yeh; other Arabic-script letters (for example Urdu tteh, ddal, rreh) are not shaped. The dir attribute and direction: rtl both apply, and default alignment follows the direction.

Complex scripts

Text in Thai, Lao, Tibetan, Khmer, Myanmar, Sinhala and the Indic scripts (Devanagari, Bengali, Gujarati, Gurmukhi, Oriya, Tamil, Telugu, Kannada, Malayalam), and all Arabic, is shaped through the font's own OpenType tables. Mark attachment places stacked Thai vowels and tone marks and Arabic diacritics; syllables are reordered (pre-base vowel signs move in front of their consonant, a reph is placed after the base as each script requires, Khmer coeng-ro and Myanmar kinzi are positioned) and the font's half forms, conjuncts and subscripts (such as क्ष) are applied. For Arabic, fonts that lack the Arabic Presentation Forms glyphs are shaped through their own init/medi/fina features and cursive attachment.

This needs a font that contains the script. EggPdf tries your font-family list and @font-face fonts first, then well-known script fonts (Nirmala UI, Leelawadee UI, Myanmar Text, Noto Sans …), preferring a font that also has Latin letters so a mixed run such as "Invoice नमस्ते" is not half boxes; if none of those is installed, any installed font covering the script is used. That text is embedded in its own font, so Latin text elsewhere keeps the typeface you asked for. Output was checked against Chrome's rendering for each script listed above.

Long Thai, Lao, Khmer and Myanmar paragraphs wrap at syllable boundaries; this is a heuristic, since real word breaking needs a dictionary (lines may end mid-word). line-height: normal uses the shaping font's ascent, descent and line gap, so tall fonts such as Myanmar Text don't collide. Text is reordered with the full Unicode Bidirectional Algorithm (UAX #9: explicit embeddings, overrides and isolates, weak-type and neutral resolution, paired brackets, mirroring) using the CSS direction as the paragraph direction. Scripts beyond those listed are not shaped.