Hello! I've never been particularly satisfied with the font rendering in JavaFX. In particular, on Linux, the text always appears very soft and blurry compared to non-JavaFX applications on the same system. Even applications that render antialiased text with Java2D seem to look better.
I decided to take a look into it to see if anything could be done about this, and I have some questions. I'm only looking at the Freetype implementation in Prism currently, as that's all I can realistically test on at the moment. For reference, here's how text rendered at 16px using Terminus TTF looks today: https://ataxia.io7m.com/2023/12/12/hinting_nobitmaps_normal.png I'm not sure if I'm alone on this, but I find this almost migraine- inducing. No other application on my system, including those that use Freetype, using that font at that size will render as blurry as that. Looking at modules/javafx.graphics/src/main/java/com/sun/javafx/font/freetype/FTFo ntFile.java, I see this in initGlyph(): ``` int flags = OSFreetype.FT_LOAD_RENDER | OSFreetype.FT_LOAD_NO_HINTING | OSFreetype.FT_LOAD_NO_BITMAP; ``` Additionally, the code might also add the FT_LOAD_TARGET_NORMAL or FT_LOAD_TARGET_LCD flags later, but I'll assume FT_LOAD_TARGET_NORMAL for the sake of avoiding combinatorial explosions in testing at this point. Essentially, we discard hinting information, and we discard bitmap information. I'm not sure why we do either of these things. I decided to try different combinations of flags to see what would happen. Here's FT_LOAD_RENDER | FT_LOAD_NO_BITMAP (no bitmaps, but using hinting data): https://ataxia.io7m.com/2023/12/12/hinting_nobitmaps_normal.png That's no real improvement. Here's FT_LOAD_RENDER | FT_LOAD_NO_HINTING (ignore hinting data, but use bitmaps if they are included): https://ataxia.io7m.com/2023/12/12/nohinting_bitmaps_normal.png That, to my poor suffering eyes, is already a _vast_ improvement. Let's try including both hinting and bitmaps (FT_LOAD_RENDER): https://ataxia.io7m.com/2023/12/12/hinting_bitmaps_normal.png Inspecting that in an image editor shows the pixels of the text to be identical. So, clearly, Terminus TTF includes bitmaps for smaller text sizes. Let's try another font such as Droid Sans that renders crisply at ~10pt sizes on my system, and that I'm reasonably confident doesn't include any bitmaps. Here's the JavaFX default (FT_LOAD_NO_HINTING | FT_LOAD_NO_BITMAP): https://ataxia.io7m.com/2023/12/12/droid_12_nohinting_nobitmaps.png That's pretty nasty. Let's enable hinting (FT_LOAD_NO_BITMAP): https://ataxia.io7m.com/2023/12/12/droid_12_hinting_nobitmaps.png That's already a lot better. If you overlay the two images in an image editor, it's clear that the glyph shapes are not quite the same (with hinting, some glyphs are ever-so-slightly taller). For completeness, let's allow bitmaps: https://ataxia.io7m.com/2023/12/12/droid_12_hinting_bitmaps.png The rendered glyphs are pixel-identical. Now, most modern desktops have options to disable antialiasing for text under a given size. Antialiasing on 10pt text is rarely an improvement over just not having it as there are so few pixels to work with. I decided to experiment a bit with turning off antialiasing. This requires setting the load target to FT_LOAD_TARGET_MONO so that Freetype returns a monochrome image instead of what amounts to an alpha coverage map. Unfortunately, this does also change the format of the image returned to a 1bpp image instead of an 8bpp greyscale image, and JavaFX isn't equipped to handle that. However, we can do the conversion manually if we see that bitmap.pixel_mode == 1, and then the rest of JavaFX doesn't need to care about it: ``` if (bitmap.pixel_mode == 1) { byte[] newBuffer = new byte[width * height]; for (int y = 0; y < height; y++) { final var rowOffset = y * width; for (int x = 0; x < width; x++) { final var byteOffset = rowOffset + x; newBuffer[byteOffset] = bitAt(buffer, x, y, pitch); } } buffer = newBuffer; } private static byte bitAt(byte[] buffer, int x, int y, int pitch) { final var byteOffset = (y * pitch) + (x / 8); final var bitOffset = 7 - (x % 8); final var bit = (buffer[byteOffset] >>> bitOffset) & 1; return (byte) (bit == 1 ? 0xff : 0x00); } ``` Here's the JavaFX default of (FT_LOAD_NO_HINTING | FT_LOAD_NO_BITMAP) combined with FT_LOAD_TARGET_MONO: https://ataxia.io7m.com/2023/12/12/droid_12_nohinting_nobitmaps_mono.png That's not a typeface even a mother could love. :) However, what happens if we enable hinting? Here's (FT_LOAD_NO_BITMAP | FT_LOAD_TARGET_MONO): https://ataxia.io7m.com/2023/12/12/droid_12_hinting_nobitmaps_mono.png I mean, it's not exactly wonderful for Droid Sans 12 (the O is a little mangled), but that's more an issue with the font itself. It's certainly better than the result _without_ hinting. Amusingly, here's DejaVu Sans at 7pt, (FT_LOAD_NO_BITMAP | FT_LOAD_TARGET_MONO): https://ataxia.io7m.com/2023/12/12/dejavu_12_hinting_nobitmaps_mono.png That, to my eyes, looks pretty good. The JavaFX defaults for the same font are not good: https://ataxia.io7m.com/2023/12/12/dejavu_12_nohinting_nobitmaps_normal.png I've tried on multiple systems (all Linux, however), and I've yet to be able to contrive a situation where the JavaFX defaults give better rendering results with any combinations of font sizes, with or without AA. A brief inspection of the JDK's Java2D sources show that it does conditionally use FT_LOAD_NO_HINTING depending on various settings (there are comments about FT_LOAD_NO_HINTING yielding constant-sized glyphs, which supposedly can make things easier in some cases). The Java2D results on the systems I tested are consistently better. So I guess my questions are: * Why do we discard hinting information? * Why do we discard bitmaps? * Would JavaFX accept patches to allow hinting, bitmaps, and FT_LOAD_TARGET_MONO? Ideally this should be developer-controlled, so I would need to come up with a pleasant API for it. My experience has been that most JavaFX applications tend to bundle fonts rather than relying on anything the system has. I suspect that, given that developers are including their own fonts, they are the best equipped to answer questions about hinting and AA, rather than just setting values and hoping that the font they get will work well, so an explicit API might be fine. -- Mark Raynsford | https://www.io7m.com