Files
MeowOS-For-R3P2/patch_v2/reader_app/app/book.c
T
AskaEth 7c7d891570 feat: M6.44+M6.45 收编 — 真机 0.8.45 现场保全(reader章节/key_router按键路由/WiFi连接修复/ncm bench+IME修复)
脏工作区现场收编:devdocs/local/m6_44_45_dirty_tracked.patch(46 个已跟踪文本文件,
+3090/-585, 已对 M6.43 基线干净适配) + m6_44_45_untracked_src.tar.gz 中 patch_v2/ 下
13 个新增源文件, 合成单个提交, 内容与容器工作树(R3ProII/patch_v2)逐字节一致。

- reader 章节: 新增 core/rdchapter.{h,c} — TXT 章节规范解析器, 扫描 512KB 码位对齐段文件
  逐段流式识别标题行(行首缩进≤4/显示宽≤40/行内标点≤1 三重约束防误判), 输出标题+全书字符
  偏移+段号/段内偏移+阿拉伯章号(中文数字归一), 首章前内容归"卷首"; book/reader/rdenc/
  rdlayout/settings 接入章节跳转与进度锚点, 附 chapter_smoke/turn_repro/build_turn_repro.sh
- key_router 按键路由: 新增 meow_launcherd/key_router.{h,c} — launcher-UI 页物理键统一路由
  (P1 仅音量键), 把读取边沿+防抖+长按 held+ramp 推进从 5 个会话收敛为进程内单开;
  各页每圈 key_router_pump() 取防抖后边沿, 动作(调音量/光标导航)仍由调用页决定,
  保留 run_menu 的 meow_mode 分支; App 前台 volume_page 武装时边沿抑制;
  main_menu/now_playing/notif_center/settings_page/subpage 接入, 附 test_key_router_vol/test_np_keys
- WiFi 连接修复: meow_netmgrd connect 有界等待 wpa_state=COMPLETED(普通 10s/隐藏网络 15s,
  env 可调)修设置页连接无响应; list_networks/scan_results 的 \xNN 转义还原始字节, 修中文 SSID
  永不命中致新建同名 open 记录顶影; launcher netmgr_ipc 配读超时; 附 test_wifi_connect(桩
  wpa_cli 驱动 static wifi_connect_ex), test_netmgrd.sh 补用例; wifi_page 同步
- ncm bench: 新增 bench/lyric_sim.c、bench/plpage_sim.c, vol_sync_sim 与 ncm core/app 同步
- SDK IME 修复: shift 大小写、K_ENTER 只提交不关、阻塞 IME 循环喂 on_tick 不被遮挡、
  IME 打开 ~200ms TAP 冷却窗吞 stray-TAP; 附 sdk/test_ime_fix.c
- 同期: meow_audiod 音量异步 ACK 线程(代际防 fd/槽复用竞态)、watchdog 音量收编(首选 audiod
  权威, settings.conf 覆盖路径)、panel 文件搜索(F1-④)与蓝牙只读详情(F1-③)、各类 build 脚本同步

范围红线: third_party/legado-E-main 原开发者未跟踪, 本次不入库(保留 devdocs/local tar 参考);
二进制不入库(后续重编); devdocs/ 不入库。59 文件全部在 patch_v2/ 下, 无 third_party/devdocs。
验证: git apply --check 干净; 59 文件 md5 与容器 patch_v2 逐一比对 59/59 一致(含
key_router.c/rdchapter.c/meow_ui_ime.c/test_wifi_connect.c 抽查); 可执行位保守
(check_lgpl_release.sh 100755, build_turn_repro.sh 100644 与容器一致)。
2026-11-04 20:02:28 +08:00

