FontCacheMac.mm   [plain text]


/*
 * Copyright (C) 2006, 2007, 2008, 2009 Apple Inc. All rights reserved.
 * Copyright (C) 2007 Nicholas Shanks <webkit@nickshanks.com>
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 * 1.  Redistributions of source code must retain the above copyright
 *     notice, this list of conditions and the following disclaimer. 
 * 2.  Redistributions in binary form must reproduce the above copyright
 *     notice, this list of conditions and the following disclaimer in the
 *     documentation and/or other materials provided with the distribution. 
 * 3.  Neither the name of Apple Inc. ("Apple") nor the names of
 *     its contributors may be used to endorse or promote products derived
 *     from this software without specific prior written permission. 
 *
 * THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
 * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

#import "config.h"
#import "FontCache.h"

#if !PLATFORM(IOS)

#import "CoreGraphicsSPI.h"
#import "CoreTextSPI.h"
#import "Font.h"
#import "FontCascade.h"
#import "FontPlatformData.h"
#import "NSFontSPI.h"
#import "WebCoreNSStringExtras.h"
#import "WebCoreSystemInterface.h"
#import <AppKit/AppKit.h>
#import <wtf/MainThread.h>
#import <wtf/NeverDestroyed.h>
#import <wtf/Optional.h>
#import <wtf/StdLibExtras.h>
#import <wtf/Threading.h>
#import <wtf/text/AtomicStringHash.h>

namespace WebCore {

#if !ENABLE(PLATFORM_FONT_LOOKUP)

#define SYNTHESIZED_FONT_TRAITS (NSBoldFontMask | NSItalicFontMask)

#define IMPORTANT_FONT_TRAITS (0 \
    | NSCompressedFontMask \
    | NSCondensedFontMask \
    | NSExpandedFontMask \
    | NSItalicFontMask \
    | NSNarrowFontMask \
    | NSPosterFontMask \
    | NSSmallCapsFontMask \
)

static bool acceptableChoice(NSFontTraitMask desiredTraits, NSFontTraitMask candidateTraits)
{
    desiredTraits &= ~SYNTHESIZED_FONT_TRAITS;
    return (candidateTraits & desiredTraits) == desiredTraits;
}

static bool betterChoice(NSFontTraitMask desiredTraits, int desiredWeight, NSFontTraitMask chosenTraits, int chosenWeight, NSFontTraitMask candidateTraits, int candidateWeight)
{
    if (!acceptableChoice(desiredTraits, candidateTraits))
        return false;

    // A list of the traits we care about.
    // The top item in the list is the worst trait to mismatch; if a font has this
    // and we didn't ask for it, we'd prefer any other font in the family.
    const NSFontTraitMask masks[] = {
        NSPosterFontMask,
        NSSmallCapsFontMask,
        NSItalicFontMask,
        NSCompressedFontMask,
        NSCondensedFontMask,
        NSExpandedFontMask,
        NSNarrowFontMask,
        0
    };

    int i = 0;
    NSFontTraitMask mask;
    while ((mask = masks[i++])) {
        bool desired = desiredTraits & mask;
        bool chosenHasUnwantedTrait = desired != (chosenTraits & mask);
        bool candidateHasUnwantedTrait = desired != (candidateTraits & mask);
        if (!candidateHasUnwantedTrait && chosenHasUnwantedTrait)
            return true;
        if (!chosenHasUnwantedTrait && candidateHasUnwantedTrait)
            return false;
    }

    int chosenWeightDeltaMagnitude = abs(chosenWeight - desiredWeight);
    int candidateWeightDeltaMagnitude = abs(candidateWeight - desiredWeight);

    // If both are the same distance from the desired weight, prefer the candidate if it is further from medium.
    if (chosenWeightDeltaMagnitude == candidateWeightDeltaMagnitude)
        return abs(candidateWeight - 6) > abs(chosenWeight - 6);

    // Otherwise, prefer the one closer to the desired weight.
    return candidateWeightDeltaMagnitude < chosenWeightDeltaMagnitude;
}

// Keep a cache for mapping desired font families to font families actually available on the system for performance.
using AvailableFamilyMap = HashMap<std::pair<AtomicString, NSFontTraitMask>, AtomicString>;
static AvailableFamilyMap& desiredFamilyToAvailableFamilyMap()
{
    ASSERT(isMainThread());
    static NeverDestroyed<AvailableFamilyMap> map;
    return map;
}

static bool hasDesiredFamilyToAvailableFamilyMapping(const AtomicString& desiredFamily, NSFontTraitMask desiredTraits, NSString*& availableFamily)
{
    AtomicString value = desiredFamilyToAvailableFamilyMap().get(std::make_pair(desiredFamily, desiredTraits));
    availableFamily = value.isEmpty() ? nil : static_cast<NSString*>(value);
    return !value.isNull();
}

static inline void rememberDesiredFamilyToAvailableFamilyMapping(const AtomicString& desiredFamily, NSFontTraitMask desiredTraits, NSString* availableFamily)
{
    static const unsigned maxCacheSize = 128;
    auto& familyMapping = desiredFamilyToAvailableFamilyMap();
    ASSERT(familyMapping.size() <= maxCacheSize);
    if (familyMapping.size() >= maxCacheSize)
        familyMapping.remove(familyMapping.begin());

    // Store nil as an emptyAtom to distinguish from missing values (nullAtom).
    AtomicString value = availableFamily ? AtomicString(availableFamily) : emptyAtom;
    familyMapping.add(std::make_pair(desiredFamily, desiredTraits), value);
}

static int toAppKitFontWeight(FontWeight fontWeight)
{
    static const int appKitFontWeights[] = {
        2, // FontWeight100
        3, // FontWeight200
        4, // FontWeight300
        5, // FontWeight400
        6, // FontWeight500
        8, // FontWeight600
        9, // FontWeight700
        10, // FontWeight800
        12, // FontWeight900
    };
    return appKitFontWeights[fontWeight];
}

static inline FontWeight appkitWeightToFontWeight(NSInteger appKitWeight)
{
    if (appKitWeight == 1)
        return FontWeight100;
    if (appKitWeight == 2)
        return FontWeight200;
    if (appKitWeight <= 4)
        return FontWeight300;
    if (appKitWeight == 5)
        return FontWeight400;
    if (appKitWeight == 6)
        return FontWeight500;
    if (appKitWeight <= 8)
        return FontWeight600;
    if (appKitWeight == 9)
        return FontWeight700;
    if (appKitWeight <= 11)
        return FontWeight800;
    return FontWeight900;
}

static NSFontTraitMask toNSFontTraits(CTFontSymbolicTraits traits)
{
    NSFontTraitMask result = 0;
    if (traits & kCTFontBoldTrait)
        result |= NSBoldFontMask;
    if (traits & kCTFontItalicTrait)
        result |= NSItalicFontMask;
    return result;
}

#endif // PLATFORM_FONT_LOOKUP

static CGFloat toNSFontWeight(FontWeight fontWeight)
{
    static const CGFloat nsFontWeights[] = {
        NSFontWeightUltraLight,
        NSFontWeightThin,
        NSFontWeightLight,
        NSFontWeightRegular,
        NSFontWeightMedium,
        NSFontWeightSemibold,
        NSFontWeightBold,
        NSFontWeightHeavy,
        NSFontWeightBlack
    };
    ASSERT(fontWeight >= 0 && fontWeight <= 8);
    return nsFontWeights[fontWeight];
}

RetainPtr<CTFontRef> platformFontWithFamilySpecialCase(const AtomicString& family, FontWeight weight, CTFontSymbolicTraits desiredTraits, float size)
{
    if (equalLettersIgnoringASCIICase(family, "-webkit-system-font") || equalLettersIgnoringASCIICase(family, "-apple-system") || equalLettersIgnoringASCIICase(family, "-apple-system-font")) {
        RetainPtr<CTFontRef> result = toCTFont([NSFont systemFontOfSize:size weight:toNSFontWeight(weight)]);
        if (desiredTraits & kCTFontItalicTrait) {
            if (auto italicizedFont = adoptCF(CTFontCreateCopyWithSymbolicTraits(result.get(), size, nullptr, desiredTraits, desiredTraits)))
                result = italicizedFont;
        }
        return result;
    }

    if (equalLettersIgnoringASCIICase(family, "-apple-system-monospaced-numbers")) {
        int numberSpacingType = kNumberSpacingType;
        int monospacedNumbersSelector = kMonospacedNumbersSelector;
        RetainPtr<CFNumberRef> numberSpacingNumber = adoptCF(CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &numberSpacingType));
        RetainPtr<CFNumberRef> monospacedNumbersNumber = adoptCF(CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &monospacedNumbersSelector));
        CFTypeRef featureKeys[] = { kCTFontFeatureTypeIdentifierKey, kCTFontFeatureSelectorIdentifierKey };
        CFTypeRef featureValues[] = { numberSpacingNumber.get(), monospacedNumbersNumber.get() };
        RetainPtr<CFDictionaryRef> featureIdentifier = adoptCF(CFDictionaryCreate(kCFAllocatorDefault, featureKeys, featureValues, WTF_ARRAY_LENGTH(featureKeys), &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks));
        CFTypeRef featureIdentifiers[] = { featureIdentifier.get() };
        RetainPtr<CFArrayRef> featureArray = adoptCF(CFArrayCreate(kCFAllocatorDefault, featureIdentifiers, 1, &kCFTypeArrayCallBacks));
        CFTypeRef attributesKeys[] = { kCTFontFeatureSettingsAttribute };
        CFTypeRef attributesValues[] = { featureArray.get() };
        RetainPtr<CFDictionaryRef> attributes = adoptCF(CFDictionaryCreate(kCFAllocatorDefault, attributesKeys, attributesValues, WTF_ARRAY_LENGTH(attributesKeys), &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks));

        RetainPtr<CTFontRef> result = toCTFont([NSFont systemFontOfSize:size]);
        return adoptCF(CTFontCreateCopyWithAttributes(result.get(), size, nullptr, adoptCF(CTFontDescriptorCreateWithAttributes(attributes.get())).get()));
    }

    if (equalLettersIgnoringASCIICase(family, "-apple-menu"))
        return toCTFont([NSFont menuFontOfSize:size]);

    if (equalLettersIgnoringASCIICase(family, "-apple-status-bar"))
        return toCTFont([NSFont labelFontOfSize:size]);

    return nullptr;
}

#if !ENABLE(PLATFORM_FONT_LOOKUP)
RetainPtr<CTFontRef> platformFontWithFamily(const AtomicString& family, CTFontSymbolicTraits requestedTraits, FontWeight weight, TextRenderingMode, float size)
{
    NSFontManager *fontManager = [NSFontManager sharedFontManager];
    NSString *availableFamily;
    int chosenWeight;
    NSFont *font;

    NSFontTraitMask desiredTraits = toNSFontTraits(requestedTraits);
    NSFontTraitMask desiredTraitsForNameMatch = desiredTraits | (weight >= FontWeight600 ? NSBoldFontMask : 0);
    if (hasDesiredFamilyToAvailableFamilyMapping(family, desiredTraitsForNameMatch, availableFamily)) {
        if (!availableFamily) {
            // We already know the desired font family does not map to any available font family.
            return nil;
        }
    }

    if (!availableFamily) {
        NSString *desiredFamily = family;

        // Do a simple case insensitive search for a matching font family.
        // NSFontManager requires exact name matches.
        // This addresses the problem of matching arial to Arial, etc., but perhaps not all the issues.
        for (availableFamily in [fontManager availableFontFamilies]) {
            if ([desiredFamily caseInsensitiveCompare:availableFamily] == NSOrderedSame)
                break;
        }

        if (!availableFamily) {
            // Match by PostScript name.
            NSFont *nameMatchedFont = nil;
            for (NSString *availableFont in [fontManager availableFonts]) {
                if ([desiredFamily caseInsensitiveCompare:availableFont] == NSOrderedSame) {
                    nameMatchedFont = [NSFont fontWithName:availableFont size:size];

                    // Special case Osaka-Mono. According to <rdar://problem/3999467>, we need to
                    // treat Osaka-Mono as fixed pitch.
                    if ([desiredFamily caseInsensitiveCompare:@"Osaka-Mono"] == NSOrderedSame && !desiredTraitsForNameMatch)
                        return toCTFont(nameMatchedFont);

                    NSFontTraitMask traits = [fontManager traitsOfFont:nameMatchedFont];
                    if ((traits & desiredTraitsForNameMatch) == desiredTraitsForNameMatch)
                        return toCTFont([fontManager convertFont:nameMatchedFont toHaveTrait:desiredTraitsForNameMatch]);

                    availableFamily = [nameMatchedFont familyName];
                    break;
                }
            }
        }

        rememberDesiredFamilyToAvailableFamilyMapping(family, desiredTraitsForNameMatch, availableFamily);
        if (!availableFamily)
            return nil;
    }

    // Found a family, now figure out what weight and traits to use.
    bool choseFont = false;
    chosenWeight = 0;
    NSFontTraitMask chosenTraits = 0;
    NSString *chosenFullName = 0;

    int appKitDesiredWeight = toAppKitFontWeight(weight);
    NSArray *fonts = [fontManager availableMembersOfFontFamily:availableFamily];
    for (NSArray *fontInfo in fonts) {
        // Array indices must be hard coded because of lame AppKit API.
        NSString *fontFullName = [fontInfo objectAtIndex:0];
        NSInteger fontWeight = [[fontInfo objectAtIndex:2] intValue];
        NSFontTraitMask fontTraits = [[fontInfo objectAtIndex:3] unsignedIntValue];

        BOOL newWinner;
        if (!choseFont)
            newWinner = acceptableChoice(desiredTraits, fontTraits);
        else
            newWinner = betterChoice(desiredTraits, appKitDesiredWeight, chosenTraits, chosenWeight, fontTraits, fontWeight);

        if (newWinner) {
            choseFont = YES;
            chosenWeight = fontWeight;
            chosenTraits = fontTraits;
            chosenFullName = fontFullName;

            if (chosenWeight == appKitDesiredWeight && (chosenTraits & IMPORTANT_FONT_TRAITS) == (desiredTraits & IMPORTANT_FONT_TRAITS))
                break;
        }
    }

    if (!choseFont)
        return nil;

    font = [NSFont fontWithName:chosenFullName size:size];

    if (!font)
        return nil;

    NSFontTraitMask actualTraits = 0;
    if (desiredTraits & NSFontItalicTrait)
        actualTraits = [fontManager traitsOfFont:font];
    FontWeight actualWeight = appkitWeightToFontWeight([fontManager weightOfFont:font]);

    bool syntheticBold = isFontWeightBold(weight) && isFontWeightBold(actualWeight);
    bool syntheticOblique = (desiredTraits & NSFontItalicTrait) && !(actualTraits & NSFontItalicTrait);

    // There are some malformed fonts that will be correctly returned by -fontWithFamily:traits:weight:size: as a match for a particular trait,
    // though -[NSFontManager traitsOfFont:] incorrectly claims the font does not have the specified trait. This could result in applying 
    // synthetic bold on top of an already-bold font, as reported in <http://bugs.webkit.org/show_bug.cgi?id=6146>. To work around this
    // problem, if we got an apparent exact match, but the requested traits aren't present in the matched font, we'll try to get a font from 
    // the same family without those traits (to apply the synthetic traits to later).
    NSFontTraitMask nonSyntheticTraits = desiredTraits;

    if (syntheticBold)
        nonSyntheticTraits &= ~NSBoldFontMask;

    if (syntheticOblique)
        nonSyntheticTraits &= ~NSItalicFontMask;

    if (nonSyntheticTraits != desiredTraits) {
        NSFont *fontWithoutSyntheticTraits = [fontManager fontWithFamily:availableFamily traits:nonSyntheticTraits weight:chosenWeight size:size];
        if (fontWithoutSyntheticTraits)
            font = fontWithoutSyntheticTraits;
    }

    return toCTFont(font);
}
#endif

void platformInvalidateFontCache()
{
#if !ENABLE(PLATFORM_FONT_LOOKUP)
    desiredFamilyToAvailableFamilyMap().clear();
#endif
}

RetainPtr<CTFontRef> platformLookupFallbackFont(CTFontRef font, FontWeight, const AtomicString& locale, const UChar* characters, unsigned length)
{
    RetainPtr<CFStringRef> localeString;
#if __MAC_OS_X_VERSION_MIN_REQUIRED > 101100
    if (!locale.isNull())
        localeString = locale.string().createCFString();
#else
    UNUSED_PARAM(locale);
#endif

    CFIndex coveredLength = 0;
    return adoptCF(CTFontCreateForCharactersWithLanguage(font, characters, length, localeString.get(), &coveredLength));
}

Ref<Font> FontCache::lastResortFallbackFont(const FontDescription& fontDescription)
{
    // FIXME: Would be even better to somehow get the user's default font here.  For now we'll pick
    // the default that the user would get without changing any prefs.
    if (RefPtr<Font> font = fontForFamily(fontDescription, AtomicString("Times", AtomicString::ConstructFromLiteral)))
        return *font;

    // The Times fallback will almost always work, but in the highly unusual case where
    // the user doesn't have it, we fall back on Lucida Grande because that's
    // guaranteed to be there, according to Nathan Taylor. This is good enough
    // to avoid a crash at least.
    return *fontForFamily(fontDescription, AtomicString("Lucida Grande", AtomicString::ConstructFromLiteral), nullptr, nullptr, false);
}

} // namespace WebCore

#endif // !PLATFORM(IOS)