Published by breki on 22 Apr 2010 at 07:56 pm
Kerning Or How To Outfox GDI+
(image taken from Wikipedia)
I’m working on a text-on-path algorithm for Maperitive to be able to render things like street names. GDI+ does not have a built-in function for drawing texts on a path, so I’m stuck with writing my own.
Although there are a lot of implementations of such an algorithm out there, they all miss one important thing: kerning. Kerning is basically telling a character to move a bit closer to its neighbor if there is enough space for them to huddle together:
(image taken from Wikipedia)
Since the path can consist of several line segments (and even curves), you need to render each character (glyph to be more precise) separately to be able to rotate it using the angle of the line segment it will be drawn on. The problem is determining how far from the previous character to place the next character. GDI+ offers a method called MeasureString, but calling it on a single isolated character returns a character width value without any kerning taken into account, so characters like V and A can appear too spread apart. There is another method, MeasureCharacterRanges, which can measure each individual character width for a given text, but it has a practical limit of 32 characters and is pretty processor- and resource-intensive. I’ve used it for the text-on-path algorithm for Kosmos but the results weren’t very satisfactory.
From my (basic) understanding of how kerning usually works on computer typography, a font which has kerning information stores kerning offsets for character pairs. So, for example, V and A will have a different (usually stronger) kerning than say, character pair V and T. The problem with GDI+ is that you don’t really have access to this information.
One option would be to use old GDI’s font and text functions to fetch the information about the kerning pairs, but these need to be P/invoked and thus will probably not work on Mono, which makes them unusable for Maperitive – I’m really trying to make Maperitive run on multiple platforms.
So after spending a day in brainstorming, I got an idea on how to handle kerning myself. It’s really simple: I will calculate the kerning myself (using the mentioned MeasureString method). The formula is simple:
<character kerned width> = <character non-kerned width> – (<character pair non-kerned width> – <character pair kerned width>)
I can get all of the three values on the right of the equation using the MeasureString: first I’ll measure each character’s individual (non-kerned) width and then I’ll measure the width when they are drawn together. The difference between these is the actual kerning, which is then used to calculate the kerned width of the first character.
The added twist to this tale is that I’ll keep this information in a cache, to keep the expensive calls to the MeasureString method to the minimum. This cache could even be persisted to the disk. Of course, each font family and font style dictates its own kerning, so the cache needs to be extended with this information.
One potential problem is how to handle kerning information for different font heights. Since Maperitive supports continuous zooming levels, the number of different font heights used is (almost) limitless. I’ve decided to cut some corners here: the kerning will be calculated (and stored) for some nominal font height (say 100 em) and then interpolated for any needed height. We’ll see how that goes… next time.


