blob: 9dea6d2172b63ca3dc4942ef5acd51ce751b47c5 [file] [log] [blame]
// Copyright (c) 2011 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "content/common/font_list.h"
#import <Cocoa/Cocoa.h>
#include <CoreFoundation/CoreFoundation.h>
#include <CoreText/CoreText.h>
#include <utility>
#include "base/logging.h"
#include "base/mac/foundation_util.h"
#include "base/mac/scoped_cftyperef.h"
#include "base/strings/sys_string_conversions.h"
#include "base/values.h"
// The code here is unusually skeptical about the macOS APIs returning non-null
// values. An earlier version was reverted due to crashing tests on bots running
// older macOS versions. The DCHECKs are there to expedite debugging similar
// problems.
namespace content {
namespace {
// Core Text-based localized family name lookup.
//
// This class is not thread-safe.
//
// This class caches some state for an efficient implementation of
// [NSFontManager localizedNameForFamily:face:] using the Core Text API.
class FontFamilyResolver {
public:
FontFamilyResolver() {
DCHECK(mandatory_attributes_ != nullptr);
DCHECK(font_descriptor_attributes_ != nullptr);
}
~FontFamilyResolver() = default;
FontFamilyResolver(const FontFamilyResolver&) = delete;
FontFamilyResolver& operator=(const FontFamilyResolver&) = delete;
// Returns a localized font family name for the given family name.
base::ScopedCFTypeRef<CFStringRef> CopyLocalizedFamilyName(
CFStringRef family_name) {
DCHECK(family_name != nullptr);
CFDictionarySetValue(font_descriptor_attributes_.get(),
kCTFontFamilyNameAttribute, family_name);
base::ScopedCFTypeRef<CTFontDescriptorRef> raw_descriptor(
CTFontDescriptorCreateWithAttributes(
font_descriptor_attributes_.get()));
DCHECK(raw_descriptor != nullptr)
<< "CTFontDescriptorCreateWithAttributes returned null";
base::ScopedCFTypeRef<CFArrayRef> normalized_descriptors(
CTFontDescriptorCreateMatchingFontDescriptors(
raw_descriptor, mandatory_attributes_.get()));
return CopyLocalizedFamilyNameFrom(family_name,
normalized_descriptors.get());
}
// True if the font should be hidden from Chrome.
//
// On macOS 10.15, CTFontManagerCopyAvailableFontFamilyNames() filters hidden
// fonts. This is not true on older version of macOS that Chrome still
// supports. The unittest FontTest.GetFontListDoesNotIncludeHiddenFonts can be
// used to determine when it's safe to slim down / remove this function.
static bool IsHiddenFontFamily(CFStringRef family_name) {
DCHECK(family_name != nullptr);
DCHECK_GT(CFStringGetLength(family_name), 0);
// macOS 10.13 includes names that start with . (period). These fonts should
// not be shown to users.
return CFStringGetCharacterAtIndex(family_name, 0) == '.';
}
private:
// Returns the first font descriptor matching the given family name.
//
// `descriptors` must be an array of CTFontDescriptors whose font family name
// attribute is populated.
//
// `descriptors` may be null, representing an empty array. This case is
// handled because CTFontDescriptorCreateMatchingFontDescriptors() may
// return null, even on macOS 11. Discovery documented in crbug.com/1235042.
//
// Returns null if none of the descriptors match.
static base::ScopedCFTypeRef<CTFontDescriptorRef> FindFirstWithFamilyName(
CFStringRef family_name,
CFArrayRef descriptors) {
DCHECK(family_name != nullptr);
CFIndex descriptor_count = descriptors ? CFArrayGetCount(descriptors) : 0;
for (CFIndex i = 0; i < descriptor_count; ++i) {
CTFontDescriptorRef descriptor =
base::mac::CFCastStrict<CTFontDescriptorRef>(
CFArrayGetValueAtIndex(descriptors, i));
DCHECK(descriptor != nullptr)
<< "The descriptors array has a null element.";
base::ScopedCFTypeRef<CFStringRef> descriptor_family_name(
base::mac::CFCastStrict<CFStringRef>(CTFontDescriptorCopyAttribute(
descriptor, kCTFontFamilyNameAttribute)));
if (CFStringCompare(family_name, descriptor_family_name,
/*compareOptions=*/0) == kCFCompareEqualTo) {
return base::ScopedCFTypeRef<CTFontDescriptorRef>(
descriptor, base::scoped_policy::RETAIN);
}
}
return base::ScopedCFTypeRef<CTFontDescriptorRef>(nullptr);
}
// Returns a localized font family name for the given family name.
//
// `descriptors` must be an array of normalized CTFontDescriptors representing
// all the font descriptors on the system matching the given family name.
//
// `descriptors` may be null, representing an empty array. This case is
// handled because CTFontDescriptorCreateMatchingFontDescriptors() may
// return null, even on macOS 11. Discovery documented in crbug.com/1235042.
//
// The given family name is returned as a fallback, if none of the descriptors
// match the desired font family name.
static base::ScopedCFTypeRef<CFStringRef> CopyLocalizedFamilyNameFrom(
CFStringRef family_name,
CFArrayRef descriptors) {
DCHECK(family_name != nullptr);
base::ScopedCFTypeRef<CTFontDescriptorRef> descriptor =
FindFirstWithFamilyName(family_name, descriptors);
if (descriptor == nullptr) {
DLOG(WARNING) << "Will use non-localized family name for font family: "
<< family_name;
return base::ScopedCFTypeRef<CFStringRef>(family_name,
base::scoped_policy::RETAIN);
}
base::ScopedCFTypeRef<CFStringRef> localized_family_name(
base::mac::CFCastStrict<CFStringRef>(
CTFontDescriptorCopyLocalizedAttribute(descriptor,
kCTFontFamilyNameAttribute,
/*language=*/nullptr)));
// CTFontDescriptorCopyLocalizedAttribute() is only supposed to return null
// if the desired attribute (family name) is not present.
//
// We found that the function may return null, even when given a normalized
// font descriptor whose attribute (family name) exists --
// FindFirstWithFamilyName() only returns descriptors whose non-localized
// family name attribute is equal to a given string. Discovery documented in
// crbug.com/1235090.
if (localized_family_name == nullptr) {
DLOG(WARNING) << "Will use non-localized family name for font family: "
<< family_name;
return base::ScopedCFTypeRef<CFStringRef>(family_name,
base::scoped_policy::RETAIN);
}
return localized_family_name;
}
// Creates the set stored in |mandatory_attributes_|.
static base::ScopedCFTypeRef<CFSetRef> CreateMandatoryAttributes() {
CFStringRef set_values[] = {kCTFontFamilyNameAttribute};
return base::ScopedCFTypeRef<CFSetRef>(CFSetCreate(
kCFAllocatorDefault, reinterpret_cast<const void**>(set_values),
base::size(set_values), &kCFTypeSetCallBacks));
}
// Creates the mutable dictionary stored in |font_descriptor_attributes_|.
static base::ScopedCFTypeRef<CFMutableDictionaryRef>
CreateFontDescriptorAttributes() {
return base::ScopedCFTypeRef<CFMutableDictionaryRef>(
CFDictionaryCreateMutable(kCFAllocatorDefault, /*capacity=*/1,
&kCFTypeDictionaryKeyCallBacks,
&kCFTypeDictionaryValueCallBacks));
}
// Used for all CTFontDescriptorCreateMatchingFontDescriptors() calls.
//
// Caching this dictionary saves one dictionary creation per lookup.
const base::ScopedCFTypeRef<CFSetRef> mandatory_attributes_ =
CreateMandatoryAttributes();
// Used for all CTFontDescriptorCreateMatchingFontDescriptors() calls.
//
// This dictionary has exactly one key, kCTFontFamilyNameAttribute. The value
// associated with the key is overwritten as needed.
//
// Caching this dictionary saves one dictionary creation per lookup.
const base::ScopedCFTypeRef<CFMutableDictionaryRef>
font_descriptor_attributes_ = CreateFontDescriptorAttributes();
};
} // namespace
std::unique_ptr<base::ListValue> GetFontList_SlowBlocking() {
@autoreleasepool {
FontFamilyResolver resolver;
base::ScopedCFTypeRef<CFArrayRef> cf_family_names(
CTFontManagerCopyAvailableFontFamilyNames());
DCHECK(cf_family_names != nullptr)
<< "CTFontManagerCopyAvailableFontFamilyNames returned null";
NSArray* family_names = base::mac::CFToNSCast(cf_family_names.get());
// Maps localized font family names to non-localized names.
NSMutableDictionary* family_name_map = [NSMutableDictionary
dictionaryWithCapacity:CFArrayGetCount(cf_family_names)];
for (NSString* family_name in family_names) {
DCHECK(family_name != nullptr)
<< "CTFontManagerCopyAvailableFontFamilyNames returned an array with "
<< "a null element";
CFStringRef family_name_cf = base::mac::NSToCFCast(family_name);
if (FontFamilyResolver::IsHiddenFontFamily(family_name_cf))
continue;
base::ScopedCFTypeRef<CFStringRef> cf_normalized_family_name =
resolver.CopyLocalizedFamilyName(family_name_cf);
DCHECK(cf_normalized_family_name != nullptr)
<< "FontFamilyResolver::CopyLocalizedFamilyName returned null";
family_name_map[base::mac::CFToNSCast(cf_normalized_family_name)] =
family_name;
}
// The Apple documentation for CTFontManagerCopyAvailableFontFamilyNames
// states that it returns family names sorted for user interface display.
// https://developer.apple.com/documentation/coretext/1499494-ctfontmanagercopyavailablefontfa
//
// This doesn't seem to be the case, at least on macOS 10.15.3.
NSArray* sorted_localized_family_names = [family_name_map
keysSortedByValueUsingSelector:@selector(localizedStandardCompare:)];
std::vector<base::Value> font_list;
for (NSString* localized_family_name in sorted_localized_family_names) {
NSString* family_name = family_name_map[localized_family_name];
std::vector<base::Value> font_list_item;
font_list_item.reserve(2);
font_list_item.emplace_back(base::SysNSStringToUTF8(family_name));
font_list_item.emplace_back(
base::SysNSStringToUTF8(localized_family_name));
font_list.emplace_back(std::move(font_list_item));
}
return std::make_unique<base::ListValue>(base::Value(font_list).TakeList());
}
}
} // namespace content