脏工作区现场收编: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 与容器一致)。
1260 lines
53 KiB
C
1260 lines
53 KiB
C
/* 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;
|
||
}
|