Freetype and subpixel rendering


#1

Hello Vinn and others,

I had a go at getting your freetype amalgamation working. it works! Although i had to override the default look and feel before i could emit new TypeFace objects. Presumably, this is what you have to do.

Excellent job on this btw! i’m seeing text a lot clearer within the 7-14pt range.

My question is regarding subpixel rendering. Looks like this is not possible with Juce unmodified. I noticed you had some code to draw glyphs as part of your VFLib code, but it’s commented out. Am i right i assuming this is work towards this goal, or does it work already but require Juce changes.

or has anyone else made any progress on subpixel placement?

thanks for any info
– hugh.


#2

When FreeType hints glyphs it does it with the assumption that they are placed at integral pixel boundaries. Placing a hinted glyph at a fractional coordinate will screw up the appearance.

The commented out code you refer to, used FreeType to render the glyphs instead of Juce. Unfortunately, this code broke at some point due to changes in Juce. I haven’t bothered to fix it because FreeType’s output was identical to Juce (or was it the other way around?) There’s apparently nothing magical going in with respect to rasterizing hinted glyphs. So there is likely nothing to be gained from using FreeType to render the glyphs. With the exception of LCD rendering of course.


#3

My tip is to create a typeface for each font size with FreeType. While this seems to fly in the face of what a typeface is for, it gives me clear rendering that I just can’t get otherwise. So my L&F creates a selection of typefaces at different heights and when I request a particular font height it returns the closest one. This introduces a granularity in the font sizes, and I need to turn off the feature which automatically narrows labels to fit as that just looks horrible in comparison. But to me, those trade offs are worth it for nice text on Windows.


#4

Duh! That’s what you’re supposed to do! This is the override in L&F you need to make FreeTypeFaces work:

const Typeface::Ptr CustomLookAndFeel::getTypefaceForFont (const Font &font)
{
  Typeface::Ptr tf;

  String faceName (font.getTypefaceName());

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

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

  return tf;
}

#5

No, I mean one for each font size. So I create a typeface for size 10, another for 11 and so on. Sounds dumb, but try it and you will see the difference. If you remain skeptical, I’ll see if I can dig out (or create) a demo after I turn my PC back on tomorrow.


#6

Yes, that’s what I’m talking about. You have to create a new FreeType face for each size. When FreeType hints a type face, it does it for a particular size. Drawing hinted glyphs at a size that differs from the size it was created at originally produces the wrong result. If you look at my LookAndFeel override, you see that it creates a new hinted face for each distinct size:

