Skip to main content

Fonts

Every piece of text in a PDF needs a font file behind it, and PdfPinata embeds that file in the document. The core PdfPinata package does not look for font files itself. A font resolver does that: it turns a family name and a style, such as "Liberation Sans, bold", into the bytes of one font file. You register one resolver when your application starts, before you create any XFont.

The two backend packages each include a resolver that reads the fonts installed on the machine. If your fonts are not installed, for example in a container or on a web server, write your own resolver or point a backend resolver at your own files.

Register a resolver

Register the resolver from your backend package once, at startup:

using PdfPinata.Fonts;
using PdfPinata.Utils;

GlobalFontSettings.FontResolver = new SkiaFontResolver(); // PdfPinata.Skia
// or
GlobalFontSettings.FontResolver = new ImageSharpFontResolver(); // PdfPinata.ImageSharp

Both resolvers search the operating system's font folders the first time they are asked for a font:

  • On Windows, they read %SystemRoot%\Fonts and the per-user fonts folder.
  • On Linux, they ask fontconfig. If fontconfig is not installed, they read the usual font folders, such as /usr/share/fonts.
  • On macOS, they read /Library/Fonts/.
  • On other platforms, such as Android and iOS, the search throws. Use your own files there, as the next section shows.

They find .ttf, .otf, .ttc and .otc files, and each face in a collection file is available on its own.

The Installation page covers the rest of the backend setup.

Use your own font files

A server or container often has few fonts or none. You then have two choices.

Point a backend resolver at your files. SetupFontsFiles replaces the system search with the files you give it, and works on every platform:

var resolver = new SkiaFontResolver();
resolver.SetupFontsFiles(Directory.GetFiles("/app/fonts"));
GlobalFontSettings.FontResolver = resolver;

Write your own resolver. Implement IFontResolver, which has three members:

  • ResolveTypeface(familyName, isBold, isItalic) returns a FontResolverInfo that names one face, or null if you cannot serve the request.
  • GetFont(faceName) returns the bytes of the face you named.
  • DefaultFontName is the family to use when a document asks for no font in particular. For example, PinataLayout's Normal style uses it.

This resolver serves one family from files embedded in your assembly, and answers every family name with it:

using System.IO;
using PdfPinata.Fonts;

public sealed class EmbeddedFontResolver : IFontResolver
{
public string DefaultFontName => "Open Sans";

public FontResolverInfo ResolveTypeface(string familyName, bool isBold, bool isItalic)
{
if (isBold && isItalic) return new FontResolverInfo("OpenSans-BoldItalic.ttf");
if (isBold) return new FontResolverInfo("OpenSans-Bold.ttf");
if (isItalic) return new FontResolverInfo("OpenSans-Italic.ttf");
return new FontResolverInfo("OpenSans-Regular.ttf");
}

public byte[] GetFont(string faceName)
{
using Stream stream = typeof(EmbeddedFontResolver).Assembly
.GetManifestResourceStream("MyApp.Fonts." + faceName)
?? throw new FileNotFoundException("No embedded font called " + faceName);
using var bytes = new MemoryStream();
stream.CopyTo(bytes);
return bytes.ToArray();
}
}

The face name is yours to choose, but each face must have a different one. PdfPinata caches the answers for the life of the process, so it calls GetFont once for each face.

GetFont must return a single face. To serve one face of a .ttc or .otc collection, pass the file's bytes and the face's index to TrueTypeCollection.ExtractFace (namespace PdfPinata.Utils) and return the result.

The demos use a resolver of this kind, so that they look the same on every machine. It serves "Liberation Sans", "Liberation Serif", "Source Code Pro" and "Noto Sans Arabic" from embedded files. Those names do not work in your application unless your resolver serves them.

Families, styles and sizes

An XFont is a family name, a size in points and an XFontStyle. The four styles are Regular, Bold, Italic and BoldItalic:

src/SampleApp/Demos/FontsDemo.cs
(string Family, string Label)[] families =
[
(Sans, "Liberation Sans - TrueType outlines, the metrics of Arial"),
(Serif, "Liberation Serif - TrueType outlines, the metrics of Times New Roman"),
(Mono, "Source Code Pro - PostScript (CFF) outlines, regular face only")
];

(XFontStyle Style, string Label)[] styles =
[
(XFontStyle.Regular, "Regular"),
(XFontStyle.Bold, "Bold"),
(XFontStyle.Italic, "Italic"),
(XFontStyle.BoldItalic, "Bold italic")
];

foreach (var (family, label) in families)
{
gfx.DrawString(label, note, XBrushes.DimGray, new XPoint(56, y));
y += 16;

foreach (var (style, styleLabel) in styles)
{
gfx.DrawString($"{styleLabel} - Sphinx of black quartz, judge my vow",
new XFont(family, 13, style), XBrushes.Black, new XPoint(70, y));
y += 19;
}

y += 10;
}

MeasureString tells you how much room text takes in a given font, so you can place lines by their real height instead of by a fixed step:

src/SampleApp/Demos/FontsDemo.cs
foreach (var size in new[] { 6.0, 8, 10, 12, 16, 21, 28, 38 })
{
var font = new XFont(Serif, size);
var measured = gfx.MeasureString("Handgloves", font);

gfx.DrawString($"{size:0}pt", note, XBrushes.LightSlateGray,
new XPoint(56, y + measured.Height));
gfx.DrawString("Handgloves", font, XBrushes.Black,
new XPoint(96, y + measured.Height));

y += measured.Height + 4;
}

Simulated bold and italic

If a family has no bold or italic file, the resolver can ask PdfPinata to fake the style. It draws simulated bold by stroking the outline of each glyph as well as filling it, and simulated italic by slanting the upright glyphs. The backend resolvers do this for you: they use the closest face the family has and simulate only what is missing.

In your own resolver, pass XStyleSimulations flags with the face:

XStyleSimulations simulate =
(isBold ? XStyleSimulations.BoldSimulation : XStyleSimulations.None)
| (isItalic ? XStyleSimulations.ItalicSimulation : XStyleSimulations.None);

return new FontResolverInfo("SourceCodePro-Regular.otf", simulate);

A simulated style is a fallback. The demo sets a stroked bold beside a designed one, and the designed face looks better:

src/SampleApp/Demos/FontsDemo.cs
gfx.DrawString("Source Code Pro bold is stroked:", note, XBrushes.DimGray,
new XPoint(56, y));
gfx.DrawString("Handgloves 123", new XFont(Mono, 20, XFontStyle.Bold),
XBrushes.Black, new XPoint(250, y + 2));
y += 26;

gfx.DrawString("Liberation Sans bold is drawn:", note, XBrushes.DimGray,
new XPoint(56, y));
gfx.DrawString("Handgloves 123", new XFont(Sans, 20, XFontStyle.Bold),
XBrushes.Black, new XPoint(250, y + 2));
y += 34;

Underline and strikeout

XFontStyle.Underline and XFontStyle.Strikeout draw a plain line:

src/SampleApp/Demos/FontsDemo.cs
gfx.DrawString("XFontStyle.Underline", new XFont(Sans, 12, XFontStyle.Underline),
XBrushes.Black, new XPoint(70, y));
gfx.DrawString("XFontStyle.Strikeout", new XFont(Sans, 12, XFontStyle.Strikeout),
XBrushes.Black, new XPoint(260, y));
y += 26;

For a dotted or dashed line, or a line in a different colour from the text, set Underline, Strikeout and DecorationColor on an XStringFormat instead:

src/SampleApp/Demos/FontsDemo.cs
var body = new XFont(Sans, 12);
foreach (var decoration in new[]
{
XTextDecoration.Single, XTextDecoration.Words, XTextDecoration.Dotted,
XTextDecoration.Dash, XTextDecoration.DotDash, XTextDecoration.DotDotDash
})
{
var format = new XStringFormat
{
Underline = decoration,
DecorationColor = XColors.Crimson
};

gfx.DrawString($"XTextDecoration.{decoration} underlines these words", body,
XBrushes.Black, new XRect(70, y, 400, 18), format);
y += 24;
}

gfx.DrawString("and a strikeout, dashed, in the colour of the text", body,
XBrushes.Black, new XRect(70, y, 400, 18),
new XStringFormat { Strikeout = XTextDecoration.Dash });

Embedding

PdfPinata always embeds the fonts a document uses, and there is no setting to turn this off. How much of the font goes into the file depends on its outlines:

  • TrueType outlines (most .ttf files) are subset. The PDF holds only the glyphs the document uses.
  • PostScript (CFF) outlines (many .otf files) are embedded whole. A document that uses one character of such a font carries the whole file.

Unicode and font embedding shows what each choice writes to the file.

Things to know

  • Register the resolver before you create a font. Reading GlobalFontSettings.FontResolver before one is set throws an InvalidOperationException. After a font has been created, setting a different resolver also throws. Setting the same instance again does nothing.
  • An unknown family does not fail with the backend resolvers. SkiaFontResolver and ImageSharpFontResolver answer a family they cannot find with a face from another installed family. To get null instead, set NullIfFontNotFound = true on the resolver. If a resolver returns null, creating the XFont throws InvalidOperationException.
  • A missing glyph draws as an empty box. If the face has no glyph for a character, you get the font's .notdef glyph and no error. Font fallback can fix this: see International text.
  • Large CFF fonts make large files. A CJK font with CFF outlines can add several megabytes to every document that uses it. Prefer a TrueType version of the font if one exists.
  • Text as outlines needs one more setting. XGraphicsPath.AddString needs GlobalFontSettings.GlyphOutlineProvider set, to SkiaGlyphOutlineProvider or ImageSharpGlyphOutlineProvider. Only XTextFormatter drop caps also use it, and they work without it. You can set it at any time.
  • Coming from PDFsharp or PdfSharpCore? There is no default resolver, and there is no PdfFontEmbedding setting, because fonts are always embedded. See Migrating.

See it in action

The Fonts demo sets three families in four styles, compares a simulated bold with a designed one, draws a size ramp and shows every decoration style.

The full Fonts demo
src/SampleApp/Demos/FontsDemo.cs
const string Sans = "Liberation Sans";
const string Serif = "Liberation Serif";
const string Mono = "Source Code Pro";

var document = new PdfDocument();

// ---- Page one: families and styles -------------------------------------------
var page = document.AddPage();
var gfx = XGraphics.FromPdfPage(page);

var heading = new XFont(Sans, 9, XFontStyle.Bold);
var note = new XFont(Sans, 8);
double y = 60;

void Heading(string text)
{
gfx.DrawString(text.ToUpperInvariant(), heading, XBrushes.SteelBlue,
new XPoint(56, y));
gfx.DrawLine(XPens.LightGray, 56, y + 5, 540, y + 5);
y += 22;
}

Heading("Families and styles");

(string Family, string Label)[] families =
[
(Sans, "Liberation Sans - TrueType outlines, the metrics of Arial"),
(Serif, "Liberation Serif - TrueType outlines, the metrics of Times New Roman"),
(Mono, "Source Code Pro - PostScript (CFF) outlines, regular face only")
];

(XFontStyle Style, string Label)[] styles =
[
(XFontStyle.Regular, "Regular"),
(XFontStyle.Bold, "Bold"),
(XFontStyle.Italic, "Italic"),
(XFontStyle.BoldItalic, "Bold italic")
];

foreach (var (family, label) in families)
{
gfx.DrawString(label, note, XBrushes.DimGray, new XPoint(56, y));
y += 16;

foreach (var (style, styleLabel) in styles)
{
gfx.DrawString($"{styleLabel} - Sphinx of black quartz, judge my vow",
new XFont(family, 13, style), XBrushes.Black, new XPoint(70, y));
y += 19;
}

y += 10;
}