1260 lines
53 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/book.c — 书桥实现(见 app/book.h):把 rd_src(TXT/FB2/EPUB/MOBI 段契约 + CBZ 图页)
* + engine(FreeType/rdimg) 串成「一本书」。
*
* 文本格式:build_index 逐段 rd_src_next → 每段 utf8→码点→rd_layout → 页索引(flat:段+段内字符偏移)
* (即 rdtxt 同款 UTF-8 段契约喂排版器);渲染按段重显。锚定 (段,字符偏移) 与字号无关。
* CBZ:纯图页,npages=图片数,render 用 rdimg 解码+降采样写内容区。
*/
#include "book.h"
#include "rdsrc.h"
#include "rdtxt.h"
#include "seg.h"
#include "rdchapter.h"
#include "rdlayout.h"
#include "rdimg.h"
#include "../engine/rdft.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
typedef struct seg {
int page_start; /* 全局页表起点 */
int page_count; /* 本段页数 */
} seg_t;
/* Phase 2C:双段 cps 窗口槽——只保留段解码码点(utf8 转完即 free,不再驻留整段 lines/pages)。 */
typedef struct cps_slot {
int seg; /* 段下标;-1 = 空槽 */
uint32_t *cps;
size_t n;
} cps_slot;
/* 滞回释放/预取阈值(按全书页数百分比;真机可调)。 */
#define RD_WIN_NEAR_PCT 2 /* 距下一段界 ≤2%:预取下一段 */
#define RD_WIN_FAR_PCT 4 /* >4% 滑出:远槽可被下一段预取替换(由距离最近者策略体现) */
struct rd_book {
char path[400];
char font[400];
char base[256];
const char *encname; /* TXT 有效编码可读名(含覆盖后);非 TXT = "" */
rd_enc_t enc; /* TXT 有效编码;非 TXT = RD_ENC_UTF8 */
rdft *ft;
rd_src *src;
rd_fmt_t fmt;
int is_img; /* 1=CBZ 图片书 */
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;
/* Phase 2C:双段 cps 窗口(渲染微排版取码点用;替换旧单段 seg_lines/seg_pages 缓存) */
cps_slot win[2];
/* Phase 2A:拼接页表滚动状态(上一段的「末页页首→段尾」码点 tail;建下一段时拼接) */
uint32_t *tail_cps; size_t tail_n;
int tail_seg; /* tail 所属段;-1 = 无 */
int32_t tail_seg_char; /* tail 在所属段的起始字符偏移 */
/* Phase 2C:渲染微排版复用缓冲(拼接页临时码点 + 一页行缓冲,避免每帧 malloc) */
uint32_t *stitch_cps; size_t stitch_cap;
rd_line *render_lines; int render_lines_cap;
/* R3.2 分段流式(TXT):段目录 + 全局段数 + 已建段数 + 段/页表容量(增量补齐用) */
char seg_dir[400];
int seg_n; /* 段文件总数(manifest);fallback = nsegs(全建) */
int seg_built; /* 已布局段数;fallback = nsegs */
int segs_cap, pages_cap; /* segs[]/pages[] 当前容量(只在 seg 路径增量增长) */
/* R3.3 章节(TXT):解析结果 + 就绪/无章节标志(延迟解析,不拖慢首屏) */
rdchapter chaps;
int chaps_ready; /* 1=chaps 已填充(含确无章节) */
int chaps_none; /* 1=确无章节(扫描过 n==0)或该书不可用 */
};
/* ---- 页索引持久化(MeowData;见 rd_book_set_index_dir) ----
* build_index 对整书逐段 reflow 建 flat 页表,是开大书最贵的步骤(无持久化时每次打开都重跑)。
* 宿主快 ≠ 真机快:同一书 MIPS 上仍需数秒。这里把 (segs + pages) 落盘成 {path 哈希}.idx,
* 以 (file_size, file_mtime, font path+mt, fs, ls, content_w, content_h, enc) 为失效键——
* 书/字号/行距/版面/编码/字体任一变了即自动重排并重写,其余情况命中缓存秒开。
* 纯 libc(无 SDK):reader.c 用 meow_config_dir 推导 MeowData dir 注入;冒烟/无卡时 dir 为空 → 直排不持久。 */
static char g_index_dir[400]; /* 索引缓存目录; 空 = 禁用 */
static int seg_ensure_built(rd_book *b, int upto_seg); /* 段路径补齐(下文定义) */
void rd_book_set_index_dir(const char *dir)
{
if (dir && dir[0]) { strncpy(g_index_dir, dir, sizeof g_index_dir - 1); g_index_dir[sizeof g_index_dir - 1] = 0; }
else g_index_dir[0] = 0;
}
/* R3.2:分段流式缓存根(MeowData/os.meow.reader)。存到 g_seg_base 并同步 rdseg。 */
static char g_seg_base[400];
void rd_book_set_seg_base(const char *base)
{
if (base && base[0]) { strncpy(g_seg_base, base, sizeof g_seg_base - 1); g_seg_base[sizeof g_seg_base - 1] = 0; }
else g_seg_base[0] = 0;
rdseg_set_base(g_seg_base);
}
#define IDX_MAGIC0 'R'
#define IDX_MAGIC1 'I'
#define IDX_MAGIC2 'D'
#define IDX_MAGIC3 'X'
#define IDX_VER 2u
static int fio_rd(FILE *f, void *p, size_t n) { return fread(p, 1, n, f) == n; }
static int fio_wr(FILE *f, const void *p, size_t n) { return fwrite(p, 1, n, f) == n; }
static uint32_t idx_fnv1a(const char *s)
{
uint32_t h = 2166136261u;
for (const unsigned char *p = (const unsigned char *)s; *p; p++) { h ^= *p; h *= 16777619u; }
return h;
}
static int index_cache_path(const rd_book *b, char *out, int outsz)
{
if (!g_index_dir[0]) return -1;
if (snprintf(out, (size_t)outsz, "%s/%08x.idx", g_index_dir, idx_fnv1a(b->path)) >= outsz) return -1;
return 0;
}
/* 编码枚举 → 可读名(R3:Big5 正确解码后名实相符)。 */
static const char *enc_to_name(rd_enc_t enc)
{
switch (enc) {
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";
case RD_ENC_ASCII: return "ASCII";
default: return "";
}
}
static const char *fmt_encname(rd_fmt_t f, rd_enc_t enc)
{
if (f == RD_FMT_TXT) return enc_to_name(enc);
return "";
}
static int book_meas(uint32_t cp, int fs, int *adv, void *ctx)
{
(void)fs;
rd_book *b = (rd_book *)ctx;
return rdft_advance(b->ft, cp, adv);
}
/* 失效键:书 + 版面 + 编码 + 字体。任一变化 → 缓存不命中(重排)。 */
typedef struct {
uint64_t file_size;
int64_t file_mtime;
int64_t font_mtime; /* 字体文件 mtime;无字体(近似测宽) = 0 */
int32_t fs, ls, cw, ch, enc;
char font[320];
} idx_key;
static int index_key_fill(rd_book *b, idx_key *k)
{
memset(k, 0, sizeof *k);
struct stat st;
if (stat(b->path, &st) != 0) { k->file_size = 0; k->file_mtime = 0; }
else { k->file_size = (uint64_t)st.st_size; k->file_mtime = (int64_t)st.st_mtime; }
if (b->font[0]) {
/* b->font[400] 已 NUL 结尾(open 时 <400 才收);memcpy 319B 恒在界内且 k 已清零 → 余下为 0 */
memcpy(k->font, b->font, sizeof k->font - 1);
struct stat fs2;
if (stat(b->font, &fs2) == 0) k->font_mtime = (int64_t)fs2.st_mtime;
}
k->fs = b->fs; k->ls = b->ls; k->cw = b->content_w; k->ch = b->content_h;
k->enc = (int32_t)b->enc;
return 0;
}
static int index_key_write(const idx_key *k, FILE *f)
{
const uint8_t magic[4] = { IDX_MAGIC0, IDX_MAGIC1, IDX_MAGIC2, IDX_MAGIC3 };
uint16_t ver = IDX_VER;
return fio_wr(f, magic, sizeof magic) && fio_wr(f, &ver, sizeof ver) &&
fio_wr(f, &k->file_size, sizeof k->file_size) &&
fio_wr(f, &k->file_mtime, sizeof k->file_mtime) && fio_wr(f, &k->font_mtime, sizeof k->font_mtime) &&
fio_wr(f, &k->fs, sizeof k->fs) && fio_wr(f, &k->ls, sizeof k->ls) &&
fio_wr(f, &k->cw, sizeof k->cw) && fio_wr(f, &k->ch, sizeof k->ch) &&
fio_wr(f, &k->enc, sizeof k->enc) && fio_wr(f, k->font, sizeof k->font);
}
static int index_key_read(idx_key *k, FILE *f)
{
uint8_t magic[4];
if (!fio_rd(f, magic, sizeof magic)) return 0;
if (magic[0] != IDX_MAGIC0 || magic[1] != IDX_MAGIC1 || magic[2] != IDX_MAGIC2 || magic[3] != IDX_MAGIC3) return 0;
uint16_t ver = 0;
if (!fio_rd(f, &ver, sizeof ver) || ver != IDX_VER) return 0;
if (!fio_rd(f, &k->file_size, sizeof k->file_size) || !fio_rd(f, &k->file_mtime, sizeof k->file_mtime) ||
!fio_rd(f, &k->font_mtime, sizeof k->font_mtime) || !fio_rd(f, &k->fs, sizeof k->fs) ||
!fio_rd(f, &k->ls, sizeof k->ls) || !fio_rd(f, &k->cw, sizeof k->cw) ||
!fio_rd(f, &k->ch, sizeof k->ch) || !fio_rd(f, &k->enc, sizeof k->enc) ||
!fio_rd(f, k->font, sizeof k->font)) return 0;
k->font[sizeof k->font - 1] = 0;
return 1;
}
static int index_key_eq(const idx_key *a, const idx_key *b)
{
return a->file_size == b->file_size && a->file_mtime == b->file_mtime &&
a->font_mtime == b->font_mtime && a->fs == b->fs && a->ls == b->ls &&
a->cw == b->cw && a->ch == b->ch && a->enc == b->enc &&
strncmp(a->font, b->font, sizeof a->font) == 0;
}
/* 读缓存:头部与当前键一致则载入 (segs,pages) 并自检一致;返回 1=命中,0=未命中/损坏(调用方重排)。 */
static int index_cache_load(rd_book *b)
{
char path[440];
if (index_cache_path(b, path, sizeof path) != 0) return 0;
FILE *f = fopen(path, "rb");
if (!f) return 0;
idx_key key, cur;
int rc = 0;
if (index_key_read(&key, f)) {
index_key_fill(b, &cur);
if (index_key_eq(&key, &cur)) {
int32_t nsegs = 0, npages = 0;
if (fio_rd(f, &nsegs, sizeof nsegs) && fio_rd(f, &npages, sizeof npages) &&
nsegs >= 1 && npages >= 1 && nsegs <= 1000000 && npages <= 8000000 &&
(uint64_t)npages <= cur.file_size + 1 && (uint64_t)nsegs <= cur.file_size + 1) {
seg_t *segs = (seg_t *)malloc((size_t)nsegs * sizeof(seg_t));
rd_book_page *pages = (rd_book_page *)malloc((size_t)npages * sizeof(rd_book_page));
if (segs && pages &&
fio_rd(f, segs, (size_t)nsegs * sizeof(seg_t)) &&
fio_rd(f, pages, (size_t)npages * sizeof(rd_book_page))) {
/* 自检:page_count 求和 == npages 且 page_start 链正确;每页 seg 落在所属段区间。 */
int ok = 1, run = 0;
for (int32_t i = 0; i < nsegs && ok; i++) {
if (segs[i].page_count < 0 || segs[i].page_start != run) ok = 0;
else run += segs[i].page_count;
}
if (ok && run == npages) {
for (int32_t p = 0; p < npages && ok; p++) {
int32_t sg = pages[p].seg;
if (sg < 0 || sg >= nsegs || pages[p].seg_char < 0 ||
p < segs[sg].page_start || p >= segs[sg].page_start + segs[sg].page_count) ok = 0;
}
}
if (ok) {
b->segs = segs; b->pages = pages;
b->nsegs = (int)nsegs; b->npages = (int)npages;
rc = 1; /* 命中:调用方免 build_index */
} else { free(segs); free(pages); }
} else { free(segs); free(pages); }
}
}
}
fclose(f);
return rc;
}
/* 写缓存:先写 .tmp 再 rename(原子替换);失败静默(下次重排)。 */
static void index_cache_save(rd_book *b)
{
char path[440];
if (b->nsegs <= 0 || b->npages <= 0) return; /* 空书无页表可缓存 */
if (index_cache_path(b, path, sizeof path) != 0) return;
/* 目录由 reader.c 在 set_index_dir 前建好;这里 fopen 失败(无目录/只读)→ 静默跳过(下次重排)。 */
char tmp[480];
if (snprintf(tmp, sizeof tmp, "%s.tmp", path) >= (int)sizeof tmp) return;
FILE *f = fopen(tmp, "wb");
if (!f) return;
idx_key key;
int ok = 1;
index_key_fill(b, &key);
ok = ok && index_key_write(&key, f);
ok = ok && fio_wr(f, &b->nsegs, sizeof b->nsegs) && fio_wr(f, &b->npages, sizeof b->npages);
if (ok && b->nsegs > 0) ok = fio_wr(f, b->segs, (size_t)b->nsegs * sizeof(seg_t));
if (ok && b->npages > 0) ok = fio_wr(f, b->pages, (size_t)b->npages * sizeof(rd_book_page));
fclose(f);
if (ok) { rename(tmp, path); } else { remove(tmp); }
}
/* ---- R3.3 章节缓存(MeowData/os.meow.reader/index/<hash>.chap,键=书字节+编码) ----
* 章节 char_off/seg_idx/seg_char 仅依赖书字节与编码(不含字号/行距/字体/版面),故失效键
* (file_size, file_mtime, enc) 即够,与页索引(含 font/fs/ls/cw/ch)分开、独立秒开。
* 目录为 index 同父;g_index_dir 为空(宿主/无卡)→ 不持久,仅内存。 */
#define CHAP_MAGIC0 'R'
#define CHAP_MAGIC1 'C'
#define CHAP_MAGIC2 'H'
#define CHAP_MAGIC3 'P'
#define CHAP_VER 1u
static int chap_cache_path(const rd_book *b, char *out, int outsz)
{
if (!g_index_dir[0]) return -1;
if (snprintf(out, (size_t)outsz, "%s/%08x.chap", g_index_dir, idx_fnv1a(b->path)) >= outsz) return -1;
return 0;
}
/* 章节缓存键:书字节 + 编码(含 BOM/编码覆盖),跟页索引键不同(无字体/字号/版面)。 */
static int chap_cache_load(rd_book *b)
{
char path[440];
if (chap_cache_path(b, path, sizeof path) != 0) return 0;
FILE *f = fopen(path, "rb");
if (!f) return 0;
uint8_t magic[4];
uint16_t ver = 0;
uint64_t fsize = 0; int64_t fmtime = 0; int32_t fenc = 0;
int32_t nsegs = 0, nch = 0; int64_t total = 0;
int ok = 0;
if (fio_rd(f, magic, sizeof magic) && magic[0] == CHAP_MAGIC0 && magic[1] == CHAP_MAGIC1 &&
magic[2] == CHAP_MAGIC2 && magic[3] == CHAP_MAGIC3 &&
fio_rd(f, &ver, sizeof ver) && ver == CHAP_VER &&
fio_rd(f, &fsize, sizeof fsize) && fio_rd(f, &fmtime, sizeof fmtime) &&
fio_rd(f, &fenc, sizeof fenc) &&
fio_rd(f, &nsegs, sizeof nsegs) && fio_rd(f, &total, sizeof total) &&
fio_rd(f, &nch, sizeof nch) &&
nsegs >= 0 && nch >= 0 && nsegs <= 1000000 && nch <= RDCHAP_MAX_ENTRIES) {
struct stat st;
if (stat(b->path, &st) == 0 && (uint64_t)st.st_size == fsize &&
(int64_t)st.st_mtime == fmtime && (int32_t)b->enc == fenc) {
rdchapter c; rdchapter_init(&c);
c.seg_char_start = (int64_t *)malloc((size_t)(nsegs + 1) * sizeof(int64_t));
c.entries = (rdchapter_entry *)malloc((size_t)(nch ? nch : 1) * sizeof(rdchapter_entry));
if (c.seg_char_start && c.entries &&
fio_rd(f, c.seg_char_start, (size_t)(nsegs + 1) * sizeof(int64_t))) {
int eok = 1;
for (int i = 0; i < nch && eok; i++) {
rdchapter_entry *e = &c.entries[i];
if (!fio_rd(f, &e->char_off, sizeof e->char_off) ||
!fio_rd(f, &e->seg_idx, sizeof e->seg_idx) ||
!fio_rd(f, &e->seg_char, sizeof e->seg_char) ||
!fio_rd(f, &e->number, sizeof e->number) ||
!fio_rd(f, e->title, sizeof e->title)) eok = 0;
}
if (eok) {
c.n = nch; c.nsegs = nsegs;
c.has_pre = (nch > 0 && c.entries[0].char_off == 0 && c.entries[0].number == -1) ? 1 : 0;
snprintf(c.pre_title, sizeof c.pre_title, "%s", (nch > 0) ? c.entries[0].title : "卷首");
b->chaps = c;
b->chaps_ready = 1;
b->chaps_none = (nch == 0);
ok = 1;
} else { rdchapter_free(&c); }
} else { rdchapter_free(&c); }
}
}
fclose(f);
return ok;
}
static void chap_cache_save(rd_book *b)
{
if (!b->chaps_ready || b->chaps.nsegs <= 0) return;
char path[440];
if (chap_cache_path(b, path, sizeof path) != 0) return;
char tmp[480];
if (snprintf(tmp, sizeof tmp, "%s.tmp", path) >= (int)sizeof tmp) return;
FILE *f = fopen(tmp, "wb");
if (!f) return;
struct stat st;
uint64_t fsize = 0; int64_t fmtime = 0;
if (stat(b->path, &st) == 0) { fsize = (uint64_t)st.st_size; fmtime = (int64_t)st.st_mtime; }
const uint8_t magic[4] = { CHAP_MAGIC0, CHAP_MAGIC1, CHAP_MAGIC2, CHAP_MAGIC3 };
uint16_t ver = CHAP_VER;
int32_t nsegs = b->chaps.nsegs, fenc = (int32_t)b->enc, nch = b->chaps.n;
int64_t total = b->chaps.seg_char_start ? b->chaps.seg_char_start[nsegs] : 0;
int ok = 1;
ok = ok && fio_wr(f, magic, sizeof magic) && fio_wr(f, &ver, sizeof ver) &&
fio_wr(f, &fsize, sizeof fsize) && fio_wr(f, &fmtime, sizeof fmtime) &&
fio_wr(f, &fenc, sizeof fenc) &&
fio_wr(f, &nsegs, sizeof nsegs) && fio_wr(f, &total, sizeof total) &&
fio_wr(f, &nch, sizeof nch);
if (ok && nsegs > 0) ok = fio_wr(f, b->chaps.seg_char_start, (size_t)(nsegs + 1) * sizeof(int64_t));
if (ok && nch > 0) {
for (int i = 0; i < nch; i++) {
const rdchapter_entry *e = &b->chaps.entries[i];
ok = ok && fio_wr(f, &e->char_off, sizeof e->char_off) &&
fio_wr(f, &e->seg_idx, sizeof e->seg_idx) &&
fio_wr(f, &e->seg_char, sizeof e->seg_char) &&
fio_wr(f, &e->number, sizeof e->number) &&
fio_wr(f, e->title, sizeof e->title);
}
}
fclose(f);
if (ok) { rename(tmp, path); } else { remove(tmp); }
}
/* 确保章节解析(幂等):命中缓存 → 就绪;未命中 → 扫描(读全部段文件)。
* 可靠性(R3.4):仅「扫描全程成功」才落盘(含无章节 n==0);扫描中途出错(段读/解码/内存)
* 不落盘、本次降级无章节,下次重试。返回:0=成功(就绪);-1=不可用(非 TXT/无段目录);
* -2=扫描出错(不落盘)。 */
static int chapter_ensure(rd_book *b)
{
if (!b) return -1;
if (b->chaps_ready) return 0;
if (b->is_img || b->fmt != RD_FMT_TXT || !b->seg_dir[0]) {
b->chaps_none = 1; b->chaps_ready = 1;
return -1;
}
if (chap_cache_load(b)) return 0;
int rc = rdchapter_scan(b->seg_dir, b->enc, b->base, &b->chaps);
if (rc != 0) { b->chaps_none = 1; b->chaps_ready = 1; return -2; } /* 扫描失败:不落盘,降级无章节 */
b->chaps_none = (b->chaps.n == 0);
b->chaps_ready = 1;
chap_cache_save(b); /* 可靠(成功扫描,无论有无章节)→ 落盘 */
return 0;
}
int rd_book_chapter_ready(rd_book *b) { return (b && b->chaps_ready) ? 1 : 0; }
/* R3.4 手动重解析本书章节:先扫描到临时结果,成功才「清旧缓存/旧内存 + 换入新结果 + 落盘」;
* 失败则保留旧缓存/旧内存(绝不产生错位章节),不落盘、返回 -2。返回:n=章节数 / 0=无章节(成功)/
* -1=不可用(非 TXT+seg)/ -2=扫描失败。幂等。 */
int rd_book_reparse_chapters(rd_book *b)
{
if (!b) return -1;
if (b->is_img || b->fmt != RD_FMT_TXT || !b->seg_dir[0]) return -1;
rdchapter tmp; rdchapter_init(&tmp);
int rc = rdchapter_scan(b->seg_dir, b->enc, b->base, &tmp);
if (rc != 0) { rdchapter_free(&tmp); return -2; } /* 失败:保留旧缓存/旧内存,未落盘 */
rdchapter_free(&b->chaps); /* 清除内存旧解析结果 */
b->chaps = tmp;
b->chaps_none = (b->chaps.n == 0);
b->chaps_ready = 1;
char path[440];
if (chap_cache_path(b, path, sizeof path) == 0) remove(path); /* 清缓存文件 */
chap_cache_save(b); /* 可靠结果 → 落盘 */
return b->chaps.n; /* >=0 */
}
int rd_book_chapter_n(rd_book *b)
{
if (!b) return -1;
if (b->is_img || b->fmt != RD_FMT_TXT) return -1;
if (chapter_ensure(b) != 0) return -1;
return b->chaps.n;
}
int rd_book_chapter(rd_book *b, int i, char *title, int tsz,
int64_t *char_off, int *seg, int32_t *seg_char, int *number)
{
if (!b || !title || i < 0) return -1;
if (chapter_ensure(b) != 0) return -1;
if (i >= b->chaps.n || !b->chaps.entries) return -1;
const rdchapter_entry *e = &b->chaps.entries[i];
if (tsz > 0) { snprintf(title, (size_t)tsz, "%s", e->title); }
if (char_off) *char_off = e->char_off;
if (seg) *seg = e->seg_idx;
if (seg_char) *seg_char = e->seg_char;
if (number) *number = e->number;
return 0;
}
int64_t rd_book_page_char_off(rd_book *b, int page)
{
if (!b || b->npages <= 0 || page < 0 || page >= b->npages) return -1;
if (chapter_ensure(b) != 0) return -1;
if (!b->chaps.seg_char_start || b->chaps.nsegs <= 0) return -1;
int seg = b->pages[page].seg;
if (seg < 0 || seg >= b->chaps.nsegs) return -1;
return b->chaps.seg_char_start[seg] + b->pages[page].seg_char;
}
int rd_book_page_for_char(rd_book *b, int64_t char_off)
{
if (!b || b->npages <= 0) return -1;
if (chapter_ensure(b) != 0) return -1;
int nseg = b->chaps.nsegs;
if (!b->chaps.seg_char_start || nseg <= 0) return -1;
int64_t total = b->chaps.seg_char_start[nseg];
if (total <= 0) return 0;
if (char_off < 0) char_off = 0;
if (char_off >= total) char_off = total - 1;
/* 二分所在段:最大的 i 使 seg_char_start[i] <= char_off */
int lo = 0, hi = nseg - 1, s = 0;
while (lo <= hi) {
int mid = (lo + hi) / 2;
if (b->chaps.seg_char_start[mid] <= char_off) { s = mid; lo = mid + 1; }
else hi = mid - 1;
}
if (seg_ensure_built(b, s) != 0) return -1;
if (s >= b->nsegs) s = b->nsegs - 1;
int local = (int)(char_off - b->chaps.seg_char_start[s]);
int p0 = b->segs[s].page_start, pc = b->segs[s].page_count;
if (pc <= 0) return (p0 < b->npages) ? p0 : (b->npages - 1);
/* 段内二分:最后一个 seg_char <= local 的页 = 含该字符的页 */
int lo2 = p0, hi2 = p0 + pc - 1, best = p0;
while (lo2 <= hi2) {
int mid = (lo2 + hi2) / 2;
if (b->pages[mid].seg_char <= local) { best = mid; lo2 = mid + 1; }
else hi2 = mid - 1;
}
return (best < b->npages) ? best : (b->npages - 1);
}
int rd_book_chapter_for_page(rd_book *b, int page, int *idx)
{
if (!b || !idx) return -1;
if (chapter_ensure(b) != 0) return -1;
if (b->chaps.n <= 0) { *idx = -1; return 0; }
if (page < 0 || page >= b->npages) return -1;
int64_t po = rd_book_page_char_off(b, page);
if (po < 0) { *idx = -1; return 0; }
int lo = 0, hi = b->chaps.n - 1, best = -1;
while (lo <= hi) {
int mid = (lo + hi) / 2;
if (b->chaps.entries[mid].char_off <= po) { best = mid; lo = mid + 1; }
else hi = mid - 1;
}
*idx = best;
return 0;
}
/* ---- 段访问(文件/段模式跳转) ---- */
int rd_book_seg_n(rd_book *b)
{
if (!b || b->is_img || b->fmt != RD_FMT_TXT || !b->seg_dir[0]) return -1;
return (b->seg_n > 0) ? b->seg_n : b->nsegs;
}
int rd_book_seg_page(rd_book *b, int seg)
{
if (!b || seg < 0) return -1;
if (b->seg_dir[0] && b->fmt == RD_FMT_TXT) {
if (seg >= b->seg_n) return -1;
if (seg_ensure_built(b, seg) != 0) return -1;
if (seg >= b->nsegs) return -1;
return b->segs[seg].page_start;
}
return (seg < b->nsegs) ? b->segs[seg].page_start : -1;
}
int rd_book_seg_page_count(rd_book *b, int seg)
{
if (!b || seg < 0 || seg >= b->nsegs) return -1;
return b->segs[seg].page_count;
}
int rd_book_seg_of_page(rd_book *b, int page)
{
if (!b || b->npages <= 0 || page < 0 || page >= b->npages) return -1;
return b->pages[page].seg;
}
/* ---- 文本:建索引(逐段 layout) ---- */
static int build_index(rd_book *b)
{
rd_src_reset(b->src);
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) return -6;
b->nsegs = b->npages = 0;
uint8_t *utf8 = (uint8_t *)malloc(RDTXT_SEG_UTF8_MAX + 8);
if (!utf8) return -6;
int seg_idx = 0;
size_t ulen = 0;
while (rd_src_next(b->src, utf8, RDTXT_SEG_UTF8_MAX + 8, &ulen) == 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_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); 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].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)); }
memset(&b->pages[b->npages], 0, sizeof b->pages[b->npages]);
b->pages[b->npages].seg = seg_idx;
b->pages[b->npages].seg_char = out.pages[p].first_char;
b->pages[b->npages].flags = (uint8_t)(out.lines[out.pages[p].first_line].para_start ? 1u : 0u);
b->npages++;
}
free(lines); free(pages);
seg_idx++;
}
free(utf8);
b->nsegs = seg_idx;
return 0;
}
/* ---- 段解码:把段 idx 的原始字节 → utf8 → 码点(malloc 返回,调用方 free)。
* seg 路径读 512KB 段文件;非 seg 路径按 rd_src_seg 重显。窗口/建表/tail 重建共用。 ---- */
static int seg_load_cps(rd_book *b, int seg_idx, uint32_t **cps_out, size_t *n_out)
{
if (!cps_out || !n_out) return -1;
*cps_out = NULL; *n_out = 0;
uint8_t *utf8 = NULL; size_t cap = 0, ulen = 0;
if (b->seg_dir[0] && b->fmt == RD_FMT_TXT) {
size_t rawlen = 0;
if (rdseg_rawlen(b->seg_dir, seg_idx, &rawlen) != 0) return -4;
cap = rawlen * 3 / 2 + 16;
if (cap < RDTXT_SEG_UTF8_MAX + 8) cap = RDTXT_SEG_UTF8_MAX + 8;
utf8 = (uint8_t *)malloc(cap);
if (!utf8) return -6;
uint8_t *raw = (uint8_t *)malloc(rawlen ? rawlen : 1);
if (!raw) { free(utf8); return -6; }
size_t rn = 0;
int rc = rdseg_load(b->seg_dir, seg_idx, raw, rawlen, &rn);
if (rc != 0) { free(raw); free(utf8); return -4; }
int r2 = rd_to_utf8(b->enc, raw, rn, utf8, cap, &ulen);
free(raw);
if (r2 == RD_ENC_ERR_BUFSZ) { free(utf8); return -2; }
if (r2 != RD_ENC_OK) { free(utf8); return -3; }
} else {
cap = RDTXT_SEG_UTF8_MAX + 8;
utf8 = (uint8_t *)malloc(cap);
if (!utf8) return -6;
if (rd_src_seg(b->src, seg_idx, utf8, cap, &ulen) != 0) { free(utf8); return -4; }
}
uint32_t *cps = NULL; size_t n = 0;
int rc = rd_utf8_to_cp(utf8, ulen, &cps, &n);
free(utf8);
if (rc != RD_ENC_OK) { free(cps); return -5; }
*cps_out = cps; *n_out = n;
return 0;
}
/* ---- Phase 2A:拼接页表滚动状态(tail) ---- */
static void tail_clear(rd_book *b)
{
free(b->tail_cps);
b->tail_cps = NULL;
b->tail_n = 0;
b->tail_seg = -1;
b->tail_seg_char = 0;
}
/* 缓存续建时 tail 不在内存(未持久化)→ 由段 seg 的末页首字符重算(读该段 cps 截取末页 tail)。 */
static int tail_rebuild(rd_book *b, int seg)
{
if (b->tail_seg == seg && b->tail_cps && b->tail_n > 0) return 0;
tail_clear(b);
if (seg < 0 || seg >= b->seg_built) return -1;
int pc = b->segs[seg].page_count;
if (pc <= 0) return -1;
int lastp = b->segs[seg].page_start + pc - 1;
if (lastp < 0 || lastp >= b->npages) return -1;
int32_t sc = b->pages[lastp].seg_char;
if (sc < 0) return -1;
uint32_t *cps = NULL; size_t n = 0;
if (seg_load_cps(b, seg, &cps, &n) != 0) return -1;
if ((size_t)sc > n) { free(cps); return -1; }
b->tail_n = n - (size_t)sc;
b->tail_cps = (uint32_t *)malloc((b->tail_n ? b->tail_n : 1) * sizeof(uint32_t));
if (!b->tail_cps) { free(cps); return -6; }
if (b->tail_n) memcpy(b->tail_cps, cps + sc, b->tail_n * sizeof(uint32_t));
b->tail_seg = seg;
b->tail_seg_char = sc;
free(cps);
return 0;
}
static int pages_push(rd_book *b, int seg, int32_t seg_char, uint8_t flags)
{
if (b->npages >= b->pages_cap) {
int nc = b->pages_cap ? b->pages_cap * 2 : 1024;
rd_book_page *np = (rd_book_page *)realloc(b->pages, (size_t)nc * sizeof(rd_book_page));
if (!np) return -6;
b->pages = np; b->pages_cap = nc;
}
memset(&b->pages[b->npages], 0, sizeof b->pages[b->npages]);
b->pages[b->npages].seg = seg;
b->pages[b->npages].seg_char = seg_char;
b->pages[b->npages].flags = flags;
b->npages++;
return 0;
}
/* ---- Phase 2A:滚动拼接。把 seg_idx 段码点布局并追加到 flat 页表。
* 建 N+1 时:删 N 的临时末页 → tail_cps + cps_new 拼接重排 → 首张拼接页回填 N(锚不变),
* 其余页按 first_char − tail_n 归入 N+1。末段/首段无 tail 时按整段直排。 ---- */
static int append_seg_pages(rd_book *b, int seg_idx, const uint32_t *cps, size_t n)
{
int have_tail = (seg_idx > 0 && b->tail_seg == seg_idx - 1 && b->tail_cps && b->tail_n > 0);
if (have_tail) {
if (b->segs[seg_idx - 1].page_count <= 0) return -4;
b->npages--; /* 删上一段临时末页(拼接页原位回填) */
b->segs[seg_idx - 1].page_count--;
}
size_t stitched_n = have_tail ? b->tail_n + n : n;
uint32_t *scps = NULL;
const uint32_t *chars;
if (have_tail) {
scps = (uint32_t *)malloc((stitched_n ? stitched_n : 1) * sizeof(uint32_t));
if (!scps) return -6;
memcpy(scps, b->tail_cps, b->tail_n * sizeof(uint32_t));
memcpy(scps + b->tail_n, cps, n * sizeof(uint32_t));
chars = scps;
} else {
chars = cps;
}
rd_line *lines = (rd_line *)malloc((stitched_n ? stitched_n : 1) * sizeof(rd_line));
rd_page *pages = (rd_page *)malloc((stitched_n ? stitched_n : 1) * sizeof(rd_page));
if (!lines || !pages) { free(lines); free(pages); free(scps); return -6; }
rd_layout_in in;
rd_layout_out out;
in.chars = chars; in.n = (int32_t)stitched_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)(stitched_n ? stitched_n : 1); out.max_pages = (int)(stitched_n ? stitched_n : 1);
int lrc = rd_layout(&in, &out);
if (lrc != RD_LAYOUT_OK) { free(lines); free(pages); free(scps); return -4; }
int first_own = 0;
if (have_tail) {
if (out.npages < 1) { free(lines); free(pages); free(scps); return -4; }
int rc = pages_push(b, seg_idx - 1, b->tail_seg_char,
(uint8_t)(out.lines[out.pages[0].first_line].para_start ? 1u : 0u));
if (rc != 0) { free(lines); free(pages); free(scps); return rc; }
b->segs[seg_idx - 1].page_count++;
first_own = 1;
}
b->segs[seg_idx].page_start = b->npages;
b->segs[seg_idx].page_count = 0;
for (int p = first_own; p < out.npages; p++) {
int32_t local = out.pages[p].first_char - (have_tail ? (int32_t)b->tail_n : 0);
if (local < 0) local = 0;
int rc = pages_push(b, seg_idx, local,
(uint8_t)(out.lines[out.pages[p].first_line].para_start ? 1u : 0u));
if (rc != 0) { free(lines); free(pages); free(scps); return rc; }
b->segs[seg_idx].page_count++;
}
/* 保存本段末页 tail(供建下一段拼接;末段也保存但不会用到)。 */
tail_clear(b);
if (b->segs[seg_idx].page_count > 0) {
int lastp = b->segs[seg_idx].page_start + b->segs[seg_idx].page_count - 1;
int32_t sc = b->pages[lastp].seg_char;
if (sc < 0 || (size_t)sc > n) { free(lines); free(pages); free(scps); return -4; }
b->tail_n = n - (size_t)sc;
b->tail_cps = (uint32_t *)malloc((b->tail_n ? b->tail_n : 1) * sizeof(uint32_t));
if (!b->tail_cps) { b->tail_n = 0; free(lines); free(pages); free(scps); return -6; }
if (b->tail_n) memcpy(b->tail_cps, cps + sc, b->tail_n * sizeof(uint32_t));
b->tail_seg = seg_idx;
b->tail_seg_char = sc;
}
free(lines); free(pages); free(scps);
return 0;
}
/* ---- R3.2 段路径:把 [seg_built, upto_seg] 逐段(512KB 段文件 → cps → 拼接布局)补齐进 flat 页表。
* 幂等:已建段跳过。开书只建到目标(页 0/保存进度),翻页/跳转再按需补齐 —— 免整书一次性布局。 */
static int seg_ensure_built(rd_book *b, int upto_seg)
{
if (!b->seg_dir[0] || b->fmt != RD_FMT_TXT) return 0;
int total = b->seg_n > 0 ? b->seg_n : 1;
if (upto_seg >= total) upto_seg = total - 1;
if (upto_seg < 0) upto_seg = 0;
int built_any = 0;
while (b->seg_built <= upto_seg) {
int idx = b->seg_built;
if (b->segs_cap <= idx) {
int nc = b->segs_cap ? b->segs_cap * 2 : 64;
if (nc <= idx) nc = idx + 1;
seg_t *ns = (seg_t *)realloc(b->segs, (size_t)nc * sizeof(seg_t));
if (!ns) return -6;
b->segs = ns; b->segs_cap = nc;
}
if (idx > 0 && tail_rebuild(b, idx - 1) != 0) return -4;
uint32_t *cps = NULL; size_t n = 0;
int rc = seg_load_cps(b, idx, &cps, &n);
if (rc != 0) return rc;
rc = append_seg_pages(b, idx, cps, n);
free(cps);
if (rc != 0) return rc;
b->seg_built++;
built_any = 1;
}
b->nsegs = b->seg_built;
if (built_any) index_cache_save(b);
return 0;
}
/* ---- Phase 2C:双段 cps 窗口(渲染微排版取码点;替换旧单段整段 lines/pages 缓存) ---- */
static void win_clear(rd_book *b)
{
for (int i = 0; i < 2; i++) {
free(b->win[i].cps);
b->win[i].cps = NULL;
b->win[i].n = 0;
b->win[i].seg = -1;
}
}
static cps_slot *win_find(rd_book *b, int seg)
{
if (b->win[0].seg == seg) return &b->win[0];
if (b->win[1].seg == seg) return &b->win[1];
return NULL;
}
static int win_load_seg(rd_book *b, int seg_idx, cps_slot *slot)
{
uint32_t *cps = NULL; size_t n = 0;
int rc = seg_load_cps(b, seg_idx, &cps, &n);
if (rc != 0) return rc;
free(slot->cps);
slot->cps = cps; slot->n = n; slot->seg = seg_idx;
return 0;
}
/* 确保段 seg_idx 码点在窗口内。双槽都占时,淘汰「离 seg_idx 更远」的槽(距相等淘汰更小段下标,
* 倾向保留下一段);滞回释放由 reader_on_tick 的 2%/4% 预取门控体现。 */
static int win_ensure(rd_book *b, int seg_idx)
{
if (seg_idx < 0 || seg_idx >= b->nsegs) return -1;
if (win_find(b, seg_idx)) return 0;
int victim = -1;
if (b->win[0].seg < 0) victim = 0;
else if (b->win[1].seg < 0) victim = 1;
else {
int d0 = b->win[0].seg - seg_idx; if (d0 < 0) d0 = -d0;
int d1 = b->win[1].seg - seg_idx; if (d1 < 0) d1 = -d1;
if (d0 > d1) victim = 0;
else if (d1 > d0) victim = 1;
else victim = (b->win[0].seg < b->win[1].seg) ? 0 : 1;
}
return win_load_seg(b, seg_idx, &b->win[victim]);
}
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;
char *dot = strrchr(x->base, '.'); if (dot) *dot = 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) { goto close; }
if (rd_src_open(&x->src, path) != 0) goto close;
x->fmt = rd_src_fmt(x->src);
x->enc = RD_ENC_UTF8;
rd_enc_t tmp_enc = RD_ENC_UTF8;
if (rd_src_txt_enc(x->src, &tmp_enc) == 0) x->enc = tmp_enc;
x->encname = fmt_encname(x->fmt, x->enc);
x->is_img = (x->fmt == RD_FMT_CBZ);
if (!x->is_img) {
if (rdft_init(&x->ft) != 0) goto close;
if (font_path && rdft_load_font(x->ft, x->font) != 0) { rdft_free(x->ft); x->ft = NULL; goto close; }
if (x->ft) rdft_set_px(x->ft, fs);
x->win[0].seg = x->win[1].seg = -1;
x->tail_seg = -1;
int seg_mode = 0;
/* R3.2 分段流式(TXT 且段 base 已配置):开书切 512KB 段文件,逐段按需布局(不全量)。 */
if (g_seg_base[0] && x->fmt == RD_FMT_TXT) {
char sdir[600];
if (rdseg_dir_for(x->path, sdir, sizeof sdir) == 0) {
strncpy(x->seg_dir, sdir, sizeof x->seg_dir - 1); x->seg_dir[sizeof x->seg_dir - 1] = 0;
int ns = rdseg_build(x->path, x->enc, x->seg_dir);
if (ns > 0) { x->seg_n = ns; seg_mode = 1; }
}
}
if (seg_mode) {
/* 命中缓存(书+版面+编码+字体未变)→ 载入已建 (segs,pages);否则仅布局段 0 起步,
* 翻页/跳转再 seg_ensure_built 补齐(治大书开书整表布局卡 UI 线程)。 */
x->segs = NULL; x->pages = NULL; x->nsegs = x->npages = 0;
x->seg_built = 0; x->segs_cap = x->pages_cap = 0;
if (index_cache_load(x) != 1) {
if (seg_ensure_built(x, 0) != 0) goto close;
}
if (x->nsegs < 1) { /* 段 0 空(理论)→ 兜底再建 */
if (seg_ensure_built(x, 0) != 0) goto close;
}
x->segs_cap = x->nsegs; x->pages_cap = x->npages;
x->seg_built = x->nsegs; /* 缓存命中时恢复到已建段数 */
} else {
/* 页索引缓存命中(书+字号+版面+编码+字体未变)→ 免整书重排,秒开;未命中 → 重排并落盘。 */
if (index_cache_load(x) != 1) {
if (build_index(x) != 0) goto close;
index_cache_save(x);
}
}
} else {
int pages = rd_src_pages(x->src);
x->npages = pages > 0 ? pages : 0;
}
x->cur = 0;
*b = x;
return 0;
close:
rd_book_close(x);
return -1;
}
void rd_book_close(rd_book *b)
{
if (!b) return;
free(b->segs);
free(b->pages);
win_clear(b);
tail_clear(b);
free(b->stitch_cps);
free(b->render_lines);
rdchapter_free(&b->chaps);
if (b->ft) rdft_free(b->ft);
if (b->src) rd_src_free(b->src);
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; }
int rd_book_is_image(rd_book *b) { return b ? b->is_img : 0; }
const char *rd_book_encname(const rd_book *b) { return b ? (b->encname ? b->encname : "") : ""; }
rd_enc_t rd_book_enc_get(const rd_book *b) { return b ? b->enc : RD_ENC_UTF8; }
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_path(const rd_book *b) { return b ? b->path : ""; }
/* R3:TXT 编码切换——重开 src(带覆盖编码)、重建页索引、回锚当前页。非 TXT/CBZ 返回 -1。
* 事务式:先开新 src 成功才换(失败保留旧 src,书仍可用,返回负值)。 */
int rd_book_set_enc(rd_book *b, rd_enc_t enc)
{
if (!b || b->is_img || b->fmt != RD_FMT_TXT) return -1;
if (enc == b->enc) return b->cur; /* 同编码,无需重建 */
int seg; int32_t ch;
rd_book_anchor(b, &seg, &ch);
rd_enc_t oldenc = b->enc;
rd_src *ns = NULL;
if (rd_src_open_with_enc(&ns, b->path, &enc) != 0) return -2; /* 旧 src 未动 */
rd_src_free(b->src); b->src = ns; /* 换新 */
b->enc = enc;
b->encname = fmt_encname(b->fmt, b->enc);
/* 重建索引(复用现值 fs/ls/content_*),再回锚 */
free(b->segs); free(b->pages); b->segs = NULL; b->pages = NULL;
b->nsegs = b->npages = 0; b->seg_built = 0; b->segs_cap = b->pages_cap = 0;
win_clear(b); /* 编码变了 → 码点空间不同,窗口/tail 全失效 */
tail_clear(b);
if (b->seg_dir[0]) {
/* R3.2 段路径:编码变了 → 段文件边界/解码不同 → 重建段文件 + 按需布局(段 0 起步) */
if (oldenc != b->enc) rdseg_clear(b->seg_dir);
b->seg_n = rdseg_build(b->path, b->enc, b->seg_dir);
if (seg_ensure_built(b, 0) != 0) return -3;
index_cache_save(b);
} else {
if (build_index(b) != 0) return -3; /* 新 src 有效但索引空 → 渲染返回 -1,安全降级 */
index_cache_save(b); /* 编码切换后重新落盘(键含 enc,与旧缓存并存不冲突) */
}
return rd_book_jump_to_anchor(b, seg, ch, b->fs, b->ls);
}
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;
/* R3.2 段路径:翻到已建表尾之外 → 按需补下一段(一次翻页只补到够用,不全量) */
if (b->seg_dir[0] && b->fmt == RD_FMT_TXT && np >= b->npages && b->seg_built < b->seg_n) {
while (np >= b->npages && b->seg_built < b->seg_n) {
if (seg_ensure_built(b, b->seg_built) != 0) break;
}
}
if (np >= b->npages) np = b->npages - 1;
b->cur = np;
return np;
}
int rd_book_set_page(rd_book *b, int page)
{
if (!b || b->npages <= 0) return 0;
/* R3.2 段路径:跳到已建表外 → 按需补段到目标页(跳到哪建到哪) */
if (b->seg_dir[0] && b->fmt == RD_FMT_TXT && page >= b->npages && b->seg_built < b->seg_n) {
while (page >= b->npages && b->seg_built < b->seg_n) {
if (seg_ensure_built(b, b->seg_built) != 0) break;
}
}
if (page < 0) page = 0;
if (page >= b->npages) page = b->npages - 1;
b->cur = page;
return page;
}
/* Phase 2C:空闲预取下一段(reader_on_tick 调)。当前页距下一段界 ≤2% 时把 s+1 段码点
* 提前解码进窗口(命中/未到阈值则幂等返回),翻到段界即零突发。 */
int rd_book_prefetch(rd_book *b)
{
if (!b || b->is_img || b->npages <= 0) return -1;
if (!(b->seg_dir[0] && b->fmt == RD_FMT_TXT)) return 0; /* 仅 TXT 段路径有段界预取意义 */
int cur = b->cur; if (cur < 0) cur = 0; if (cur >= b->npages) cur = b->npages - 1;
int s = b->pages[cur].seg;
if (s < 0 || s + 1 >= b->nsegs) return 0;
int bnd = b->segs[s + 1].page_start;
int d = cur - bnd; if (d < 0) d = -d;
if ((long long)d * 100 / b->npages > RD_WIN_NEAR_PCT) return 0;
return win_ensure(b, s + 1);
}
/* ---- Phase 2C:页级微排版。把 page_no 从页首字符起只排一页(复用 rd_layout_page)。
* 拼接页(TXT 段界末页)在窗口两段 cps 相接的临时缓冲上排,天然连续跨段。
* 输出:*chars 与 *chars_n = 绘制用码点视图(窗口 cps 或 b->stitch_cps,下次调用前勿 free),
* b->render_lines 填行;*nlines 行数。返回 0=成功,1=空页,负=错误。 */
static int page_micro_layout(rd_book *b, int page_no, const uint32_t **chars, size_t *chars_n, int *nlines)
{
*chars = NULL; *chars_n = 0; *nlines = 0;
rd_book_page *pg = &b->pages[page_no];
int seg_idx = (int)pg->seg;
if (seg_idx < 0 || seg_idx >= b->nsegs) return -1;
int stitched = (b->seg_dir[0] && b->fmt == RD_FMT_TXT) &&
(page_no == b->segs[seg_idx].page_start + b->segs[seg_idx].page_count - 1) &&
(seg_idx + 1 < b->nsegs);
if (win_ensure(b, seg_idx) != 0) return -2;
if (stitched && win_ensure(b, seg_idx + 1) != 0) return -2;
int lh = (b->fs * b->ls) / 100; if (lh < 1) lh = 1;
int max_lines = b->content_h / lh + 2;
if (b->render_lines_cap < max_lines) {
rd_line *t = (rd_line *)realloc(b->render_lines, (size_t)max_lines * sizeof(rd_line));
if (!t) return -6;
b->render_lines = t; b->render_lines_cap = max_lines;
}
cps_slot *s0 = win_find(b, seg_idx);
const uint32_t *view; size_t view_n; int32_t start;
if (!stitched) {
view = s0->cps; view_n = s0->n;
start = pg->seg_char;
if (start < 0) start = 0;
if ((size_t)start > view_n) start = (int32_t)view_n;
} else {
cps_slot *s1 = win_find(b, seg_idx + 1);
int32_t tail = pg->seg_char; if (tail < 0) tail = 0;
if ((size_t)tail > s0->n) return 1;
size_t tail_n = s0->n - (size_t)tail;
/* 拼接页只吃「一页能装下」的下一段前缀;按一页行数×行宽×2(零宽字符余量)估上界。 */
size_t budget = (size_t)(max_lines + 2) * (size_t)(b->content_w + 2) * 2u + 64u;
size_t take = s1->n < budget ? s1->n : budget;
size_t total = tail_n + take;
if (b->stitch_cap < total) {
uint32_t *t = (uint32_t *)realloc(b->stitch_cps, (total ? total : 1) * sizeof(uint32_t));
if (!t) return -6;
b->stitch_cps = t; b->stitch_cap = total;
}
memcpy(b->stitch_cps, s0->cps + tail, tail_n * sizeof(uint32_t));
memcpy(b->stitch_cps + tail_n, s1->cps, take * sizeof(uint32_t));
view = b->stitch_cps; view_n = total;
start = 0;
}
int mid_para = !(pg->flags & 1u);
rd_layout_in in;
in.chars = view; in.n = (int32_t)view_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;
int32_t next_start = 0;
int rc = rd_layout_page(&in, start, mid_para, b->render_lines, max_lines, nlines, &next_start);
if (rc == RD_LAYOUT_ERR_OVER) return -4;
if (rc != RD_LAYOUT_OK) return -3;
*chars = view; *chars_n = view_n;
return (*nlines > 0) ? 0 : 1;
}
/* ---- 文本页渲染(FreeType 字形 → 全屏 565) ---- */
static int render_text_page(rd_book *b, int page_no, uint16_t *out, int pitch, uint16_t bg565, uint16_t fg565)
{
int npix = b->pw * b->ph;
for (int i = 0; i < npix; i++) out[i] = bg565;
const uint32_t *chars = NULL; size_t chars_n = 0; int nlines = 0;
int rc = page_micro_layout(b, page_no, &chars, &chars_n, &nlines);
if (rc != 0) return rc;
int asc = rdft_ascender(b->ft);
int screen_x0 = b->mx;
int screen_y0 = b->top;
for (int li = 0; li < nlines; li++) {
rd_line *ln = &b->render_lines[li];
int pen = screen_x0 + ln->x;
int baseline = screen_y0 + ln->y + asc;
for (int32_t ci = ln->s; ci < ln->e; ci++) {
if (ci < 0 || (size_t)ci >= chars_n) continue;
uint32_t cp = chars[ci];
int adv = 0, bw = 0, bh = 0, left = 0, btop = 0;
const uint8_t *bmp = NULL;
rdft_glyph(b->ft, cp, &adv, &bmp, &bw, &bh, &left, &btop);
if (bmp && bw > 0)
rdft_blend565(out, pitch, b->pw, b->ph, pen, baseline, bmp, bw, bh, left, btop, fg565, bg565);
pen += adv;
}
}
return 0;
}
/* ---- CBZ 图片页渲染(rdimg 解码+降采样 → 内容区居中) ---- */
static int render_img_page(rd_book *b, int page_no, uint16_t *out, int pitch, uint16_t bg565)
{
int npix = b->pw * b->ph;
for (int i = 0; i < npix; i++) out[i] = bg565;
uint8_t *data = NULL; size_t size = 0;
if (rd_src_page(b->src, page_no, &data, &size) != 0) return -1;
uint16_t *scaled = NULL; int sw = 0, sh = 0;
if (rdimg_decode_scale(data, size, b->content_w, b->content_h, &scaled, &sw, &sh) != 0) {
free(data); return -2;
}
int x0 = b->mx + (b->content_w - sw) / 2;
int y0 = b->top + (b->content_h - sh) / 2;
int pitch_px = pitch / 2;
for (int y = 0; y < sh; y++) {
int ty = y0 + y; if (ty < 0 || ty >= b->ph) continue;
for (int x = 0; x < sw; x++) {
int tx = x0 + x; if (tx < 0 || tx >= b->pw) continue;
out[(size_t)ty * pitch_px + tx] = scaled[(size_t)y * sw + x];
}
}
free(scaled);
free(data);
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;
if (b->is_img) return render_img_page(b, page_no, out, pitch, bg565);
return render_text_page(b, page_no, out, pitch, bg565, fg565);
}
/* ---- 重排 + 回锚 ---- */
int rd_book_set_size(rd_book *b, int fs, int ls)
{
if (!b) return -1;
if (b->is_img) { b->fs = fs; b->ls = ls; return 0; }
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 (b->is_img) {
if (seg >= 0 && seg < b->npages) b->cur = seg;
return b->cur;
}
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);
win_clear(b); /* 页表重建 → 窗口/tail 状态整体复位(码点可复用,但按重建统一清) */
tail_clear(b);
seg_t *nos = b->segs; int n_oldsegs = b->nsegs;
rd_book_page *nop = b->pages; int n_oldpages = b->npages;
b->segs = NULL; b->pages = NULL; b->nsegs = b->npages = 0;
if (b->seg_dir[0] && b->fmt == RD_FMT_TXT) {
/* R3.2 段路径:只重布局到含锚的段(不全量;段文件不变,仅页表变) */
b->seg_built = 0; b->segs_cap = b->pages_cap = 0;
if (seg_ensure_built(b, seg) != 0) { b->segs = nos; b->pages = nop; b->nsegs = n_oldsegs; b->npages = n_oldpages; return -5; }
free(nos); free(nop);
index_cache_save(b);
} else {
if (build_index(b) != 0) { b->segs = nos; b->pages = nop; b->nsegs = n_oldsegs; b->npages = n_oldpages; return -5; }
free(nos); free(nop);
index_cache_save(b); /* 字号/行距改变后重新落盘(键含 fs/ls) */
}
} else if (b->seg_dir[0] && b->fmt == RD_FMT_TXT && seg >= b->seg_built) {
/* 锚在未建段 → 按需补齐(重开续读/目录跳转到后部) */
if (seg_ensure_built(b, seg) != 0) return -5;
}
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 字库兜底(文本格式) ---- */
int rd_book_render_text(rd_book *b, int page_no, rd_book_line_fn fn, void *ctx)
{
if (!b || !fn || b->is_img || b->npages <= 0 || page_no < 0 || page_no >= b->npages) return -1;
const uint32_t *chars = NULL; size_t chars_n = 0; int nlines = 0;
int rc = page_micro_layout(b, page_no, &chars, &chars_n, &nlines);
if (rc != 0) return rc;
uint8_t *line_utf8 = (uint8_t *)malloc((size_t)(b->content_w * 4 + 32));
if (!line_utf8) return -6;
for (int li = 0; li < nlines; li++) {
rd_line *ln = &b->render_lines[li];
int x = b->mx + ln->x, y = b->top + ln->y;
size_t o = 0;
for (int32_t ci = ln->s; ci < ln->e; ci++) {
if (ci < 0 || (size_t)ci >= chars_n) continue;
uint8_t tmp[4];
int n = rd_cp_to_utf8(chars[ci], tmp);
if (o + (size_t)n + 1 > (size_t)(b->content_w * 4 + 32)) break;
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;
}
/* ---- 目录(转 rd_src) ---- */
int rd_book_toc_n(const rd_book *b)
{
return (b && b->src && !b->is_img) ? rd_src_toc_n(b->src) : 0;
}
int rd_book_toc(const rd_book *b, int i, char *title, int tsz, int *seg, int32_t *ch)
{
if (!b || !b->src || b->is_img || !title) return -1;
rd_toc_entry t;
if (rd_src_toc(b->src, i, &t) != 0) return -1;
strncpy(title, t.title, (size_t)tsz - 1); title[tsz - 1] = 0;
if (seg) *seg = t.seg;
if (ch) *ch = t.ch;
return 0;
}