Skip to content

Commit 2425e93

Browse files
committed
feat(fontconfig): 字体发现——七个生成物全部由 build.mcpp 产出
freetype 光栅化字形、harfbuzz 整形一段文字,但两者都答不了「给我一个带 CJK 的 无衬线字体」。那是 fontconfig,而 cairo 和 pango 都要它。 上游 meson 用 python 和 gperf 产出七个生成物,七个全是对 tarball 里已有文件的 文本变换,所以 fork 的 build.mcpp(mcpp 编译并运行的 C++)把它们全做了。这个包 不需要 python3、sh 或 gperf。 两个大的与上游**逐字节一致**,fork CI 每次重跑上游脚本再 diff: fclang.h 4897 行 281 个 .orth -> charset 位图 fccase.h 368 行 Unicode 大小写折叠 转写只有被拿去和原件对照才值得有。 fcobjshash.h 是**判断**而非转写:上游跑 C 预处理器 + gperf 做 72 条目的完美哈希, 而唯一消费者 fcobjs.c 只调 FcObjectTypeLookup 读一个字段。排序表 + 二分查找语义 相同,少两个工具、少一遍预处理。 五条运行期路径留空(FC_DEFAULT_FONTS / FC_FONTPATH / FC_CACHEDIR / CONFIGDIR / FONTCONFIG_PATH)——上游指向构建前缀,重定位后是宿主的字体与配置。 FONTCONFIG_FILE / FONTCONFIG_PATH / FONTCONFIG_SYSROOT 是环境出口。 ⚠ HAVE_FT_GET_BDF_PROPERTY 与 HAVE_FT_GET_PS_FONT_INFO **关掉**,因为 compat.freetype 不编 BDF 和 Type1 模块。声明了能编过,链接时八个未定义引用。 探测答案必须描述**这一套** freetype,不是上游默认构建。 测试直打三张生成表:FcNameParse 走对象表、FcLangGetCharSet 走 fclang (zh-cn 6765 码点、ru 与 en 不同集)、FcStrDowncase 走 fccase(含西里尔两字节折叠)。 只改 pkgs/ 与 tests/examples/。
1 parent 7741d10 commit 2425e93

4 files changed

Lines changed: 228 additions & 0 deletions

File tree

