// Copyright 2015 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#import "third_party/blink/renderer/platform/fonts/mac/font_matcher_mac.h"
#import <AppKit/AppKit.h>
#import <CoreText/CoreText.h>
#include <Foundation/Foundation.h>
#include "base/apple/bridging.h"
#import "base/apple/foundation_util.h"
#include "base/apple/scoped_cftyperef.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/renderer/platform/font_family_names.h"
#include "third_party/blink/renderer/platform/fonts/font_selection_types.h"
#include "third_party/blink/renderer/platform/runtime_enabled_features.h"
#include "third_party/blink/renderer/platform/testing/runtime_enabled_features_test_helpers.h"
#include "third_party/blink/renderer/platform/wtf/text/atomic_string.h"
using base::apple::CFToNSOwnershipCast;
using base::apple::CFToNSPtrCast;
using base::apple::NSToCFOwnershipCast;
using base::apple::ObjCCast;
using base::apple::ScopedCFTypeRef;
namespace blink {
namespace {
struct FontName {
const char* full_font_name;
const char* postscript_name;
const char* family_name;
};
// If these font names are unavailable on future Mac OS versions, please try to
// find replacements or remove individual lines.
const FontName FontNames[] = {
{"American Typewriter Condensed Light", "AmericanTypewriter-CondensedLight",
"American Typewriter"},
{"Apple Braille Outline 6 Dot", "AppleBraille-Outline6Dot",
"Apple Braille"},
{"Arial Narrow Bold Italic", "ArialNarrow-BoldItalic", "Arial Narrow"},
{"Baskerville SemiBold Italic", "Baskerville-SemiBoldItalic",
"Baskerville"},
{"Devanagari MT", "DevanagariMT", "Devanagari MT"},
{"DIN Alternate Bold", "DINAlternate-Bold", "DIN Alternate"},
{"Gill Sans Light Italic", "GillSans-LightItalic", "Gill Sans"},
{"Malayalam Sangam MN", "MalayalamSangamMN", "Malayalam Sangam MN"},
{"Hiragino Maru Gothic ProN W4", "HiraMaruProN-W4",
"Hiragino Maru Gothic ProN"},
{"Hiragino Sans W3", "HiraginoSans-W3", "Hiragino Sans"},
};
const FontName CommonFontNames[] = {
{"Avenir-Roman", "Avenir-Roman", "Avenir"},
{"CourierNewPS-BoldMT", "CourierNewPS-BoldMT", "Courier New"},
{"Helvetica-Light", "Helvetica-Light", "Helvetica"},
{"HelveticaNeue-CondensedBlack", "HelveticaNeue-CondensedBlack",
"Helvetica Neue"},
{"Menlo-Bold", "Menlo-Bold", "Menlo"},
{"Tahoma", "Tahoma", "Tahoma"},
{"TimesNewRomanPS-BoldItalicMT", "TimesNewRomanPS-BoldItalicMT",
"Times New Roman"},
};
const char* FamiliesWithBoldItalicFaces[] = {"Baskerville", "Cochin", "Georgia",
"GillSans"};
ScopedCFTypeRef<CTFontRef> MatchCTFontFamily(const AtomicString& font_name,
FontSelectionValue desired_weight,
FontSelectionValue desired_slant,
FontSelectionValue desired_width,
float size) {
if (RuntimeEnabledFeatures::FontMatchingCTMigrationEnabled()) {
return MatchFontFamily(font_name, desired_weight, desired_slant,
desired_width, size);
}
NSFontTraitMask traits = 0;
if (desired_slant != kNormalSlopeValue) {
traits |= NSFontItalicTrait;
}
if (desired_width > kNormalWidthValue) {
traits |= NSFontExpandedTrait;
}
if (desired_width < kNormalWidthValue) {
traits |= NSFontCondensedTrait;
}
return ScopedCFTypeRef<CTFontRef>(NSToCFOwnershipCast(
MatchNSFontFamily(font_name, traits, desired_weight, size)));
}
void TestFontWithBoldAndItalicTraits(const AtomicString& font_name) {
ScopedCFTypeRef<CTFontRef> font_italic = MatchCTFontFamily(
font_name, kNormalWeightValue, kItalicSlopeValue, kNormalWidthValue, 11);
EXPECT_TRUE(font_italic);
CTFontSymbolicTraits italic_font_traits =
CTFontGetSymbolicTraits(font_italic.get());
EXPECT_TRUE(italic_font_traits & kCTFontTraitItalic);
ScopedCFTypeRef<CTFontRef> font_bold_italic = MatchCTFontFamily(
font_name, kBoldWeightValue, kItalicSlopeValue, kNormalWidthValue, 11);
EXPECT_TRUE(font_bold_italic);
CTFontSymbolicTraits bold_italic_font_traits =
CTFontGetSymbolicTraits(font_bold_italic.get());
EXPECT_TRUE(bold_italic_font_traits & kCTFontTraitItalic);
EXPECT_TRUE(bold_italic_font_traits & kCTFontTraitBold);
}
void TestFontMatchingByFamilyName(const char* font_name) {
ScopedCFTypeRef<CTFontRef> font =
MatchCTFontFamily(AtomicString(font_name), kNormalWeightValue,
kNormalSlopeValue, kNormalWidthValue, 11);
EXPECT_TRUE(font);
ScopedCFTypeRef<CFStringRef> matched_family_name(
CTFontCopyFamilyName(font.get()));
ScopedCFTypeRef<CFStringRef> expected_family_name(
CFStringCreateWithCString(nullptr, font_name, kCFStringEncodingUTF8));
EXPECT_EQ(
CFStringCompare(matched_family_name.get(), expected_family_name.get(),
kCFCompareCaseInsensitive),
kCFCompareEqualTo);
}
void TestFontMatchingByPostscriptName(const char* font_name) {
ScopedCFTypeRef<CTFontRef> font =
MatchCTFontFamily(AtomicString(font_name), kNormalWeightValue,
kNormalSlopeValue, kNormalWidthValue, 11);
EXPECT_TRUE(font);
ScopedCFTypeRef<CFStringRef> matched_postscript_name(
CTFontCopyPostScriptName(font.get()));
ScopedCFTypeRef<CFStringRef> expected_postscript_name(
CFStringCreateWithCString(nullptr, font_name, kCFStringEncodingUTF8));
EXPECT_EQ(CFStringCompare(matched_postscript_name.get(),
expected_postscript_name.get(),
kCFCompareCaseInsensitive),
kCFCompareEqualTo);
}
void TestCTAndNSMatchEqual(const char* font_name,
float size,
int weight,
int style,
int stretch) {
ScopedCFTypeRef<CTFontRef> matched_font = MatchFontFamily(
AtomicString(font_name), FontSelectionValue(weight),
FontSelectionValue(style), FontSelectionValue(stretch), size);
NSFontTraitMask traits = (style != kNormalSlopeValue) ? NSFontItalicTrait : 0;
ScopedCFTypeRef<CTFontRef> matched_ns_font(
base::apple::NSToCFOwnershipCast(MatchNSFontFamily(
AtomicString(font_name), traits, FontSelectionValue(weight), size)));
if (matched_font || matched_ns_font) {
EXPECT_TRUE(matched_font);
EXPECT_TRUE(matched_ns_font);
ScopedCFTypeRef<CFStringRef> matched_font_name(
CTFontCopyPostScriptName(matched_font.get()));
EXPECT_TRUE(matched_font_name);
ScopedCFTypeRef<CFStringRef> matched_ns_font_name(
CTFontCopyPostScriptName(matched_ns_font.get()));
EXPECT_TRUE(matched_ns_font_name);
EXPECT_TRUE(
CFStringCompare(matched_font_name.get(), matched_ns_font_name.get(),
kCFCompareCaseInsensitive) == kCFCompareEqualTo);
}
}
} // namespace
TEST(FontMatcherMacTest, MatchSystemFont) {
ScopedCFTypeRef<CTFontRef> font = MatchSystemUIFont(
kNormalWeightValue, kNormalSlopeValue, kNormalWidthValue, 11);
EXPECT_TRUE(font);
}
TEST(FontMatcherMacTest, MatchSystemFontItalic) {
ScopedCFTypeRef<CTFontRef> font = MatchSystemUIFont(
kNormalWeightValue, kItalicSlopeValue, kNormalWidthValue, 11);
EXPECT_TRUE(font);
NSDictionary* traits = CFToNSOwnershipCast(CTFontCopyTraits(font.get()));
NSNumber* slant_num =
ObjCCast<NSNumber>(traits[CFToNSPtrCast(kCTFontSlantTrait)]);
float slant = slant_num.floatValue;
EXPECT_NE(slant, 0.0);
}
TEST(FontMatcherMacTest, MatchSystemFontWithWeightVariations) {
// Mac SystemUI font supports weight variations between 1 and 1000.
int min_weight = 1;
int max_weight = 1000;
FourCharCode wght_tag = 'wght';
for (int weight = min_weight - 1; weight <= max_weight + 1; weight += 50) {
if (weight != kNormalWeightValue) {
ScopedCFTypeRef<CTFontRef> font = MatchSystemUIFont(
FontSelectionValue(weight), kNormalSlopeValue, kNormalWidthValue, 11);
EXPECT_TRUE(font);
NSDictionary* variations =
CFToNSOwnershipCast(CTFontCopyVariation(font.get()));
NSNumber* actual_weight_num = ObjCCast<NSNumber>(variations[@(wght_tag)]);
EXPECT_TRUE(actual_weight_num);
float actual_weight = actual_weight_num.floatValue;
float expected_weight =
std::max(min_weight, std::min(max_weight, weight));
EXPECT_EQ(actual_weight, expected_weight);
}
}
}
TEST(FontMatcherMacTest, MatchSystemFontWithWidthVariations) {
// Mac SystemUI font supports width variations between 30 and 150.
int min_width = 30;
int max_width = 150;
FourCharCode wdth_tag = 'wdth';
for (int width = min_width - 10; width <= max_width + 10; width += 10) {
if (width != kNormalWidthValue) {
ScopedCFTypeRef<CTFontRef> font = MatchSystemUIFont(
kNormalWidthValue, kNormalSlopeValue, FontSelectionValue(width), 11);
EXPECT_TRUE(font);
NSDictionary* variations =
CFToNSOwnershipCast(CTFontCopyVariation(font.get()));
NSNumber* actual_width_num = ObjCCast<NSNumber>(variations[@(wdth_tag)]);
EXPECT_TRUE(actual_width_num);
float actual_width = actual_width_num.floatValue;
float expected_width = std::max(min_width, std::min(max_width, width));
EXPECT_EQ(actual_width, expected_width);
}
}
}
TEST(FontMatcherMacTest, FontFamilyMatchingUnavailableFont) {
ScopedCFTypeRef<CTFontRef> font = MatchCTFontFamily(
AtomicString(
"ThisFontNameDoesNotExist07F444B9-4DDF-4A41-8F30-C80D4ED4CCA2"),
kNormalWeightValue, kNormalSlopeValue, kNormalWidthValue, 12);
EXPECT_FALSE(font);
}
TEST(FontMatcherMacTest, FontFamilyMatchingLastResortFont) {
ScopedCFTypeRef<CTFontRef> last_resort_font =
MatchCTFontFamily(AtomicString("lastresort"), kNormalWeightValue,
kNormalSlopeValue, kNormalWidthValue, 11);
EXPECT_FALSE(last_resort_font);
ScopedCFTypeRef<CTFontRef> last_resort_font_bold =
MatchCTFontFamily(AtomicString("lastresort"), kBoldWeightValue,
kNormalSlopeValue, kNormalWidthValue, 11);
EXPECT_FALSE(last_resort_font_bold);
}
TEST(FontMatcherMacTest, MatchUniqueUnavailableFont) {
ScopedCFTypeRef<CTFontRef> font = MatchUniqueFont(
AtomicString(
"ThisFontNameDoesNotExist07F444B9-4DDF-4A41-8F30-C80D4ED4CCA2"),
12);
EXPECT_FALSE(font);
}
class TestFontMatchingByName : public testing::TestWithParam<FontName> {};
INSTANTIATE_TEST_SUITE_P(FontMatcherMacTest,
TestFontMatchingByName,
testing::ValuesIn(FontNames));
INSTANTIATE_TEST_SUITE_P(CommonFontMatcherMacTest,
TestFontMatchingByName,
testing::ValuesIn(CommonFontNames));
// 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.
TEST_P(TestFontMatchingByName, MatchingByFamilyName) {
const FontName font_name = TestFontMatchingByName::GetParam();
TestFontMatchingByFamilyName(font_name.family_name);
}
TEST_P(TestFontMatchingByName, MatchingByPostscriptName) {
const FontName font_name = TestFontMatchingByName::GetParam();
TestFontMatchingByPostscriptName(font_name.postscript_name);
}
TEST_P(TestFontMatchingByName, MatchUniqueFontByFullFontName) {
const FontName font_name = TestFontMatchingByName::GetParam();
ScopedCFTypeRef<CTFontRef> font =
MatchUniqueFont(AtomicString(font_name.full_font_name), 12);
EXPECT_TRUE(font);
}
TEST_P(TestFontMatchingByName, MatchUniqueFontByPostscriptName) {
const FontName font_name = TestFontMatchingByName::GetParam();
ScopedCFTypeRef<CTFontRef> font =
MatchUniqueFont(AtomicString(font_name.postscript_name), 12);
EXPECT_TRUE(font);
}
class TestFontMatchingByNameAndWeight
: public testing::Test,
public testing::WithParamInterface<std::tuple<FontName, int, bool>> {};
INSTANTIATE_TEST_SUITE_P(FontMatcherMacTest,
TestFontMatchingByNameAndWeight,
::testing::Combine(::testing::ValuesIn(FontNames),
::testing::Range(100, 900, 100),
::testing::ValuesIn({true,
false})));
INSTANTIATE_TEST_SUITE_P(
CommonFontMatcherMacTest,
TestFontMatchingByNameAndWeight,
::testing::Combine(::testing::ValuesIn(CommonFontNames),
::testing::Range(100, 900, 100),
::testing::ValuesIn({true, false})));
TEST_P(TestFontMatchingByNameAndWeight, TestCTAndNSMatchEqual) {
struct FontName font_name;
int weight;
bool flag;
std::tie(font_name, weight, flag) = GetParam();
ScopedFontFamilyPostscriptMatchingCTMigrationForTest scoped_feature(flag);
// AppKit computes weight values of some fonts not as discrete as CoreText.
// This is causing matching results of CoreText approach for some weight
// values of several font families to be more precise than AppKit's. For
// instance, if desired weight is 300, with AppKit approach will match
// "HelveticaNeue-Thin" font, while with CoreText we will match
// "HelveticaNeue-Light". This fonts should be skipped in this test.
// This issue is described in the comment under
// "MatchFamilyWithWeightVariations" test.
if (strcmp(font_name.family_name, "Helvetica Neue") == 0 ||
strcmp(font_name.family_name, "Hiragino Sans") == 0) {
return;
}
TestCTAndNSMatchEqual(font_name.family_name, 11, weight, kNormalSlopeValue,
kNormalWidthValue);
TestCTAndNSMatchEqual(font_name.family_name, 11, weight, kItalicSlopeValue,
kNormalWidthValue);
TestCTAndNSMatchEqual(font_name.postscript_name, 11, weight,
kNormalSlopeValue, kNormalWidthValue);
TestCTAndNSMatchEqual(font_name.postscript_name, 11, weight,
kItalicSlopeValue, kNormalWidthValue);
}
class TestFontWithTraitsMatching : public testing::TestWithParam<const char*> {
};
INSTANTIATE_TEST_SUITE_P(FontMatcherMacTest,
TestFontWithTraitsMatching,
testing::ValuesIn(FamiliesWithBoldItalicFaces));
TEST_P(TestFontWithTraitsMatching, FontFamilyMatchingWithBoldItalicTraits) {
const char* font_name = TestFontWithTraitsMatching::GetParam();
TestFontWithBoldAndItalicTraits(AtomicString(font_name));
}
TEST(FontMatcherMacTest, FontFamilyMatchingWithBoldCondensedTraits) {
AtomicString family_name = AtomicString("American Typewriter");
ScopedCFTypeRef<CTFontRef> font_condensed =
MatchCTFontFamily(family_name, kNormalWeightValue, kNormalSlopeValue,
kCondensedWidthValue, 11);
EXPECT_TRUE(font_condensed);
CTFontSymbolicTraits condensed_font_traits =
CTFontGetSymbolicTraits(font_condensed.get());
EXPECT_TRUE(condensed_font_traits & NSFontCondensedTrait);
ScopedCFTypeRef<CTFontRef> font_bold_condensed =
MatchCTFontFamily(family_name, kBoldWeightValue, kNormalSlopeValue,
kCondensedWidthValue, 11);
EXPECT_TRUE(font_bold_condensed.get());
CTFontSymbolicTraits bold_condensed_font_traits =
CTFontGetSymbolicTraits(font_bold_condensed.get());
EXPECT_TRUE(bold_condensed_font_traits & NSFontCondensedTrait);
EXPECT_TRUE(bold_condensed_font_traits & NSFontCondensedTrait);
}
TEST(FontMatcherMacTest, MatchFamilyWithWeightVariations) {
// For some fonts AppKit returns inconsistent weight values in the font
// information, retrieved using `availableFontsForFamily`. For instance, both
// "NotoSansMyanmar-Light" and "NotoSansMyanmar-Thin" have AppKit weight value
// of 3, while "NotoSansMyanmar-Thin" should be thinner than
// "NotoSansMyanmar-Light".
// This behavior is affecting matching results. For instance, in this test, if
// the "FontFamilyStyleMatchingCTMigration" flag is off, we are using
// `availableFontsForFamily`, so for `weight=300` we will match
// "NotoSansMyanmar-Thin" font instead of "NotoSansMyanmar-Light".
// The same issue might appear with CoreText but less often. For instance, for
// both "AppleSDGothicNeo-Heavy" and "AppleSDGothicNeo-ExtraBold" CoreText
// returns font weight value 0.56, although weight value of
// "AppleSDGothicNeo-Heavy" is higher than weight value of
// "AppleSDGothicNeo-ExtraBold". However, for fonts in "Noto Sans Myanmar"
// family CoreText returns the correct weight values.
// Hence we only run this test with the "FontFamilyStyleMatchingCTMigration"
// flag on.
ScopedFontFamilyStyleMatchingCTMigrationForTest scoped_feature(true);
AtomicString family_name = AtomicString("Noto Sans Myanmar");
for (int weight = 100; weight <= 900; weight += 100) {
ScopedCFTypeRef<CTFontRef> font =
MatchCTFontFamily(family_name, FontSelectionValue(weight),
kNormalSlopeValue, kNormalWidthValue, 11);
NSDictionary* traits = CFToNSOwnershipCast(CTFontCopyTraits(font.get()));
NSNumber* actual_weight_num =
ObjCCast<NSNumber>(traits[CFToNSPtrCast(kCTFontWeightTrait)]);
float actual_ct_weight = actual_weight_num.floatValue;
int actual_weight = ToCSSFontWeight(actual_ct_weight);
EXPECT_EQ(actual_weight, weight);
}
}
} // namespace blink