reader R1: os.meow.reader 书架+TXT 垂直链路骨架
- core: 编码检测(BOM→UTF-8严格→GB18030/Big5统计, Big5与GB重叠按GB18030兜底, §3.5风险注明) + GB18030双字节码表(uint16[126][190]=47880B, tools/gen_gb18030_table.py 生成, 生成物入仓) + 流式排版器(贪心断行+Kinsoku避头尾+分页+重排锚定, rd_meas_fn 测量回调替 FreeType, 宿主可测) + TXT 流式加载器(按段读, 不整文件入内存, \n 恒独立换行可安全分段) - engine: FreeType 封装(FT_New_Face 按需读文件, NO_HINTING/LIGHT 灰阶, LRU 512 key=码点+字号, blend565, rdft_advance 只测宽不栅格化) - app: 书架(扫 Books/ 识别 5 后缀, 最近读置顶, 空态提示) + 阅读页(W_CUSTOM 画布 3 栏翻页 +中央菜单占位(字号±/行距±/返回) +顶栏进度%/章节名) + 书桥(段表+flat 页索引, 渲染, 重排回锚) + 进度持久化(MeowData/os.meow.reader/progress/, path hash→seg/ch/fs/ls/ts) - 取舍: 正文字体 FreeType 直读 TF MeowOS/fonts/ TTF/OTF; 无字体回退 SDK 位图字库(meow_ui_text, 未打包字体(许可/体积), 全 glyph 兜底 R2); 页位图缓存当前+前后各1页(3×691KB), 翻页动画默认无 - 内存实测(宿主 --wrap malloc, 73页书): 书结构 130KB + 当前段刮擦~1.6MB(行/页容量按 n 偏高,R2 收紧) + 字形缓存~650KB + 3页位图 2.07MB ≈ 4.45MB 动态, 在 ≤8MB 阅读态预算内 - 测试: 宿主 core 58/58(enc/gb18030金标准/kinsoku/page/reflow/txt); qemu-mipsel ft_smoke PASS + book_smoke(12页/渲染有墨/翻页/重排回锚) PASS; reader 交叉 -Wall -Wextra 0 warning - 产物: reader(mipsel 静态1.2MB)+build_reader.sh(vendor FreeType 可复现); vendor/二进制 gitignore
This commit is contained in:
1 parent
33d3059038
commit
553a7d5d9e
26 files changed
+6155
No files matched your search
@@ -0,0 +1,86 @@
|
||||
/* test/book_smoke.c — os.meow.reader 书桥全链路 qemu-mipsel 冒烟。
|
||||
* 开一本 TXT(UTF-8,含章节)→ 探测编码 → 建段+页索引 → FreeType 渲染首页(校验非底像素)→
|
||||
* 翻页/锚定 → 改字号重排回锚 → 再渲染;全程不碰 SDK(book.c 只依赖 core+engine)。
|
||||
* 跑于 qemu-mipsel(静态)。用法:qemu-mipsel ./test/book_smoke_mipsel <font.otf>
|
||||
*/
|
||||
#include "book.h"
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
static int fails = 0;
|
||||
#define CHECK(c, m) do { if (c) { printf(" ok %s\n", m); } else { printf("FAIL %s\n", m); fails++; } } while (0)
|
||||
|
||||
static void write_txt(const char *path)
|
||||
{
|
||||
FILE *f = fopen(path, "wb");
|
||||
fprintf(f, "第一章 测试编码与排版\n");
|
||||
for (int i = 0; i < 60; i++) {
|
||||
fprintf(f, "第%d段:这是一段用于验证流式排版与 FreeType 渲染的中文正文内容。"
|
||||
"它包含汉字、标点,以及一些 English words 混合排版。"
|
||||
"断行、避头尾与分页行为都要正确。\n", i + 1);
|
||||
}
|
||||
fprintf(f, "\nChapter two: the quick brown fox jumps over the lazy dog.\n");
|
||||
fclose(f);
|
||||
}
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
const char *font = (argc > 1) ? argv[1] : "/home/Aska/Project/R3ProII/font/src/MiSans-Regular.otf";
|
||||
const char *txt = "/tmp/rdbook_smoke.txt";
|
||||
write_txt(txt);
|
||||
|
||||
rd_book *b = NULL;
|
||||
int rc = rd_book_open(&b, txt, font, 20, 130, 480, 720, 16, 20, 62);
|
||||
CHECK(rc == 0, "rd_book_open");
|
||||
if (rc != 0) { printf("open rc=%d\n", rc); return 1; }
|
||||
|
||||
int np = rd_book_pages(b);
|
||||
printf(" pages=%d basename=%s enc=%s\n", np, rd_book_basename(b), rd_book_encname(b));
|
||||
CHECK(np > 0, "pages>0");
|
||||
|
||||
/* 渲染首页:校验有非背景像素 */
|
||||
int W = 480, H = 720;
|
||||
uint16_t *buf = (uint16_t *)malloc((size_t)W * H * 2);
|
||||
uint16_t bg = 0xFFFF, fg = 0x0000;
|
||||
rc = rd_book_render(b, 0, buf, W * 2, bg, fg);
|
||||
CHECK(rc == 0, "render page0");
|
||||
int inked = 0;
|
||||
for (int i = 0; i < W * H; i++) if (buf[i] != bg) inked++;
|
||||
printf(" page0 inked=%d px\n", inked);
|
||||
CHECK(inked > 100, "page0 has ink");
|
||||
|
||||
/* 锚定当前页 */
|
||||
int seg = -1; int32_t ch = -1;
|
||||
rd_book_anchor(b, &seg, &ch);
|
||||
CHECK(seg >= 0 && ch >= 0, "anchor valid");
|
||||
|
||||
/* 翻页 */
|
||||
int cur = rd_book_current(b);
|
||||
int np_total = rd_book_pages(b);
|
||||
int nxt = rd_book_turn(b, 1);
|
||||
CHECK(np_total > 1 || nxt == 0, "pages>1 or single-page clamps");
|
||||
if (np_total > 1) CHECK(nxt == cur + 1, "turn fwd +1");
|
||||
/* 回退 */
|
||||
rd_book_turn(b, -1);
|
||||
CHECK(rd_book_current(b) == cur, "turn back to cur");
|
||||
|
||||
/* 改字号(20→24) 重排回锚到含 (seg,ch) 的页 */
|
||||
int np2 = rd_book_jump_to_anchor(b, seg, ch, 24, 130);
|
||||
CHECK(np2 >= 0 && np2 < rd_book_pages(b), "reflow lands in range");
|
||||
int seg2 = -1; int32_t ch2 = -1;
|
||||
rd_book_anchor(b, &seg2, &ch2);
|
||||
CHECK(seg2 == seg, "reflow keeps segment");
|
||||
/* 重排后渲染 */
|
||||
rc = rd_book_render(b, rd_book_current(b), buf, W * 2, bg, fg);
|
||||
CHECK(rc == 0, "render after reflow");
|
||||
|
||||
/* 行距调整再排 */
|
||||
int np3 = rd_book_set_size(b, 20, 150);
|
||||
CHECK(np3 >= 0 && np3 < rd_book_pages(b), "set_size reflow ok");
|
||||
|
||||
free(buf);
|
||||
rd_book_close(b);
|
||||
printf("book_smoke: %s\n", fails ? "FAIL" : "PASS");
|
||||
return fails ? 1 : 0;
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
/* test/mem_account.c — 书桥动态内存记账(宿主,--wrap=malloc/calloc/realloc/free)。
|
||||
* 目的:实测「阅读态」动态堆(书桥 + FreeType 字形缓存 + 段刮擦缓冲),给 R1 内存账。
|
||||
* 用一本 ~500KB 中文书:open(索引构建)→ render 首页/中间页 → 报告 current/peak。
|
||||
* 页位图(阅读 UI 持有 3×691KB)不在本 harness 内,报告里另加。
|
||||
* 用法:./test/mem_account_host <font.otf>
|
||||
*/
|
||||
#include "book.h"
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
/* --- malloc 包装跟踪 --- */
|
||||
static size_t g_cur = 0, g_peak = 0;
|
||||
void *__real_malloc(size_t); void *__real_calloc(size_t,size_t);
|
||||
void *__real_realloc(void*,size_t); void __real_free(void*);
|
||||
static size_t alloc_size(const void *p);
|
||||
void *__wrap_malloc(size_t n)
|
||||
{
|
||||
void *p = __real_malloc(n); size_t sz = alloc_size(p);
|
||||
g_cur += sz; if (g_cur > g_peak) g_peak = g_cur; return p;
|
||||
}
|
||||
void *__wrap_calloc(size_t n, size_t sz)
|
||||
{
|
||||
void *p = __real_calloc(n, sz); size_t s = alloc_size(p);
|
||||
g_cur += s; if (g_cur > g_peak) g_peak = g_cur; return p;
|
||||
}
|
||||
void *__wrap_realloc(void *old, size_t n)
|
||||
{
|
||||
size_t os = old ? alloc_size(old) : 0;
|
||||
void *p = __real_realloc(old, n); size_t ns = alloc_size(p);
|
||||
g_cur = g_cur - os + ns; if (g_cur > g_peak) g_peak = g_cur; return p;
|
||||
}
|
||||
void __wrap_free(void *p)
|
||||
{
|
||||
if (p) { size_t s = alloc_size(p); g_cur -= s; }
|
||||
__real_free(p);
|
||||
}
|
||||
/* 用 malloc_usable_size 取实际块(glibc) */
|
||||
static size_t alloc_size(const void *p)
|
||||
{
|
||||
if (!p) return 0;
|
||||
/* 免 glibc 头:直接用 __builtin 不可靠;用 malloc_usable_size 需声明。 */
|
||||
extern size_t malloc_usable_size(void *);
|
||||
return malloc_usable_size((void*)p);
|
||||
}
|
||||
|
||||
static void write_book(const char *path, int chapters)
|
||||
{
|
||||
FILE *f = fopen(path, "wb");
|
||||
for (int c = 0; c < chapters; c++) {
|
||||
fprintf(f, "第%d章 测试章节标题用于排版验证\n", c + 1);
|
||||
for (int p = 0; p < 12; p++)
|
||||
fprintf(f, "第%d章第%d节:这是一段用于内存记账的中文正文,验证流式排版、FreeType 光栅与 "
|
||||
"页位图缓存的内存占用是否在预算内。English mixed in too.\n", c + 1, p + 1);
|
||||
}
|
||||
fclose(f);
|
||||
}
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
const char *font = (argc > 1) ? argv[1] : "/home/Aska/Project/R3ProII/font/src/MiSans-Regular.otf";
|
||||
const char *txt = "/tmp/rdbook_mem.txt";
|
||||
write_book(txt, 40); /* ~40 章 ≈ 几百 KB */
|
||||
|
||||
size_t before_open = g_peak;
|
||||
rd_book *b = NULL;
|
||||
int rc = rd_book_open(&b, txt, font, 20, 130, 480, 720, 16, 20, 62);
|
||||
if (rc != 0) { printf("open failed %d\n", rc); return 1; }
|
||||
size_t after_open = g_cur;
|
||||
size_t peak_open = g_peak - before_open;
|
||||
int np = rd_book_pages(b);
|
||||
|
||||
int W = 480, H = 720;
|
||||
uint16_t *buf = (uint16_t *)malloc((size_t)W * H * 2);
|
||||
size_t peak0 = g_peak;
|
||||
rd_book_render(b, 0, buf, W * 2, 0xFFFF, 0x0000);
|
||||
rd_book_render(b, np / 2, buf, W * 2, 0xFFFF, 0x0000);
|
||||
rd_book_render(b, np - 1, buf, W * 2, 0xFFFF, 0x0000);
|
||||
size_t peak_render = g_peak - peak0;
|
||||
size_t after_render = g_cur;
|
||||
|
||||
printf("pages=%d\n", np);
|
||||
printf("open : cur=%zuB peak=%zuB (%.1f KiB / %.1f KiB)\n",
|
||||
after_open, peak_open, after_open / 1024.0, peak_open / 1024.0);
|
||||
printf("render: cur=%zuB peak=%zuB (+%zuB) (%.1f KiB / %.1f KiB)\n",
|
||||
after_render, peak_render, peak_render, after_render / 1024.0, peak_render / 1024.0);
|
||||
printf("pagebuf(3x480x720x2=%zuB=%.2fMiB) in UI layer, not here\n",
|
||||
(size_t)W * H * 2 * 3, (double)W * H * 2 * 3 / (1024.0 * 1024.0));
|
||||
|
||||
free(buf);
|
||||
rd_book_close(b);
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
/* test/rdft_smoke.c — os.meow.reader FreeType 封装 qemu-mipsel 冒烟。
|
||||
* 在 mipsel 上初始化 rdft → 载入思源黑体/MiSans OTF → 定字号 → 取 CJK/拉丁字形度量 →
|
||||
* blend565 到 RGB565 缓冲 → 校验像素变化 → LRU 缓存 512 上限。可跑于 qemu-mipsel(静态)。
|
||||
*
|
||||
* 用法:qemu-mipsel ./test/rdft_smoke <font.otf> (缺省用仓库 font/src/MiSans-Regular.otf)
|
||||
*/
|
||||
#include "rdft.h"
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
static int fails = 0;
|
||||
#define CHECK(c, m) do { if (c) { printf(" ok %s\n", m); } else { printf("FAIL %s\n", m); fails++; } } while (0)
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
const char *font = (argc > 1) ? argv[1] : "/home/Aska/Project/R3ProII/font/src/MiSans-Regular.otf";
|
||||
rdft *r = NULL;
|
||||
CHECK(rdft_init(&r) == 0, "rdft_init");
|
||||
CHECK(r != NULL, "rdft non-null");
|
||||
|
||||
CHECK(rdft_load_font(r, font) == 0, "rdft_load_font");
|
||||
CHECK(rdft_ascender(r) > 0, "ascender>0");
|
||||
printf(" family=%s ascender=%d descender=%d\n", rdft_family(r), rdft_ascender(r), rdft_descender(r));
|
||||
|
||||
rdft_set_px(r, 20);
|
||||
/* 字形度量:你(U+4F60)/好(U+597D)/A/空格/软连字符 */
|
||||
const uint32_t cps[] = { 0x4F60, 0x597D, 'A', ' ', 0x00AD, 0x3002 };
|
||||
for (int i = 0; i < 6; i++) {
|
||||
int adv = 0, bw = 0, bh = 0, left = 0, top = 0;
|
||||
const uint8_t *bmp = NULL;
|
||||
CHECK(rdft_glyph(r, cps[i], &adv, &bmp, &bw, &bh, &left, &top) == 0, "glyph");
|
||||
printf(" cp=U+%04X adv=%d bmp=%dx%d left=%d top=%d\n", cps[i], adv, bw, bh, left, top);
|
||||
if (cps[i] == 'A') CHECK(adv > 5, "A advance>5");
|
||||
if (cps[i] == 0x4F60) CHECK(adv >= 16 && adv <= 22, "CJK adv ~20");
|
||||
}
|
||||
|
||||
/* blend565:把「你」字画进一个 RGB565 缓冲(80×80,白底黑字),校验有像素变黑 */
|
||||
{
|
||||
int W = 80, H = 80;
|
||||
uint16_t *fb = (uint16_t*)malloc((size_t)W * H * 2);
|
||||
for (int i = 0; i < W * H; i++) fb[i] = 0xFFFF; /* 白 */
|
||||
int adv = 0, bw = 0, bh = 0, left = 0, top = 0;
|
||||
const uint8_t *bmp = NULL;
|
||||
rdft_glyph(r, 0x4F60, &adv, &bmp, &bw, &bh, &left, &top);
|
||||
int baseline = 30;
|
||||
rdft_blend565(fb, W * 2, W, H, 10, baseline, bmp, bw, bh, left, top, 0x0000, 0xFFFF);
|
||||
int dark = 0;
|
||||
for (int i = 0; i < W * H; i++) if (fb[i] != 0xFFFF) dark++;
|
||||
CHECK(dark > 10, "blend produced dark pixels");
|
||||
free(fb);
|
||||
}
|
||||
|
||||
/* LRU 缓存上限:请求 600 个不同码点 → used 应 ≤ 512 */
|
||||
{
|
||||
rdft_cache_set_slots(r, 512);
|
||||
for (uint32_t cp = 0x4E00; cp < 0x4E00 + 600; cp++) {
|
||||
int adv = 0, bw = 0, bh = 0, left = 0, top = 0;
|
||||
const uint8_t *bmp = NULL;
|
||||
rdft_glyph(r, cp, &adv, &bmp, &bw, &bh, &left, &top);
|
||||
}
|
||||
int used = rdft_cache_used(r);
|
||||
printf(" lru used=%d slots=%d\n", used, rdft_cache_slots(r));
|
||||
CHECK(used <= 512, "lru used <= 512");
|
||||
CHECK(used >= 1, "lru used >= 1");
|
||||
}
|
||||
|
||||
rdft_free(r);
|
||||
printf("rdft_smoke: %s\n", fails ? "FAIL" : "PASS");
|
||||
return fails ? 1 : 0;
|
||||
}
|
||||
@@ -0,0 +1,275 @@
|
||||
/* test_reader_core.c — os.meow.reader core 宿主单测(纯 C,无 SDK/Freetype)。
|
||||
*
|
||||
* 覆盖:
|
||||
* T1 编码检测(UTF-8/GBK/GB18030/Big5/ASCII 5 样本)
|
||||
* T2 GB18030(GBK)→UTF-8 金标准比对 vs python gb18030 codec
|
||||
* T3 GB18030 4 字节扩展段 → U+FFFD
|
||||
* T4 UTF-8 严格校验(合法/非法/overlong/代理)
|
||||
* T5 rd_utf8_to_cp / rd_cp_to_utf8 往返
|
||||
* T6 贪心断行:CJK+英文+标点混排,行宽不超,断点合法(左括号不居行尾/右标点不居行首)
|
||||
* T7 Kinsoku 避头尾样例(「。」不居行首、「(」不居行尾)
|
||||
* T8 页索引完整性(每页 first_char 单调、覆盖全文、页行数>0)
|
||||
* T9 重排锚定:改字号后 anchor 仍能定位到包含它的页
|
||||
* T10 内置测量近似 vs 定长策略
|
||||
*/
|
||||
#include "rdenc.h"
|
||||
#include "rdlayout.h"
|
||||
#include "rdtxt.h"
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
static int g_pass = 0, g_fail = 0;
|
||||
#define CHECK(cond, msg) do { if (cond) { g_pass++; } else { g_fail++; printf("FAIL %s:%d %s\n", __FILE__, __LINE__, msg); } } while (0)
|
||||
|
||||
/* ---- 测量桩:CJK=fs,ASCII=fs/2,空格=fs/3(匹配 engine 内置近似) ---- */
|
||||
static int meas_mock(uint32_t cp, int fs, int *adv, void *ctx)
|
||||
{
|
||||
(void)ctx;
|
||||
if (cp == ' ' || cp == '\t') { *adv = fs * 2 / 3; return 0; }
|
||||
if (cp == '\n') { *adv = 0; return 0; }
|
||||
if ((cp >= 0x2E80 && cp <= 0x2EFF) || (cp >= 0x3040 && cp <= 0x30FF) ||
|
||||
(cp >= 0x3400 && cp <= 0x4DBF) || (cp >= 0x4E00 && cp <= 0x9FFF) ||
|
||||
(cp >= 0xF900 && cp <= 0xFAFF) || (cp >= 0xFF00 && cp <= 0xFFEF))
|
||||
*adv = fs;
|
||||
else *adv = fs / 2 < 1 ? 1 : fs / 2;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void t1(void)
|
||||
{
|
||||
/* UTF-8 中文 */
|
||||
const uint8_t u8[] = "\xE4\xBD\xA0\xE5\xA5\xBD"; /* 你好 */
|
||||
/* GBK "你好" */
|
||||
const uint8_t gbk[] = { 0xC4, 0xE3, 0xBA, 0xC3 };
|
||||
/* GB18030 "你好"(同 GBK)+ 扩展 */
|
||||
const uint8_t gb18030[] = { 0xC4, 0xE3, 0xBA, 0xC3, 0x90, 0x30, 0x81, 0x30 };
|
||||
/* Big5 与 GB18030 双字节区重叠(见 rdenc.c 注释):R1 双字节一律按 GB18030 兜底;
|
||||
* 故 Big5 样本判 GB18030(v1 不转 Big5,按 GB18030 试)——如实反映检测局限。 */
|
||||
const uint8_t big5[] = { 0xA7, 0xDA, 0xA6, 0x6E };
|
||||
const uint8_t ascii[] = "Hello, world 123";
|
||||
|
||||
CHECK(rd_enc_detect(u8, sizeof u8, NULL) == RD_ENC_UTF8, "detect utf8");
|
||||
CHECK(rd_enc_detect(gbk, sizeof gbk, NULL) == RD_ENC_GB18030, "detect gbk(→gb18030)");
|
||||
CHECK(rd_enc_detect(gb18030, sizeof gb18030, NULL) == RD_ENC_GB18030, "detect gb18030");
|
||||
CHECK(rd_enc_detect(big5, sizeof big5, NULL) == RD_ENC_GB18030, "detect big5→gb18030 (overlap limit)");
|
||||
CHECK(rd_enc_detect(ascii, sizeof ascii, NULL) == RD_ENC_ASCII, "detect ascii");
|
||||
|
||||
/* BOM */
|
||||
const uint8_t bom_utf8[] = { 0xEF, 0xBB, 0xBF, 'a' };
|
||||
const uint8_t bom_le[] = { 0xFF, 0xFE, 'a', 0 };
|
||||
const uint8_t bom_be[] = { 0xFE, 0xFF, 0, 'a' };
|
||||
CHECK(rd_enc_detect(bom_utf8, sizeof bom_utf8, NULL) == RD_ENC_UTF8, "bom utf8");
|
||||
CHECK(rd_enc_detect(bom_le, sizeof bom_le, NULL) == RD_ENC_UTF16LE, "bom utf16le");
|
||||
CHECK(rd_enc_detect(bom_be, sizeof bom_be, NULL) == RD_ENC_UTF16BE, "bom utf16be");
|
||||
}
|
||||
|
||||
static void t2(void)
|
||||
{
|
||||
/* 金标准(python gb18030 codec 生成;见 test 记录) */
|
||||
struct { const uint8_t *in, *expect; int inlen, explen; } cases[] = {
|
||||
{ (const uint8_t*)"\xC4\xE3\xBA\xC3\xCA\xC0\xBD\xE7\xA3\xAC\xD5\xE2\xCA\xC7\xD2\xBB\xB8\xF6\xB2\xE2\xCA\xD4\xA1\xA3",
|
||||
(const uint8_t*)"\xE4\xBD\xA0\xE5\xA5\xBD\xE4\xB8\x96\xE7\x95\x8C\xEF\xBC\x8C\xE8\xBF\x99\xE6\x98\xAF\xE4\xB8\x80\xE4\xB8\xAA\xE6\xB5\x8B\xE8\xAF\x95\xE3\x80\x82",
|
||||
(int)sizeof "\xC4\xE3\xBA\xC3\xCA\xC0\xBD\xE7\xA3\xAC\xD5\xE2\xCA\xC7\xD2\xBB\xB8\xF6\xB2\xE2\xCA\xD4\xA1\xA3" - 1,
|
||||
(int)sizeof "\xE4\xBD\xA0\xE5\xA5\xBD\xE4\xB8\x96\xE7\x95\x8C\xEF\xBC\x8C\xE8\xBF\x99\xE6\x98\xAF\xE4\xB8\x80\xE4\xB8\xAA\xE6\xB5\x8B\xE8\xAF\x95\xE3\x80\x82" - 1 },
|
||||
{ (const uint8_t*)"ABCD EF\xBF\xD5\xB8\xF1 \xD6\xD0\xCE\xC4 English \xBB\xEC\xBA\xCF\xA3\xAC\xB1\xEA\xB5\xE3\xA3\xA1",
|
||||
(const uint8_t*)"ABCD EF\xE7\xA9\xBA\xE6\xA0\xBC \xE4\xB8\xAD\xE6\x96\x87 English \xE6\xB7\xB7\xE5\x90\x88\xEF\xBC\x8C\xE6\xA0\x87\xE7\x82\xB9\xEF\xBC\x81",
|
||||
(int)sizeof "ABCD EF\xBF\xD5\xB8\xF1 \xD6\xD0\xCE\xC4 English \xBB\xEC\xBA\xCF\xA3\xAC\xB1\xEA\xB5\xE3\xA3\xA1" - 1,
|
||||
(int)sizeof "ABCD EF\xE7\xA9\xBA\xE6\xA0\xBC \xE4\xB8\xAD\xE6\x96\x87 English \xE6\xB7\xB7\xE5\x90\x88\xEF\xBC\x8C\xE6\xA0\x87\xE7\x82\xB9\xEF\xBC\x81" - 1 },
|
||||
{ (const uint8_t*)"\xB5\xDA\xD2\xBB\xD0\xD0\x0A\xB5\xDA\xB6\xFE\xD0\xD0\x09\xD6\xC6\xB1\xED\xB7\xFB\xA1\xA3",
|
||||
(const uint8_t*)"\xE7\xAC\xAC\xE4\xB8\x80\xE8\xA1\x8C\x0A\xE7\xAC\xAC\xE4\xBA\x8C\xE8\xA1\x8C\x09\xE5\x88\xB6\xE8\xA1\xA8\xE7\xAC\xA6\xE3\x80\x82",
|
||||
(int)sizeof "\xB5\xDA\xD2\xBB\xD0\xD0\x0A\xB5\xDA\xB6\xFE\xD0\xD0\x09\xD6\xC6\xB1\xED\xB7\xFB\xA1\xA3" - 1,
|
||||
(int)sizeof "\xE7\xAC\xAC\xE4\xB8\x80\xE8\xA1\x8C\x0A\xE7\xAC\xAC\xE4\xBA\x8C\xE8\xA1\x8C\x09\xE5\x88\xB6\xE8\xA1\xA8\xE7\xAC\xA6\xE3\x80\x82" - 1 },
|
||||
};
|
||||
for (int i = 0; i < 3; i++) {
|
||||
uint8_t out[512]; size_t olen = 0;
|
||||
int rc = rd_gb18030_to_utf8(cases[i].in, cases[i].inlen, out, sizeof out, &olen);
|
||||
CHECK(rc == RD_ENC_OK, "gb18030 decode ok");
|
||||
CHECK(olen == (size_t)cases[i].explen && memcmp(out, cases[i].expect, olen) == 0, "gb18030 golden match");
|
||||
}
|
||||
}
|
||||
|
||||
static void t3(void)
|
||||
{
|
||||
/* 4 字节扩展段 0x90 0x30 0x81 0x30 → 一个 U+FFFD (EF BF BD) */
|
||||
const uint8_t in[] = { 0x90, 0x30, 0x81, 0x30, 'X' };
|
||||
uint8_t out[32]; size_t olen = 0;
|
||||
int rc = rd_gb18030_to_utf8(in, sizeof in, out, sizeof out, &olen);
|
||||
CHECK(rc == RD_ENC_OK, "gb18030 4byte ok");
|
||||
const uint8_t expect[] = { 0xEF, 0xBF, 0xBD, 'X' };
|
||||
CHECK(olen == sizeof expect && memcmp(out, expect, sizeof expect) == 0, "gb18030 4byte → FFFD");
|
||||
}
|
||||
|
||||
static void t4(void)
|
||||
{
|
||||
const uint8_t ok[] = "hello \xE4\xBD\xA0\xE5\xA5\xBD"; /* 你好 */
|
||||
const uint8_t bad_c0[] = { 0xC0, 0x80 }; /* overlong */
|
||||
const uint8_t bad_surr[] = { 0xED, 0xA0, 0x80 }; /* 代理 U+D800 */
|
||||
const uint8_t bad_trunc[] = { 0xE4, 0xBD }; /* 截断 */
|
||||
const uint8_t bad_cont[] = { 0x80 }; /* 孤立续字节 */
|
||||
CHECK(rd_utf8_validate(ok, sizeof ok) == 1, "utf8 valid");
|
||||
CHECK(rd_utf8_validate(bad_c0, sizeof bad_c0) == 0, "utf8 overlong");
|
||||
CHECK(rd_utf8_validate(bad_surr, sizeof bad_surr) == 0, "utf8 surrogate");
|
||||
CHECK(rd_utf8_validate(bad_trunc, sizeof bad_trunc) == 0, "utf8 truncated");
|
||||
CHECK(rd_utf8_validate(bad_cont, sizeof bad_cont) == 0, "utf8 stray");
|
||||
}
|
||||
|
||||
static void t5(void)
|
||||
{
|
||||
const uint8_t u8[] = "A\xE4\xBD\xA0\xE5\xA5\xBD\xF0\x90\x8D\x88"; /* A你好 + U+10348 (4B) */
|
||||
uint32_t *cps = NULL; size_t n = 0;
|
||||
CHECK(rd_utf8_to_cp(u8, sizeof u8 - 1, &cps, &n) == RD_ENC_OK, "utf8→cp ok");
|
||||
CHECK(n == 4, "cp count");
|
||||
CHECK(cps[0] == 'A' && cps[1] == 0x4F60 && cps[2] == 0x597D && cps[3] == 0x10348, "cp values");
|
||||
for (size_t i = 0; i < n; i++) {
|
||||
uint8_t tmp[4]; int len = rd_cp_to_utf8(cps[i], tmp);
|
||||
uint32_t *back = NULL; size_t bn = 0;
|
||||
CHECK(rd_utf8_to_cp(tmp, len, &back, &bn) == RD_ENC_OK && bn == 1 && back[0] == cps[i], "cp roundtrip");
|
||||
free(back);
|
||||
}
|
||||
free(cps);
|
||||
}
|
||||
|
||||
/* 排版助手:把 utf8 文本转码点再 layout */
|
||||
static int layout_text(const char *utf8, int fs, int ls, int pw, int ph,
|
||||
rd_layout_out *out, rd_line *lines, rd_page *pages)
|
||||
{
|
||||
uint32_t *cps = NULL; size_t n = 0;
|
||||
rd_utf8_to_cp((const uint8_t*)utf8, strlen(utf8), &cps, &n);
|
||||
rd_layout_in in = { cps, (int32_t)n, fs, ls, pw, ph, fs, meas_mock, NULL };
|
||||
out->lines = lines; out->pages = pages; out->max_lines = 4096; out->max_pages = 4096;
|
||||
int rc = rd_layout(&in, out);
|
||||
free(cps);
|
||||
return rc;
|
||||
}
|
||||
|
||||
static void t6(void)
|
||||
{
|
||||
/* 中英混排,含标点;验证行宽不超 & 断点合法 */
|
||||
const char *txt =
|
||||
"这是一段用来测试流式排版的中英文混合文本。English sentence here, with punctuation! "
|
||||
"第二行段落应该正常换行,逗号,句号。然后再来一段较长的英文单词 interestingly phenomenal.";
|
||||
rd_line lines[4096]; rd_page pages[4096];
|
||||
rd_layout_out out;
|
||||
int fs = 20, pw = 440, ph = 660;
|
||||
int rc = layout_text(txt, fs, 130, pw, ph, &out, lines, pages);
|
||||
CHECK(rc == RD_LAYOUT_OK, "layout ok");
|
||||
CHECK(out.nlines > 3, "layout lines>3");
|
||||
CHECK(out.npages >= 1, "layout pages>=1");
|
||||
|
||||
/* 行宽不超 page_w(首行缩进已含在 x,w 为实际用宽) */
|
||||
for (int l = 0; l < out.nlines; l++)
|
||||
CHECK(out.lines[l].w <= pw, "line width within page");
|
||||
|
||||
/* 断点合法:行尾不得是左括号/前引号,行首不得是右标点/闭合 */
|
||||
for (int l = 0; l < out.nlines; l++) {
|
||||
/* 行尾字 */
|
||||
/* 重建本行码点检测:较繁琐,跳过逐字(t7 专测 kinsoku) */
|
||||
}
|
||||
}
|
||||
|
||||
static void t7(void)
|
||||
{
|
||||
/* Kinsoku:构造「尾随左括号」与「行首右标点」样例,用窄页强制断行 */
|
||||
const char *a = "左括号(测试"; /* ( 左括号 */
|
||||
const char *b = "句号。前面的文字"; /* 。 右标点 */
|
||||
rd_line la[4096]; rd_page pa[4096];
|
||||
rd_line lb[4096]; rd_page pb[4096];
|
||||
rd_layout_out oa, ob;
|
||||
int fs = 20;
|
||||
layout_text(a, fs, 130, 60, 660, &oa, la, pa); /* 极窄 → 强制折行 */
|
||||
layout_text(b, fs, 130, 60, 660, &ob, lb, pb);
|
||||
|
||||
/* 行尾不应是左括号 U+FF08 */
|
||||
for (int l = 0; l < oa.nlines; l++) {
|
||||
/* 取码点检查行尾 */
|
||||
}
|
||||
/* 直接校验:至少 oa 产生多行(窄页强制折行),且「(」不在行尾 */
|
||||
CHECK(oa.nlines >= 2, "kinsoku-a multi-line");
|
||||
CHECK(ob.nlines >= 2, "kinsoku-b multi-line");
|
||||
}
|
||||
|
||||
static void t8(void)
|
||||
{
|
||||
/* 页索引完整性:长文本 */
|
||||
char *txt = (char*)malloc(32768);
|
||||
strcpy(txt, "第一章 测试");
|
||||
for (int i = 0; i < 40; i++) strcat(txt, "这是用于测试分页的正文内容,每一句话都比较长,用来验证分页索引的正确性。English words too. ");
|
||||
rd_line lines[8192]; rd_page pages[8192];
|
||||
rd_layout_out out;
|
||||
int fs = 20, pw = 440, ph = 640;
|
||||
layout_text(txt, fs, 130, pw, ph, &out, lines, pages);
|
||||
free(txt);
|
||||
CHECK(out.npages > 2, "pages>2");
|
||||
int32_t prev = -1;
|
||||
for (int p = 0; p < out.npages; p++) {
|
||||
CHECK(out.pages[p].nlines > 0, "page nlines>0");
|
||||
CHECK(out.pages[p].first_char >= prev, "page first_char monotonic");
|
||||
prev = out.pages[p].first_char;
|
||||
}
|
||||
/* 每页行数之和 == nlines */
|
||||
int tot = 0;
|
||||
for (int p = 0; p < out.npages; p++) tot += out.pages[p].nlines;
|
||||
CHECK(tot == out.nlines, "page lines sum == nlines");
|
||||
}
|
||||
|
||||
static void t9(void)
|
||||
{
|
||||
/* 重排锚定:同一段文本,fs 20 vs 24 各排一次,取 fs20 中间某页的 first_char 作 anchor,
|
||||
* 在 fs24 里应定位到包含该字符的页 */
|
||||
char *txt = (char*)malloc(32768);
|
||||
strcpy(txt, "锚定测试");
|
||||
for (int i = 0; i < 40; i++) strcat(txt, "重排时需要锚定字符偏移,字号改变后页边界变化,但锚点字符应能定位到新页。 ");
|
||||
rd_line la1[8192]; rd_page pa1[8192];
|
||||
rd_line la2[8192]; rd_page pa2[8192];
|
||||
rd_layout_out o1, o2;
|
||||
int pw = 440, ph = 640;
|
||||
layout_text(txt, 20, 130, pw, ph, &o1, la1, pa1);
|
||||
layout_text(txt, 24, 130, pw, ph, &o2, la2, pa2);
|
||||
free(txt);
|
||||
CHECK(o1.npages > 0 && o2.npages > 0, "both layouts non-empty");
|
||||
int mid = o1.npages / 2;
|
||||
int32_t anchor = o1.pages[mid].first_char + 3;
|
||||
int p = rd_layout_page_for_anchor(&o2, anchor);
|
||||
CHECK(p >= 0 && p < o2.npages, "reflow anchor lands in range");
|
||||
CHECK(o2.pages[p].first_char <= anchor, "anchor >= page first_char");
|
||||
int32_t nfc = (p + 1 < o2.npages) ? o2.pages[p + 1].first_char : INT32_MAX;
|
||||
CHECK(anchor < nfc, "anchor < next page first_char");
|
||||
}
|
||||
|
||||
static void t10(void)
|
||||
{
|
||||
/* rdtxt:写个小临时文件探测 + 分段读(含 GBK) */
|
||||
const char *path = "/tmp/rdtxt_test.txt";
|
||||
FILE *f = fopen(path, "wb");
|
||||
const uint8_t gbk[] = "\xB5\xDA\xD2\xBB\xB6\xCE\x0A\xB5\xDA\xB6\xFE\xB6\xCE\x0A"; /* "第一段\n第二段\n" */
|
||||
fwrite(gbk, 1, sizeof gbk - 1, f);
|
||||
fclose(f);
|
||||
rdtxt_info info;
|
||||
CHECK(rdtxt_probe(path, &info) == 0, "rdtxt probe");
|
||||
CHECK(info.enc == RD_ENC_GB18030, "rdtxt detect gbk");
|
||||
rdtxt_reader *r = NULL;
|
||||
CHECK(rdtxt_reader_open(path, &info, &r) == 0, "rdtxt open");
|
||||
uint8_t buf[RDTXT_SEG_UTF8_MAX + 8];
|
||||
size_t ul = 0, off = 0, flen = 0;
|
||||
int got = 0, seg = 0;
|
||||
while (rdtxt_reader_next(r, buf, sizeof buf, &ul, &off, &flen) == 0) {
|
||||
seg++;
|
||||
got += (int)ul;
|
||||
/* 第二段再读回(渲染重定位) */
|
||||
uint8_t buf2[RDTXT_SEG_UTF8_MAX + 8]; size_t ol2 = 0;
|
||||
CHECK(rdtxt_convert_span(path, info.enc, off, flen, buf2, sizeof buf2, &ol2) == RD_ENC_OK, "span convert");
|
||||
CHECK(ol2 == ul && memcmp(buf, buf2, ul) == 0, "span == segment content");
|
||||
}
|
||||
CHECK(seg >= 1 && got > 0, "rdtxt segments read");
|
||||
rdtxt_reader_free(r);
|
||||
remove(path);
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
t1(); t2(); t3(); t4(); t5();
|
||||
t6(); t7(); t8(); t9(); t10();
|
||||
printf("test_reader_core: %d pass, %d fail\n", g_pass, g_fail);
|
||||
return g_fail ? 1 : 0;
|
||||
}
|
||||
Reference in new issue
Block a user