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.

14 comments:

dumbledad said...

Fabulous post Daniel; so useful for the code I'm writing at the moment. Thank you. When you say "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" would you enumerate all the techniques inbetween?

Daniel said...

What I had in mind was: Label, TextBlock, FlowDocument and its associated viewers, FormattedText, GlyphRun. Arranged in an approximate order where you get more power, but more responsibility, like gradually turning into Spider-Man.

A.V.Ebrahimi said...

Great document about undocumented fact!
I don't know it supports Arabic RTL and GPOS table of font, but worth a try.

A.V.Ebrahimi said...

Just now I checked DrawGlyphRun with Arabic text, seems this routine is just a Glyph Renderer and it doesn't look at special tables in OpenType font, e.g. GPOS, GSUB...

And about the performance, the FlowDocument is still much faster than DrawText (and it's friends).

I wish I had access to FlowDocumentScrollViewer source, so I could tune it to my special needs!

Unknown said...

what's the "dc" in your code sample?

Daniel said...

@electroglyph - sorry I didn't make that clearer. Above the sample it says I'm rendering to a DrawingContext - that's what dc is. You can get one passed to you by deriving your own class Foo from FrameworkElement and overriding OnRender. Then reference your Foo in your XAML to get it to appear on the screen.

Unknown said...

Great, thanks for the explanation Daniel. I had an idea for syntax highlighting by drawing a new GlyphRun over an existing one, do you think that's a terrible idea?

Daniel said...

Depends what you're extending. I've done a syntax highlighting editor by using RichTextBox and simply applying formatting to it whenever the user edits the text. Works okay for small snippets of code. The drawback to working on GlyphRuns is that you may not be passed enough text in each run to figure out how to highlight it. Also you will only be able to overwrite in a different color - bold text would need extra space, so that would be impossible.

Anonymous said...

thank your help.i want to calculate the width of a character,the GlyphTypeface can get it.

AgeKay said...

This is great. I adapted your code and it's 30x faster than the build-in DrawText method. But now I want to draw the text at a 90 degree angle. Do you have an idea on how to do that?

Daniel said...

To rotate the text you would push a RotateTransform onto the DrawingContext by calling PushTransform, before doing the drawing. Afterwards, call Pop on the DrawingContext so the rotation won't apply to any subsequent drawing.

AgeKay said...

Oh right. That is very smart! Thanks so much!

Anonymous said...

My guess is the bidiLevel parameter defines the text orientation. It is derived from Unicode bidi algorithm described in the Unicode standard. Generally if the text you want to display does not contain Unicode control characters and contains only left to right characters + neutral characters this routine will be useful. For the more advanced stuff check out the free C# bidi implementation (you can also try to optimize it to support incremental bidi processing which is kind of hard :))

bhalgatashish said...

I could not digest this immediately.
But a quick question ,
I am basically in need to calculate the height of the line with the specified font in RichText box.
Can we do it using GlyphRun ?