Skip to content

Commit 2d882e0

Browse files
committed
Add CJK font support and fallback mechanism in AFont class
1 parent 457a63c commit 2d882e0

3 files changed

Lines changed: 232 additions & 6 deletions

File tree

aui.views/src/AUI/Font/AFont.cpp

Lines changed: 202 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
/*
1+
/*
22
* AUI Framework - Declarative UI toolkit for modern C++20
33
* Copyright (C) 2020-2025 Alex2772 and Contributors
44
*
@@ -14,11 +14,16 @@
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

2328
AFont::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+
4660
AString 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

61240
glm::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;

aui.views/src/AUI/Font/AFont.h

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
/*
1+
/*
22
* AUI Framework - Declarative UI toolkit for modern C++20
33
* Copyright (C) 2020-2025 Alex2772 and Contributors
44
*
@@ -115,12 +115,30 @@ class API_AUI_VIEWS AFont {
115115
AByteBuffer mFontDataBuffer;
116116
FT_FaceRec_* mFace;
117117

118+
// Fallback font for glyphs not present in the primary font (e.g. CJK)
119+
AByteBuffer mFallbackFontDataBuffer;
120+
FT_FaceRec_* mFallbackFace = nullptr;
121+
AString mFallbackFontPath;
122+
bool mFallbackAttempted = false;
123+
118124
AMap<FontKey, FontData> mCharData;
119125

120126
FontData& getFontEntry(unsigned size, FontRendering fr) {
121127
return mCharData[FontKey{size, fr}];
122128
}
123129

130+
/**
131+
* @brief Checks if the primary font contains the given glyph.
132+
* @return true if the glyph is available in the font.
133+
*/
134+
bool hasGlyph(char32_t codepoint) const;
135+
136+
/**
137+
* @brief Ensures a fallback CJK font face is loaded for glyphs not in the primary font.
138+
* Uses fontconfig to find a system CJK font.
139+
*/
140+
void ensureFallbackFace();
141+
124142
Character renderGlyph(const FontEntry& fs, AChar glyph);
125143

126144
public:
@@ -134,6 +152,8 @@ class API_AUI_VIEWS AFont {
134152

135153
glm::vec2 getKerning(wchar_t left, wchar_t right);
136154

155+
~AFont();
156+
137157
AFont(const AFont&) = delete;
138158

139159
Character& getCharacter(const FontEntry& charset, AChar glyph);

examples/ui/views/src/ExampleWindow.cpp

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,15 @@ ExampleWindow::ExampleWindow() : AWindow("Examples", 800_dp, 700_dp) {
208208
AScrollArea::Builder().withContents(std::conditional_t<
209209
aui::platform::current::is_mobile(), Vertical, Horizontal> {
210210
Vertical {
211+
// CJK font test
212+
GroupBox {
213+
Label { "CJK Font Test" },
214+
Vertical {
215+
Label { "Chinese: 中文测试" },
216+
Label { "Japanese: 日本語テスト" },
217+
Label { "Korean: 한국어 테스트" },
218+
} AUI_OVERRIDE_STYLE { LayoutSpacing { 4_dp } },
219+
},
211220
// buttons
212221
GroupBox {
213222
Label { "Buttons" },

0 commit comments

Comments
 (0)