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:
MeowOS committed 2026-09-01 23:26:42 +08:00
1 parent 33d3059038
commit 553a7d5d9e
26 files changed
+6155

No files matched your search

+9
View File
@@ -0,0 +1,9 @@
# os.meow.reader 构建产物(不入仓)
/vendor/
/reader
/test/test_reader_core_host
/test/rdft_smoke_mipsel
/test/book_smoke_mipsel
/test/mem_account_host
*.o
*.a
+59
View File
@@ -0,0 +1,59 @@
/* app.c — os.meow.reader 应用装配层:main + 初始化 + 事件分发。
*
* 职责(SDK_VIEW_FRAMEWORK_DESIGN.md §B.2):main 只做 init/建页/跑事件循环/清理;把
* meow_ui_run 的平铺输入回调(key/tap/swipe/tick/touch)路由到当前活动页(shelf/reader)。
* 页面切换走 meow_view_page_push/pop(on_enter/on_leave 唯一入口)。
*/
#include "reader_internal.h"
#include "meow_ui_internal.h" /* 阅读页 blit 需要 back_buf 访问器(决策见报告) */
#include <signal.h>
static meow_view *g_shelf_view;
static meow_view *g_reader_view;
static void on_sigterm(int s) { (void)s; meow_ui_request_exit(); }
static void app_on_key(int keycode, int is_down) { (void)keycode; (void)is_down; }
static void app_on_tap(int x, int y)
{
if (reader_is_active()) { reader_on_tap(x, y); return; }
shelf_on_tap(x, y);
}
static void app_on_swipe(int dir, int dx, int dy)
{
(void)dy; (void)dx;
if (reader_is_active()) { reader_on_swipe(dir, dx, dy); return; }
if (dir == MEOW_SWIPE_LEFT) { meow_ui_request_exit(); }
}
static void app_on_tick(void)
{
uint64_t now = meow_ui_time_ms();
if (reader_is_active()) { reader_on_tick(now); return; }
}
int main(void)
{
meow_log(MEOW_LOG_INFO, "start");
meow_ui_opts o = { 480, 720 };
if (meow_ui_init(&o) != 0) { meow_log(MEOW_LOG_ERROR, "init failed"); return 1; }
meow_theme_add_listener(NULL);
signal(SIGTERM, on_sigterm);
g_shelf_view = shelf_view_create();
g_reader_view = reader_view_create();
meow_view_page_push(ACT_SHELF);
meow_ui_events ev = { app_on_key, app_on_tap, app_on_swipe, app_on_tick,
NULL, NULL, NULL };
meow_ui_run(&ev);
meow_view_free(g_reader_view);
meow_view_free(g_shelf_view);
meow_ui_shutdown();
meow_log(MEOW_LOG_INFO, "exit");
return 0;
}
+1
View File
@@ -0,0 +1 @@
{"name":"Reader","exec":"/usr/data/mnt/sd_0/MeowAPPs/os.meow.reader/reader","package":"os.meow.reader","version":"0.1","version_code":1,"framework":1,"fonts":"full","author":"MeowOS","icon":"icon.png"}
+346
View File
@@ -0,0 +1,346 @@
/* app/book.c — 书桥实现(见 app/book.h)。 */
#include "book.h"
#include "rdtxt.h"
#include "rdlayout.h"
#include "../engine/rdft.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct seg {
size_t file_off, file_len;
int page_start; /* 全局页表起点 */
int page_count; /* 本段页数 */
} seg_t;
struct rd_book {
char path[400];
char font[400];
char base[256]; /* 文件名 */
const char *encname;
rdft *ft;
rd_enc_t enc;
int fs, ls, pw, ph, mx, my, top;
int content_w, content_h;
seg_t *segs; int nsegs;
rd_book_page *pages; int npages; /* flat 页表 */
int cur; /* 当前页 */
/* 当前渲染段的复用缓冲 */
uint8_t *seg_utf8; size_t seg_utf8_cap;
uint32_t *seg_cps; size_t seg_cps_n, seg_cps_cap;
/* 当前段布局 */
rd_line *seg_lines; rd_page *seg_pages; int seg_nlines, seg_npages;
int seg_cache_idx; /* 当前缓冲对应段;-1=无 */
};
static const char *encname_of(rd_enc_t e)
{
switch (e) {
case RD_ENC_UTF8: return "UTF-8";
case RD_ENC_UTF16LE: return "UTF-16LE";
case RD_ENC_UTF16BE: return "UTF-16BE";
case RD_ENC_GB18030: return "GB18030";
case RD_ENC_BIG5: return "Big5(按GB18030)";
case RD_ENC_ASCII: return "ASCII";
}
return "?";
}
/* 测量回调:排版器逐字测 advance → rdft_advance(不栅格化)。 */
static int book_meas(uint32_t cp, int fs, int *adv, void *ctx)
{
(void)fs;
rd_book *b = (rd_book *)ctx;
/* advance 受字号影响:rdft 已 set_px(fs),故用当前 rdft 度量即可 */
return rdft_advance(b->ft, cp, adv);
}
static int build_index(rd_book *b)
{
int rc = 0;
rdtxt_reader *r = NULL;
if (rdtxt_reader_open(b->path, &(rdtxt_info){ .enc = b->enc, .file_size = 0, .path = "" },
&r) != 0) return -1;
/* rdtxt_reader_open 以 info 的 enc 为准(path 域不读) */
size_t cap_segs = 64, cap_pages = 1024;
b->segs = (seg_t *)malloc(cap_segs * sizeof(seg_t));
b->pages = (rd_book_page *)malloc(cap_pages * sizeof(rd_book_page));
if (!b->segs || !b->pages) { rd_book_close(b); return -6; }
b->nsegs = b->npages = 0;
uint8_t *utf8 = (uint8_t *)malloc(RDTXT_SEG_UTF8_MAX + 8);
if (!utf8) { rd_book_close(b); return -6; }
size_t off = 0, flen = 0, ulen = 0;
int seg_idx = 0;
while (rdtxt_reader_next(r, utf8, RDTXT_SEG_UTF8_MAX + 8, &ulen, &off, &flen) == 0) {
if (seg_idx >= (int)cap_segs) { cap_segs *= 2; b->segs = (seg_t *)realloc(b->segs, cap_segs * sizeof(seg_t)); }
uint32_t *cps = NULL; size_t n = 0;
rd_utf8_to_cp(utf8, ulen, &cps, &n); /* rd_utf8_to_cp 内部 malloc */
rd_layout_in in;
rd_layout_out out;
rd_line *lines = (rd_line *)malloc((n ? n : 1) * sizeof(rd_line));
rd_page *pages = (rd_page *)malloc((n ? n : 1) * sizeof(rd_page));
if (!lines || !pages) { free(lines); free(pages); free(cps); free(utf8); rd_book_close(b); return -6; }
in.chars = cps; in.n = (int32_t)n; in.fs = b->fs; in.line_scale = b->ls;
in.page_w = b->content_w; in.page_h = b->content_h; in.indent = b->fs;
in.meas = book_meas; in.mctx = b;
out.lines = lines; out.pages = pages; out.max_lines = (int)(n ? n : 1); out.max_pages = (int)(n ? n : 1);
/* 布局容量不足时翻倍重试(罕见)。 */
int lrc = rd_layout(&in, &out);
free(cps);
if (lrc != RD_LAYOUT_OK) { free(lines); free(pages); break; } /* 段为空/异常:跳过 */
/* 登记段 + 页 */
b->segs[seg_idx].file_off = off;
b->segs[seg_idx].file_len = flen;
b->segs[seg_idx].page_start = b->npages;
b->segs[seg_idx].page_count = out.npages;
for (int p = 0; p < out.npages; p++) {
if (b->npages >= (int)cap_pages) { cap_pages *= 2; b->pages = (rd_book_page *)realloc(b->pages, cap_pages * sizeof(rd_book_page)); }
b->pages[b->npages].seg = seg_idx;
b->pages[b->npages].seg_char = out.pages[p].first_char;
b->npages++;
}
free(lines); free(pages);
seg_idx++;
}
free(utf8);
rdtxt_reader_free(r);
b->nsegs = seg_idx;
rc = 0;
return rc;
}
int rd_book_open(rd_book **b, const char *path, const char *font_path,
int fs, int ls, int pw, int ph, int mx, int my, int top)
{
if (!b || !path) return -1;
rd_book *x = (rd_book *)calloc(1, sizeof *x);
if (!x) return -6;
if (strlen(path) >= sizeof x->path) { free(x); return -2; }
strcpy(x->path, path);
if (font_path) { if (strlen(font_path) >= sizeof x->font) { free(x); return -2; } strcpy(x->font, font_path); }
/* 文件名 */
const char *sl = strrchr(path, '/');
strncpy(x->base, sl ? sl + 1 : path, sizeof x->base - 1); x->base[sizeof x->base - 1] = 0;
if (fs <= 0) fs = 20;
if (ls <= 0) ls = 130;
x->fs = fs; x->ls = ls; x->pw = pw; x->ph = ph; x->mx = mx; x->my = my; x->top = top;
x->content_w = pw - 2 * mx;
x->content_h = ph - top - my;
if (x->content_w < 10 || x->content_h < 10) { close: free(x); return -3; }
rdtxt_info info;
if (rdtxt_probe(path, &info) != 0) goto close;
x->enc = info.enc;
x->encname = encname_of(info.enc);
if (rdft_init(&x->ft) != 0) goto close;
if (font_path && rdft_load_font(x->ft, x->font) != 0) { rdft_free(x->ft); goto close; }
if (x->ft) rdft_set_px(x->ft, fs);
x->seg_cache_idx = -1;
if (build_index(x) != 0) { rd_book_close(x); return -5; }
x->cur = 0;
*b = x;
return 0;
}
void rd_book_close(rd_book *b)
{
if (!b) return;
free(b->segs);
free(b->pages);
free(b->seg_utf8);
free(b->seg_cps);
free(b->seg_lines);
free(b->seg_pages);
if (b->ft) rdft_free(b->ft);
free(b);
}
int rd_book_pages(rd_book *b) { return b ? b->npages : 0; }
int rd_book_current(rd_book *b) { return b ? b->cur : 0; }
int rd_book_font(rd_book *b) { return b ? b->fs : 0; }
int rd_book_ls(rd_book *b) { return b ? b->ls : 0; }
void rd_book_geom(rd_book *b, int *fs, int *ls, int *pw, int *ph, int *mx, int *my, int *top)
{
if (!b) return;
if (fs) *fs = b->fs;
if (ls) *ls = b->ls;
if (pw) *pw = b->pw;
if (ph) *ph = b->ph;
if (mx) *mx = b->mx;
if (my) *my = b->my;
if (top) *top = b->top;
}
const char *rd_book_basename(const rd_book *b) { return b ? b->base : ""; }
const char *rd_book_encname(const rd_book *b) { return b ? b->encname : ""; }
const char *rd_book_path(const rd_book *b) { return b ? b->path : ""; }
void rd_book_anchor(rd_book *b, int *seg, int32_t *ch)
{
if (!b || b->npages <= 0) { if (seg) *seg = 0; if (ch) *ch = 0; return; }
int p = b->cur < 0 ? 0 : (b->cur >= b->npages ? b->npages - 1 : b->cur);
if (seg) *seg = b->pages[p].seg;
if (ch) *ch = b->pages[p].seg_char;
}
int rd_book_turn(rd_book *b, int delta)
{
if (!b || b->npages <= 0) return 0;
int np = b->cur + delta;
if (np < 0) np = 0;
if (np >= b->npages) np = b->npages - 1;
b->cur = np;
return np;
}
/* --- 读回一段并布局到复用缓冲(渲染用)--- */
static int load_seg(rd_book *b, int seg_idx)
{
if (seg_idx == b->seg_cache_idx) return 0;
seg_t *s = &b->segs[seg_idx];
size_t cap = RDTXT_SEG_UTF8_MAX + 8;
if (!b->seg_utf8) b->seg_utf8 = (uint8_t *)malloc(cap);
if (!b->seg_utf8) return -6;
size_t ulen = 0;
if (rdtxt_convert_span(b->path, b->enc, s->file_off, s->file_len, b->seg_utf8, cap, &ulen) != RD_ENC_OK)
return -4;
size_t n = 0;
free(b->seg_cps); /* rd_utf8_to_cp 内部 malloc 新数组 */
b->seg_cps = NULL;
if (rd_utf8_to_cp(b->seg_utf8, ulen, &b->seg_cps, &n) != RD_ENC_OK) return -5;
b->seg_cps_n = n;
/* 布局(容量:段码点数为上界) */
if (b->seg_lines) free(b->seg_lines);
if (b->seg_pages) free(b->seg_pages);
b->seg_lines = (rd_line *)malloc((n ? n : 1) * sizeof(rd_line));
b->seg_pages = (rd_page *)malloc((n ? n : 1) * sizeof(rd_page));
if (!b->seg_lines || !b->seg_pages) return -6;
rd_layout_in in;
rd_layout_out out;
in.chars = b->seg_cps; in.n = (int32_t)n; in.fs = b->fs; in.line_scale = b->ls;
in.page_w = b->content_w; in.page_h = b->content_h; in.indent = b->fs;
in.meas = book_meas; in.mctx = b;
out.lines = b->seg_lines; out.pages = b->seg_pages; out.max_lines = (int)(n ? n : 1); out.max_pages = (int)(n ? n : 1);
int lrc = rd_layout(&in, &out);
if (lrc != RD_LAYOUT_OK) { b->seg_nlines = b->seg_npages = 0; b->seg_cache_idx = -1; return -4; }
b->seg_nlines = out.nlines; b->seg_npages = out.npages;
b->seg_cache_idx = seg_idx;
return 0;
}
int rd_book_render(rd_book *b, int page_no, uint16_t *out, int pitch, uint16_t bg565, uint16_t fg565)
{
if (!b || !out || b->npages <= 0 || page_no < 0 || page_no >= b->npages) return -1;
int npix = b->pw * b->ph;
for (int i = 0; i < npix; i++) out[i] = bg565;
rd_book_page *pg = &b->pages[page_no];
int seg_idx = (int)pg->seg;
if (seg_idx < 0 || seg_idx >= b->nsegs) return -1;
if (load_seg(b, seg_idx) != 0) return -2;
int local_page = page_no - b->segs[seg_idx].page_start;
if (local_page < 0 || local_page >= b->seg_npages) return 1; /* 空页 */
rd_page *pp = &b->seg_pages[local_page];
int asc = rdft_ascender(b->ft);
int screen_x0 = b->mx;
int screen_y0 = b->top;
for (int li = 0; li < pp->nlines; li++) {
rd_line *ln = &b->seg_lines[pp->first_line + li];
int pen = screen_x0 + ln->x;
int baseline = screen_y0 + ln->y + asc;
for (int32_t ci = ln->s; ci < ln->e; ci++) {
uint32_t cp = b->seg_cps[ci];
int adv = 0, bw = 0, bh = 0, left = 0, top = 0;
const uint8_t *bmp = NULL;
rdft_glyph(b->ft, cp, &adv, &bmp, &bw, &bh, &left, &top);
if (bmp && bw > 0)
rdft_blend565(out, pitch, b->pw, b->ph, pen, baseline, bmp, bw, bh, left, top, fg565, bg565);
pen += adv;
if (cp == '\t') pen += adv; /* 制表符已由布局按空格 advance 计量,此处保持游标 */
}
}
return 0;
}
/* 重排 + 回锚:以 (seg,ch) 为锚重排全书;返回重排后的页(0=成功,负值=错误)。 */
int rd_book_set_size(rd_book *b, int fs, int ls)
{
if (!b) return -1;
if (fs <= 0) fs = b->fs;
if (ls <= 0) ls = b->ls;
int anchor_seg, anchor_ch;
rd_book_anchor(b, &anchor_seg, &anchor_ch);
return rd_book_jump_to_anchor(b, anchor_seg, anchor_ch, fs, ls);
}
int rd_book_jump_to_anchor(rd_book *b, int seg, int32_t ch, int fs, int ls)
{
if (!b) return -1;
if (fs != b->fs || ls != b->ls) {
b->fs = fs; b->ls = ls;
b->content_w = b->pw - 2 * b->mx;
b->content_h = b->ph - b->top - b->my;
if (b->ft) rdft_set_px(b->ft, fs);
/* 重建索引 */
seg_t *nos = b->segs; int n_old_segs = b->nsegs;
rd_book_page *nop = b->pages; int n_old_pages = b->npages;
b->segs = NULL; b->pages = NULL; b->nsegs = b->npages = 0;
if (build_index(b) != 0) {
b->segs = nos; b->pages = nop; b->nsegs = n_old_segs; b->npages = n_old_pages;
return -5;
}
free(nos); free(nop);
}
/* 回锚:找第一个 (seg==anchorSeg && seg_char>=anchorCh) 的页,或段内第一个 seg_char>=ch;跨段按 seg 序 */
int target = 0;
for (int p = 0; p < b->npages; p++) {
int ps = b->pages[p].seg;
if (ps < seg) { target = p + 1; continue; }
if (ps > seg) { break; }
if (b->pages[p].seg_char >= ch) { target = p; break; }
target = p;
}
if (target >= b->npages) target = b->npages - 1;
if (target < 0) target = 0;
b->cur = target;
return target;
}
/* SDK 字库兜底:逐行回调正文文本(无 TTF/OTF 时用 meow_ui_text 渲染)。 */
int rd_book_render_text(rd_book *b, int page_no, rd_book_line_fn fn, void *ctx)
{
if (!b || !fn || b->npages <= 0 || page_no < 0 || page_no >= b->npages) return -1;
rd_book_page *pg = &b->pages[page_no];
int seg_idx = (int)pg->seg;
if (seg_idx < 0 || seg_idx >= b->nsegs) return -1;
if (load_seg(b, seg_idx) != 0) return -2;
int local_page = page_no - b->segs[seg_idx].page_start;
if (local_page < 0 || local_page >= b->seg_npages) return 1;
rd_page *pp = &b->seg_pages[local_page];
/* 单线程渲染:每行最多 content_w 个单宽字符 → ≤4B/字 UTF-8 */
uint8_t *line_utf8 = (uint8_t *)malloc((size_t)(b->content_w * 4 + 32));
if (!line_utf8) return -6;
for (int li = 0; li < pp->nlines; li++) {
rd_line *ln = &b->seg_lines[pp->first_line + li];
int x = b->mx + ln->x, y = b->top + ln->y;
/* 组 utf8 */
size_t o = 0;
for (int32_t ci = ln->s; ci < ln->e; ci++) {
uint8_t tmp[4];
int n = rd_cp_to_utf8(b->seg_cps[ci], tmp);
memcpy(line_utf8 + o, tmp, (size_t)n); o += (size_t)n;
}
line_utf8[o] = 0;
fn(x, y, (const char *)line_utf8, (int)o, ctx);
}
free(line_utf8);
return 0;
}
+64
View File
@@ -0,0 +1,64 @@
/* app/book.h — 书桥:把 core(TXT/排版) + engine(FreeType) 串成「一本 TXT 书」。
*
* 打开一本书:探测编码 → 逐段 rdtxt 流式读 → 每段 utf8→码点 → rd_layout(测量=rdft_advance)
* → 页索引(flat:每页=段+段内字符偏移)+ 段表(file_off/file_len/page_start/page_count)。
* 渲染页:定位段 → 重读该段 → 重排 → 取该页行 → 逐字 rdft_glyph → blend565 到全屏缓冲。
* 锚定/进度:(段,字符偏移),与字号无关;改字号/行距触发后台重排并回锚(§6.3)。
*
* 内存纪律:单段(文本+cps+布局)≈ 320KB 冲击,随段释放;页索引 flat ~每页 12B;3 页位图由
* 阅读 view 持有(本模块只填一个渲染目标缓冲,不缓存整页)。
*/
#ifndef RDBOOK_H
#define RDBOOK_H
#include "rdenc.h"
#include <stddef.h>
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
typedef struct rd_book rd_book;
typedef struct rd_book_page {
int32_t seg; /* 段下标 */
int32_t seg_char; /* 段内字符偏移(页首) */
} rd_book_page;
/* 打开书:path=TXT;font_path=FreeType 字体;geom=版面(fs 字号 / ls 行距% / pw,ph 全屏 /
* mx,my 左右/上下边距 / top 顶部 inset(状态栏+顶条))。0=成功。 */
int rd_book_open(rd_book **b, const char *path, const char *font_path,
int fs, int ls, int pw, int ph, int mx, int my, int top);
void rd_book_close(rd_book *b);
int rd_book_pages(rd_book *b);
int rd_book_current(rd_book *b); /* 当前页 */
int rd_book_turn(rd_book *b, int delta); /* +1/-1;clamp 到 [0,npages-1] */
void rd_book_anchor(rd_book *b, int *seg, int32_t *ch);/* 当前页锚(段,字符偏移) */
int rd_book_jump_to_anchor(rd_book *b, int seg, int32_t ch, int fs, int ls); /* 重排 + 回锚 */
int rd_book_set_size(rd_book *b, int fs, int ls); /* 重排 + 回锚(anchor=当前页) */
/* 渲染页到全屏缓冲:out 为 pw*ph RGB565(每行 pitch 字节);先填 bg565 再画正文。
* 0=成功;1=该页为空(无正文,如末页空白)。 */
int rd_book_render(rd_book *b, int page_no, uint16_t *out, int pitch, uint16_t bg565, uint16_t fg565);
/* SDK 字库兜底:逐行回调(每行一个 utf8 文本串,屏幕坐标 x,y 已含 inset/边距)。
* 用于「无 TTF/OTF → 用 meow_ui_text 位图字库渲染正文」的降级路径(见 reader 视图取舍)。 */
typedef void (*rd_book_line_fn)(int x, int y, const char *utf8, int len, void *ctx);
int rd_book_render_text(rd_book *b, int page_no, rd_book_line_fn fn, void *ctx);
/* 书名/信息(章节名:TXT 无章节=文件名;enc 可读名)。调用方 strcpy 到自己的缓冲。 */
const char *rd_book_basename(const rd_book *b); /* 文件名(路径尾分量) */
const char *rd_book_encname(const rd_book *b);
const char *rd_book_path(const rd_book *b); /* 原始路径 */
/* 字体/几何 */
int rd_book_font(rd_book *b); /* 当前字号 */
int rd_book_ls(rd_book *b); /* 行距% */
void rd_book_geom(rd_book *b, int *fs, int *ls, int *pw, int *ph, int *mx, int *my, int *top);
#ifdef __cplusplus
}
#endif
#endif /* RDBOOK_H */
+102
View File
@@ -0,0 +1,102 @@
/* app/progress.c — 进度持久化(实现见 app/progress.h)。 */
#include "progress.h"
#include "meow_ui.h" /* meow_config_dir() */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
/* 从 MEOW_DATA_DIR(<TF>/MeowData/<pkg>/data)剥掉尾 "/data",得到 <TF>/MeowData/<pkg>。 */
static void strip_data_suffix(const char *d, char *out, int outsz)
{
if (!d || !d[0]) { strncpy(out, "/tmp", (size_t)outsz - 1); out[outsz - 1] = 0; return; }
size_t n = strlen(d);
const char *suffix = "/data";
size_t sl = strlen(suffix);
size_t keep;
if (n >= sl && strcmp(d + n - sl, suffix) == 0) keep = n - sl;
else keep = n;
if (keep >= (size_t)outsz) keep = (size_t)outsz - 1;
memcpy(out, d, keep);
out[keep] = 0;
}
int rdprog_dir(char *out, int outsz)
{
if (!out || outsz <= 0) return -1;
const char *data = meow_config_dir();
char base[360];
strip_data_suffix(data ? data : "", base, sizeof base);
if (snprintf(out, (size_t)outsz, "%s/progress", base) >= outsz) return -2;
/* mkdir -p */
char tmp[400];
strncpy(tmp, out, sizeof tmp - 1); tmp[sizeof tmp - 1] = 0;
for (char *p = tmp + 1; *p; p++) {
if (*p == '/') { *p = 0; mkdir(tmp, 0755); *p = '/'; }
}
mkdir(tmp, 0755);
return 0;
}
void rdprog_hash(const char *book_path, char *out, int outsz)
{
uint32_t h = 2166136261u;
for (const unsigned char *p = (const unsigned char *)book_path; *p; p++) {
h ^= *p; h *= 16777619u;
}
snprintf(out, (size_t)outsz, "%08x.prog", h);
}
static int rdprog_path(const char *book_path, char *out, int outsz,
int *created_dir)
{
char dir[400];
if (rdprog_dir(dir, sizeof dir) != 0) return -1;
char name[24];
rdprog_hash(book_path, name, sizeof name);
if (snprintf(out, (size_t)outsz, "%s/%s", dir, name) >= outsz) return -2;
if (created_dir) *created_dir = 1;
return 0;
}
int rdprog_load(const char *book_path, rdprog *p)
{
if (!book_path || !p) return -1;
char path[440];
if (rdprog_path(book_path, path, sizeof path, NULL) != 0) return -1;
FILE *f = fopen(path, "rb");
if (!f) return 0;
char line[256];
int seg = 0, fs = 0, ls = 0; int32_t ch = 0; uint64_t ts = 0; int v = 0, found = 0;
while (fgets(line, sizeof line, f)) {
if (sscanf(line, "v=%d", &v) == 1) { if (v == 1) found = 1; }
else if (sscanf(line, "seg=%d", &seg) == 1) { /* no-op */ }
else if (sscanf(line, "ch=%d", &ch) == 1) { /* no-op */ }
else if (sscanf(line, "fs=%d", &fs) == 1) { /* no-op */ }
else if (sscanf(line, "ls=%d", &ls) == 1) { /* no-op */ }
else if (sscanf(line, "ts=%llu", (unsigned long long *)&ts) == 1) { /* no-op */ }
}
fclose(f);
if (!found) return 0;
p->valid = 1; p->seg = seg; p->ch = ch; p->fs = fs; p->ls = ls; p->ts = ts;
return 1;
}
int rdprog_save(const char *book_path, const rdprog *p)
{
if (!book_path || !p) return -1;
char path[440];
if (rdprog_path(book_path, path, sizeof path, NULL) != 0) return -1;
FILE *f = fopen(path, "wb");
if (!f) return -1;
/* 先写临时再改原子(exFAT 无 rename+replace 语义保证,直接写亦可接受) */
fprintf(f, "v=1\n");
fprintf(f, "seg=%d\n", p->seg);
fprintf(f, "ch=%d\n", (int)p->ch);
fprintf(f, "fs=%d\n", p->fs);
fprintf(f, "ls=%d\n", p->ls);
fprintf(f, "ts=%llu\n", (unsigned long long)p->ts);
fclose(f);
return 0;
}
+42
View File
@@ -0,0 +1,42 @@
/* app/progress.h — 进度持久化:`MeowData/os.meow.reader/progress/`(每书 = path hash 文件)。
*
* 内容(文本,可读可调):v=1 / seg=<段> / ch=<字符偏移> / fs=<字号> / ls=<行距%> / ts=<unix>。
* 锚定是(段,字符偏移),与字号无关 → 换字号书签不失效(READER_ENGINE_NOTES §6.3)。
* 无 SDK 之外依赖;目录推导:MEOW_DATA_DIR(launcher 注入 <TF>/MeowData/<pkg>/data)去皮 /data →
* <TF>/MeowData/<pkg>,接 "progress";无 env(宿主/无卡)回退 /tmp/meowcfg_<pid>/progress。
*/
#ifndef RDPROG_H
#define RDPROG_H
#include <stddef.h>
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
typedef struct rdprog {
int valid; /* 1=有档 */
int seg;
int32_t ch;
int fs;
int ls;
uint64_t ts; /* 最近阅读 unix 秒 */
} rdprog;
/* 组装 progress 目录路径并 mkdir -p。成功 0(out 填目录绝对路径);负值错误。 */
int rdprog_dir(char *out, int outsz);
/* 由书路径算 hash 文件名(FNV-1a 32bit hex)写 out(≥20 字节)。 */
void rdprog_hash(const char *book_path, char *out, int outsz);
/* 读进度:1=有档(填 p);0=无/不存在;负值=IO 错。 */
int rdprog_load(const char *book_path, rdprog *p);
/* 写进度。0=成功;负值=IO/无法建目录(不崩)。 */
int rdprog_save(const char *book_path, const rdprog *p);
#ifdef __cplusplus
}
#endif
#endif /* RDPROG_H */
+334
View File
@@ -0,0 +1,334 @@
/* app/reader.c — 阅读页:W_CUSTOM 画布 + 三栏交互 + 中央菜单占位 + 顶栏进度/章节名 + 进度持久化。
*
* 渲染:W_CUSTOM 画布覆盖全屏;render_cb 画帧 = 填底色 → 翻页 blit(FreeType 页位图缓存 /
* 无字体则 SDK 位图字库兜底直绘)→ 顶栏 → 菜单覆盖层。翻页 = 局部脏区(flush_region)。
* 交互:点击左/右 1/3 翻页;中央唤菜单(字号±/行距±/返回);左滑=下一页、右滑=上一页。
* 进度:打开自动续读;换字号重排回锚(段,字符偏移);离开/翻页时保存。
*/
#include "../reader_internal.h"
#include "meow_ui_internal.h" /* back_buf/pitch/bpp 访问器(决策见报告) */
#include "book.h"
#include "progress.h"
#include <dirent.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <time.h>
#define RD_MENU_H 64
#define RD_MENU_BTN 5
static rd_book *g_book;
static int g_reader_active;
static meow_widget *g_canvas;
static int g_pw, g_ph, g_top, g_mx, g_my;
static int g_fs, g_ls;
#define RD_PAGE_CACHE 3
#define RD_PAGE_SLOT_CUR 1
static uint16_t *g_page_buf[RD_PAGE_CACHE]; /* [0]=prev [1]=cur [2]=next(FreeType 页位图) */
static int g_has_font; /* 1=FreeType 路径;0=SDK 字库兜底 */
static int g_menu_open;
static char g_font_path[400];
static int g_notice_ms;
static uint32_t g_page_bg, g_page_fg; /* 正文页底/字色(24bit) */
static char g_chapter[128]; /* 顶栏章节名(TXT 无章节=文件名) */
#define RD_READER_DEFAULT_FS 20
#define RD_READER_DEFAULT_LS 130
/* 前向声明(reader_reflow* 依赖 reader_refill/redraw,定义在后) */
static void reader_redraw(void);
static int reader_refill_page(void);
static void reader_reflow_persist(void);
/* ---------- 工具 ---------- */
static uint16_t rgb565(uint32_t c)
{
int r = (c >> 16) & 0xFF, g = (c >> 8) & 0xFF, b = c & 0xFF;
return (uint16_t)(((r >> 3) << 11) | ((g >> 2) << 5) | (b >> 3));
}
/* 递归(深度≤3)扫 MeowOS/fonts/ 找第一个 .otf/.ttf。0=找到。 */
static int find_font_rec(const char *dir, int depth)
{
if (depth > 3) return 0;
DIR *dp = opendir(dir);
if (!dp) return 0;
struct dirent *de;
int found = 0;
while ((de = readdir(dp)) && !found) {
if (de->d_name[0] == '.') continue;
char full[480];
if (snprintf(full, sizeof full, "%s/%s", dir, de->d_name) >= (int)sizeof full) continue;
struct stat st;
if (stat(full, &st) != 0) continue;
if (S_ISDIR(st.st_mode)) { found = find_font_rec(full, depth + 1); }
else {
const char *d = strrchr(de->d_name, '.');
if (d && (strcasecmp(d, ".otf") == 0 || strcasecmp(d, ".ttf") == 0)) {
if (strlen(full) < sizeof g_font_path) {
strcpy(g_font_path, full);
found = 1;
}
}
}
}
closedir(dp);
return found;
}
static const char *resolve_font(void)
{
g_font_path[0] = 0;
if (find_font_rec(READER_SD_ROOT "/MeowOS/fonts", 0)) return g_font_path;
return NULL;
}
static void chapter_name(void)
{
/* TXT 无章节 → 文件名;R1 固定。 */
snprintf(g_chapter, sizeof g_chapter, "%s", rd_book_basename(g_book));
}
static void reader_reflow(void)
{
if (!g_book) return;
int seg = 0; int32_t ch = 0;
rd_book_anchor(g_book, &seg, &ch); /* 记录当前锚,重排后回锚 */
rd_book_jump_to_anchor(g_book, seg, ch, g_fs, g_ls);
reader_refill_page();
reader_redraw();
reader_reflow_persist();
}
static void reader_reflow_persist(void)
{
if (!g_book) return;
int seg = 0; int32_t ch = 0;
rd_book_anchor(g_book, &seg, &ch);
rdprog p;
p.valid = 1; p.seg = seg; p.ch = ch; p.fs = g_fs; p.ls = g_ls; p.ts = (uint64_t)time(NULL);
rdprog_save(rd_book_path(g_book), &p);
}
/* ---------- 渲染(W_CUSTOM render_cb) ---------- */
static void reader_draw_line_atlas(int x, int y, const char *utf8, int len, void *ctx)
{
(void)ctx; (void)len;
meow_ui_text(x, y, utf8, (g_fs <= 20) ? 20 : 28, g_page_fg);
}
static void reader_canvas_render(meow_widget *w, const meow_theme *th, void *ctx)
{
(void)w; (void)ctx;
meow_ui_clear(g_page_bg);
if (g_book) {
int page = rd_book_current(g_book);
if (g_has_font && g_page_buf[RD_PAGE_SLOT_CUR]) {
/* 翻页 blit:行拷贝当前页位图 → back_buf(局部脏区由 flush_region 上屏) */
unsigned char *bb = meow_ui_back_buf();
int pitch = meow_ui_back_buf_pitch();
int bpp = meow_ui_back_buf_bpp();
uint16_t *src = g_page_buf[RD_PAGE_SLOT_CUR];
if (bb && pitch > 0 && bpp == 16) {
for (int y = 0; y < g_ph; y++)
memcpy(bb + (size_t)y * pitch, src + (size_t)y * g_pw, (size_t)g_pw * 2);
}
} else {
rd_book_render_text(g_book, page, reader_draw_line_atlas, NULL);
}
}
/* 顶栏:进度% + 章节名(避开系统状态栏) */
int sbh = meow_ui_statusbar_height();
int pages = rd_book_pages(g_book);
int page = rd_book_current(g_book);
char pct[16];
int p = pages > 0 ? (page * 100 / pages) : 0;
snprintf(pct, sizeof pct, "%d%%", p);
meow_ui_fill_rect(0, sbh, g_pw, (g_top - 4 - sbh), g_page_bg);
meow_ui_text(16, sbh + 4, g_chapter[0] ? g_chapter : "阅读", 20, th->fg);
meow_ui_text_right(g_pw - 16, sbh + 4, pct, 20, th->fg);
meow_ui_draw_rect(0, g_top - 5, g_pw, 1, th->outline);
if (g_menu_open) {
int y = g_ph - RD_MENU_H;
meow_ui_fill_rect_alpha(0, y, g_pw, RD_MENU_H, 0x000000, 160);
/* 5 按钮:返回 | 字号− | 字号+ | 行距− | 行距+ */
int bw = g_pw / RD_MENU_BTN;
const char *labels[RD_MENU_BTN] = { "返回", "字-", "字+", "距-", "距+" };
for (int i = 0; i < RD_MENU_BTN; i++) {
int bx = i * bw;
meow_ui_fill_rect(bx + 4, y + 12, bw - 8, RD_MENU_H - 24, th->surfaceContainer);
meow_ui_text_center(bx + bw / 2, y + (RD_MENU_H - 20) / 2 - 6, labels[i], 20, th->fg);
}
}
}
static void reader_redraw(void)
{
if (!g_canvas) return;
meow_widget_draw(g_canvas); /* → render_cb 画帧 */
meow_ui_flush_region(0, 0, g_pw, g_ph);
}
static void reader_free_page_bufs(void)
{
for (int i = 0; i < RD_PAGE_CACHE; i++) { free(g_page_buf[i]); g_page_buf[i] = NULL; }
}
/* 渲染当前页 + 前后各一页到 3 槽缓存(对齐设计「当前±前±后」;动画档为 无/滑动/淡入 R1 默认无)。 */
static int reader_refill_page(void)
{
if (!g_book) return -1;
if (g_has_font && g_page_buf[RD_PAGE_SLOT_CUR]) {
uint16_t bg = rgb565(g_page_bg), fg = rgb565(g_page_fg);
int cur = rd_book_current(g_book);
int np = rd_book_pages(g_book);
int slots[RD_PAGE_CACHE] = { cur - 1, cur, cur + 1 };
for (int i = 0; i < RD_PAGE_CACHE; i++) {
int p = slots[i];
if (p >= 0 && p < np) rd_book_render(g_book, p, g_page_buf[i], g_pw * 2, bg, fg);
else for (int y = 0; y < g_ph; y++) /* 空白槽填背景 */
for (int x = 0; x < g_pw; x++) g_page_buf[i][(size_t)y * g_pw + x] = bg;
}
return 0;
}
return 0; /* 兜底路径:render_text 直绘 */
}
/* ---------- 打开 ---------- */
void reader_open(const char *path)
{
if (!path || !path[0]) return;
if (g_book) { rd_book_close(g_book); g_book = NULL; }
reader_free_page_bufs();
const char *fp = resolve_font();
g_has_font = (fp != NULL);
g_fs = RD_READER_DEFAULT_FS; g_ls = RD_READER_DEFAULT_LS;
/* 进度(含字号/行距偏好) */
rdprog prog;
int hasprog = (rdprog_load(path, &prog) == 1) && prog.valid;
if (hasprog) { if (prog.fs >= 14 && prog.fs <= 40) g_fs = prog.fs; if (prog.ls >= 100 && prog.ls <= 200) g_ls = prog.ls; }
int sbh = meow_ui_statusbar_height();
g_pw = meow_ui_width(); g_ph = meow_ui_height();
g_top = sbh + 34; g_mx = 16; g_my = 20;
const meow_theme *th = meow_theme_get();
g_page_bg = th->surface; g_page_fg = th->onSurface;
/* 开书(FreeType 路径带齐几何;无字体则 g_has_font=0 → atlas 兜底) */
int rc = rd_book_open(&g_book, path, fp, g_fs, g_ls, g_pw, g_ph, g_mx, g_my, g_top);
if (rc != 0) {
meow_log(MEOW_LOG_WARN, "book open failed rc=%d path=%s", rc, path);
g_book = NULL; g_has_font = 0;
g_notice_ms = 3000;
return;
}
chapter_name();
if (g_has_font) {
reader_free_page_bufs();
for (int i = 0; i < RD_PAGE_CACHE; i++)
g_page_buf[i] = (uint16_t *)malloc((size_t)g_pw * (size_t)g_ph * 2);
if (!g_page_buf[RD_PAGE_SLOT_CUR]) { reader_free_page_bufs(); g_has_font = 0; }
}
/* 续读:进度锚(段,字符偏移)→ 跳页 */
if (hasprog && g_book) {
rd_book_jump_to_anchor(g_book, prog.seg, prog.ch, g_fs, g_ls);
}
reader_refill_page();
g_menu_open = 0;
if (g_reader_active) reader_redraw();
}
/* ---------- 视图生命周期 ---------- */
static void reader_on_create(meow_view *v, void *ctx) { (void)v; (void)ctx; }
static void reader_on_enter(meow_view *v, void *ctx)
{
(void)v; (void)ctx;
g_reader_active = 1;
if (g_book) { reader_refill_page(); chapter_name(); }
reader_redraw();
}
static void reader_on_leave(meow_view *v, void *ctx)
{
(void)v; (void)ctx;
reader_reflow_persist(); /* 离开保存进度 */
g_reader_active = 0;
}
static void reader_on_destroy(meow_view *v, void *ctx)
{
(void)v; (void)ctx;
if (g_book) rd_book_close(g_book);
g_book = NULL;
reader_free_page_bufs();
g_menu_open = 0; g_reader_active = 0;
}
meow_view *reader_view_create(void)
{
static meow_view_hooks hooks = { reader_on_create, reader_on_enter, reader_on_leave,
reader_on_destroy, NULL, NULL, NULL };
meow_view *v = meow_view_create(ACT_READER, MEOW_VIEW_ACTIVITY, &hooks, NULL);
g_canvas = meow_widget_custom_create(0, 0, 480, 720, reader_canvas_render, NULL, NULL);
meow_view_add_widget(v, g_canvas);
return v;
}
int reader_is_active(void) { return g_reader_active; }
void reader_on_tap(int x, int y)
{
if (!g_book) { return; }
if (g_menu_open) {
/* 菜单按钮命中 */
int y0 = g_ph - RD_MENU_H;
if (y >= y0) {
int bw = g_pw / RD_MENU_BTN;
int idx = x / bw;
switch (idx) {
case 0: /* 返回书架 */
g_menu_open = 0;
reader_reflow_persist();
meow_view_page_pop();
return;
case 1: if (g_fs > 14) { g_fs -= 2; reader_reflow(); } break;
case 2: if (g_fs < 40) { g_fs += 2; reader_reflow(); } break;
case 3: if (g_ls > 100) { g_ls -= 5; reader_reflow(); } break;
case 4: if (g_ls < 200) { g_ls += 5; reader_reflow(); } break;
}
if (y >= y0) return;
}
g_menu_open = 0; /* 点菜单外关闭 */
reader_redraw();
return;
}
/* 三栏:左 1/3 上一页、右 1/3 下一页、中央唤菜单 */
if (x < g_pw / 3) { rd_book_turn(g_book, -1); reader_refill_page(); reader_redraw(); }
else if (x > g_pw * 2 / 3) { rd_book_turn(g_book, 1); reader_refill_page(); reader_redraw(); }
else { g_menu_open = 1; reader_redraw(); }
}
void reader_on_swipe(int dir, int dx, int dy)
{
(void)dx; (void)dy;
if (!g_book) return;
if (g_menu_open) { g_menu_open = 0; reader_redraw(); return; }
if (dir == MEOW_SWIPE_LEFT) { rd_book_turn(g_book, 1); }
else if (dir == MEOW_SWIPE_RIGHT) { rd_book_turn(g_book, -1); }
else return;
reader_refill_page(); reader_redraw();
}
void reader_on_tick(uint64_t now)
{
(void)now;
if (g_notice_ms > 0) { g_notice_ms--; if (g_notice_ms == 0) reader_redraw(); }
}
+184
View File
@@ -0,0 +1,184 @@
/* app/shelf.c — 书架:扫 TF 卡 `Books/`,识别 TXT/FB2/EPUB/MOBI/CBZ(R1 仅 TXT 可开,
* 其它显示「R2 支持」占位);卡片列表 + 最近阅读置顶(按 progress ts 降序)。
* 用 W_LISTVIEW(结构化列表)+ App 自绘标题。空态=清屏提示。
*/
#include "../reader_internal.h"
#include "progress.h"
#include <dirent.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#define SHELF_LIST_X 0
#define SHELF_LIST_Y 64
#define SHELF_LIST_W 480
#define SHELF_LIST_H (720 - 64)
typedef struct shelf_book {
char path[400];
char title[128];
char sub[160];
int fmt; /* 0 txt,1 fb2,2 epub,3 mobi,4 cbz */
long size;
uint64_t ts; /* 最近阅读 unix;无=0 */
} shelf_book;
static shelf_book g_books[256];
static int g_nbooks;
static int g_active;
static meow_widget *g_list;
static uint32_t g_bg, g_fg, g_dim;
static char g_notice[160]; /* 空态/提示 */
static int g_notice_ms; /* 剩余显示 ms(0=无) */
static const char *fmt_name(int f)
{
switch (f) { case 0: return "TXT"; case 1: return "FB2"; case 2: return "EPUB";
case 3: return "MOBI"; case 4: return "CBZ"; default: return "?"; }
}
static int fmt_from_ext(const char *name)
{
const char *d = strrchr(name, '.');
if (!d) return -1;
if (strcasecmp(d, ".txt") == 0) return 0;
if (strcasecmp(d, ".fb2") == 0) return 1;
if (strcasecmp(d, ".epub") == 0) return 2;
if (strcasecmp(d, ".mobi") == 0) return 3;
if (strcasecmp(d, ".cbz") == 0) return 4;
return -1;
}
static void fmt_size(char *out, int outsz, long s)
{
if (s >= 1 << 20) snprintf(out, outsz, "%.1fMB", (double)s / (1 << 20));
else if (s >= 1 << 10) snprintf(out, outsz, "%.0fKB", (double)s / (1 << 10));
else snprintf(out, outsz, "%ldB", s);
}
static int cmp_recent_then_name(const void *a, const void *b)
{
const shelf_book *x = (const shelf_book *)a, *y = (const shelf_book *)b;
if (x->ts != y->ts) return (x->ts < y->ts) ? 1 : -1; /* 最近在前 */
return strcasecmp(x->title, y->title);
}
static void scan_books(void)
{
const char *dir = READER_SD_ROOT "/Books";
g_nbooks = 0;
DIR *dp = opendir(dir);
if (!dp) {
snprintf(g_notice, sizeof g_notice, "未找到 %s 目录,请放入书籍(TXT)", dir);
g_notice_ms = 4000;
return;
}
struct dirent *de;
while ((de = readdir(dp)) && g_nbooks < 256) {
if (de->d_name[0] == '.') continue;
int f = fmt_from_ext(de->d_name);
if (f < 0) continue;
shelf_book *b = &g_books[g_nbooks];
if (snprintf(b->path, sizeof b->path, "%s/%s", dir, de->d_name) >= (int)sizeof b->path) continue;
strncpy(b->title, de->d_name, sizeof b->title - 1); b->title[sizeof b->title - 1] = 0;
char *dot = strrchr(b->title, '.');
if (dot) *dot = 0; /* 标题去扩展名 */
b->fmt = f;
struct stat st;
b->size = (stat(b->path, &st) == 0) ? (long)st.st_size : 0;
rdprog p;
b->ts = (rdprog_load(b->path, &p) == 1) ? p.ts : 0;
char sz[24]; fmt_size(sz, sizeof sz, b->size);
snprintf(b->sub, sizeof b->sub, "%s · %s%s", fmt_name(f), sz,
b->ts ? " · 读" : "");
g_nbooks++;
}
closedir(dp);
qsort(g_books, (size_t)g_nbooks, sizeof(shelf_book), cmp_recent_then_name);
}
/* listview 行源 */
static int shelf_list_src(int idx, meow_list_item *out, void *ctx)
{
(void)ctx;
if (idx < 0 || idx >= g_nbooks) return 0;
const shelf_book *b = &g_books[idx];
out->icon_kind = MEOW_LIST_ICON_FILE;
out->title = b->title;
out->subtitle = b->sub;
out->right = MEOW_LIST_RIGHT_ARROW;
out->enabled = 1;
out->on = 0;
return 1;
}
static void shelf_list_tapcb(int idx, int op, void *ctx)
{
(void)op; (void)ctx;
if (idx < 0 || idx >= g_nbooks) return;
const shelf_book *b = &g_books[idx];
if (b->fmt == 0) { reader_open(b->path); }
else { snprintf(g_notice, sizeof g_notice, "%s 格式 R2 支持(R1 先看 TXT)", fmt_name(b->fmt)); g_notice_ms = 2500; }
}
/* view 生命周期 */
static void shelf_on_create(meow_view *v, void *ctx) { (void)v; (void)ctx; }
static void shelf_on_enter(meow_view *v, void *ctx)
{
(void)v; (void)ctx;
g_active = 1;
scan_books(); /* 回到书架即重扫(新增/最近读置顶刷新) */
const meow_theme *th = meow_theme_get();
g_bg = th->bg; g_fg = th->fg; g_dim = th->dim;
/* 无控件时重建列表(首次/回到书架) */
if (g_list) { meow_widget_free(g_list); g_list = NULL; }
g_list = meow_listview_create(SHELF_LIST_X, SHELF_LIST_Y, SHELF_LIST_W, SHELF_LIST_H,
g_nbooks, shelf_list_src, NULL, MEOW_LIST_SEL_SINGLE);
meow_listview_set_on_tap(g_list, shelf_list_tapcb, NULL);
shelf_redraw();
}
static void shelf_on_leave(meow_view *v, void *ctx) { (void)v; (void)ctx; g_active = 0; }
static void shelf_on_destroy(meow_view *v, void *ctx)
{
(void)v; (void)ctx;
if (g_list) { meow_widget_free(g_list); g_list = NULL; }
}
meow_view *shelf_view_create(void)
{
static meow_view_hooks hooks = { shelf_on_create, shelf_on_enter, shelf_on_leave,
shelf_on_destroy, NULL, NULL, NULL };
return meow_view_create(ACT_SHELF, MEOW_VIEW_ACTIVITY, &hooks, NULL);
}
int shelf_is_active(void) { return g_active; }
void shelf_redraw(void)
{
const meow_theme *th = meow_theme_get();
g_bg = th->bg; g_fg = th->fg; g_dim = th->dim;
meow_ui_clear(g_bg);
meow_ui_text(20, 22, "书架", 28, g_fg);
/* 副标题:格式化说明 */
char sub[96]; snprintf(sub, sizeof sub, "Books/ · %d 本", g_nbooks);
meow_ui_text(20, 24 + 34, sub, 20, g_dim);
if (g_list) meow_widget_draw(g_list);
if (g_nbooks == 0) {
meow_ui_text(30, 320, "书架为空:请往 TF 卡 Books/ 放书籍", 20, g_dim);
meow_ui_text(30, 348, "支持 TXT(可读) / FB2 / EPUB / MOBI / CBZ(R2)", 20, g_dim);
}
if (g_notice_ms > 0) {
meow_ui_fill_rect(0, 620, 480, 44, g_bg);
meow_ui_text(20, 630, g_notice, 20, th->primary);
}
meow_ui_flush();
}
int shelf_on_tap(int x, int y)
{
if (g_list && g_nbooks > 0) {
int idx = meow_listview_tap_at(g_list, x, y);
if (idx >= 0) { shelf_redraw(); return 1; } /* 选中已由 SDK 标脏;重绘 */
}
return 0;
}
+93
View File
@@ -0,0 +1,93 @@
#!/bin/bash
# build_reader.sh — os.meow.reader R1 可复现构建:vendor FreeType(mipsel musl 静态) → 链 reader 二进制
# + 宿主 core 单测 + qemu-mipsel FreeType 冒烟。
#
# 产物:
# reader mipsel 静态 ELF(链 meow_ui.a + FreeType + core + engine)
# test/test_reader_core_host 宿主 core 单测(enc/layout/kinsoku/page/reflow/txt)
# test/rdft_smoke_mipsel qemu-mipsel FreeType 封装冒烟
#
# 约束:只写本目录(patch_v2/reader_app/);不改 sdk/;-Wall -Wextra 0 warning。
# vendor/(FreeType 源码+构建产物)gitignore,不入仓;build 脚本可复现。
set -e
cd "$(dirname "$0")"
ROOT="$HOME/Project/R3ProII"
CC="$ROOT/.tools/zig-mipsel-cc"
SYSROOT="$ROOT/.tools/mipsel-linux-muslsf-cross/mipsel-linux-muslsf"
HOST_CC="${HOST_CC:-gcc}"
FT_VER="2.14.3"
VENDOR="$PWD/vendor"
FT_SRC="$VENDOR/freetype-src"
FT_BUILD="$VENDOR/freetype-build"
FT_INC_SRC="$FT_SRC/include"
FT_INC_BUILD="$FT_BUILD/include"
# ---------- 1) FreeType vendor 拉取 ----------
mkdir -p "$VENDOR"
fetch_freetype() {
# 优先级:env FREETYPE_SRC → 本目录 vendor 已存在 → /tmp/freetype → 下载
if [ -n "$FREETYPE_SRC" ] && [ -f "$FREETYPE_SRC/CMakeLists.txt" ]; then
cp -r "$FREETYPE_SRC" "$FT_SRC"
elif [ ! -d "$FT_SRC" ] && [ -f /tmp/freetype/CMakeLists.txt ]; then
cp -r /tmp/freetype "$FT_SRC"
elif [ ! -d "$FT_SRC" ]; then
echo "==> downloading freetype $FT_VER"
rm -rf "$FT_SRC"
( cd "$VENDOR" && curl -fsSL -o ft.tar.xz \
"https://download.savannah.gnu.org/releases/freetype/freetype-$FT_VER.tar.xz" \
&& mkdir -p "$FT_SRC" && tar xf ft.tar.xz -C "$FT_SRC" --strip-components=1 && rm ft.tar.xz )
fi
[ -f "$FT_SRC/CMakeLists.txt" ] || { echo "FREETYPE_SRC 不可用/下载失败"; exit 1; }
}
fetch_freetype
# ---------- 2) FreeType mipsel 交叉静态构建(禁用可选项,只要最简) ----------
if [ ! -f "$FT_BUILD/libfreetype.a" ] || [ -n "$FORCE_FT" ]; then
rm -rf "$FT_BUILD"
cmake -S "$FT_SRC" -B "$FT_BUILD" \
-DCMAKE_SYSTEM_NAME=Linux \
-DCMAKE_C_COMPILER="$CC" \
-DCMAKE_TRY_COMPILE_TARGET_TYPE=STATIC_LIBRARY \
-DCMAKE_BUILD_TYPE=Release \
-DBUILD_SHARED_LIBS=OFF \
-DFT_DISABLE_ZLIB=TRUE -DFT_DISABLE_BZIP2=TRUE -DFT_DISABLE_PNG=TRUE \
-DFT_DISABLE_HARFBUZZ=TRUE -DFT_DISABLE_BROTLI=TRUE \
>/dev/null
cmake --build "$FT_BUILD" -j"$(nproc)" >/dev/null 2>&1
fi
[ -f "$FT_BUILD/libfreetype.a" ] || { echo "FreeType build FAILED"; exit 1; }
FT_INCS="-I$FT_BUILD -I$FT_INC_BUILD -I$FT_INC_SRC"
echo "==> freetype.a: $(du -h "$FT_BUILD/libfreetype.a" | cut -f1)"
# ---------- 3) 宿主 core 单测 ----------
echo "==> host core test"
$HOST_CC -O2 -Wall -Wextra -Icore -I. test/test_reader_core.c \
core/rdenc.c core/rdlayout.c core/rdtxt.c -o test/test_reader_core_host -lm
./test/test_reader_core_host
# ---------- 4) qemu-mipsel FreeType 冒烟 ----------
echo "==> mipsel ft smoke"
$CC -Os -static -Wall -Wextra -Iengine $FT_INCS test/rdft_smoke.c engine/rdft.c \
"$FT_BUILD/libfreetype.a" -o test/rdft_smoke_mipsel
timeout 120 qemu-mipsel ./test/rdft_smoke_mipsel "${FONT:-$ROOT/font/src/MiSans-Regular.otf}"
# ---------- 4b) qemu-mipsel 书桥全链路冒烟 ----------
echo "==> mipsel book smoke"
$CC -Os -static -Wall -Wextra -Iapp -Icore -Iengine $FT_INCS test/book_smoke.c \
app/book.c core/rdenc.c core/rdlayout.c core/rdtxt.c engine/rdft.c \
"$FT_BUILD/libfreetype.a" -o test/book_smoke_mipsel
timeout 120 qemu-mipsel ./test/book_smoke_mipsel "${FONT:-$ROOT/font/src/MiSans-Regular.otf}"
# ---------- 5) reader 二进制(mipsel 静态) ----------
echo "==> reader binary"
CORE="core/rdenc.c core/rdlayout.c core/rdtxt.c"
ENGINE="engine/rdft.c"
APP="app.c app/shelf.c app/reader.c app/book.c app/progress.c"
$CC -Os -static -Wall -Wextra -I../sdk -I. -Icore -Iapp -Iengine $FT_INCS \
$CORE $APP $ENGINE \
../sdk/meow_ui.a "$SYSROOT/lib/libz.a" "$FT_BUILD/libfreetype.a" -lpthread -o reader
ls -la reader
file reader
echo " 运行(qemu): qemu-mipsel ./reader"
echo "build_reader OK"
File diff suppressed because it is too large. Load diff
+274
View File
@@ -0,0 +1,274 @@
/* rdenc.c — 文本编码检测 + GB18030→UTF-8 + UTF-8 工具(实现见 rdenc.h)。 */
#include "rdenc.h"
#include "gb18030_table.h"
#include <stdlib.h>
#include <string.h>
/* ================= UTF-8 严格校验 ================= */
int rd_utf8_validate(const uint8_t *buf, size_t len)
{
size_t i = 0;
while (i < len) {
uint8_t b = buf[i];
if (b < 0x80) { i++; continue; }
unsigned n, lo, hi;
if ((b & 0xE0) == 0xC0) { n = 2; lo = 0x80; hi = 0xBF; }
else if ((b & 0xF0) == 0xE0) { n = 3; lo = 0x80; hi = 0xBF; }
else if ((b & 0xF8) == 0xF0) { n = 4; lo = 0x80; hi = 0xBF; }
else return 0;
/* 拒绝 overlong / 代理码 / 超 0x10FFFF */
if (n == 2 && (b & 0xFE) == 0xC0) return 0; /* C0/C1 overlong */
if (n == 3 && b == 0xE0) lo = 0xA0; /* 防 overlong < U+0800 */
if (n == 3 && b == 0xED) hi = 0x9F; /* 防代理 U+D800..DFFF */
if (n == 4 && b == 0xF0) lo = 0x90; /* 防 overlong < U+10000 */
if (n == 4 && b == 0xF4) hi = 0x8F; /* 防 > U+10FFFF */
if (i + n > len) return 0;
for (unsigned k = 1; k < n; k++) {
uint8_t c = buf[i + k];
uint8_t cl = (k == 1) ? (uint8_t)lo : 0x80;
uint8_t ch = (k == 1) ? (uint8_t)hi : 0xBF;
if (c < cl || c > ch) return 0;
}
i += n;
}
return 1;
}
/* ================= GB18030 双字节 → UTF-8 ================= */
static inline int gb_trail_ok(unsigned b)
{
return (b >= 0x40 && b <= 0x7E) || (b >= 0x80 && b <= 0xFE);
}
static inline int gb_col(unsigned b)
{
/* 列 = b - 0x40 - (b >= 0x80);b 已在合法范围(不含 0x7F) */
return (int)b - 0x40 - (b >= 0x80);
}
/* 1-3 字节 UTF-8 编码,返回字节数。cp 为 BMP(≤0xFFFF)。 */
static int enc_utf8(uint32_t cp, uint8_t *o)
{
if (cp <= 0x7F) { o[0] = (uint8_t)cp; return 1; }
if (cp <= 0x7FF) {
o[0] = (uint8_t)(0xC0 | (cp >> 6));
o[1] = (uint8_t)(0x80 | (cp & 0x3F));
return 2;
}
o[0] = (uint8_t)(0xE0 | (cp >> 12));
o[1] = (uint8_t)(0x80 | ((cp >> 6) & 0x3F));
o[2] = (uint8_t)(0x80 | (cp & 0x3F));
return 3;
}
int rd_cp_to_utf8(uint32_t cp, uint8_t out[4])
{
if (cp <= 0x7F) { out[0] = (uint8_t)cp; return 1; }
if (cp <= 0x7FF) {
out[0] = (uint8_t)(0xC0 | (cp >> 6));
out[1] = (uint8_t)(0x80 | (cp & 0x3F));
return 2;
}
if (cp <= 0xFFFF) {
out[0] = (uint8_t)(0xE0 | (cp >> 12));
out[1] = (uint8_t)(0x80 | ((cp >> 6) & 0x3F));
out[2] = (uint8_t)(0x80 | (cp & 0x3F));
return 3;
}
out[0] = (uint8_t)(0xF0 | (cp >> 18));
out[1] = (uint8_t)(0x80 | ((cp >> 12) & 0x3F));
out[2] = (uint8_t)(0x80 | ((cp >> 6) & 0x3F));
out[3] = (uint8_t)(0x80 | (cp & 0x3F));
return 4;
}
int rd_gb18030_to_utf8(const uint8_t *in, size_t inlen, uint8_t *out, size_t outcap, size_t *outlen)
{
if (!in || !outlen) return RD_ENC_ERR_INVAL;
if (!out && outcap) return RD_ENC_ERR_INVAL;
size_t o = 0;
size_t i = 0;
while (i < inlen) {
uint8_t b = in[i];
if (b < 0x80) {
if (o + 1 > outcap) { *outlen = o + 1; return RD_ENC_ERR_BUFSZ; }
out[o++] = b; i++; continue;
}
if (b >= 0x81 && b <= 0xFE && i + 1 < inlen) {
uint8_t t = in[i + 1];
if (gb_trail_ok(t)) {
uint32_t cp = gb_2byte[b - RD_GB_LEAD0][gb_col(t)];
uint8_t tmp[4];
int n;
if (cp == 0) { cp = 0xFFFD; }
n = enc_utf8(cp, tmp);
if (o + (size_t)n > outcap) { *outlen = o + (size_t)n; return RD_ENC_ERR_BUFSZ; }
memcpy(out + o, tmp, (size_t)n); o += (size_t)n;
i += 2; continue;
}
if (t >= 0x30 && t <= 0x39) { /* 4 字节扩展段 → 跳 4 字节 + U+FFFD */
if (i + 4 > inlen) { /* 残缺:只跳可用部分 */
uint8_t tmp[4]; int n = enc_utf8(0xFFFD, tmp);
if (o + (size_t)n > outcap) { *outlen = o + (size_t)n; return RD_ENC_ERR_BUFSZ; }
memcpy(out + o, tmp, (size_t)n); o += (size_t)n;
i = inlen; break;
}
uint8_t tmp[4]; int n = enc_utf8(0xFFFD, tmp);
if (o + (size_t)n > outcap) { *outlen = o + (size_t)n; return RD_ENC_ERR_BUFSZ; }
memcpy(out + o, tmp, (size_t)n); o += (size_t)n;
i += 4; continue;
}
}
/* 非法首字节 / 尾字节不成对 → U+FFFD */
{
uint8_t tmp[4]; int n = enc_utf8(0xFFFD, tmp);
if (o + (size_t)n > outcap) { *outlen = o + (size_t)n; return RD_ENC_ERR_BUFSZ; }
memcpy(out + o, tmp, (size_t)n); o += (size_t)n;
i++; continue;
}
}
*outlen = o;
return RD_ENC_OK;
}
/* ================= 编码检测 ================= */
static inline int gb_trail_q(unsigned b)
{
return (b >= 0x40 && b <= 0x7E) || (b >= 0x80 && b <= 0xFE);
}
static inline int big5_trail_q(unsigned b)
{
return (b >= 0x40 && b <= 0x7E) || (b >= 0xA1 && b <= 0xFE);
}
rd_enc_t rd_enc_detect(const uint8_t *buf, size_t len, rd_enc_stats *st)
{
if (st) { st->gb_pairs = st->big5_pairs = st->stray = 0; }
if (!buf || len == 0) return RD_ENC_ASCII;
/* 1) BOM */
if (len >= 3 && buf[0] == 0xEF && buf[1] == 0xBB && buf[2] == 0xBF) return RD_ENC_UTF8;
if (len >= 2 && buf[0] == 0xFF && buf[1] == 0xFE) return RD_ENC_UTF16LE;
if (len >= 2 && buf[0] == 0xFE && buf[1] == 0xFF) return RD_ENC_UTF16BE;
/* 2) 严格 UTF-8 */
if (rd_utf8_validate(buf, len)) {
/* 纯 ASCII 判 ASCII(信息性;UTF-8 子集) */
int ascii = 1;
for (size_t i = 0; i < len; i++) if (buf[i] >= 0x80) { ascii = 0; break; }
return ascii ? RD_ENC_ASCII : RD_ENC_UTF8;
}
/* 3) 双字节区段统计(GB18030 兼容 GBK;Big5 尾字节不同)。
* GB 有效 = 码表 gb_2byte 查到非 0(GBK/GB18030 已映射码位);
* Big5 专属 = 结构上 Big5 合法、但对 GB 码表无效(Big5 独有码位)。
* 二者比较更可靠(结构上每个 GBK 双字节对也是合法 Big5 结构,无法区分)。 */
long gb = 0, big5 = 0, stray = 0;
for (size_t i = 0; i < len; ) {
uint8_t b = buf[i];
if (b < 0x80) { i++; continue; }
if (b >= 0x81 && b <= 0xFE && i + 1 < len) {
uint8_t t = buf[i + 1];
int gb_ok = gb_trail_q(t) &&
gb_2byte[b - RD_GB_LEAD0][gb_col(t)] != 0;
int b5_ok = big5_trail_q(t);
if (gb_ok) gb++;
if (b5_ok && !gb_ok) big5++; /* Big5 专属:GB 码表无效 */
i += 2; continue;
}
stray++; i++;
}
if (st) { st->gb_pairs = gb; st->big5_pairs = big5; st->stray = stray; }
/* 阈值(经验值):双字节对太少 → 乱码/不确定 → 兜底 GB18030(最常见中文编码)。
* 注(READER_ENGINE_NOTES §3.5 风险):Big5 与 GB18030 双字节区大量重叠,码表有效性
* 无法可靠区分(简体 GB2312 文本 trail 0xA1-0xFE 恰是 Big5 trail 区)。R1 双字节一律
* 按 GB18030 兜底;Big5 经此检测也按 GB18030 试(v1 不转),真实区分需用户手动切编码
* (列 R2)。 */
if (gb + big5 < (long)(len / 5)) return RD_ENC_GB18030;
return RD_ENC_GB18030;
}
/* ================= UTF-16 → UTF-8(简易,BOM 已对齐) ================= */
static int utf16_to_utf8(rd_enc_t enc, const uint8_t *in, size_t inlen,
uint8_t *out, size_t outcap, size_t *outlen)
{
int be = (enc == RD_ENC_UTF16BE);
/* 跳过 BOM(2 字节) */
size_t i = 0;
if (inlen >= 2 && ((be && in[0] == 0xFE && in[1] == 0xFF) ||
(!be && in[0] == 0xFF && in[1] == 0xFE))) i = 2;
size_t o = 0;
while (i + 1 < inlen) {
uint32_t u = be ? (((uint32_t)in[i] << 8) | in[i + 1])
: (((uint32_t)in[i + 1] << 8) | in[i]);
i += 2;
uint32_t cp = u;
if (u >= 0xD800 && u <= 0xDBFF) { /* 高代理 */
if (i + 1 < inlen) {
uint32_t u2 = be ? (((uint32_t)in[i] << 8) | in[i + 1])
: (((uint32_t)in[i + 1] << 8) | in[i]);
if (u2 >= 0xDC00 && u2 <= 0xDFFF) {
cp = 0x10000 + ((u - 0xD800) << 10) + (u2 - 0xDC00);
i += 2;
} else cp = 0xFFFD;
} else cp = 0xFFFD;
} else if (u >= 0xDC00 && u <= 0xDFFF) cp = 0xFFFD; /* 孤立低代理 */
uint8_t tmp[4];
int n = rd_cp_to_utf8(cp, tmp);
if (o + (size_t)n > outcap) { *outlen = o + (size_t)n; return RD_ENC_ERR_BUFSZ; }
memcpy(out + o, tmp, (size_t)n); o += (size_t)n;
}
*outlen = o;
return RD_ENC_OK;
}
int rd_to_utf8(rd_enc_t enc, const uint8_t *in, size_t inlen, uint8_t *out, size_t outcap, size_t *outlen)
{
if (enc == RD_ENC_UTF16LE || enc == RD_ENC_UTF16BE) return utf16_to_utf8(enc, in, inlen, out, outcap, outlen);
if (enc == RD_ENC_UTF8 || enc == RD_ENC_ASCII) {
/* 去 UTF-8 BOM(若有) */
size_t off = 0;
if (inlen >= 3 && in[0] == 0xEF && in[1] == 0xBB && in[2] == 0xBF) off = 3;
if (inlen - off > outcap) { *outlen = inlen - off; return RD_ENC_ERR_BUFSZ; }
memcpy(out, in + off, inlen - off); *outlen = inlen - off;
return RD_ENC_OK;
}
/* GB18030 / Big5(v1 按 GB18030 试) */
return rd_gb18030_to_utf8(in, inlen, out, outcap, outlen);
}
/* ================= UTF-8 → 码点数组 ================= */
int rd_utf8_to_cp(const uint8_t *utf8, size_t len, uint32_t **cps, size_t *n)
{
if (!utf8 || !cps || !n) return RD_UTF8_ERR_INVAL;
*cps = NULL; *n = 0;
/* 上界:每个输入字节至多一个码点(单字节 ASCII / 非法的每字节一个 U+FFFD)。 */
uint32_t *arr = (uint32_t *)malloc((len ? len : 1) * sizeof(uint32_t));
if (!arr) return RD_UTF8_ERR_NOMEM;
size_t cnt = 0, i = 0;
while (i < len) {
uint8_t b = utf8[i];
if (b < 0x80) { arr[cnt++] = b; i++; continue; }
unsigned q, lo, hi;
if ((b & 0xE0) == 0xC0) { q = 2; lo = 0x80; hi = 0xBF; }
else if ((b & 0xF0) == 0xE0) { q = 3; lo = 0x80; hi = 0xBF; }
else if ((b & 0xF8) == 0xF0) { q = 4; lo = 0x80; hi = 0xBF; }
else { arr[cnt++] = 0xFFFD; i++; continue; }
if (q == 2 && (b & 0xFE) == 0xC0) { arr[cnt++] = 0xFFFD; i++; continue; }
if (q == 3 && b == 0xE0) lo = 0xA0;
if (q == 3 && b == 0xED) hi = 0x9F;
if (q == 4 && b == 0xF0) lo = 0x90;
if (q == 4 && b == 0xF4) hi = 0x8F;
if (i + q > len) { arr[cnt++] = 0xFFFD; i++; continue; }
uint32_t cp = b & (0xFF >> (q + 1));
int bad = 0;
for (unsigned k = 1; k < q; k++) {
uint8_t c = utf8[i + k];
uint8_t cl = (k == 1) ? (uint8_t)lo : 0x80, ch = (k == 1) ? (uint8_t)hi : 0xBF;
if (c < cl || c > ch) { bad = 1; break; }
cp = (cp << 6) | (c & 0x3F);
}
if (bad) { arr[cnt++] = 0xFFFD; i++; continue; }
arr[cnt++] = cp; i += q;
}
*cps = arr; *n = cnt;
return RD_ENC_OK;
}
+68
View File
@@ -0,0 +1,68 @@
/* rdenc.h — 阅读器文本编码检测 + GB18030→UTF-8 解码 + UTF-8 工具(纯 C,宿主可测)。
*
* 对齐 devdocs/plan/READER_ENGINE_NOTES.md §3(zLibrary 轻量化:BOM + 严格 UTF-8 + 双字节区段统计,
* 不抄 N-gram 马尔可夫):
* - BOM 优先:FE FF=UTF-16BE、FF FE=UTF-16LE、EF BB BF=UTF-8;
* - 无 BOM → 严格 UTF-8 校验(合法即 UTF-8;ASCII 属 UTF-8 子集);
* - 非 UTF-8 → GB18030(GBK 超集) / Big5 双字节区段统计(阈值经验值,见 READER_ENGINE_NOTES §3.3);
* - **R1 只做 GB18030→UTF-8 双字节映射**(uint16[126][190] 码表),4 字节扩展段 → U+FFFD;
* Big5 检测出来也按 GB18030 试(v1 不转 Big5,正文会乱但可读,注明)。
*
* 无 SDK 依赖;`#include "gb18030_table.h"`(生成物,见 tools/gen_gb18030_table.py)。
*/
#ifndef RDENC_H
#define RDENC_H
#include <stddef.h>
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
typedef enum {
RD_ENC_ASCII = 0, /* 纯 7-bit,无非法字节(UTF-8 子集) */
RD_ENC_UTF8,
RD_ENC_UTF16LE,
RD_ENC_UTF16BE,
RD_ENC_GB18030, /* GBK 超集(解码器按 2 字节码表) */
RD_ENC_BIG5, /* v1 检测但按 GB18030 解码 */
} rd_enc_t;
/* 检测统计(诊断 + 阈值用;值可为 0)。 */
typedef struct rd_enc_stats {
long gb_pairs; /* 合法 GB18030/GBK 双字节对计数 */
long big5_pairs; /* 合法 Big5 双字节对计数 */
long stray; /* 0x80..0xFF 中无法成对的散落字节 */
} rd_enc_stats;
/* 编码检测:对 buf[0..len)(建议前 64KB)返回最可能编码,并填 st(可 NULL)。 */
rd_enc_t rd_enc_detect(const uint8_t *buf, size_t len, rd_enc_stats *st);
/* 严格 UTF-8 校验:结构非法即返回 0;合法返回 1(允许含 NUL?不,遇 NUL 提前停)。 */
int rd_utf8_validate(const uint8_t *buf, size_t len);
/* GB18030(GBK) 双字节 → UTF-8。成功 0;输出不足 RD_ENC_ERR_BUFSZ 且 *outlen=所需;非法字节→U+FFFD。
* 4 字节扩展段(lead+0x30..0x39 开头)→ 跳过该 4 字节并输出一个 U+FFFD。 */
#define RD_ENC_OK 0
#define RD_ENC_ERR_INVAL -1 /* 参数非法 */
#define RD_ENC_ERR_BUFSZ -2 /* out 不足;*outlen 已填所需 */
int rd_gb18030_to_utf8(const uint8_t *in, size_t inlen, uint8_t *out, size_t outcap, size_t *outlen);
/* UTF-8 → 码点数组(布局用;malloc 分配 *cps,调用方 free)。成功 0。 *n = 码点数。
* 非法序列按一个 U+FFFD 收(宽松);len 为输入字节长。 */
#define RD_UTF8_ERR_INVAL -1
#define RD_UTF8_ERR_NOMEM -2
int rd_utf8_to_cp(const uint8_t *utf8, size_t len, uint32_t **cps, size_t *n);
/* 码点 → UTF-8(写 out,返回写入字节数;out 须 ≥4 字节)。 */
int rd_cp_to_utf8(uint32_t cp, uint8_t out[4]);
/* 通用:任意检测编码 → UTF-8(rdenc 已知的各编码)。UTF-16LE/BE 走简易 BOM 对齐 + 码点转换;
* Big5 v1 按 GB18030 双字节表转(会乱但可读,注明)。返回码同 rd_gb18030_to_utf8。 */
int rd_to_utf8(rd_enc_t enc, const uint8_t *in, size_t inlen, uint8_t *out, size_t outcap, size_t *outlen);
#ifdef __cplusplus
}
#endif
#endif /* RDENC_H */
+257
View File
@@ -0,0 +1,257 @@
/* rdlayout.c — 流式排版器 core(实现见 rdlayout.h)。纯 C,宿主可测,无 SDK/Freetype 依赖。
*
* 两遍:
* A) 贪心断行:逐段扫描,记录「最后一个允许断且能放下」的位置;溢出即断;
* Kinsoku 微调(左括号不居行尾 / 右标点不居行首,右标点簇 ≤2 上拉)。
* *连续* y(含段间距)写入 line.y——仅供分页阶段参考。
* B) 分页:按行高+段间距累计超内容高断页,得到 pages[](每页 = lines[] 区间 + 首字符偏移);
* 孤行守卫 post-pass:段首行不单独留页底(孤儿上推)。
*/
#include "rdlayout.h"
#include <stdint.h>
/* ---------- 字符属性 ---------- */
enum {
RD_CH_SPACE = 1u << 0,
RD_CH_CJK = 1u << 1,
RD_CH_HYPH = 1u << 2,
RD_CH_HARD = 1u << 3,
RD_CH_OPEN = 1u << 4, /* 左括号/前引号:行尾禁断 */
RD_CH_CLOSE = 1u << 5, /* 右标点:行首禁断 */
RD_CH_CJKPT = 1u << 6,
};
static int is_cjk_wide(uint32_t c)
{
if (c >= 0x2E80 && c <= 0x2EFF) return 1;
if (c >= 0x3040 && c <= 0x30FF) return 1;
if (c >= 0x3400 && c <= 0x4DBF) return 1;
if (c >= 0x4E00 && c <= 0x9FFF) return 1;
if (c >= 0xF900 && c <= 0xFAFF) return 1;
if (c >= 0xFF00 && c <= 0xFFEF) return 1;
if (c >= 0x20000) return 1;
return 0;
}
static int is_cjk_left_punct(uint32_t c)
{
switch (c) {
case 0x2018: case 0x201C:
case 0x3008: case 0x300A: case 0x300C: case 0x300E: case 0x3010:
case 0xFF08:
return 1;
default: return 0;
}
}
static int is_cjk_close_punct(uint32_t c)
{
switch (c) {
case 0x3001: case 0x3002:
case 0xFF01: case 0xFF0C: case 0xFF0E: case 0xFF1A: case 0xFF1B:
case 0xFF1F:
case 0x3009: case 0x300B: case 0x300D: case 0x300F: case 0x3011:
case 0xFF09:
case 0x2026:
return 1;
default: return 0;
}
}
static int is_latin_close(uint32_t c)
{
return c == ',' || c == '.' || c == ';' || c == ':' || c == '!' || c == '?' ||
c == ')' || c == ']' || c == '}' || c == '"' || c == '\'';
}
static int is_latin_open(uint32_t c)
{
return c == '(' || c == '[' || c == '{' || c == '"' || c == '\'';
}
static unsigned char_props(uint32_t c)
{
unsigned f = 0;
if (c == ' ') f |= RD_CH_SPACE;
else if (c == '\n') f |= RD_CH_HARD;
else if (c == 0xAD) f |= RD_CH_HYPH;
if (is_cjk_wide(c)) {
f |= RD_CH_CJK;
if (is_cjk_left_punct(c)) f |= RD_CH_CJKPT | RD_CH_OPEN;
else if (is_cjk_close_punct(c)) f |= RD_CH_CJKPT | RD_CH_CLOSE;
else if ((c >= 0x3000 && c <= 0x303F) || (c >= 0xFF00 && c <= 0xFFEF)) f |= RD_CH_CJKPT;
} else {
if (is_latin_open(c)) f |= RD_CH_OPEN;
else if (is_latin_close(c)) f |= RD_CH_CLOSE;
}
return f;
}
/* 在 chars[idx](即断点后一整段)后能否断开:左括号不居行尾、右标点不居行首。 */
static int wrap_ok_after(const uint32_t *ch, int pe, int idx)
{
if (idx < 0) return 0;
unsigned cur = char_props(ch[idx]);
if (cur & RD_CH_OPEN) return 0; /* 行尾禁:左括号/前引号 */
if (idx + 1 < pe) {
unsigned nxt = char_props(ch[idx + 1]);
if (nxt & RD_CH_CLOSE) return 0; /* 行首禁:右标点 */
}
return 1;
}
static int approx_adv(uint32_t cp, int fs)
{
if (cp == '\n' || cp == '\r') return 0;
if (cp == ' ' || cp == '\t') return fs * 2 / 3 < 1 ? 1 : fs * 2 / 3;
if (is_cjk_wide(cp)) return fs;
return fs / 2 < 1 ? 1 : fs / 2;
}
static int char_adv(const rd_layout_in *in, uint32_t cp)
{
int adv = 0;
if (in->meas && in->meas(cp, in->fs, &adv, in->mctx) == 0 && adv >= 0) return adv;
return approx_adv(cp, in->fs);
}
/* 段内断一行;返回该行后剩余段起点(=line_e;达到段末则 == pe)。 */
static int para_break_line(const rd_layout_in *in, int pe, int32_t *line_s_in,
int first_line_of_para, rd_line *ln)
{
int32_t line_s = *line_s_in;
int32_t pos = line_s;
int lastNormal = -1;
int x = 0;
int32_t i = pos;
while (i < pe) {
uint32_t c = in->chars[i];
int adv = char_adv(in, c);
if (x + adv > in->page_w && i > line_s) break; /* 溢出断 */
x += adv;
unsigned f = char_props(c);
if ((f & (RD_CH_SPACE | RD_CH_CJK | RD_CH_HYPH)) && wrap_ok_after(in->chars, pe, i)) lastNormal = i;
i++;
}
int32_t line_e;
if (i < pe) { /* 溢出 */
if (lastNormal >= line_s) line_e = lastNormal + 1;
else line_e = (i > line_s) ? i : (line_s + 1 < pe ? line_s + 1 : pe);
} else {
line_e = i; /* 段末自然结束 */
}
if (line_e <= line_s) line_e = (line_s + 1 < pe) ? line_s + 1 : pe;
/* Kinsoku 微调(v1 子集) */
while (line_e > line_s + 1 && is_cjk_left_punct(in->chars[line_e - 1])) line_e--; /* 左括号推下行 */
if (line_e < pe && is_cjk_close_punct(in->chars[line_e])) { /* 右标点上拉 ≤2 */
int cnt = 1;
while (line_e + cnt < pe && is_cjk_close_punct(in->chars[line_e + cnt])) cnt++;
int pull = cnt < 3 ? cnt : 2;
while (pull-- > 0 && line_e < pe) line_e++;
}
if (line_e <= line_s) line_e = (line_s + 1 < pe) ? line_s + 1 : pe;
/* 实际宽 */
int lw = 0;
for (int32_t k = line_s; k < line_e; k++) lw += char_adv(in, in->chars[k]);
if (lw > in->page_w) lw = in->page_w;
ln->s = line_s;
ln->e = line_e;
ln->x = first_line_of_para ? in->indent : 0;
ln->w = lw;
ln->para_start = first_line_of_para ? 1 : 0;
ln->para_end = (line_e >= pe) ? 1 : 0;
*line_s_in = line_e;
return line_e;
}
int rd_layout(const rd_layout_in *in, rd_layout_out *out)
{
if (!in || !out || !in->chars || in->n < 0 || !out->lines || !out->pages) return RD_LAYOUT_ERR_INVAL;
if (in->fs <= 0 || in->page_w <= 0 || in->page_h <= 0) return RD_LAYOUT_ERR_INVAL;
int lh = (in->fs * in->line_scale) / 100;
if (lh < 1) lh = 1;
int pspacing = lh / 2;
if (pspacing < 0) pspacing = 0;
int nlines = 0, npages = 0;
int y = 0; /* 连续 y(阶段 A;段间距已加) */
int para_pending = 0; /* 是否已有段落在前(需段间距) */
int32_t ps = 0;
while (ps < in->n) {
while (ps < in->n && (in->chars[ps] == '\n' || in->chars[ps] == '\r')) ps++;
if (ps >= in->n) break;
int32_t pe = ps;
while (pe < in->n && in->chars[pe] != '\n') pe++;
if (para_pending && nlines > 0) y += pspacing;
para_pending = 1;
int32_t pos = ps;
int first_line = 1;
while (pos < pe) {
rd_line ln;
if (nlines >= out->max_lines) return RD_LAYOUT_ERR_OVER;
para_break_line(in, pe, &pos, first_line, &ln);
ln.y = y;
out->lines[nlines++] = ln;
y += lh;
first_line = 0;
if (ln.para_end) break;
}
ps = pos;
while (ps < in->n && in->chars[ps] == '\n') ps++;
if (pos >= pe && ps >= in->n) break;
}
/* 阶段 B:分页 */
if (nlines == 0) { out->nlines = 0; out->npages = 0; return RD_LAYOUT_OK; }
int page_start = 0;
int cur_y = 0;
for (int l = 0; l < nlines; l++) {
int extra = (out->lines[l].para_start && l > page_start) ? pspacing : 0;
int h = lh + extra;
if (cur_y + h > in->page_h && l > page_start) {
int np_start = l;
/* 孤行守卫:上页最后一行是段首行且段未结束 → 该行随段推到下一页 */
if (l - 1 >= page_start && out->lines[l - 1].para_start && !out->lines[l - 1].para_end) {
np_start = l - 1;
}
if (npages >= out->max_pages) return RD_LAYOUT_ERR_OVER;
out->pages[npages].first_char = out->lines[page_start].s;
out->pages[npages].first_line = page_start;
out->pages[npages].nlines = np_start - page_start;
npages++;
page_start = np_start;
cur_y = 0;
}
/* 重新计算本行在本页的 top(若 page_start 因孤儿调整而变化,extra 需重算) */
extra = (out->lines[l].para_start && l > page_start) ? pspacing : 0;
out->lines[l].y = cur_y + extra;
cur_y += lh + extra;
}
if (nlines > page_start) {
if (npages >= out->max_pages) return RD_LAYOUT_ERR_OVER;
out->pages[npages].first_char = out->lines[page_start].s;
out->pages[npages].first_line = page_start;
out->pages[npages].nlines = nlines - page_start;
npages++;
}
out->nlines = nlines;
out->npages = npages;
return RD_LAYOUT_OK;
}
int rd_layout_page_for_anchor(const rd_layout_out *out, int32_t anchor_char)
{
if (!out || out->npages <= 0) return 0;
for (int p = 0; p < out->npages; p++) {
int32_t fc = out->pages[p].first_char;
int32_t nfc = (p + 1 < out->npages) ? out->pages[p + 1].first_char : INT32_MAX;
if (anchor_char < nfc && (anchor_char >= fc || p == 0)) return p;
}
return out->npages - 1;
}
+75
View File
@@ -0,0 +1,75 @@
/* rdlayout.h — 流式排版器 core:贪心断行 + Kinsoku 避头尾 + 分页 + 重排锚定。
*
* 对齐 devdocs/plan/READER_ENGINE_NOTES.md §1(crengine lvtextfm 思想,不链库):
* - 贪心断行(非 Knuth-Plass):断点候选 = 空格 / CJK / 软连字符;`\n` 强制换行;
* - Kinsoku 避头尾:字符属性(AVOID_WRAP_AFTER/BEFORE)+ CJK 标点区间(左括号不居行尾、
* 右标点不居行首,右标点簇 ≤2 位移优先上拉);
* - 分页:行高累计超内容高断页;孤行守卫(段首行不单独留页底)。
* - 测量由 `rd_meas_fn` 回调提供(设备=FreeType,宿主单测=桩),core 零 SDK/Freetype 依赖。
*
* 坐标系:输入 chars 为本章/分段码点数组;断行输出在「内容区」坐标(x=0..page_w-1,y=0..page_h-1,
* 外边距/版心外空由调用方裁掉)。首行缩进 = in->indent。
*/
#ifndef RDLAYOUT_H
#define RDLAYOUT_H
#include <stddef.h>
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
/* 测量:cp→advance 像素。返回 0=成功(*adv 已填,≥0)。 */
typedef int (*rd_meas_fn)(uint32_t cp, int fs_px, int *adv, void *ctx);
typedef struct rd_layout_in {
const uint32_t *chars; /* 本章/分段码点数组(调用方持有,layout 不拷贝) */
int32_t n; /* 码点数 */
int fs; /* 字号 px */
int line_scale; /* 行距百分比(100..200):LH = fs*line_scale/100 */
int page_w; /* 内容区宽 px */
int page_h; /* 内容区高 px */
int indent; /* 首行缩进 px */
rd_meas_fn meas; /* 测量回调(NULL → 用内置近似宽) */
void *mctx;
} rd_layout_in;
typedef struct rd_line {
int32_t s, e; /* 本行码点区间 [s,e)(s=行首,e=行尾+1,<= 段结束) */
int x; /* 行首 x(内容区坐标;首行=indent 否则 0) */
int y; /* 行顶 y(内容区坐标) */
int w; /* 已用行宽 px(≤ page_w) */
uint8_t para_start; /* 1=段的第一个物理行 */
uint8_t para_end; /* 1=段的最后一个物理行 */
} rd_line;
typedef struct rd_page {
int32_t first_char; /* 本页首字符在本章内的偏移(页索引锚) */
int first_line; /* lines[] 起始下标 */
int nlines; /* 本页行数 */
} rd_page;
typedef struct rd_layout_out {
rd_line *lines; /* 容量 in->...(调用方给) */
rd_page *pages; /* 容量 in->...(调用方给) */
int max_lines;
int max_pages;
int nlines;
int npages;
} rd_layout_out;
#define RD_LAYOUT_OK 0
#define RD_LAYOUT_ERR_INVAL -1
#define RD_LAYOUT_ERR_OVER -2 /* lines/pages 容量不足(npages/nlines 已填实际需要) */
int rd_layout(const rd_layout_in *in, rd_layout_out *out);
/* 重排锚定:在 layout 输出里找第一个满足 `line.s <= anchor_char < 下个 line.s` 的**行**所属的页。
* 返回页下标(0..npages-1);anchor_char 越界 clamp 到首/末页。 */
int rd_layout_page_for_anchor(const rd_layout_out *out, int32_t anchor_char);
#ifdef __cplusplus
}
#endif
#endif /* RDLAYOUT_H */
+116
View File
@@ -0,0 +1,116 @@
/* rdtxt.c — TXT 流式加载器(实现见 rdtxt.h)。 */
#include "rdtxt.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
struct rdtxt_reader {
FILE *f;
char path[320];
rd_enc_t enc;
uint8_t *raw; /* 原始字节块缓冲 */
size_t rawsz; /* 分配容量 */
int eof;
};
int rdtxt_probe(const char *path, rdtxt_info *info)
{
if (!path || !info) return -1;
FILE *f = fopen(path, "rb");
if (!f) return -1;
if (fseek(f, 0, SEEK_END) != 0) { fclose(f); return -2; }
long sz = ftell(f);
if (sz < 0) { fclose(f); return -2; }
fseek(f, 0, SEEK_SET);
uint8_t buf[65536];
size_t n = fread(buf, 1, sizeof buf, f);
rd_enc_stats st;
rd_enc_t enc = rd_enc_detect(buf, n, &st);
fclose(f);
info->enc = enc;
info->file_size = sz;
if (strlen(path) >= sizeof info->path) return -2;
strcpy(info->path, path);
return 0;
}
int rdtxt_reader_open(const char *path, const rdtxt_info *info, rdtxt_reader **r)
{
if (!path || !info || !r) return -1;
rdtxt_reader *x = (rdtxt_reader *)calloc(1, sizeof *x);
if (!x) return -6;
x->f = fopen(path, "rb");
if (!x->f) { free(x); return -1; }
x->raw = (uint8_t *)malloc(RDTXT_SEG_RAW_MAX + 1);
if (!x->raw) { fclose(x->f); free(x); return -6; }
x->rawsz = RDTXT_SEG_RAW_MAX + 1;
if (strlen(path) >= sizeof x->path) { free(x->raw); fclose(x->f); free(x); return -2; }
strcpy(x->path, path);
x->enc = info->enc;
x->eof = 0;
*r = x;
return 0;
}
void rdtxt_reader_free(rdtxt_reader *r)
{
if (!r) return;
if (r->f) fclose(r->f);
free(r->raw);
free(r);
}
const char *rdtxt_reader_path(const rdtxt_reader *r) { return r ? r->path : ""; }
rd_enc_t rdtxt_reader_enc(const rdtxt_reader *r) { return r ? r->enc : RD_ENC_UTF8; }
int rdtxt_reader_next(rdtxt_reader *r, uint8_t *utf8, size_t cap,
size_t *utf8len, size_t *file_off, size_t *file_len)
{
if (!r || !utf8 || !utf8len || !file_off || !file_len) return -1;
if (r->eof) return 1;
long start = ftell(r->f);
if (start < 0) { r->eof = 1; return 1; }
size_t raw_n = 0;
int got = 0;
while (raw_n < r->rawsz - 1) {
int c = fgetc(r->f);
if (c == EOF) { r->eof = 1; break; }
r->raw[raw_n++] = (uint8_t)c;
got = 1;
/* 段落边界达成目标字节约停止(`\n` 恒为独立换行) */
if (c == '\n' && raw_n >= RDTXT_SEG_UTF8_MAX / 2) break;
}
if (!got) { r->eof = 1; return 1; }
size_t outlen = 0;
int rc = rd_to_utf8(r->enc, r->raw, raw_n, utf8, cap, &outlen);
if (rc == RD_ENC_ERR_BUFSZ) return -2; /* 调用方 cap 太小 */
if (rc != RD_ENC_OK) return -3;
*utf8len = outlen;
*file_off = (size_t)start;
*file_len = raw_n;
return 0;
}
int rdtxt_convert_span(const char *path, rd_enc_t enc, size_t raw_off, size_t raw_len,
uint8_t *out, size_t cap, size_t *outlen)
{
if (!path || !out || !outlen) return -1;
FILE *f = fopen(path, "rb");
if (!f) return -1;
if (fseek(f, (long)raw_off, SEEK_SET) != 0) { fclose(f); return -2; }
/* 读整段(raw_len ≤ RDTXT_SEG_RAW_MAX,有界) */
size_t want = raw_len;
uint8_t *tmp = (uint8_t *)malloc(want ? want : 1);
if (!tmp) { fclose(f); return -6; }
size_t got = fread(tmp, 1, want, f);
fclose(f);
int rc = rd_to_utf8(enc, tmp, got, out, cap, outlen);
free(tmp);
return rc;
}
+56
View File
@@ -0,0 +1,56 @@
/* rdtxt.h — TXT 流式加载器(骨架):探测编码 → 按段流式读,不整文件入内存。
*
* 对齐 READER_ENGINE_NOTES §7 #4:读 `Books/` 下 .txt → 检测编码 → 转 UTF-8 → 按段喂排版器;
* 书签/进度锚定 (章节id,字符偏移)。TXT 段 = 以 `\n` 为界的段落流按目标字节数聚合,段 =
* 一个排版/渲染单元;每段记录 {file_off, file_len, utf8_len},渲染时按 file_off 重定位读回,
* 确定性一致(编码在文件头一次检测,分段/转换仅依赖该编码)。
*
* `\n`(0x0A) 在 GB18030/GBK/UTF-8/ASCII 中恒为独立控制符(尾字节域 0x40-0x7E|0x80-0xFE 不含 0x0A),
* 故可安全在原始字节层的换行边界分段。纯 C,宿主可测;无 SDK 依赖。
*/
#ifndef RDTXT_H
#define RDTXT_H
#include "rdenc.h"
#include <stddef.h>
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
/* 段 UTF-8 上限(内存纪律:单段 ≤ 此值;编码源另 ≤ RDTXT_SEG_RAW_MAX)。 */
#define RDTXT_SEG_UTF8_MAX (96 * 1024)
#define RDTXT_SEG_RAW_MAX (RDTXT_SEG_UTF8_MAX) /* 原始字节软上限 */
typedef struct rdtxt_info {
rd_enc_t enc;
long file_size;
char path[320];
} rdtxt_info;
/* 探测:打开文件、取大小、检测编码(前 64KB)。成功 0;无文件 -1;非文件 -2。 */
int rdtxt_probe(const char *path, rdtxt_info *info);
/* 状态流式读取器 */
typedef struct rdtxt_reader rdtxt_reader;
/* 打开(info 须来自 rdtxt_probe)。成功 0;失败负值。 */
int rdtxt_reader_open(const char *path, const rdtxt_info *info, rdtxt_reader **r);
/* 读下一段。utf8 缓冲须 ≥ cap(建议 RDTXT_SEG_UTF8_MAX+8)。成功 0(填 utf8len/file_off/file_len);
* 1=EOF 无更多段;负值=错误。 */
int rdtxt_reader_next(rdtxt_reader *r, uint8_t *utf8, size_t cap,
size_t *utf8len, size_t *file_off, size_t *file_len);
void rdtxt_reader_free(rdtxt_reader *r);
const char *rdtxt_reader_path(const rdtxt_reader *r);
rd_enc_t rdtxt_reader_enc(const rdtxt_reader *r);
/* 渲染重定位:把文件字节区间 [raw_off, raw_off+raw_len) 转成 UTF-8 到 out(cap 建议同上限)。 */
int rdtxt_convert_span(const char *path, rd_enc_t enc, size_t raw_off, size_t raw_len,
uint8_t *out, size_t cap, size_t *outlen);
#ifdef __cplusplus
}
#endif
#endif /* RDTXT_H */
+215
View File
@@ -0,0 +1,215 @@
/* engine/rdft.c — FreeType 封装实现(见 engine/rdft.h)。
* 依赖 FreeType 2(静态篇 mipsel musl);无 SDK 依赖。
*/
#include "rdft.h"
#include <ft2build.h>
#include FT_FREETYPE_H
#include <stdlib.h>
#include <string.h>
typedef struct {
uint64_t key; /* (px<<32)|cp */
uint64_t stamp; /* LRU 时间戳 */
int adv; /* 水平 advance(px) */
uint8_t *bmp; /* 灰阶位图拷贝(malloc;bw==0 时 NULL) */
int bw, bh, pitch, left, top;
} glyph_slot;
struct rdft {
FT_Library lib;
FT_Face face;
int px;
glyph_slot *slots;
int nslots; /* 容量 */
int used; /* 在线条目数 */
uint64_t clock; /* 单调戳 */
int has_font;
};
int rdft_init(rdft **r)
{
if (!r) return -1;
rdft *x = (rdft *)calloc(1, sizeof *x);
if (!x) return -6;
if (FT_Init_FreeType(&x->lib) != 0) { free(x); return -3; }
x->px = 20;
x->nslots = 512;
x->slots = (glyph_slot *)calloc((size_t)x->nslots, sizeof(glyph_slot));
if (!x->slots) { FT_Done_FreeType(x->lib); free(x); return -6; }
x->used = 0;
x->clock = 1;
*r = x;
return 0;
}
int rdft_load_font(rdft *r, const char *path)
{
if (!r || !path) return -1;
if (r->face) { FT_Done_Face(r->face); r->face = NULL; }
if (r->slots) { for (int i = 0; i < r->nslots; i++) free(r->slots[i].bmp); }
if (r->slots) memset(r->slots, 0, (size_t)r->nslots * sizeof(glyph_slot));
r->used = 0;
if (FT_New_Face(r->lib, path, 0, &r->face) != 0) { r->has_font = 0; return -1; }
r->has_font = 1;
if (r->px > 0) FT_Set_Pixel_Sizes(r->face, 0, r->px);
return 0;
}
void rdft_set_px(rdft *r, int px)
{
if (!r) return;
if (px <= 0) px = r->px;
r->px = px;
if (r->face) FT_Set_Pixel_Sizes(r->face, 0, (FT_UInt)px);
/* key 含字号,清缓存防旧字号位图残留 */
if (r->slots) { for (int i = 0; i < r->nslots; i++) free(r->slots[i].bmp); memset(r->slots, 0, (size_t)r->nslots * sizeof(glyph_slot)); }
r->used = 0;
}
int rdft_ascender(rdft *r)
{
if (!r || !r->face) return 0;
return (int)(r->face->size->metrics.ascender >> 6);
}
int rdft_descender(rdft *r)
{
if (!r || !r->face) return 0;
return (int)(r->face->size->metrics.descender >> 6);
}
const char *rdft_family(rdft *r)
{
if (!r || !r->face || !r->face->family_name) return "";
return r->face->family_name;
}
static glyph_slot *slot_find(rdft *r, uint64_t key)
{
for (int i = 0; i < r->nslots; i++)
if (r->slots[i].key == key && r->slots[i].stamp) return &r->slots[i];
return NULL;
}
static glyph_slot *slot_lru_evict(rdft *r)
{
/* 找未用槽(stamp==0)或最旧使用 */
glyph_slot *oldest = NULL;
uint64_t mins = UINT64_MAX;
for (int i = 0; i < r->nslots; i++) {
if (!r->slots[i].stamp) { r->used++; return &r->slots[i]; } /* 空槽 */
if (r->slots[i].stamp < mins) { mins = r->slots[i].stamp; oldest = &r->slots[i]; }
}
free(oldest->bmp);
oldest->bmp = NULL; oldest->key = 0; oldest->stamp = 0;
return oldest;
}
int rdft_glyph(rdft *r, uint32_t cp, int *adv_x,
const uint8_t **bitmap, int *bw, int *bh, int *left, int *top)
{
if (!r || !r->face || !adv_x) return -1;
if (r->clock == UINT64_MAX) { r->clock = 1; } /* 时钟回绕(罕见) */
uint64_t key = ((uint64_t)(uint32_t)r->px << 32) | cp;
glyph_slot *s = slot_find(r, key);
if (s) {
s->stamp = r->clock++;
*adv_x = s->adv;
*bitmap = s->bmp; *bw = s->bw; *bh = s->bh; *left = s->left; *top = s->top;
return 0;
}
/* miss:光栅化 */
FT_UInt gi = FT_Get_Char_Index(r->face, cp);
if (r->px <= 0) FT_Set_Pixel_Sizes(r->face, 0, (FT_UInt)r->px);
if (FT_Load_Glyph(r->face, gi, FT_LOAD_RENDER | FT_LOAD_NO_HINTING | FT_LOAD_TARGET_LIGHT) != 0) {
/* 空字形(缺字)→ 仅 advance */
*adv_x = r->face->glyph->advance.x >> 6;
*bitmap = NULL; *bw = *bh = *left = *top = 0;
return 0;
}
FT_GlyphSlot g = r->face->glyph;
int a = (int)((g->advance.x >= 0 ? g->advance.x + 0x20 : g->advance.x - 0x20) >> 6);
if (a < 0) a = 0;
s = slot_lru_evict(r);
s->key = key; s->stamp = r->clock++; s->adv = a;
s->bmp = NULL; s->bw = 0; s->bh = 0; s->pitch = 0; s->left = 0; s->top = 0;
if (g->bitmap.buffer && g->bitmap.width > 0 && g->bitmap.rows > 0) {
int w = (int)g->bitmap.width, rows = (int)g->bitmap.rows, pitch = (int)g->bitmap.pitch;
uint8_t *b = (uint8_t *)malloc((size_t)w * (size_t)rows);
if (b) {
/* 紧凑拷贝(每行 = w 字节),blend 用 bw 作行步长 */
for (int r_ = 0; r_ < rows; r_++)
memcpy(b + (size_t)r_ * (size_t)w, g->bitmap.buffer + (size_t)r_ * (size_t)pitch, (size_t)w);
s->bmp = b; s->bw = w; s->bh = rows; s->pitch = w;
s->left = g->bitmap_left; s->top = g->bitmap_top;
}
}
*adv_x = s->adv;
*bitmap = s->bmp; *bw = s->bw; *bh = s->bh; *left = s->left; *top = s->top;
return 0;
}
int rdft_advance(rdft *r, uint32_t cp, int *adv_x)
{
if (!r || !r->face || !adv_x) return -1;
FT_UInt gi = FT_Get_Char_Index(r->face, cp);
if (r->px <= 0) FT_Set_Pixel_Sizes(r->face, 0, (FT_UInt)r->px);
if (FT_Load_Glyph(r->face, gi, FT_LOAD_NO_HINTING | FT_LOAD_TARGET_LIGHT) != 0) {
*adv_x = r->face->glyph->advance.x >> 6;
return 0;
}
int a = (int)((r->face->glyph->advance.x >= 0 ? r->face->glyph->advance.x + 0x20
: r->face->glyph->advance.x - 0x20) >> 6);
*adv_x = a < 0 ? 0 : a;
return 0;
}
void rdft_blend565(uint16_t *fb, int pitch_bytes, int tw, int th,
int x, int y, const uint8_t *bmp, int bw, int bh, int left, int top,
uint16_t fg565, uint16_t bg565)
{
if (!fb || !bmp || bw <= 0 || bh <= 0) return;
int fg_r = (fg565 >> 11) & 0x1F, fg_g = (fg565 >> 5) & 0x3F, fg_b = fg565 & 0x1F;
int bg_r = (bg565 >> 11) & 0x1F, bg_g = (bg565 >> 5) & 0x3F, bg_b = bg565 & 0x1F;
int start_x = x + left, base_y = y; /* y 为基线 */
int start_y = base_y - top;
int pitch_px = pitch_bytes / 2; /* 目标每行像素数 */
for (int row = 0; row < bh; row++) {
int ty = start_y + row;
if (ty < 0) continue;
if (ty >= th) break;
const uint8_t *src = bmp + (size_t)row * (size_t)bw; /* 紧凑:每行 bw */
for (int col = 0; col < bw; col++) {
int tx = start_x + col;
if (tx < 0) continue;
if (tx >= tw) break;
unsigned a = src[col];
if (a == 0) continue;
uint16_t *px = fb + (size_t)ty * (size_t)pitch_px + tx;
int r = (fg_r * a + bg_r * (255 - a)) / 255;
int g = (fg_g * a + bg_g * (255 - a)) / 255;
int b = (fg_b * a + bg_b * (255 - a)) / 255;
*px = (uint16_t)((r << 11) | (g << 5) | b);
}
}
}
void rdft_cache_set_slots(rdft *r, int slots)
{
if (!r || slots < 0 || slots == r->nslots) return;
glyph_slot *na = (glyph_slot *)calloc((size_t)(slots ? slots : 1), sizeof(glyph_slot));
if (!na) return;
for (int i = 0; i < r->nslots; i++) free(r->slots[i].bmp);
free(r->slots);
r->slots = na; r->nslots = (slots ? slots : 1); r->used = 0;
}
int rdft_cache_slots(rdft *r) { return r ? r->nslots : 0; }
int rdft_cache_used(rdft *r) { return r ? r->used : 0; }
void rdft_free(rdft *r)
{
if (!r) return;
if (r->slots) { for (int i = 0; i < r->nslots; i++) free(r->slots[i].bmp); }
free(r->slots);
if (r->face) FT_Done_Face(r->face);
if (r->lib) FT_Done_FreeType(r->lib);
free(r);
}
+68
View File
@@ -0,0 +1,68 @@
/* engine/rdft.h — FreeType 正文封装(meow_reader_ft_*)。
*
* 对齐 READER_ENGINE_NOTES §5:`FT_New_Face(path)` 按需读文件(不整文件载入 → 零大块 anon);
* 灰阶 `FT_LOAD_RENDER` + `FT_LOAD_NO_HINTING | FT_LOAD_TARGET_LIGHT`;LRU 字形缓存 512(key=码点+字号);
* blend565 灰阶位图混到 RGB565 缓冲。
*
* 依赖:freetype2 静态库(build_reader.sh 编 mipsel musl)。本 TU 只含封装逻辑,无 SDK 依赖。
*/
#ifndef RDFT_H
#define RDFT_H
#include <stddef.h>
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
typedef struct rdft rdft; /* 不透明 */
/* 初始化 FreeType。0=成功。 */
int rdft_init(rdft **r);
/* 载入字体文件路径(TTF/OTF)。0=成功。同一 rdft 可换字体(重载清除 glyph 缓存)。 */
int rdft_load_font(rdft *r, const char *path);
/* 定字号(px;>0)。改字号会使 glyph 缓存失效(key 含字号,故等价重查但成本高,直接清)。 */
void rdft_set_px(rdft *r, int px);
/* 当前字号 ascender/descender(px);未载入字体返回 0。 */
int rdft_ascender(rdft *r);
int rdft_descender(rdft *r);
/* 字体族名(face->family_name UTF-8;无字体返回 "")。 */
const char *rdft_family(rdft *r);
/* 只测水平 advance(px),不栅格化、不进 glyph 缓存(排版器逐字测量用,省 rasterize 成本)。
* 缺字符返回缺字 advance。0=成功。 */
int rdft_advance(rdft *r, uint32_t cp, int *adv_x);
/* 取字形:cp→advance(px) + 灰阶位图(来自 LRU 缓存或本次光栅化)。
* 返回 0=成功;位图可能 bw==0(空白字形,仅 advance 有意义)。
* **注意**:返回的 bitmap 指针归缓存所有,调用方须在本次使用完毕后再调 rdft_glyph(避免被新
* 光栅化项逐出)。adv_x 为水平 advance(px,按 26.6 舍入)。 */
int rdft_glyph(rdft *r, uint32_t cp, int *adv_x,
const uint8_t **bitmap, int *bw, int *bh, int *left, int *top);
/* 灰阶位图 blend 到 RGB565 缓冲。
* fb = 目标缓冲首像素(565 小端);pitch_bytes = 每行字节数;tw/th = 目标像素区宽/高;
* x,y = 目标像素区原点(绝对像素);fg565/bg565 = 前景/背景 565 色值;
* bmp/left/top 为 rdft_glyph 返回值(bmp 为紧凑灰度,每行 = bw 字节);字画在 (x+left, y - top)
* (y 为基线)。越界像素被钳制跳过。 */
void rdft_blend565(uint16_t *fb, int pitch_bytes, int tw, int th,
int x, int y, const uint8_t *bmp, int bw, int bh, int left, int top,
uint16_t fg565, uint16_t bg565);
/* LRU 缓存容量(槽位数;默认 512;≥0 设)。 */
void rdft_cache_set_slots(rdft *r, int slots);
int rdft_cache_slots(rdft *r);
/* 当前缓存占用(条目数)。 */
int rdft_cache_used(rdft *r);
/* 释放。 */
void rdft_free(rdft *r);
#ifdef __cplusplus
}
#endif
#endif /* RDFT_H */
Binary file not shown.

