chromium/third_party/blink/renderer/platform/fonts/mac/font_matcher_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.
 */

#ifdef UNSAFE_BUFFERS_BUILD
// TODO(crbug.com/351564777): Remove this and convert code to safer constructs.
#pragma allow_unsafe_buffers
#endif

#import "third_party/blink/renderer/platform/fonts/mac/font_matcher_mac.h"

#import <AppKit/AppKit.h>
#import <CoreText/CoreText.h>
#import <Foundation/Foundation.h>
#import <math.h>

#include "base/apple/bridging.h"
#include "base/apple/foundation_util.h"
#include "base/apple/scoped_cftyperef.h"
#include "third_party/blink/renderer/platform/fonts/font_cache.h"
#include "third_party/blink/renderer/platform/fonts/font_selection_types.h"
#include "third_party/blink/renderer/platform/runtime_enabled_features.h"
#import "third_party/blink/renderer/platform/wtf/hash_set.h"
#include "third_party/blink/renderer/platform/wtf/text/atomic_string.h"
#import "third_party/blink/renderer/platform/wtf/text/atomic_string_hash.h"
#import "third_party/blink/renderer/platform/wtf/text/string_impl.h"

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

// Forward declare Mac SPIs. `CTFontCopyVariationAxesInternal()` is working
// faster than a public `CTFontCopyVariationAxes()` because it does not
// localize variation axis name string, see
// https://github.com/WebKit/WebKit/commit/1842365d413ed87868e7d33d4fad1691fa3a8129.
// We don't need localized variation axis name, so we can use
// `CTFontCopyVariationAxesInternal()` instead.
// Request for public API: FB13788219.
extern "C" CFArrayRef CTFontCopyVariationAxesInternal(CTFontRef);

namespace blink {

namespace {

const FourCharCode kWeightTag = 'wght';
const FourCharCode kWidthTag = 'wdth';

const int kCTNormalTraitsValue = 0;

CTFontSymbolicTraits kImportantTraitsMask =
    kCTFontTraitItalic | kCTFontTraitBold | kCTFontTraitCondensed |
    kCTFontTraitExpanded;

const NSFontTraitMask SYNTHESIZED_FONT_TRAITS =
    (NSBoldFontMask | NSItalicFontMask);

const NSFontTraitMask IMPORTANT_FONT_TRAITS =
    (NSCompressedFontMask | NSCondensedFontMask | NSExpandedFontMask |
     NSItalicFontMask | NSNarrowFontMask | NSPosterFontMask |
     NSSmallCapsFontMask);

BOOL AcceptableChoice(NSFontTraitMask desired_traits,
                      NSFontTraitMask candidate_traits) {
  desired_traits &= ~SYNTHESIZED_FONT_TRAITS;
  return (candidate_traits & desired_traits) == desired_traits;
}

BOOL BetterChoice(NSFontTraitMask desired_traits,
                  NSInteger desired_weight,
                  NSFontTraitMask chosen_traits,
                  NSInteger chosen_weight,
                  NSFontTraitMask candidate_traits,
                  NSInteger candidate_weight) {
  if (!AcceptableChoice(desired_traits, candidate_traits))
    return NO;

  // 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 kMasks[] = {NSPosterFontMask,    NSSmallCapsFontMask,
                                    NSItalicFontMask,    NSCompressedFontMask,
                                    NSCondensedFontMask, NSExpandedFontMask,
                                    NSNarrowFontMask};

  for (NSFontTraitMask mask : kMasks) {
    BOOL desired = (desired_traits & mask) != 0;
    BOOL chosen_has_unwanted_trait = desired != ((chosen_traits & mask) != 0);
    BOOL candidate_has_unwanted_trait =
        desired != ((candidate_traits & mask) != 0);
    if (!candidate_has_unwanted_trait && chosen_has_unwanted_trait)
      return YES;
    if (!chosen_has_unwanted_trait && candidate_has_unwanted_trait)
      return NO;
  }

  NSInteger chosen_weight_delta_magnitude = abs(chosen_weight - desired_weight);
  NSInteger candidate_weight_delta_magnitude =
      abs(candidate_weight - desired_weight);

