To Hint or Not To Hint? That Is The Question

Thanks - I always knew that hinted fonts looked like shit, but it’s nice to see it demonstrated.

Honestly, you can argue that the individual letters are crisper, but the spacing is an epic fail. What’s the point of sharper letters if you can’t tell where the word-breaks are? In the 3rd line, is there a space after “dog”? Even in the quite-large 5th line, the “p” and “e” are fused together, and in lots of lines there are gaps between the “1” and “2”. Horrible.

And of course your comparison isn’t even fair - white-on-black text should favour hinting, and the software renderer isn’t using clear-type, which would dramatically improve it. Try doing your side-by-side comparison with black-on-white, on a mac using the CoreGraphics renderer, and you’ll find the hinted stuff looks even more shoddy in comparison.

Sorry, maybe you were hoping to convince me of the merits of hinting, but I’m afraid you’ve had the opposite effect!

I’m sure I have a bug or two with the spacing, this was just the first iteration, but the point is that tweaking the Typeface and PositionedGlyph class to support this, along with a few tweaks to glyph arrangement, was actually not the nightmare that we thought it would be and consists of only a few lines of changes.

Thanks for agreeing that the hinted lettering is more crisp. There is definitely room for improvement with the spacing but I was only able to spend a day on it, and that part is specifically in the freetype CustomTypeface I wrote and not part of the small changes I made to Juce.

The gap you are seeing with the numbers is actually a feature of the font I’m using. The digits are mono-spaced:

[attachment=1]linotype.png[/attachment]

Please don’t take my desire to have hinted lettering in my application as a personal critique! For my use case I have a large interface panel that has many, many small controls that are labeled (one or two words). And there are a lot of numerical displays with small fonts.

I think part of the reason that the spacing is off in that first image, is because I had to introduce a ‘fudge’ factor for the font height. Apparently, what Win32 produces and what FreeType produces for the same exact TrueType font, are different. So my code has a compatibility mode where I scale everything so that the dimensions of type using FreeType agrees with what comes out of the Windows outline extraction. If I turn that compatibility mode off, I get better spacing. So I think that this is just a bug on my end. Check it out, with the compatibility flag off:

[attachment=0]hint_demo2.png[/attachment]

So are you open minded in seeing how this was done with minimal Juce changes? Or is the idea of FreeType hinting Juce output completely dead?

Totally understood! And please don’t take my irrational hatred of it as a personal slight, either!

Sorry, it’s just not on my radar at the moment. Trying to concentrate on some other things and this would just be a distraction.

I totally understand and I’m not asking for you to put hinting into Juce. However, would you mind some VERY small tweaks to the related classes in order to support a hinted CustomTypeface (which of course would be provided externally, and there would be NO effort on your part for that)? I’m talking about really tiny one line changes for example

virtual bool Typeface::canGlyphsBeSubpixelPositioned () { return true; } // new function
virtual bool Typeface::isCachedTypefaceFor (const Font&) { return true; } // new function

and

    void CachedGlyph::draw (SavedState& state, const float x, const float y) const
    {
        if (edgeTable != 0)
            //state.fillEdgeTable (*edgeTable, x, roundToInt (y)); // old line
            state.fillEdgeTable (*edgeTable, font.getTypeface()->canGlyphsBeSubpixelPositioned ? x : rountToInt(x), roundToInt (y));
    }

You said in a previous post “A Typeface represents a typeface in its most general form, and the Font represents a typeface at a particular size and style. Trying to bend those meanings would end in tears”, but I found a really easy way to get this thing to work with just one extra function call in Font::findTypefaceFor().

So, pretty please with a cherry on top, and to shut everyone up about font hinting for good, after I am done getting my external FreeType CustomTypeface with hinting finished (which does NOT need to be part of Juce and can be added by other people to their own application, along with compiling and linking manually to Freetype), would you consider with an open mind the very small Juce changes needed to support it?

Ok, well send me your tweaks and I’ll take a look…

Thanks Jules! I’m still working on it…I want this thing to be absolutely perfect and you are right about some of the kerning/advance being off in that first demo image. I found a bug in my code. I’m also looking REALLY hard at the changes I made to the Juce framework and trying to ruthlessly make it as small as possible, I already found a better way of doing it that requires even less Juce changes (and the changes were already small to begin with so this is great).

So before I present it I want to make sure it is as polished as possible so you will like it.

This is what I changed in Juce to get it working

Allow derived classes to announce that they are hinted typefaces.

