CoreText Support


#1

JUCE should have CoreText support because:

  1. It is necessary for future Complex Text support in JUCE
  2. Some fonts don’t display properly using the current text rendering system (Examples: Batang, Playbill, Stencil, Wide Latin)
  3. It will allow us to create Paths on iOS without needing Edge Tables

I have implemented such support and hope to see it make its way into the tip.
The code below can act as a drop in replacement for juce_mac_Fonts.mm. I eliminated all legacy code to make it shorter and easier to read. This means that the code will only run on OS X 10.5+ and iOS 3.2+.
I figured you would want to do the integration into the existing juce_mac_Fonts.mm yourself since it will require more preprocessor code.
I didn’t change Font::findAllTypefaceNames() to use CoreText since the existing code works across all OSX/iOS versions.

To use this code on iOS you will need to:

  • Add #import <CoreText/CoreText.h> to the #if JUCE_IOS in Juce_Mac_NativeIncludes.h
  • Add the CoreText framework to your JUCE project

Is this something that can be merged into tip? Is there anything else I can do to speed up that process? thoughts? questions?

// (This file gets included by juce_mac_NativeCode.mm, rather than being
// compiled on its own).
#if JUCE_INCLUDED_FILE

#if MAC_OS_X_VERSION_MIN_REQUIRED < MAC_OS_X_VERSION_10_5
  END_JUCE_NAMESPACE
  @interface NSFont (PrivateHack)
    - (NSGlyph) _defaultGlyphForChar: (unichar) theChar;
  @end
  BEGIN_JUCE_NAMESPACE
#endif

//==============================================================================
class MacTypeface  : public Typeface
{
public:
    //==============================================================================
    MacTypeface (const Font& font)
        : Typeface (font.getTypefaceName())
    {
        const ScopedAutoReleasePool pool;
        renderingTransform = CGAffineTransformIdentity;

        bool needsItalicTransform = false;

        ctFontRef = CTFontCreateWithName(PlatformUtilities::juceStringToCFString (font.getTypefaceName()), 1024, 0);
        
        if (font.isItalic())
        {
            CTFontRef newFont = CTFontCreateCopyWithSymbolicTraits (ctFontRef, 0.0, 0, kCTFontItalicTrait, kCTFontItalicTrait); 
            
            if (newFont == 0)
                needsItalicTransform = true; // couldn't find a proper italic version, so fake it with a transform..
            else
            {
                CFRelease (ctFontRef);
                ctFontRef = newFont;
            }
        }
        
        if (font.isBold())
        {
            CTFontRef newFont = CTFontCreateCopyWithSymbolicTraits (ctFontRef, 0.0, 0, kCTFontBoldTrait, kCTFontBoldTrait);
            if (newFont != 0)
            {
                CFRelease (ctFontRef);
                ctFontRef = newFont;
            }
        }
        
        ascent = std::abs ((float) CTFontGetAscent(ctFontRef));
        float totalSize = ascent + std::abs ((float) CTFontGetDescent(ctFontRef));
        ascent /= totalSize;
        
        pathTransform = AffineTransform::identity.scale (1.0f / totalSize, 1.0f / totalSize);
        
        if (needsItalicTransform)
        {
            pathTransform = pathTransform.sheared (-0.15f, 0.0f);
            renderingTransform.c = 0.15f;
        }
                
        fontRef = CTFontCopyGraphicsFont(ctFontRef, 0);
            
        const int totalHeight = abs (CGFontGetAscent (fontRef)) + abs (CGFontGetDescent (fontRef));
        const float ctTotalHeight = abs (CTFontGetAscent (ctFontRef)) + abs (CTFontGetDescent (ctFontRef));
        unitsToHeightScaleFactor = 1.0f / ctTotalHeight;            
        fontHeightToCGSizeFactor = CGFontGetUnitsPerEm (fontRef) / (float) totalHeight;            
    }

    ~MacTypeface()
    {
        if (fontRef != 0)
            CGFontRelease (fontRef);
        
        if (ctFontRef != 0)
            CFRelease (ctFontRef);
    }

    float getAscent() const
    {
        return ascent;
    }

    float getDescent() const
    {
        return 1.0f - ascent;
    }
    