const Typeface::Ptr CustomLookAndFeel::getTypefaceForFont (const Font &font)
{
...
    tf = vf::FreeTypeFaces::createTypefaceForFont (f);

This works together with the override that Jules added, isSuitableForFont(), so that each distinct font height gets its own brand new hinted Typeface.

So, you’re doing it right.


#7

Ah, I get it - much more elegant. When was isSuitableForFont() added? I don’t remember seeing that.


#8

Yes indeed. this is what i do too. overiding the default L&F. AFAIK there’s no way to override component specific L&F for this because the font system requests it from the default L&F. This means you have to change fonts globally, although what i do i give the “FT” ones a new name.

[code]const Typeface::Ptr FTLook::getTypefaceForFont (const Font& font)
{
Typeface::Ptr tp;
tp = FreeTypeFaces::createTypefaceForFont(font);

if (!tp)
    tp = parentT::getTypefaceForFont(font);

return tp;

}
[/code]

so anyhow, I’m noticing differences in scale. check these examples (hinting < 18pt). Top line is free type, bottom line is Juce:

FT < Juce, no hinting

FT == Juce, Hinting

FT < Juce (a bit), no hinting

FT > Juce (hinting)

FT > Juce (no hinting)

FT >> Juce, hinting

weird.


#9

Jules added it when he was finally convinced that hinted output is useful and desired. Before this change, the Juce typeface cache assumed that the outlines were the same for a given face regardless of size.


#10

Hi,

I’ve got a major improvement for you freetype guys.

One of the killers to Vinn’s current implementation is that initialising the font takes too long. my test window that allows the font to resize basically chokes on all the font initialisation required. Also large fonts like DroidSansFallback.ttf just dont load at all.

In order to initialise the JUCE kerning table, the current way is to iterate over all N^2 characters of the font. that basically canes the performance right there. The reason is that there is no FT way to extract the font table, so the only way is to find it by brute force. Vinn has done the best he could within the contraints.

im beginning to really hate FreeType. here’s my reasons:

(1) the code sucks.
(2) you cant get the kerning table.
(3) you cant map from a glyph index to a charcode
(4) the code reminds me of projects that i only work on when paid.

Regarding point (3), the official reason is that there is no unique mapping. However, sometimes you need to do this. there should be some sort of iterator variants notwithstanding.

so i decided that FT is useless to me unless i hack it. im going to add my hacks as a supplementary file so as not to change or invalidate the amalgamation.

new function: `FT_Get_Kerning_Pairs’ in ftextra.c and ftextra.c. to use #include “ftextra.c” in the same file as includes “FreeTypeAmalgam.c” so it can get to the definitions.

ftextra.h

[code]#ifndef ftextra_h
#define ftextra_h

FT_EXPORT_DEF( FT_Error )
FT_Get_Kerning_Pairs( FT_Face face,
FT_UInt kern_mode,
FT_UInt* pair_count,
FT_Short** kerns);

#endif // ftextra_h
[/code]

ftextra.c

[code]#include “ftextra.h”

FT_LOCAL_DEF( FT_UInt )
tt_face_get_kerning_pairs( TT_Face face,
short* glyph_pairs)
{
FT_UInt count, mask = 1;
FT_Byte* p = face->kern_table;
FT_Byte* p_limit = p + face->kern_table_size;
FT_UInt total_pairs = 0;

p   += 4;
mask = 0x0001;

for ( count = face->num_kern_tables;
      count > 0 && p + 6 <= p_limit;
      count--, mask <<= 1 )
{
    FT_Byte* base     = p;
    FT_Byte* next     = base;
    FT_UInt  version  = FT_NEXT_USHORT( p );
    FT_UInt  length   = FT_NEXT_USHORT( p );
    FT_UInt  coverage = FT_NEXT_USHORT( p );
    FT_UInt  num_pairs;
    FT_Int   value    = 0;

    FT_UNUSED( version );

    next = base + length;

    if ( next > p_limit )  /* handle broken table */
        next = p_limit;

    if ( ( face->kern_avail_bits & mask ) == 0 )
        goto NextTable;

    if ( p + 8 > next )
        goto NextTable;

    num_pairs = FT_NEXT_USHORT( p );
    p        += 6;

    if ( ( next - p ) < 6 * (int)num_pairs )  /* handle broken count  */
        num_pairs = (FT_UInt)( ( next - p ) / 6 );

    switch ( coverage >> 8 )
    {
    case 0:
        {
            FT_UInt  count2;
            total_pairs += num_pairs;

            if (glyph_pairs)
                for ( count2 = num_pairs; count2 > 0; count2-- )
                {
                    FT_ULong  key = FT_NEXT_ULONG( p );

                    *glyph_pairs++ = (key >> 16); // left
                    *glyph_pairs++ = (short)key; // right

                    value = FT_PEEK_SHORT( p );
                
                    *glyph_pairs++ = (short)value; 

                    p += 2;
                }
        }
        break;

        /*
         *  We don't support format 2 because we haven't seen a single font
         *  using it in real life...
         */

    default:
        ;
    }

NextTable:
    p = next;
}

return total_pairs;

}

static FT_UInt
tt_get_kerning_pairs( FT_Face ttface, /* TT_Face /
short
pairs)
{
TT_Face face = (TT_Face)ttface;
return tt_face_get_kerning_pairs( face, pairs);
}

FT_EXPORT_DEF( FT_Error )
FT_Get_Kerning_Pairs( FT_Face face,
FT_UInt kern_mode,
FT_UInt* pair_count,
FT_Short** kerns)
{
FT_Error error = FT_Err_Ok;
FT_Driver driver;

if ( !face )
    return FT_Err_Invalid_Face_Handle;

if ( !kerns  || !pair_count)
    return FT_Err_Invalid_Argument;

*kerns = 0;
*pair_count = 0;

driver = face->driver;

// be sure the interface is TT
if ( driver->clazz->get_kerning == tt_get_kerning)
{
    unsigned int i;
    FT_Short* kp = 0;

    // find out how many pairs we have
    FT_UInt n = tt_get_kerning_pairs( face, 0);

    if (n)
    {
        // allocate 6 shorts for each:
        // LEFT, RIGHT, VALUE
        kp = (FT_Short*)malloc(n*6*sizeof(FT_Short));
    }

    if (!kp)
        return error; // bail

    // now fill in the values
    tt_get_kerning_pairs(face, kp);

    // caller gets count & memory. NB: remember to free!
    *kerns = kp;
    *pair_count = n;
    
    for (i = 0; i < n; ++i)
    {
        int v = kp[2]; // value
        if ( kern_mode != FT_KERNING_UNSCALED )
        {
            v = FT_MulFix( v, face->size->metrics.x_scale );
            
            if ( kern_mode != FT_KERNING_UNFITTED )
            {
                /* we scale down kerning values for small ppem values */
                /* to avoid that rounding makes them too big.         */
                /* `25' has been determined heuristically.            */
                if ( face->size->metrics.x_ppem < 25 )
                    v = FT_MulDiv( v, face->size->metrics.x_ppem, 25 );

                v = FT_PIX_ROUND( v);
            }
        }

        // clamp to range of signed short
        if (v < -32768) v = -32768;
        else if (v > 32767) v = 32767;

        // put it back
        kp[2] = (short)v;

        // to next entry
        kp += 3;
    }
}