virtual bool Typeface::isHinted () { return false; } // new function

Shift hinted typefaces by whole pixels only. Thanks to Jules’ great design skills most of the code goes through this one routine, but there might be one or two other places I don’t know about that would need a similar adjustment. I found that it covered the case of Label, and most of the text routines in the Graphics class. Not sure about the TextEditor.

void GlyphArrangement::moveRangeOfGlyphs()
{
  //...
  if (num > 0 && glyphs.getUnchecked (0)->font.getTypeface()->isHinted())
    dx = floor (dx + .5f);
  //...
}

As Jules pointed out, “A Typeface represents a typeface in its most general form, and the Font represents a typeface at a particular size and style. Trying to bend those meanings would end in tears.”

Changing that would be a nightmare. Fortunately, we don’t have to do that :slight_smile: All we need to do is modify the Typeface cache so that even though a CustomTypeface exists with the same name, its considered
a different font if its hinted. Another virtual function in Typeface:

virtual bool Typeface::useTypefaceFor (const Font& font) { return true; }

Now we change the cache to call this routine

const Typeface::Ptr TypefaceCache::findTypefaceFor (const Font& font)
{
    //....
        if (face->flags == flags
             && face->typefaceName == faceName
             && face->typeFace->useTypefaceFor (font)) // additional test

At this point, we have the minimum amount of Juce code that needs to be changed to support these hinted faces, excluding any places like moveRangeOfGlyphs that I might have missed.

[size=150]ONLY FIVE LINES OF CODE!!![/size]

With only these changes to Juce, I was able to write my own CustomTypeface using hinted FreeType-loaded fonts. The result looks like this:

[attachment=2]hinting_demo.gif[/attachment]

[attachment=1]juce.png[/attachment]

[attachment=0]freetype.png[/attachment]

My FreeType CustomTypeFace class requires the following:

  • Add FreeType to your project includes and libraries
  • Turn your .ttf file into a static variable with BinaryBuilder and embed it in your application
  • Override getTypefaceForFont() in your subclass of LookAndFeel:
const Typeface::Ptr YourLookAndFeel::getTypefaceForFont (const Font &font)
{
  // this is a cheap fix for DocumentWindow and MenuBarComponent oddities
  float fontHeight = font.getHeight();
  if (font.getHeight()<1)
    return LookAndFeel::getTypefaceForFont (font);

  return new FreeTypeHintedFace (fontHeight,
     pointerToFileData,
     numberOfBytesInFileData);
}

Now don’t be thinking that I’m going to be making this work with system fonts, i.e. dig into the Windows/Mac/Linux operating system specific place where the fonts are kept and parse those files myself. Although on Linux I would imagine its easy since Juce already does that. All I needed was to make the embedded font I bought appear hinted on screen.

Ok… I think you’ve managed to appease me enough to add some changes for this.

Your changes to Typeface look fine to me, but I don’t like what you did in moveRangeOfGlyphs(). Snapping a delta value to an integer has got to be the wrong way to do it, and your code makes a rather hacky assumption that there’s only one font. Plus, you ignore other places where moveBy() is called directly. Surely a better plan would be to snap the glyph positions when they get rendered?

Thanks Jules! I’ve been working my ass off on this and to be honest…my app looks incredible now. The hinting made a huge difference in the small controls.

You’re right…and originally I tried using roundToInt(x) in CachedGlyph::draw() but it caused spacing issues. After reading your post I tried roundToInt(x) again and painfully stepped through every character that drew and to make a long story short, imagine my surprise when I saw that roundToInt() produces different results when the fractional part is exactly 0.5! ARGH!!!

Using Justification::centred on a string of text that is an odd number of pixels wide, causes the initial x coordinate of the text to have a fractional part exactly equal to 0.5.

So chars at 16.5 and 23.5 should have gone to 17 and 24 (with the change to moveRangeOfGlyphs) but when I used roundToInt(x) I was getting 16 and 24! Off by one pixel, and the source of some spacing issues that you noticed in one of my earlier screenshots.

This change to CachedGlyph::draw() seems to work, and lets me take out the change in moveRangeOfGlyphs. I wonder about the treatment of the y coordinate.

    void CachedGlyph::draw (SavedState& state, const float x, const float y) const
    {
        if (edgeTable != 0)
        {
          if (font.getTypeface()->isHinted())
            state.fillEdgeTable (*edgeTable, floor(x+.5f), roundToInt(y));
          else
            state.fillEdgeTable (*edgeTable, x, roundToInt(y));
        }
    }

This is way clean!!!

In order for outlines produced by a hinted CustomTypeFace to stay true, the following requirements must be met:

  • The normalized outlines have to be scaled back up at exactly the font height they were generated at
  • Obviously no transformation matrices! Rotation and shears? not happening lol
  • Glyphs must be placed with integer x and y coordinates after scaling up, in a way that preserves the distance between glyphs (i.e. always round .5 consistently)

Where can I post my FreeTypeFaces.h and FreeTypeFaces.cpp ? As file attachment or just use the {code} tag?

It also might be helpful if the size of the Typeface cache could be adjusted via a function since each hinted height counts as an individual Typeface.

I’m pretty much done implementing my FreeType CustomTypeFace, its working incredibly well and all of the nagging rounding issues and spacing weirdness has been completely banished from the pixel-perfect kingdom! As soon as I see the changes in the latest tip that support this, I will publish my source code! It’s ready to ship!

Here’s a teaser of the header file

FreeTypeFaces.h

/*******************************************************************************

FreeType CustomTypeFace with hinting for Juce
By Vincent Falco

Juce:
http://www.rawmaterialsoftware.com

--------------------------------------------------------------------------------

License: MIT License (http://www.opensource.org/licenses/mit-license.php)
Copyright (c) 2011 by Vincent Falco

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

*******************************************************************************/

#ifndef FREETYPEFACES_H
#define FREETYPEFACES_H

#include "juce.h"

// This singleton uses FreeType to open font files and
// extract glyph outlines, with the option of using hinting
// at a customizable range of sizes.
class FreeTypeFaces
{
public:
  // Add a font file that has been loaded into memory.
  // If appendStyleToFaceName is false, the style name is
  // not added to the family name to create the typeface name.
  // For example "Helvetica Neue LT Com 65 Medium" becomes
  // "Helvetica Neue LT Com" if appendStyleToFaceName is false.
  //
  // This is useful when you have two styled versions of a face,
  // for example "Helvetica Neue LT Com 65 Medium" and
  // "Helvetica Neue LT Com 75 Bold" and you want to treat them
  // as a single font with an optional bold style. Adding both
  // of these faces with appendStyleToFaceName=false will allow them
  // to work as a single typeface named "Helvetica Neue LT Com"
  // while also respecting the bold flag in the Font object.
  //
  // On the other hand if you have many different weights of the
  // same font you might want appendStyleToFaceName=true so that
  // you can precisely identify which weight you want, for advanced
  // typographists.
  //
  // If useFreeTypeRendering is true and the font gets hinted,
  // it will use FreeType to rasterize the glyph outlines, taking
  // advantage of even more hinting information (if present).
  static void addFaceFromMemory (float minHintedHeight,
                                 float maxHintedHeight,
                                 bool useFreeTypeRendering,
                                 const void* faceFileData,
                                 int faceFileBytes,
                                 bool appendStyleToFaceName = false);

  // This will created a hinted or unhinted Typeface depending
  // on the size of the font, and the range of heights given when
  // the face was added. If the font does not match any faces
  // previoulsy added with addFaceFromMemory(), this function returns 0.
  static Typeface::Ptr createTypefaceForFont (const Font& font);
};

/* To use a hinted font for your entire application you will need to
   implement your own custom LookAndFeel, and override the getTypefaceForFont()
   function. This is an example of what mine looks like:

class CustomLookAndFeel
{
public:
  CustomLookAndFeel()
  {
    // Add the TrueType font "Helvetica Neue LT Com 65 Medium" and
    // activate hinting when the font height is between 7 and 12 inclusive.
    // The font data was generated by running the Juce BinaryBuilder
    // program on the actual TrueType font file (.ttf)
    FreeTypeFaces::getInstance()->addFaceFromMemory(
      7.f, 12.f,
      binaries::helveticaneueltcommd_ttf,
      binaries::helveticaneueltcommd_ttfSize);
  }

  // This function will replace the default sans serif font used
  // throughout the application to use our hinted FreeType face.
  const Typeface::Ptr getTypefaceForFont (const Font &font)
  {
    Typeface::Ptr tf;

    if (font.getHeight()>=1) // ignore tiny requests
    {
      String faceName (font.getTypefaceName());

      if (faceName == Font::getDefaultSansSerifFontName())
      {
        // use our hinted font
        Font f(font);
        f.setTypefaceName ("Helvetica Neue LT Com");
        tf = FreeTypeFaces::createTypefaceForFont (f);
      }
    }

    if (!tf)
      tf = LookAndFeel::getTypefaceForFont (font);

    return tf;
  }
};


*/

#endif

The great and selfless person!
Look forward to!

Ok, I’ve committed some changes now - check it out and let me know if there’s a problem…

Something is definitely broken. There’s nothing wrong with the isHinted() and suitableTypefaceFor(), but something about how the defaultFace is placed into the cache is different from before. What else changed?

This change broke my custom LookAndFeel::getTypefaceForFont()

+        // (can't initialise defaultFace in the constructor or in getDefaultTypeface() because of recursion).\r
+        if (defaultFace == 0)\r
+            defaultFace = LookAndFeel::getDefaultLookAndFeel().getTypefaceForFont (Font());

If the first font we look for is the defaultFace, then this ends up calling getTypefaceForFont() twice, and both times with zero or really small font heights (god I hate that lol). If there was a way to not call getTypefaceForFont() twice for the same font when defaultFace==0 I think that would be helpful.

I was able to fix it in my custom LookAndFeel.

EVERYTHING IS WORKING NOW!

I published my FreeType CustomTypeFace in the Useful Tools section of the forum:
http://rawmaterialsoftware.com/viewforum.php?f=6

Yeah, sorry, that was a last-minute typo that got in there - I’ve checked-in a fixed version now.

To be honest, I prefer Juce’s current version for the very small font (< 10pt), but starting from 10 to 14pt, the hinted version looks better. Then (> 14pt), both seems the same to me.

Anyway, I still think that the gamma curve of the path rendering of a font, in Juce’s version, shouldn’t be linear (gamma = 1), but specific to the contrast of the font (black over white, or inverted), so it would increase the contrast and give better result than the Freetype, wrong, hinting.

Edit: The ‘z’ in lazy of 15pt in juce looks blurry vertically. I don’t understand why, since it’s an horizontal bar.

Well guys this whole hinting project has definitely been a learning experience for me I have had to learn about True Type, Open Type, all the various ways that fonts get hinted.

So, it turns out the the “Helvetica Neue” font that I paid bucks for from LinoType (with an embeddable license) doesn’t have any hints. To say that I am pissed would be quite an understatement. I guess its my fault for not doing enough research.

All of the output in my example screenshots are from FreeType’s “autohinter” which is their way of working around the minefield of patents related to font hinting (which have now expired). The FreeType auto-hinting module is actually pretty damn good as you can see. It ignores hinting information in the font (which my font doesn’t have anyway) and uses its own algorithms to figure out where the stems and curves need to be adjusted to get grid fitted.

Obviously, a well hinted font that has been tuned by hand, is going to give tremendously superior results than the auto-hinter. I’ve been having a hard time getting a set of test fonts with well defined characteristics. I’m on the FreeType mailing list trying to learn more.

The short story is that yes I got hinting working, but the quality is of course going to depend on the font and my screenshots are not the best example (still its pretty good).

When I get my hands on some proper fonts (anyone got some pointers for me?) I will change my CustomTypeface to use the bytecode hinter when its available.

Jules awesome changes man thanks a lot, I see you also added an API for adjusting the Typeface cache size. What are your thoughts on just a few more tweaks?

Allow the Typeface to create the PositionedGlyph:

juce_Typeface.h

virtual PositionedGlyph* Typeface::createPositionedGlyph (float x, float y, float w,
                                                  const Font& font, juce_wchar character, int glyph)
{ return new PositionedGlyph (x, y, w, font, character, glyph); }

juce_GlyphArrangement.cpp

//GlyphArrangement::addCurtailedLineOfText()
glyphs.add (font.getTypeface()->createPositionedGlyph (xOffset + thisX, yOffset, nextX - thisX,
  font, unicodeText[i], newGlyphs.getUnchecked(i)));

Change PositionedGlyph to allow subclassing: virtual functions, change private to protected, expose the private constructor:

juce_GlyphArrangement.h

    virtual PositionedGlyph::~PositionedGlyph() {}
    virtual void PositionedGlyph::draw (const Graphics& g) const;
    virtual void PositionedGlyph::draw (const Graphics& g, const AffineTransform& transform) const; 
    virtual void PositionedGlyph::createPath (Path& path) const;
public:
    PositionedGlyph (float x, float y, float w, const Font& font, juce_wchar character, int glyph);
protected:

This will allow bitmap strikes (fonts that have glyphs without outlines, or fonts that are entirely bitmapped at some or all sizes).