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:
1 parent
33d3059038
commit
553a7d5d9e
26 files changed
+6155
No files matched your search
File diff suppressed because it is too large.
Load diff
@@ -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;
|
||||
}
|
||||
@@ -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 */
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 */
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 */
|
||||
Reference in new issue
Block a user