feat(launcher): 字体扫描迁移三级目录 (agent-193) — <family>/<weight>/<tier>/ 支持(两级向后兼容); FONT_PRESETS_MAX 17→64; 设置页 family→weight 两级不变,同weight多tier按加载大字库开关选档(关=common优先/开=gbk优先); index 落盘含三级条目

launcher ab8e610f→d3cae88b; test_font_tier/test_font_weight PASS
已知缺口: SDK meow_font.c MEOW_FONT_INDEX_MAX=32 截断(>32套需同步提); S_NET_MAX=8 family 上限; default 字体仍两级
This commit is contained in:
MeowOS committed 2026-08-31 05:09:42 +08:00
1 parent 6603e6686c
commit 3fd4726198
5 files changed
+366 -54

No files matched your search

+104 -45
View File
@@ -2,17 +2,20 @@
/* 本文件由拆分工具从 meow_launcherd.c 按职责搬移;逻辑与原文 0-diff(仅 static 可见性按跨模块需求调整)。*/
#include "meow_launcher_internal.h"
/* M2.6 字重结构:卡内字体 = <卡根>/MeowOS/fonts/<family>/<weight>/(三件套)。
* 每个 preset = 一套 <family>/<weight>(如 misans/regular)。值 = 卡根相对路径
* MeowOS/fonts/<family>/<weight>/font.png;family/weight 单列供设置页「family→字重」子菜单
* 分组与 meow_font_effective_paths_tiered 按 family+weight 选库。 */
/* M2.6 字重结构 + M2.8 三级定案(FONT_SYSTEM):卡内字体 =
* <卡根>/MeowOS/fonts/<family>/<weight>/(两级,向后兼容)或
* <卡根>/MeowOS/fonts/<family>/<weight>/<tier>/(三级,tier=common/gbk/full)。
* 每个 preset = 一套 <family>/<weight>[/<tier>](如 misans/regular 或 source_han_sans_cn/regular/gbk)。
* 值 = 卡根相对路径 MeowOS/fonts/<family>/<weight>[/<tier>]/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/<family>/<weight>/ 里找 charset 匹配(gbk 请求接受
* gbk/full;full 请求接受 full)的那套,以其 font.png/idx 绝对路径返回(family = 一级目录名);
* - tier>0 → 在已扫描的 preset(两级或三级,值路径 = MeowOS/fonts/<family>/<weight>[/<tier>]/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/<family>/<weight> 套件是否合法:三件套齐全 → 解析 label+档位+字重。
/* 探测一套件是否合法:三件套齐全 → 解析 label+档位+字重。
* dirpath = <卡根>/MeowOS/fonts/<family>/<weight>(两级)或 .../<family>/<weight>/<tier>(三级)。
* tier_dir:三级时传该 tier 目录名(common/gbk/full,权威档位);两级传 NULL(档位读
* font.json charset,缺省 common)。
* 返回 1=进列表,0=跳过(缺组件/json 不可读)。sha 失败仅 WARN。
* 输出:label=显示名 `<name> <Weight>(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);
/* 显示名 = `<name> <Weight>(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)
}
/* 扫描 <card_root>/MeowOS/fonts/ 下两级 <family>/<weight>/,重建字体 preset 列表。
* 每个 (family,weight) 组合一套(如 misans/regular、misans/bold);family 级 'default' 由
* 索引 0(默认字体)单独处理,不进循环,避免重复。上限 FONT_PRESETS_MAX
* (1 默认 + ≤16 用户字体)。无卡/无目录时仅默认。
/* 把一个探测成功的套装登记为 preset i(值路径 = MeowOS/fonts/<family>/<weight>[/<tier>]/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;
}
/* 扫描 <card_root>/MeowOS/fonts/ 下字体,重建字体 preset 列表。
* 布局(FONT_SYSTEM 三级定案):
* - 两级(向后兼容):<family>/<weight>/font.png → 每 (family,weight) 一套,tier 读 font.json
* charset(缺省 common)。
* - 三级(定案):<family>/<weight>/<tier>/(tier=common/gbk/full,每 tier 一套三件套)→
* 每 (family,weight,tier) 一套,tier=目录名(权威)。
* - 同一 (family,weight) 下:若 weight 根有 font.png(两级)则按两级;否则扫其 <tier>/ 子目录。
* family 级 'default' 由索引 0(默认字体)单独处理,不进循环,避免重复。上限 FONT_PRESETS_MAX
* (1 默认 + 卡内各套,按 (family,weight,tier) 一套一 preset 计数)。无卡/无目录时仅默认。
*
* M2.58 释放语义(规格 §5.7 需求 4「扫→写 index→释放(或按需读 index)」结论):
* - 持久来源 = <card_root>/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) {
/* 两级(旧结构向后兼容):<family>/<weight>/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;
}
/* 三级定案:进入 <weight>/<tier>/ 逐套探测,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);
}