Skip to main content

Drawing text

XGraphics.DrawString draws one line of text on a page. You give it the string, an XFont, a brush for the colour, and either a point or a rectangle. An XStringFormat controls where the text sits and how it is spaced. For text that wraps over several lines, use XTextFormatter instead.

This page assumes you have registered a font resolver. See Fonts.

At a point or in a rectangle

With a point, the text's baseline sits on the point and the text runs to the right of it. With a rectangle and a format, the format places the text inside the rectangle. The library does not draw the rectangle:

src/SampleApp/Demos/TextDemo.cs
gfx.DrawLine(XPens.Crimson, 48, 90, 300, 90);
gfx.DrawString("Drawn at a point: this is the baseline", body, XBrushes.Black,
new XPoint(48, 90));

// The rectangle overload places the string inside the box according to the
// format. The box itself is never drawn by the library.
var box = new XRect(320, 76, 228, 28);
gfx.DrawRectangle(boxPen, box);
gfx.DrawString("Centred in a rectangle", body, XBrushes.Black, box,
XStringFormats.Center);

Alignment

XStringFormats has ready-made formats for the nine combinations of top, centre and bottom with left, centre and right:

src/SampleApp/Demos/TextDemo.cs
(string Name, XStringFormat Format)[] presets =
{
("TopLeft", XStringFormats.TopLeft),
("TopCenter", XStringFormats.TopCenter),
("TopRight", XStringFormats.TopRight),
("CenterLeft", XStringFormats.CenterLeft),
("Center", XStringFormats.Center),
("CenterRight", XStringFormats.CenterRight),
("BottomLeft", XStringFormats.BottomLeft),
("BottomCenter", XStringFormats.BottomCenter),
("BottomRight", XStringFormats.BottomRight)
};

for (var index = 0; index < presets.Length; index++)
{
// ReSharper disable once PossibleLossOfFraction
var cell = new XRect(48 + index % 3 * 172, 144 + index / 3 * 72, 160, 60);
gfx.DrawRectangle(boxPen, cell);
gfx.DrawString(presets[index].Name, body, XBrushes.Black, cell,
presets[index].Format);
}

It also has BaseLineLeft, BaseLineCenter and BaseLineRight, which put the baseline on the top edge of the rectangle. XStringFormats.Default is BaseLineLeft, and it is what DrawString uses when you pass no format.

To build your own format, set Alignment (an XStringAlignment: Near, Center or Far) and LineAlignment (an XLineAlignment). LineAlignment has Near, Center, Far and BaseLine, and three more that follow the HTML canvas: Hanging, Ideographic and SvgMiddle.

BaseLine reads only the top edge of the rectangle. Give the rectangle a height of 0:

src/SampleApp/Demos/TextDemo.cs
gfx.DrawLine(XPens.Crimson, 48, 440, 300, 440);
gfx.DrawString("BaseLine sits on the rule", body, XBrushes.Black,
new XRect(48, 440, 252, 0),
new XStringFormat { LineAlignment = XLineAlignment.BaseLine });

Measure text

MeasureString returns the width and height of a string in the same units you draw in. Use it to fit a rule under the text, place the next word, or check whether a line fits:

src/SampleApp/Demos/TextDemo.cs
const string measured = "MeasureString gives this rule its length";
var size = gfx.MeasureString(measured, body);
gfx.DrawString(measured, body, XBrushes.Black, new XPoint(48, 400));
gfx.DrawLine(new XPen(XColors.SteelBlue, 1), 48, 404, 48 + size.Width, 404);
gfx.DrawString($"{size.Width:0.#} x {size.Height:0.#} points", note,
XBrushes.DimGray, new XPoint(48 + size.Width + 10, 400));

If you draw with an XStringFormat that changes spacing, pass the same format to MeasureString(text, font, format). The measurement then includes the spacing.

Spacing, scaling and slant

XStringFormat carries the PDF text-state settings:

  • CharacterSpacing adds space after every glyph, in points. A negative value tightens the text.
  • WordSpacing adds space after every space character only.
  • HorizontalScaling stretches or squeezes the glyphs, as a percentage. 100 is normal.
  • ObliqueAngle slants the upright glyphs by an angle in degrees. This is not an italic: a real italic face has different letter shapes.