After

Width:  |  Height:  |  Size: 452 B

+35
View File
@@ -0,0 +1,35 @@
/* reader_internal.h — os.meow.reader 应用内部共享声明(视图 + 装配)。
*
* 层级(仿 hello_app):app.c 只做 init/建页/事件分发;shelf(书架)、reader(阅读页)各一个
* meow_view(ACTIVITY)。阅读页用 W_CUSTOM 画布 + App 自绘 back_buf(翻页 blit 局部脏区)。
*/
#ifndef READER_INTERNAL_H
#define READER_INTERNAL_H
#include "meow_ui.h"
#include "meow_ui_view.h"
#define ACT_SHELF 0
#define ACT_READER 1
/* ---- 书架(app/shelf.c)---- */
meow_view *shelf_view_create(void);
int shelf_is_active(void);
int shelf_on_tap(int x, int y);
void shelf_redraw(void);
/* ---- 阅读页(app/reader.c)---- */
meow_view *reader_view_create(void);
int reader_is_active(void);
void reader_on_tap(int x, int y);
void reader_on_swipe(int dir, int dx, int dy);
void reader_on_tick(uint64_t now);
/* 打开一本书(书架调;成功进入阅读页)。path=TXT 完整路径。 */
void reader_open(const char *path);
/* 返回可执行文件所在包根(用于定位 /usr/data/mnt/sd_0)——由 app.c 注入与否;
* R1 用固定 TF 根 /usr/data/mnt/sd_0。 */
#define READER_SD_ROOT "/usr/data/mnt/sd_0"
#endif /* READER_INTERNAL_H */
+86
View File
@@ -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;
}
+93
View File
@@ -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;
}
+71
View File
@@ -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;
}
+275
View File
@@ -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;
}
@@ -0,0 +1,63 @@
#!/usr/bin/env python3
"""gen_gb18030_table.py — 生成 GB18030 双字节 → Unicode BMP 码位表(源码生成物,入仓)。
输出:core/gb18030_table.h 的 `static const uint16_t gb_2byte[126][190]`。
为什么这样生成(对齐 devdocs/plan/READER_ENGINE_NOTES.md §3.4):
- 用 Python 内置 `gb18030` codec 枚举全部 2 字节码位(GB18030 向后兼容 GBK,
故本表覆盖 GBK 全部合法双字节对 + 部分扩展)。
- 表大小 = 126 引导字节(0x81..0xFE)× 190 尾字节 = 23,940 项 × 2B = 47,880 B。
- 0 值 = 未映射;解码器查表得码位,命中 0 → U+FFFD。
- 尾字节列布局 = [0x40..0x7E] 紧接 [0x80..0xFE](跳过 0x7F);索引 idx(b):
b in 0x40..0x7F -> b-0x40 ;b in 0x80..0xFE -> b - 0x41
(即 b - 0x40 - (b>=0x80))。
用法:`python3 tools/gen_gb18030_table.py > core/gb18030_table.h`
(可用 env GB18030_TABLE_H 覆写输出路径。)
"""
import sys
LEADS = list(range(0x81, 0xFF)) # 0x81..0xFE → 126 个
TRAILS = list(range(0x40, 0x7F)) + list(range(0x80, 0xFF)) # 0x40..0x7E,0x80..0xFE → 190 个
INDEX_OF = {b: i for i, b in enumerate(TRAILS)} # 尾字节 → 列号
def build():
g = [0] * (len(LEADS) * len(TRAILS))
for k, lead in enumerate(LEADS):
for j, tr in enumerate(TRAILS):
try:
s = bytes([lead, tr]).decode('gb18030')
if len(s) == 1 and ord(s) <= 0xFFFF:
g[k * len(TRAILS) + j] = ord(s)
except UnicodeDecodeError:
pass
return g
def emit(g):
rows, cols = len(LEADS), len(TRAILS)
out = sys.stdout
out.write("/* gb18030_table.h — GB18030 双字节 → Unicode BMP 码位表(生成物,勿手改;\n")
out.write(" * 由 tools/gen_gb18030_table.py 用 python3 `gb18030` codec 生成)。\n")
out.write(" * 布局:gb_2byte[lead-0x81][trail_col];列号 idx(b)= b-0x40-(b>=0x80);\n")
out.write(" * 值 0 = 未映射(解码器输出 U+FFFD)。46 bytes 行格式可读。*/\n")
out.write("#ifndef RD_GB18030_TABLE_H\n#define RD_GB18030_TABLE_H\n\n")
out.write("#include <stdint.h>\n\n")
out.write("#define RD_GB_LEADS %d\n" % rows)
out.write("#define RD_GB_TRAILS %d\n" % cols)
out.write("#define RD_GB_LEAD0 0x81\n")
out.write("#define RD_GB_TRAIL0 0x40\n\n")
out.write("/* gb_2byte[126][190]:rdgb_lookup(lead, trail) 返回 BMP 码位或 0。 */\n")
out.write("static const uint16_t gb_2byte[%d][%d] = {\n" % (rows, cols))
for k in range(rows):
out.write(" {")
for j in range(cols):
v = g[k * cols + j]
if j % 8 == 0:
out.write("\n " if j else "")
out.write("0x%04X,%s" % (v, "" if j == cols - 1 else ""))
out.write("\n }%s\n" % ("," if k != rows - 1 else ""))
out.write("};\n\n#endif /* RD_GB18030_TABLE_H */\n")
if __name__ == '__main__':
emit(build())