  // If both are the same distance from the desired weight, prefer the candidate
  // if it is further from medium.
  if (chosen_weight_delta_magnitude == candidate_weight_delta_magnitude)
    return abs(candidate_weight - 6) > abs(chosen_weight - 6);

  // Otherwise, prefer the one closer to the desired weight.
  return candidate_weight_delta_magnitude < chosen_weight_delta_magnitude;
}

CTFontSymbolicTraits ComputeDesiredTraits(FontSelectionValue desired_weight,
                                          FontSelectionValue desired_slant,
                                          FontSelectionValue desired_width) {
  CTFontSymbolicTraits traits = 0;
  if (desired_weight >= kBoldThreshold) {
    traits |= kCTFontTraitBold;
  }
  if (desired_slant != kNormalSlopeValue) {
    traits |= kCTFontTraitItalic;
  }
  if (desired_width > kNormalWidthValue) {
    traits |= kCTFontTraitExpanded;
  }
  if (desired_width < kNormalWidthValue) {
    traits |= kCTFontTraitCondensed;
  }
  return traits;
}

NSFontTraitMask ComputeDesiredTraitsNS(FontSelectionValue desired_weight,
                                       FontSelectionValue desired_slant,
                                       FontSelectionValue desired_width) {
  NSFontTraitMask traits = 0;
  if (desired_weight >= kBoldThreshold) {
    traits |= NSBoldFontMask;
  }
  if (desired_slant != kNormalSlopeValue) {
    traits |= NSItalicFontMask;
  }
  if (desired_width > kNormalWidthValue) {
    traits |= NSExpandedFontMask;
  }
  if (desired_width < kNormalWidthValue) {
    traits |= NSCondensedFontMask;
  }
  return traits;
}

bool BetterChoiceCT(CTFontSymbolicTraits desired_traits,
                    int desired_weight,
                    CTFontSymbolicTraits chosen_traits,
                    int chosen_weight,
                    CTFontSymbolicTraits candidate_traits,
                    int candidate_weight) {
  // 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 CTFontSymbolicTraits kMasks[] = {kCTFontTraitCondensed,
                                         kCTFontTraitExpanded,
                                         kCTFontTraitItalic, kCTFontTraitBold};

  for (CTFontSymbolicTraits mask : kMasks) {
    // CoreText reports that "HiraginoSans-W5" font with AppKit weight 6 (which
    // we map to CSS weight 500), has a bold trait. Since we consider bold
    // threshold to be CSS weight 600, we will not match this font even if
    // `desired_weight=500` was requested, but instead we will match
    // "HiraginoSans-W4" with AppKit font weight 5 (CSS font weight 400).
    // This check ignores the bold trait value if the `candidate_weight` is the
    // same as requested.
    if (mask == kCTFontBoldTrait && candidate_weight == desired_weight &&
        chosen_weight != desired_weight) {
      return true;
    }
    bool desired = (desired_traits & mask) != 0;
    bool chosen_has_unwanted_trait = desired != ((chosen_traits & mask) != 0);
    bool candidate_has_unwanted_trait =
        desired != ((candidate_traits & mask) != 0);
    if (!candidate_has_unwanted_trait && chosen_has_unwanted_trait) {
      return true;
    }
    if (!chosen_has_unwanted_trait && candidate_has_unwanted_trait) {
      return false;
    }
  }

  int chosen_weight_delta_magnitude = abs(chosen_weight - desired_weight);
  int candidate_weight_delta_magnitude = abs(candidate_weight - desired_weight);

  // If both are the same distance from the desired weight, prefer the candidate
  // if it is further from medium, i.e. 500.
  if (chosen_weight_delta_magnitude == candidate_weight_delta_magnitude) {
    return abs(candidate_weight - 500) > abs(chosen_weight - 500);
  }

  // Otherwise, prefer the one closer to the desired weight.
  return candidate_weight_delta_magnitude < chosen_weight_delta_magnitude;
}

