Usage of hinting in UIs is on the way out.
macOS stopped applying hints ages ago. Apple also canned LCD text.
High DPI displays are obsoleting the raison d'etre of both of these.
A big problem with hinting is that it distorts both the size and the shape, so UIs do not scale evenly
and animations look jerky.
Another is that a poorly hinted font is far worse than no hinting at all.


-phil.


On 12/12/23 6:10 AM, Mark Raynsford wrote:
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.


Reply via email to