Files
MeowOS-For-R3P2/patch_v2/reader_app/app/reader.c
T
MeowOS 553a7d5d9e 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
2026-09-01 23:26:42 +08:00

335 lines
12 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* 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(); }
}