// This function is similar to `BestStyleMatchForFamily` except
// it uses AppKit `availableMembersOfFontFamily` instead of CoreText API
// to retrieve information about the fonts from the desired family.
// `availableMembersOfFontFamily` returns the list of name,
// weight and style of all fonts in family, which we are comparing against
// `desired_traits` and `desired_weight` to find the best matched font's name.
// Unlike `BestStyleMatchForFamily` where we create returned font from the best
// matched font's descriptor, here we are creating the return font from matched
// font's postscript name.
ScopedCFTypeRef<CTFontRef> BestStyleMatchForFamilyNS(
    CFStringRef family_name,
    CTFontSymbolicTraits desired_traits,
    int desired_weight,
    float size) {
  DCHECK(!RuntimeEnabledFeatures::FontFamilyStyleMatchingCTMigrationEnabled());
  NSFontManager* font_manager = NSFontManager.sharedFontManager;
  NSArray<NSArray*>* fonts =
      [font_manager availableMembersOfFontFamily:CFToNSPtrCast(family_name)];

  NSString* matched_font_name;
  CTFontSymbolicTraits chosen_traits;
  int chosen_weight;
  for (NSArray* font_info in fonts) {
    int candidate_weight = kNormalWeightValue;
    NSNumber* candidate_weight_ns = font_info[2];
    if (candidate_weight_ns) {
      candidate_weight = AppKitToCSSFontWeight(candidate_weight_ns.intValue);
    }

    CTFontSymbolicTraits candidate_traits = kCTNormalTraitsValue;
    NSNumber* candidate_traits_ns = font_info[3];
    if (candidate_traits_ns) {
      candidate_traits = candidate_traits_ns.intValue & kImportantTraitsMask;
    }

    if (!matched_font_name ||
        BetterChoiceCT(desired_traits, desired_weight, chosen_traits,
                       chosen_weight, candidate_traits, candidate_weight)) {
      matched_font_name = font_info[0];
      chosen_traits = candidate_traits;
      chosen_weight = candidate_weight;

      if (chosen_weight == desired_weight &&
          (chosen_traits & kImportantTraitsMask) ==
              (desired_traits & kImportantTraitsMask)) {
        break;
      }
    }
  }
  if (!matched_font_name) {
    return ScopedCFTypeRef<CTFontRef>(nullptr);
  }

  return ScopedCFTypeRef<CTFontRef>(
      CTFontCreateWithName(NSToCFPtrCast(matched_font_name), size, nullptr));
}

ScopedCFTypeRef<CTFontRef> BestStyleMatchForFamily(
    CFStringRef family_name,
    CTFontSymbolicTraits desired_traits,
    int desired_weight,
    float size) {
  DCHECK(RuntimeEnabledFeatures::FontFamilyStyleMatchingCTMigrationEnabled());
  // We need the order of the fonts in the family be same as in
  // `availableMembersOfFontFamily` so that the matching results are the same.
  // That's why we don't pass kCTFontCollectionRemoveDuplicatesOption, it might
  // change the order and therefore might change the matching result.
  ScopedCFTypeRef<CTFontCollectionRef> all_system_fonts(
      CTFontCollectionCreateFromAvailableFonts(nullptr));

  ScopedCFTypeRef<CFArrayRef> fonts_in_family(
      CTFontCollectionCreateMatchingFontDescriptorsForFamily(
          all_system_fonts.get(), family_name, NULL));
  if (!fonts_in_family) {
    return ScopedCFTypeRef<CTFontRef>(nullptr);
  }

  ScopedCFTypeRef<CTFontRef> matched_font_in_family;
  CTFontSymbolicTraits chosen_traits;
  int chosen_weight;

  for (CFIndex i = 0; i < CFArrayGetCount(fonts_in_family.get()); ++i) {
    CTFontDescriptorRef descriptor = CFCast<CTFontDescriptorRef>(
        CFArrayGetValueAtIndex(fonts_in_family.get(), i));
    if (!descriptor) {
      continue;
    }

    int candidate_traits = kCTNormalTraitsValue;
    int candidate_weight = kNormalWeightValue;
    ScopedCFTypeRef<CFTypeRef> traits_ref(
        CTFontDescriptorCopyAttribute(descriptor, kCTFontTraitsAttribute));
    NSDictionary* traits =
        CFToNSPtrCast(CFCast<CFDictionaryRef>(traits_ref.get()));
    if (traits) {
      NSNumber* candidate_traits_num =
          ObjCCast<NSNumber>(traits[CFToNSPtrCast(kCTFontSymbolicTrait)]);
      if (candidate_traits_num) {
        candidate_traits = candidate_traits_num.intValue;
      }

      NSNumber* candidate_weight_num =
          ObjCCast<NSNumber>(traits[CFToNSPtrCast(kCTFontWeightTrait)]);
      if (candidate_weight_num) {
        candidate_weight = ToCSSFontWeight(candidate_weight_num.floatValue);
      }
    }

    if (!matched_font_in_family ||
        BetterChoiceCT(desired_traits, desired_weight, chosen_traits,
                       chosen_weight, candidate_traits, candidate_weight)) {
      matched_font_in_family.reset(
          CTFontCreateWithFontDescriptor(descriptor, size, nullptr));
      chosen_traits = candidate_traits;
      chosen_weight = candidate_weight;
      // If we found a font with the exact weight and traits we asked for, we
      // can finish the search and return the font, otherwise we will continue
      // searching among the fonts in family to find the best (not necessarily
      // exact) match in traits and weight.
      if (chosen_weight == desired_weight &&
          (chosen_traits & kImportantTraitsMask) ==
              (desired_traits & kImportantTraitsMask)) {
        return matched_font_in_family;
      }
    }
  }
  return matched_font_in_family;
}