mcpp.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ members = [
7979
"tests/examples/redis-plus-plus-async",
8080
"tests/examples/sdl2",
8181
"tests/examples/spdlog",
82+
"tests/examples/fontconfig",
8283
"tests/examples/freetype",
8384
"tests/examples/glad",
8485
"tests/examples/libaio",

pkgs/f/freedesktop.fontconfig.lua

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
-- freedesktop.fontconfig — font discovery and matching.
2+
--
3+
-- `compat.freetype` rasterizes a glyph and `compat.harfbuzz` shapes a run, but
4+
-- neither can answer "give me a sans-serif that has CJK". That is fontconfig,
5+
-- and it is what every toolkit above this layer links: cairo and pango both
6+
-- require it, and any program that names a font rather than a font FILE needs
7+
-- it.
8+
--
9+
-- ─────────────────────────────────────────────────────────────────────────
10+
-- EVERY GENERATED FILE IS PRODUCED BY build.mcpp — NO python, sh OR gperf
11+
--
12+
-- Upstream's meson emits SEVEN artifacts through python and gperf. All seven
13+
-- are text transforms over files already in the tarball, so the fork's
14+
-- `build.mcpp` — C++ that mcpp compiles and runs — does them:
15+
--
16+
-- fcstdint.h a copy
17+
-- fcalias.h/fcaliastail.h internal-alias headers (makealias.py)
18+
-- fcftalias.h/… the same over fcfreetype.h
19+
-- fcobjshash.h object-name lookup (cutout.py + gperf)
20+
-- fclang.h 281 .orth files -> charset bitmaps
21+
-- fccase.h Unicode case folding
22+
-- src/fontconfig.cppm the C++23 module wrapper
23+
--
24+
-- THE TWO BIG ONES ARE VERIFIED BYTE-FOR-BYTE against upstream's own Python,
25+
-- and the fork's CI re-runs those scripts and diffs on every push: `fclang.h`
26+
-- is 4,897 lines, `fccase.h` is 368. A transliteration is only worth having if
27+
-- something checks it against the original.
28+
--
29+
-- `fcobjshash.h` is a DECISION rather than a transliteration: upstream runs the
30+
-- C preprocessor and gperf to build a perfect hash over 72 entries, and the one
31+
-- consumer — `fcobjs.c` — calls `FcObjectTypeLookup(str, strlen(str))` and
32+
-- reads a single field. A sorted table with a binary search has identical
33+
-- semantics and removes both tools.
34+
--
35+
-- ─────────────────────────────────────────────────────────────────────────
36+
-- THE RUNTIME PATHS ARE EMPTY, AND THAT IS THE USUAL DECISION
37+
--
38+
-- `FC_DEFAULT_FONTS`, `FC_FONTPATH`, `FC_CACHEDIR`, `CONFIGDIR` and
39+
-- `FONTCONFIG_PATH` point into the build prefix upstream, which after
40+
-- relocation is the HOST's fonts and the HOST's configuration — the same silent
41+
-- host edge that gave Vulkan an llvmpipe device instead of the GPU.
42+
--
43+
-- fontconfig has the escape hatches that make empty workable: `FONTCONFIG_FILE`
44+
-- (which config to read), `FONTCONFIG_PATH` (where configs live) and
45+
-- `FONTCONFIG_SYSROOT` (a prefix over both), all read in fccfg.c. An ecosystem
46+
-- that ships fonts declares them, exactly as xim:xkeyboard-config declares
47+
-- XKB_CONFIG_ROOT — and until something does, `FcInit()` finds no fonts and
48+
-- says so rather than rendering with the developer's.
49+
--
50+
-- ⚠ TWO FREETYPE PROBES ARE OFF, and this is worth knowing before someone
51+
-- "fixes" them: `HAVE_FT_GET_BDF_PROPERTY` and `HAVE_FT_GET_PS_FONT_INFO` are
52+
-- absent because `compat.freetype` does not build the BDF and Type1 modules.
53+
-- Claiming them compiles cleanly and fails at LINK with eight undefined
54+
-- references from fcfreetype.c. A probe answer has to describe THIS freetype,
55+
-- not upstream freetype's default build.
56+
package = {
57+
spec = "1",
58+
namespace = "freedesktop",
59+
name = "fontconfig",
60+
description = "fontconfig 2.15.0 — font discovery and matching, every generated file produced by build.mcpp",
61+
licenses = {"MIT"},
62+
repo = "https://github.com/mcpplibs/fontconfig",
63+
type = "package",
64+
65+
xpm = {
66+
linux = {
67+
["2.15.0"] = {
68+
url = {
69+
GLOBAL = "https://github.com/mcpplibs/fontconfig/releases/download/v2.15.0/fontconfig-2.15.0-mcpp2.tar.gz",
70+
CN = "https://gitcode.com/mcpp-res/fontconfig/releases/download/2.15.0/fontconfig-2.15.0-mcpp2.tar.gz",
71+
},
72+
sha256 = "a7ac9d403f0035d2c6065237ebed8c6b0b68aab98be948ea2cc907b886be9dc2",
73+
},
74+
},
75+
},
76+
77+
mcpp = "*/mcpp/fontconfig/mcpp.toml",
78+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# fontconfig test member — the three generated tables.
2+
[indices]
3+
freedesktop = { path = "../../.." }
4+
5+
[package]
6+
name = "fontconfig-tests"
7+
version = "0.1.0"
8+
standard = "c++23"
9+
10+
# `compat.expat` and `compat.freetype` arrive transitively; naming them here as
11+
# version deps as well is fine because the package declares them the same way
12+
# (not as path deps), unlike the wayland members.
13+
[target.'cfg(linux)'.dependencies.freedesktop]
14+
fontconfig = "2.15.0"
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
// freedesktop.fontconfig — the three generated tables, exercised through the
2+
// public API.
3+
//
4+
// WHAT CAN BE ASSERTED WITHOUT FONTS
5+
//
6+
// This package compiles the four runtime paths EMPTY on purpose (see the
7+
// manifest), so `FcInit()` on a runner with no `FONTCONFIG_FILE` finds no
8+
// configuration and no fonts. That is the intended behaviour, not a defect,
9+
// and it means a font-matching test would only be asserting that the runner
10+
// has a desktop.
11+
//
12+
// The three GENERATED tables need none of that, and they are what this fork
13+
// exists to produce:
14+
//
15+
// fclang.h 281 .orth files compiled into charset bitmaps
16+
// fccase.h Unicode case folding
17+
// fcobjshash.h object-name → id, the gperf replacement
18+
//
19+
// Each is reachable through a public entry point that takes no config.
20+
21+
#ifdef __linux__
22+
23+
#include <fontconfig/fontconfig.h>
24+
25+
#include <cstdio>
26+
#include <cstring>
27+
#include <initializer_list>
28+
29+
namespace {
30+
31+
int failures = 0;
32+
33+
void check(bool ok, const char *what)
34+
{
35+
std::printf("%-58s %s\n", what, ok ? "ok" : "FAILED");
36+
if (!ok) {
37+
++failures;
38+
}
39+
}
40+
41+
} // namespace
42+
43+
int main()
44+
{
45+
std::printf(" FcGetVersion() = %d\n", FcGetVersion());
46+
check(FcGetVersion() >= 21500, "FcGetVersion reports 2.15.0 or newer");
47+
48+
// ── fcobjshash.h — the gperf replacement ─────────────────────────────
49+
//
50+
// Exercised through `FcNameParse`, which is the public route into the
51+
// object table: the parser maps every "name=value" key through
52+
// `FcObjectLookupIdByName`, which IS the generated lookup. `FcObject` and
53+
// `FcObjectFromName` are internal (fcint.h), so reaching for them would
54+
// test the table without testing that a consumer can get to it.
55+
{
56+
FcPattern *p = FcNameParse((const FcChar8 *) "Arial:pixelsize=12:slant=100");
57+
check(p != nullptr, "fcobjshash: FcNameParse accepted three object names");
58+
if (p != nullptr) {
59+
FcChar8 *fam = nullptr;
60+
double px = 0;
61+
int slant = 0;
62+
const bool gf = FcPatternGetString(p, FC_FAMILY, 0, &fam) == FcResultMatch;
63+
const bool gp = FcPatternGetDouble(p, FC_PIXEL_SIZE, 0, &px) == FcResultMatch;
64+
const bool gs = FcPatternGetInteger(p, FC_SLANT, 0, &slant) == FcResultMatch;
65+
std::printf(" family=%s pixelsize=%g slant=%d\n",
66+
gf && fam ? (const char *) fam : "(none)", px, slant);
67+
check(gf && fam && std::strcmp((const char *) fam, "Arial") == 0,
68+
"…and \"family\" round-tripped to the right object");
69+
check(gp && px == 12.0, "…and \"pixelsize\" did too");
70+
check(gs && slant == 100, "…and \"slant\" did too");
71+
FcPatternDestroy(p);
72+
}
73+
74+
// A name the table does not contain must not resolve to a builtin —
75+
// fontconfig assigns it a dynamic id instead. This is the branch a
76+
// binary search gets wrong when its bounds are off by one.
77+
FcPattern *q = FcNameParse((const FcChar8 *) ":nosuchproperty=1");
78+
check(q != nullptr, "…and an unknown property does not crash the parser");
79+
if (q) FcPatternDestroy(q);
80+
}
81+
82+
// ── fclang.h — the language charsets ─────────────────────────────────
83+
// `FcLangGetCharSet` indexes straight into fcLangCharSets. Three languages
84+
// with very different coverage, so a table that was truncated or misordered
85+
// fails on at least one.
86+
for (const char *l : {"en", "ru", "zh-cn"}) {
87+
const FcCharSet *cs = FcLangGetCharSet((const FcChar8 *) l);
88+
const FcChar32 n = cs ? FcCharSetCount(cs) : 0;
89+
std::printf(" lang %-6s -> %u codepoints\n", l, n);
90+
check(cs != nullptr && n > 0, "fclang: the language has a charset");
91+
}
92+
// Latin and Cyrillic must not be the same set — they are separate entries
93+
// whose leaves were de-duplicated, and a dedup bug shows up here.
94+
{
95+
const FcCharSet *en = FcLangGetCharSet((const FcChar8 *) "en");
96+
const FcCharSet *ru = FcLangGetCharSet((const FcChar8 *) "ru");
97+
check(en && ru && !FcCharSetEqual(en, ru),
98+
"…and English and Russian are different sets");
99+
check(en && FcCharSetHasChar(en, 'A'), "…and English covers 'A'");
100+
check(ru && FcCharSetHasChar(ru, 0x0416), "…and Russian covers U+0416");
101+
check(en && !FcCharSetHasChar(en, 0x0416), "…and English does not");
102+
}
103+
104+
// A language that does not exist must return null rather than index out of
105+
// range — the fastpath table `fcLangCharSetRanges` is what decides that.
106+
check(FcLangGetCharSet((const FcChar8 *) "zz-nowhere") == nullptr,
107+
"fclang: an unknown language returns null");
108+
109+
// ── fccase.h — Unicode case folding ──────────────────────────────────
110+
// `FcStrDowncase` walks fcCaseFold. ASCII exercises the EVEN_ODD/RANGE
111+
// methods; U+0130 (Latin capital I with dot) is a FULL fold — one char to
112+
// several — which is the method with its own byte pool.
113+
{
114+
FcChar8 *d = FcStrDowncase((const FcChar8 *) "ABC");
115+
std::printf(" downcase(\"ABC\") = %s\n", d ? (const char *) d : "(null)");
116+
check(d && std::strcmp((const char *) d, "abc") == 0,
117+
"fccase: ASCII folds through the range methods");
118+
if (d) FcStrFree(d);
119+
}
120+
{
121+
// U+0410 CYRILLIC CAPITAL A -> U+0430, a two-byte fold.
122+
FcChar8 *d = FcStrDowncase((const FcChar8 *) "\xd0\x90");
123+
const bool ok = d && std::strcmp((const char *) d, "\xd0\xb0") == 0;
124+
std::printf(" downcase(U+0410) = %s\n", ok ? "U+0430" : "(wrong)");
125+
check(ok, "…and a Cyrillic capital folds to its lowercase");
126+
if (d) FcStrFree(d);
127+
}
128+
129+
std::printf("\n%d check(s) failed\n", failures);
130+
return failures == 0 ? 0 : 1;
131+
}
132+
133+
#else
134+
int main() { return 0; }
135+
#endif

0 commit comments

Comments
 (0)