Getting Text Metrics in Firemonkey

Screen Shot 2013-01-06 at 2.11.24 PM

Firemonkey’s abstract TCanvas class has been providing the dimensions of the bounding rectangle of some text on itself. On recent updates it has deprecated providing text rectangle directly in the canvas, in stead encouraged users to use TTextLayout abstract class which handles several text-based operations on the native context of the platforms. So it is possible to get the bounding rectangle of a string on any canvas. Is this enough? Though I am sure this class will improve to involve many new textual features, at current time it lacks some major needs of a developer who designs text drawing applications.

When words of different sizes aligned from top you can't get a proper sentence view

When words in different sizes aligned from top you can’t get a proper sentence view

For the first time, I have faced a feature lack when I saw even TMS’s HTML Text was unable to align words in an HTML notation that have got different sizes. Because having text rectangle is not enough to locate words in different sizes because of the missing baseline information. If you align the words from the top the smaller word’s baseline will be upper (as in the above image), if you align the bottoms the baseline will be lower, so to make a correct match you should know where are the baselines of each word exactly located. So we need to know the Ascent value of the text. If have got the ascent value, we can easily align the words on the baseline as seen on following image. For a complete information on typography see this article on Wikipedia.

For a proper drawing of words in different sizes you should align them on their baselines.

For a proper drawing of words in different sizes you should align them on their baselines.

As seen on the above image, having the information of ascent values for each word we can align them properly on the baseline of the sentence. However FMX TTextLayout doesn’t provide this information. So I decided to add a new function to my PlatformExtensions unit to provide metrics information that are gathered from the drawing of that specific font in a specific size. The new function is GetTextMetrics and hast this following structure.


Class Procedure GetTextMetrics(Text:String; Font:TFont; var TextRect:TRectF;
                               var Ascent,Descent:Single;
                               var CapHeight,XHeight:Single);virtual;abstract;

Getting Text Metrics in Windows

Firemonkey uses two different context types on Windows platform that are Windows GDIP and Direct2D. The default one is Windows GDI plus context which is a set of device independent drawing API based on the old GDI. Because of its device independency GDIP lacks of some important drawing features such as XOR brush, but it is always possible to handle these extra operations by locking the context to device dependent HDC and doing things on the device bitmap. What I mean is; getting font metrics is not as easy as it was old GDI based on HDC, so I used the limited information of the GDIP and estimated some others like (x height  of text) by using statistical multipliers. However someone other can collapse the context to HDC level and calculate more detailed font metrics on that level. I didn’t prefer it to be consistent with the design logic of GDIP, and used TGPFontFamily class of GDIP to get the information about the font. Note that the metrics values gathered from this class is in logical units so should be converted to pixels using the proportions.

Here are my calculations:

fSize := FGPFont.GetHeight(FGraphics);
cAscent := FGpFamily.GetCellAscent(FontStyle);
cDescent := FGpFamily.GetCellDescent(FontStyle);
emHeight := FGpFamily.GetEmHeight(FontStyle);
lSpacing := FGPFamily.GetLineSpacing(FontStyle);
iLeading := cAscent+cDescent - emHeight;
eLeading := lSpacing - cAscent-cDescent;
Ascent := fSize * (cAscent)/lSpacing;
Descent := fSize * (descent)/lSpacing;
CapHeight := fSize * (cAscent-iLeading)/lSpacing;
XHeight :=  fsize * (cAscent-iLeading) *(7/10) /lSpacing;

Note that the calculations use TGPFont, TGPFontFamily and TGPGraphics classes to deal with font metrics. The FGPFont and FGPGraphics objects are not accessible through standard class hierarchy, however we can use RTTI functions to access them.


RttiField := RttiContext.GetType(Layout.ClassType).GetField('FGPFont');
if assigned(RttiField) then
begin
  FGPFont := TGpFont(RttiField.GetValue(Layout).AsObject);
  RttiField := RttiContext.GetType(Layout.ClassType).GetField('FGraphics');
  FGraphics := TGpGraphics(RttiField.GetValue(Layout).AsObject);

  FGPFamily := TGPFontFamily.Create;
  FGPFont.GetFamily(FGPFamily);
  // Get Metrics
  // ...
end;

Another point to be mentioned here is that, above calculations work only if the DefaultCanvas is a GDIP canvas. If the developer selects to use the D2D canvas, because of the absence of the FGPFont object in RTTI the text metrics will not be updated. Since I don’t have any experience on Direct2D I haven’t worked on it, but I will be happy if someone does it and send me so that I can put it in the class code by giving credits to his/her name.

Getting Text Metrics in Mac OS X

Things are always easier for programmers in Mac side. You can see below how it is easy getting font metrics using CoreText API of Cocoa.

LFontRef := CTFontCreateWithName(CFSTR(Layout.Font.Family), Layout.Font.Size, nil);
Ascent := CTFontGetAscent(LFontRef);
Descent := CTFontGetDescent(LFontRef);
CapHeight := CTFontGetCapHeight(LFontRef);
XHeight := CTFontGetXHeight(LFontRef);
CFRelease(LFontRef);

You can see all the details of using TTextLayout class and how to extend it to get Text/Font Metrics in my PlatformExtensions units. You can get the source code of the PlatformExtensions classes with the demo application from this SVN link. For non-programmers the compiled Win32, Win64, MacOSX (Thanks to Firemonkey) applications are also available to download.

3 thoughts on “Getting Text Metrics in Firemonkey

  1. Philip

    Another approach that should work for all platforms and graphics contexts is to use Canvas.TextToPath to create a TPathData object, then step through the data points of the path checking for the minimum and maximum co-ordinates. Doing this separately for selected characters, such as x (for x-height), k (for ascenders), etc. and judicious use of alignment allows you to work out most (if not all) of the metrics one would need.
    I have tried this for Windows XP with Delphi XE2 with good results, but have not tried other cases. It may not be quite as good as the design specs, for the typeface but should be pretty close.

    Reply
  2. Philip

    The position of the baseline is just as easy as most other measurements. Pick a suitable flat-bottomed letter without descenders (e.g. A, m, T, x), and find the maximum y co-ordinate of the data points. That’s the baseline relative to the top of the TLabel.

    Reply

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s