    float getStringWidth (const String& text)
    {
        if (ctFontRef == 0 || text.isEmpty())
            return 0;
        
        float x = 0;
        
        CFStringRef keys[] = { kCTFontAttributeName, kCTLigatureAttributeName };
        const short zero = 0;
        CFNumberRef numberRef = CFNumberCreate(0, kCFNumberShortType, &zero);
        CFTypeRef values[] = { ctFontRef, numberRef };
        CFDictionaryRef attr = CFDictionaryCreate (NULL, (const void **)&keys, (const void **)&values,
                                                  sizeof(keys) / sizeof(keys[0]), &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
        CFRelease (numberRef);
        CFAttributedStringRef attribString = CFAttributedStringCreate (0, PlatformUtilities::juceStringToCFString (text), attr);
        CFRelease (attr);
        CTLineRef line = CTLineCreateWithAttributedString (attribString);

        CFArrayRef runArray = CTLineGetGlyphRuns (line);
        for (CFIndex i = 0; i < CFArrayGetCount (runArray); ++i)
        {
            CTRunRef run = (CTRunRef)CFArrayGetValueAtIndex (runArray, i);
            CFIndex length = CTRunGetGlyphCount (run);
            HeapBlock <CGSize> advances (length);
            CTRunGetAdvances (run, CFRangeMake(0, 0), advances);
            for (int j = 0; j < length; ++j)
                x += (float) advances[j].width;
        }
        
        CFRelease (line);
        CFRelease (attribString);

        return x * unitsToHeightScaleFactor;
    }

    void getGlyphPositions (const String& text, Array <int>& resultGlyphs, Array <float>& xOffsets)
    {
        xOffsets.add (0);

        if (ctFontRef == 0 || text.isEmpty())
            return;
        
        float x = 0;
        
        CFStringRef keys[] = { kCTFontAttributeName, kCTLigatureAttributeName };
        const short zero = 0;
        CFNumberRef numberRef = CFNumberCreate(0, kCFNumberShortType, &zero);
        CFTypeRef values[] = { ctFontRef, numberRef };
        CFDictionaryRef attr = CFDictionaryCreate (NULL, (const void **)&keys, (const void **)&values,
                                                   sizeof(keys) / sizeof(keys[0]), &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
        CFRelease (numberRef);
        CFAttributedStringRef attribString = CFAttributedStringCreate (0, PlatformUtilities::juceStringToCFString (text), attr);
        CFRelease (attr);
        CTLineRef line = CTLineCreateWithAttributedString (attribString);

        CFArrayRef runArray = CTLineGetGlyphRuns (line);
        for (CFIndex i = 0; i < CFArrayGetCount (runArray); ++i)
        {
            CTRunRef run = (CTRunRef)CFArrayGetValueAtIndex (runArray, i);
            CFIndex length = CTRunGetGlyphCount (run);
            HeapBlock <CGSize> advances (length);
            CTRunGetAdvances (run, CFRangeMake(0, 0), advances);
            HeapBlock <CGGlyph> glyphs (length);
            CTRunGetGlyphs (run, CFRangeMake(0, 0), glyphs);
            for (int j = 0; j < length; ++j)
            {
                x += (float) advances[j].width;
                xOffsets.add (x * unitsToHeightScaleFactor);
                resultGlyphs.add (glyphs[j]);
            }
        }
        
        CFRelease (line);
        CFRelease (attribString);
    }

    EdgeTable* getEdgeTableForGlyph (int glyphNumber, const AffineTransform& transform)
    {
        Path path;

        if (getOutlineForGlyph (glyphNumber, path) && ! path.isEmpty())
            return new EdgeTable (path.getBoundsTransformed (transform).getSmallestIntegerContainer().expanded (1, 0),
                                  path, transform);

        return nullptr;
    }

    bool getOutlineForGlyph (int glyphNumber, Path& path)
    {        
        // we might need to apply a transform to the path, so it mustn't have anything else in it
        jassert (path.isEmpty());
        
        CGPathRef pathRef = CTFontCreatePathForGlyph(ctFontRef, (CGGlyph) glyphNumber, 0);

        CGPathApply (pathRef, &path, pathApplier);
        CFRelease(pathRef);
        
        path.applyTransform (pathTransform);
        return true;
    }

    //==============================================================================
    CGFontRef fontRef;
    
    float fontHeightToCGSizeFactor;
    CGAffineTransform renderingTransform;

private:
    float ascent, unitsToHeightScaleFactor;
    CTFontRef ctFontRef;
    
    AffineTransform pathTransform;

    
    static void pathApplier(void* info, const CGPathElement* element)
    {
        Path* path = (Path*) info;
        
        switch (element->type)
        {
            case kCGPathElementMoveToPoint:         path->startNewSubPath ((float) element->points[0].x, (float) -element->points[0].y); break;
            case kCGPathElementAddLineToPoint:      path->lineTo ((float) element->points[0].x, (float) -element->points[0].y); break;
            case kCGPathElementAddCurveToPoint:     path->cubicTo ((float) element->points[0].x, (float) -element->points[0].y,
                                                               (float) element->points[1].x, (float) -element->points[1].y,
                                                               (float) element->points[2].x, (float) -element->points[2].y); break;
            case kCGPathElementAddQuadCurveToPoint: path->quadraticTo ((float) element->points[0].x, (float) -element->points[0].y,
                                                              (float) element->points[1].x, (float) -element->points[1].y); break;
            case kCGPathElementCloseSubpath:        path->closeSubPath(); break;
            default:                                jassertfalse; break;
        }
    }

    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MacTypeface);
};

const Typeface::Ptr Typeface::createSystemTypefaceFor (const Font& font)
{
    return new MacTypeface (font);
}

//==============================================================================
const StringArray Font::findAllTypefaceNames()
{
    StringArray names;

    const ScopedAutoReleasePool pool;

   #if JUCE_IOS
    NSArray* fonts = [UIFont familyNames];
   #else
    NSArray* fonts = [[NSFontManager sharedFontManager] availableFontFamilies];
   #endif

    for (unsigned int i = 0; i < [fonts count]; ++i)
        names.add (nsStringToJuce ((NSString*) [fonts objectAtIndex: i]));

    names.sort (true);
    return names;
}

void Font::getPlatformDefaultFontNames (String& defaultSans, String& defaultSerif, String& defaultFixed, String& defaultFallback)
{
   #if JUCE_IOS
    defaultSans  = "Helvetica";
    defaultSerif = "Times New Roman";
    defaultFixed = "Courier New";
   #else
    defaultSans  = "Lucida Grande";
    defaultSerif = "Times New Roman";
    defaultFixed = "Monaco";
   #endif

    defaultFallback = "Arial Unicode MS";
}

#endif

#2

Thanks very much!

Though I have to admit to groaning when I saw this… Apple changes its font APIs more often than most people change their socks, and I’ve been hoping to avoid CoreText until the need to support older OSes like 10.4 and iOS2 had disappeared…

Probably most people are still building apps that support those targets, so adding CoreText to the existing font code will add another layer of #ifdef hell to the already messy conglomeration of APIs in there. I’ll take a look though, I agree that it has to happen at some point!


#3

FWIW, 10.4 supports tend to disappear as people move to support 64bits version which only works correctly in 10.5.


#4

Do you have a threshold for when you’ll drop support these older OS’s?

Since the iPad only works with 3.2+, there’s no problem there.

With the iPhone and iTouch, we need at least 4.0 since 3.2 was iPad only. Usage stats are showing 90%+ on iOS 4. [1] [2] Unfortunately, the 1st gen iPhone and iTouch can’t be upgraded to iOS4, they are stuck at 3.1.3. So there are devices out there that will never make it to 4 or higher.

[1] http://www.quora.com/What-proportion-of-all-iPhone-owners-use-iOS4-*-today
[2] http://insights.chitika.com/2011/ios-update-ipads-iphones-running-high-rate-of-ios-4/

Mac OS X 10.5+ usage is around 93%.

Netmarket Share
66.92 - 10.6
25.05 - 10.5
7.07 - 10.4
0.01 - Other

Stat Owl
66.80
25.67
6.68
0.86

Webmasterpro [3]
69.49
22.03
6.77
1.69

Wikipedia [4]
60.68
29.41
8.31
1.58

[3] http://www.webmasterpro.de/portal/webanalyse-systeme.html
[4] http://stats.wikimedia.org/archive/squid_reports/2011-02/SquidReportOperatingSystems.htm


#5

Wow - 10.4 support is as low as 7%?? Excellent! I’ve not got any particular cut-off point, but would really love to ditch 10.4!

I think what I’ll probably do is to make one more release which does support 10.4, and then scrap it.

Anyway, just having a look at your CoreText stuff, I think what I’ll do is to write a new separate CoreText font handler, not trying to merge it with the old stuff, so eventually I can just cut away all the old code cleanly… Will check something in later today or tomorrow.


#6

So I just updated to the tip and I think some change related to this seems to have caused a breakage on the Mac…

[quote]In file included from …/…/…/…/…/rec/src/…/…/juce/amalgamation/…/src/native/mac/juce_mac_NativeCode.mm:225,
from …/…/…/…/…/rec/src/…/…/juce/amalgamation/juce_amalgamated_template.cpp:411,
from /Users/tom/Documents/development/rec/projects/slow/Builds/MacOSX/…/…/JuceLibraryCode/JuceLibraryCode.mm:15:
…/…/…/…/…/rec/src/…/…/juce/amalgamation/…/src/native/mac/juce_mac_Fonts.mm: In member function ‘virtual float juce::MacTypeface::getStringWidth(const juce::String&)’:
…/…/…/…/…/rec/src/…/…/juce/amalgamation/…/src/native/mac/juce_mac_Fonts.mm:151: error: ‘CTRunGetAdvances’ was not declared in this scope
…/…/…/…/…/rec/src/…/…/juce/amalgamation/…/src/native/mac/juce_mac_Fonts.mm: In member function ‘virtual void juce::MacTypeface::getGlyphPositions(const juce::String&, juce::Array<int, juce::DummyCriticalSection>&, juce::Array<float, juce::DummyCriticalSection>&)’:
…/…/…/…/…/rec/src/…/…/juce/amalgamation/…/src/native/mac/juce_mac_Fonts.mm:186: error: ‘CTRunGetAdvances’ was not declared in this scope
[/quote]


#7

I did mess up the iOS build briefly (but fixed it in a check-in yesterday). But the OSX build seems fine here…? I’ve been happily building for all kinds of SDK versions, and had no problems.


#8

This is my first update to JUCE in almost three months, so it might well not be associated with this last change - BUT I did some A/B testing and didn’t make it go away!

I have my older repo (how do I find its version number?) and I completely clean, rebuild and run my project… works fine. Then I switch in the new version of Juce, clean and rebuild, and I get those errors. Just to make sure, I downloaded a completely fresh version from the tip.

My build setup is fairly vanilla, I run OS/X, 10.6, target OS/X 10.5, use juce_amalgamated, statically link to quite a few libraries but nothing else remarkable.

Looking at the code, I don’t quite see how it actually compiles. CTRunGetAdvances requires CTRun.h which doesn’t seem to be included anywhere. I haven’t figured out how actually include CTRun.h “correctly” but if I just paste a declaration of CTRunGetAdvances() into juce_mac_Fonts.mm then it does compile - but then doesn’t find CTRunGetAdvances on link, so there’s clearly a systemic issue.


#9

Ok, if you build with the 10.6 sdk it’s always fine (targeting 10.4, 10.5 or 10.6), but if you use the 10.5 sdk it needs extra headers to be included.

TBH the best approach is probably to always build with the latest SDK, but to set the deployment target 10.5, that way it can use newer features if they’re available, but is still backwards-compatible.


#10

No, sorry - ignore that last post… Looks like the problem is that it uses a couple of functions which are 10.6 only. I’ll just switch it so that you have to have the 10.6 sdk to build the coretext support.


#11

Are sure it is a “couple of functions” and not just CTRunAdvances? Based on TomSwirly’s output and my look at the docs, it looks like CTRunAdvances is the only method that is 10.6 only.

There is a way to make CoreText work without CTRunAdvances. It is quite unpleasant compared to CTRunAdvances but it works.

We use the glyph positions to calculate the advances. In order to get the last advance, we have to add an extra character (the period) to the initial string. Finally, when we provide the glyphs, we need to make sure we don’t give that period glyph back as well.

Old

New


#12

Nice stuff, sonic!

I believe that you are in fact correct in claiming that it’s just this one symbol missing, as I only get that one missing from my link.

Your fix seems plausible. I tried to apply your changes, but didn’t get it to work - there seem to be two occurrences of CTRunGetPositions and you only address one, though I believe the same fix would work for the other.

For the moment, I reverted to my previous Juce!


#13

I’ve fixed this now - if you’re using the 10.5 SDK it just uses the old code, but if it’s 10.6 it’ll use CoreText. Probably best to stick to the 10.6 SDK anyway, even if you’re building with 10.5 compatibility.


#14

Yeah that snippet was just a short way of showing the changes. You’d need to make changes to both the getStringWidth and getGlyphPositions methods in juce_mac_Fonts.mm. The getStringWidth only require the first three changes since the last change is specific to getGlyphPositions.

Do you know what happens when you build using 10.6 sdk, target 10.5 and then run on 10.5? There are no selector/function checks for CTRunAdvances and the docs say that CTRunAdvances isn’t available as a public API on 10.5 so won’t that cause problems?
Well regardless, at least there is a way for it to work in both 10.5 and 10.6 such that you can dump the old 10.4 code when the time comes.


#15

I was curious so I looked into it further.

Here’s what the doc’s have to say:

Well the current code doesn’t check if CTRunGetAdvances is null and instead just uses the function (aka references the symbol) so things should break on 10.5 right?

Wrong!

I took the JUCE tip, cut out the old text code so only CoreText was there. Built it on Snow Leopard (10.6) using the latest public 10.6 SDK (Xcode 4). Set the deployment target to 10.5. Ran it on Leopard (10.5).
And to my surprise, when I ran the JUCE Demo on 10.5 it worked fine.

I can only guess that this means CTRunGetAdvances exists in the 10.5 version of CoreText.framework even though it doesn’t exist in the 10.5 CoreText Public API.

Which is great, there is no need for my code to recreate the advances. Just use the 10.6 SDK, as Jules said.


#16

Yeah, I saw a snippet of Apple code somewhere where they use those functions as undocumented API calls in 10.5, so they were obviously hidden away in there.