src/SampleApp/Demos/TextDemo.cs
// Tc in the content stream: extra space after every glyph, negative to tighten.
foreach (var spacing in new[] { -0.4, 0.0, 2.0 })
Row($"CharacterSpacing = {spacing}", new XStringFormat { CharacterSpacing = spacing });

y += 8;

// Tw: extra space after every space character only, so it stretches the gaps
// between words and leaves the words themselves alone.
foreach (var spacing in new[] { 0.0, 4.0, 10.0 })
Row($"WordSpacing = {spacing}", new XStringFormat { WordSpacing = spacing });

y += 8;

// Tz: the glyphs are stretched or squeezed horizontally, a percentage of normal.
foreach (var scale in new[] { 60.0, 100.0, 150.0 })
Row($"HorizontalScaling = {scale}", new XStringFormat { HorizontalScaling = scale });

y += 8;

// A skew rather than an italic: the upright glyphs are slanted, where a real italic
// is a different set of shapes. The Fonts demo sets the two side by side.
Row("ObliqueAngle = 12", new XStringFormat { ObliqueAngle = 12 });

Superscripts and subscripts

TextRise moves the baseline up (positive) or down (negative) by a number of points without changing the glyph size. For a superscript or a subscript, use a smaller font as well:

src/SampleApp/Demos/TextDemo.cs
var small = new XFont(Sans, 8);
double x = 48;
gfx.DrawString("H", sample, XBrushes.Black, new XPoint(x, y));
x += gfx.MeasureString("H", sample).Width;
gfx.DrawString("2", small, XBrushes.Black, new XRect(x, y, 20, 0),
new XStringFormat { TextRise = -3, LineAlignment = XLineAlignment.BaseLine });
x += gfx.MeasureString("2", small).Width;
gfx.DrawString("SO", sample, XBrushes.Black, new XPoint(x, y));
x += gfx.MeasureString("SO", sample).Width;
gfx.DrawString("4", small, XBrushes.Black, new XRect(x, y, 20, 0),
new XStringFormat { TextRise = -3, LineAlignment = XLineAlignment.BaseLine });
x += gfx.MeasureString("4", small).Width + 24;

gfx.DrawString("x", sample, XBrushes.Black, new XPoint(x, y));
x += gfx.MeasureString("x", sample).Width;
gfx.DrawString("2", small, XBrushes.Black, new XRect(x, y, 20, 0),
new XStringFormat { TextRise = 5, LineAlignment = XLineAlignment.BaseLine });

Fill, outline, or both

Which of a brush and a pen you pass decides how the glyphs are painted. A brush fills them, a pen outlines them, and both together fill and then outline them. You must pass at least one:

src/SampleApp/Demos/TextDemo.cs
var display = new XFont(Sans, 40, XFontStyle.Bold);
var outline = new XPen(XColors.Crimson, 0.8);

gfx.DrawString("Filled", display, XBrushes.Black, new XPoint(48, 120));
gfx.DrawString("Stroked", display, outline, XBrushes.Transparent, new XPoint(48, 172));
gfx.DrawString("Both", display, outline, new XSolidBrush(XColors.Wheat),
new XPoint(48, 224));

The brush sets the text colour. XBrushes has named colours, and new XSolidBrush(colour) takes an XColor made with XColor.FromArgb, XColor.FromCmyk or XColor.FromGrayScale. See Shapes, pens and brushes for colours, pens and transparency.

A link is a clickable rectangle on the page. The text under it is a separate drawing, and the library draws nothing for the link. Measure the text, draw it, and add the link over the same area:

src/SampleApp/Demos/TextDemo.cs
const string linkText = "The PdfPinata repository";
var linkFont = new XFont(Sans, 12, XFontStyle.Underline);
var linkSize = gfx.MeasureString(linkText, linkFont);
gfx.DrawString(linkText, linkFont, XBrushes.MediumBlue, new XPoint(48, y));
gfx.AddWebLink(new XRect(48, y - linkSize.Height + 3, linkSize.Width, linkSize.Height),
"https://github.com/PinataLabs/PdfPinata");

y += 26;

// A destination is a named place in this document. A named link goes to it
// without knowing which page it ended up on, which is what keeps it correct
// after the pages have been moved or the document resized.
gfx.AddNamedDestination("colours", new XPoint(48, 260));
const string namedText = "Back up to the colours on this page";
var namedSize = gfx.MeasureString(namedText, linkFont);
gfx.DrawString(namedText, linkFont, XBrushes.MediumBlue, new XPoint(48, y));
gfx.AddNamedLink(new XRect(48, y - namedSize.Height + 3, namedSize.Width, namedSize.Height),
"colours");

