chromium/third_party/blink/renderer/platform/fonts/mac/font_cache_mac.mm

/*
 * Copyright (C) 2006, 2007, 2008, 2009 Apple Inc. All rights reserved.
 * Copyright (C) 2007 Nicholas Shanks <[email protected]>
 *
 * 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 Computer, 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 "third_party/blink/renderer/platform/fonts/font_cache.h"

#include <memory>

#import <AppKit/AppKit.h>
#import <CoreFoundation/CoreFoundation.h>
#import <CoreText/CoreText.h>
#include <Foundation/Foundation.h>

#include "base/apple/bridging.h"
#include "base/apple/foundation_util.h"
#include "base/apple/scoped_cftyperef.h"
#include "base/location.h"
#include "base/metrics/histogram_macros.h"
#include "base/timer/elapsed_timer.h"
#include "third_party/blink/public/platform/platform.h"
#include "third_party/blink/renderer/platform/font_family_names.h"
#include "third_party/blink/renderer/platform/fonts/font_description.h"
#include "third_party/blink/renderer/platform/fonts/font_face_creation_params.h"
#include "third_party/blink/renderer/platform/fonts/font_platform_data.h"
#include "third_party/blink/renderer/platform/fonts/mac/font_matcher_mac.h"
#include "third_party/blink/renderer/platform/fonts/mac/font_platform_data_mac.h"
#include "third_party/blink/renderer/platform/fonts/simple_font_data.h"
#include "third_party/blink/renderer/platform/runtime_enabled_features.h"
#include "third_party/blink/renderer/platform/scheduler/public/main_thread.h"
#include "third_party/blink/renderer/platform/web_test_support.h"
#include "third_party/blink/renderer/platform/wtf/functional.h"
#include "third_party/blink/renderer/platform/wtf/std_lib_extras.h"
#include "third_party/blink/renderer/platform/wtf/wtf.h"

using base::apple::CFToNSOwnershipCast;
using base::apple::CFToNSPtrCast;
using base::apple::NSToCFOwnershipCast;
using base::apple::NSToCFPtrCast;
using base::apple::ScopedCFTypeRef;

// Forward declare Mac SPIs.
// Request for public API: rdar://13803570
@interface NSFont (WebKitSPI)
+ (NSFont*)findFontLike:(NSFont*)font
              forString:(NSString*)string
              withRange:(NSRange)range
             inLanguage:(id)useNil;
+ (NSFont*)findFontLike:(NSFont*)font
           forCharacter:(UniChar)uc
             inLanguage:(id)useNil;
@end

namespace blink {

namespace {

const float kCTNormalWeightValue = 0.0;
CTFontSymbolicTraits TraitsMask = kCTFontTraitItalic | kCTFontTraitBold |
                                  kCTFontTraitCondensed | kCTFontTraitExpanded;

ScopedCFTypeRef<CTFontRef> CreateCopyWithTraitsAndWeightFromFont(
    CTFontRef font,
    CTFontSymbolicTraits traits,
    float weight,
    float size) {
  ScopedCFTypeRef<CFStringRef> family_name(CTFontCopyFamilyName(font));
  // Some broken fonts may lack a postscript name (nameID="6"), full font
  // name (nameId="4") or family name (nameID="1") in the 'name' font table, see
  // https://learn.microsoft.com/en-us/typography/opentype/spec/name.
  // For these fonts `family_name` will be null, compare
  // https://crbug.com/1521364
  if (!family_name) {
    return ScopedCFTypeRef<CTFontRef>(nullptr);
  }

  NSDictionary* traits_dict = @{
    CFToNSPtrCast(kCTFontSymbolicTrait) : @(traits),
    CFToNSPtrCast(kCTFontWeightTrait) : @(weight),
  };
  NSDictionary* attributes = @{
    CFToNSPtrCast(kCTFontFamilyNameAttribute) :
        CFToNSPtrCast(family_name.get()),
    CFToNSPtrCast(kCTFontTraitsAttribute) : traits_dict,
  };

  ScopedCFTypeRef<CTFontDescriptorRef> descriptor(
      CTFontDescriptorCreateWithAttributes(NSToCFPtrCast(attributes)));
  // When we try to find the substitute font of "Menlo Regular" with italic
  // traits attribute for the selected code point using
  // `CTFontCreateCopyWithAttributes`, it gives us the same "Menlo Regular" font
  // rather than "Menlo Italic". So we are using
  // `CTFontCreateWithFontDescriptor` to find the better style match within the
  // family.
  return ScopedCFTypeRef<CTFontRef>(
      CTFontCreateWithFontDescriptor(descriptor.get(), size, nullptr));
}

bool IsLastResortFont(CTFontRef font) {
  ScopedCFTypeRef<CFStringRef> font_name(CTFontCopyPostScriptName(font));
  return font_name && CFStringCompare(font_name.get(), CFSTR("LastResort"),
                                      0) == kCFCompareEqualTo;
}

ScopedCFTypeRef<CTFontRef> GetSubstituteFont(CTFontRef ct_font,
                                             UChar32 character,
                                             float size) {
  DCHECK(RuntimeEnabledFeatures::FontMatchingCTMigrationEnabled());

  auto bytes = base::bit_cast<std::array<UInt8, 4>>(character);
  ScopedCFTypeRef<CFStringRef> string(CFStringCreateWithBytes(
      kCFAllocatorDefault, std::data(bytes), std::size(bytes),
      kCFStringEncodingUTF32LE, false));
  CFRange range = CFRangeMake(0, CFStringGetLength(string.get()));

  ScopedCFTypeRef<CTFontRef> substitute_font;
  // System API might return colored "Apple Color Emoji" font for some emoji
  // codepoints. But if emoji codepoint was requested and
  // fallback_priority != kEmojiEmoji, it means that we need a monochromatic
  // (text) presentation of emoji. For that we use hardcoded monochromatic emoji
  // font.
  if (RuntimeEnabledFeatures::SystemFallbackEmojiVSSupportEnabled() &&
      Character::IsEmoji(character)) {
    ScopedCFTypeRef<CTFontRef> emoji_font(
        CTFontCreateWithName(CFSTR("Apple Symbols"), size, nullptr));
    if (emoji_font) {
      substitute_font.reset(
          CTFontCreateForString(emoji_font.get(), string.get(), range));
    }
  } else if (!ct_font) {
    // For some web fonts for which we use FreeType backend (for instance some
    // color fonts), `ct_font` is null. For these fonts we still want to have a
    // substitute font for a character. We are using the default value of
    // standard font from user settings defined in
    // `chrome/app/resources/locale_settings_mac.grd` as the font to substitute
    // from in `CTFontCreateForString`.
    ScopedCFTypeRef<CTFontRef> font_to_substitute(
        CTFontCreateWithName(CFSTR("Times"), size, nullptr));
    substitute_font.reset(
        CTFontCreateForString(font_to_substitute.get(), string.get(), range));
  } else {
    substitute_font.reset(CTFontCreateForString(ct_font, string.get(), range));
  }

  if (!substitute_font || IsLastResortFont(substitute_font.get())) {
    return ScopedCFTypeRef<CTFontRef>(nullptr);
  }
  return substitute_font;
}

// Some fonts may have appearance information in the upper 16 bits,
// for example for "Times Roman" traits = (1 << 28) and for "Helvetica"
// traits = (1 << 30).
// We only need to care about typeface information in the lower 16 bits and
// need to check only whether the traits we care about mismatch (i.e.
// font-stretch, font-style and font-weight corresponding traits). So that
// later we can try to find a font within the same family with the desired
// typeface.
bool TraitsMismatch(CTFontSymbolicTraits desired_traits,
                    CTFontSymbolicTraits found_traits) {
  return (desired_traits & TraitsMask) != (found_traits & TraitsMask);
}

const FontPlatformData* GetAlternateFontPlatformData(
    const FontDescription& font_description,
    UChar32 character,
    const FontPlatformData& platform_data) {
  DCHECK(RuntimeEnabledFeatures::FontMatchingCTMigrationEnabled());
  CTFontRef ct_font = platform_data.CtFont();

  float size = font_description.ComputedPixelSize();

  ScopedCFTypeRef<CTFontRef> substitute_font(
      GetSubstituteFont(ct_font, character, size));
  if (!substitute_font) {
    return nullptr;
  }

  auto get_ct_font_weight = [](CTFontRef font) -> float {
    NSDictionary* font_traits = CFToNSOwnershipCast(CTFontCopyTraits(font));

    float weight = kCTNormalWeightValue;
    if (font_traits) {
      NSNumber* weight_num = base::apple::ObjCCast<NSNumber>(
          font_traits[CFToNSPtrCast(kCTFontWeightTrait)]);
      if (weight_num) {
        weight = weight_num.floatValue;
      }
    }
    return weight;
  };

  CTFontSymbolicTraits traits;
  float weight = ToCTFontWeight(font_description.Weight());
  if (ct_font) {
    traits = CTFontGetSymbolicTraits(ct_font);
    if (platform_data.synthetic_bold_) {
      traits |= kCTFontTraitBold;
    }
    if (platform_data.synthetic_italic_) {
      traits |= kCTFontTraitItalic;
    }
  } else {
    traits = font_description.Style() ? kCTFontTraitItalic : 0;
  }

  CTFontSymbolicTraits substitute_font_traits =
      CTFontGetSymbolicTraits(substitute_font.get());
  float substitute_font_weight = get_ct_font_weight(substitute_font.get());

  if (TraitsMismatch(traits, substitute_font_traits) ||
      (weight != substitute_font_weight) || !ct_font) {
    ScopedCFTypeRef<CTFontRef> best_variation =
        CreateCopyWithTraitsAndWeightFromFont(substitute_font.get(), traits,
                                              weight, size);

    if (best_variation) {
      CTFontSymbolicTraits best_variation_font_traits =
          CTFontGetSymbolicTraits(best_variation.get());
      float best_variation_font_weight =
          get_ct_font_weight(best_variation.get());
      ScopedCFTypeRef<CFCharacterSetRef> char_set(
          CTFontCopyCharacterSet(best_variation.get()));
      if ((!ct_font || best_variation_font_traits != substitute_font_traits ||
           best_variation_font_weight != substitute_font_weight) &&
          char_set &&
          CFCharacterSetIsLongCharacterMember(char_set.get(), character)) {
        substitute_font = best_variation;
        substitute_font_traits = CTFontGetSymbolicTraits(substitute_font.get());
      }
    }
  }

  bool synthetic_bold = (traits & kCTFontTraitBold) &&
                        !(substitute_font_traits & kCTFontTraitBold);
  bool synthetic_italic = (traits & kCTFontTraitItalic) &&
                          !(substitute_font_traits & kCTFontTraitItalic);

  return FontPlatformDataFromCTFont(
      substitute_font.get(), font_description.EffectiveFontSize(),
      font_description.SpecifiedSize(), synthetic_bold, synthetic_italic,
      font_description.TextRendering(), ResolvedFontFeatures(),
      platform_data.Orientation(), font_description.FontOpticalSizing(),
      nullptr);
}

bool IsSystemFontName(const AtomicString& font_name) {
  return !font_name.empty() && font_name[0] == '.';
}

inline bool IsAppKitFontWeightBold(NSInteger app_kit_font_weight) {
  return app_kit_font_weight >= 7;
}

void FontCacheRegisteredFontsChangedNotificationCallback(
    CFNotificationCenterRef,
    void* observer,
    CFStringRef name,
    const void*,
    CFDictionaryRef) {
  DCHECK_EQ(observer, &FontCache::Get());
  DCHECK(CFEqual(name, kCTFontManagerRegisteredFontsChangedNotification));
  FontCache::InvalidateFromAnyThread();
}

}  // namespace

const char kColorEmojiFontMac[] = "Apple Color Emoji";

// static
const AtomicString& FontCache::LegacySystemFontFamily() {
  return font_family_names::kBlinkMacSystemFont;
}

// static
void FontCache::InvalidateFromAnyThread() {
  if (!IsMainThread()) {
    Thread::MainThread()
        ->GetTaskRunner(MainThreadTaskRunnerRestricted())
        ->PostTask(FROM_HERE,
                   WTF::BindOnce(&FontCache::InvalidateFromAnyThread));
    return;
  }
  FontCache::Get().Invalidate();
}

void FontCache::PlatformInit() {
  CFNotificationCenterAddObserver(
      CFNotificationCenterGetLocalCenter(), this,
      FontCacheRegisteredFontsChangedNotificationCallback,
      kCTFontManagerRegisteredFontsChangedNotification, /*object=*/nullptr,
      CFNotificationSuspensionBehaviorDeliverImmediately);
}