// Only a regular face of Source Code Pro is carried, so the bold and italic above
// are not designed faces at all: the resolver answers with XStyleSimulations and
// the library strokes the outline to fake weight and skews it to fake slant. Set
// the two rows side by side and the difference is plain - simulated bold is
// uniformly fatter, where a drawn bold face redistributes weight around the letter.
Heading("Simulated against designed");

gfx.DrawString("Source Code Pro bold is stroked:", note, XBrushes.DimGray,
new XPoint(56, y));
gfx.DrawString("Handgloves 123", new XFont(Mono, 20, XFontStyle.Bold),
XBrushes.Black, new XPoint(250, y + 2));
y += 26;

gfx.DrawString("Liberation Sans bold is drawn:", note, XBrushes.DimGray,
new XPoint(56, y));
gfx.DrawString("Handgloves 123", new XFont(Sans, 20, XFontStyle.Bold),
XBrushes.Black, new XPoint(250, y + 2));
y += 34;

// A family nothing here carries. The resolver answers it rather than failing, so a
// document written against fonts that are not present still lays out identically
// everywhere instead of falling back to whatever the machine has.
Heading("A family that is not carried");
gfx.DrawString("new XFont(\"Comic Sans MS\", 13) resolves to the sans:", note,
XBrushes.DimGray, new XPoint(56, y));
gfx.DrawString("and is drawn like this", new XFont("Comic Sans MS", 13),
XBrushes.Black, new XPoint(320, y));

// ---- Page two: sizes and decorations -----------------------------------------
page = document.AddPage();
gfx = XGraphics.FromPdfPage(page);
y = 60;

Heading("A size ramp");

// Each line is placed from the height of the one before it rather than by a fixed
// step, so the gaps stay right as the size changes. MeasureString answers in the
// same units the page is drawn in. The size label goes on the left, where a long
// word set large cannot grow into it.
foreach (var size in new[] { 6.0, 8, 10, 12, 16, 21, 28, 38 })
{
var font = new XFont(Serif, size);
var measured = gfx.MeasureString("Handgloves", font);

gfx.DrawString($"{size:0}pt", note, XBrushes.LightSlateGray,
new XPoint(56, y + measured.Height));
gfx.DrawString("Handgloves", font, XBrushes.Black,
new XPoint(96, y + measured.Height));

y += measured.Height + 4;
}

y += 28;
Heading("Decorations");

// XFontStyle carries underline and strikeout, which is the older way and gives no
// say over how the line is drawn.
gfx.DrawString("XFontStyle.Underline", new XFont(Sans, 12, XFontStyle.Underline),
XBrushes.Black, new XPoint(70, y));
gfx.DrawString("XFontStyle.Strikeout", new XFont(Sans, 12, XFontStyle.Strikeout),
XBrushes.Black, new XPoint(260, y));
y += 26;

// XStringFormat carries the same two as XTextDecoration, which chooses the pattern
// and lets the line take a colour of its own - the one thing a caller cannot do by
// drawing the rule by hand afterwards, since it would have to measure the text.
var body = new XFont(Sans, 12);
foreach (var decoration in new[]
{
XTextDecoration.Single, XTextDecoration.Words, XTextDecoration.Dotted,
XTextDecoration.Dash, XTextDecoration.DotDash, XTextDecoration.DotDotDash
})
{
var format = new XStringFormat
{
Underline = decoration,
DecorationColor = XColors.Crimson
};

gfx.DrawString($"XTextDecoration.{decoration} underlines these words", body,
XBrushes.Black, new XRect(70, y, 400, 18), format);
y += 24;
}

gfx.DrawString("and a strikeout, dashed, in the colour of the text", body,
XBrushes.Black, new XRect(70, y, 400, 18),
new XStringFormat { Strikeout = XTextDecoration.Dash });