Files
eepp/src/tests/unit_tests/systemfontresolver_tests.cpp
T
Martín Lucas Golini 301b7458d2 graphics: improve native system font fallback
Initialize SystemFontResolver when enabled and make its lifecycle and font-list
population thread-safe.

Use native codepoint fallback matching through Fontconfig on Linux, BSD, and
Haiku, DirectWrite on Windows, and Core Text on macOS and iOS. Preserve the
cached FreeType scan as a fallback when native matching is unavailable.

Enable system fonts by default for UIApplication and warm the font database in
the background. Allow applications to override the policy and disable the
automatic default in the unit-test runner.

Fix glyph fallback checks to respect the resolver's enabled state and add
coverage for concurrent warm-up and shaped symbol fallback.

Update efsw.
2026-08-23 16:59:23 -03:00

652 lines
23 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#include "utest.hpp"
#include <eepp/graphics/fonttruetype.hpp>
#include <eepp/graphics/resourcescope.hpp>
#include <eepp/graphics/systemfontresolver.hpp>
#include <eepp/system/filesystem.hpp>
#include <eepp/system/sys.hpp>
#include <eepp/system/thread.hpp>
#if EE_PLATFORM == EE_PLATFORM_LINUX
#include <dirent.h>
#endif
using namespace EE;
using namespace EE::Graphics;
using namespace EE::System;
static std::string getFontsDir() {
return Sys::getProcessPath() + "../assets/fonts/";
}
#if EE_PLATFORM == EE_PLATFORM_LINUX
static std::size_t getOpenFileDescriptorCount() {
DIR* directory = opendir( "/proc/self/fd" );
if ( !directory )
return 0;
std::size_t count = 0;
while ( dirent* entry = readdir( directory ) ) {
if ( entry->d_name[0] != '.' )
++count;
}
closedir( directory );
return count;
}
#endif
UTEST( SystemFontResolver, singletonLifecycle ) {
SystemFontResolver::destroySingleton();
SystemFontResolver::setEnabled( false );
UTEST_PRINT_STEP( "Enabling creates singleton" );
SystemFontResolver::setEnabled( true );
EXPECT_TRUE( SystemFontResolver::existsSingleton() != nullptr );
UTEST_PRINT_STEP( "Create singleton" );
auto* resolver = SystemFontResolver::createSingleton();
EXPECT_TRUE( resolver != nullptr );
EXPECT_TRUE( SystemFontResolver::existsSingleton() != nullptr );
UTEST_PRINT_STEP( "Destroy singleton" );
SystemFontResolver::destroySingleton();
EXPECT_TRUE( SystemFontResolver::existsSingleton() == nullptr );
UTEST_PRINT_STEP( "Re-create via instance()" );
auto* resolver2 = SystemFontResolver::instance();
EXPECT_TRUE( resolver2 != nullptr );
EXPECT_TRUE( SystemFontResolver::existsSingleton() != nullptr );
SystemFontResolver::destroySingleton();
SystemFontResolver::setEnabled( false );
}
UTEST( SystemFontResolver, workerWarmUp ) {
SystemFontResolver::setEnabled( true );
auto* resolver = SystemFontResolver::instance();
Thread warmUpThread( [resolver] { resolver->warmUp(); } );
warmUpThread.launch();
warmUpThread.wait();
EXPECT_FALSE( resolver->isLoading() );
#if EE_PLATFORM == EE_PLATFORM_LINUX || EE_PLATFORM == EE_PLATFORM_BSD || \
EE_PLATFORM == EE_PLATFORM_WIN || EE_PLATFORM == EE_PLATFORM_MACOS || \
EE_PLATFORM == EE_PLATFORM_IOS || EE_PLATFORM == EE_PLATFORM_HAIKU
EXPECT_FALSE( resolver->enumerate().empty() );
#endif
SystemFontResolver::setEnabled( false );
SystemFontResolver::destroySingleton();
}
UTEST( SystemFontResolver, fallbackWaitsForConcurrentWarmUp ) {
SystemFontResolver::setEnabled( true );
auto* resolver = SystemFontResolver::instance();
resolver->invalidateCache();
Thread warmUpThread( [resolver] { resolver->warmUp(); } );
warmUpThread.launch();
FontDesc fallback = resolver->getFallbackForCodepoint( 'A', FontWeight::Normal, false );
warmUpThread.wait();
EXPECT_FALSE( resolver->isLoading() );
EXPECT_FALSE( fallback.path.empty() );
EXPECT_FALSE( resolver->enumerate().empty() );
SystemFontResolver::setEnabled( false );
SystemFontResolver::destroySingleton();
}
UTEST( SystemFontResolver, genericFamilyFromName ) {
SystemFontResolver::setEnabled( true );
EXPECT_EQ( GenericFamily::Serif, SystemFontResolver::genericFamilyFromName( "serif" ) );
EXPECT_EQ( GenericFamily::SansSerif,
SystemFontResolver::genericFamilyFromName( "sans-serif" ) );
EXPECT_EQ( GenericFamily::Monospace, SystemFontResolver::genericFamilyFromName( "monospace" ) );
EXPECT_EQ( GenericFamily::Cursive, SystemFontResolver::genericFamilyFromName( "cursive" ) );
EXPECT_EQ( GenericFamily::Fantasy, SystemFontResolver::genericFamilyFromName( "fantasy" ) );
EXPECT_EQ( GenericFamily::SystemUi, SystemFontResolver::genericFamilyFromName( "system-ui" ) );
EXPECT_EQ( GenericFamily::Emoji, SystemFontResolver::genericFamilyFromName( "emoji" ) );
EXPECT_EQ( GenericFamily::None,
SystemFontResolver::genericFamilyFromName( "bogus-font-name" ) );
EXPECT_EQ( GenericFamily::None, SystemFontResolver::genericFamilyFromName( "" ) );
EXPECT_EQ( GenericFamily::SansSerif,
SystemFontResolver::genericFamilyFromName( "SANS-SERIF" ) );
EXPECT_EQ( GenericFamily::Monospace,
SystemFontResolver::genericFamilyFromName( " monospace " ) );
SystemFontResolver::destroySingleton();
SystemFontResolver::setEnabled( false );
}
UTEST( SystemFontResolver, enumerate ) {
SystemFontResolver::setEnabled( true );
auto* resolver = SystemFontResolver::instance();
const auto& fonts = resolver->enumerate();
UTEST_PRINT_INFO( String::format( "Enumerated %zu system fonts", fonts.size() ).c_str() );
#if EE_PLATFORM == EE_PLATFORM_LINUX || EE_PLATFORM == EE_PLATFORM_BSD || \
EE_PLATFORM == EE_PLATFORM_HAIKU
EXPECT_TRUE_MSG( fonts.size() > 0, "Fontconfig should find fonts on Linux/BSD/Haiku" );
#elif EE_PLATFORM == EE_PLATFORM_WIN
EXPECT_TRUE_MSG( fonts.size() > 0, "DirectWrite should find fonts on Windows" );
#elif EE_PLATFORM == EE_PLATFORM_MACOS || EE_PLATFORM == EE_PLATFORM_IOS
EXPECT_TRUE_MSG( fonts.size() > 0, "CoreText should find fonts on macOS/iOS" );
#endif
for ( const auto& desc : fonts ) {
EXPECT_FALSE_MSG( desc.path.empty(),
( "Font path must not be empty for family: " + desc.family ).c_str() );
break;
}
SystemFontResolver::setEnabled( false );
SystemFontResolver::destroySingleton();
}
UTEST( SystemFontResolver, enumerateFamily ) {
SystemFontResolver::setEnabled( true );
auto* resolver = SystemFontResolver::instance();
auto fonts = resolver->enumerateFamily( "DejaVu Sans" );
UTEST_PRINT_INFO( String::format( "Found %zu DejaVu Sans fonts", fonts.size() ).c_str() );
for ( const auto& desc : fonts ) {
EXPECT_FALSE( desc.path.empty() );
}
if ( !fonts.empty() ) {
UTEST_PRINT_INFO( "Verify faceIndex is set" );
EXPECT_EQ( (Uint32)0, fonts[0].faceIndex );
}
SystemFontResolver::setEnabled( false );
SystemFontResolver::destroySingleton();
}
UTEST( SystemFontResolver, findVerdana ) {
SystemFontResolver::setEnabled( true );
auto* resolver = SystemFontResolver::instance();
FontQuery query;
query.family = "Verdana";
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 || \
EE_PLATFORM == EE_PLATFORM_HAIKU
if ( resolver->enumerate().size() > 0 ) {
if ( !desc.path.empty() ) {
EXPECT_STDSTREQ( "Verdana", desc.family );
}
}
#endif
SystemFontResolver::setEnabled( false );
SystemFontResolver::destroySingleton();
}
UTEST( SystemFontResolver, resolveFromNamesList ) {
SystemFontResolver::setEnabled( true );
auto* resolver = SystemFontResolver::instance();
UTEST_PRINT_STEP( "Resolve with fallback to sans-serif" );
FontDesc desc = resolver->resolveFromNamesList( "NonExistentFont12345, sans-serif",
FontWeight::Normal, false );
#if EE_PLATFORM == EE_PLATFORM_EMSCRIPTEN
EXPECT_TRUE_MSG( desc.path.empty(), "Emscripten has no system fonts" );
#else
if ( !desc.path.empty() ) {
EXPECT_FALSE( desc.family.empty() );
}
#endif
UTEST_PRINT_STEP( "Resolve empty string" );
FontDesc descEmpty = resolver->resolveFromNamesList( "", FontWeight::Normal, false );
EXPECT_TRUE( descEmpty.path.empty() );
SystemFontResolver::setEnabled( false );
SystemFontResolver::destroySingleton();
}
UTEST( SystemFontResolver, resolve ) {
SystemFontResolver::setEnabled( true );
auto* resolver = SystemFontResolver::instance();
UTEST_PRINT_STEP( "Resolve a known family" );
FontQuery query;
query.family = "sans-serif";
query.weight = FontWeight::Normal;
query.italic = false;
FontDesc desc = resolver->resolve( query );
if ( !desc.path.empty() ) {
EXPECT_FALSE( desc.family.empty() );
}
UTEST_PRINT_STEP( "Resolve with bogus family" );
query.family = "zzz_not_a_real_font_family_zzz";
FontDesc descBogus = resolver->resolve( query );
EXPECT_TRUE( descBogus.path.empty() );
SystemFontResolver::setEnabled( false );
SystemFontResolver::destroySingleton();
}
UTEST( SystemFontResolver, resolveGeneric ) {
SystemFontResolver::setEnabled( true );
auto* resolver = SystemFontResolver::instance();
UTEST_PRINT_STEP( "Resolve monospace" );
FontDesc descMono =
resolver->resolveGeneric( GenericFamily::Monospace, FontWeight::Normal, false );
UTEST_PRINT_INFO(
String::format( "Monospace default: %s at %s", descMono.family, descMono.path ).c_str() );
UTEST_PRINT_STEP( "Resolve sans-serif" );
FontDesc descSans =
resolver->resolveGeneric( GenericFamily::SansSerif, FontWeight::Normal, false );
UTEST_PRINT_STEP( "Resolve serif" );
FontDesc descSerif =
resolver->resolveGeneric( GenericFamily::Serif, FontWeight::Normal, false );
UTEST_PRINT_STEP( "Resolve None" );
FontDesc descNone = resolver->resolveGeneric( GenericFamily::None, FontWeight::Normal, false );
EXPECT_TRUE( descNone.path.empty() );
SystemFontResolver::setEnabled( false );
SystemFontResolver::destroySingleton();
}
UTEST( SystemFontResolver, fontContainsCodepoint ) {
SystemFontResolver::setEnabled( true );
auto* resolver = SystemFontResolver::instance();
std::string fontPath = getFontsDir() + "DejaVuSansMono.ttf";
ASSERT_TRUE_MSG( FileSystem::fileExists( fontPath ),
( "Font file not found: " + fontPath ).c_str() );
UTEST_PRINT_STEP( "ASCII letter 'A'" );
EXPECT_TRUE( resolver->fontContainsCodepoint( fontPath, 'A' ) );
UTEST_PRINT_STEP( "CJK character U+65E5 (日)" );
bool hasCJK = resolver->fontContainsCodepoint( fontPath, 0x65E5 );
UTEST_PRINT_INFO(
String::format( "DejaVuSansMono has CJK U+65E5: %s", hasCJK ? "yes" : "no" ).c_str() );
UTEST_PRINT_STEP( "Non-existent file" );
EXPECT_FALSE( resolver->fontContainsCodepoint( "/nonexistent/font.ttf", 'A' ) );
SystemFontResolver::setEnabled( false );
SystemFontResolver::destroySingleton();
}
UTEST( SystemFontResolver, getFallbackForCodepoint ) {
SystemFontResolver::setEnabled( true );
auto* resolver = SystemFontResolver::instance();
UTEST_PRINT_STEP( "Look up fallback for CJK U+65E5 (日)" );
FontDesc desc = resolver->getFallbackForCodepoint( 0x65E5, FontWeight::Normal, false );
if ( !desc.path.empty() ) {
UTEST_PRINT_INFO(
String::format( "Fallback for U+65E5: %s (%s)", desc.family, desc.path ).c_str() );
EXPECT_FALSE( desc.family.empty() );
}
UTEST_PRINT_STEP( "Look up fallback for Arabic U+0627 (ا)" );
FontDesc descArabic = resolver->getFallbackForCodepoint( 0x0627, FontWeight::Normal, false );
if ( !descArabic.path.empty() ) {
UTEST_PRINT_INFO(
String::format( "Fallback for U+0627: %s (%s)", descArabic.family, descArabic.path )
.c_str() );
}
SystemFontResolver::setEnabled( false );
SystemFontResolver::destroySingleton();
}
#if EE_PLATFORM == EE_PLATFORM_LINUX
UTEST( SystemFontResolver, fallbackProbeDoesNotRetainFontDescriptors ) {
SystemFontResolver::setEnabled( true );
auto* resolver = SystemFontResolver::instance();
resolver->enumerate();
const std::size_t descriptorsBefore = getOpenFileDescriptorCount();
resolver->getFallbackForCodepoint( 0x10FFFF, FontWeight::Normal, false );
const std::size_t descriptorsAfter = getOpenFileDescriptorCount();
EXPECT_TRUE_MSG( descriptorsAfter <= descriptorsBefore + 4,
String::format( "Fallback probing changed the descriptor count by %lld",
static_cast<long long>( descriptorsAfter ) -
static_cast<long long>( descriptorsBefore ) )
.c_str() );
SystemFontResolver::setEnabled( false );
SystemFontResolver::destroySingleton();
}
#endif
UTEST( SystemFontResolver, invalidateCache ) {
SystemFontResolver::setEnabled( true );
auto* resolver = SystemFontResolver::instance();
UTEST_PRINT_STEP( "Cache a resolve result" );
FontQuery query;
query.family = "sans-serif";
query.weight = FontWeight::Normal;
FontDesc desc1 = resolver->resolve( query );
UTEST_PRINT_STEP( "Invalidate cache" );
resolver->invalidateCache();
UTEST_PRINT_STEP( "Re-resolve after invalidation" );
FontDesc desc2 = resolver->resolve( query );
EXPECT_STDSTREQ( desc1.path, desc2.path );
EXPECT_STDSTREQ( desc1.family, desc2.family );
SystemFontResolver::setEnabled( false );
SystemFontResolver::destroySingleton();
}
UTEST( SystemFontResolver, getSystemFont ) {
SystemFontResolver::setEnabled( true );
auto* resolver = SystemFontResolver::instance();
FontDesc sysFont = resolver->getSystemFont();
FontDesc monoFont = resolver->getSystemMonospaceFont();
SystemFontResolver::setEnabled( false );
SystemFontResolver::destroySingleton();
}
UTEST( SystemFontResolver, genericFallbackPurity ) {
SystemFontResolver::setEnabled( true );
auto* resolver = SystemFontResolver::instance();
std::vector<const char*> scriptSuffixes = {
"georgian", "cjk", "arabic", "hebrew", "armenian", "lao", "thai",
"devanagari", "tamil", "bengali", "gurmukhi", "gujarati", "oriya", "telugu",
"kannada", "malayalam", "sinhala", "khmer", "tibetan", "myanmar", "ethiopic",
"cherokee", "canadian", "mongolian", "yi", "nko", "tifinagh", "vai",
"bamum", "coptic", "glagolitic", "gothic", "old", "ugaritic", "osmanya",
"osmanya", "phags", "syloti" };
auto checkFamily = [&scriptSuffixes]( const FontDesc& desc, const char* genericName ) {
if ( desc.path.empty() )
return;
std::string lowerFamily = String::toLower( desc.family );
for ( const char* suffix : scriptSuffixes ) {
std::string suffixStr( suffix );
if ( lowerFamily.find( suffixStr ) != std::string::npos ) {
UTEST_PRINT_INFO(
String::format( "WARNING: %s fallback resolved to script-specific font: %s",
genericName, desc.family )
.c_str() );
}
}
};
UTEST_PRINT_STEP( "Check serif generic fallback" );
FontDesc serif = resolver->resolveGeneric( GenericFamily::Serif, FontWeight::Normal, false );
checkFamily( serif, "serif" );
UTEST_PRINT_STEP( "Check sans-serif generic fallback" );
FontDesc sans = resolver->resolveGeneric( GenericFamily::SansSerif, FontWeight::Normal, false );
checkFamily( sans, "sans-serif" );
UTEST_PRINT_STEP( "Check monospace generic fallback" );
FontDesc mono = resolver->resolveGeneric( GenericFamily::Monospace, FontWeight::Normal, false );
checkFamily( mono, "monospace" );
SystemFontResolver::setEnabled( false );
SystemFontResolver::destroySingleton();
}
UTEST( SystemFontResolver, resolveFromNamesListRealWorld ) {
SystemFontResolver::setEnabled( true );
auto* resolver = SystemFontResolver::instance();
UTEST_PRINT_STEP( "Resolve with double-quoted font names" );
FontDesc desc1 = resolver->resolveFromNamesList(
"\"Helvetica Neue\", Arial, Helvetica, \"Nimbus Sans L\", sans-serif", FontWeight::Normal,
false );
UTEST_PRINT_INFO( String::format( "Resolved: %s (%s)", desc1.family, desc1.path ).c_str() );
UTEST_PRINT_STEP( "Resolve with single-quoted font names" );
FontDesc desc2 =
resolver->resolveFromNamesList( "'Times New Roman', serif", FontWeight::Normal, false );
UTEST_PRINT_INFO( String::format( "Resolved: %s (%s)", desc2.family, desc2.path ).c_str() );
UTEST_PRINT_STEP( "Resolve with mixture of quoted and unquoted" );
FontDesc desc3 = resolver->resolveFromNamesList(
"Roboto, \"Helvetica Neue\", Arial, sans-serif", FontWeight::Normal, false );
UTEST_PRINT_INFO( String::format( "Resolved: %s (%s)", desc3.family, desc3.path ).c_str() );
UTEST_PRINT_STEP( "Georgia, serif (regression: substring overmatch)" );
FontDesc desc4 = resolver->resolveFromNamesList( "Georgia, \"Bitstream Charter\", serif",
FontWeight::Normal, false );
UTEST_PRINT_INFO( String::format( "Resolved: %s (%s)", desc4.family, desc4.path ).c_str() );
if ( !desc4.path.empty() ) {
std::string lower = String::toLower( desc4.family );
EXPECT_TRUE_MSG(
lower == "georgia" || lower == "times new roman" ||
lower.find( "serif" ) != std::string::npos,
( "Should resolve to Georgia or a serif font, got: " + desc4.family ).c_str() );
}
SystemFontResolver::setEnabled( false );
SystemFontResolver::destroySingleton();
}
UTEST( SystemFontResolver, resolveGenericWeights ) {
SystemFontResolver::setEnabled( true );
auto* resolver = SystemFontResolver::instance();
UTEST_PRINT_STEP( "Resolve sans-serif at multiple weights" );
FontDesc normal =
resolver->resolveGeneric( GenericFamily::SansSerif, FontWeight::Normal, false );
FontDesc bold = resolver->resolveGeneric( GenericFamily::SansSerif, FontWeight::Bold, false );
FontDesc light = resolver->resolveGeneric( GenericFamily::SansSerif, FontWeight::Light, false );
FontDesc black = resolver->resolveGeneric( GenericFamily::SansSerif, FontWeight::Black, false );
UTEST_PRINT_INFO( String::format( "Normal: %s", normal.family ).c_str() );
UTEST_PRINT_INFO( String::format( "Bold: %s", bold.family ).c_str() );
UTEST_PRINT_INFO( String::format( "Light: %s", light.family ).c_str() );
UTEST_PRINT_INFO( String::format( "Black: %s", black.family ).c_str() );
if ( !normal.path.empty() )
EXPECT_FALSE( normal.family.empty() );
SystemFontResolver::setEnabled( false );
SystemFontResolver::destroySingleton();
}
UTEST( SystemFontResolver, resolveGenericWeightPreference ) {
SystemFontResolver::setEnabled( true );
auto* resolver = SystemFontResolver::instance();
UTEST_PRINT_STEP( "Normal weight should prefer Normal or close weight" );
FontDesc normal =
resolver->resolveGeneric( GenericFamily::SansSerif, FontWeight::Normal, false );
if ( !normal.path.empty() ) {
int diff = std::abs( (int)normal.weight - (int)FontWeight::Normal );
EXPECT_TRUE_MSG( diff <= 300, ( "Normal weight diff too large: " + normal.family +
" weight=" + String::toString( (int)normal.weight ) )
.c_str() );
}
UTEST_PRINT_STEP( "Bold weight should prefer Bold or close weight" );
FontDesc bold = resolver->resolveGeneric( GenericFamily::SansSerif, FontWeight::Bold, false );
if ( !bold.path.empty() ) {
int diff = std::abs( (int)bold.weight - (int)FontWeight::Bold );
UTEST_PRINT_INFO(
String::format( "Bold: %s (weight=%d diff=%d)", bold.family, (int)bold.weight, diff )
.c_str() );
}
UTEST_PRINT_STEP( "Light weight should prefer Light or close weight" );
FontDesc light = resolver->resolveGeneric( GenericFamily::SansSerif, FontWeight::Light, false );
if ( !light.path.empty() ) {
int diff = std::abs( (int)light.weight - (int)FontWeight::Light );
UTEST_PRINT_INFO(
String::format( "Light: %s (weight=%d diff=%d)", light.family, (int)light.weight, diff )
.c_str() );
}
SystemFontResolver::setEnabled( false );
SystemFontResolver::destroySingleton();
}
UTEST( SystemFontResolver, resolveGenericItalicFallback ) {
SystemFontResolver::setEnabled( true );
auto* resolver = SystemFontResolver::instance();
UTEST_PRINT_STEP( "Request italic — should return a font even if no italic variant" );
FontDesc italic =
resolver->resolveGeneric( GenericFamily::SansSerif, FontWeight::Normal, true );
if ( !italic.path.empty() ) {
UTEST_PRINT_INFO(
String::format( "Italic: %s (italic=%d)", italic.family, (int)italic.italic ).c_str() );
EXPECT_FALSE( italic.family.empty() );
}
SystemFontResolver::setEnabled( false );
SystemFontResolver::destroySingleton();
}
UTEST( SystemFontResolver, resolveBoldFromNamesList ) {
SystemFontResolver::setEnabled( true );
auto* resolver = SystemFontResolver::instance();
UTEST_PRINT_STEP( "Resolve Arial with bold weight" );
FontDesc desc = resolver->resolveFromNamesList( "Arial, sans-serif", FontWeight::Bold, false );
UTEST_PRINT_INFO(
String::format( "Bold: %s (weight=%d)", desc.family, (int)desc.weight ).c_str() );
if ( !desc.path.empty() ) {
EXPECT_FALSE( desc.family.empty() );
}
SystemFontResolver::setEnabled( false );
SystemFontResolver::destroySingleton();
}
UTEST( SystemFontResolver, invalidateCachePersistence ) {
SystemFontResolver::setEnabled( true );
auto* resolver = SystemFontResolver::instance();
UTEST_PRINT_STEP( "First resolve" );
FontQuery query;
query.family = "sans-serif";
query.weight = FontWeight::Normal;
FontDesc first = resolver->resolve( query );
UTEST_PRINT_INFO( String::format( "First: %s (%s)", first.family, first.path ).c_str() );
UTEST_PRINT_STEP( "Invalidate cache" );
resolver->invalidateCache();
UTEST_PRINT_STEP( "Second resolve after invalidation" );
FontDesc second = resolver->resolve( query );
UTEST_PRINT_INFO( String::format( "Second: %s (%s)", second.family, second.path ).c_str() );
EXPECT_STDSTREQ( first.path, second.path );
EXPECT_STDSTREQ( first.family, second.family );
SystemFontResolver::setEnabled( false );
SystemFontResolver::destroySingleton();
}
UTEST( SystemFontResolver, glyphFallbackRoundTrip ) {
SystemFontResolver::setEnabled( true );
auto* resolver = SystemFontResolver::instance();
UTEST_PRINT_STEP( "Look up fallback for CJK U+65E5" );
FontDesc cjk = resolver->getFallbackForCodepoint( 0x65E5, FontWeight::Normal, false );
if ( !cjk.path.empty() ) {
UTEST_PRINT_INFO( String::format( "CJK fallback: %s", cjk.family ).c_str() );
EXPECT_TRUE( resolver->fontContainsCodepoint( cjk.path, 0x65E5 ) );
}
UTEST_PRINT_STEP( "Look up fallback for Arabic U+0627" );
FontDesc arabic = resolver->getFallbackForCodepoint( 0x0627, FontWeight::Normal, false );
if ( !arabic.path.empty() ) {
UTEST_PRINT_INFO( String::format( "Arabic fallback: %s", arabic.family ).c_str() );
EXPECT_TRUE( resolver->fontContainsCodepoint( arabic.path, 0x0627 ) );
}
UTEST_PRINT_STEP( "Verify ASCII fallback" );
FontDesc ascii = resolver->getFallbackForCodepoint( 'A', FontWeight::Normal, false );
if ( !ascii.path.empty() ) {
UTEST_PRINT_INFO( String::format( "ASCII fallback: %s", ascii.family ).c_str() );
EXPECT_TRUE( resolver->fontContainsCodepoint( ascii.path, 'A' ) );
}
SystemFontResolver::setEnabled( false );
SystemFontResolver::destroySingleton();
}
UTEST( FontTrueType_faceIndex, loadWithDefaultFaceIndex ) {
FileSystem::changeWorkingDirectory( Sys::getProcessPath() );
std::string fontPath = getFontsDir() + "DejaVuSansMono.ttf";
ASSERT_TRUE( FileSystem::fileExists( fontPath ) );
FontTrueType* font = FontTrueType::New( "Test-faceIndex-default" ).get();
bool loaded = font->loadFromFile( fontPath );
ASSERT_TRUE( loaded );
EXPECT_TRUE( font->loaded() );
defaultResourceScope().eraseLocalFont( font );
}
UTEST( FontTrueType_faceIndex, newWithFaceIndex ) {
FileSystem::changeWorkingDirectory( Sys::getProcessPath() );
std::string fontPath = getFontsDir() + "DejaVuSansMono.ttf";
ASSERT_TRUE( FileSystem::fileExists( fontPath ) );
FontTrueType* font = FontTrueType::New( "Test-faceIndex-explicit", fontPath, 0 ).get();
ASSERT_TRUE( font != nullptr );
EXPECT_TRUE( font->loaded() );
defaultResourceScope().eraseLocalFont( font );
}
UTEST( FontTrueType_faceIndex, loadFromMemoryFaceIndex ) {
FileSystem::changeWorkingDirectory( Sys::getProcessPath() );
std::string fontPath = getFontsDir() + "DejaVuSansMono.ttf";
ASSERT_TRUE( FileSystem::fileExists( fontPath ) );
ScopedBuffer buf;
FileSystem::fileGet( fontPath, buf );
FontTrueType* font = FontTrueType::New( "Test-faceIndex-memory" ).get();
bool loaded = font->loadFromMemory( buf.get(), buf.length(), true, 0 );
ASSERT_TRUE( loaded );
EXPECT_TRUE( font->loaded() );
defaultResourceScope().eraseLocalFont( font );
}