const SimpleFontData* FontCache::PlatformFallbackFontForCharacter(
    const FontDescription& font_description,
    UChar32 character,
    const SimpleFontData* font_data_to_substitute,
    FontFallbackPriority fallback_priority) {
  if (fallback_priority == FontFallbackPriority::kEmojiEmoji) {
    if (const SimpleFontData* emoji_font =
            GetFontData(font_description, AtomicString(kColorEmojiFontMac))) {
      return emoji_font;
    }
  }

  const FontPlatformData& platform_data =
      font_data_to_substitute->PlatformData();

  const FontPlatformData* alternate_font = nullptr;
  if (RuntimeEnabledFeatures::FontMatchingCTMigrationEnabled()) {
    alternate_font = GetAlternateFontPlatformData(font_description, character,
                                                  platform_data);
  } else {
    // FIXME: We should fix getFallbackFamily to take a UChar32
    // and remove this split-to-UChar16 code.
    UChar code_units[2];
    int code_units_length;
    if (character <= 0xFFFF) {
      code_units[0] = character;
      code_units_length = 1;
    } else {
      code_units[0] = U16_LEAD(character);
      code_units[1] = U16_TRAIL(character);
      code_units_length = 2;
    }

    NSFont* ns_font = CFToNSPtrCast(platform_data.CtFont());

    NSString* string = [[NSString alloc]
        initWithCharacters:reinterpret_cast<UniChar*>(code_units)
                    length:code_units_length];
    NSFont* substitute_font =
        [NSFont findFontLike:ns_font
                   forString:string
                   withRange:NSMakeRange(0, code_units_length)
                  inLanguage:nil];

    // FIXME: Remove this SPI usage: http://crbug.com/255122
    if (!substitute_font && code_units_length == 1) {
      substitute_font = [NSFont findFontLike:ns_font
                                forCharacter:code_units[0]
                                  inLanguage:nil];
    }
    if (!substitute_font) {
      return nullptr;
    }

    // Use the family name from the AppKit-supplied substitute font, requesting
    // the traits, weight, and size we want. One way this does better than the
    // original AppKit request is that it takes synthetic bold and oblique into
    // account.  But it does create the possibility that we could end up with a
    // font that doesn't actually cover the characters we need.

    NSFontManager* font_manager = NSFontManager.sharedFontManager;

    NSFontTraitMask traits;
    NSInteger weight;
    CGFloat size;

    if (ns_font) {
      traits = [font_manager traitsOfFont:ns_font];
      if (platform_data.synthetic_bold_) {
        traits |= NSBoldFontMask;
      }
      if (platform_data.synthetic_italic_) {
        traits |= NSFontItalicTrait;
      }
      weight = [font_manager weightOfFont:ns_font];
      size = [ns_font pointSize];
    } else {
      // For custom fonts nsFont is nil.
      traits = font_description.Style() ? NSFontItalicTrait : 0;
      weight = ToAppKitFontWeight(font_description.Weight());
      size = font_description.ComputedPixelSize();
    }

    NSFontTraitMask substitute_font_traits =
        [font_manager traitsOfFont:substitute_font];
    NSInteger substitute_font_weight =
        [font_manager weightOfFont:substitute_font];

    if (traits != substitute_font_traits || weight != substitute_font_weight ||
        !ns_font) {
      if (NSFont* best_variation =
              [font_manager fontWithFamily:substitute_font.familyName
                                    traits:traits
                                    weight:weight
                                      size:size]) {
        if ((!ns_font ||
             [font_manager traitsOfFont:best_variation] !=
                 substitute_font_traits ||
             [font_manager weightOfFont:best_variation] !=
                 substitute_font_weight) &&
            [[best_variation coveredCharacterSet]
                longCharacterIsMember:character]) {
          substitute_font = best_variation;
        }
      }
    }

    substitute_font_traits = [font_manager traitsOfFont:substitute_font];
    substitute_font_weight = [font_manager weightOfFont:substitute_font];

    bool synthetic_bold = IsAppKitFontWeightBold(weight) &&
                          !IsAppKitFontWeightBold(substitute_font_weight);

    alternate_font = FontPlatformDataFromCTFont(
        NSToCFPtrCast(substitute_font), font_description.EffectiveFontSize(),
        font_description.SpecifiedSize(), synthetic_bold,
        (traits & NSFontItalicTrait) &&
            !(substitute_font_traits & NSFontItalicTrait),
        font_description.TextRendering(), ResolvedFontFeatures(),
        platform_data.Orientation(), font_description.FontOpticalSizing(),
        /*variation_settings=*/nullptr);
  }

  if (!alternate_font)
    return nullptr;

  return FontDataFromFontPlatformData(alternate_font);
}

