- 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
347 lines
13 KiB
C
347 lines
13 KiB
C
/* 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;
|
|
}
|