y += 26;

const string pageText = "To page one";
var pageSize = gfx.MeasureString(pageText, linkFont);
gfx.DrawString(pageText, linkFont, XBrushes.MediumBlue, new XPoint(48, y));
gfx.AddDocumentLink(new XRect(48, y - pageSize.Height + 3, pageSize.Width, pageSize.Height), 1);
  • AddWebLink opens a URL.
  • AddNamedDestination marks a place in the document, and AddNamedLink goes to it. A named link still works after pages are moved, inserted or resized.
  • AddDocumentLink goes to a page by number. The first page is 1.

The rectangles use the same coordinates and units as your drawing. Navigation and viewer preferences covers destinations in more detail.

Things to know

  • DrawString draws one line. It does not wrap. A line feed does not start a new line: it is dropped, and the words either side of it run together. A tab is drawn as one space. See Control characters.
  • A new XStringFormat is not the default format. new XStringFormat() aligns the text to the top left of the rectangle, but DrawString without a format aligns the baseline. If you create a format only to set spacing or direction, also set LineAlignment = XLineAlignment.BaseLine, or the text moves down by the height of the font.
  • A link is invisible. Without the colour and underline you draw yourself, nobody can see it.
  • Underline with the font or with the format. XFontStyle.Underline draws a plain line. XStringFormat.Underline and Strikeout take an XTextDecoration for dotted and dashed lines, and DecorationColor gives the line its own colour. See Fonts.
  • Right-to-left text is reordered for you. XStringFormat.TextDirection sets the paragraph direction. See International text.

See it in action

The Text demo places text at a point and in all nine alignments, measures it, draws the spacing settings side by side, paints text three ways, and adds three kinds of link.

The full Text demo
src/SampleApp/Demos/TextDemo.cs
const string Sans = "Liberation Sans";

var document = new PdfDocument();
var body = new XFont(Sans, 11);
var note = new XFont(Sans, 8);
var headingFont = new XFont(Sans, 9, XFontStyle.Bold);
var boxPen = new XPen(XColors.Gainsboro, 0.5);

// ---- Page one: where the string goes -----------------------------------------
var page = document.AddPage();
var gfx = XGraphics.FromPdfPage(page);

void Heading(string text, double y)
{
gfx.DrawString(text.ToUpperInvariant(), headingFont, XBrushes.SteelBlue,
new XPoint(48, y));
gfx.DrawLine(XPens.LightGray, 48, y + 5, 548, y + 5);
}

Heading("A point, or a rectangle and a format", 56);

// The point overload puts the baseline of the text at the point. Nothing is
// centred, nothing is measured, and the string runs to the right of it.
gfx.DrawLine(XPens.Crimson, 48, 90, 300, 90);
gfx.DrawString("Drawn at a point: this is the baseline", body, XBrushes.Black,
new XPoint(48, 90));

// The rectangle overload places the string inside the box according to the
// format. The box itself is never drawn by the library.
var box = new XRect(320, 76, 228, 28);
gfx.DrawRectangle(boxPen, box);
gfx.DrawString("Centred in a rectangle", body, XBrushes.Black, box,
XStringFormats.Center);

Heading("The nine presets", 124);

// Every combination of near, centre and far in both directions. Each is drawn in
// the same rectangle so it is the format alone that moves the words.
(string Name, XStringFormat Format)[] presets =
{
("TopLeft", XStringFormats.TopLeft),
("TopCenter", XStringFormats.TopCenter),
("TopRight", XStringFormats.TopRight),
("CenterLeft", XStringFormats.CenterLeft),
("Center", XStringFormats.Center),
("CenterRight", XStringFormats.CenterRight),
("BottomLeft", XStringFormats.BottomLeft),
("BottomCenter", XStringFormats.BottomCenter),
("BottomRight", XStringFormats.BottomRight)
};

for (var index = 0; index < presets.Length; index++)
{
// ReSharper disable once PossibleLossOfFraction
var cell = new XRect(48 + index % 3 * 172, 144 + index / 3 * 72, 160, 60);
gfx.DrawRectangle(boxPen, cell);
gfx.DrawString(presets[index].Name, body, XBrushes.Black, cell,
presets[index].Format);
}

