/* * test_multi_font.c — MeowOS 多字体功能宿主导出单测。 * * 验证两块(MEOWOS_SPEC_REF §5.7 多字体 / §5.2 分类路由): * (1) meow_font_class_of() 字符分类器:各代表 codepoint 归类正确。 * (2) 逐字符路由:meow_font_drawtext/width_text/line_height/has_glyph 在 multi_font * 开启时按类别用对应槽字库;关闭(默认)时与单字体一致;family 去重;≤4 套上限截断。 * * 为避免依赖仓库真实字体(不同机器/来源不确定),此处用「假 atlas/stub」:离线合成两张 * 微型位图字体(合法 PNG 灰度8bit + MFNI v2 idx),各含 1-3 个字形、不同 advance,故 * 路由结果可精确断言。 * - lat(slot0/全局):'A' adv=10、'0' adv=10、' '(0x20) adv=10 —— 只有拉丁/数字/空格。 * - cjk(slot1):'中'(0x4E2D) adv=30 —— 只有 CJK。 * * 用法:宿主 gcc -I. test_multi_font.c meow_font.c -lz -o test_multi_font && ./test_multi_font * 退出码 0=全过;非 0=某断言失败(打印 FAIL)。 */ #include "meow_font.h" #include #include #include #include #include /* ---------------- 微型合成字库(假 atlas/stub) ---------------- */ static void wr_be32(unsigned char **p, uint32_t v) { (*p)[0]=v>>24;(*p)[1]=v>>16;(*p)[2]=v>>8;(*p)[3]=v; *p+=4; } static void wr_le16(unsigned char **p, uint16_t v) { (*p)[0]=v&0xFF;(*p)[1]=v>>8; *p+=2; } static void wr_le32(unsigned char **p, uint32_t v) { (*p)[0]=v&0xFF;(*p)[1]=(v>>8)&0xFF;(*p)[2]=(v>>16)&0xFF;(*p)[3]=(v>>24)&0xFF; *p+=4; } static void png_chunk2(unsigned char *dst, size_t *off, const char *type, const unsigned char *data, size_t dlen) { unsigned char *o = dst + *off; wr_be32(&o, (uint32_t)dlen); memcpy(o, type, 4); o += 4; if (dlen) { memcpy(o, data, dlen); o += dlen; } /* CRC = crc32(type) 续 crc32(data) */ uint32_t c = crc32(0L, (const Bytef *)type, 4); if (dlen) c = crc32(c, (const Bytef *)data, (uInt)dlen); wr_be32(&o, c); *off += 12 + dlen; } /* 生成一张合法 PNG:w x h、灰度8bit、非隔行、filter=0 全零像素。返回 malloc 缓冲。 */ static unsigned char *make_png(int w, int h, size_t *outlen) { const unsigned char sig[8] = {0x89,'P','N','G','\r','\n',0x1a,'\n'}; /* IHDR data (13B) */ unsigned char ihdr[13]; ihdr[0]=w>>24; ihdr[1]=w>>16; ihdr[2]=w>>8; ihdr[3]=w; ihdr[4]=h>>24; ihdr[5]=h>>16; ihdr[6]=h>>8; ihdr[7]=h; ihdr[8]=8; /* bit depth 8 */ ihdr[9]=0; /* color type 0 = gray */ ihdr[10]=0; ihdr[11]=0; ihdr[12]=0; /* compression/filter/interlace */ int rowbytes = w; /* depth8 gray */ size_t rawlen = (size_t)(rowbytes + 1) * h; unsigned char *raw = (unsigned char *)calloc(1, rawlen); /* 每行 filter 字节=0 + 像素=0 */ if (!raw) return NULL; for (int y = 0; y < h; y++) raw[y * (rowbytes + 1)] = 0; uLongf destlen = compressBound((uLong)rawlen); unsigned char *idat = (unsigned char *)malloc(destlen); if (compress2(idat, &destlen, raw, (uLong)rawlen, Z_DEFAULT_COMPRESSION) != Z_OK) { free(raw); free(idat); return NULL; } free(raw); size_t total = 8 + 12 + 13 + 12 + destlen + 12; unsigned char *out = (unsigned char *)malloc(total); if (!out) { free(idat); return NULL; } size_t off = 0; memcpy(out + off, sig, 8); off += 8; png_chunk2(out, &off, "IHDR", ihdr, 13); png_chunk2(out, &off, "IDAT", idat, destlen); png_chunk2(out, &off, "IEND", NULL, 0); free(idat); *outlen = off; return out; } /* 生成 MFNI v2 idx:1 个面(px 字号),planes 覆盖全 PNG,含 nglyphs 个字(cp/adv 升序)。 */ static unsigned char *make_idx(int px, int ascent, int descent, int plane_w, int plane_h, const uint32_t *cps, const int *advs, int nglyphs, size_t *outlen) { size_t len = 16 + 20 + (size_t)nglyphs * 16; unsigned char *out = (unsigned char *)calloc(1, len); unsigned char *o = out; memcpy(o, "MFNI", 4); o += 4; wr_le16(&o, 2); /* version=2 */ *o++ = 8; /* depth */ *o++ = 1; /* nfaces */ wr_le32(&o, (uint32_t)plane_w); wr_le32(&o, (uint32_t)plane_h); /* face */ wr_le16(&o, (uint16_t)px); /* px */ wr_le16(&o, 0); /* plane_x */ wr_le16(&o, 0); /* plane_y */ wr_le16(&o, (uint16_t)plane_w); /* plane_w */ wr_le16(&o, (uint16_t)plane_h); /* plane_h */ wr_le16(&o, (uint16_t)ascent); /* ascent (s16) */ wr_le16(&o, (uint16_t)descent); /* descent (s16) */ wr_le16(&o, (uint16_t)nglyphs); /* glyph_count */ wr_le32(&o, 16 + 20); /* goff */ /* glyphs 升序 */ for (int i = 0; i < nglyphs; i++) { wr_le32(&o, cps[i]); /* cp */ wr_le16(&o, 0); /* x */ wr_le16(&o, 0); /* y */ wr_le16(&o, (uint16_t)px); /* w */ wr_le16(&o, (uint16_t)px); /* h */ wr_le16(&o, (uint16_t)advs[i]); /* advance */ *o++ = 0; /* dx */ *o++ = 0; /* dy */ } *outlen = len; return out; } /* 写一套字库 .png/.idx,返回 0 成功。 */ static int write_font(const char *base, int px, int ascent, int descent, const uint32_t *cps, const int *advs, int nglyphs) { char pngp[512], idxp[512]; snprintf(pngp, sizeof pngp, "%s.png", base); snprintf(idxp, sizeof idxp, "%s.idx", base); size_t plen = 0, ilen = 0; unsigned char *png = make_png(64, 64, &plen); if (!png) { fprintf(stderr, "make_png failed\n"); return -1; } unsigned char *idx = make_idx(px, ascent, descent, 64, 64, cps, advs, nglyphs, &ilen); if (!idx) { free(png); return -1; } FILE *f = fopen(pngp, "wb"); if (!f) { free(png); free(idx); return -1; } fwrite(png, 1, plen, f); fclose(f); f = fopen(idxp, "wb"); if (!f) { free(png); free(idx); return -1; } fwrite(idx, 1, ilen, f); fclose(f); free(png); free(idx); return 0; } /* ---------------- 断言 ---------------- */ static int g_fail = 0; #define CHECK(cond, msg) do { \ if (!(cond)) { g_fail++; fprintf(stderr, "FAIL: %s (line %d)\n", msg, __LINE__); } \ else printf("ok : %s\n", msg); \ } while (0) /* ---------------- 主测 ---------------- */ int main(void) { /* 造两套 stub 字库 */ const uint32_t lat_cps[] = {0x20, 0x30, 0x41}; /* 空格 '0' 'A' */ const int lat_adv[] = {10, 10, 10}; const uint32_t cjk_cps[] = {0x4E2D}; /* '中' */ const int cjk_adv[] = {30}; if (write_font("/tmp/mf_lat", 20, 20, 6, lat_cps, lat_adv, 3) != 0) return 2; if (write_font("/tmp/mf_cjk", 20, 25, 8, cjk_cps, cjk_adv, 1) != 0) return 2; /* ---- (1) 字符分类器 ---- */ printf("\n== 分类器 meow_font_class_of ==\n"); CHECK(meow_font_class_of('A') == MEOW_FCLS_LATIN, "A -> LATIN"); CHECK(meow_font_class_of('z') == MEOW_FCLS_LATIN, "z -> LATIN"); CHECK(meow_font_class_of('5') == MEOW_FCLS_NUM, "5 -> NUM"); CHECK(meow_font_class_of(' ') == MEOW_FCLS_LATIN_SYM, "spc -> LATIN_SYM"); CHECK(meow_font_class_of(',') == MEOW_FCLS_LATIN_SYM, ", -> LATIN_SYM"); CHECK(meow_font_class_of('(') == MEOW_FCLS_LATIN_SYM, "( -> LATIN_SYM"); CHECK(meow_font_class_of('.') == MEOW_FCLS_LATIN_SYM, ". -> LATIN_SYM"); CHECK(meow_font_class_of(0x4E2D) == MEOW_FCLS_CJK, "U+4E2D -> CJK"); CHECK(meow_font_class_of(0x9FFF) == MEOW_FCLS_CJK, "U+9FFF -> CJK"); CHECK(meow_font_class_of(0x3400) == MEOW_FCLS_CJK, "U+3400(ExtA) -> CJK"); CHECK(meow_font_class_of(0x20000) == MEOW_FCLS_CJK, "U+20000(ExtB) -> CJK"); CHECK(meow_font_class_of(0x3001) == MEOW_FCLS_CJK_SYM, "U+3001(、) -> CJK_SYM"); CHECK(meow_font_class_of(0x3000) == MEOW_FCLS_CJK_SYM, "U+3000(全角空格) -> CJK_SYM"); CHECK(meow_font_class_of(0xFF0C) == MEOW_FCLS_CJK_SYM, "U+FF0C(,) -> CJK_SYM"); CHECK(meow_font_class_of(0xFF10) == MEOW_FCLS_CJK_SYM, "U+FF10(0) -> CJK_SYM"); CHECK(meow_font_class_of(0x2026) == MEOW_FCLS_CJK_SYM, "U+2026(…) -> CJK_SYM"); CHECK(meow_font_class_of(0x00B7) == MEOW_FCLS_CJK_SYM, "U+00B7(·) -> CJK_SYM"); CHECK(meow_font_class_of(0x3042) == MEOW_FCLS_OTHER, "U+3042(あ) -> OTHER"); CHECK(meow_font_class_of(0xAC00) == MEOW_FCLS_OTHER, "U+AC00(가) -> OTHER"); CHECK(meow_font_class_of(0xE9) == MEOW_FCLS_OTHER, "U+00E9(é) -> OTHER"); /* ---- (2) 多字体路由 ---- */ printf("\n== 单字体基线(multi off,全局=lat)==\n"); int r = meow_font_load("/tmp/mf_lat.png", "/tmp/mf_lat.idx"); CHECK(r == 0, "load global lat"); CHECK(meow_font_has_size(20), "lat has size 20"); CHECK(meow_font_width_text("A", 20) == 10, "w(A)=10 (lat)"); CHECK(meow_font_width_text("0", 20) == 10, "w(0)=10 (lat)"); /* lat 无 '中' → 缺字回退空格 adv=10 */ CHECK(meow_font_width_text("中", 20) == 10, "w(中)=10 fallback (lat no cjk)"); CHECK(meow_font_width_text("A中", 20) == 20, "w(A中)=20 (single font)"); CHECK(meow_font_has_glyph(0x4E2D) == 0, "has_glyph(中)=0 (lat lacks)"); CHECK(meow_font_has_glyph('A') == 1, "has_glyph(A)=1 (lat has)"); CHECK(meow_font_line_height(20) == 26, "lh(20)=26 (lat ascent20+desc6)"); printf("\n== 开启 multi_font(class_cjk -> cjk 槽)==\n"); meow_font_class_cfg cfg[MEOW_FCLS_COUNT]; memset(cfg, 0, sizeof cfg); cfg[MEOW_FCLS_CJK].png = "/tmp/mf_cjk.png"; cfg[MEOW_FCLS_CJK].idx = "/tmp/mf_cjk.idx"; char gpng[64], gidx[64]; snprintf(gpng, sizeof gpng, "/tmp/mf_lat.png"); snprintf(gidx, sizeof gidx, "/tmp/mf_lat.idx"); int distinct = 0, truncd = 0; r = meow_font_config_multifont(1, gpng, gidx, cfg, &distinct, &truncd); CHECK(r == 0, "config_multifont ok"); CHECK(distinct == 2, "distinct=2 (lat + cjk)"); CHECK(truncd == 0, "not truncated"); CHECK(meow_font_width_text("A", 20) == 10, "w(A)=10 (routed -> global/lat, latin class)"); CHECK(meow_font_width_text("0", 20) == 10, "w(0)=10 (num class -> global)"); CHECK(meow_font_width_text("中", 20) == 30, "w(中)=30 (routed -> cjk slot)"); CHECK(meow_font_width_text("A中", 20) == 40, "w(A中)=40 (per-char routing)"); CHECK(meow_font_has_glyph(0x4E2D) == 1, "has_glyph(中)=1 (routed -> cjk)"); CHECK(meow_font_line_height(20) == 33, "lh(20)=33 (max(lat26, cjk33))"); /* 关闭 → 立即回到单字体 */ meow_font_config_multifont(0, gpng, gidx, cfg, &distinct, &truncd); CHECK(meow_font_width_text("A中", 20) == 20, "off -> w(A中)=20 again"); CHECK(meow_font_has_glyph(0x4E2D) == 0, "off -> has_glyph(中)=0 again"); /* drawtext smoke:吃一个混合串不崩(当前 cfg 仍为合法 cjk 配置) */ printf("\n== drawtext smoke(不影响断言,仅验证不崩)==\n"); meow_font_set_fb(64, 64, 128); static uint16_t buf[64 * 64]; memset(buf, 0, sizeof buf); meow_font_config_multifont(1, gpng, gidx, cfg, &distinct, &truncd); meow_font_drawtext(buf, 0, 0, "A中0,", 0xFFFF, 20); printf("drew 'A中0,' ok\n"); printf("\n== family 去重 / 上限 ==\n"); /* latin 类别也指到 cjk → 应去重,distinct 仍为 2 */ memset(cfg, 0, sizeof cfg); cfg[MEOW_FCLS_CJK].png = "/tmp/mf_cjk.png"; cfg[MEOW_FCLS_CJK].idx = "/tmp/mf_cjk.idx"; cfg[MEOW_FCLS_LATIN].png = "/tmp/mf_cjk.png"; cfg[MEOW_FCLS_LATIN].idx = "/tmp/mf_cjk.idx"; meow_font_config_multifont(1, gpng, gidx, cfg, &distinct, &truncd); CHECK(distinct == 2, "dedupe: latin==cjk path -> distinct=2"); CHECK(truncd == 0, "dedupe: not truncated"); /* 超上限:6 类各指向一个不同、可加载的字体(除全局)→ 第 5 个起截断。 * MEOW_MAX_SLOTS=5(global + ≤4 类别),故最多 5 套不同字库。 */ { /* 生成 5 个额外不同字库(不同 advance,保证去重键不同) */ const uint32_t fcps[] = {0x4E2D}; int fav = 100; char paths[8][64], idxs[8][64]; for (int i = 2; i < 7; i++) { /* 生成 /tmp/mf_trunc2..6 */ snprintf(paths[i], sizeof paths[i], "/tmp/mf_trunc%d", i); if (write_font(paths[i], 20, 25, 8, fcps, &fav, 1) != 0) return 2; snprintf(paths[i], sizeof paths[i], "/tmp/mf_trunc%d.png", i); snprintf(idxs[i], sizeof idxs[i], "/tmp/mf_trunc%d.idx", i); fav += 50; } char pp[6][64]; /* 6 类全部指向不同非全局字库 */ snprintf(pp[MEOW_FCLS_CJK], sizeof pp[0], "/tmp/mf_trunc2.png"); snprintf(pp[MEOW_FCLS_CJK_SYM], sizeof pp[0], "/tmp/mf_trunc3.png"); snprintf(pp[MEOW_FCLS_NUM], sizeof pp[0], "/tmp/mf_trunc4.png"); snprintf(pp[MEOW_FCLS_LATIN], sizeof pp[0], "/tmp/mf_trunc5.png"); snprintf(pp[MEOW_FCLS_LATIN_SYM], sizeof pp[0], "/tmp/mf_trunc6.png"); snprintf(pp[MEOW_FCLS_OTHER], sizeof pp[0], "/tmp/mf_trunc2.png"); /* 与 CJK 同 → 去重,不新增 */ for (int i = 0; i < 6; i++) { cfg[i].png = pp[i]; cfg[i].idx = NULL; } /* 修 idx:write_font 已生对应 .idx,需给出 */ char ppi[6][64]; const char *maps[6] = {"/tmp/mf_trunc2", "/tmp/mf_trunc3", "/tmp/mf_trunc4", "/tmp/mf_trunc5", "/tmp/mf_trunc6", "/tmp/mf_trunc2"}; for (int i = 0; i < 6; i++) { snprintf(ppi[i], sizeof ppi[i], "%s.idx", maps[i]); cfg[i].idx = ppi[i]; } meow_font_config_multifont(1, gpng, gidx, cfg, &distinct, &truncd); CHECK(truncd == 1, "6 distinct -> truncated=1"); CHECK(distinct == MEOW_MAX_SLOTS, "distinct capped at MEOW_MAX_SLOTS"); } /* ---- (3) 字体档位分层:meow_font_reload_tier(大字库按档装载/切换)---- * 用同两套 stub(lat=common 模拟,cjk=gbk 模拟)+ 一个不会释放的 cjk 大面,验证 * tier>0 装载大字库、切换 unload 旧/load 新、失败回退。 */ printf("\n== 字体档位分层 meow_font_reload_tier ==\n"); /* 档位 0=common/1=gbk/2=full(与 meow_ui.h MEOW_FONT_TIER_* 一致;本测试只链 meow_font.h) */ r = meow_font_reload_tier(0, "/tmp/mf_lat.png", "/tmp/mf_lat.idx"); CHECK(r == 0, "reload_tier(common, lat) ok"); CHECK(meow_font_has_glyph('A') == 1, "reload common -> has_glyph(A)=1"); CHECK(meow_font_has_glyph(0x4E2D) == 0, "reload common -> has_glyph(中)=0 (lat no cjk)"); CHECK(meow_font_width_text("A", 20) == 10, "reload common -> w(A)=10"); /* 切到大字库(cjk 槽):应 unload 旧 lat 再 load cjk → '中' 可用、'A' 缺。 */ r = meow_font_reload_tier(1, "/tmp/mf_cjk.png", "/tmp/mf_cjk.idx"); CHECK(r == 0, "reload_tier(gbk, cjk) ok"); CHECK(meow_font_has_glyph(0x4E2D) == 1, "reload gbk -> has_glyph(中)=1"); CHECK(meow_font_has_glyph('A') == 0, "reload gbk -> has_glyph(A)=0 (cjk lacks lat)"); CHECK(meow_font_width_text("中", 20) == 30, "reload gbk -> w(中)=30 (cjk slot)"); CHECK(meow_font_line_height(20) == 33, "reload gbk -> lh(20)=33 (cjk ascent25+desc8)"); /* 切回 common:再 unload cjk / load lat → 'A' 回来。 */ r = meow_font_reload_tier(0, "/tmp/mf_lat.png", "/tmp/mf_lat.idx"); CHECK(r == 0, "reload_tier(common) again ok"); CHECK(meow_font_has_glyph('A') == 1, "reload back common -> has_glyph(A)=1"); CHECK(meow_font_has_glyph(0x4E2D) == 0, "reload back common -> has_glyph(中)=0"); CHECK(meow_font_width_text("A", 20) == 10, "reload back common -> w(A)=10"); /* Full 档(tier=2)同一套 cjk 作为大字库装载也应成功(档仅记录,装载同路径)。 */ r = meow_font_reload_tier(2, "/tmp/mf_cjk.png", "/tmp/mf_cjk.idx"); CHECK(r == 0, "reload_tier(full, cjk) ok"); CHECK(meow_font_has_glyph(0x4E2D) == 1, "reload full -> has_glyph(中)=1"); /* 非法参数:NULL/空路径 → 负值(不崩),且字库已被上次 reload 释放(档位装载失败不驻留)。 */ r = meow_font_reload_tier(1, NULL, NULL); CHECK(r < 0, "reload_tier(NULL paths) -> negative"); CHECK(meow_font_has_glyph('A') == 0, "reload failed -> font unloaded (has_glyph=0)"); printf("\n%s (%d failures)\n", g_fail ? "RESULT: FAIL" : "RESULT: PASS", g_fail); return g_fail ? 1 : 0; }