NSFont* MatchByPostscriptNameNS(const AtomicString& desired_family_string,
                                float size) {
  NSString* desired_family = desired_family_string;
  for (NSString* available_font in NSFontManager.sharedFontManager
           .availableFonts) {
    if ([desired_family caseInsensitiveCompare:available_font] ==
        NSOrderedSame) {
      return [NSFont fontWithName:available_font size:size];
    }
  }
  return nullptr;
}

}  // namespace

ScopedCFTypeRef<CTFontRef> MatchUniqueFont(const AtomicString& unique_font_name,
                                           float size) {
  // Note the header documentation: when matching, the system first searches for
  // fonts with its value as their PostScript name, then falls back to searching
  // for fonts with its value as their family name, and then falls back to
  // searching for fonts with its value as their display name.
  ScopedCFTypeRef<CFStringRef> desired_name(
      unique_font_name.Impl()->CreateCFString());
  ScopedCFTypeRef<CTFontRef> matched_font(
      CTFontCreateWithName(desired_name.get(), size, nullptr));
  DCHECK(matched_font);

  // CoreText will usually give us *something* but not always an exactly matched
  // font.
  ScopedCFTypeRef<CFStringRef> matched_postscript_name(
      CTFontCopyPostScriptName(matched_font.get()));
  ScopedCFTypeRef<CFStringRef> matched_full_font_name(
      CTFontCopyFullName(matched_font.get()));
  // If the found font does not match in PostScript name or full font name, it's
  // not the exact match that is required, so return nullptr.
  if (matched_postscript_name &&
      CFStringCompare(matched_postscript_name.get(), desired_name.get(),
                      kCFCompareCaseInsensitive) != kCFCompareEqualTo &&
      matched_full_font_name &&
      CFStringCompare(matched_full_font_name.get(), desired_name.get(),
                      kCFCompareCaseInsensitive) != kCFCompareEqualTo) {
    return ScopedCFTypeRef<CTFontRef>(nullptr);
  }

  return matched_font;
}