Heading("Measuring, and the baseline", 372);

// MeasureString answers in the units the page is drawn in, so a rule of exactly
// the width of the text can be drawn under it.
const string measured = "MeasureString gives this rule its length";
var size = gfx.MeasureString(measured, body);
gfx.DrawString(measured, body, XBrushes.Black, new XPoint(48, 400));
gfx.DrawLine(new XPen(XColors.SteelBlue, 1), 48, 404, 48 + size.Width, 404);
gfx.DrawString($"{size.Width:0.#} x {size.Height:0.#} points", note,
XBrushes.DimGray, new XPoint(48 + size.Width + 10, 400));

// XLineAlignment.BaseLine puts the baseline of the text on the top edge of the
// rectangle rather than fitting the text inside it, which is the right choice
// when the position that matters is the line the text sits on. The rectangle's
// height must be exactly 0 - there is nothing for the text to be aligned within,
// and passing a height throws rather than quietly ignoring it.
gfx.DrawLine(XPens.Crimson, 48, 440, 300, 440);
gfx.DrawString("BaseLine sits on the rule", body, XBrushes.Black,
new XRect(48, 440, 252, 0),
new XStringFormat { LineAlignment = XLineAlignment.BaseLine });

Heading("DrawString does not wrap", 480);

// DrawString draws one line. A newline is not a line break here and is not drawn
// either - it is dropped, the way MeasureString has always dropped it, so the two
// words either side of it run together rather than being separated by the box the
// font draws for a character it has no glyph for. A tab is drawn as the single
// space it measures as. Wrapping and breaking are XTextFormatter's job - see the
// Layout demo.
gfx.DrawString("A newline\nvanishes between these words, a tab\tis the space it "
+ "measures as, and a long line runs off the edge of the page rather than "
+ "wrapping", body, XBrushes.Black,
new XPoint(48, 510));

// ---- Page two: the state a string is drawn under -------------------------------
page = document.AddPage();
gfx = XGraphics.FromPdfPage(page);

Heading("Spacing and scaling", 56);

var sample = new XFont(Sans, 13);
double y = 84;

void Row(string label, XStringFormat format)
{
gfx.DrawString(label, note, XBrushes.DimGray, new XPoint(48, y));
gfx.DrawString("Handgloves and quartz", sample, XBrushes.Black,
new XRect(190, y - 12, 360, 20), format);
y += 30;
}

// Tc in the content stream: extra space after every glyph, negative to tighten.
foreach (var spacing in new[] { -0.4, 0.0, 2.0 })
Row($"CharacterSpacing = {spacing}", new XStringFormat { CharacterSpacing = spacing });

y += 8;

// Tw: extra space after every space character only, so it stretches the gaps
// between words and leaves the words themselves alone.
foreach (var spacing in new[] { 0.0, 4.0, 10.0 })
Row($"WordSpacing = {spacing}", new XStringFormat { WordSpacing = spacing });

y += 8;

// Tz: the glyphs are stretched or squeezed horizontally, a percentage of normal.
foreach (var scale in new[] { 60.0, 100.0, 150.0 })
Row($"HorizontalScaling = {scale}", new XStringFormat { HorizontalScaling = scale });

y += 8;

// A skew rather than an italic: the upright glyphs are slanted, where a real italic
// is a different set of shapes. The Fonts demo sets the two side by side.
Row("ObliqueAngle = 12", new XStringFormat { ObliqueAngle = 12 });

// Ts: the baseline moves up or down without changing the size of the glyphs, so a
// superscript needs the rise and a smaller font together.
Heading("Text rise", y + 6);
y += 34;

var small = new XFont(Sans, 8);
double x = 48;
gfx.DrawString("H", sample, XBrushes.Black, new XPoint(x, y));
x += gfx.MeasureString("H", sample).Width;
gfx.DrawString("2", small, XBrushes.Black, new XRect(x, y, 20, 0),
new XStringFormat { TextRise = -3, LineAlignment = XLineAlignment.BaseLine });
x += gfx.MeasureString("2", small).Width;
gfx.DrawString("SO", sample, XBrushes.Black, new XPoint(x, y));
x += gfx.MeasureString("SO", sample).Width;
gfx.DrawString("4", small, XBrushes.Black, new XRect(x, y, 20, 0),
new XStringFormat { TextRise = -3, LineAlignment = XLineAlignment.BaseLine });
x += gfx.MeasureString("4", small).Width + 24;

