In my previous OpenFL and Lime devlogs, I’ve mostly talked about the technical details of implementing new features in OpenFL. Not this time… but don’t worry! There’s more of that coming soon. I have a couple new features currently in development that I’m planning to write about once I get them committed to the Git repository. However, today, I thought that I would share my journey fixing some text issues that popped up recently in Lime 8.2.
If you’re not familiar, OpenFL is an implementation of the APIs available in Adobe Flash Player (and Adobe AIR) using the Haxe programming language. Projects built with Haxe can be cross-compiled to JavaScript, C++, and many other targets — making it available on the web, desktop apps, mobile apps, game consoles, and more. Lime is a dependency of OpenFL, developed by the same team, and it exposes various native C++ libraries to Haxe (such as SDL, FreeType, and various audio and image format decoders).
Some fonts render incorrectly in Lime 8.2.0
Soon after the release of Lime 8.2.0, some users reported that certain fonts weren’t rendering correctly. Many fonts were fine, though, which is how this got missed in testing. The fonts that didn’t render correctly were resulting in lines that were very close together, with almost zero gap in between them.
My initial investigation showed that the broken fonts had different ascent and descent values compared to Lime 8.1.3 (I’ll go into detail about what those terms mean in a little bit).
Interestingly, I also discovered that when compiling with Lime’s flash/air targets, the same fonts (when embedded into the SWF generated by Lime) were getting rendered incorrectly in Flash Player or AIR. Obviously, we couldn’t have replaced Flash’s C++ text rendering code with our own (even accidentally), so that’s pretty strange. When Lime embeds a font for Flash, it manually writes the binary SWF tags that tell Flash how to render each of the font’s characters, and how lines should be laid out relative to each other. That code in Lime that writes binary SWF tags shares something in common with OpenFL’s text rendering code: Both use the open source FreeType library to parse and read the font files.
We had updated FreeType to version 2.10.0 in Lime 8.2.0. Simply put: the new FreeType was the obvious culprit for this bug. Rolling back to an older version of FreeType fixed the issue, and that’s what we did in Lime 8.2.1. That resulted in a different letter spacing issue, though, and it could have been fixed by downgrading FreeType even further. I didn’t want to do that.
It would be nice to be able to update FreeType from time to time to get the latest bug fixes and enhancements. I wanted to know why updating FreeType changed the line spacing, and if there was something I could do to restore the behavior from the older FreeType version with that newer FreeType version so that OpenFL text rendering would more closely match Flash Player and AIR, which behaves more like the older FreeType. So my investigation continued, and I fell down quite the rabbit hole on the subject of text rendering.
A bit of terminology about text line metrics
To get everyone on the same page, let’s define some terms used when measuring and laying out text, including the ones that I promised to define earlier. The image below is from the Adobe Flash documentation, and was licensed CC-BY-SA 3.0 by Adobe.
First, let’s talk about the baseline. The baseline is where the bottom of most characters will align when you position them next to each other. In the picture below, the bottom of the W, the bottom of the circular part of the q, and at the bottom of the x are all resting on the baseline (drawn horizontally in red from the far left side of the image to the far right side).
The ascent of a text line is the distance from the baseline to the highest point of the line. In the picture above, the ascent is drawn vertically in green to the right of the x character. It starts at the baseline and goes a bit above the height of the W.
Some characters are partially rendered below the baseline, such as lower case g, y, or j. The descent is the distance from the baseline to the lowest point of the line. In the picture above, the descent is drawn vertically in green a bit below and to the right of the ascent. It starts at the baseline and extends a bit below the q.
Leading or line gap is the spacing between lines. In the picture above, the leading is drawn vertically in green a bit below and to the right of the decent. It starts at the descent of the first line and extends to the top of the ascent of the second line.
Line height is distance from the baseline of one line to the baseline of the next. This is a value that may be customized by the font designer based on aesthetic taste. In the picture above, it is drawn vertically in green on the left of the first line. In OpenFL, and Adobe Flash before it, the line height is typically expected to be equal the sum of the ascent, descent, and leading/gap.
How do font files store text line metrics?
So it might be a little bit weird, but values like ascent and descent may be stored in three (possibly even more, for all I know…) different places in a font file. Even weirder, each of those different places may store a completely different value than the others. What?!
Strange, but true. For ascent, there’s hheaAscender from the hhea OpenType table, typoAscender from the OS/2 OpenType table, and winAscent also from the OS/2 OpenType table. Descent and line gap have similar values in those tables.
So, if there are duplicates with different values, how do you know which one is correct? Well… that’s where things get tricky. Not everyone agrees about which one should be considered correct. WHAT?!
That’s right! Programs like Microsoft Word and Adobe Photoshop don’t necessarily render the same string of text with the same text metrics when using the exact same font file. In other words, Word might select one set of ascent and descent values (such as winAscent and winDescent), while Photoshop selects a different set (such as typoAscender and typoDescender). Even stranger: Word and Photoshop might actually choose the same set of metrics for certain font files.
Basically, Word and Photoshop each have different algorithms for determining which of the metrics from the font file should be selected. These algorithms might depend on whether some of the metrics are missing or not, or if certain flags have been set elsewhere in the font data to indicate that the designer preferred certain metrics over others. Part of why there are multiple sets of metrics is backwards compatibility with older, legacy programs and operating systems that can’t be updated to handle new font format improvements. It’s quite the complex beast!
Customizing FreeType’s algorithm
Upgrading FreeType in Lime 8.2.0 drastically changed the text line metrics passed to OpenFL (and also used by Lime for generating embedded SWF fonts). This is because FreeType changed its algorithm for determining how to choose the correct metrics between the 2.9.1 version and the 2.10.0 version.
As a result, when using FreeType 2.10.0, it’s likely that ascent and descent values will be noticeably smaller than when using FreeType 2.9.1. This isn’t necessarily bad, but OpenFL’s text layout algorithm makes some assumptions about the relationship among line height, line gap, ascent and descent. Those assumptions were safe to make with FreeType 2.9.1’s algorithm, but not with FreeType 2.10.0’s new algorithm. In the future, we might consider upgrading OpenFL’s text rendering code to support both algorithms without issues. However, it’s a non-trivial change, and it may risk introducing new bugs. For the time being, it’s better if we could upgrade FreeType to 2.10.0 or newer, but keep FreeType 2.9.1’s algorithm.
Luckily, FreeType’s algorithm for choosing text line metrics is not set in stone. Developers are welcome to provide their own custom algorithm for choosing metrics, if they desire. With that in mind, it is actually pretty easy to copy the algorithm from FreeType 2.9.1 and use it with FreeType 2.10.0, so that’s exactly what I did in Lime 8.2.2.
For reference, here are links to the original C code used for choosing text line metrics in both versions of FreeType, if you want to see how they’re different.
https://github.com/freetype/freetype/blob/VER-2-9-1/src/sfnt/sfobjs.c#L1633-L1688
https://github.com/freetype/freetype/blob/VER-2-10-0/src/sfnt/sfobjs.c#L1644-L1713
Each snippet includes a comment that explains why they chose to implement the algorithm the way that they did.
And here’s Lime’s new implementation, based on FreeType 2.9.1 (which also includes fallback to FreeType 2.10.0’s implementation, if any metric is missing, for some reason):
https://github.com/openfl/lime/blob/8.2.2/project/src/text/Font.cpp#L575-L622
That’s it! Lime 8.2.2 was released at the end of 2024, so if you’re still in Lime 8.2.0 or 8.2.1, you should probably update Lime to get this fix.
If you’re interested into diving deeper into the topic of font files and their various metrics, I highly recommend reading Glyphsapp Tutorial: Vertical Metrics. In addition to the FreeType documentation, the Glyphsapp tutorial was definitely my most referenced resource while trying to understand how this all worked. It has the right amount of detail, and it carefully guides you through a journey of understanding it all, and I was really impressed.