1- /*
1+ /*
22 * AUI Framework - Declarative UI toolkit for modern C++20
33 * Copyright (C) 2020-2025 Alex2772 and Contributors
44 *
1414#include < freetype/ftsnames.h>
1515#include " AUI/Font/FreeType.h"
1616#include " AUI/Platform/AFontManager.h"
17+ #include " AUI/Logging/ALogger.h"
1718#include < fstream>
1819#include < string>
1920#include " AUI/Common/AStringVector.h"
2021#include " AFont.h"
2122
23+ #if AUI_PLATFORM_LINUX
24+ #include < fontconfig/fontconfig.h>
25+ #endif
26+
2227
2328AFont::AFont (AFontManager* fm, const AString& path) :
2429 ft(fm->mFreeType ) {
@@ -43,6 +48,15 @@ AFont::AFont(AFontManager* fm, const AUrl& url) :
4348 }
4449}
4550
51+ AFont::~AFont () {
52+ if (mFallbackFace ) {
53+ FT_Done_Face (mFallbackFace );
54+ }
55+ if (mFace ) {
56+ FT_Done_Face (mFace );
57+ }
58+ }
59+
4660AString AFont::getFontFamilyName () const {
4761 FT_SfntName name;
4862 FT_Get_Sfnt_Name (mFace , 0 , &name);
@@ -57,6 +71,171 @@ bool AFont::isItalic() const {
5771 return mFace ->style_flags & FT_STYLE_FLAG_ITALIC ;
5872}
5973
74+ bool AFont::hasGlyph (char32_t codepoint) const {
75+ return FT_Get_Char_Index (mFace , codepoint) != 0 ;
76+ }
77+
78+ void AFont::ensureFallbackFace () {
79+ if (mFallbackFace ) {
80+ return ; // already loaded
81+ }
82+ if (mFallbackAttempted ) {
83+ // Already tried and failed; don't retry every glyph
84+ return ;
85+ }
86+ mFallbackAttempted = true ;
87+
88+ #if AUI_PLATFORM_LINUX
89+ // Use fontconfig to find a CJK-capable sans-serif font.
90+ // Ensure fontconfig is initialized before using default config (nullptr).
91+ if (!FcInit ()) {
92+ ALogger::warn (" Font" ) << " FcInit() failed; CJK fallback unavailable" ;
93+ return ;
94+ }
95+ // Use a charset-based query with a common CJK character to ensure the font actually has CJK glyphs.
96+ FcPattern* pattern = FcPatternCreate ();
97+ FcCharSet* charset = FcCharSetCreate ();
98+ FcCharSetAddChar (charset, 0x4E2D ); // U+4E2D = '中' (common CJK character)
99+ FcPatternAddCharSet (pattern, FC_CHARSET , charset);
100+ FcPatternAddString (pattern, FC_FAMILY , reinterpret_cast <const FcChar8*>(" sans" ));
101+ FcConfigSubstitute (nullptr , pattern, FcMatchPattern);
102+ FcDefaultSubstitute (pattern);
103+ FcCharSetDestroy (charset);
104+
105+ FcResult result;
106+ FcPattern* match = FcFontMatch (nullptr , pattern, &result);
107+ if (match) {
108+ if (result == FcResultMatch) {
109+ FcChar8* path = nullptr ;
110+ if (FcPatternGetString (match, FC_FILE , 0 , &path) == FcResultMatch) {
111+ mFallbackFontPath = reinterpret_cast <const char *>(path);
112+
113+ // Try to load the fallback font face.
114+ // TTC collections may need face_index > 0; try index 0 first (usually Regular).
115+ if (FT_New_Face (ft->getFt (), mFallbackFontPath .toStdString ().c_str (), 0 , &mFallbackFace )) {
116+ ALogger::warn (" Font" ) << " Failed to load fallback font: " << mFallbackFontPath ;
117+ mFallbackFace = nullptr ;
118+ mFallbackFontPath .clear ();
119+ } else {
120+ ALogger::info (" Font" ) << " Loaded CJK fallback font: " << mFallbackFontPath ;
121+ }
122+ }
123+ }
124+ FcPatternDestroy (match);
125+ }
126+ FcPatternDestroy (pattern);
127+ #elif AUI_PLATFORM_WIN
128+ // Try known CJK font files from the Windows Fonts directory.
129+ // Order: most-comprehensive first (Microsoft YaHei covers CJK + Kana,
130+ // Malgun Gothic covers CJK + Hangul, then region-specific fallbacks).
131+ const char * cjkFonts[] = {
132+ " msyh.ttc" , // Microsoft YaHei (Simplified Chinese, includes CJK + Kana)
133+ " malgun.ttf" , // Malgun Gothic (Korean, includes Hangul + CJK)
134+ " simsun.ttc" , // SimSun (Chinese Traditional)
135+ " msgothic.ttc" , // MS Gothic (Japanese)
136+ " yugothic.ttf" , // Yu Gothic (Japanese)
137+ " meiryo.ttc" , // Meiryo (Japanese)
138+ };
139+ static const AString fontsDir = [] {
140+ char buf[MAX_PATH ];
141+ UINT len = GetWindowsDirectoryA (buf, MAX_PATH );
142+ if (len > 0 && len < MAX_PATH ) {
143+ return AString (buf) + " \\ Fonts\\ " ;
144+ }
145+ ALogger::warn (" Font" ) << " GetWindowsDirectoryA() failed, falling back to C:\\ Windows\\ Fonts\\ " ;
146+ return AString (" C:\\ Windows\\ Fonts\\ " );
147+ }();
148+ for (auto & f : cjkFonts) {
149+ AString fontPath = fontsDir + f;
150+ if (FT_New_Face (ft->getFt (), fontPath.toStdString ().c_str (), 0 , &mFallbackFace ) == 0 ) {
151+ mFallbackFontPath = fontPath;
152+ ALogger::info (" Font" ) << " Loaded CJK fallback font: " << mFallbackFontPath ;
153+ break ;
154+ }
155+ }
156+ if (!mFallbackFace ) {
157+ ALogger::warn (" Font" ) << " No CJK fallback font found on Windows" ;
158+ }
159+ #elif AUI_PLATFORM_MACOS
160+ // macOS system CJK fonts (stable paths since OS X 10.11).
161+ const char * cjkFontsMac[] = {
162+ " /System/Library/Fonts/PingFang.ttc" ,
163+ " /System/Library/Fonts/AppleSDGothicNeo.ttc" ,
164+ " /System/Library/Fonts/Hiragino Sans.ttc" ,
165+ " /System/Library/Fonts/Supplemental/AppleSDGothicNeo.ttc" ,
166+ };
167+ for (auto & f : cjkFontsMac) {
168+ AString fontPath (f);
169+ if (FT_New_Face (ft->getFt (), fontPath.toStdString ().c_str (), 0 , &mFallbackFace ) == 0 ) {
170+ mFallbackFontPath = fontPath;
171+ ALogger::info (" Font" ) << " Loaded CJK fallback font: " << mFallbackFontPath ;
172+ break ;
173+ }
174+ }
175+ if (!mFallbackFace ) {
176+ ALogger::warn (" Font" ) << " No CJK fallback font found on macOS" ;
177+ }
178+ #elif AUI_PLATFORM_ANDROID
179+ // Android system CJK fonts (varies by API level).
180+ const char * cjkFontsAndroid[] = {
181+ " /system/fonts/NotoSansCJK-Regular.ttc" , // Android 5-9
182+ " /system/fonts/NotoSansSC-Regular.otf" , // Android 10+ (Chinese)
183+ " /system/fonts/NotoSansKR-Regular.otf" , // Android 10+ (Korean)
184+ " /system/fonts/NotoSansJP-Regular.otf" , // Android 10+ (Japanese)
185+ };
186+ for (auto & f : cjkFontsAndroid) {
187+ AString fontPath (f);
188+ if (FT_New_Face (ft->getFt (), fontPath.toStdString ().c_str (), 0 , &mFallbackFace ) == 0 ) {
189+ mFallbackFontPath = fontPath;
190+ ALogger::info (" Font" ) << " Loaded CJK fallback font: " << mFallbackFontPath ;
191+ break ;
192+ }
193+ }
194+ if (!mFallbackFace ) {
195+ ALogger::warn (" Font" ) << " No CJK fallback font found on Android" ;
196+ }
197+ #elif AUI_PLATFORM_IOS
198+ // iOS: try system font paths first (some iOS versions allow reading them),
199+ // then fall back to a bundled resource if available.
200+ const char * cjkFontsIos[] = {
201+ " /System/Library/Fonts/PingFang.ttc" ,
202+ " /System/Library/Fonts/AppleSDGothicNeo.ttc" ,
203+ };
204+ {
205+ bool loaded = false ;
206+ for (auto & f : cjkFontsIos) {
207+ AString fontPath (f);
208+ if (FT_New_Face (ft->getFt (), fontPath.toStdString ().c_str (), 0 , &mFallbackFace ) == 0 ) {
209+ mFallbackFontPath = fontPath;
210+ ALogger::info (" Font" ) << " Loaded CJK fallback font: " << mFallbackFontPath ;
211+ loaded = true ;
212+ break ;
213+ }
214+ }
215+ if (!loaded) {
216+ // Try bundled resource
217+ try {
218+ mFallbackFontDataBuffer = AByteBuffer::fromStream (AUrl (" :uni/font/NotoSansCJK-Fallback.ttf" ).open ());
219+ if (FT_New_Memory_Face (ft->getFt (),
220+ (const FT_Byte*) mFallbackFontDataBuffer .data (),
221+ mFallbackFontDataBuffer .getSize (), 0 , &mFallbackFace ) == 0 ) {
222+ mFallbackFontPath = AString (" :uni/font/NotoSansCJK-Fallback.ttf" );
223+ ALogger::info (" Font" ) << " Loaded CJK fallback font from bundle" ;
224+ loaded = true ;
225+ }
226+ } catch (...) {
227+ // bundled font not present
228+ }
229+ }
230+ if (!loaded) {
231+ ALogger::warn (" Font" ) << " No CJK fallback font found on iOS" ;
232+ }
233+ }
234+ #else
235+ // Unknown platform; mark fallback as unavailable.
236+ ALogger::warn (" Font" ) << " CJK font fallback not available on this platform" ;
237+ #endif
238+ }
60239
61240glm::vec2 AFont::getKerning (wchar_t left, wchar_t right) {
62241 FT_Vector vec2;
@@ -69,7 +248,22 @@ AFont::Character AFont::renderGlyph(const FontEntry& fs, AChar glyph) {
69248 int size = fs.first .size ;
70249 FontRendering fr = fs.first .fr ;
71250
72- FT_Set_Pixel_Sizes (mFace , 0 , size);
251+ // Determine which face to use: try primary, fall back to CJK font if glyph missing.
252+ FT_Face face = nullptr ;
253+ if (hasGlyph (glyph.codepoint ())) {
254+ face = mFace ;
255+ } else {
256+ ensureFallbackFace ();
257+ if (mFallbackFace ) {
258+ face = mFallbackFace ;
259+ }
260+ }
261+ if (!face) {
262+ // No font has this glyph; return empty character.
263+ return Character{};
264+ }
265+
266+ FT_Set_Pixel_Sizes (face, 0 , size);
73267
74268 FT_Int32 flags = FT_LOAD_RENDER ;
75269
@@ -78,11 +272,14 @@ AFont::Character AFont::renderGlyph(const FontEntry& fs, AChar glyph) {
78272 if (fr == FontRendering::NEAREST )
79273 flags |= FT_LOAD_TARGET_MONO ;
80274
81- FT_Error e = FT_Load_Char (mFace , glyph.codepoint (), flags);
275+ FT_Error e = FT_Load_Char (face , glyph.codepoint (), flags);
82276 if (e) {
83- throw std::runtime_error ((" Cannot load char: error code" + AString::number (e)).toStdString ());
277+ // Neither face can render this glyph.
278+ ALogger::debug (" Font" ) << " FT_Load_Char failed for U+"
279+ << std::hex << std::uint32_t (glyph.codepoint ()) << " (error " << std::dec << e << " )" ;
280+ return Character{};
84281 }
85- FT_GlyphSlot g = mFace ->glyph ;
282+ FT_GlyphSlot g = face ->glyph ;
86283 if (g->bitmap .width && g->bitmap .rows ) {
87284 const float div = 1 .f / 64 .f ;
88285 int width = g->bitmap .width ;
0 commit comments