reader R1: os.meow.reader 书架+TXT 垂直链路骨架

- core: 编码检测(BOM→UTF-8严格→GB18030/Big5统计, Big5与GB重叠按GB18030兜底, §3.5风险注明)
  + GB18030双字节码表(uint16[126][190]=47880B, tools/gen_gb18030_table.py 生成, 生成物入仓)
  + 流式排版器(贪心断行+Kinsoku避头尾+分页+重排锚定, rd_meas_fn 测量回调替 FreeType, 宿主可测)
  + TXT 流式加载器(按段读, 不整文件入内存, \n 恒独立换行可安全分段)
- engine: FreeType 封装(FT_New_Face 按需读文件, NO_HINTING/LIGHT 灰阶, LRU 512 key=码点+字号,
  blend565, rdft_advance 只测宽不栅格化)
- app: 书架(扫 Books/ 识别 5 后缀, 最近读置顶, 空态提示) + 阅读页(W_CUSTOM 画布 3 栏翻页
  +中央菜单占位(字号±/行距±/返回) +顶栏进度%/章节名) + 书桥(段表+flat 页索引, 渲染, 重排回锚)
  + 进度持久化(MeowData/os.meow.reader/progress/, path hash→seg/ch/fs/ls/ts)
- 取舍: 正文字体 FreeType 直读 TF MeowOS/fonts/ TTF/OTF; 无字体回退 SDK 位图字库(meow_ui_text,
  未打包字体(许可/体积), 全 glyph 兜底 R2); 页位图缓存当前+前后各1页(3×691KB), 翻页动画默认无
- 内存实测(宿主 --wrap malloc, 73页书): 书结构 130KB + 当前段刮擦~1.6MB(行/页容量按 n 偏高,R2 收紧)
  + 字形缓存~650KB + 3页位图 2.07MB ≈ 4.45MB 动态, 在 ≤8MB 阅读态预算内
- 测试: 宿主 core 58/58(enc/gb18030金标准/kinsoku/page/reflow/txt); qemu-mipsel ft_smoke PASS
  + book_smoke(12页/渲染有墨/翻页/重排回锚) PASS; reader 交叉 -Wall -Wextra 0 warning
- 产物: reader(mipsel 静态1.2MB)+build_reader.sh(vendor FreeType 可复现); vendor/二进制 gitignore
This commit is contained in:
MeowOS committed 2026-09-01 23:26:42 +08:00
1 parent 33d3059038
commit 553a7d5d9e
26 files changed
+6155

No files matched your search