void ClampVariationValuesToFontAcceptableRange(
    ScopedCFTypeRef<CTFontRef> ct_font,
    FontSelectionValue& weight,
    FontSelectionValue& width) {
  // `CTFontCopyVariationAxesInternal()` is only supported on MacOS 12+, so
  // we are enabling it only on MacOS 13+ because these are our benchmarking
  // platforms.
  NSArray* all_axes;
  if (@available(macOS 13.0, *)) {
    all_axes =
        CFToNSOwnershipCast(CTFontCopyVariationAxesInternal(ct_font.get()));
  } else {
    all_axes = CFToNSOwnershipCast(CTFontCopyVariationAxes(ct_font.get()));
  }
  if (!all_axes) {
    return;
  }

  for (id id_axis in all_axes) {
    NSDictionary* axis = ObjCCast<NSDictionary>(id_axis);
    if (!axis) {
      continue;
    }

    NSNumber* axis_id = ObjCCast<NSNumber>(
        axis[CFToNSPtrCast(kCTFontVariationAxisIdentifierKey)]);
    if (!axis_id) {
      continue;
    }
    int axis_id_value = axis_id.intValue;

    NSNumber* axis_min_number = ObjCCast<NSNumber>(
        axis[CFToNSPtrCast(kCTFontVariationAxisMinimumValueKey)]);
    if (!axis_min_number) {
      continue;
    }
    double axis_min_value = axis_min_number.doubleValue;

    NSNumber* axis_max_number = ObjCCast<NSNumber>(
        axis[CFToNSPtrCast(kCTFontVariationAxisMaximumValueKey)]);
    if (!axis_max_number) {
      continue;
    }
    double axis_max_value = axis_max_number.doubleValue;

    FontSelectionRange capabilities_range({FontSelectionValue(axis_min_value),
                                           FontSelectionValue(axis_max_value)});

    if (axis_id_value == kWeightTag && weight != kNormalWeightValue) {
      weight = capabilities_range.clampToRange(weight);
    }
    if (axis_id_value == kWidthTag && width != kNormalWidthValue) {
      width = capabilities_range.clampToRange(width);
    }
  }
}

ScopedCFTypeRef<CTFontRef> MatchSystemUIFont(FontSelectionValue desired_weight,
                                             FontSelectionValue desired_slant,
                                             FontSelectionValue desired_width,
                                             float size) {
  ScopedCFTypeRef<CTFontRef> ct_font(
      CTFontCreateUIFontForLanguage(kCTFontUIFontSystem, size, nullptr));
  // CoreText should always return a system-ui font.
  DCHECK(ct_font);

  CTFontSymbolicTraits desired_traits = 0;

  if (desired_slant != kNormalSlopeValue) {
    desired_traits |= kCTFontItalicTrait;
  }

  if (desired_weight >= kBoldThreshold) {
    desired_traits |= kCTFontBoldTrait;
  }

  if (desired_traits) {
    ct_font.reset(CTFontCreateCopyWithSymbolicTraits(
        ct_font.get(), size, nullptr, desired_traits, desired_traits));
  }

  if (desired_weight == kNormalWeightValue &&
      desired_width == kNormalWidthValue) {
    return ct_font;
  }

  ClampVariationValuesToFontAcceptableRange(ct_font, desired_weight,
                                            desired_width);

  NSMutableDictionary* variations = [NSMutableDictionary dictionary];
  if (desired_weight != kNormalWeightValue) {
    variations[@(kWeightTag)] = @(static_cast<float>(desired_weight));
  }
  if (desired_width != kNormalWidthValue) {
    variations[@(kWidthTag)] = @(static_cast<float>(desired_width));
  }

  NSDictionary* attributes = @{
    CFToNSPtrCast(kCTFontVariationAttribute) : variations,
  };

  ScopedCFTypeRef<CTFontDescriptorRef> var_font_desc(
      CTFontDescriptorCreateWithAttributes(NSToCFPtrCast(attributes)));

  return ScopedCFTypeRef<CTFontRef>(CTFontCreateCopyWithAttributes(
      ct_font.get(), size, nullptr, var_font_desc.get()));
}

