diff --git a/patch_v2/meow_launcherd/font_theme.c b/patch_v2/meow_launcherd/font_theme.c index 23ff6ac..0988dca 100644 --- a/patch_v2/meow_launcherd/font_theme.c +++ b/patch_v2/meow_launcherd/font_theme.c @@ -2,17 +2,20 @@ /* 本文件由拆分工具从 meow_launcherd.c 按职责搬移;逻辑与原文 0-diff(仅 static 可见性按跨模块需求调整)。*/ #include "meow_launcher_internal.h" -/* M2.6 字重结构:卡内字体 = <卡根>/MeowOS/fonts///(三件套)。 - * 每个 preset = 一套 /(如 misans/regular)。值 = 卡根相对路径 - * MeowOS/fonts///font.png;family/weight 单列供设置页「family→字重」子菜单 - * 分组与 meow_font_effective_paths_tiered 按 family+weight 选库。 */ +/* M2.6 字重结构 + M2.8 三级定案(FONT_SYSTEM):卡内字体 = + * <卡根>/MeowOS/fonts///(两级,向后兼容)或 + * <卡根>/MeowOS/fonts////(三级,tier=common/gbk/full)。 + * 每个 preset = 一套 /[/](如 misans/regular 或 source_han_sans_cn/regular/gbk)。 + * 值 = 卡根相对路径 MeowOS/fonts//[/]/font.png;family/weight 单列供设置页 + * 「family→字重」子菜单分组与 meow_font_effective_paths_tiered 按 family+weight 选库。 */ const char *font_presets[FONT_PRESETS_MAX]; /* SC_PRESET 值 = 卡根相对路径 */ const char *font_preset_labels[FONT_PRESETS_MAX]; /* SC_PRESET 中文标签(含字重+档位大小标记) */ -/* M2.5x 字体档位:每套的 charset 档("common"/"gbk"/"full",取自 font.json,缺省 "common") - * 与 glyph_count(信息用)。供档位分层装载(meow_font_effective_paths_tiered)与设置页标注。 */ -static const char *font_preset_tiers[FONT_PRESETS_MAX]; +/* M2.5x/M2.8 字体档位:每套的档("common"/"gbk"/"full";两级取 font.json charset,缺省 + * "common";三级取 tier 目录名,权威)与 glyph_count(信息用)。供档位分层装载 + * (meow_font_effective_paths_tiered)与设置页标注/选档。 */ +const char *font_preset_tiers[FONT_PRESETS_MAX]; static int font_preset_glyphs[FONT_PRESETS_MAX]; @@ -216,10 +219,12 @@ int meow_font_effective_tier(int app_tier) * - 生效档 = max(app 声明档, font.load_full ? GBK : COMMON):系统「加载大字库」开 → * 强制 ≥gbk; * - tier<=common → 走既有 meow_font_effective_paths(font.file/卡默认/固件); - * - tier>0 → 在已扫描的 MeowOS/fonts/// 里找 charset 匹配(gbk 请求接受 - * gbk/full;full 请求接受 full)的那套,以其 font.png/idx 绝对路径返回(family = 一级目录名); + * - tier>0 → 在已扫描的 preset(两级或三级,值路径 = MeowOS/fonts//[/]/font.png) + * 里找档位匹配(gbk 请求接受 gbk/full;full 请求接受 full)的那套,以其 font.png/idx 绝对路径返回 + * (family = 一级目录名)。 * - M2.6 字重:优先选「与用户当前 font.file 同 family(其次同 weight)」的大字库,避免 - * 大字库跳到另一家字体;找不到匹配 → 回退 meow_font_effective_paths(避免点到固件小库)。 + * 大字库跳到另一家字体(三级同 family 下按 tier 找 gbk/full,见下);找不到匹配 → + * 回退 meow_font_effective_paths(避免点到固件小库)。 * 返回 0 成功(out_png/out_idx/out_family 已填),-1 无可用。 */ int meow_font_effective_paths_tiered(int tier, char *out_png, int plen, char *out_idx, int ilen, char *out_family, int flen) @@ -462,11 +467,15 @@ static void font_tier_marker(const char *charset, int glyph_count, char *out, in } -/* 探测一个 <卡根>/MeowOS/fonts// 套件是否合法:三件套齐全 → 解析 label+档位+字重。 +/* 探测一套件是否合法:三件套齐全 → 解析 label+档位+字重。 + * dirpath = <卡根>/MeowOS/fonts//(两级)或 ...///(三级)。 + * tier_dir:三级时传该 tier 目录名(common/gbk/full,权威档位);两级传 NULL(档位读 + * font.json charset,缺省 common)。 * 返回 1=进列表,0=跳过(缺组件/json 不可读)。sha 失败仅 WARN。 * 输出:label=显示名 ` (common≈4k)`(如 `MiSans Regular(common≈4k)`); * charset/glyph_count=档位信息;wtout=weight slug(缺省 "regular")。 */ static int meow_font_probe_weight(const char *dirpath, const char *family, const char *weight, + const char *tier_dir, char *label, int llen, char *charset, int clen, int *glyph_count, char *wtout, int wtlen, char *nameout, int namelen) @@ -475,8 +484,11 @@ static int meow_font_probe_weight(const char *dirpath, const char *family, const snprintf(ppath, sizeof ppath, "%s/font.png", dirpath); snprintf(ipath, sizeof ipath, "%s/font.idx", dirpath); snprintf(jpath, sizeof jpath, "%s/font.json", dirpath); + const int is3 = (tier_dir != NULL && tier_dir[0] != 0); + const char *tsep = is3 ? "/" : ""; + const char *tiernm = is3 ? tier_dir : ""; if (access(ppath, R_OK) != 0 || access(ipath, R_OK) != 0 || access(jpath, R_OK) != 0) { - logmsg("fonts: skip '%s/%s' (缺 font.png/font.idx/font.json)", family, weight); + logmsg("fonts: skip '%s/%s%s%s' (缺 font.png/font.idx/font.json)", family, weight, tsep, tiernm); return 0; } char name[64] = "", jsha_png[80] = "", jsha_idx[80] = ""; @@ -484,27 +496,28 @@ static int meow_font_probe_weight(const char *dirpath, const char *family, const meow_font_json_probe(jpath, name, sizeof name, jsha_png, sizeof jsha_png, jsha_idx, sizeof jsha_idx, cs, sizeof cs, &gc, wts, sizeof wts); const char *wt = wts[0] ? wts : (weight ? weight : "regular"); + /* 档位:三级定案 = tier 目录名(权威);两级(向后兼容)= font.json charset(缺省 common)。 */ + const char *tier = is3 ? tier_dir : (cs[0] ? cs : "common"); if (wtout) snprintf(wtout, wtlen, "%s", wt); if (nameout) snprintf(nameout, namelen, "%s", name[0] ? name : family); /* 显示名 = ` (common≈4k)`(字重缺省 Regular) */ char wd[24]; font_weight_display(wt, wd, sizeof wd); char marker[24]; - font_tier_marker(cs, gc, marker, sizeof marker); + font_tier_marker(tier, gc, marker, sizeof marker); snprintf(label, llen, "%s %s(%s)", name[0] ? name : family, wd, marker); - if (charset) snprintf(charset, clen, "%s", cs[0] ? cs : "common"); + if (charset) snprintf(charset, clen, "%s", tier); if (glyph_count) *glyph_count = gc; /* 完整性校验:仅 WARN,不硬失败;该套仍标记可选。 */ char apng[80] = "", aidx[80] = ""; if (font_sha256_file(ppath, apng, sizeof apng) != 0) - logmsg("fonts: '%s/%s' sha256(png) 不可用,跳过校验", family, wt); + logmsg("fonts: '%s/%s%s%s' sha256(png) 不可用,跳过校验", family, wt, tsep, tiernm); else if (jsha_png[0] && !font_sha_eq(apng, jsha_png)) - logmsg("fonts: '%s/%s' WARN sha256_png 不匹配(仍可选)", family, wt); + logmsg("fonts: '%s/%s%s%s' WARN sha256_png 不匹配(仍可选)", family, wt, tsep, tiernm); if (font_sha256_file(ipath, aidx, sizeof aidx) != 0) - logmsg("fonts: '%s/%s' sha256(idx) 不可用,跳过校验", family, wt); + logmsg("fonts: '%s/%s%s%s' sha256(idx) 不可用,跳过校验", family, wt, tsep, tiernm); else if (jsha_idx[0] && !font_sha_eq(aidx, jsha_idx)) - logmsg("fonts: '%s/%s' WARN sha256_idx 不匹配(仍可选)", family, wt); - logmsg("fonts: add '%s/%s' (%s) charset=%s glyph_count=%d", family, wt, label, - cs[0]?cs:"common", gc); + logmsg("fonts: '%s/%s%s%s' WARN sha256_idx 不匹配(仍可选)", family, wt, tsep, tiernm); + logmsg("fonts: add '%s/%s%s%s' (%s) tier=%s glyph_count=%d", family, wt, tsep, tiernm, label, tier, gc); return 1; } @@ -572,15 +585,51 @@ static void meow_font_write_index(const char *card_root) } -/* 扫描 /MeowOS/fonts/ 下两级 //,重建字体 preset 列表。 - * 每个 (family,weight) 组合一套(如 misans/regular、misans/bold);family 级 'default' 由 - * 索引 0(默认字体)单独处理,不进循环,避免重复。上限 FONT_PRESETS_MAX - * (1 默认 + ≤16 用户字体)。无卡/无目录时仅默认。 +/* 把一个探测成功的套装登记为 preset i(值路径 = MeowOS/fonts//[/]/font.png)。 + * tier 非空 → 三级路径;否则 → 两级路径(旧结构向后兼容)。返回 0 成功,-1 已满。 */ +static int font_presets_add(const char *family, const char *weight, const char *tier, + const char *label, const char *cs, int gc, + const char *wt, const char *nm) +{ + if (font_presets_n >= FONT_PRESETS_MAX) return -1; + int i = font_presets_n; + if (tier && tier[0]) + snprintf(font_preset_val_buf[i], sizeof font_preset_val_buf[i], + "%s/%s/%s/%s/font.png", MEOW_FONTS_CFG, family, weight, tier); + else + snprintf(font_preset_val_buf[i], sizeof font_preset_val_buf[i], + "%s/%s/%s/font.png", MEOW_FONTS_CFG, family, weight); + snprintf(font_preset_label_buf[i], sizeof font_preset_label_buf[i], "%s", label); + snprintf(font_preset_tier_buf[i], sizeof font_preset_tier_buf[i], "%s", cs && cs[0] ? cs : "common"); + snprintf(font_preset_fam_buf[i], sizeof font_preset_fam_buf[i], "%s", family); + snprintf(font_preset_wt_buf[i], sizeof font_preset_wt_buf[i], "%s", wt && wt[0] ? wt : "regular"); + snprintf(font_preset_name_buf[i], sizeof font_preset_name_buf[i], "%s", nm ? nm : family); + font_presets[i] = font_preset_val_buf[i]; + font_preset_labels[i] = font_preset_label_buf[i]; + font_preset_tiers[i] = font_preset_tier_buf[i]; + font_preset_fams[i] = font_preset_fam_buf[i]; + font_preset_wts[i] = font_preset_wt_buf[i]; + font_preset_names[i] = font_preset_name_buf[i]; + font_preset_glyphs[i] = gc; + font_presets_n++; + return 0; +} + + +/* 扫描 /MeowOS/fonts/ 下字体,重建字体 preset 列表。 + * 布局(FONT_SYSTEM 三级定案): + * - 两级(向后兼容)://font.png → 每 (family,weight) 一套,tier 读 font.json + * charset(缺省 common)。 + * - 三级(定案):///(tier=common/gbk/full,每 tier 一套三件套)→ + * 每 (family,weight,tier) 一套,tier=目录名(权威)。 + * - 同一 (family,weight) 下:若 weight 根有 font.png(两级)则按两级;否则扫其 / 子目录。 + * family 级 'default' 由索引 0(默认字体)单独处理,不进循环,避免重复。上限 FONT_PRESETS_MAX + * (1 默认 + 卡内各套,按 (family,weight,tier) 一套一 preset 计数)。无卡/无目录时仅默认。 * * M2.58 释放语义(规格 §5.7 需求 4「扫→写 index→释放(或按需读 index)」结论): * - 持久来源 = /MeowOS/config/fonts/index(扫描后由 meow_font_write_index 落盘)。 * SDK 只读该文件,从不依赖本内存数组。 - * - font_presets* / *_buf 均为「静态、有界(FONT_PRESETS_MAX=17)、非堆」数组:无内存泄漏, + * - font_presets* / *_buf 均为「静态、有界(FONT_PRESETS_MAX=64)、非堆」数组:无内存泄漏, * 故无运行期 free;「释放」含义 = 两个扫描点(启动 / 进设置页)重建 + 每套固定上限防膨胀。 * - 设置字体列表(font.file / font.class_* 下拉、family→字重子菜单)在本视图内显示时用 * 该数组;离开字体列表视图(run_settings_page 退出)无需显式 free(静态数组),下次 @@ -630,27 +679,37 @@ void meow_font_scan_presets_root(const char *card_root) struct dirent *we; while ((we = readdir(w)) && font_presets_n < FONT_PRESETS_MAX) { if (we->d_name[0] == '.') continue; - char dirpath[400], label[96], cs[8] = ""; int gc = 0; char wt[32] = "", nm[64] = ""; + char dirpath[400], pngcheck[420]; snprintf(dirpath, sizeof dirpath, "%s/%s", fampath, we->d_name); - if (meow_font_probe_weight(dirpath, de->d_name, we->d_name, label, sizeof label, - cs, sizeof cs, &gc, wt, sizeof wt, nm, sizeof nm) == 0) - continue; /* 缺组件/损坏 → 跳过该套 */ - int i = font_presets_n; - snprintf(font_preset_val_buf[i], sizeof font_preset_val_buf[i], - "%s/%s/%s/font.png", MEOW_FONTS_CFG, de->d_name, we->d_name); - snprintf(font_preset_label_buf[i], sizeof font_preset_label_buf[i], "%s", label); - snprintf(font_preset_tier_buf[i], sizeof font_preset_tier_buf[i], "%s", cs[0]?cs:"common"); - snprintf(font_preset_fam_buf[i], sizeof font_preset_fam_buf[i], "%s", de->d_name); - snprintf(font_preset_wt_buf[i], sizeof font_preset_wt_buf[i], "%s", wt); - snprintf(font_preset_name_buf[i], sizeof font_preset_name_buf[i], "%s", nm); - font_presets[i] = font_preset_val_buf[i]; - font_preset_labels[i] = font_preset_label_buf[i]; - font_preset_tiers[i] = font_preset_tier_buf[i]; - font_preset_fams[i] = font_preset_fam_buf[i]; - font_preset_wts[i] = font_preset_wt_buf[i]; - font_preset_names[i] = font_preset_name_buf[i]; - font_preset_glyphs[i] = gc; - font_presets_n++; + snprintf(pngcheck, sizeof pngcheck, "%s/font.png", dirpath); + char label[96] = "", cs[8] = ""; int gc = 0; char wt[32] = "", nm[64] = ""; + if (access(pngcheck, R_OK) == 0) { + /* 两级(旧结构向后兼容)://font.png,tier 读 font.json charset。 */ + if (meow_font_probe_weight(dirpath, de->d_name, we->d_name, NULL, + label, sizeof label, cs, sizeof cs, &gc, + wt, sizeof wt, nm, sizeof nm) == 0) + continue; /* 缺组件/损坏 → 跳过该套 */ + font_presets_add(de->d_name, we->d_name, NULL, label, cs, gc, wt, nm); + continue; + } + /* 三级定案:进入 // 逐套探测,tier=目录名(权威档位)。 */ + DIR *t = opendir(dirpath); + if (!t) continue; + struct dirent *te; + while ((te = readdir(t)) && font_presets_n < FONT_PRESETS_MAX) { + if (te->d_name[0] == '.') continue; + /* tier 仅接受 common/gbk/full(FONT_SYSTEM §1);杂散子目录不进列表。 */ + if (strcmp(te->d_name, "common") && strcmp(te->d_name, "gbk") && + strcmp(te->d_name, "full")) continue; + char tpath[420]; + snprintf(tpath, sizeof tpath, "%s/%s", dirpath, te->d_name); + if (meow_font_probe_weight(tpath, de->d_name, we->d_name, te->d_name, + label, sizeof label, cs, sizeof cs, &gc, + wt, sizeof wt, nm, sizeof nm) == 0) + continue; /* 缺组件/损坏 → 跳过该 tier 套 */ + font_presets_add(de->d_name, we->d_name, te->d_name, label, cs, gc, wt, nm); + } + closedir(t); } closedir(w); } diff --git a/patch_v2/meow_launcherd/meow_launcher_internal.h b/patch_v2/meow_launcherd/meow_launcher_internal.h index f360248..9830bb3 100644 --- a/patch_v2/meow_launcherd/meow_launcher_internal.h +++ b/patch_v2/meow_launcherd/meow_launcher_internal.h @@ -115,7 +115,11 @@ extern "C" { #define UI_FONT_PNG_SYS "/usr/share/meowos/font/font.png" #define UI_FONT_IDX_SYS "/usr/share/meowos/font/font.idx" #define MEOW_FONTS_CFG "MeowOS/fonts" -#define FONT_PRESETS_MAX 17 /* 1 默认 + ≤16 卡内字体(防爆) */ +/* 字体 preset 上限(FONT_SYSTEM 三级定案后按 (family,weight,tier) 一套一 preset): + * 1 默认 + 卡内各套(如 source_han_sans_cn:7 字重 common + regular/medium/bold gbk = 10 套, + * 叠加既有 6 套 + 后续多语言/多档位余量)。静态有界、非堆,全数组 BSS 约 36KB,可接受。 + * 注:SDK meow_font.c MEOW_FONT_INDEX_MAX=32 读 index 时取前 32 条(sdk/ 并行作业,未同步改)。 */ +#define FONT_PRESETS_MAX 64 #define ROW_H 52 #define ROW_TOP 96 #define EXIT_ROW 640 @@ -511,6 +515,7 @@ extern int fb_fd; extern const char *font_preset_fams[FONT_PRESETS_MAX]; extern const char *font_preset_labels[FONT_PRESETS_MAX]; extern const char *font_preset_names[FONT_PRESETS_MAX]; +extern const char *font_preset_tiers[FONT_PRESETS_MAX]; extern const char *font_preset_wts[FONT_PRESETS_MAX]; extern const char *font_presets[FONT_PRESETS_MAX]; extern int font_presets_n; diff --git a/patch_v2/meow_launcherd/meow_launcherd b/patch_v2/meow_launcherd/meow_launcherd index d15a02a..4961767 100755 Binary files a/patch_v2/meow_launcherd/meow_launcherd and b/patch_v2/meow_launcherd/meow_launcherd differ diff --git a/patch_v2/meow_launcherd/settings_page.c b/patch_v2/meow_launcherd/settings_page.c index eb1e5c2..5bd612b 100644 --- a/patch_v2/meow_launcherd/settings_page.c +++ b/patch_v2/meow_launcherd/settings_page.c @@ -824,10 +824,34 @@ ddone: } -/* ---- 字体行「family→字重」两级子菜单(M2.6 字重结构)---- +/* 三级 (family,weight) 下多 tier 的选档偏好分(越大越优先;设置页写 font.file/class 值用)。 + * 关(font.load_full=0)→ common 优先(回退 gbk→full);开 → gbk 优先(回退 full→common)。 */ +static int font_tier_pick_rank(const char *tier, int load_full) +{ + int is_common = tier && !strcmp(tier, "common"); + int is_gbk = tier && !strcmp(tier, "gbk"); + int is_full = tier && !strcmp(tier, "full"); + if (!load_full) { + if (is_common) return 3; + if (is_gbk) return 2; + if (is_full) return 1; + return 0; + } + if (is_gbk) return 3; + if (is_full) return 2; + if (is_common) return 1; + return 0; +} + + +/* ---- 字体行「family→字重」两级子菜单(M2.6 字重结构;M2.8 三级适配)---- * 一级:列出扫描到的去重 family(class 行首项「跟随全局」=空值);选 family 后自动进入 * 二级:该 family 的可用字重(regular/bold/italic…)。选中写 out_val = 卡根相对路径 - * MeowOS/fonts///font.png(或 class 行「跟随全局」=空串)。 + * MeowOS/fonts///font.png(两级)或 ...////font.png(三级, + * 或 class 行「跟随全局」=空串)。 + * M2.8 三级冲突:同 (family,weight) 出现多个 tier → 选择器仍为 family→weight 两级、不新增 tier + * 级菜单;每 weight 只显一行(代表 = 按「当前加载大字库」开关选档:关=common 优先、开=gbk 优先, + * 见 font_tier_pick_rank),写值 = 该代表 tier 的卡根相对路径。 * 复用 s_dd_modal(与 WiFi/关于 同款模态)。返回 1=已选中(out_val 已填),0=取消。 */ static int s_font_dropdown(const srow_t *r, char *out_val, int vlen) { @@ -868,21 +892,47 @@ static int s_font_dropdown(const srow_t *r, char *out_val, int vlen) if (is_cls && pick == 0) { snprintf(out_val, vlen, ""); return 1; } /* 跟随全局 */ const char *fam = fam_slug[pick]; - /* ---- 二级:该 family 的字重列表(值=完整卡根相对路径,显示=label)---- */ + /* ---- 二级:该 family 的字重列表(去重:同 (family,weight) 只显一行)---- + * 三级结构下同 (family,weight) 出现多个 tier(common/gbk/full)→ 不新增 tier 级菜单,按 + * 「当前加载大字库」开关定档:关=common 优先、开=gbk 优先(font_tier_pick_rank)。 */ + int load_full = 0; + meow_settings_get_switch("font.load_full", &load_full); + char wt_pick[S_NET_MAX][32]; /* 该行代表的 weight slug */ + int wt_rank[S_NET_MAX]; /* 该行当前档位偏好分(保留最高分 tier 为行代表) */ char wt_val[S_NET_MAX][MEOW_SET_MAX_LEN]; char wt_text[S_NET_MAX][96]; int wn = 0; - for (int i = 0; i < font_presets_n && wn < S_NET_MAX; i++) { + for (int i = 0; i < font_presets_n; i++) { if (!font_preset_fams[i] || strcmp(font_preset_fams[i], fam)) continue; - snprintf(wt_val[wn], sizeof wt_val[0], "%s", font_presets[i]); - snprintf(wt_text[wn], sizeof wt_text[0], "%s", font_preset_labels[i]); - wn++; + const char *wt = font_preset_wts[i] ? font_preset_wts[i] : "regular"; + const char *tier = font_preset_tiers[i] ? font_preset_tiers[i] : "common"; + int rank = font_tier_pick_rank(tier, load_full); + int slot = -1; + for (int k = 0; k < wn; k++) if (!strcmp(wt_pick[k], wt)) { slot = k; break; } + if (slot < 0) { + if (wn >= S_NET_MAX) break; + snprintf(wt_pick[wn], sizeof wt_pick[0], "%s", wt); + wt_rank[wn] = -1; + slot = wn; wn++; + } + if (rank > wt_rank[slot]) { /* 保留档位偏好最高的一套作为该 weight 行代表 */ + wt_rank[slot] = rank; + snprintf(wt_val[slot], sizeof wt_val[0], "%s", font_presets[i]); + snprintf(wt_text[slot], sizeof wt_text[0], "%s", font_preset_labels[i]); + } } if (wn <= 0) return 0; /* 该 family 无字重(理论不出现:能进 family 必已探到字重) */ const char *witem[S_NET_MAX]; for (int i = 0; i < wn; i++) witem[i] = wt_text[i]; + /* 当前位置高亮:按当前 font.file 的 (family,weight) 命中所显示 weight 行(不依赖 tier 精确匹配, + * 因三级下 font.file 具体 tier 可能与该行代表 tier 不同)。 */ int cur_wt = -1; - for (int i = 0; i < wn; i++) if (cur[0] && !strcmp(wt_val[i], cur)) cur_wt = i; + { + char cfam2[64] = "", cwt2[32] = ""; + if (cur[0]) font_family_weight_from_png(cur, cfam2, sizeof cfam2, cwt2, sizeof cwt2); + for (int i = 0; i < wn; i++) + if (cwt2[0] && !strcmp(wt_pick[i], cwt2)) { cur_wt = i; break; } + } int wp = s_dd_modal(r->label, witem, wn, cur_wt); if (wp < 0) return 0; snprintf(out_val, vlen, "%s", wt_val[wp]); diff --git a/patch_v2/meow_launcherd/test_font_tier.py b/patch_v2/meow_launcherd/test_font_tier.py new file mode 100644 index 0000000..dad95bb --- /dev/null +++ b/patch_v2/meow_launcherd/test_font_tier.py @@ -0,0 +1,198 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +test_font_tier.py — MeowOS 字体三级(FONT_SYSTEM 定案)宿主导出单测。 + +验证 launcher 侧 font 扫描对三级结构的处理(FONT_SYSTEM.md §1/§4.5): + (1) 三级扫描:<卡根>/MeowOS/fonts////(tier=common/gbk/full)→ 每 + (family,weight,tier) 一套,值 = MeowOS/fonts////font.png,tier=目录名。 + (2) 两级向后兼容://font.png → 按旧口径,tier 读 font.json charset。 + (3) 同 (family,weight) 下 weight 根有 font.png → 按两级(不另扫 tier 子目录)。 + (4) index 落盘:/MeowOS/config/fonts/index 每套一条 6 字段 + "family","weight","path","label","tier",glyph_count;三级条目含三级 path + tier 列。 + (5) 缺件(漏 font.idx 等)子目录 → 该套跳过。 + +方法:构造假卡根(两/三级混合 + 缺件 + 非法目录),用 qemu-mipsel 跑真实 +meow_launcherd --fontscan ,断言 stdout preset 列表与 index 文件。 + +用法(在 patch_v2/meow_launcherd 下): + 先 build_launcher.sh 生成 meow_launcherd(mipsel 静态 ELF),再: + python3 test_font_tier.py + 依赖 qemu-mipsel(qemu-user)。退出码 0=全过,非 0=断言失败。 +""" +import csv +import json +import os +import shutil +import subprocess +import sys +import tempfile + +ROOT = os.path.dirname(os.path.abspath(__file__)) +BIN = os.path.join(ROOT, "meow_launcherd") +FIX_ROOT = tempfile.mkdtemp(prefix="mftier_", dir="/tmp") +FONTS = os.path.join(FIX_ROOT, "MeowOS", "fonts") + + +def mk_font(rel, family, name, weight, tier, gcount): + """写一套 /[/]/font.png+font.idx+font.json(合法三件套)。""" + d = os.path.join(FONTS, rel) + os.makedirs(d, exist_ok=True) + with open(os.path.join(d, "font.png"), "wb") as f: + f.write(b"\x89PNG\r\n\x1a\nPLACEHOLDER") + with open(os.path.join(d, "font.idx"), "wb") as f: + f.write(b"MFNI\x02\x00PLACEHOLDER") + meta = { + "family": family, "name": name, "weight": weight, "glyph_count": gcount, + "charset": tier, "tier": tier, "size_list": [20, 28], "bit_depth": 4, + "sha256_png": "0" * 64, "sha256_idx": "0" * 64, + } + with open(os.path.join(d, "font.json"), "w", encoding="utf-8") as f: + json.dump(meta, f, ensure_ascii=False, indent=2) + + +def run_scan(): + r = subprocess.run(["qemu-mipsel", BIN, "--fontscan", FIX_ROOT], + capture_output=True, text=True, timeout=120) + return r.returncode, r.stdout + + +def read_index(): + p = os.path.join(FIX_ROOT, "MeowOS", "config", "fonts", "index") + if not os.path.exists(p): + return [] + rows = [] + with open(p, encoding="utf-8") as f: + for line in f: + line = line.strip() + if not line or line.startswith("#"): + continue + rows.append(next(csv.reader([line]))) + return rows + + +def main(): + if not os.path.exists(BIN): + print(f"FAIL: 未找到 {BIN},请先跑 build_launcher.sh"); return 2 + if shutil.which("qemu-mipsel") is None: + print("FAIL: 缺 qemu-mipsel"); return 2 + + # ---- 构造卡根 ---- + mk_font("default/regular", "default", "字体", "regular", "common", 4110) # 两级默认 + mk_font("misans/regular", "misans", "MiSans", "regular", "common", 4110) # 两级(旧结构) + # source_han_sans_cn 三级:regular/common + regular/gbk(同 weight 双 tier) + mk_font("source_han_sans_cn/regular/common", "source_han_sans_cn", "思源黑体 CN", "regular", "common", 4110) + mk_font("source_han_sans_cn/regular/gbk", "source_han_sans_cn", "思源黑体 CN", "regular", "gbk", 20000) + mk_font("source_han_sans_cn/regular/full", "source_han_sans_cn", "思源黑体 CN", "regular", "full", 30000) + mk_font("source_han_sans_cn/bold/common", "source_han_sans_cn", "思源黑体 CN", "bold", "common", 4110) + # 同 (family,weight) 下 weight 根有 font.png(两级)+ 同 weight 也带 common/ 子目录 → 应按两级处理 + d = os.path.join(FONTS, "dual", "regular") + os.makedirs(d, exist_ok=True) + with open(os.path.join(d, "font.png"), "wb") as f: + f.write(b"\x89PNG\r\n\x1a\nPLACEHOLDER") + with open(os.path.join(d, "font.idx"), "wb") as f: + f.write(b"MFNI\x02\x00PLACEHOLDER") + with open(os.path.join(d, "font.json"), "w", encoding="utf-8") as f: + json.dump({"family": "dual", "name": "Dual", "weight": "regular", + "glyph_count": 1000, "charset": "common"}, f) + os.makedirs(os.path.join(d, "common"), exist_ok=True) # 应被忽略(已在两级分支) + # 缺件三级:source_han_sans_cn/medium 下无 font.idx → 跳过 + broken = os.path.join(FONTS, "source_han_sans_cn", "medium", "full") + os.makedirs(broken, exist_ok=True) + with open(os.path.join(broken, "font.png"), "wb") as f: + f.write(b"PNG") + with open(os.path.join(broken, "font.json"), "w") as f: + json.dump({"family": "source_han_sans_cn", "name": "思源黑体 CN", "weight": "medium"}, f) + # 杂散三级子目录(tier 名非常规 common/gbk/full,但三件套齐全)→ 应被跳过(仅认 3 档) + stray = os.path.join(FONTS, "source_han_sans_cn", "regular", "backup") + os.makedirs(stray, exist_ok=True) + for n, b in (("font.png", b"PNG"), ("font.idx", b"IDX"), ("font.json", b'{}')): + with open(os.path.join(stray, n), "wb") as f: + f.write(b) + + rc, out = run_scan() + if rc != 0: + print(f"FAIL: qemu --fontscan 返回 {rc}\n{out}"); return 1 + + presets = {} + famwt = {} + for line in out.splitlines(): + line = line.strip() + if line.startswith("fonts="): + continue + if line and line[0].isdigit(): + parts = line.split(" -> ") + if len(parts) != 2: + continue + idx = int(parts[0].split()[0]) + rest, famwt_part = parts[1].split(" (fam=", 1) + path = rest.strip() + fam = famwt_part.split(" wt=")[0] + wt = famwt_part.split(" wt=")[1].rstrip(")") + presets[path] = idx + famwt[path] = (fam, wt) + + fails = 0 + def check(cond, msg): + nonlocal fails + if not cond: + fails += 1 + print(f"FAIL: {msg}") + else: + print(f"ok : {msg}") + + n = int(next((l.split("=")[1] for l in out.splitlines() if l.startswith("fonts=")), "0")) + check(n == len(presets), f"fonts={n} 与预设解析数 {len(presets)} 一致") + + # 预期 preset(默认 + dual/misans 两级 + shsc 4 套三级 = 7) + check(len(presets) == 7, f"扫描产出 7 套(实际 {len(presets)})") + check("MeowOS/fonts/default/regular/font.png" in presets, "两级默认在列表") + check("MeowOS/fonts/misans/regular/font.png" in presets, "两级 misans/regular 在列表") + check("MeowOS/fonts/dual/regular/font.png" in presets, "dual/regular(weight 根 font.png)按两级处理") + p_common = "MeowOS/fonts/source_han_sans_cn/regular/common/font.png" + p_gbk = "MeowOS/fonts/source_han_sans_cn/regular/gbk/font.png" + p_full = "MeowOS/fonts/source_han_sans_cn/regular/full/font.png" + p_bold = "MeowOS/fonts/source_han_sans_cn/bold/common/font.png" + check(p_common in presets, "三级 shsc/regular/common 在列表") + check(p_gbk in presets, "三级 shsc/regular/gbk 在列表") + check(p_full in presets, "三级 shsc/regular/full 在列表") + check(p_bold in presets, "三级 shsc/bold/common 在列表") + check(famwt.get(p_gbk) == ("source_han_sans_cn", "regular"), "fam/wt 三级条目系家族+字重") + check(all("medium" not in k for k in presets), "缺 font.idx 的 shsc/medium/full 被跳过") + check(all("/regular/backup/" not in k for k in presets), "杂散 tier 子目录 backup(非常规档名)被跳过") + # 同 weight 下的 common 子目录(dual/regular/common)不应被当作三级产出(已在两级分支) + check(all("/dual/regular/common/" not in k for k in presets), "dual/regular 的 common/ 子目录未产生独立条目") + + # ---- index 文件 6 字段 + 三级条目 ---- + rows = read_index() + check(len(rows) == len(presets), f"index 条数 {len(rows)} == 扫描数 {len(presets)}") + def find(path): + for r in rows: + if len(r) >= 3 and r[2] == path: + return r + return None + r = find(p_gbk) + check(r is not None, "index 含三级 gbk 条目") + if r: + check(len(r) == 6, f"index 每套 6 字段(实际 {len(r)})") + check(r[0] == "source_han_sans_cn" and r[1] == "regular", "index family/weight 列正确") + check(r[4] == "gbk", f"index tier 列 = gbk(实际 {r[4]})") + check(r[5] == "20000", f"index glyph_count 列 = 20000(实际 {r[5]})") + r = find(p_common) + check(r is not None and r[4] == "common", "index 三级 common 条目 tier=common") + r = find(p_full) + check(r is not None and r[4] == "full", "index 三级 full 条目 tier=full") + r = find("MeowOS/fonts/dual/regular/font.png") + check(r is not None and r[4] == "common", "index 两级 dual 条目 tier=common(读 charset)") + r = find("MeowOS/fonts/default/regular/font.png") + check(r is not None and r[0] == "default", "index 默认行 family=default") + # 三级 label(stdout 含档位标记) + check("思源黑体 CN Regular(gbk≈20k)" in out, "label 含 `思源黑体 CN Regular(gbk≈20k)`") + check("思源黑体 CN Regular(common≈4k)" in out, "label 含 `思源黑体 CN Regular(common≈4k)`") + + print(f"\n{'RESULT: PASS' if fails == 0 else 'RESULT: FAIL'} ({fails} failures)") + return 1 if fails else 0 + + +if __name__ == "__main__": + sys.exit(main())