diff --git a/src/eepp/graphics/systemfontresolver.cpp b/src/eepp/graphics/systemfontresolver.cpp index 0c73ba0ad..12b95b51b 100644 --- a/src/eepp/graphics/systemfontresolver.cpp +++ b/src/eepp/graphics/systemfontresolver.cpp @@ -137,6 +137,35 @@ struct FreeTypeState { return found; } + bool findFaceIndex( const std::string& path, const std::string& postScriptName, + EE::Uint32& faceIndex ) { + Lock lock( mutex ); + FT_Face face{ nullptr }; + if ( !library || FT_New_Face( library, path.c_str(), -1, &face ) != 0 ) + return false; + + const FT_Long numFaces = face->num_faces; + FT_Done_Face( face ); + if ( numFaces == 1 ) { + faceIndex = 0; + return true; + } + if ( postScriptName.empty() ) + return false; + for ( FT_Long i = 0; i < numFaces; ++i ) { + if ( FT_New_Face( library, path.c_str(), i, &face ) != 0 ) + continue; + const char* facePostScriptName = FT_Get_Postscript_Name( face ); + const bool matches = facePostScriptName && postScriptName == facePostScriptName; + FT_Done_Face( face ); + if ( matches ) { + faceIndex = static_cast( i ); + return true; + } + } + return false; + } + void clearProbeCache() { Lock lock( mutex ); probeCache.clear(); @@ -777,6 +806,11 @@ void SystemFontResolver::populateFontList() const { return; CFIndex count = CFArrayGetCount( descriptors ); + std::shared_ptr ftState = getFTState(); + if ( !ftState ) { + CFRelease( descriptors ); + return; + } for ( CFIndex i = 0; i < count; ++i ) { CFStringRef familyNameRef = (CFStringRef)CFArrayGetValueAtIndex( descriptors, i ); @@ -792,8 +826,12 @@ void SystemFontResolver::populateFontList() const { CTFontDescriptorRef familyDesc = CTFontDescriptorCreateWithAttributes( queryDict ); CFRelease( queryDict ); - CFSetRef mandatoryAttrs = CFSetCreate( - kCFAllocatorDefault, (const void**)&kCTFontURLAttribute, 1, &kCFTypeSetCallBacks ); + // mandatoryAttributes describes which query attributes must match; it is not a list of + // attributes to return. Requiring the URL (which is absent from the query) lets CoreText + // substitute Helvetica for families such as Verdana. + CFSetRef mandatoryAttrs = + CFSetCreate( kCFAllocatorDefault, (const void**)&kCTFontFamilyNameAttribute, 1, + &kCFTypeSetCallBacks ); CFArrayRef matchingDescs = CTFontDescriptorCreateMatchingFontDescriptors( familyDesc, mandatoryAttrs ); @@ -822,6 +860,20 @@ void SystemFontResolver::populateFontList() const { if ( fontPath.empty() ) continue; + CFStringRef postScriptNameRef = + (CFStringRef)CTFontDescriptorCopyAttribute( desc, kCTFontNameAttribute ); + std::string postScriptName = cfStringToStd( postScriptNameRef ); + if ( postScriptNameRef ) + CFRelease( postScriptNameRef ); + + CFStringRef matchedFamilyRef = + (CFStringRef)CTFontDescriptorCopyAttribute( desc, kCTFontFamilyNameAttribute ); + std::string matchedFamily = cfStringToStd( matchedFamilyRef ); + if ( matchedFamilyRef ) + CFRelease( matchedFamilyRef ); + if ( normalizeFamily( matchedFamily ) != normalizeFamily( familyName ) ) + continue; + CFDictionaryRef traits = (CFDictionaryRef)CTFontDescriptorCopyAttribute( desc, kCTFontTraitsAttribute ); CGFloat weightVal = 0.0; @@ -857,9 +909,12 @@ void SystemFontResolver::populateFontList() const { } FontDesc fontDesc; - fontDesc.family = familyName; + fontDesc.family = matchedFamily; fontDesc.path = fontPath; - fontDesc.faceIndex = static_cast( j ); + // j is only the descriptor's position in matchingDescs and is unrelated to the face's + // index inside a TTC. Match CoreText's PostScript name against the actual file faces. + if ( !ftState->findFaceIndex( fontPath, postScriptName, fontDesc.faceIndex ) ) + continue; fontDesc.weight = ctWeightToFontWeight( weightVal ); fontDesc.stretch = ctWidthToFontStretch( widthVal ); fontDesc.italic = ( slantVal > 0.0 ); diff --git a/src/tests/unit_tests/systemfontresolver_tests.cpp b/src/tests/unit_tests/systemfontresolver_tests.cpp index f3473003d..a29c39abe 100644 --- a/src/tests/unit_tests/systemfontresolver_tests.cpp +++ b/src/tests/unit_tests/systemfontresolver_tests.cpp @@ -128,6 +128,24 @@ UTEST( SystemFontResolver, findVerdana ) { query.weight = FontWeight::Normal; query.italic = false; FontDesc desc = resolver->resolve( query ); +#if EE_PLATFORM == EE_PLATFORM_MACOS + auto verdanaFaces = resolver->enumerateFamily( "Verdana" ); + if ( !verdanaFaces.empty() ) { + EXPECT_FALSE( desc.path.empty() ); + EXPECT_STDSTREQ( "Verdana", desc.family ); + EXPECT_TRUE_MSG( + desc.path.find( "Verdana" ) != std::string::npos, + ( "Verdana must not resolve through a substituted CoreText family: " + desc.path ) + .c_str() ); + for ( const FontDesc& face : verdanaFaces ) { + FontTrueTypePtr font = defaultResourceScope().getFontService().loadSystemFont( face ); + EXPECT_TRUE_MSG( + font && font->loaded(), + ( "CoreText face index must load the selected Verdana face: " + face.getFileKey() ) + .c_str() ); + } + } +#endif #if EE_PLATFORM == EE_PLATFORM_LINUX || EE_PLATFORM == EE_PLATFORM_BSD || \ EE_PLATFORM == EE_PLATFORM_WIN || EE_PLATFORM == EE_PLATFORM_MACOS if ( resolver->enumerate().size() > 0 ) {