const SimpleFontData* FontCache::GetLastResortFallbackFont(
    const FontDescription& font_description) {
  // 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.
  const SimpleFontData* simple_font_data =
      GetFontData(font_description, font_family_names::kTimes,
                  AlternateFontName::kAllowAlternate);
  if (simple_font_data)
    return simple_font_data;

  // 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 GetFontData(font_description, font_family_names::kLucidaGrande,
                     AlternateFontName::kAllowAlternate);
}

const FontPlatformData* FontCache::CreateFontPlatformData(
    const FontDescription& font_description,
    const FontFaceCreationParams& creation_params,
    float size,
    AlternateFontName alternate_name) {
  // CoreText restricts the access to the system dot prefixed fonts, so return
  // nullptr to use fallback font instead.
  if (IsSystemFontName(creation_params.Family())) {
    return nullptr;
  }

  NSFontTraitMask traits = font_description.Style() ? NSFontItalicTrait : 0;

  ScopedCFTypeRef<CTFontRef> matched_font;
  if (alternate_name == AlternateFontName::kLocalUniqueFace &&
      RuntimeEnabledFeatures::FontSrcLocalMatchingEnabled()) {
    matched_font = MatchUniqueFont(creation_params.Family(), size);
  } else if (creation_params.Family() == font_family_names::kSystemUi) {
    matched_font =
        MatchSystemUIFont(font_description.Weight(), font_description.Style(),
                          font_description.Stretch(), size);
  } else if (RuntimeEnabledFeatures::FontMatchingCTMigrationEnabled()) {
    matched_font = MatchFontFamily(
        creation_params.Family(), font_description.Weight(),
        font_description.Style(), font_description.Stretch(), size);
  } else {
    matched_font = ScopedCFTypeRef<CTFontRef>(NSToCFOwnershipCast(
        MatchNSFontFamily(creation_params.Family(), traits,
                          font_description.Weight(), size)));
  }
  if (!matched_font)
    return nullptr;

  bool synthetic_bold, synthetic_italic;
  if (RuntimeEnabledFeatures::FontMatchingCTMigrationEnabled()) {
    CTFontSymbolicTraits matched_font_traits =
        CTFontGetSymbolicTraits(matched_font.get());

    bool desired_bold = font_description.Weight() > FontSelectionValue(500);
    bool matched_font_bold = matched_font_traits & kCTFontTraitBold;
    bool synthetic_bold_requested = (desired_bold && !matched_font_bold) ||
                                    font_description.IsSyntheticBold();
    synthetic_bold =
        synthetic_bold_requested && font_description.SyntheticBoldAllowed();

    bool desired_italic = font_description.Style();
    bool matched_font_italic = matched_font_traits & kCTFontTraitItalic;
    bool synthetic_italic_requested =
        (desired_italic && !matched_font_italic) ||
        font_description.IsSyntheticItalic();
    synthetic_italic =
        synthetic_italic_requested && font_description.SyntheticItalicAllowed();
  } else {
    NSFontManager* font_manager = NSFontManager.sharedFontManager;
    NSFontTraitMask actual_traits = 0;
    if (font_description.Style()) {
      actual_traits =
          [font_manager traitsOfFont:CFToNSPtrCast(matched_font.get())];
    }
    NSInteger actual_weight =
        [font_manager weightOfFont:CFToNSPtrCast(matched_font.get())];

    NSInteger app_kit_weight = ToAppKitFontWeight(font_description.Weight());

    bool synthetic_bold_requested = (IsAppKitFontWeightBold(app_kit_weight) &&
                                     !IsAppKitFontWeightBold(actual_weight)) ||
                                    font_description.IsSyntheticBold();
    synthetic_bold =
        synthetic_bold_requested && font_description.SyntheticBoldAllowed();

    bool synthetic_italic_requested = ((traits & NSFontItalicTrait) &&
                                       !(actual_traits & NSFontItalicTrait)) ||
                                      font_description.IsSyntheticItalic();
    synthetic_italic =
        synthetic_italic_requested && font_description.SyntheticItalicAllowed();
  }

  // FontPlatformData::typeface() is null in the case of Chromium out-of-process
  // font loading failing.  Out-of-process loading occurs for registered fonts
  // stored in non-system locations.  When loading fails, we do not want to use
  // the returned FontPlatformData since it will not have a valid SkTypeface.
  const FontPlatformData* platform_data = FontPlatformDataFromCTFont(
      matched_font.get(), size, font_description.SpecifiedSize(),
      synthetic_bold, synthetic_italic, font_description.TextRendering(),
      ResolvedFontFeatures(), font_description.Orientation(),
      font_description.FontOpticalSizing(),
      font_description.VariationSettings());
  if (!platform_data || !platform_data->Typeface()) {
    return nullptr;
  }
  return platform_data;
}

}  // namespace blink