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