gfx.DrawString("x", sample, XBrushes.Black, new XPoint(x, y));
x += gfx.MeasureString("x", sample).Width;
gfx.DrawString("2", small, XBrushes.Black, new XRect(x, y, 20, 0),
new XStringFormat { TextRise = 5, LineAlignment = XLineAlignment.BaseLine });

gfx.DrawString("a negative rise for the subscript, a positive one for the power",
note, XBrushes.DimGray, new XPoint(48, y + 22));

// ---- Page three: paint, colour and links ---------------------------------------
page = document.AddPage();
gfx = XGraphics.FromPdfPage(page);

Heading("Fill, stroke, or both", 56);

// There is no rendering-mode property. Which of the brush and the pen is given
// decides it: brush alone fills (Tr 0), pen alone strokes the outline (Tr 1),
// and both together fills and then strokes (Tr 2).
var display = new XFont(Sans, 40, XFontStyle.Bold);
var outline = new XPen(XColors.Crimson, 0.8);

gfx.DrawString("Filled", display, XBrushes.Black, new XPoint(48, 120));
gfx.DrawString("Stroked", display, outline, XBrushes.Transparent, new XPoint(48, 172));
gfx.DrawString("Both", display, outline, new XSolidBrush(XColors.Wheat),
new XPoint(48, 224));

gfx.DrawString("brush only", note, XBrushes.DimGray, new XPoint(300, 116));
gfx.DrawString("pen only", note, XBrushes.DimGray, new XPoint(300, 168));
gfx.DrawString("pen and brush", note, XBrushes.DimGray, new XPoint(300, 220));

Heading("Colour", 260);

(string Label, XColor Colour)[] colours =
{
("XColor.FromArgb(220, 60, 60)", XColor.FromArgb(220, 60, 60)),
("XColor.FromArgb(90, 0, 0, 255) - alpha", XColor.FromArgb(90, 0, 0, 255)),
("XColor.FromCmyk(0.8, 0, 0.4, 0.1)", XColor.FromCmyk(0.8, 0, 0.4, 0.1)),
("XColor.FromGrayScale(0.45)", XColor.FromGrayScale(0.45))
};

y = 288;
foreach ((var label, var colour) in colours)
{
// A tint behind the alpha row, so that the transparency has something to
// show through.
gfx.DrawRectangle(new XSolidBrush(XColor.FromArgb(255, 245, 220)), 44, y - 13, 240, 20);
gfx.DrawString(label, new XFont(Sans, 13), new XSolidBrush(colour),
new XPoint(48, y));
y += 26;
}

Heading("Links", y + 6);
y += 34;

// AddWebLink takes a rectangle in the same coordinates the drawing uses. The
// library draws nothing - the blue and the underline are the caller's job, and
// without them the link is invisible.
const string linkText = "The PdfPinata repository";
var linkFont = new XFont(Sans, 12, XFontStyle.Underline);
var linkSize = gfx.MeasureString(linkText, linkFont);
gfx.DrawString(linkText, linkFont, XBrushes.MediumBlue, new XPoint(48, y));
gfx.AddWebLink(new XRect(48, y - linkSize.Height + 3, linkSize.Width, linkSize.Height),
"https://github.com/PinataLabs/PdfPinata");

y += 26;

// A destination is a named place in this document. A named link goes to it
// without knowing which page it ended up on, which is what keeps it correct
// after the pages have been moved or the document resized.
gfx.AddNamedDestination("colours", new XPoint(48, 260));
const string namedText = "Back up to the colours on this page";
var namedSize = gfx.MeasureString(namedText, linkFont);
gfx.DrawString(namedText, linkFont, XBrushes.MediumBlue, new XPoint(48, y));
gfx.AddNamedLink(new XRect(48, y - namedSize.Height + 3, namedSize.Width, namedSize.Height),
"colours");

y += 26;

const string pageText = "To page one";
var pageSize = gfx.MeasureString(pageText, linkFont);
gfx.DrawString(pageText, linkFont, XBrushes.MediumBlue, new XPoint(48, y));
gfx.AddDocumentLink(new XRect(48, y - pageSize.Height + 3, pageSize.Width, pageSize.Height), 1);