dave on 23 Apr 2010 at 16:13 #
What do you plan to do about curved text and kerning? You’ll need to expand some pairs and contract others depending on whether the tops or the bottom of the letters are closer together.
e.g. see http://www.fonts.com/AboutFonts/Articles/fyti/TypeOnACurve.htm
breki on 23 Apr 2010 at 19:21 #
@dave,
Yes, curves are another problem here. But first I need to decide how to introduce curves in Maperitive. Right now I’m using DrawCurve which draws cardinal splines, but the results are sometimes problematic. The solution would probably involve Beziers (something like http://wiki.openstreetmap.org/wiki/Bezier_curves).
The text-on-path solution I’m working on now is less ambitious: I want to achieve some decent text rendering on straight line segments and introduce some simple label placement heuristics into it.
BTW: you can avoid problems with kerning on curved text if you actually warp the characters (not just rotate them like I intend to). See http://www.planetclegg.com/projects/WarpingTextToSplines.html
Aybe on 21 Dec 2010 at 19:21 #
Hello,
First thanks for that great article !
I haven’t found the second part of the article, telling how it went with the height interpolation.
Can you tell how it went ?
Thank you
Aybe on 24 Dec 2010 at 18:30 #
Hi again,
I’ve been busy creating my own font renderer and I think I got it right now. It was almost perfect beside that on some font sizes, the drawing wasn’t exactly the same as when using Graphics.DrawString for drawing the whole text.
I got it working perfectly by using TextRenderer class methods instead of the Graphics class methods (Measuring and drawing). Now the text is perfect and it even looks a little better as TextRenderer actually outperforms GDI+, measurement is more precise than in it, but yes, it’s a little slower …
Here’s an excellent article about it : http://support.microsoft.com/kb/307208
Thanks
breki on 27 Dec 2010 at 10:39 #
@Aybe,
Height interpolation is a bit problematic, I haven’t found a good way to do it with 100% precision, so right now I’m just using the Graphics.MeasureString to calculate the height. Right now the speed is my first priority and the quality the second, so I’m satisfied with the results.
One possible option would be to use Pango: http://www.pango.org/ . I haven’t done much work with it, but from what I remember, it is definitively slower than GDI/GDI+.
Aybe on 27 Dec 2010 at 20:35 #
In fact I was wrong, While TextRenderer is precise, there’s definitely more control with GDI+; and for some reasons, ClearType don’t work with it … it’s ugly.
I have no problems with kerning using AA, but I still have some using the grid-fitted versions, which looks even better. Kerning remains proportional but is a little scaled …
So MeasureString does the job pretty well but right now I am trying with MeasureCharacterRanges, using these regions …
My needs are a little special in fact, I needed to render musical symbols and surprisingly, most pro music fonts are bad, they are mostly formatted with pre-alignment and so on, which makes them unusable at all.
So I started looking out at Windows’ fonts, and they pretty much have all these Unicode symbols, moreover, on many fonts !
I did try many font rasterizers, AngelCode’s BMFont was the only one knowing these symbols, but guess what, kerning isn’t perfect, and I had to write another renderer as well.
Thus, I decided to create everything myself …
Well pretty much the opposite of you, priority here is quality, speed is not really important as it’ll be an offline process
Thanks
ChrisP on 10 Feb 2011 at 12:40 #
How far ist the work on the text labeling algorithm? I recently made a city map of central Munich for a research project with Maperitive. I had to do a lot of manual adjustment in Inkscape which I expected since hardly any of the labeling algorithms in free software are perfect. Especial labels from different layers tend to overlap in case you want not onyl streets labeled. The kerning is a good point. In short streets one needs narrow spacing while on long big streets the spacing between the letters could be large to stretch the label rather than having it labled many times. Also in OSM streets with seperated lanes are recorded as separate path and thus labeled twice which often does not look nice.
Probably a good article to start with when thinking about cartographic labeling is this one:
http://www.merl.com/papers/docs/TR96-04.pdf
breki on 10 Feb 2011 at 13:29 #
@ChrisP,
Label auto-placement is my next big feature I’m already working on. I’m doing a city map for a customer and I’m facing the same issues you’re facing: - label abbreviations: already working, but I need to find a way for users to specify them externally (some text file or something)
label compression: for short streets, the font can be compressed horizontally. This is already working (available in the next release)
label overlapping detection: this will be available soon, labels with lower priority will be hidden if overlapping. This is a first step and it’s much easier to implement than repositioning. Repositioning (like the one described in the article) will be done later.
duplicate labels detection: will be implemented soon.
label stretching & repeating: hopefully soon.
Thanks for the article, I’ve been studying quite a few of them, but this one I haven’t read so far.
headkaze on 19 Aug 2012 at 9:51 #
I couldn’t get accurate kerning data using your idea and Graphics.MeasureString but using TextRenderer.MeasureText I’ve had some success. Infact the results I’m getting are the same as those output by AngleCode’s Bitmap Font Generator which I know for a fact uses the GetKerningPairs GDI function.
// Written by headkaze based on an idea by breki // http://igorbrejc.net/development/c/kerning-or-how-to-outfox-gdi private void CalculateKerningPairs(Graphics g, System.Drawing.Font font, char c) { string sChar = c.ToString(); Size charSize = TextRenderer.MeasureText(g, sChar, font, Size.Empty, TextFormatFlags.NoPrefix | TextFormatFlags.NoPadding);
}