return error;

}
[/code]

now to can change Vinns code in vf_FreeTypeFaces.cpp'. replaceaddKerningPairs’ with this one.
Sorry about the bogus index - > character map i have to create. this is annoying to say the least. anyone know a better way?

[code] void addKerningPairs()
{
FT_UInt pairCount;
FT_Short* pairs;

  // make index map
  int ng = m_face->num_glyphs;
  if (ng <= 0 || ng > 65535)
      return; // bail

  FT_ULong* gmap = new FT_ULong[ng];

  // the count will be less 
  int cc = 0;
  FT_UInt leftGlyphIndex;
  FT_ULong leftCharCode = FT_Get_First_Char (m_face, &leftGlyphIndex);
  while (leftGlyphIndex)
  {
      gmap[leftGlyphIndex] = leftCharCode;
      ++cc;
      leftCharCode = FT_Get_Next_Char (m_face,
                                       leftCharCode, &leftGlyphIndex);

      if (cc >= ng)
          break; // exceeded claimed glyph count!
  }

#define IDX2CHAR(_x) (_x < cc ? gmap[_x] : 0)

  if (!FT_Get_Kerning_Pairs(m_face,
                            m_kerningMode,
                            &pairCount,
                            &pairs))
  {

      unsigned int i;
      FT_Short* pp = pairs;
      for (i = 0; i < pairCount; ++i)
      {
          FT_Long left = IDX2CHAR(pp[0]);
          FT_Long right = IDX2CHAR(pp[1]);
          FT_Short v = pp[2];
          
          if (left && right && v)
          {
              float extraAmount = m_kerningScale * v;
              addKerningPair (left, right, extraAmount);
          }
          pp += 3;
      }
      
      // remember to free the pairs
      if (pairs) free(pairs);
  }

  // clean up
  delete gmap;

}[/code]

things load much faster now.

– hugh.


#11

Hugh, you precisely summarized my thoughts while i was writing FreeTypeFaces.

I suggest you post this to the FreeType development list, they are quite responsive to things like this. We can easily get official support for extracting the kerning pairs in an efficient fashion.

I agree that kerning is a serious weakness of FreeTypeFaces.


#12

they wont be interested. if you google for this problem, they arent interested in supporting it; apparently kerning with pairs is out in favor of something newer.

personally, i think it’s political. same reason you cant get the charcode from the index. you can go into their stuff, but not out. they dont want people like us using another system to do the rendering. FT is just not designed in a properly open way. and on the subject of mapping back to charcodes, i dont understand why this isnt possible given the charcode are unicode.

anyhow, you’re welcome incorporate my hacks into your code if you like. I reached the point where i had to make these changes to make FT useful within JUCE.

– hugh.


#13

Could you possibly submit a pull request?


#14

Seems you were right. From the mailing list:

[quote]> […] The problem is that I have to do an operation on the order of

O(N^2) where N = number of glyphs, to extract the kerning pairs. It
would be a lot easier if FreeType offered some sort of iterator that
lets me enumerate each kerning pair.