// We first attempt to find a match by `desired_family_string` family name. If
// we failed to do so, we then try to find a match by postscript name. If during
// postscript matching we found font that has desired traits we will return it,
// otherwise we will do one more pass of family matching with the found with
// postscript matching font's family name.
// We perform matching by PostScript name for legacy and compatibility reasons
// (Safari also does it), although CSS specs do not require that, see
// crbug.com/641861.
ScopedCFTypeRef<CTFontRef> MatchFontFamily(
    const AtomicString& desired_family_string,
    FontSelectionValue desired_weight,
    FontSelectionValue desired_slant,
    FontSelectionValue desired_width,
    float size) {
  if (!desired_family_string) {
    return ScopedCFTypeRef<CTFontRef>(nullptr);
  }
  ScopedCFTypeRef<CFStringRef> desired_name(
      desired_family_string.Impl()->CreateCFString());

  // Due to the way we detect whether we can in-process load a font using
  // `CanLoadInProcess`, compare
  // third_party/blink/renderer/platform/fonts/mac/font_platform_data_mac.mm,
  // we cannot match the LastResort font on Mac.
  // TODO(crbug.com/1519877): We should allow matching LastResort font.
  if (CFStringCompare(desired_name.get(), CFSTR("LastResort"),
                      kCFCompareCaseInsensitive) == kCFCompareEqualTo) {
    return ScopedCFTypeRef<CTFontRef>(nullptr);
  }

  CTFontSymbolicTraits desired_traits =
      ComputeDesiredTraits(desired_weight, desired_slant, desired_width);

  // CoreText's API for retrieving all system fonts from desired family
  // is working much slower than AppKits `availableMembersOfFontFamily`.
  // Filed in Apple Feedback Assistant, FB13615032.
  // This caused several performance regressions, compare
  // https://crbug.com/328483352. While we await feedback from
  // Apple, we re-introduce the previous AppKit-based style matching, using
  // NSFontManager availableMembersOfFontFamily API. We gate this change on a
  // separate flag.
  ScopedCFTypeRef<CTFontRef> match_in_family =
      RuntimeEnabledFeatures::FontFamilyStyleMatchingCTMigrationEnabled()
          ? BestStyleMatchForFamily(desired_name.get(), desired_traits,
                                    desired_weight, size)
          : BestStyleMatchForFamilyNS(desired_name.get(), desired_traits,
                                      desired_weight, size);

  if (!match_in_family) {
    // We first try to find font by postscript name. If the found font has
    // desired traits we will return it otherwise we will try to find the best
    // match in the found font's family.
    if (RuntimeEnabledFeatures::
            FontFamilyPostscriptMatchingCTMigrationEnabled()) {
      ScopedCFTypeRef<CTFontRef> matched_font(
          CTFontCreateWithName(desired_name.get(), size, nullptr));
      ScopedCFTypeRef<CFStringRef> matched_postscript_name(
          CTFontCopyPostScriptName(matched_font.get()));
      if (matched_postscript_name &&
          CFStringCompare(matched_postscript_name.get(), desired_name.get(),
                          kCFCompareCaseInsensitive) == kCFCompareEqualTo) {
        CTFontSymbolicTraits traits =
            CTFontGetSymbolicTraits(matched_font.get());
        if ((desired_traits & traits) == desired_traits) {
          return matched_font;
        }

        ScopedCFTypeRef<CFStringRef> matched_family_name(
            CTFontCopyFamilyName(matched_font.get()));
        return RuntimeEnabledFeatures::
                       FontFamilyStyleMatchingCTMigrationEnabled()
                   ? BestStyleMatchForFamily(matched_family_name.get(),
                                             desired_traits, desired_weight,
                                             size)
                   : BestStyleMatchForFamilyNS(matched_family_name.get(),
                                               desired_traits, desired_weight,
                                               size);
      }
    } else {
      NSFont* postscript_match_font =
          MatchByPostscriptNameNS(desired_family_string, size);
      if (postscript_match_font) {
        NSFontTraitMask desired_traits_ns = ComputeDesiredTraitsNS(
            desired_weight, desired_slant, desired_width);
        NSFontManager* font_manager = NSFontManager.sharedFontManager;
        NSFontTraitMask traits =
            [font_manager traitsOfFont:postscript_match_font];
        if ((traits & desired_traits_ns) == desired_traits_ns) {
          return ScopedCFTypeRef<CTFontRef>(NSToCFOwnershipCast([font_manager
              convertFont:postscript_match_font
              toHaveTrait:desired_traits_ns]));
        }

        return RuntimeEnabledFeatures::
                       FontFamilyStyleMatchingCTMigrationEnabled()
                   ? BestStyleMatchForFamily(
                         NSToCFPtrCast(postscript_match_font.familyName),
                         desired_traits, desired_weight, size)
                   : BestStyleMatchForFamilyNS(
                         NSToCFPtrCast(postscript_match_font.familyName),
                         desired_traits, desired_weight, size);
      }
    }
  }
  return match_in_family;
}