File diff suppressed because it is too large. Load diff
+274
View File
@@ -0,0 +1,274 @@
/* rdenc.c — 文本编码检测 + GB18030→UTF-8 + UTF-8 工具(实现见 rdenc.h)。 */
#include "rdenc.h"
#include "gb18030_table.h"
#include <stdlib.h>
#include <string.h>
/* ================= UTF-8 严格校验 ================= */
int rd_utf8_validate(const uint8_t *buf, size_t len)
{
size_t i = 0;
while (i < len) {
uint8_t b = buf[i];
if (b < 0x80) { i++; continue; }
unsigned n, lo, hi;
if ((b & 0xE0) == 0xC0) { n = 2; lo = 0x80; hi = 0xBF; }
else if ((b & 0xF0) == 0xE0) { n = 3; lo = 0x80; hi = 0xBF; }
else if ((b & 0xF8) == 0xF0) { n = 4; lo = 0x80; hi = 0xBF; }
else return 0;
/* 拒绝 overlong / 代理码 / 超 0x10FFFF */
if (n == 2 && (b & 0xFE) == 0xC0) return 0; /* C0/C1 overlong */
if (n == 3 && b == 0xE0) lo = 0xA0; /* 防 overlong < U+0800 */
if (n == 3 && b == 0xED) hi = 0x9F; /* 防代理 U+D800..DFFF */
if (n == 4 && b == 0xF0) lo = 0x90; /* 防 overlong < U+10000 */
if (n == 4 && b == 0xF4) hi = 0x8F; /* 防 > U+10FFFF */
if (i + n > len) return 0;
for (unsigned k = 1; k < n; k++) {
uint8_t c = buf[i + k];
uint8_t cl = (k == 1) ? (uint8_t)lo : 0x80;
uint8_t ch = (k == 1) ? (uint8_t)hi : 0xBF;
if (c < cl || c > ch) return 0;
}
i += n;
}
return 1;
}
/* ================= GB18030 双字节 → UTF-8 ================= */
static inline int gb_trail_ok(unsigned b)
{
return (b >= 0x40 && b <= 0x7E) || (b >= 0x80 && b <= 0xFE);
}
static inline int gb_col(unsigned b)
{
/* 列 = b - 0x40 - (b >= 0x80);b 已在合法范围(不含 0x7F) */
return (int)b - 0x40 - (b >= 0x80);
}
/* 1-3 字节 UTF-8 编码,返回字节数。cp 为 BMP(≤0xFFFF)。 */
static int enc_utf8(uint32_t cp, uint8_t *o)
{
if (cp <= 0x7F) { o[0] = (uint8_t)cp; return 1; }
if (cp <= 0x7FF) {
o[0] = (uint8_t)(0xC0 | (cp >> 6));
o[1] = (uint8_t)(0x80 | (cp & 0x3F));
return 2;
}
o[0] = (uint8_t)(0xE0 | (cp >> 12));
o[1] = (uint8_t)(0x80 | ((cp >> 6) & 0x3F));
o[2] = (uint8_t)(0x80 | (cp & 0x3F));
return 3;
}
int rd_cp_to_utf8(uint32_t cp, uint8_t out[4])
{
if (cp <= 0x7F) { out[0] = (uint8_t)cp; return 1; }
if (cp <= 0x7FF) {
out[0] = (uint8_t)(0xC0 | (cp >> 6));
out[1] = (uint8_t)(0x80 | (cp & 0x3F));
return 2;
}
if (cp <= 0xFFFF) {
out[0] = (uint8_t)(0xE0 | (cp >> 12));
out[1] = (uint8_t)(0x80 | ((cp >> 6) & 0x3F));
out[2] = (uint8_t)(0x80 | (cp & 0x3F));
return 3;
}
out[0] = (uint8_t)(0xF0 | (cp >> 18));
out[1] = (uint8_t)(0x80 | ((cp >> 12) & 0x3F));
out[2] = (uint8_t)(0x80 | ((cp >> 6) & 0x3F));
out[3] = (uint8_t)(0x80 | (cp & 0x3F));
return 4;
}
int rd_gb18030_to_utf8(const uint8_t *in, size_t inlen, uint8_t *out, size_t outcap, size_t *outlen)
{
if (!in || !outlen) return RD_ENC_ERR_INVAL;
if (!out && outcap) return RD_ENC_ERR_INVAL;
size_t o = 0;
size_t i = 0;
while (i < inlen) {
uint8_t b = in[i];
if (b < 0x80) {
if (o + 1 > outcap) { *outlen = o + 1; return RD_ENC_ERR_BUFSZ; }
out[o++] = b; i++; continue;
}
if (b >= 0x81 && b <= 0xFE && i + 1 < inlen) {
uint8_t t = in[i + 1];
if (gb_trail_ok(t)) {
uint32_t cp = gb_2byte[b - RD_GB_LEAD0][gb_col(t)];
uint8_t tmp[4];
int n;
if (cp == 0) { cp = 0xFFFD; }
n = enc_utf8(cp, tmp);
if (o + (size_t)n > outcap) { *outlen = o + (size_t)n; return RD_ENC_ERR_BUFSZ; }
memcpy(out + o, tmp, (size_t)n); o += (size_t)n;
i += 2; continue;
}
if (t >= 0x30 && t <= 0x39) { /* 4 字节扩展段 → 跳 4 字节 + U+FFFD */
if (i + 4 > inlen) { /* 残缺:只跳可用部分 */
uint8_t tmp[4]; int n = enc_utf8(0xFFFD, tmp);
if (o + (size_t)n > outcap) { *outlen = o + (size_t)n; return RD_ENC_ERR_BUFSZ; }
memcpy(out + o, tmp, (size_t)n); o += (size_t)n;
i = inlen; break;
}
uint8_t tmp[4]; int n = enc_utf8(0xFFFD, tmp);
if (o + (size_t)n > outcap) { *outlen = o + (size_t)n; return RD_ENC_ERR_BUFSZ; }
memcpy(out + o, tmp, (size_t)n); o += (size_t)n;
i += 4; continue;
}
}
/* 非法首字节 / 尾字节不成对 → U+FFFD */
{
uint8_t tmp[4]; int n = enc_utf8(0xFFFD, tmp);
if (o + (size_t)n > outcap) { *outlen = o + (size_t)n; return RD_ENC_ERR_BUFSZ; }
memcpy(out + o, tmp, (size_t)n); o += (size_t)n;
i++; continue;
}
}
*outlen = o;
return RD_ENC_OK;
}
/* ================= 编码检测 ================= */
static inline int gb_trail_q(unsigned b)
{
return (b >= 0x40 && b <= 0x7E) || (b >= 0x80 && b <= 0xFE);
}
static inline int big5_trail_q(unsigned b)
{
return (b >= 0x40 && b <= 0x7E) || (b >= 0xA1 && b <= 0xFE);
}
rd_enc_t rd_enc_detect(const uint8_t *buf, size_t len, rd_enc_stats *st)
{
if (st) { st->gb_pairs = st->big5_pairs = st->stray = 0; }
if (!buf || len == 0) return RD_ENC_ASCII;
/* 1) BOM */
if (len >= 3 && buf[0] == 0xEF && buf[1] == 0xBB && buf[2] == 0xBF) return RD_ENC_UTF8;
if (len >= 2 && buf[0] == 0xFF && buf[1] == 0xFE) return RD_ENC_UTF16LE;
if (len >= 2 && buf[0] == 0xFE && buf[1] == 0xFF) return RD_ENC_UTF16BE;
/* 2) 严格 UTF-8 */
if (rd_utf8_validate(buf, len)) {
/* 纯 ASCII 判 ASCII(信息性;UTF-8 子集) */
int ascii = 1;
for (size_t i = 0; i < len; i++) if (buf[i] >= 0x80) { ascii = 0; break; }
return ascii ? RD_ENC_ASCII : RD_ENC_UTF8;
}
/* 3) 双字节区段统计(GB18030 兼容 GBK;Big5 尾字节不同)。
* GB 有效 = 码表 gb_2byte 查到非 0(GBK/GB18030 已映射码位);
* Big5 专属 = 结构上 Big5 合法、但对 GB 码表无效(Big5 独有码位)。
* 二者比较更可靠(结构上每个 GBK 双字节对也是合法 Big5 结构,无法区分)。 */
long gb = 0, big5 = 0, stray = 0;
for (size_t i = 0; i < len; ) {
uint8_t b = buf[i];
if (b < 0x80) { i++; continue; }
if (b >= 0x81 && b <= 0xFE && i + 1 < len) {
uint8_t t = buf[i + 1];
int gb_ok = gb_trail_q(t) &&
gb_2byte[b - RD_GB_LEAD0][gb_col(t)] != 0;
int b5_ok = big5_trail_q(t);
if (gb_ok) gb++;
if (b5_ok && !gb_ok) big5++; /* Big5 专属:GB 码表无效 */
i += 2; continue;
}
stray++; i++;
}
if (st) { st->gb_pairs = gb; st->big5_pairs = big5; st->stray = stray; }
/* 阈值(经验值):双字节对太少 → 乱码/不确定 → 兜底 GB18030(最常见中文编码)。
* 注(READER_ENGINE_NOTES §3.5 风险):Big5 与 GB18030 双字节区大量重叠,码表有效性
* 无法可靠区分(简体 GB2312 文本 trail 0xA1-0xFE 恰是 Big5 trail 区)。R1 双字节一律
* 按 GB18030 兜底;Big5 经此检测也按 GB18030 试(v1 不转),真实区分需用户手动切编码
* (列 R2)。 */
if (gb + big5 < (long)(len / 5)) return RD_ENC_GB18030;
return RD_ENC_GB18030;
}
/* ================= UTF-16 → UTF-8(简易,BOM 已对齐) ================= */
static int utf16_to_utf8(rd_enc_t enc, const uint8_t *in, size_t inlen,
uint8_t *out, size_t outcap, size_t *outlen)
{
int be = (enc == RD_ENC_UTF16BE);
/* 跳过 BOM(2 字节) */
size_t i = 0;
if (inlen >= 2 && ((be && in[0] == 0xFE && in[1] == 0xFF) ||
(!be && in[0] == 0xFF && in[1] == 0xFE))) i = 2;
size_t o = 0;
while (i + 1 < inlen) {
uint32_t u = be ? (((uint32_t)in[i] << 8) | in[i + 1])
: (((uint32_t)in[i + 1] << 8) | in[i]);
i += 2;
uint32_t cp = u;
if (u >= 0xD800 && u <= 0xDBFF) { /* 高代理 */
if (i + 1 < inlen) {
uint32_t u2 = be ? (((uint32_t)in[i] << 8) | in[i + 1])
: (((uint32_t)in[i + 1] << 8) | in[i]);
if (u2 >= 0xDC00 && u2 <= 0xDFFF) {
cp = 0x10000 + ((u - 0xD800) << 10) + (u2 - 0xDC00);
i += 2;
} else cp = 0xFFFD;
} else cp = 0xFFFD;
} else if (u >= 0xDC00 && u <= 0xDFFF) cp = 0xFFFD; /* 孤立低代理 */
uint8_t tmp[4];
int n = rd_cp_to_utf8(cp, tmp);
if (o + (size_t)n > outcap) { *outlen = o + (size_t)n; return RD_ENC_ERR_BUFSZ; }
memcpy(out + o, tmp, (size_t)n); o += (size_t)n;
}
*outlen = o;
return RD_ENC_OK;
}
int rd_to_utf8(rd_enc_t enc, const uint8_t *in, size_t inlen, uint8_t *out, size_t outcap, size_t *outlen)
{
if (enc == RD_ENC_UTF16LE || enc == RD_ENC_UTF16BE) return utf16_to_utf8(enc, in, inlen, out, outcap, outlen);
if (enc == RD_ENC_UTF8 || enc == RD_ENC_ASCII) {
/* 去 UTF-8 BOM(若有) */
size_t off = 0;
if (inlen >= 3 && in[0] == 0xEF && in[1] == 0xBB && in[2] == 0xBF) off = 3;
if (inlen - off > outcap) { *outlen = inlen - off; return RD_ENC_ERR_BUFSZ; }
memcpy(out, in + off, inlen - off); *outlen = inlen - off;
return RD_ENC_OK;
}
/* GB18030 / Big5(v1 按 GB18030 试) */
return rd_gb18030_to_utf8(in, inlen, out, outcap, outlen);
}
/* ================= UTF-8 → 码点数组 ================= */
int rd_utf8_to_cp(const uint8_t *utf8, size_t len, uint32_t **cps, size_t *n)
{
if (!utf8 || !cps || !n) return RD_UTF8_ERR_INVAL;
*cps = NULL; *n = 0;
/* 上界:每个输入字节至多一个码点(单字节 ASCII / 非法的每字节一个 U+FFFD)。 */
uint32_t *arr = (uint32_t *)malloc((len ? len : 1) * sizeof(uint32_t));
if (!arr) return RD_UTF8_ERR_NOMEM;
size_t cnt = 0, i = 0;
while (i < len) {
uint8_t b = utf8[i];
if (b < 0x80) { arr[cnt++] = b; i++; continue; }
unsigned q, lo, hi;
if ((b & 0xE0) == 0xC0) { q = 2; lo = 0x80; hi = 0xBF; }
else if ((b & 0xF0) == 0xE0) { q = 3; lo = 0x80; hi = 0xBF; }
else if ((b & 0xF8) == 0xF0) { q = 4; lo = 0x80; hi = 0xBF; }
else { arr[cnt++] = 0xFFFD; i++; continue; }
if (q == 2 && (b & 0xFE) == 0xC0) { arr[cnt++] = 0xFFFD; i++; continue; }
if (q == 3 && b == 0xE0) lo = 0xA0;
if (q == 3 && b == 0xED) hi = 0x9F;
if (q == 4 && b == 0xF0) lo = 0x90;
if (q == 4 && b == 0xF4) hi = 0x8F;
if (i + q > len) { arr[cnt++] = 0xFFFD; i++; continue; }
uint32_t cp = b & (0xFF >> (q + 1));
int bad = 0;
for (unsigned k = 1; k < q; k++) {
uint8_t c = utf8[i + k];
uint8_t cl = (k == 1) ? (uint8_t)lo : 0x80, ch = (k == 1) ? (uint8_t)hi : 0xBF;
if (c < cl || c > ch) { bad = 1; break; }
cp = (cp << 6) | (c & 0x3F);
}
if (bad) { arr[cnt++] = 0xFFFD; i++; continue; }
arr[cnt++] = cp; i += q;
}
*cps = arr; *n = cnt;
return RD_ENC_OK;
}
+68
View File
@@ -0,0 +1,68 @@
/* rdenc.h — 阅读器文本编码检测 + GB18030→UTF-8 解码 + UTF-8 工具(纯 C,宿主可测)。
*
* 对齐 devdocs/plan/READER_ENGINE_NOTES.md §3(zLibrary 轻量化:BOM + 严格 UTF-8 + 双字节区段统计,
* 不抄 N-gram 马尔可夫):
* - BOM 优先:FE FF=UTF-16BE、FF FE=UTF-16LE、EF BB BF=UTF-8;
* - 无 BOM → 严格 UTF-8 校验(合法即 UTF-8;ASCII 属 UTF-8 子集);
* - 非 UTF-8 → GB18030(GBK 超集) / Big5 双字节区段统计(阈值经验值,见 READER_ENGINE_NOTES §3.3);
* - **R1 只做 GB18030→UTF-8 双字节映射**(uint16[126][190] 码表),4 字节扩展段 → U+FFFD;
* Big5 检测出来也按 GB18030 试(v1 不转 Big5,正文会乱但可读,注明)。
*
* 无 SDK 依赖;`#include "gb18030_table.h"`(生成物,见 tools/gen_gb18030_table.py)。
*/
#ifndef RDENC_H
#define RDENC_H
#include <stddef.h>
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
typedef enum {
RD_ENC_ASCII = 0, /* 纯 7-bit,无非法字节(UTF-8 子集) */
RD_ENC_UTF8,
RD_ENC_UTF16LE,
RD_ENC_UTF16BE,
RD_ENC_GB18030, /* GBK 超集(解码器按 2 字节码表) */
RD_ENC_BIG5, /* v1 检测但按 GB18030 解码 */
} rd_enc_t;
/* 检测统计(诊断 + 阈值用;值可为 0)。 */
typedef struct rd_enc_stats {
long gb_pairs; /* 合法 GB18030/GBK 双字节对计数 */
long big5_pairs; /* 合法 Big5 双字节对计数 */
long stray; /* 0x80..0xFF 中无法成对的散落字节 */
} rd_enc_stats;
/* 编码检测:对 buf[0..len)(建议前 64KB)返回最可能编码,并填 st(可 NULL)。 */
rd_enc_t rd_enc_detect(const uint8_t *buf, size_t len, rd_enc_stats *st);
/* 严格 UTF-8 校验:结构非法即返回 0;合法返回 1(允许含 NUL?不,遇 NUL 提前停)。 */
int rd_utf8_validate(const uint8_t *buf, size_t len);
/* GB18030(GBK) 双字节 → UTF-8。成功 0;输出不足 RD_ENC_ERR_BUFSZ 且 *outlen=所需;非法字节→U+FFFD。
* 4 字节扩展段(lead+0x30..0x39 开头)→ 跳过该 4 字节并输出一个 U+FFFD。 */
#define RD_ENC_OK 0
#define RD_ENC_ERR_INVAL -1 /* 参数非法 */
#define RD_ENC_ERR_BUFSZ -2 /* out 不足;*outlen 已填所需 */
int rd_gb18030_to_utf8(const uint8_t *in, size_t inlen, uint8_t *out, size_t outcap, size_t *outlen);
/* UTF-8 → 码点数组(布局用;malloc 分配 *cps,调用方 free)。成功 0。 *n = 码点数。
* 非法序列按一个 U+FFFD 收(宽松);len 为输入字节长。 */
#define RD_UTF8_ERR_INVAL -1
#define RD_UTF8_ERR_NOMEM -2
int rd_utf8_to_cp(const uint8_t *utf8, size_t len, uint32_t **cps, size_t *n);
/* 码点 → UTF-8(写 out,返回写入字节数;out 须 ≥4 字节)。 */
int rd_cp_to_utf8(uint32_t cp, uint8_t out[4]);
/* 通用:任意检测编码 → UTF-8(rdenc 已知的各编码)。UTF-16LE/BE 走简易 BOM 对齐 + 码点转换;
* Big5 v1 按 GB18030 双字节表转(会乱但可读,注明)。返回码同 rd_gb18030_to_utf8。 */
int rd_to_utf8(rd_enc_t enc, const uint8_t *in, size_t inlen, uint8_t *out, size_t outcap, size_t *outlen);
#ifdef __cplusplus
}
#endif
#endif /* RDENC_H */
+257
View File
@@ -0,0 +1,257 @@
/* rdlayout.c — 流式排版器 core(实现见 rdlayout.h)。纯 C,宿主可测,无 SDK/Freetype 依赖。
*
* 两遍:
* A) 贪心断行:逐段扫描,记录「最后一个允许断且能放下」的位置;溢出即断;
* Kinsoku 微调(左括号不居行尾 / 右标点不居行首,右标点簇 ≤2 上拉)。
* *连续* y(含段间距)写入 line.y——仅供分页阶段参考。
* B) 分页:按行高+段间距累计超内容高断页,得到 pages[](每页 = lines[] 区间 + 首字符偏移);
* 孤行守卫 post-pass:段首行不单独留页底(孤儿上推)。
*/
#include "rdlayout.h"
#include <stdint.h>
/* ---------- 字符属性 ---------- */
enum {
RD_CH_SPACE = 1u << 0,
RD_CH_CJK = 1u << 1,
RD_CH_HYPH = 1u << 2,
RD_CH_HARD = 1u << 3,
RD_CH_OPEN = 1u << 4, /* 左括号/前引号:行尾禁断 */
RD_CH_CLOSE = 1u << 5, /* 右标点:行首禁断 */
RD_CH_CJKPT = 1u << 6,
};
static int is_cjk_wide(uint32_t c)
{
if (c >= 0x2E80 && c <= 0x2EFF) return 1;
if (c >= 0x3040 && c <= 0x30FF) return 1;
if (c >= 0x3400 && c <= 0x4DBF) return 1;
if (c >= 0x4E00 && c <= 0x9FFF) return 1;
if (c >= 0xF900 && c <= 0xFAFF) return 1;
if (c >= 0xFF00 && c <= 0xFFEF) return 1;
if (c >= 0x20000) return 1;
return 0;
}
static int is_cjk_left_punct(uint32_t c)
{
switch (c) {
case 0x2018: case 0x201C:
case 0x3008: case 0x300A: case 0x300C: case 0x300E: case 0x3010:
case 0xFF08:
return 1;
default: return 0;
}
}
static int is_cjk_close_punct(uint32_t c)
{
switch (c) {
case 0x3001: case 0x3002:
case 0xFF01: case 0xFF0C: case 0xFF0E: case 0xFF1A: case 0xFF1B:
case 0xFF1F:
case 0x3009: case 0x300B: case 0x300D: case 0x300F: case 0x3011:
case 0xFF09:
case 0x2026:
return 1;
default: return 0;
}
}
static int is_latin_close(uint32_t c)
{
return c == ',' || c == '.' || c == ';' || c == ':' || c == '!' || c == '?' ||
c == ')' || c == ']' || c == '}' || c == '"' || c == '\'';
}
static int is_latin_open(uint32_t c)
{
return c == '(' || c == '[' || c == '{' || c == '"' || c == '\'';
}
static unsigned char_props(uint32_t c)
{
unsigned f = 0;
if (c == ' ') f |= RD_CH_SPACE;
else if (c == '\n') f |= RD_CH_HARD;
else if (c == 0xAD) f |= RD_CH_HYPH;
if (is_cjk_wide(c)) {
f |= RD_CH_CJK;
if (is_cjk_left_punct(c)) f |= RD_CH_CJKPT | RD_CH_OPEN;
else if (is_cjk_close_punct(c)) f |= RD_CH_CJKPT | RD_CH_CLOSE;
else if ((c >= 0x3000 && c <= 0x303F) || (c >= 0xFF00 && c <= 0xFFEF)) f |= RD_CH_CJKPT;
} else {
if (is_latin_open(c)) f |= RD_CH_OPEN;
else if (is_latin_close(c)) f |= RD_CH_CLOSE;
}
return f;
}
/* 在 chars[idx](即断点后一整段)后能否断开:左括号不居行尾、右标点不居行首。 */
static int wrap_ok_after(const uint32_t *ch, int pe, int idx)
{
if (idx < 0) return 0;
unsigned cur = char_props(ch[idx]);
if (cur & RD_CH_OPEN) return 0; /* 行尾禁:左括号/前引号 */
if (idx + 1 < pe) {
unsigned nxt = char_props(ch[idx + 1]);
if (nxt & RD_CH_CLOSE) return 0; /* 行首禁:右标点 */
}
return 1;
}
static int approx_adv(uint32_t cp, int fs)
{
if (cp == '\n' || cp == '\r') return 0;
if (cp == ' ' || cp == '\t') return fs * 2 / 3 < 1 ? 1 : fs * 2 / 3;
if (is_cjk_wide(cp)) return fs;
return fs / 2 < 1 ? 1 : fs / 2;
}
static int char_adv(const rd_layout_in *in, uint32_t cp)
{
int adv = 0;
if (in->meas && in->meas(cp, in->fs, &adv, in->mctx) == 0 && adv >= 0) return adv;
return approx_adv(cp, in->fs);
}
/* 段内断一行;返回该行后剩余段起点(=line_e;达到段末则 == pe)。 */
static int para_break_line(const rd_layout_in *in, int pe, int32_t *line_s_in,
int first_line_of_para, rd_line *ln)
{
int32_t line_s = *line_s_in;
int32_t pos = line_s;
int lastNormal = -1;
int x = 0;
int32_t i = pos;
while (i < pe) {
uint32_t c = in->chars[i];
int adv = char_adv(in, c);
if (x + adv > in->page_w && i > line_s) break; /* 溢出断 */
x += adv;
unsigned f = char_props(c);
if ((f & (RD_CH_SPACE | RD_CH_CJK | RD_CH_HYPH)) && wrap_ok_after(in->chars, pe, i)) lastNormal = i;
i++;
}
int32_t line_e;
if (i < pe) { /* 溢出 */
if (lastNormal >= line_s) line_e = lastNormal + 1;
else line_e = (i > line_s) ? i : (line_s + 1 < pe ? line_s + 1 : pe);
} else {
line_e = i; /* 段末自然结束 */
}
if (line_e <= line_s) line_e = (line_s + 1 < pe) ? line_s + 1 : pe;
/* Kinsoku 微调(v1 子集) */
while (line_e > line_s + 1 && is_cjk_left_punct(in->chars[line_e - 1])) line_e--; /* 左括号推下行 */
if (line_e < pe && is_cjk_close_punct(in->chars[line_e])) { /* 右标点上拉 ≤2 */
int cnt = 1;
while (line_e + cnt < pe && is_cjk_close_punct(in->chars[line_e + cnt])) cnt++;
int pull = cnt < 3 ? cnt : 2;
while (pull-- > 0 && line_e < pe) line_e++;
}
if (line_e <= line_s) line_e = (line_s + 1 < pe) ? line_s + 1 : pe;
/* 实际宽 */
int lw = 0;
for (int32_t k = line_s; k < line_e; k++) lw += char_adv(in, in->chars[k]);
if (lw > in->page_w) lw = in->page_w;
ln->s = line_s;
ln->e = line_e;
ln->x = first_line_of_para ? in->indent : 0;
ln->w = lw;
ln->para_start = first_line_of_para ? 1 : 0;
ln->para_end = (line_e >= pe) ? 1 : 0;
*line_s_in = line_e;
return line_e;
}
int rd_layout(const rd_layout_in *in, rd_layout_out *out)
{
if (!in || !out || !in->chars || in->n < 0 || !out->lines || !out->pages) return RD_LAYOUT_ERR_INVAL;
if (in->fs <= 0 || in->page_w <= 0 || in->page_h <= 0) return RD_LAYOUT_ERR_INVAL;
int lh = (in->fs * in->line_scale) / 100;
if (lh < 1) lh = 1;
int pspacing = lh / 2;
if (pspacing < 0) pspacing = 0;
int nlines = 0, npages = 0;
int y = 0; /* 连续 y(阶段 A;段间距已加) */
int para_pending = 0; /* 是否已有段落在前(需段间距) */
int32_t ps = 0;
while (ps < in->n) {
while (ps < in->n && (in->chars[ps] == '\n' || in->chars[ps] == '\r')) ps++;
if (ps >= in->n) break;
int32_t pe = ps;
while (pe < in->n && in->chars[pe] != '\n') pe++;
if (para_pending && nlines > 0) y += pspacing;
para_pending = 1;
int32_t pos = ps;
int first_line = 1;
while (pos < pe) {
rd_line ln;
if (nlines >= out->max_lines) return RD_LAYOUT_ERR_OVER;
para_break_line(in, pe, &pos, first_line, &ln);
ln.y = y;
out->lines[nlines++] = ln;
y += lh;
first_line = 0;
if (ln.para_end) break;
}
ps = pos;
while (ps < in->n && in->chars[ps] == '\n') ps++;
if (pos >= pe && ps >= in->n) break;
}
/* 阶段 B:分页 */
if (nlines == 0) { out->nlines = 0; out->npages = 0; return RD_LAYOUT_OK; }
int page_start = 0;
int cur_y = 0;
for (int l = 0; l < nlines; l++) {
int extra = (out->lines[l].para_start && l > page_start) ? pspacing : 0;
int h = lh + extra;
if (cur_y + h > in->page_h && l > page_start) {
int np_start = l;
/* 孤行守卫:上页最后一行是段首行且段未结束 → 该行随段推到下一页 */
if (l - 1 >= page_start && out->lines[l - 1].para_start && !out->lines[l - 1].para_end) {
np_start = l - 1;
}
if (npages >= out->max_pages) return RD_LAYOUT_ERR_OVER;
out->pages[npages].first_char = out->lines[page_start].s;
out->pages[npages].first_line = page_start;
out->pages[npages].nlines = np_start - page_start;
npages++;
page_start = np_start;
cur_y = 0;
}
/* 重新计算本行在本页的 top(若 page_start 因孤儿调整而变化,extra 需重算) */
extra = (out->lines[l].para_start && l > page_start) ? pspacing : 0;
out->lines[l].y = cur_y + extra;
cur_y += lh + extra;
}
if (nlines > page_start) {
if (npages >= out->max_pages) return RD_LAYOUT_ERR_OVER;
out->pages[npages].first_char = out->lines[page_start].s;
out->pages[npages].first_line = page_start;
out->pages[npages].nlines = nlines - page_start;
npages++;
}
out->nlines = nlines;
out->npages = npages;
return RD_LAYOUT_OK;
}
int rd_layout_page_for_anchor(const rd_layout_out *out, int32_t anchor_char)
{
if (!out || out->npages <= 0) return 0;
for (int p = 0; p < out->npages; p++) {
int32_t fc = out->pages[p].first_char;
int32_t nfc = (p + 1 < out->npages) ? out->pages[p + 1].first_char : INT32_MAX;
if (anchor_char < nfc && (anchor_char >= fc || p == 0)) return p;
}
return out->npages - 1;
}
+75
View File
@@ -0,0 +1,75 @@
/* rdlayout.h — 流式排版器 core:贪心断行 + Kinsoku 避头尾 + 分页 + 重排锚定。
*
* 对齐 devdocs/plan/READER_ENGINE_NOTES.md §1(crengine lvtextfm 思想,不链库):
* - 贪心断行(非 Knuth-Plass):断点候选 = 空格 / CJK / 软连字符;`\n` 强制换行;
* - Kinsoku 避头尾:字符属性(AVOID_WRAP_AFTER/BEFORE)+ CJK 标点区间(左括号不居行尾、
* 右标点不居行首,右标点簇 ≤2 位移优先上拉);
* - 分页:行高累计超内容高断页;孤行守卫(段首行不单独留页底)。
* - 测量由 `rd_meas_fn` 回调提供(设备=FreeType,宿主单测=桩),core 零 SDK/Freetype 依赖。
*
* 坐标系:输入 chars 为本章/分段码点数组;断行输出在「内容区」坐标(x=0..page_w-1,y=0..page_h-1,
* 外边距/版心外空由调用方裁掉)。首行缩进 = in->indent。
*/
#ifndef RDLAYOUT_H
#define RDLAYOUT_H
#include <stddef.h>
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
/* 测量:cp→advance 像素。返回 0=成功(*adv 已填,≥0)。 */
typedef int (*rd_meas_fn)(uint32_t cp, int fs_px, int *adv, void *ctx);
typedef struct rd_layout_in {
const uint32_t *chars; /* 本章/分段码点数组(调用方持有,layout 不拷贝) */
int32_t n; /* 码点数 */
int fs; /* 字号 px */
int line_scale; /* 行距百分比(100..200):LH = fs*line_scale/100 */
int page_w; /* 内容区宽 px */
int page_h; /* 内容区高 px */
int indent; /* 首行缩进 px */
rd_meas_fn meas; /* 测量回调(NULL → 用内置近似宽) */
void *mctx;
} rd_layout_in;
typedef struct rd_line {
int32_t s, e; /* 本行码点区间 [s,e)(s=行首,e=行尾+1,<= 段结束) */
int x; /* 行首 x(内容区坐标;首行=indent 否则 0) */
int y; /* 行顶 y(内容区坐标) */
int w; /* 已用行宽 px(≤ page_w) */
uint8_t para_start; /* 1=段的第一个物理行 */
uint8_t para_end; /* 1=段的最后一个物理行 */
} rd_line;
typedef struct rd_page {
int32_t first_char; /* 本页首字符在本章内的偏移(页索引锚) */
int first_line; /* lines[] 起始下标 */
int nlines; /* 本页行数 */
} rd_page;
typedef struct rd_layout_out {
rd_line *lines; /* 容量 in->...(调用方给) */
rd_page *pages; /* 容量 in->...(调用方给) */
int max_lines;
int max_pages;
int nlines;
int npages;
} rd_layout_out;
#define RD_LAYOUT_OK 0
#define RD_LAYOUT_ERR_INVAL -1
#define RD_LAYOUT_ERR_OVER -2 /* lines/pages 容量不足(npages/nlines 已填实际需要) */
int rd_layout(const rd_layout_in *in, rd_layout_out *out);
/* 重排锚定:在 layout 输出里找第一个满足 `line.s <= anchor_char < 下个 line.s` 的**行**所属的页。
* 返回页下标(0..npages-1);anchor_char 越界 clamp 到首/末页。 */
int rd_layout_page_for_anchor(const rd_layout_out *out, int32_t anchor_char);
#ifdef __cplusplus
}
#endif
#endif /* RDLAYOUT_H */
+116
View File
@@ -0,0 +1,116 @@
/* rdtxt.c — TXT 流式加载器(实现见 rdtxt.h)。 */
#include "rdtxt.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
struct rdtxt_reader {
FILE *f;
char path[320];
rd_enc_t enc;
uint8_t *raw; /* 原始字节块缓冲 */
size_t rawsz; /* 分配容量 */
int eof;
};
int rdtxt_probe(const char *path, rdtxt_info *info)
{
if (!path || !info) return -1;
FILE *f = fopen(path, "rb");
if (!f) return -1;
if (fseek(f, 0, SEEK_END) != 0) { fclose(f); return -2; }
long sz = ftell(f);
if (sz < 0) { fclose(f); return -2; }
fseek(f, 0, SEEK_SET);
uint8_t buf[65536];
size_t n = fread(buf, 1, sizeof buf, f);
rd_enc_stats st;
rd_enc_t enc = rd_enc_detect(buf, n, &st);
fclose(f);
info->enc = enc;
info->file_size = sz;
if (strlen(path) >= sizeof info->path) return -2;
strcpy(info->path, path);
return 0;
}
int rdtxt_reader_open(const char *path, const rdtxt_info *info, rdtxt_reader **r)
{
if (!path || !info || !r) return -1;
rdtxt_reader *x = (rdtxt_reader *)calloc(1, sizeof *x);
if (!x) return -6;
x->f = fopen(path, "rb");
if (!x->f) { free(x); return -1; }
x->raw = (uint8_t *)malloc(RDTXT_SEG_RAW_MAX + 1);
if (!x->raw) { fclose(x->f); free(x); return -6; }
x->rawsz = RDTXT_SEG_RAW_MAX + 1;
if (strlen(path) >= sizeof x->path) { free(x->raw); fclose(x->f); free(x); return -2; }
strcpy(x->path, path);
x->enc = info->enc;
x->eof = 0;
*r = x;
return 0;
}
void rdtxt_reader_free(rdtxt_reader *r)
{
if (!r) return;
if (r->f) fclose(r->f);
free(r->raw);
free(r);
}
const char *rdtxt_reader_path(const rdtxt_reader *r) { return r ? r->path : ""; }
rd_enc_t rdtxt_reader_enc(const rdtxt_reader *r) { return r ? r->enc : RD_ENC_UTF8; }
int rdtxt_reader_next(rdtxt_reader *r, uint8_t *utf8, size_t cap,
size_t *utf8len, size_t *file_off, size_t *file_len)
{
if (!r || !utf8 || !utf8len || !file_off || !file_len) return -1;
if (r->eof) return 1;
long start = ftell(r->f);
if (start < 0) { r->eof = 1; return 1; }
size_t raw_n = 0;
int got = 0;
while (raw_n < r->rawsz - 1) {
int c = fgetc(r->f);
if (c == EOF) { r->eof = 1; break; }
r->raw[raw_n++] = (uint8_t)c;
got = 1;
/* 段落边界达成目标字节约停止(`\n` 恒为独立换行) */
if (c == '\n' && raw_n >= RDTXT_SEG_UTF8_MAX / 2) break;
}
if (!got) { r->eof = 1; return 1; }
size_t outlen = 0;
int rc = rd_to_utf8(r->enc, r->raw, raw_n, utf8, cap, &outlen);
if (rc == RD_ENC_ERR_BUFSZ) return -2; /* 调用方 cap 太小 */
if (rc != RD_ENC_OK) return -3;
*utf8len = outlen;
*file_off = (size_t)start;
*file_len = raw_n;
return 0;
}
int rdtxt_convert_span(const char *path, rd_enc_t enc, size_t raw_off, size_t raw_len,
uint8_t *out, size_t cap, size_t *outlen)
{
if (!path || !out || !outlen) return -1;
FILE *f = fopen(path, "rb");
if (!f) return -1;
if (fseek(f, (long)raw_off, SEEK_SET) != 0) { fclose(f); return -2; }
/* 读整段(raw_len ≤ RDTXT_SEG_RAW_MAX,有界) */
size_t want = raw_len;
uint8_t *tmp = (uint8_t *)malloc(want ? want : 1);
if (!tmp) { fclose(f); return -6; }
size_t got = fread(tmp, 1, want, f);
fclose(f);
int rc = rd_to_utf8(enc, tmp, got, out, cap, outlen);
free(tmp);
return rc;
}
+56
View File
@@ -0,0 +1,56 @@
/* rdtxt.h — TXT 流式加载器(骨架):探测编码 → 按段流式读,不整文件入内存。
*
* 对齐 READER_ENGINE_NOTES §7 #4:读 `Books/` 下 .txt → 检测编码 → 转 UTF-8 → 按段喂排版器;
* 书签/进度锚定 (章节id,字符偏移)。TXT 段 = 以 `\n` 为界的段落流按目标字节数聚合,段 =
* 一个排版/渲染单元;每段记录 {file_off, file_len, utf8_len},渲染时按 file_off 重定位读回,
* 确定性一致(编码在文件头一次检测,分段/转换仅依赖该编码)。
*
* `\n`(0x0A) 在 GB18030/GBK/UTF-8/ASCII 中恒为独立控制符(尾字节域 0x40-0x7E|0x80-0xFE 不含 0x0A),
* 故可安全在原始字节层的换行边界分段。纯 C,宿主可测;无 SDK 依赖。
*/
#ifndef RDTXT_H
#define RDTXT_H
#include "rdenc.h"
#include <stddef.h>
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
/* 段 UTF-8 上限(内存纪律:单段 ≤ 此值;编码源另 ≤ RDTXT_SEG_RAW_MAX)。 */
#define RDTXT_SEG_UTF8_MAX (96 * 1024)
#define RDTXT_SEG_RAW_MAX (RDTXT_SEG_UTF8_MAX) /* 原始字节软上限 */
typedef struct rdtxt_info {
rd_enc_t enc;
long file_size;
char path[320];
} rdtxt_info;
/* 探测:打开文件、取大小、检测编码(前 64KB)。成功 0;无文件 -1;非文件 -2。 */
int rdtxt_probe(const char *path, rdtxt_info *info);
/* 状态流式读取器 */
typedef struct rdtxt_reader rdtxt_reader;
/* 打开(info 须来自 rdtxt_probe)。成功 0;失败负值。 */
int rdtxt_reader_open(const char *path, const rdtxt_info *info, rdtxt_reader **r);
/* 读下一段。utf8 缓冲须 ≥ cap(建议 RDTXT_SEG_UTF8_MAX+8)。成功 0(填 utf8len/file_off/file_len);
* 1=EOF 无更多段;负值=错误。 */
int rdtxt_reader_next(rdtxt_reader *r, uint8_t *utf8, size_t cap,
size_t *utf8len, size_t *file_off, size_t *file_len);
void rdtxt_reader_free(rdtxt_reader *r);
const char *rdtxt_reader_path(const rdtxt_reader *r);
rd_enc_t rdtxt_reader_enc(const rdtxt_reader *r);
/* 渲染重定位:把文件字节区间 [raw_off, raw_off+raw_len) 转成 UTF-8 到 out(cap 建议同上限)。 */
int rdtxt_convert_span(const char *path, rd_enc_t enc, size_t raw_off, size_t raw_len,
uint8_t *out, size_t cap, size_t *outlen);
#ifdef __cplusplus
}
#endif
#endif /* RDTXT_H */