The developer who brought this to my attention claims that “they
arent interested in supporting it” (they meaning the FreeType
developers). But my experience with FreeType development has only
been positive. Is there anything we can do about this?

Reading the thread, I must say that Hugh is right basically. The number of fonts which have full-featured kern' tables is rapidly diminishing. Today, full kerning is in theGPOS’ table, and kern' only holds the absolute minimum of necessary kerning values for backwards-compatibility, apoor-man’s kerning’, so to say.

Please don’t forget that FreeType’s job is to render glyphs, nothing else. Accessing additional font tables (gasp, kern, etc.) should be handled by a higher-level library. Well, Behdad tries me to convince that gasp' is something for FreeType but I'm not convinced yet... However,kern’ certainly doesn’t belong to FreeType, and to fully support `GPOS’ kerning you need an OpenType layout engine like Harfbuzz.

If you want better support for kerning from the `kern’ table, something like Hugh’s code is OK, and adding it to your amalgamation project looks like a good solution.

Regarding the other issue, this is, getting the character code from the glyph index, Hugh seems to completely forget OpenType features which makes it impossible to invert the mapping. As an example, think of using small capitals which get mapped onto the character code positions of lowercase characters.

In case there are valid glyph names, Adobe’s glyph list algorithm (AGL) partially works. However, complex scripts do glyph reordering, insert more glyphs, etc., etc. For such cases even the AGL will not work.

Similar to Hugh’s solution for better `kern’ table handling it should be rather easy to add a small function which provides poor-man’s glyph to character code support if the cmap is directly used.[/quote]


#15

Yes, there are holes in my hacks! Although i check that TT is valid, if the kerning isnt in the tt kern table, it wont see it. For a better version, it would need versions for the other forms of kern table representation.

i’ve tried it here with the four sans fonts that im currently using and they’re ok. in any case, i’ll know what font im using in my code because, to use this, i have to load it and add it to FT manually. that means i can check my fonts work with FT.

Actually, ive just transplanted the code into my windows app and it look really quite nice when the hinting is on. the lines are a lot cleaner when i have small text in boxes (which i have a lot). im happy so far.

one thing i just discovered, be sure to disable directwrite in AppConfig.h, otherwise it only works if the font you’re working with in FT is also installed in your machine. This is the case if you’re working with `TextLayout’ anyhow,.

i’ll try that pull request, see if it works.
– hugh.


#16

Hmm…I don’t quite understand. I’m loading the font from a binary embedded in the app. Would that matter?


#17

You’ve got to do the following:

  1. Fork VFLib on Github (press the Fork button)

  2. Apply your changes in a new branch (e.g. git checkout -b fast-ft-kern)

  3. Push your branch to your remote fork

  4. Through the Github web page press the “Pull Request” button on the branch with your changes


#18

pull request is in. thanks for the directions. i hope it works.

regarding AppConfig.h - that’s what i thought, until i finally tracked it down.

Basically, if you use TextLayout and, under windows, directwrite is enabled (which it is by default in AppConfig.h), it uses windows installed fonts to perform the layout and the attributes of the layout are changed to use fonts that are installed under windows.

so, when i use a font that is not present under windows, although this is manually embeded in the binary, it messes up. this could be a JUCE bug. what you’ll see in your custom lookandfeel is a request for a different font than the one you’re using. im seeing “Sergio UI”. whatever.

Anyhow, i think this is only a problem with `TextLayout’. My test program works ok and doesnt use it.

– hugh.


#19

Yes, I see it now. juce_win32_DirectWriteTypeLayout.cpp.


#20

so i just compiled everthing for Android. Amalgam works, although there’s a duplicate defintion of `Byte’ which gcc doesnt like. After that it works fine.

First thing to notice is the text is looking really clear. my app is a calendar that displays a whole month on one page and tries to draw the appointment text inside each day box. the text is 13 point.

i try it on a 4 inch phone and a 7 inch tablet. Wow, the phone looks very good. before you couldnt read it properly, now you can. on the tablet, the text is too small and i need to use a larger font. I guess the hinting wont be as useful here.

one thing occurs to me. a friend of mine says he cant read small text anyhow - long sighted. for short sighted people like me, small text is fone. what im wondering is whether the preference for small crisp text vs slightly fuzzy text (eg non-hinted) goes with the kind of eyesight of the user. just a thought.

anyhow, this is the answer for small text in boxes!
– hugh.