// Family name is somewhat of a misnomer here.  We first attempt to find an
// exact match comparing the desiredFamily to the PostScript name of the
// installed fonts.  If that fails we then do a search based on the family
// names of the installed fonts.
NSFont* MatchNSFontFamily(const AtomicString& desired_family_string,
                          NSFontTraitMask desired_traits,
                          FontSelectionValue desired_weight,
                          float size) {
  DCHECK_NE(desired_family_string, FontCache::LegacySystemFontFamily());

  NSString* desired_family = desired_family_string;
  NSFontManager* font_manager = NSFontManager.sharedFontManager;

  // From macOS 10.15 `NSFontManager.availableFonts` does not list certain
  // fonts that availableMembersOfFontFamily actually shows results for, for
  // example "Hiragino Kaku Gothic ProN" is not listed, only Hiragino Sans is
  // listed. We previously enumerated availableFontFamilies and looked for a
  // case-insensitive string match here, but instead, we can rely on
  // availableMembersOfFontFamily here to do a case-insensitive comparison, then
  // set available_family to desired_family if the result was not empty.
  // See https://crbug.com/1000542
  NSString* available_family = nil;
  NSArray* fonts_in_family =
      [font_manager availableMembersOfFontFamily:desired_family];
  if (fonts_in_family && fonts_in_family.count) {
    available_family = desired_family;
  }

  NSInteger app_kit_font_weight = ToAppKitFontWeight(desired_weight);
  if (!available_family) {
    // Match by PostScript name.
    NSFont* name_matched_font =
        MatchByPostscriptNameNS(desired_family_string, size);
    if (!name_matched_font) {
      return nil;
    }

    available_family = name_matched_font.familyName;
    NSFontTraitMask desired_traits_for_name_match =
        desired_traits | (app_kit_font_weight >= 7 ? NSBoldFontMask : 0);

    // Special case Osaka-Mono.  According to <rdar://problem/3999467>, we
    // need to treat Osaka-Mono as fixed pitch.
    if ([available_family caseInsensitiveCompare:@"Osaka-Mono"] ==
            NSOrderedSame &&
        desired_traits_for_name_match == 0) {
      return name_matched_font;
    }

    NSFontTraitMask traits = [font_manager traitsOfFont:name_matched_font];
    if ((traits & desired_traits_for_name_match) ==
        desired_traits_for_name_match) {
      return [font_manager convertFont:name_matched_font
                           toHaveTrait:desired_traits_for_name_match];
    }
  }

  // Found a family, now figure out what weight and traits to use.
  BOOL chose_font = false;
  NSInteger chosen_weight = 0;
  NSFontTraitMask chosen_traits = 0;
  NSString* chosen_full_name = nil;

  NSArray<NSArray*>* fonts =
      [font_manager availableMembersOfFontFamily:available_family];
  for (NSArray* font_info in fonts) {
    // Each font is represented by an array of four elements:

    // TODO(https://crbug.com/1442008): The docs say that font_info[0] is the
    // PostScript name of the font, but it's treated as the full name here.
    // Either the docs are wrong and we should note that here for future readers
    // of the code, or the docs are right and we should fix this.
    // https://developer.apple.com/documentation/appkit/nsfontmanager/1462316-availablemembersoffontfamily
    NSString* font_full_name = font_info[0];
    // font_info[1] is "the part of the font name used in the font panel that's
    // not the font name". This is not needed.
    NSInteger font_weight = [font_info[2] intValue];
    NSFontTraitMask font_traits = [font_info[3] unsignedIntValue];

    BOOL new_winner;
    if (!chose_font) {
      new_winner = AcceptableChoice(desired_traits, font_traits);
    } else {
      new_winner =
          BetterChoice(desired_traits, app_kit_font_weight, chosen_traits,
                       chosen_weight, font_traits, font_weight);
    }

    if (new_winner) {
      chose_font = YES;
      chosen_weight = font_weight;
      chosen_traits = font_traits;
      chosen_full_name = font_full_name;

      if (chosen_weight == app_kit_font_weight &&
          (chosen_traits & IMPORTANT_FONT_TRAITS) ==
              (desired_traits & IMPORTANT_FONT_TRAITS))
        break;
    }
  }

  if (!chose_font)
    return nil;

  NSFont* font = [NSFont fontWithName:chosen_full_name size:size];

  if (!font)
    return nil;

  if (RuntimeEnabledFeatures::MacFontsDeprecateFontTraitsWorkaroundEnabled()) {
    return font;
  }

  NSFontTraitMask actual_traits = 0;
  if (desired_traits & NSFontItalicTrait)
    actual_traits = [font_manager traitsOfFont:font];
  NSInteger actual_weight = [font_manager weightOfFont:font];

  bool synthetic_bold = app_kit_font_weight >= 7 && actual_weight < 7;
  bool synthetic_italic = (desired_traits & NSFontItalicTrait) &&
                          !(actual_traits & 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 non_synthetic_traits = desired_traits;

  if (synthetic_bold)
    non_synthetic_traits &= ~NSBoldFontMask;

  if (synthetic_italic)
    non_synthetic_traits &= ~NSItalicFontMask;

  if (non_synthetic_traits != desired_traits) {
    NSFont* font_without_synthetic_traits =
        [font_manager fontWithFamily:available_family
                              traits:non_synthetic_traits
                              weight:chosen_weight
                                size:size];
    if (font_without_synthetic_traits)
      font = font_without_synthetic_traits;
  }

  return font;
}

int ToAppKitFontWeight(FontSelectionValue font_weight) {
  float weight = font_weight;
  if (weight <= 50 || weight >= 950)
    return 5;

  size_t select_weight = roundf(weight / 100) - 1;
  static int app_kit_font_weights[] = {
      2,   // FontWeight100
      3,   // FontWeight200
      4,   // FontWeight300
      5,   // FontWeight400
      6,   // FontWeight500
      8,   // FontWeight600
      9,   // FontWeight700
      10,  // FontWeight800
      12,  // FontWeight900
  };
  DCHECK_GE(select_weight, 0ul);
  DCHECK_LE(select_weight, std::size(app_kit_font_weights));
  return app_kit_font_weights[select_weight];
}

// CoreText font weight ranges are taken from `GetFontWeightFromCTFont` in
// `ui/gfx/platform_font_mac.mm`
int ToCSSFontWeight(float ct_font_weight) {
  constexpr struct {
    float weight_lower;
    float weight_upper;
    int css_weight;
  } weights[] = {
      {-1.0, -0.70, 100},   // Thin (Hairline)
      {-0.70, -0.45, 200},  // Extra Light (Ultra Light)
      {-0.45, -0.10, 300},  // Light
      {-0.10, 0.10, 400},   // Normal (Regular)
      {0.10, 0.27, 500},    // Medium
      {0.27, 0.35, 600},    // Semi Bold (Demi Bold)
      {0.35, 0.50, 700},    // Bold
      {0.50, 0.60, 800},    // Extra Bold (Ultra Bold)
      {0.60, 1.0, 900},     // Black (Heavy)
  };
  for (const auto& item : weights) {
    if (item.weight_lower <= ct_font_weight &&
        ct_font_weight <= item.weight_upper) {
      return item.css_weight;
    }
  }
  return kNormalWeightValue;
}

float ToCTFontWeight(int css_weight) {
  if (css_weight <= 50 || css_weight >= 950) {
    return 0.0;
  }
  const float weights[] = {
      -0.80,  // Thin (Hairline)
      -0.60,  // Extra Light (Ultra Light)
      -0.40,  // Light
      0.0,    // Normal (Regular)
      0.23,   // Medium
      0.30,   // Semi Bold (Demi Bold)
      0.40,   // Bold
      0.56,   // Extra Bold (Ultra Bold)
      0.62,   // Black (Heavy)
  };
  int index = (css_weight - 50) / 100;
  return weights[index];
}

// AppKit font weight ranges are taken from `ToNSFontManagerWeight` in
// `ui/gfx/platform_font_mac.mm`.
int AppKitToCSSFontWeight(int appkit_font_weight) {
  if (appkit_font_weight < 0) {
    return kNormalWeightValue;
  }
  if (appkit_font_weight < 7) {
    return std::max((appkit_font_weight - 1) * 100,
                    static_cast<int>(kThinWeightValue));
  }
  return std::min((appkit_font_weight - 2) * 100,
                  static_cast<int>(kBlackWeightValue));
}

}  // namespace blink