Drawing text
Table of contents
- Fonts and font families
- Filling and stroking text
- Positioning text
- Drawing underlined text
- Drawing text along a path
- Drawing formatted text
- Text spacing
- Adding text to a
GraphicsPath
- Exporting documents containing text
- Custom fonts and font libraries
- Forcing a PDF document to embed the standard fonts
Drawing text is another very common operation that you may want to perform. This is achieved by using the FillText
and StrokeText
methods (as usual, separate methods are used to fill and stroke). Both of these methods require a Font
parameter, that determines the font that is used to draw the text.
Note: the text features in VectSharp have sort of a Western bias, in the sense that they were designed with a left-to-right text layout in mind and expecting a Latin script. They might work fine with other writing systems/scripts, but unexpected issues may occur at any point.
Unfortunately, I am not familiar with other writing systems, and as a result I cannot provide a reliable implementation (essentially, I would not be able to recognise if something is wrong!). However, if you have any suggestions or comments on specific situations, feel free to open an issue in the GitHub repository!
Fonts and font families
A Font
object can be created by providing a FontFamily
and specifying a font size:
A FontFamily
, such as Times Roman
or Helvetica Bold
, contains information about the shape of the letters (or, “glyphs”) that will be drawn, which is scaled using the font size. Note that VectSharp treats version of the same font that differ in style or weight (e.g., Helvetica
, Helvetica Bold
and Helvetica Oblique
) as completely different entities.
A FontFamily
can be initialised either by providing the path to a TTF file, or by leveraging one of the fourteen “standard fonts” embedded within VectSharp. These correspond to the standard fonts of the PDF format and are available in the FontFamily.StandardFontFamilies
enum. They include:
Helvetica
,HelveticaBold
,HelveticaOblique
andHelveticaBoldOblique
, representing a sans-serif font.TimesRoman
,TimesBold
,TimesItalic
andTimesBoldItalic
, representing a serif font.Courier
,CourierBold
,CourierOblique
andCourierBoldOblique
, representing a monospaced font.Symbol
, which contains symbols such as greek letters.ZapfDingbats
, which contains various ornamental symbols.
VectSharp embeds an open-source metrics-compatible version of each of these fonts.
When creating a PDF document using VectSharp.PDF, these standard fonts are not embedded within the document (unlike fonts specified via a TTF file), which results in a reduced file size; when the file is opened with a PDF viewer, the program uses its own variants of the standard fonts to typeset the text. Since these fonts all have the same metrics (i.e., glyph width), the result should always look more or less the same.
When an SVG image is created with VectSharp.SVG, instead, a subset of each font that has been used is embedded within the document, including only the glyphs that have been actually used. This is also done for PDF documents with font families that have been specified using a TTF file. This ensures that the text will actually look the same when the file is opened.
This behaviour can be changed by providing a value for the textOption
parameter when exporting the SVG/PDF. This will be explored in more detail later.
A FontFamily
object can be created by invoking the FontFamily.ResolveFontFamily
static method. This method accepts a single parameter, which can be:
- The path to a TTF file (as a
string
), e.g./path/to/font.ttf
. - The name of a standard font as a
string
, e.g.Helvetica
orTimes Roman
. - An element of the
FontFamily.StandardFontFamilies
enum, e.g.FontFamily.StandardFontFamilies.HelveticaBold
.
Note that successive calls to the FontFamily.ResolveFontFamily
method with the same argument will return the same object (i.e. FontFamily.ResolveFontFamily("Helvetica") == FontFamily.ResolveFontFamily("Helvetica")
is true
).
Filling and stroking text
Once you have a FontFamily
and you have used it to create a Font
, you are ready to draw text using the FillText
or StrokeText
methods. In addition to the usual parameters to specify the fill and stroke colours and the stroke options, these methods have the following specific parameters:
- The coordinates of the origin of the text, specified either as a two
double
s, or as a singlePoint
. - The text to draw (as a
string
). - The
Font
to use to draw the text.
The following example demonstrates how a simple text string can be drawn:
Positioning text
Precisely positioning text is not an easy task. Normally, when you use the StrokeText
or FillText
methods, the origin of the text (i.e., the point that you specify) is interpreted as the top-left corner of the bounding box of the text as it is drawn, including all ascenders, descenders, and bearings.
This can be inconvenient if you wish to draw multiple text strings one after the other. For example, if you draw a lowercase c
next to an upper case T
providing points with the same y coordinate, you will notice that they are not aligned:
Expand code
You can change this behaviour by supplying the optional textBaseline
parameter. This is a value of the TextBaselines
enum, which includes:
TextBaselines.Top
, which specifies the default behaviour.TextBaselines.Bottom
, which specifies that the y coordinate of the text position should correspond to the bottom of the text (including any descenders).TextBaselines.Middle
, which specifies that the y coordinate of the text position should correspond to the vertical midpoint of the text (i.e., halfway betweenTextBaselines.Top
andTextBaselines.Bottom
).TextBaselines.Baseline
, which specifies that the y coordinate of the text position should correspond to the baseline of the text, i.e. the line along which the text is drawn.
The following image shows the distinction between these baselines.
The following example shows how to use TextBaselines.Baseline
to correctly align the T
and c
from the previous example:
Precise text positioning
By leveraging the textBaseline
parameter, you can specify the position of one of four “anchors” on the text: the top-left corner, the bottom-left corner, the left-midpoint, and the left-baseline. What all these points have in common is that they refer to the left side of the text.
If you want to have more control over the horizontal position of the text, you can use the MeasureText
method of the Font
object; this method returns a Size
containing the width and height that the text will have when rendered:
You can then use the Width
property of the Size
to align the text.
To position the text even more precisely, you can use the MeasureTextAdvanced
method of the Font
object instead. This returns a Font.DetailedFontMetrics
object with a number of interesting properties:
-
Width
andHeight
are the same as returned by theMeasureText
method - the overall width and height of the rendered text. -
Top
is the distance between the baseline of the text and the highest point of the tallest glyph in the text (i.e., the distance between the greenBaseline
and the blueTop
in the figure above). This is normally $> 0$. -
Bottom
is the distance between the baseline of the text and the lowest point of the longest descender in the text (i.e., the distance between the greenBaseline
and the orangeBottom
in the figure above). For text containing glyphs that extend below the baseline (such as thep
inVectSharp
), this is $< 0$.As a result, given the vertical position of the baseline $b$, you can use these values to compute the position of the top $t$, bottom $u$ and middle $m$ of the text:
\[t = b - \mathrm{Top}\] \[u = b - \mathrm{Bottom}\] \[m = \frac{t + u}{2} = b - \frac{\mathrm{Top} + \mathrm{Bottom}}{2}\]
The LeftSideBearing
, RightSideBearing
and AdvanceWidth
properties are a bit more complicated, and explaining their meaning requires a bit more of an introduction about how text is typeset. They are described in the section below.
Details
A text string consists of a number of “glyphs”. Conceptually, you can image each glyph as corresponding to a letter. Drawing a text string means positioning the various glyphs on the image one next to the other.
For example, assuming a left-to-right writing direction, to write the string VectSharp
you would:
- Starting at the specified point, write the
V
. - Move a bit to the right and write the
e
. - Move again to the right and write the
c
. - Etc…
How much should you move to the right? Unless you are using a monospaced font, this varies with each glyph (e.g., an i
is narrower than an m
) and is the “advance width” of the glyph. The AdvanceWidth
property of the Font.DetailedFontMetrics
object is simply the sum of all the advance widths of the glyphs in the string1.
However, the actual width of a glyph may be larger than its advance width. As a result, part of the glyph will “hang” over the previous glyph (on the left) or the next glyph (on the right). The amount of glyph that hangs to the left is the “left-side bearing”, while the amount that hangs to the right is the “right-side bearing”.
The LeftSideBearing
property of the Font.DetailedFontMetrics
object corresponds to the left-side bearing of the first glyph in the text string (which determines the left-side bearing of the whole string). If the first glyph overhangs to the left (e.g. an italic letter such as f), this has a positive value.
Similarly, the RightSideBearing
property corresponds to the right-side bearing of the last glyph in the text string. Again, this has a positive value if the glyph overhangs to the right.
Drawing underlined text
The text you want to draw may need to be underlined. Unlike bold and italic variants, underlined fonts are not specified as separate files; instead, the Font
constructor takes an optional bool
argument that determines whether the text is underlined or not.
All Font
objects have an Underline
property, which is a Font.FontUnderline
describing how the text is underlined. If the font was initialised without an underline, this property will be null
. Instead, if you created the font like above, you can access the properties of the Underline
object. These include:
Position
andThickness
, which specify the position and thickness of the underline as a multiple of the font size (e.g., a thickness of0.0625
will result in an underline that is1
unit thick with a font size of16
, and2
units thick with a font size of32
). The default values for these properties are determined from the font file.LineCap
, which determines the shape of the ends of the underline (butt, round or square).SkipDescenders
, which is abool
that determines whether the underline is drawn as a continuous line, or if it breaks in order not to overlap glyphs that fall below the baseline (which is the default).FollowItalicAngle
: if this is true, for italic fonts the ends of the underline are slanted by the same angle as the font (try it in the example below).
The following example illustrates the effect of these settings.
If you want to have finer control over the appearance of the underline, you can draw the text and its underline separately. For example, this makes it possible to have an underline of a different colour than the text.
To do this, you need to create two Font
s: one with the underline, and one without the underline. Then, you draw the text using the FillText
/StrokeText
method with the font without the underline, and use the FillTextUnderline
/StrokeTextUnderline
method to only draw the underline. The following example shows how to do this.
Drawing text along a path
Normally, text is drawn on a horizontal line. However, using VectSharp you can also make the text flow along a GraphicsPath
instead. This is achieved with the FillTextOnPath
and StrokeTextOnPath
methods.
Instead of a Point
, the first parameter of these methods is the GraphicsPath
on which the text is drawn. These methods also have two optional parameters that determine the alignment of the text on the path:
anchor
determines the anchoring of the text to the path (i.e. whether the anchor is on the left side of the text, on the right side, or at the centre).reference
is adouble
that determines the position of the anchor with respect to theGraphicsPath
. This value ranges from0
to1
, where0
represents the start of the path and1
represents the end of the path.
For example, setting the anchor
to TextAnchors.Left
and the reference
to 0
(which is the default) will align the left side of the text with the start of the path. Instead, setting anchor
to TextAnchors.Center
and the reference
to 0.5
will align the centre of the text with the centre of the path. Finally, an anchor
of TextAnchors.Right
and a reference
of 1
will align the right side of the text with the end of the path.
The following example shows how to draw text along a path.
Note that if you try to make the text flow on a path with very narrow curves or cusps, the results might be not very appealing. Therefore, this feature is best used with gentle paths.
Drawing formatted text
In addition to drawing simple text strings, you can also draw text with more complicated formatting. This is achieved by using the overloads of the FillText
and StrokeText
methods that take a IEnumerable<FormattedText>
parameter for the text
instead of a string
.
A FormattedText
object represents a single piece of text with a uniform formatting. An IEnumerable
of these objects thus represents a collection of text spans with different formattings. The easiest way to create such a collection is to use the FormattedText.Format
method; this method takes a string
as an parameter and parses formatting information in order to produce an IEnumerable<FormattedText>
.
When you supply a string to FormattedText.Format
, you can specify the format of the string using HTML-like tags. For example, "This text is <b>bold</b>"
would be transformed in a collection of two items: one with text "This text is "
and a regular font, and one with text "bold"
and a bold font.
The following format specifiers are currently supported:
<b></b>
or<strong></strong>
produces bold text, e.g.This text is <b>bold</b>
becomes: “This text is bold”.<i></i>
or<em></em>
produces italic text, e.g.This text is in <i>italics</i>
becomes: “This text is in italics”.<u></u>
produces underlined text, e.g.This text is <u>underlined</u>
becomes: “This text is underlined”.<sup></sup>
produces a superscript, e.g.This text is <sup>superscript</sup>
becomes: “This text is superscript”.<sub></sub>
produces a subscript, e.g.This text is <sub>subscript</sub>
becomes: “This text is subscript”.<#col></#>
, wherecol
is a CSS colour specification (e.g.rgb(0, 178, 115)
or009E73
) produces coloured text, e.g.This text is <#009E73>coloured</#>
becomes “This text is coloured”.
Naturally, you can nest multiple tags; for example This text is <b>bold and <#009E73>coloured</#></b>
becomes: “This text is bold and coloured”.
The FormattedText.Format
method has two overloads. The first, in addition to the string
to format, takes a standard font family name as an additional parameter. VectSharp then internally maps the standard font family to its bold and italic variants. This overload also requires the font size as a parameter. Alternatively, the other overload of this method takes four Font
s as parameter: these will be used to render, respectively, regular text, bold text, italic text and bold-italic text. This is useful if you wish to use a non-standard font collection.
Both parameters have two optional arguments: one is a boolean that determines whether the text is initially underlined (this is roughly equivalent to wrapping the whole string between <u></u>
, and the other is the colour that is used to draw text that does not specify a colour through the <#col></#>
tags (if this is not provided, it is determined by the colour provided in the FillText
/StrokeText
call).
You can determine the size that a IEnumerable<FormattedText>
would have when rendered by using the Measure
extension method provided on this collection. This produces a Font.DetailedFontMetrics
object that is analogous to the one produced by the Font.MeasureTextAdvanced
method for a simple string.
The following example shows how to render some formatted text.
Text spacing
The FormattedText
constructor, the FormattedText.Format
method, and some overloads of the FillText
/StrokeText
methods can take an optional spacing
parameter, which is TextSpacing
instance that specifies the amount of spacing between consecutive characters or words. The constructor for the TextSpacing
struct takes up to four double
parameters, i.e. nonWhitespaceScale
, nonWhitespaceAdd
, whitespaceScale
, whitespaceAdd
; these are used to specify a linear transformation for the advance width of each glyph.
The parameters with nonWhitespace
in the name are applied to non-whitespace characters (i.e., the ones for which char.IsWhitespace
returns false
), while the parameters with whitespace
in the name are only applied to whitespace characters (if you use the constructor that only takes two parameters, these are used for both whitespace and non-whitespace characters). In this way you can independently change the spacing between and within words.
If the “normal” advance width of a glyph is $w$, the final advance width is computed as $w’ = w * s + a$, where $s$ is the Scale
parameter and $a$ is the Add
parameter. By changing the value of $s$, you can increase or decrease the spacing proportionally to each glyph’s width, while changing $a$ allows you to alter it by a fixed amount. Naturally, the default value for the Scale
parameters is 1
, while for the Add
parameters it is 0
.
The following example shows the effect of changing the value of these parameters:
Adding text to a GraphicsPath
Finally, in addition to directly drawing the text on the Graphics
object, you may also want to add it to a GraphicsPath
instead. To do this, you can use the AddText
, AddTextOnPath
and AddTextUnderline
methods of the GraphicsPath
object. These work in the same way as the FillText
, FillTextOnPath
and FillTextUnderline
methods, and take the same arguments (of course, they do not require parameters to determine the fill or stroke colour or the stroke options).
Adding the text to a GraphicsPath
can be useful if you wish to perform some post-processing using the advanced path functions, which are described in another section. It can also be necessary in order to use the text as a clipping path (again, described in another section).
Exporting documents containing text
When you use the SaveAsSVG
or SaveAsPDF
methods to create PDF or SVG documents, you can use the optional textOption
parameter to determine what should happen to text that has been drawn on the page.
VectSharp.SVG
For the SaveAsSVG
method, the SVGContextInterpreter.TextOptions
enum has four possible values:
EmbedFonts
specifies that the whole TTF file of the font will be embedded within the SVG file.SubsetFonts
specifies that only a subset of the TTF file will be embedded, containing the glyphs that have actually been used in the image. This is the default.DoNotEmbed
specifies that no font file should be embedded in the document. When you open the SVG file, if you have the font installed in your system everything should be fine; otherwise the text may be drawn using a different font.ConvertIntoPaths
converts all text elements into paths, thus obviating the need for an embedded font file.
If you plan to edit the text included in the SVG file later, it might be appropriate to use the EmbedFonts
option, so that the full font is available and you do not have any missing glyphs. On the other end, if you wish to edit the SVG file without modifying the text, using the ConvertIntoPaths
options might be better, because some editors (such as Inkscape) do not support embedded font files.
The default setting (SubsetFonts
) is recommended if you do not plan to alter the SVG image after creating it with VectSharp.
VectSharp.PDF
For the SaveAsPDF
method, the PDFContextInterpreter.TextOptions
enum has two possible values, SubsetFonts
(the default) and ConvertIntoPaths
, which are equivalent to the options with the same name in VectSharp.SVG.
However, an important note is that if you use the standard fonts, the font file will not be embedded within the PDF, even if SubsetFonts
is selected. This is because a metrics-compatible version of the standard fonts is supposed to be available with any PDF viewing program. If you wish to make sure that the document looks exactly the same in any viewer, you may want to use the ConvertIntoPaths
option instead.
An important note is that for both VectSharp.SVG and VectSharp.PDF, using the ConvertIntoPaths
option means that the text is not recognised as such by viewer programs any more: therefore, it will not be possible e.g. to search, highlight or copy text from the document.
It is also possible to force the PDF document to include a standard font file.
VectSharp.Canvas
The PaintToCanvas
and PaintToSKCanvas
methods of VectSharp.Canvas also have an optional textOption
parameter.
The AvaloniaContextInterpreter.TextOptions
enum has three possible values:
AlwaysConvert
specifies that text should always be converted into paths before being drawn on the AvaloniaCanvas
.NeverConvert
specifies that text should never be converted into paths when it is drawn on theCanvas
.ConvertIfNecessary
(the default) specifies that text should only be converted into paths if it is necessary to preserve its appearance.
For the PaintToCanvas
method, converting the text is necessary if it is drawn using a non-standard font. If you use the NeverConvert
option in this case, the text may be drawn by Avalonia using a default font instead. If you use the AlwaysConvert
option when text is only drawn using standard fonts, this will result in reduced performance.
For the PaintToSKCanvas
method, converting the text is only necessary on Linux (because the SkiaSharp method to draw text produces a really ugly result).
Custom fonts and font libraries
In addition to using the 14 standard fonts, you may also want to create text using your own custom fonts. As explained above, this can be achieved by providing the FontFamily.ResolveFontFamily
method with the path to a TTF file; alternatively, you can also create a FontFamily
from a TTF file that has been loaded in a Stream
. This is useful, for example, if you wish to include your font as an embedded resource, or if you want to force the PDF exporter to embed one of the standard font files.
Note: always make sure that the font files you are using are in TTF format and do not use any OpenType features that are not available in TrueType (except for kerning information in the
GPOS
table). If you download a variable font file from Google Fonts, use the “static” font files instead (these are available in thestatic
subfolder when you download the font family).It is also a good idea to try opening the files you created on a system where the fonts you used are not installed, before publishing them.
In any case, always make sure that you comply with the terms of each font’s license!
To create a FontFamily
from a Stream
, you can use the constructor that takes a Stream
as its only parameter (unlike other constructors, this one is not deprecated):
However, if you export a PDF document with a font family that was created in this way, you may find out that the font is not included, and it is replaced with a default font instead. To avoid this, you need to create a new font library to replace the default font library, and add the new font family to it.
A font library is an instance of a class implementing the IFontLibrary
interface. This interface defines a number of overloads for the ResolveFontFamily
method; two of these simply resolve a font family from a string
or a FontFamily.StandardFontFamilies
enum, while the others make it possible to resolve a font family providing fallback options. A default implementation for the latter is provided by the abstract class FontLibrary
(in short, if you wish to create a new IFontLibrary
implementation, it is easier to inherit from FontLibrary
than to directly implement IFontLibrary
).
VectSharp currently contains two implementations of the IFontLibrary
interface: the DefaultFontLibrary
and the SimpleFontLibrary
.
The DefaultFontLibrary
class (which, unsurprisingly, is the default font library when nothing else is specified) implements a font library that resolves the standard font families using the font files embedded with VectSharp. It can also create a FontFamily
from a TTF file. However, this library cannot be extended, in the sense that you cannot add to this library a FontFamily
that was created in other ways (e.g. from a Stream
).
The SimpleFontLibrary
class, instead, provides this possibility. Once you add a FontFamily
to this font library, you can then resolve it using its name (e.g., "Open Sans"
).
Note: if you add multiple fonts from the same family (e.g., Open Sans Regular and Open Sans Bold), you will need to use the full name of the font family to ensure that you get the correct variation! If you are in doubt, you can check the
FontFamily
’sFamilyName
property to determine the name that you should use.
The following example shows how to use a SimpleFontLibrary
to use custom fonts.
Normally, you will want to create the SimpleFontLibrary
and load all the custom fonts at the start of your program. Then, when you actually need the font families, you invoke the FontFamily.ResolveFontFamily
method with the font family name.
Using font libraries like this opens up a few interesting possibilities. For example, the VectSharp.Fonts.Nimbus package provides replacements for the 14 standard fonts (these were the fonts that were included in the original GPL release of VectSharp; since the library moved to LGPL, a different set of fonts had to be used). To use these fonts, after installing the NuGet package, you need to set the Nimbus font library as default:
Other possibilities for future development include creating a font library that resolves font families from Google Fonts and downloads them as necessary, or one that loads fonts that are installed on the system (provided that there is a cross-platform way to access them).
Forcing a PDF document to embed the standard fonts
Normally, when you create a PDF document, the 14 standard fonts are not embedded, and the PDF viewer program that is used to open the file uses its own standard fonts to display the text. However, you may wish instead to force the library to embed the standard fonts.
The way to do this is to “trick” the library into thinking that you are not using one of the standard fonts. You can achieve this by creating a new font family using the stream of one of the standard fonts. Note that you will also have to follow the font library pattern described above.
The following example shows how to access the stream of one of the standard fonts and how to use it to create a new FontFamily
.