I have moved!

I've moved my blog
CLICK HERE

Thursday 3 July 2008

GlyphRun and So Forth

In WPF there are a number of ways to get text painted, ranging from Label at the simple end, all the way down to GlyphRun if you want to get your hands dirty.

The latter stuff seems very thinly documented. Want to know what the bidiLevel parameter to the constructor of GlyphRun means? According to the documentation, it is "a value of type Int32". Very helpful.

This is a problem because it is your only option if you want to be able to control the positions of individual characters. The layer above is based around the FormattedText class, but it adds wrapping and rich text support, which you may not need, and exposes no ability to individually adjust the character positions.

After fooling around for three hours and (this was the crucial part) stepping through the WPF source code to see how FormattedText does it, I came up with this, which is basically the sample I wish I'd had in the first place. It demonstrates that in fact working directly with the Glyph-related classes isn't too complicated after all.

It draws the text "Hello, world!" to a DrawingContext, with the standard character spacing.

Typeface typeface = new Typeface(new FontFamily("Arial"), 
                                FontStyles.Italic, 
                                FontWeights.Normal, 
                                FontStretches.Normal);

GlyphTypeface glyphTypeface;
if (!typeface.TryGetGlyphTypeface(out glyphTypeface))
    throw new InvalidOperationException("No glyphtypeface found");

string text = "Hello, world!";
double size = 40;

ushort[] glyphIndexes = new ushort[text.Length];
double[] advanceWidths = new double[text.Length];

double totalWidth = 0;

for (int n = 0; n < text.Length; n++)
{
    ushort glyphIndex = glyphTypeface.CharacterToGlyphMap[text[n]];
    glyphIndexes[n] = glyphIndex;

    double width = glyphTypeface.AdvanceWidths[glyphIndex] * size;
    advanceWidths[n] = width;

    totalWidth += width;
}

Point origin = new Point(50, 50);

GlyphRun glyphRun = new GlyphRun(glyphTypeface, 0, false, size, 
    glyphIndexes, origin, advanceWidths, null, null, null, null, 
    null, null);

dc.DrawGlyphRun(Brushes.Black, glyphRun);

double y = origin.Y;
dc.DrawLine(new Pen(Brushes.Red, 1), new Point(origin.X, y), 
    new Point(origin.X + totalWidth, y));

y -= (glyphTypeface.Baseline * size);
dc.DrawLine(new Pen(Brushes.Green, 1), new Point(origin.X, y), 
    new Point(origin.X + totalWidth, y));

y += (glyphTypeface.Height * size);
dc.DrawLine(new Pen(Brushes.Blue, 1), new Point(origin.X, y), 
    new Point(origin.X + totalWidth, y));
 
Among the major points of interest:
  • It seems to be a widespread believe that to get a GlyphTypeface, you have to pass the actual file path of the font file on your computer to the constructor. But in fact you can get a GlyphTypeface from a Typeface, if you use a method call that, if you search for it on Google, appears to have only ever been used directly by approximately four people. But it works, because FormattedText uses it.
  • You have to turn each character of the string into a different number called a glyph index. But this is just a matter of looking them up in the typeface's CharacterToGlyphMap property.
  • It is mandatory for you to specify the width of each character. I do this above by filling in the array of widths and just taking the standard width from the GlyphTypeface, but at this point you could do anything you like to the widths.
  • If you want to align the text, you have to figure it out yourself. The origin given to the GlyphRun constructor is the position of the left end of the text and the baseline. The GlyphTypeface tells you about the significant vertical lines (I draw three of them at the end). And you can figure out the total width as you set up the array of individual widths, like I do.
  • There is no built-in support for underline or strikethrough. If you want these, you have to paint them as a very thin rectangle (seriously - it's what FormattedText does). The typeface contains the suggested place to draw these decorations in properties such as UnderlinePosition and UnderlineThickness.