diff --git a/patch_v2/reader_app/.gitignore b/patch_v2/reader_app/.gitignore
index f75a91f..72d1518 100644
--- a/patch_v2/reader_app/.gitignore
+++ b/patch_v2/reader_app/.gitignore
@@ -2,8 +2,10 @@
/vendor/
/reader
/test/test_reader_core_host
+/test/loaders_smoke_host
/test/rdft_smoke_mipsel
/test/book_smoke_mipsel
+/test/formats_smoke_mipsel
/test/mem_account_host
*.o
*.a
diff --git a/patch_v2/reader_app/NOTICE b/patch_v2/reader_app/NOTICE
new file mode 100644
index 0000000..88833ab
--- /dev/null
+++ b/patch_v2/reader_app/NOTICE
@@ -0,0 +1,209 @@
+os.meow.reader (patch_v2/reader_app) — Third-Party NOTICES
+==========================================================
+
+This application statically links and/or bundles the following
+third-party components. Their copyrights and licenses are reproduced
+below. The build script (build_reader.sh) fetches the exact pinned
+versions, so the binaries can be rebuilt / re-linked from source.
+
+--------------------------------------------------------------------
+1. libmobi — Copyright (c) 2014-2024 Bartek Fabiszewski
+ project: https://github.com/bfabiszewski/libmobi
+ license: GNU LESSER GENERAL PUBLIC LICENSE, Version 3
+ (or, at your option, any later version)
+
+ Locked version (commit):
+ 906274205c11944b628da1c553b255acb1af7c55 (2024-10-29)
+ Upstream source and relink instructions:
+ https://github.com/bfabiszewski/libmobi
+
+ libmobi is linked statically into the reader binary (LGPL-3.0
+ §4 relink requirement). This NOTICE, the version lock above, and
+ the publicly reproducible build (build_reader.sh) that fetches and
+ recompiles libmobi satisfy the obligation to provide the
+ corresponding source and the ability to re-link. The libmobi
+ sources are NOT committed to this repository (they live in the
+ upstream repo at the commit above); the build script retrieves
+ exactly that commit.
+
+ Full license text (GNU LGPL v3):
+--------------------------------------------------------------------
+ GNU LESSER GENERAL PUBLIC LICENSE
+ Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc.
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+
+ This version of the GNU Lesser General Public License incorporates
+the terms and conditions of version 3 of the GNU General Public
+License, supplemented by the additional permissions listed below.
+
+ 0. Additional Definitions.
+
+ As used herein, "this License" refers to version 3 of the GNU Lesser
+General Public License, and the "GNU GPL" refers to version 3 of the GNU
+General Public License.
+
+ "The Library" refers to a covered work governed by this License,
+other than an Application or a Combined Work as defined below.
+
+ An "Application" is any work that makes use of an interface provided
+by the Library, but which is not otherwise based on the Library.
+Defining a subclass of a class defined by the Library is deemed a mode
+of using an interface provided by the Library.
+
+ A "Combined Work" is a work produced by combining or linking an
+Application with the Library. The particular version of the Library
+with which the Combined Work was made is also called the "Linked
+Version".
+
+ The "Minimal Corresponding Source" for a Combined Work means the
+Corresponding Source for the Combined Work, excluding any source code
+for portions of the Combined Work that, considered in isolation, are
+based on the Application, and not on the Linked Version.
+
+ The "Corresponding Application Code" for a Combined Work means the
+object code and/or source code for the Application, including any data
+and utility programs needed for reproducing the Combined Work from the
+Application, but excluding the System Libraries of the Combined Work.
+
+ 1. Exception to Section 3 of the GNU GPL.
+
+ You may convey a covered work under sections 3 and 4 of this License
+without being bound by section 3 of the GNU GPL.
+
+ 2. Conveying Modified Versions.
+
+ If you modify a copy of the Library, and, in your modifications, a
+facility refers to a function or data to be supplied by an Application
+that uses the facility (other than as an argument passed when the
+facility is invoked), then you may convey a copy of the modified
+version:
+
+ a) under this License, provided that you make a good faith effort to
+ ensure that, in the event an Application does not supply the
+ function or data, the facility still operates, and performs
+ whatever part of its purpose remains meaningful, or
+
+ b) under the GNU GPL, with none of the additional permissions of
+ this License applicable to that copy.
+
+ 3. Object Code Incorporating Material from Library Header Files.
+
+ The object code form of an Application may incorporate material from
+a header file that is part of the Library. You may convey such object
+code under terms of your choice, provided that, if the incorporated
+material is not limited to numerical parameters, data structure
+layouts and accessors, or small macros, inline functions and templates
+(ten or fewer lines in length), you do both of the following:
+
+ a) Give prominent notice with each copy of the object code that the
+ Library is used in it and that the Library and its use are
+ covered by this License.
+
+ b) Accompany the object code with a copy of the GNU GPL and this license
+ document.
+
+ 4. Combined Works.
+
+ You may convey a Combined Work under terms of your choice that,
+taken together, effectively do not restrict modification of the
+portions of the Library contained in the Combined Work and reverse
+engineering for debugging such modifications, if you also do each of
+the following:
+
+ a) Give prominent notice with each copy of the Combined Work that
+ the Library is used in it and that the Library and its use are
+ covered by this License.
+
+ b) Accompany the Combined Work with a copy of the GNU GPL and this license
+ document.
+
+ c) For a Combined Work that displays copyright notices during
+ execution, include the copyright notice for the Library among
+ these notices, as well as a reference directing the user to the
+ copies of the GNU GPL and this license document.
+
+ d) Do one of the following:
+
+ 0) Convey the Minimal Corresponding Source under the terms of this
+ License, and the Corresponding Application Code in a form
+ suitable for, and under terms that permit, the user to
+ recombine or relink the Application with a modified version of
+ the Linked Version to produce a modified Combined Work, in the
+ manner specified by section 6 of the GNU GPL for conveying
+ Corresponding Source.
+
+ 1) Use a suitable shared library mechanism for linking with the
+ Library. A suitable mechanism is one that (a) uses at run time
+ a copy of the Library already present on the user's computer
+ system, and (b) will operate properly with a modified version
+ of the Library that is interface-compatible with the Linked
+ Version.
+
+ e) Provide Installation Information, but only if you would otherwise
+ be required to provide such information under section 6 of the
+ GNU GPL, and only to the extent that such information is
+ necessary to install and execute a modified version of the
+ Combined Work produced by recombining or relinking the
+ Application with a modified version of the Linked Version. (If
+ you use option 4d0, the Installation Information must accompany
+ the Minimal Corresponding Source and Corresponding Application
+ Code. If you use option 4d1, you must provide the Installation
+ Information in the manner specified by section 6 of the GNU GPL
+ for conveying Corresponding Source.)
+
+ 5. Combined Libraries.
+
+ You may place library facilities that are a work based on the
+Library side by side in a single library together with other library
+facilities that are not Applications and are not covered by this
+License, and convey such a combined library under terms of your
+choice, if you do both of the following:
+
+ a) Accompany the combined library with a copy of the same work based
+ on the Library, uncombined with any other library facilities,
+ conveyed under the terms of this License.
+
+ b) Give prominent notice with the combined library that part of it
+ is a work based on the Library, and explaining where to find the
+ accompanying uncombined form of the same work.
+
+ 6. Revised Versions of the GNU Lesser General Public License.
+
+ The Free Software Foundation may publish revised and/or new versions
+of the GNU Lesser General Public License from time to time. Such new
+versions will be similar in spirit to the present version, but may
+differ in detail to address new problems or concerns.
+
+ Each version is given a distinguishing version number. If the
+Library as you received it specifies that a certain numbered version
+of the GNU Lesser General Public License "or any later version"
+applies to it, you have the option of following the terms and
+conditions either of that published version or of any later version
+published by the Free Software Foundation. If the Library as you
+received it does not specify a version number of the GNU Lesser
+General Public License, you may choose any version of the GNU Lesser
+General Public License ever published by the Free Software Foundation.
+
+ If the Library as you received it specifies that a proxy can decide
+whether future versions of the GNU Lesser General Public License shall
+apply, that proxy's public statement of acceptance of any version is
+permanent authorization for you to choose that version for the
+Library.
+
+--------------------------------------------------------------------
+2. miniz — Copyright (c) 2013-2014 RAD Game Tools and Valve Software
+ Copyright (c) 2010-2014 Rich Geldreich and Tenacious
+ Software LLC
+ license: zlib license (MIT-style, permissive)
+ project: https://github.com/richgel999/miniz
+ Used for ZIP container reading (EPUB/CBZ) and inflate.
+
+--------------------------------------------------------------------
+3. stb_image — Public domain / MIT (Sean Barrett et al., v2.30)
+ project: https://github.com/nothings/stb
+ Used for image decode + downsample (CBZ pages, covers).
+
diff --git a/patch_v2/reader_app/app/book.c b/patch_v2/reader_app/app/book.c
index 4af59f8..165f08e 100644
--- a/patch_v2/reader_app/app/book.c
+++ b/patch_v2/reader_app/app/book.c
@@ -1,101 +1,105 @@
-/* app/book.c — 书桥实现(见 app/book.h)。 */
+/* 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 "rdlayout.h"
+#include "rdimg.h"
#include "../engine/rdft.h"
#include
#include
#include
typedef struct seg {
- size_t file_off, file_len;
- int page_start; /* 全局页表起点 */
- int page_count; /* 本段页数 */
+ int page_start; /* 全局页表起点 */
+ int page_count; /* 本段页数 */
} seg_t;
struct rd_book {
- char path[400];
- char font[400];
- char base[256]; /* 文件名 */
- const char *encname;
- rdft *ft;
- rd_enc_t enc;
+ char path[400];
+ char font[400];
+ char base[256];
+ const char *encname;
+ 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; /* 当前页 */
+ seg_t *segs; int nsegs;
+ rd_book_page *pages; int npages; /* flat 页表(文本) */
+ int cur;
- /* 当前渲染段的复用缓冲 */
+ /* 当前渲染段复用缓冲 */
uint8_t *seg_utf8; size_t seg_utf8_cap;
uint32_t *seg_cps; size_t seg_cps_n, seg_cps_cap;
- /* 当前段布局 */
- rd_line *seg_lines; rd_page *seg_pages; int seg_nlines, seg_npages;
- int seg_cache_idx; /* 当前缓冲对应段;-1=无 */
+ rd_line *seg_lines; rd_page *seg_pages; int seg_nlines, seg_npages;
+ int seg_cache_idx;
};
-static const char *encname_of(rd_enc_t e)
+static const char *fmt_encname(rd_fmt_t f, const char *path)
{
- switch (e) {
- 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(按GB18030)";
- case RD_ENC_ASCII: return "ASCII";
+ if (f == RD_FMT_TXT) {
+ rdtxt_info info;
+ if (rdtxt_probe(path, &info) == 0) {
+ switch (info.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(按GB18030)";
+ case RD_ENC_ASCII: return "ASCII";
+ }
+ }
}
- return "?";
+ return "";
}
-/* 测量回调:排版器逐字测 advance → rdft_advance(不栅格化)。 */
static int book_meas(uint32_t cp, int fs, int *adv, void *ctx)
{
(void)fs;
rd_book *b = (rd_book *)ctx;
- /* advance 受字号影响:rdft 已 set_px(fs),故用当前 rdft 度量即可 */
return rdft_advance(b->ft, cp, adv);
}
+/* ---- 文本:建索引(逐段 layout) ---- */
static int build_index(rd_book *b)
{
- int rc = 0;
- rdtxt_reader *r = NULL;
- if (rdtxt_reader_open(b->path, &(rdtxt_info){ .enc = b->enc, .file_size = 0, .path = "" },
- &r) != 0) return -1;
- /* rdtxt_reader_open 以 info 的 enc 为准(path 域不读) */
+ 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) { rd_book_close(b); return -6; }
+ 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) { rd_book_close(b); return -6; }
+ if (!utf8) return -6;
- size_t off = 0, flen = 0, ulen = 0;
int seg_idx = 0;
- while (rdtxt_reader_next(r, utf8, RDTXT_SEG_UTF8_MAX + 8, &ulen, &off, &flen) == 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_utf8_to_cp 内部 malloc */
+ 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); rd_book_close(b); return -6; }
+ 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].file_off = off;
- b->segs[seg_idx].file_len = flen;
+ 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++) {
@@ -108,10 +112,39 @@ static int build_index(rd_book *b)
seg_idx++;
}
free(utf8);
- rdtxt_reader_free(r);
b->nsegs = seg_idx;
- rc = 0;
- return rc;
+ return 0;
+}
+
+/* ---- 正文重显 + 布局到复用缓冲 ---- */
+static int load_seg(rd_book *b, int seg_idx)
+{
+ if (seg_idx == b->seg_cache_idx) return 0;
+ size_t cap = RDTXT_SEG_UTF8_MAX + 8;
+ if (!b->seg_utf8) b->seg_utf8 = (uint8_t *)malloc(cap);
+ if (!b->seg_utf8) return -6;
+ size_t ulen = 0;
+ if (rd_src_seg(b->src, seg_idx, b->seg_utf8, cap, &ulen) != 0) return -4;
+ size_t n = 0;
+ free(b->seg_cps); b->seg_cps = NULL;
+ if (rd_utf8_to_cp(b->seg_utf8, ulen, &b->seg_cps, &n) != RD_ENC_OK) return -5;
+ b->seg_cps_n = n;
+ if (b->seg_lines) free(b->seg_lines);
+ if (b->seg_pages) free(b->seg_pages);
+ b->seg_lines = (rd_line *)malloc((n ? n : 1) * sizeof(rd_line));
+ b->seg_pages = (rd_page *)malloc((n ? n : 1) * sizeof(rd_page));
+ if (!b->seg_lines || !b->seg_pages) return -6;
+ rd_layout_in in;
+ rd_layout_out out;
+ in.chars = b->seg_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 = b->seg_lines; out.pages = b->seg_pages; out.max_lines = (int)(n ? n : 1); out.max_pages = (int)(n ? n : 1);
+ int lrc = rd_layout(&in, &out);
+ if (lrc != RD_LAYOUT_OK) { b->seg_nlines = b->seg_npages = 0; b->seg_cache_idx = -1; return -4; }
+ b->seg_nlines = out.nlines; b->seg_npages = out.npages;
+ b->seg_cache_idx = seg_idx;
+ return 0;
}
int rd_book_open(rd_book **b, const char *path, const char *font_path,
@@ -123,31 +156,38 @@ int rd_book_open(rd_book **b, const char *path, const char *font_path,
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) { close: free(x); return -3; }
+ if (x->content_w < 10 || x->content_h < 10) { goto close; }
- rdtxt_info info;
- if (rdtxt_probe(path, &info) != 0) goto close;
- x->enc = info.enc;
- x->encname = encname_of(info.enc);
+ if (rd_src_open(&x->src, path) != 0) goto close;
+ x->fmt = rd_src_fmt(x->src);
+ x->encname = fmt_encname(x->fmt, path);
+ x->is_img = (x->fmt == RD_FMT_CBZ);
- if (rdft_init(&x->ft) != 0) goto close;
- if (font_path && rdft_load_font(x->ft, x->font) != 0) { rdft_free(x->ft); goto close; }
- if (x->ft) rdft_set_px(x->ft, fs);
-
- x->seg_cache_idx = -1;
- if (build_index(x) != 0) { rd_book_close(x); return -5; }
+ 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->seg_cache_idx = -1;
+ if (build_index(x) != 0) goto close;
+ } 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)
@@ -160,6 +200,7 @@ void rd_book_close(rd_book *b)
free(b->seg_lines);
free(b->seg_pages);
if (b->ft) rdft_free(b->ft);
+ if (b->src) rd_src_free(b->src);
free(b);
}
@@ -167,6 +208,9 @@ 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 : "") : ""; }
+
void rd_book_geom(rd_book *b, int *fs, int *ls, int *pw, int *ph, int *mx, int *my, int *top)
{
if (!b) return;
@@ -179,7 +223,6 @@ void rd_book_geom(rd_book *b, int *fs, int *ls, int *pw, int *ph, int *mx, int *
if (top) *top = b->top;
}
const char *rd_book_basename(const rd_book *b) { return b ? b->base : ""; }
-const char *rd_book_encname(const rd_book *b) { return b ? b->encname : ""; }
const char *rd_book_path(const rd_book *b) { return b ? b->path : ""; }
void rd_book_anchor(rd_book *b, int *seg, int32_t *ch)
@@ -200,55 +243,18 @@ int rd_book_turn(rd_book *b, int delta)
return np;
}
-/* --- 读回一段并布局到复用缓冲(渲染用)--- */
-static int load_seg(rd_book *b, int seg_idx)
+/* ---- 文本页渲染(FreeType 字形 → 全屏 565) ---- */
+static int render_text_page(rd_book *b, int page_no, uint16_t *out, int pitch, uint16_t bg565, uint16_t fg565)
{
- if (seg_idx == b->seg_cache_idx) return 0;
- seg_t *s = &b->segs[seg_idx];
- size_t cap = RDTXT_SEG_UTF8_MAX + 8;
- if (!b->seg_utf8) b->seg_utf8 = (uint8_t *)malloc(cap);
- if (!b->seg_utf8) return -6;
- size_t ulen = 0;
- if (rdtxt_convert_span(b->path, b->enc, s->file_off, s->file_len, b->seg_utf8, cap, &ulen) != RD_ENC_OK)
- return -4;
- size_t n = 0;
- free(b->seg_cps); /* rd_utf8_to_cp 内部 malloc 新数组 */
- b->seg_cps = NULL;
- if (rd_utf8_to_cp(b->seg_utf8, ulen, &b->seg_cps, &n) != RD_ENC_OK) return -5;
- b->seg_cps_n = n;
- /* 布局(容量:段码点数为上界) */
- if (b->seg_lines) free(b->seg_lines);
- if (b->seg_pages) free(b->seg_pages);
- b->seg_lines = (rd_line *)malloc((n ? n : 1) * sizeof(rd_line));
- b->seg_pages = (rd_page *)malloc((n ? n : 1) * sizeof(rd_page));
- if (!b->seg_lines || !b->seg_pages) return -6;
- rd_layout_in in;
- rd_layout_out out;
- in.chars = b->seg_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 = b->seg_lines; out.pages = b->seg_pages; out.max_lines = (int)(n ? n : 1); out.max_pages = (int)(n ? n : 1);
- int lrc = rd_layout(&in, &out);
- if (lrc != RD_LAYOUT_OK) { b->seg_nlines = b->seg_npages = 0; b->seg_cache_idx = -1; return -4; }
- b->seg_nlines = out.nlines; b->seg_npages = out.npages;
- b->seg_cache_idx = seg_idx;
- 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;
int npix = b->pw * b->ph;
for (int i = 0; i < npix; i++) out[i] = bg565;
-
rd_book_page *pg = &b->pages[page_no];
int seg_idx = (int)pg->seg;
if (seg_idx < 0 || seg_idx >= b->nsegs) return -1;
if (load_seg(b, seg_idx) != 0) return -2;
int local_page = page_no - b->segs[seg_idx].page_start;
- if (local_page < 0 || local_page >= b->seg_npages) return 1; /* 空页 */
+ if (local_page < 0 || local_page >= b->seg_npages) return 1;
rd_page *pp = &b->seg_pages[local_page];
-
int asc = rdft_ascender(b->ft);
int screen_x0 = b->mx;
int screen_y0 = b->top;
@@ -258,22 +264,55 @@ int rd_book_render(rd_book *b, int page_no, uint16_t *out, int pitch, uint16_t b
int baseline = screen_y0 + ln->y + asc;
for (int32_t ci = ln->s; ci < ln->e; ci++) {
uint32_t cp = b->seg_cps[ci];
- int adv = 0, bw = 0, bh = 0, left = 0, top = 0;
+ 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, &top);
+ 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, top, fg565, bg565);
+ rdft_blend565(out, pitch, b->pw, b->ph, pen, baseline, bmp, bw, bh, left, btop, fg565, bg565);
pen += adv;
- if (cp == '\t') pen += adv; /* 制表符已由布局按空格 advance 计量,此处保持游标 */
}
}
return 0;
}
-/* 重排 + 回锚:以 (seg,ch) 为锚重排全书;返回重排后的页(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;
@@ -284,27 +323,26 @@ int rd_book_set_size(rd_book *b, int fs, int 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);
- /* 重建索引 */
- seg_t *nos = b->segs; int n_old_segs = b->nsegs;
- rd_book_page *nop = b->pages; int n_old_pages = b->npages;
+ 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 (build_index(b) != 0) {
- b->segs = nos; b->pages = nop; b->nsegs = n_old_segs; b->npages = n_old_pages;
- return -5;
- }
+ 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);
}
- /* 回锚:找第一个 (seg==anchorSeg && seg_char>=anchorCh) 的页,或段内第一个 seg_char>=ch;跨段按 seg 序 */
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 (ps > seg) break;
if (b->pages[p].seg_char >= ch) { target = p; break; }
target = p;
}
@@ -314,10 +352,10 @@ int rd_book_jump_to_anchor(rd_book *b, int seg, int32_t ch, int fs, int ls)
return target;
}
-/* SDK 字库兜底:逐行回调正文文本(无 TTF/OTF 时用 meow_ui_text 渲染)。 */
+/* ---- SDK 字库兜底(文本格式) ---- */
int rd_book_render_text(rd_book *b, int page_no, rd_book_line_fn fn, void *ctx)
{
- if (!b || !fn || b->npages <= 0 || page_no < 0 || page_no >= b->npages) return -1;
+ if (!b || !fn || b->is_img || b->npages <= 0 || page_no < 0 || page_no >= b->npages) return -1;
rd_book_page *pg = &b->pages[page_no];
int seg_idx = (int)pg->seg;
if (seg_idx < 0 || seg_idx >= b->nsegs) return -1;
@@ -325,13 +363,11 @@ int rd_book_render_text(rd_book *b, int page_no, rd_book_line_fn fn, void *ctx)
int local_page = page_no - b->segs[seg_idx].page_start;
if (local_page < 0 || local_page >= b->seg_npages) return 1;
rd_page *pp = &b->seg_pages[local_page];
- /* 单线程渲染:每行最多 content_w 个单宽字符 → ≤4B/字 UTF-8 */
uint8_t *line_utf8 = (uint8_t *)malloc((size_t)(b->content_w * 4 + 32));
if (!line_utf8) return -6;
for (int li = 0; li < pp->nlines; li++) {
rd_line *ln = &b->seg_lines[pp->first_line + li];
int x = b->mx + ln->x, y = b->top + ln->y;
- /* 组 utf8 */
size_t o = 0;
for (int32_t ci = ln->s; ci < ln->e; ci++) {
uint8_t tmp[4];
@@ -344,3 +380,19 @@ int rd_book_render_text(rd_book *b, int page_no, rd_book_line_fn fn, void *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;
+}
diff --git a/patch_v2/reader_app/app/book.h b/patch_v2/reader_app/app/book.h
index 1465bf6..3ef37f7 100644
--- a/patch_v2/reader_app/app/book.h
+++ b/patch_v2/reader_app/app/book.h
@@ -35,6 +35,11 @@ void rd_book_close(rd_book *b);
int rd_book_pages(rd_book *b);
int rd_book_current(rd_book *b); /* 当前页 */
int rd_book_turn(rd_book *b, int delta); /* +1/-1;clamp 到 [0,npages-1] */
+int rd_book_is_image(rd_book *b); /* 1=CBZ 图片页书 */
+
+/* 目录:0=无;i 项标题写到 title,目标段/字符偏移填 *seg 与 *ch。 */
+int rd_book_toc_n(const rd_book *b);
+int rd_book_toc(const rd_book *b, int i, char *title, int tsz, int *seg, int32_t *ch);
void rd_book_anchor(rd_book *b, int *seg, int32_t *ch);/* 当前页锚(段,字符偏移) */
int rd_book_jump_to_anchor(rd_book *b, int seg, int32_t ch, int fs, int ls); /* 重排 + 回锚 */
int rd_book_set_size(rd_book *b, int fs, int ls); /* 重排 + 回锚(anchor=当前页) */
diff --git a/patch_v2/reader_app/app/reader.c b/patch_v2/reader_app/app/reader.c
index f27da20..e356a5a 100644
--- a/patch_v2/reader_app/app/reader.c
+++ b/patch_v2/reader_app/app/reader.c
@@ -17,7 +17,7 @@
#include
#define RD_MENU_H 64
-#define RD_MENU_BTN 5
+#define RD_MENU_BTN 6
static rd_book *g_book;
static int g_reader_active;
@@ -29,6 +29,8 @@ static int g_fs, g_ls;
static uint16_t *g_page_buf[RD_PAGE_CACHE]; /* [0]=prev [1]=cur [2]=next(FreeType 页位图) */
static int g_has_font; /* 1=FreeType 路径;0=SDK 字库兜底 */
static int g_menu_open;
+static int g_dir_open; /* 目录覆盖层 */
+static int g_dir_top, g_dir_row_h; /* 目录列表几何(rdsrc 目录项) */
static char g_font_path[400];
static int g_notice_ms;
@@ -125,7 +127,7 @@ static void reader_canvas_render(meow_widget *w, const meow_theme *th, void *ctx
meow_ui_clear(g_page_bg);
if (g_book) {
int page = rd_book_current(g_book);
- if (g_has_font && g_page_buf[RD_PAGE_SLOT_CUR]) {
+ if ((g_has_font || rd_book_is_image(g_book)) && g_page_buf[RD_PAGE_SLOT_CUR]) {
/* 翻页 blit:行拷贝当前页位图 → back_buf(局部脏区由 flush_region 上屏) */
unsigned char *bb = meow_ui_back_buf();
int pitch = meow_ui_back_buf_pitch();
@@ -154,15 +156,41 @@ static void reader_canvas_render(meow_widget *w, const meow_theme *th, void *ctx
if (g_menu_open) {
int y = g_ph - RD_MENU_H;
meow_ui_fill_rect_alpha(0, y, g_pw, RD_MENU_H, 0x000000, 160);
- /* 5 按钮:返回 | 字号− | 字号+ | 行距− | 行距+ */
+ /* 6 按钮:返回 | 目录 | 字− | 字+ | 距− | 距+ */
int bw = g_pw / RD_MENU_BTN;
- const char *labels[RD_MENU_BTN] = { "返回", "字-", "字+", "距-", "距+" };
+ const char *labels[RD_MENU_BTN] = { "返回", "目录", "字-", "字+", "距-", "距+" };
for (int i = 0; i < RD_MENU_BTN; i++) {
int bx = i * bw;
meow_ui_fill_rect(bx + 4, y + 12, bw - 8, RD_MENU_H - 24, th->surfaceContainer);
meow_ui_text_center(bx + bw / 2, y + (RD_MENU_H - 20) / 2 - 6, labels[i], 20, th->fg);
}
}
+ if (g_dir_open && g_book) {
+ /* 目录覆盖层:半透明底 + 条目列表(当前行高亮) */
+ int n = rd_book_toc_n(g_book);
+ if (n > 0) {
+ int top = g_top + 8;
+ int row = 34;
+ int maxshow = (g_ph - top - 70) / row;
+ if (maxshow < 1) maxshow = 1;
+ if (maxshow > n) maxshow = n;
+ g_dir_top = top; g_dir_row_h = row;
+ meow_ui_fill_rect_alpha(0, g_top, g_pw, g_ph - g_top, 0x000000, 150);
+ char t[96]; int sg; int32_t cch;
+ int page = rd_book_current(g_book);
+ for (int i = 0; i < maxshow; i++) {
+ if (rd_book_toc(g_book, i, t, sizeof t, &sg, &cch) != 0) continue;
+ int y0 = top + i * row;
+ if (page == sg) meow_ui_fill_rect(0, y0, g_pw, row - 4, th->surfaceContainer);
+ meow_ui_text(24, y0, t, 20, th->fg);
+ meow_ui_draw_rect(12, y0 + row - 6, g_pw - 24, 1, th->outline);
+ }
+ meow_ui_text(20, g_ph - 34, "点条目跳转 · 点空白关闭", 16, th->dim);
+ } else {
+ g_dir_open = 0;
+ meow_ui_text(20, g_top + 40, "本书无目录", 20, th->dim);
+ }
+ }
}
static void reader_redraw(void)
@@ -181,7 +209,7 @@ static void reader_free_page_bufs(void)
static int reader_refill_page(void)
{
if (!g_book) return -1;
- if (g_has_font && g_page_buf[RD_PAGE_SLOT_CUR]) {
+ if ((g_has_font || rd_book_is_image(g_book)) && g_page_buf[RD_PAGE_SLOT_CUR]) {
uint16_t bg = rgb565(g_page_bg), fg = rgb565(g_page_fg);
int cur = rd_book_current(g_book);
int np = rd_book_pages(g_book);
@@ -230,7 +258,7 @@ void reader_open(const char *path)
}
chapter_name();
- if (g_has_font) {
+ if (g_has_font || rd_book_is_image(g_book)) {
reader_free_page_bufs();
for (int i = 0; i < RD_PAGE_CACHE; i++)
g_page_buf[i] = (uint16_t *)malloc((size_t)g_pw * (size_t)g_ph * 2);
@@ -244,7 +272,7 @@ void reader_open(const char *path)
reader_refill_page();
- g_menu_open = 0;
+ g_menu_open = 0; g_dir_open = 0;
if (g_reader_active) reader_redraw();
}
@@ -269,7 +297,7 @@ static void reader_on_destroy(meow_view *v, void *ctx)
if (g_book) rd_book_close(g_book);
g_book = NULL;
reader_free_page_bufs();
- g_menu_open = 0; g_reader_active = 0;
+ g_menu_open = 0; g_dir_open = 0; g_reader_active = 0;
}
meow_view *reader_view_create(void)
@@ -287,6 +315,30 @@ int reader_is_active(void) { return g_reader_active; }
void reader_on_tap(int x, int y)
{
if (!g_book) { return; }
+ if (g_dir_open) {
+ /* 目录覆盖层:点条目跳转,点空白关闭 */
+ int n = rd_book_toc_n(g_book);
+ if (n > 0 && x >= 0 && y >= g_dir_top) {
+ int row = g_dir_row_h > 0 ? g_dir_row_h : 34;
+ int idx = (y - g_dir_top) / row;
+ if (idx < n && idx >= 0) {
+ char t[96]; int sg; int32_t cch;
+ if (rd_book_toc(g_book, idx, t, sizeof t, &sg, &cch) == 0) {
+ if (g_book && sg >= 0) {
+ rd_book_jump_to_anchor(g_book, sg, cch, g_fs, g_ls);
+ g_dir_open = 0; g_menu_open = 0;
+ snprintf(g_chapter, sizeof g_chapter, "%s", t);
+ reader_refill_page();
+ reader_redraw();
+ reader_reflow_persist();
+ return;
+ }
+ }
+ }
+ }
+ g_dir_open = 0; reader_redraw();
+ return;
+ }
if (g_menu_open) {
/* 菜单按钮命中 */
int y0 = g_ph - RD_MENU_H;
@@ -299,10 +351,14 @@ void reader_on_tap(int x, int y)
reader_reflow_persist();
meow_view_page_pop();
return;
- case 1: if (g_fs > 14) { g_fs -= 2; reader_reflow(); } break;
- case 2: if (g_fs < 40) { g_fs += 2; reader_reflow(); } break;
- case 3: if (g_ls > 100) { g_ls -= 5; reader_reflow(); } break;
- case 4: if (g_ls < 200) { g_ls += 5; reader_reflow(); } break;
+ case 1: /* 目录 */
+ if (rd_book_toc_n(g_book) > 0) { g_dir_open = 1; g_menu_open = 0; reader_redraw(); }
+ else { g_menu_open = 0; reader_redraw(); }
+ return;
+ case 2: if (g_fs > 14) { g_fs -= 2; reader_reflow(); } break;
+ case 3: if (g_fs < 40) { g_fs += 2; reader_reflow(); } break;
+ case 4: if (g_ls > 100) { g_ls -= 5; reader_reflow(); } break;
+ case 5: if (g_ls < 200) { g_ls += 5; reader_reflow(); } break;
}
if (y >= y0) return;
}
diff --git a/patch_v2/reader_app/app/shelf.c b/patch_v2/reader_app/app/shelf.c
index fe4ee35..587532a 100644
--- a/patch_v2/reader_app/app/shelf.c
+++ b/patch_v2/reader_app/app/shelf.c
@@ -115,9 +115,7 @@ static void shelf_list_tapcb(int idx, int op, void *ctx)
{
(void)op; (void)ctx;
if (idx < 0 || idx >= g_nbooks) return;
- const shelf_book *b = &g_books[idx];
- if (b->fmt == 0) { reader_open(b->path); }
- else { snprintf(g_notice, sizeof g_notice, "%s 格式 R2 支持(R1 先看 TXT)", fmt_name(b->fmt)); g_notice_ms = 2500; }
+ reader_open(g_books[idx].path);
}
/* view 生命周期 */
@@ -165,7 +163,7 @@ void shelf_redraw(void)
if (g_list) meow_widget_draw(g_list);
if (g_nbooks == 0) {
meow_ui_text(30, 320, "书架为空:请往 TF 卡 Books/ 放书籍", 20, g_dim);
- meow_ui_text(30, 348, "支持 TXT(可读) / FB2 / EPUB / MOBI / CBZ(R2)", 20, g_dim);
+ meow_ui_text(30, 348, "支持 TXT / FB2 / EPUB / MOBI / CBZ", 20, g_dim);
}
if (g_notice_ms > 0) {
meow_ui_fill_rect(0, 620, 480, 44, g_bg);
diff --git a/patch_v2/reader_app/build_reader.sh b/patch_v2/reader_app/build_reader.sh
index d261039..70ef41e 100755
--- a/patch_v2/reader_app/build_reader.sh
+++ b/patch_v2/reader_app/build_reader.sh
@@ -1,93 +1,137 @@
#!/bin/bash
-# build_reader.sh — os.meow.reader R1 可复现构建:vendor FreeType(mipsel musl 静态) → 链 reader 二进制
-# + 宿主 core 单测 + qemu-mipsel FreeType 冒烟。
+# build_reader.sh — os.meow.reader R2 可复现构建:
+# vendor: freetype(静态)/miniz/libmobi/stb/zlib 头 → 交叉 mipsel 编 reader 二进制
+# + 宿主 core 单测(R1+loaders) + qemu-mipsel 冒烟(rdft/book/formats)。
#
# 产物:
-# reader mipsel 静态 ELF(链 meow_ui.a + FreeType + core + engine)
-# test/test_reader_core_host 宿主 core 单测(enc/layout/kinsoku/page/reflow/txt)
-# test/rdft_smoke_mipsel qemu-mipsel FreeType 封装冒烟
+# reader mipsel 静态 ELF(链 meow_ui.a + freetype + miniz + libmobi + zlib)
+# test/test_reader_core_host 宿主 core 单测(enc/layout/txt)
+# test/loaders_smoke_host 宿主四格式 loader 单测
+# test/rdft_smoke_mipsel qemu-mipsel FreeType 冒烟
+# test/book_smoke_mipsel qemu-mipsel TXT 书桥冒烟
+# test/formats_smoke_mipsel qemu-mipsel 四格式书桥冒烟
#
-# 约束:只写本目录(patch_v2/reader_app/);不改 sdk/;-Wall -Wextra 0 warning。
-# vendor/(FreeType 源码+构建产物)gitignore,不入仓;build 脚本可复现。
+# 约束:只写本目录(patch_v2/reader_app/);不改 sdk/;-Wall -Wextra 0 warning(自有代码)。
+# vendor/(第三方源码+构建产物)gitignore,不入仓;build 脚本可复现。
set -e
cd "$(dirname "$0")"
ROOT="$HOME/Project/R3ProII"
CC="$ROOT/.tools/zig-mipsel-cc"
SYSROOT="$ROOT/.tools/mipsel-linux-muslsf-cross/mipsel-linux-muslsf"
HOST_CC="${HOST_CC:-gcc}"
-
-FT_VER="2.14.3"
VENDOR="$PWD/vendor"
-FT_SRC="$VENDOR/freetype-src"
-FT_BUILD="$VENDOR/freetype-build"
-FT_INC_SRC="$FT_SRC/include"
-FT_INC_BUILD="$FT_BUILD/include"
+OBJ="$VENDOR/build"
+mkdir -p "$OBJ"
-# ---------- 1) FreeType vendor 拉取 ----------
+# ---------- 1) vendor 拉取/对齐 ----------
+FT_VER="2.14.3"; MINIZ_URL="https://codeload.github.com/richgel999/miniz/tar.gz/refs/heads/master"
+LM_URL="https://codeload.github.com/bfabiszewski/libmobi/tar.gz/refs/heads/master"
mkdir -p "$VENDOR"
fetch_freetype() {
- # 优先级:env FREETYPE_SRC → 本目录 vendor 已存在 → /tmp/freetype → 下载
- if [ -n "$FREETYPE_SRC" ] && [ -f "$FREETYPE_SRC/CMakeLists.txt" ]; then
- cp -r "$FREETYPE_SRC" "$FT_SRC"
- elif [ ! -d "$FT_SRC" ] && [ -f /tmp/freetype/CMakeLists.txt ]; then
- cp -r /tmp/freetype "$FT_SRC"
- elif [ ! -d "$FT_SRC" ]; then
- echo "==> downloading freetype $FT_VER"
- rm -rf "$FT_SRC"
- ( cd "$VENDOR" && curl -fsSL -o ft.tar.xz \
- "https://download.savannah.gnu.org/releases/freetype/freetype-$FT_VER.tar.xz" \
- && mkdir -p "$FT_SRC" && tar xf ft.tar.xz -C "$FT_SRC" --strip-components=1 && rm ft.tar.xz )
+ if [ -n "$FREETYPE_SRC" ] && [ -f "$FREETYPE_SRC/CMakeLists.txt" ]; then cp -r "$FREETYPE_SRC" vendor/freetype-src;
+ elif [ ! -d "$VENDOR/freetype-src" ] && [ -f /tmp/freetype/CMakeLists.txt ]; then cp -r /tmp/freetype "$VENDOR/freetype-src";
+ elif [ ! -d "$VENDOR/freetype-src" ]; then
+ ( cd "$VENDOR" && curl -fsSL -o ft.tar.xz "https://download.savannah.gnu.org/releases/freetype/freetype-$FT_VER.tar.xz" \
+ && mkdir -p freetype-src && tar xf ft.tar.xz -C freetype-src --strip-components=1 && rm ft.tar.xz )
fi
- [ -f "$FT_SRC/CMakeLists.txt" ] || { echo "FREETYPE_SRC 不可用/下载失败"; exit 1; }
+ [ -f "$VENDOR/freetype-src/CMakeLists.txt" ] || { echo "freetype 不可用"; exit 1; }
}
-fetch_freetype
+fetch_miniz() {
+ if [ ! -f "$VENDOR/miniz/miniz.c" ]; then
+ ( cd /tmp && curl -fsSL -o miniz.tgz "$MINIZ_URL" && mkdir -p miniz_m && tar xzf miniz.tgz -C miniz_m --strip-components=1 \
+ && cp miniz_m/miniz.c miniz_m/miniz.h miniz_m/miniz_common.h miniz_m/miniz_tdef.c miniz_m/miniz_tdef.h \
+ miniz_m/miniz_tinfl.c miniz_m/miniz_tinfl.h miniz_m/miniz_zip.c miniz_m/miniz_zip.h "$VENDOR/miniz/" )
+ fi
+ [ -f "$VENDOR/miniz/miniz.c" ] || { echo "miniz 不可用"; exit 1; }
+}
+fetch_libmobi() {
+ if [ ! -f "$VENDOR/libmobi/src/mobi.h" ]; then
+ ( cd /tmp && curl -fsSL -o lm.tgz "$LM_URL" && mkdir -p lm_m && tar xzf lm.tgz -C lm_m --strip-components=1 \
+ && mkdir -p "$VENDOR/libmobi" && cp -r lm_m/src "$VENDOR/libmobi/src" && cp lm_m/COPYING "$VENDOR/libmobi/COPYING" )
+ fi
+ [ -f "$VENDOR/libmobi/src/mobi.h" ] || { echo "libmobi 不可用"; exit 1; }
+}
+fetch_stb() { [ -f "$VENDOR/stb/stb_image.h" ] || { mkdir -p "$VENDOR/stb"; cp "$ROOT/patch_v2/sdk/stb_image.h" "$VENDOR/stb/stb_image.h"; }; }
+fetch_freetype; fetch_miniz; fetch_libmobi; fetch_stb
+if [ ! -f "$VENDOR/zlib/zlib.h" ]; then mkdir -p "$VENDOR/zlib"; cp /usr/include/zlib.h /usr/include/zconf.h "$VENDOR/zlib/" 2>/dev/null || true; fi
-# ---------- 2) FreeType mipsel 交叉静态构建(禁用可选项,只要最简) ----------
+# ---------- 2) FreeType mipsel 静态构建 ----------
+FT_SRC="$VENDOR/freetype-src"; FT_BUILD="$VENDOR/freetype-build"
if [ ! -f "$FT_BUILD/libfreetype.a" ] || [ -n "$FORCE_FT" ]; then
rm -rf "$FT_BUILD"
- cmake -S "$FT_SRC" -B "$FT_BUILD" \
- -DCMAKE_SYSTEM_NAME=Linux \
- -DCMAKE_C_COMPILER="$CC" \
- -DCMAKE_TRY_COMPILE_TARGET_TYPE=STATIC_LIBRARY \
- -DCMAKE_BUILD_TYPE=Release \
- -DBUILD_SHARED_LIBS=OFF \
- -DFT_DISABLE_ZLIB=TRUE -DFT_DISABLE_BZIP2=TRUE -DFT_DISABLE_PNG=TRUE \
- -DFT_DISABLE_HARFBUZZ=TRUE -DFT_DISABLE_BROTLI=TRUE \
- >/dev/null
+ cmake -S "$FT_SRC" -B "$FT_BUILD" -DCMAKE_SYSTEM_NAME=Linux -DCMAKE_C_COMPILER="$CC" \
+ -DCMAKE_TRY_COMPILE_TARGET_TYPE=STATIC_LIBRARY -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=OFF \
+ -DFT_DISABLE_ZLIB=TRUE -DFT_DISABLE_BZIP2=TRUE -DFT_DISABLE_PNG=TRUE -DFT_DISABLE_HARFBUZZ=TRUE -DFT_DISABLE_BROTLI=TRUE >/dev/null
cmake --build "$FT_BUILD" -j"$(nproc)" >/dev/null 2>&1
fi
-[ -f "$FT_BUILD/libfreetype.a" ] || { echo "FreeType build FAILED"; exit 1; }
-FT_INCS="-I$FT_BUILD -I$FT_INC_BUILD -I$FT_INC_SRC"
-echo "==> freetype.a: $(du -h "$FT_BUILD/libfreetype.a" | cut -f1)"
+FT_INCS="-I$FT_BUILD -I$FT_BUILD/include -I$FT_SRC/include"
-# ---------- 3) 宿主 core 单测 ----------
-echo "==> host core test"
-$HOST_CC -O2 -Wall -Wextra -Icore -I. test/test_reader_core.c \
- core/rdenc.c core/rdlayout.c core/rdtxt.c -o test/test_reader_core_host -lm
+COREINC="-Icore -Ivendor/miniz -Ivendor/stb -Ivendor/libmobi/src -Ivendor/zlib"
+MZFLAGS="-DMINIZ_NO_ZLIB_COMPATIBLE_NAMES -DMINIZ_NO_TIME"
+
+# ---------- 3) miniz + libmobi 对象(mipsel) ----------
+build_mipsel_libs() {
+ for f in vendor/miniz/miniz_tinfl.c vendor/miniz/miniz_tdef.c vendor/miniz/miniz_zip.c vendor/miniz/miniz.c; do
+ b="$(basename "$f" .c)"; [ -f "$OBJ/mz_$b.o" ] || $CC -Os -static -Wall -Wextra -Ivendor/miniz $MZFLAGS -c "$f" -o "$OBJ/mz_$b.o"
+ done
+ for f in vendor/libmobi/src/*.c; do b="$(basename "$f" .c)"; [ "$b" = "miniz" ] && continue
+ [ -f "$OBJ/lm_$b.o" ] || $CC -Os -static -w -Ivendor/libmobi/src -Ivendor/zlib -c "$f" -o "$OBJ/lm_$b.o"
+ done
+ [ -f "$OBJ/rdimg.o" ] || $CC -Os -static -Wall -Wextra -Ivendor/stb -DSTB_IMAGE_IMPLEMENTATION -c engine/rdimg.c -o "$OBJ/rdimg.o"
+}
+build_mipsel_libs
+
+# ---------- 4) 宿主 loader 单测(miniz/libmobi 用宿主编一遍) ----------
+echo "==> host core test (R1)"
+$HOST_CC -O2 -Wall -Wextra -Icore -I. test/test_reader_core.c core/rdenc.c core/rdlayout.c core/rdtxt.c -o test/test_reader_core_host -lm
./test/test_reader_core_host
-# ---------- 4) qemu-mipsel FreeType 冒烟 ----------
+echo "==> host loaders test"
+HOBJ="$OBJ/host"; mkdir -p "$HOBJ"
+for f in vendor/miniz/miniz_tinfl.c vendor/miniz/miniz_tdef.c vendor/miniz/miniz_zip.c vendor/miniz/miniz.c; do
+ b="$(basename "$f" .c)"; [ -f "$HOBJ/mz_$b.o" ] || $HOST_CC -O2 -Ivendor/miniz $MZFLAGS -c "$f" -o "$HOBJ/mz_$b.o"
+done
+for f in vendor/libmobi/src/*.c; do b="$(basename "$f" .c)"; [ "$b" = "miniz" ] && continue
+ [ -f "$HOBJ/lm_$b.o" ] || $HOST_CC -O2 -w -Ivendor/libmobi/src -Ivendor/zlib -c "$f" -o "$HOBJ/lm_$b.o"
+done
+HCORE=""
+for f in rdxml rdhtml rdsb rdzip rdsrc rdepub rdfb2 rdmobi rdcbz rdenc rdtxt; do
+ $HOST_CC -O2 -Wall -Wextra $COREINC -c core/$f.c -o "$HOBJ/core_$f.o"
+done
+python3 tools/gen_samples.py >/dev/null
+$HOST_CC -O2 -Wall -Wextra $COREINC test/loaders_smoke.c "$HOBJ"/*.o -lz -lm -o test/loaders_smoke_host
+./test/loaders_smoke_host /tmp/rd_samples
+
+# ---------- 5) qemu-mipsel FreeType 冒烟 ----------
echo "==> mipsel ft smoke"
-$CC -Os -static -Wall -Wextra -Iengine $FT_INCS test/rdft_smoke.c engine/rdft.c \
- "$FT_BUILD/libfreetype.a" -o test/rdft_smoke_mipsel
+$CC -Os -static -Wall -Wextra -Iengine $FT_INCS test/rdft_smoke.c engine/rdft.c "$FT_BUILD/libfreetype.a" -o test/rdft_smoke_mipsel
timeout 120 qemu-mipsel ./test/rdft_smoke_mipsel "${FONT:-$ROOT/font/src/MiSans-Regular.otf}"
-# ---------- 4b) qemu-mipsel 书桥全链路冒烟 ----------
-echo "==> mipsel book smoke"
-$CC -Os -static -Wall -Wextra -Iapp -Icore -Iengine $FT_INCS test/book_smoke.c \
- app/book.c core/rdenc.c core/rdlayout.c core/rdtxt.c engine/rdft.c \
- "$FT_BUILD/libfreetype.a" -o test/book_smoke_mipsel
+# ---------- 6) qemu-mipsel 书桥冒烟(TXT + 四格式) ----------
+echo "==> mipsel book smoke (TXT)"
+$CC -Os -static -Wall -Wextra -Iapp -Icore -Iengine $FT_INCS $COREINC test/book_smoke.c \
+ app/book.c core/rdenc.c core/rdlayout.c core/rdtxt.c core/rdxml.c core/rdhtml.c core/rdsb.c core/rdzip.c \
+ core/rdsrc.c core/rdepub.c core/rdfb2.c core/rdmobi.c core/rdcbz.c engine/rdft.c \
+ "$OBJ"/mz_*.o "$OBJ"/lm_*.o "$OBJ"/rdimg.o "$SYSROOT/lib/libz.a" "$FT_BUILD/libfreetype.a" -o test/book_smoke_mipsel
timeout 120 qemu-mipsel ./test/book_smoke_mipsel "${FONT:-$ROOT/font/src/MiSans-Regular.otf}"
-# ---------- 5) reader 二进制(mipsel 静态) ----------
+echo "==> mipsel formats smoke"
+$CC -Os -static -Wall -Wextra -Iapp -Icore -Iengine $FT_INCS $COREINC test/formats_smoke.c \
+ app/book.c core/rdenc.c core/rdlayout.c core/rdtxt.c core/rdxml.c core/rdhtml.c core/rdsb.c core/rdzip.c \
+ core/rdsrc.c core/rdepub.c core/rdfb2.c core/rdmobi.c core/rdcbz.c engine/rdft.c \
+ "$OBJ"/mz_*.o "$OBJ"/lm_*.o "$OBJ"/rdimg.o "$SYSROOT/lib/libz.a" "$FT_BUILD/libfreetype.a" -o test/formats_smoke_mipsel
+timeout 150 qemu-mipsel ./test/formats_smoke_mipsel "${FONT:-$ROOT/font/src/MiSans-Regular.otf}" /tmp/rd_samples
+
+# ---------- 7) reader 二进制(mipsel 静态) ----------
echo "==> reader binary"
-CORE="core/rdenc.c core/rdlayout.c core/rdtxt.c"
-ENGINE="engine/rdft.c"
-APP="app.c app/shelf.c app/reader.c app/book.c app/progress.c"
-$CC -Os -static -Wall -Wextra -I../sdk -I. -Icore -Iapp -Iengine $FT_INCS \
- $CORE $APP $ENGINE \
- ../sdk/meow_ui.a "$SYSROOT/lib/libz.a" "$FT_BUILD/libfreetype.a" -lpthread -o reader
+CORE_L="core/rdenc.c core/rdlayout.c core/rdtxt.c core/rdxml.c core/rdhtml.c core/rdsb.c core/rdzip.c core/rdsrc.c core/rdepub.c core/rdfb2.c core/rdmobi.c core/rdcbz.c"
+ENGINE_L="engine/rdft.c"
+APP_L="app.c app/shelf.c app/reader.c app/book.c app/progress.c"
+$CC -Os -static -Wall -Wextra -I../sdk -I. -Icore -Iapp -Iengine $FT_INCS $COREINC \
+ $CORE_L $APP_L $ENGINE_L "$OBJ"/mz_*.o "$OBJ"/lm_*.o "$OBJ"/rdimg.o \
+ ../sdk/meow_ui.a "$SYSROOT/lib/libz.a" "$FT_BUILD/libfreetype.a" -lpthread -o reader
ls -la reader
file reader
+md5sum reader
echo " 运行(qemu): qemu-mipsel ./reader"
echo "build_reader OK"
diff --git a/patch_v2/reader_app/core/rdcbz.c b/patch_v2/reader_app/core/rdcbz.c
new file mode 100644
index 0000000..b125c51
--- /dev/null
+++ b/patch_v2/reader_app/core/rdcbz.c
@@ -0,0 +1,128 @@
+/* rdcbz.c — CBZ loader:miniz 解 ZIP → 图片序列(自然序)→ rd_src_page 逐页原始数据(调用方
+ * stb_image 解码+降采样)。纯图阅读,不走排版器。无正文/目录元数据(标题=文件名)。
+ */
+#include "rdsrc_int.h"
+#include "rdzip.h"
+#include
+#include
+#include
+#include
+
+static void stl(char *d, int sz, const char *s)
+{
+ size_t n = s ? strlen(s) : 0;
+ if (sz > 0) { if (n >= (size_t)sz) n = (size_t)sz - 1; memcpy(d, s, n); d[n] = 0; }
+}
+typedef struct {
+ rdzip *zip;
+ char **names; int *idx; int n;
+ int cur; /* next 游标(无正文,恒 EOF) */
+ char base[128];
+} cbz_impl;
+
+static int has_img_ext(const char *name)
+{
+ const char *d = strrchr(name, '.');
+ if (!d) return 0;
+ if (strcasecmp(d, ".jpg") == 0 || strcasecmp(d, ".jpeg") == 0 ||
+ strcasecmp(d, ".png") == 0 || strcasecmp(d, ".gif") == 0 ||
+ strcasecmp(d, ".bmp") == 0 || strcasecmp(d, ".webp") == 0) return 1;
+ return 0;
+}
+/* 自然序:数字段按数值比较,其余按字节序 */
+static int natcmp(const char *a, const char *b)
+{
+ while (*a && *b) {
+ if (isdigit((unsigned char)*a) && isdigit((unsigned char)*b)) {
+ /* 跳前导 0 */
+ while (*a == '0') a++;
+ while (*b == '0') b++;
+ const char *sa = a, *sb = b;
+ while (isdigit((unsigned char)*sa)) sa++;
+ while (isdigit((unsigned char)*sb)) sb++;
+ size_t la = (size_t)(sa - a), lb = (size_t)(sb - b);
+ if (la != lb) return la < lb ? -1 : 1;
+ int c = memcmp(a, b, la);
+ if (c) return c;
+ a = sa; b = sb;
+ } else {
+ unsigned char ca = (unsigned char)*a, cb = (unsigned char)*b;
+ if (ca != cb) return ca < cb ? -1 : 1;
+ a++; b++;
+ }
+ }
+ return (*a ? 1 : (*b ? -1 : 0));
+}
+
+static void cbz_free(rd_src *s)
+{
+ cbz_impl *m = (cbz_impl *)s->impl;
+ if (!m) return;
+ if (m->zip) rdzip_close(m->zip);
+ for (int i = 0; i < m->n; i++) free(m->names[i]);
+ free(m->names);
+ free(m->idx);
+ free(m);
+ s->impl = NULL;
+}
+static int cbz_reset(rd_src *s) { ((cbz_impl *)s->impl)->cur = 0; return 0; }
+static int cbz_next(rd_src *s, uint8_t *buf, size_t cap, size_t *len) { (void)s;(void)buf;(void)cap;(void)len; return 1; }
+static int cbz_seg(rd_src *s, int idx, uint8_t *buf, size_t cap, size_t *len) { (void)s;(void)idx;(void)buf;(void)cap;(void)len; return 1; }
+static int cbz_toc_n(rd_src *s) { (void)s; return 0; }
+static int cbz_toc(rd_src *s, int i, rd_toc_entry *t) { (void)s;(void)i;(void)t; return -1; }
+static int cbz_img(rd_src *s, const char *ref, uint8_t **data, size_t *size) { (void)s;(void)ref;(void)data;(void)size; return -1; }
+static int cbz_pages(rd_src *s) { return ((cbz_impl *)s->impl)->n; }
+static int cbz_page(rd_src *s, int i, uint8_t **data, size_t *size)
+{
+ cbz_impl *m = (cbz_impl *)s->impl;
+ if (!m || i < 0 || i >= m->n || !data || !size) return -1;
+ return rdzip_read(m->zip, m->idx[i], data, size);
+}
+
+static int cbz_open(rd_src *s)
+{
+ cbz_impl *m = (cbz_impl *)calloc(1, sizeof *m);
+ if (!m) return -6;
+ s->impl = m;
+ const char *sl = strrchr(s->path, '/');
+ if (sl) { strncpy(m->base, sl + 1, sizeof m->base - 1); m->base[sizeof m->base - 1] = 0; }
+ else strncpy(m->base, s->path, sizeof m->base - 1), m->base[sizeof m->base - 1] = 0;
+ char *dot = strrchr(m->base, '.'); if (dot) *dot = 0;
+ stl(s->title, sizeof s->title, m->base);
+
+ if (rdzip_open(&m->zip, s->path) != 0) { cbz_free(s); return -2; }
+ int cnt = rdzip_count(m->zip);
+ if (cnt <= 0) { cbz_free(s); return -3; }
+ m->names = (char **)calloc((size_t)cnt, sizeof(char *));
+ m->idx = (int *)calloc((size_t)cnt, sizeof(int));
+ if (!m->names || !m->idx) { cbz_free(s); return -6; }
+ for (int i = 0; i < cnt; i++) {
+ char nm[512];
+ if (rdzip_name(m->zip, i, nm, sizeof nm) != 0) continue;
+ if (nm[0] && nm[strlen(nm)-1] == '/') continue; /* 目录项跳过 */
+ if (!has_img_ext(nm)) continue;
+ m->names[m->n] = strdup(nm);
+ m->idx[m->n++] = i;
+ }
+ /* 自然序(按名字) */
+ for (int i = 1; i < m->n; i++) {
+ char *nvi = m->names[i]; int ivi = m->idx[i];
+ int j = i - 1;
+ while (j >= 0 && natcmp(m->names[j], nvi) > 0) {
+ m->names[j+1] = m->names[j]; m->idx[j+1] = m->idx[j]; j--;
+ }
+ m->names[j+1] = nvi; m->idx[j+1] = ivi;
+ }
+ if (m->n == 0) { cbz_free(s); return -4; }
+ return 0;
+}
+
+static int cbz_probe(const uint8_t *head, size_t n)
+{
+ return (n >= 4 && head[0] == 'P' && head[1] == 'K' && head[2] == 3 && head[3] == 4);
+}
+static const rd_src_vt cbz_vt = {
+ cbz_free, cbz_reset, cbz_next, cbz_seg,
+ cbz_toc_n, cbz_toc, cbz_img, cbz_pages, cbz_page,
+};
+const rd_src_loader rd_cbz_loader = { RD_FMT_CBZ, ".cbz", cbz_probe, cbz_open, &cbz_vt };
diff --git a/patch_v2/reader_app/core/rdepub.c b/patch_v2/reader_app/core/rdepub.c
new file mode 100644
index 0000000..4c389e4
--- /dev/null
+++ b/patch_v2/reader_app/core/rdepub.c
@@ -0,0 +1,531 @@
+/* rdepub.c — EPUB loader:miniz 解 ZIP → rdxml 解析 OPF(spine/manifest)+NCX/nav(目录) →
+ * 正文 XHTML 用 rdhtml 逐行粗解析(不喂 pugixml)→ 产出与 rdtxt 相同的 UTF-8 段契约。
+ *
+ * 内存纪律:ZIP 文件句柄驻留(磁盘),章节文本按需抽/粗解析/用完即释;只保留「段表 + 目录 +
+ * 图片索引」轻量结构 → EPUB 大书不建全 DOM(对齐 READER_ENGINE_NOTES §4)。段表在 next() 期间
+ * lazy 填充,seg(idx) 按段下标重解析章节切片,逐字节确定性。
+ */
+#include "rdsrc_int.h"
+#include "rdzip.h"
+#include "rdxml.h"
+#include "rdhtml.h"
+#include "rdsb.h"
+#include
+#include
+#include
+#include
+#include
+
+/* 有界拷贝(truncate + NUL 终止;杜绝 -Wstringop-truncation) */
+static void stl(char *d, int sz, const char *s)
+{
+ size_t n = s ? strlen(s) : 0;
+ if ((size_t)sz > 0) {
+ if (n >= (size_t)sz) n = (size_t)sz - 1;
+ memcpy(d, s, n); d[n] = 0;
+ }
+}
+
+/* ---------------- 小工具 ---------------- */
+static void path_join(const char *base, const char *rel, char *out, int outsz)
+{
+ char a[400]; stl(a, sizeof a, rel);
+ if (a[0] != '/') {
+ char b[384]; stl(b, sizeof b, base);
+ size_t bl = strlen(b);
+ if (bl && bl + 1 < sizeof b) { b[bl] = '/'; b[bl + 1] = 0; }
+ size_t sl = strlen(a);
+ if (bl + sl + 1 < sizeof b) memcpy(b + bl + 1, a, sl + 1);
+ else if (sizeof b > 1) b[sizeof b - 1] = 0;
+ stl(a, sizeof a, b);
+ }
+ char *stack[96]; int nstack = 0;
+ char *save = NULL;
+ for (char *tok = strtok_r(a, "/", &save); tok; tok = strtok_r(NULL, "/", &save)) {
+ if (strcmp(tok, ".") == 0) continue;
+ if (strcmp(tok, "..") == 0) { if (nstack > 0) nstack--; continue; }
+ if (nstack < 96) stack[nstack++] = tok;
+ }
+ size_t o = 0; out[0] = 0;
+ for (int i = 0; i < nstack; i++) {
+ size_t L = strlen(stack[i]);
+ if (o + L + 2 >= (size_t)outsz) break;
+ if (o) out[o++] = '/';
+ memcpy(out + o, stack[i], L); o += L;
+ }
+ out[o] = 0;
+}
+static void path_dir(const char *p, char *out, int outsz)
+{
+ if (!p) { out[0] = 0; return; }
+ const char *sl = strrchr(p, '/');
+ if (!sl) { out[0] = 0; return; }
+ size_t n = (size_t)(sl - p);
+ if (n >= (size_t)outsz) n = (size_t)outsz - 1;
+ memcpy(out, p, n); out[n] = 0;
+}
+static void path_frag(const char *src, char *path, int pathsz, char *frag, int fragsz)
+{
+ const char *h = strchr(src, '#');
+ if (h) {
+ size_t pn = (size_t)(h - src);
+ if (pn >= (size_t)pathsz) pn = (size_t)pathsz - 1;
+ memcpy(path, src, pn); path[pn] = 0;
+ stl(frag, fragsz, h + 1);
+ } else {
+ stl(path, pathsz, src);
+ frag[0] = 0;
+ }
+}
+static int attr(const rdxml_attr *a, int na, const char *name, char *out, int sz)
+{
+ for (int i = 0; i < na; i++)
+ if (strcmp(a[i].name, name) == 0) { stl(out, sz, a[i].value); return 1; }
+ return 0;
+}
+
+/* ---------------- container.xml ---------------- */
+static void cont_open(const char *n, const rdxml_attr *a, int na, void *ctx)
+{
+ char *opf = (char *)ctx;
+ if (opf[0]) return;
+ if (strcmp(n, "rootfile") == 0) attr(a, na, "full-path", opf, 320);
+}
+static const rdxml_handler cont_h = { cont_open, NULL, NULL };
+
+/* ---------------- OPF ---------------- */
+typedef struct { char *id, *href, *mime; } opf_item;
+typedef struct {
+ int n_manifest; opf_item items[128];
+ int n_spine; char *spine_idref[512];
+ char ncx_src[320], nav_id[64], title[160], author[160];
+} opf_t;
+static void opf_free(opf_t *o)
+{
+ for (int i = 0; i < o->n_manifest; i++) { free(o->items[i].id); free(o->items[i].href); free(o->items[i].mime); }
+ for (int i = 0; i < o->n_spine; i++) free(o->spine_idref[i]);
+ memset(o, 0, sizeof *o);
+}
+typedef struct { opf_t *o; int in_manifest, in_spine, cur_label; char buf[256]; int nbuf; } opfctx;
+static void opf_open(const char *n, const rdxml_attr *a, int na, void *ctx)
+{
+ opfctx *c = (opfctx *)ctx; opf_t *o = c->o;
+ if (strcmp(n, "manifest") == 0) { c->in_manifest = 1; return; }
+ if (strcmp(n, "spine") == 0) { c->in_spine = 1; attr(a, na, "toc", o->ncx_src, sizeof o->ncx_src); return; }
+ if (strcmp(n, "metadata") == 0) return;
+ if (c->in_manifest && strcmp(n, "item") == 0) {
+ char id[128]={0}, href[320]={0}, mime[64]={0};
+ attr(a, na, "id", id, sizeof id); attr(a, na, "href", href, sizeof href); attr(a, na, "media-type", mime, sizeof mime);
+ if (id[0] && href[0] && o->n_manifest < 128) {
+ opf_item *it = &o->items[o->n_manifest++];
+ it->id = strdup(id); it->href = strdup(href); it->mime = strdup(mime);
+ }
+ if (id[0] && (strcmp(id, "nav") == 0)) stl(o->nav_id, sizeof o->nav_id, id);
+ return;
+ }
+ if (c->in_spine && strcmp(n, "itemref") == 0) {
+ char idref[128]={0}; attr(a, na, "idref", idref, sizeof idref);
+ if (idref[0] && o->n_spine < 512) o->spine_idref[o->n_spine++] = strdup(idref);
+ return;
+ }
+ if (strcmp(n, "title") == 0) { c->cur_label = 1; c->nbuf = 0; return; }
+ if (strcmp(n, "creator") == 0) { c->cur_label = 2; c->nbuf = 0; return; }
+}
+static void opf_text(const char *t, size_t len, void *ctx)
+{
+ opfctx *c = (opfctx *)ctx;
+ if (!c->cur_label) return;
+ for (size_t i = 0; i < len && c->nbuf < (int)sizeof c->buf - 1; i++) {
+ uint8_t b = (uint8_t)t[i];
+ if (b == '\r' || b == '\n' || b == '\t') continue;
+ c->buf[c->nbuf++] = (char)b;
+ }
+}
+static void opf_close(const char *n, void *ctx)
+{
+ opfctx *c = (opfctx *)ctx; opf_t *o = c->o;
+ if (strcmp(n, "manifest") == 0) { c->in_manifest = 0; return; }
+ if (strcmp(n, "spine") == 0) { c->in_spine = 0; return; }
+ if (strcmp(n, "title") == 0) { c->buf[c->nbuf] = 0; stl(o->title, sizeof o->title, c->buf); c->cur_label = 0; }
+ if (strcmp(n, "creator") == 0) { c->buf[c->nbuf] = 0; stl(o->author, sizeof o->author, c->buf); c->cur_label = 0; }
+}
+
+/* ---------------- NCX ---------------- */
+typedef struct { char label[160]; char src[320]; } ncxent;
+typedef struct { ncxent *ents; int n, cap; ncxent stk[64]; int sp; int want_label; } ncxctx;
+static void ncx_add(ncxctx *c, const char *label, const char *src)
+{
+ if (c->n >= c->cap) {
+ int nc = c->cap ? c->cap * 2 : 32;
+ ncxent *ne = (ncxent *)realloc(c->ents, (size_t)nc * sizeof(ncxent));
+ if (!ne) return;
+ c->ents = ne; c->cap = nc;
+ }
+ ncxent *e = &c->ents[c->n++];
+ stl(e->label, sizeof e->label, label);
+ stl(e->src, sizeof e->src, src);
+}
+static void ncx_open(const char *n, const rdxml_attr *a, int na, void *ctx)
+{
+ ncxctx *c = (ncxctx *)ctx;
+ if (strcmp(n, "navPoint") == 0) { if (c->sp < 64) { memset(&c->stk[c->sp], 0, sizeof(ncxent)); c->sp++; } return; }
+ if (strcmp(n, "text") == 0) { c->want_label = 1; return; }
+ if (strcmp(n, "content") == 0 && c->sp > 0) attr(a, na, "src", c->stk[c->sp - 1].src, sizeof(c->stk[c->sp - 1].src));
+}
+static void ncx_text(const char *t, size_t len, void *ctx)
+{
+ ncxctx *c = (ncxctx *)ctx;
+ if (!c->want_label || c->sp <= 0) return;
+ ncxent *e = &c->stk[c->sp - 1];
+ for (size_t i = 0; i < len; i++) {
+ uint8_t b = (uint8_t)t[i];
+ if (b == '\r' || b == '\n') continue;
+ size_t n = strlen(e->label);
+ if (n + 1 < sizeof e->label) { e->label[n] = (char)b; e->label[n + 1] = 0; }
+ }
+}
+static void ncx_close(const char *n, void *ctx)
+{
+ ncxctx *c = (ncxctx *)ctx;
+ if (strcmp(n, "text") == 0) { c->want_label = 0; return; }
+ if (strcmp(n, "navPoint") == 0 && c->sp > 0) {
+ ncxent *e = &c->stk[c->sp - 1];
+ if (e->src[0]) ncx_add(c, e->label[0] ? e->label : "?", e->src);
+ c->sp--;
+ }
+}
+
+/* ---------------- impl ---------------- */
+#define MAXSPINE 512
+typedef struct {
+ rdzip *zip;
+ char opf_dir[320];
+ char spine_path[MAXSPINE][320];
+ int n_spine;
+ int *spine_first_seg; /* 每 spine 第一个全局段下标 */
+ /* 段表(next 期间 lazy 填充) */
+ int *seg_spine; size_t *seg_off, *seg_len; int n_seg, cap_seg;
+ /* next 状态机 */
+ int next_spine;
+ rdsb sb;
+ size_t emit_off;
+ int n_toc; ncxent *toc;
+ char **img_refs; int n_img, cap_img;
+ char cover[320];
+} epub_impl;
+
+static void epub_free(rd_src *s)
+{
+ epub_impl *e = (epub_impl *)s->impl;
+ if (!e) return;
+ if (e->zip) rdzip_close(e->zip);
+ free(e->spine_first_seg);
+ free(e->seg_spine); free(e->seg_off); free(e->seg_len);
+ free(e->toc); /* label/src 为内联数组,只释放整块 */
+ for (int i = 0; i < e->n_img; i++) free(e->img_refs[i]);
+ free(e->img_refs);
+ rdsb_free(&e->sb);
+ free(e);
+ s->impl = NULL;
+}
+
+static void nav_extract(const uint8_t *nd, size_t nsz, epub_impl *e);
+
+typedef struct { epub_impl *e; rdsb *sb; char dir[320]; } spinectx;
+static void spine_text(const uint8_t *u, size_t len, void *ctx) { rdsb_put(((spinectx *)ctx)->sb, u, len); }
+static void spine_nl(void *ctx) { rdsb_nl(((spinectx *)ctx)->sb); }
+static void spine_img(const char *src, void *ctx)
+{
+ spinectx *c = (spinectx *)ctx;
+ char path[320], frag[16], abs[320];
+ path_frag(src, path, sizeof path, frag, sizeof frag);
+ path_join(c->dir, path, abs, sizeof abs);
+ if (!abs[0]) return;
+ epub_impl *e = c->e;
+ if (!e->cover[0] && strstr(abs, "cover")) stl(e->cover, sizeof e->cover, abs);
+ for (int i = 0; i < e->n_img; i++) if (strcmp(e->img_refs[i], abs) == 0) { rdsb_put(c->sb, (const uint8_t *)"\n[图]\n", 7); return; }
+ if (e->n_img >= e->cap_img) { int nc = e->cap_img ? e->cap_img * 2 : 32; char **ni = (char **)realloc(e->img_refs, (size_t)nc * sizeof(char*)); if (ni) { e->img_refs = ni; e->cap_img = nc; } }
+ if (e->n_img < e->cap_img) {
+ e->img_refs[e->n_img++] = strdup(abs);
+ rdsb_put(c->sb, (const uint8_t *)"\n[图]\n", 7);
+ }
+}
+
+static void spine_parse(epub_impl *e, int idx, rdsb *sb)
+{
+ rdsb_init(sb);
+ if (idx < 0 || idx >= e->n_spine) return;
+ uint8_t *data = NULL; size_t sz = 0;
+ if (rdzip_read_named(e->zip, e->spine_path[idx], &data, &sz) != 0) { rdsb_nl(sb); return; }
+ char dir[320]; path_dir(e->spine_path[idx], dir, sizeof dir);
+ spinectx c; c.e = e; c.sb = sb; memcpy(c.dir, dir, sizeof c.dir);
+ rdhtml_out out = { spine_text, spine_nl, spine_img, NULL };
+ if (rdhtml_parse(data, sz, &out, &c, 0) == 0) { /* 无 body → 照收全部 */
+ rdsb_free(sb); rdsb_init(sb);
+ rdhtml_parse(data, sz, &out, &c, 1);
+ }
+ while (sb->len && (sb->buf[sb->len - 1] == '\n' || sb->buf[sb->len - 1] == ' ')) sb->len--;
+ free(data);
+}
+
+static int add_seg(epub_impl *e, int sp, size_t off, size_t len)
+{
+ if (e->n_seg >= e->cap_seg) {
+ int nc = e->cap_seg ? e->cap_seg * 2 : 256;
+ int *ns = (int *)realloc(e->seg_spine, (size_t)nc * sizeof(int));
+ size_t *no = (size_t *)realloc(e->seg_off, (size_t)nc * sizeof(size_t));
+ size_t *nl = (size_t *)realloc(e->seg_len, (size_t)nc * sizeof(size_t));
+ if (!ns || !no || !nl) return -1;
+ e->seg_spine = ns; e->seg_off = no; e->seg_len = nl; e->cap_seg = nc;
+ }
+ e->seg_spine[e->n_seg] = sp;
+ e->seg_off[e->n_seg] = off;
+ e->seg_len[e->n_seg] = len;
+ e->n_seg++;
+ return 0;
+}
+
+/* 产生下一个「段元数据」并追加到段表;返回 0=ok(填 sp/off/len),1=eof。 */
+static int yield_segmeta(epub_impl *e, int *sp, size_t *off, size_t *len)
+{
+ while (1) {
+ if (e->next_spine >= e->n_spine) return 1;
+ if (e->emit_off == 0) {
+ spine_parse(e, e->next_spine, &e->sb);
+ if (e->sb.len == 0) { e->next_spine++; continue; }
+ }
+ if (e->emit_off >= e->sb.len) {
+ rdsb_free(&e->sb); rdsb_init(&e->sb); e->emit_off = 0;
+ e->next_spine++;
+ continue;
+ }
+ size_t co = e->emit_off, nlen = 0, nxt = 0;
+ int rc = rdsb_chunk(&e->sb, &co, NULL, 0, &nlen, &nxt);
+ if (rc != 0) { rdsb_free(&e->sb); rdsb_init(&e->sb); e->emit_off = 0; e->next_spine++; continue; }
+ if (co == 0) e->spine_first_seg[e->next_spine] = e->n_seg; /* 该 spine 首段 */
+ *sp = e->next_spine; *off = co; *len = nlen;
+ add_seg(e, e->next_spine, co, nlen);
+ e->emit_off = nxt;
+ return 0;
+ }
+}
+static void epub_build_all(epub_impl *e)
+{
+ int sp; size_t off, n;
+ while (yield_segmeta(e, &sp, &off, &n) == 0) { }
+}
+static int copy_segtext(epub_impl *e, int idx, uint8_t *buf, size_t cap, size_t *len)
+{
+ if (idx < 0 || idx >= e->n_seg) return 1;
+ int sp = e->seg_spine[idx];
+ rdsb sb; spine_parse(e, sp, &sb);
+ size_t co = e->seg_off[idx], n = e->seg_len[idx];
+ if (co >= sb.len) { rdsb_free(&sb); return 1; }
+ if (co + n > sb.len) n = sb.len - co;
+ if (n > cap) n = cap;
+ memcpy(buf, sb.buf + co, n); *len = n;
+ rdsb_free(&sb);
+ return 0;
+}
+
+static int epub_reset(rd_src *s)
+{
+ epub_impl *e = (epub_impl *)s->impl;
+e->next_spine = 0; e->emit_off = 0;
+ rdsb_free(&e->sb); rdsb_init(&e->sb);
+ return 0;
+}
+static int epub_next(rd_src *s, uint8_t *buf, size_t cap, size_t *len)
+{
+ epub_impl *e = (epub_impl *)s->impl;
+ int sp = -1; size_t off = 0, n = 0;
+ (void)sp;
+ int rc = yield_segmeta(e, &sp, &off, &n);
+ if (rc != 0) return rc == 1 ? 1 : -1;
+ /* 直接从当前 spine 缓冲(保留在 e->sb)切片 */
+ if (off >= e->sb.len || n == 0) return epub_next(s, buf, cap, len);
+ size_t cl = n; if (off + cl > e->sb.len) cl = e->sb.len - off;
+ if (cl > cap) cl = cap;
+ memcpy(buf, e->sb.buf + off, cl); *len = cl;
+ return 0;
+}
+static int epub_seg(rd_src *s, int idx, uint8_t *buf, size_t cap, size_t *len)
+{
+ epub_impl *e = (epub_impl *)s->impl;
+ while (e->n_seg <= idx) { /* 惰性建表到 idx */
+ int sp; size_t off, n;
+ if (yield_segmeta(e, &sp, &off, &n) != 0) break;
+ }
+ if (idx < 0 || idx >= e->n_seg) return 1;
+ return copy_segtext(e, idx, buf, cap, len);
+}
+static int epub_toc_n(rd_src *s) { const epub_impl *e = (const epub_impl *)s->impl; return e->n_toc; }
+static int epub_toc(rd_src *s, int i, rd_toc_entry *t)
+{
+ epub_impl *e = (epub_impl *)s->impl;
+ if (i < 0 || i >= e->n_toc) return -1;
+ epub_build_all(e); /* 确保段表已建,spine_first_seg 有效 */
+ char path[320], frag[16], abs[320];
+ path_frag(e->toc[i].src, path, sizeof path, frag, sizeof frag);
+ path_join(e->opf_dir, path, abs, sizeof abs);
+ int sp = -1;
+ for (int k = 0; k < e->n_spine; k++) if (strcmp(e->spine_path[k], abs) == 0) { sp = k; break; }
+ if (sp < 0) return -1;
+ stl(t->title, sizeof t->title, e->toc[i].label);
+ t->seg = e->spine_first_seg[sp];
+ t->ch = 0;
+ return 0;
+}
+static int epub_img(rd_src *s, const char *ref, uint8_t **data, size_t *size)
+{
+ const epub_impl *e = (const epub_impl *)s->impl;
+ if (!ref || !data || !size || !e->zip) return -1;
+ return rdzip_read_named(e->zip, ref, data, size);
+}
+static int epub_pages(rd_src *s) { (void)s; return 0; }
+static int epub_page(rd_src *s, int i, uint8_t **data, size_t *size) { (void)s;(void)i;(void)data;(void)size; return -1; }
+
+static int epub_open(rd_src *s)
+{
+ epub_impl *e = (epub_impl *)calloc(1, sizeof *e);
+ if (!e) return -6;
+ s->impl = e;
+ e->spine_first_seg = (int *)calloc(MAXSPINE, sizeof(int));
+ if (!e->spine_first_seg) { epub_free(s); return -6; }
+ for (int i = 1; i < MAXSPINE; i++) e->spine_first_seg[i] = -1;
+
+ if (rdzip_open(&e->zip, s->path) != 0) { epub_free(s); return -2; }
+ uint8_t *data = NULL; size_t sz = 0;
+ char opf[320] = {0};
+ if (rdzip_read_named(e->zip, "META-INF/container.xml", &data, &sz) == 0 && sz > 0) {
+ rdxml_parse(data, sz, &cont_h, opf);
+ free(data);
+ }
+ if (!opf[0]) { epub_free(s); return -3; }
+ path_dir(opf, e->opf_dir, sizeof e->opf_dir);
+
+ data = NULL; sz = 0;
+ if (rdzip_read_named(e->zip, opf, &data, &sz) != 0 || sz == 0) { epub_free(s); return -3; }
+ opf_t o; memset(&o, 0, sizeof o);
+ opfctx c; memset(&c, 0, sizeof c); c.o = &o;
+ rdxml_handler oh = { opf_open, opf_close, opf_text };
+ rdxml_parse(data, sz, &oh, &c);
+ free(data);
+ stl(s->title, sizeof s->title, o.title);
+ stl(s->author, sizeof s->author, o.author);
+
+ /* spine path */
+ e->n_spine = 0;
+ for (int i = 0; i < o.n_spine && e->n_spine < MAXSPINE; i++) {
+ const char *href = NULL;
+ for (int k = 0; k < o.n_manifest; k++) if (strcmp(o.items[k].id, o.spine_idref[i]) == 0) { href = o.items[k].href; break; }
+ if (!href) continue;
+ char abs[320]; path_join(e->opf_dir, href, abs, sizeof abs);
+ stl(e->spine_path[e->n_spine], sizeof(e->spine_path[0]), abs);
+ e->n_spine++;
+ }
+ e->spine_first_seg[0] = 0; /* 首 spine 首段=0;其余在建 seg 时填充 */
+ for (int i = 1; i < e->n_spine; i++) e->spine_first_seg[i] = 0;
+
+ /* NCX */
+ {
+ char ncx_href[320] = {0};
+ if (o.ncx_src[0]) {
+ /* spine 的 toc 属性通常是 manifest id → 解析 href;若本身就是路径则直接用 */
+ for (int k = 0; k < o.n_manifest; k++) if (strcmp(o.items[k].id, o.ncx_src) == 0) { stl(ncx_href, sizeof ncx_href, o.items[k].href); break; }
+ if (!ncx_href[0]) stl(ncx_href, sizeof ncx_href, o.ncx_src);
+ }
+ if (!ncx_href[0]) for (int k = 0; k < o.n_manifest; k++) if (strcmp(o.items[k].mime, "application/x-dtbncx+xml") == 0) { stl(ncx_href, sizeof ncx_href, o.items[k].href); break; }
+ if (ncx_href[0]) {
+ char abs[320]; path_join(e->opf_dir, ncx_href, abs, sizeof abs);
+ uint8_t *nd = NULL; size_t nsz = 0;
+ if (rdzip_read_named(e->zip, abs, &nd, &nsz) == 0 && nsz) {
+ ncxctx nc; memset(&nc, 0, sizeof nc);
+ rdxml_handler nh = { ncx_open, ncx_close, ncx_text };
+ rdxml_parse(nd, nsz, &nh, &nc);
+ e->toc = nc.ents; e->n_toc = nc.n;
+ free(nd);
+ }
+ }
+ }
+ /* nav(EPUB3,仅当无 NCX):从 nav.xhtml 抽 text */
+ if (e->n_toc == 0 && o.nav_id[0]) {
+ char abs[320];
+ for (int k = 0; k < o.n_manifest; k++) if (strcmp(o.items[k].id, o.nav_id) == 0) {
+ path_join(e->opf_dir, o.items[k].href, abs, sizeof abs);
+ uint8_t *nd = NULL; size_t nsz = 0;
+ if (rdzip_read_named(e->zip, abs, &nd, &nsz) == 0 && nsz) {
+ nav_extract(nd, nsz, e);
+ free(nd);
+ }
+ break;
+ }
+ }
+
+ opf_free(&o);
+ return 0;
+}
+
+/* nav 抽取:…text… 一个扁平目录 */
+static void nav_extract(const uint8_t *nd, size_t nsz, epub_impl *e)
+{
+ int in_a = 0; char href[320] = {0}; char label[160] = {0}; size_t llen = 0;
+ size_t i = 0;
+ while (i < nsz) {
+ if (nd[i] == '<') {
+ if (i + 1 < nsz && (nd[i+1] == '/' ) && i + 2 < nsz && tolower(nd[i+2]) == 'a') {
+ in_a = 0; i++; while (i < nsz && nd[i] != '>') i++; i++; continue;
+ }
+ /* a opening */
+ size_t k = i + 1; size_t name_s = k;
+ while (k < nsz && !strchr(" \t>\r\n/", nd[k])) k++;
+ if (k - name_s == 1 && tolower(nd[name_s]) == 'a') {
+ /* parse href */
+ in_a = 1; href[0] = 0; label[0] = 0; llen = 0;
+ while (k < nsz && nd[k] != '>') {
+ /* skip ws */
+ while (k < nsz && (nd[k] == ' ' || nd[k] == '\t')) k++;
+ size_t an = k; while (k < nsz && nd[k] != '=' && nd[k] != '>' && nd[k] != ' ' && nd[k] != '\t') k++;
+ size_t ae = k;
+ while (k < nsz && (nd[k] == ' ' || nd[k] == '\t')) k++;
+ if (k < nsz && nd[k] == '=') {
+ k++;
+ while (k < nsz && (nd[k] == ' ' || nd[k] == '\t')) k++;
+ uint8_t q = 0; if (k < nsz && (nd[k] == '"' || nd[k] == '\'')) { q = nd[k]; k++; }
+ size_t vs = k; while (k < nsz && (q ? nd[k] != q : nd[k] != '>' && nd[k] != ' ' && nd[k] != '\t')) k++;
+ size_t ve = k; if (k < nsz && q && nd[k] == q) k++;
+ if (ae - an == 4 && strncasecmp((const char*)nd + an, "href", 4) == 0) {
+ size_t L = ve - vs; if (L >= sizeof href) L = sizeof href - 1;
+ memcpy(href, nd + vs, L); href[L] = 0;
+ }
+ } else break;
+ }
+ i = k + 1; continue;
+ }
+ i = k; continue;
+ }
+ if (in_a) {
+ uint8_t b = nd[i];
+ if (b == '\r' || b == '\n' || b == '\t') { i++; continue; }
+ if (b == ' ') { /* 折叠 */ if (llen && label[llen-1] != ' ') { label[llen++] = ' '; } i++; continue; }
+ if (llen < sizeof label - 1) label[llen++] = (char)b;
+ i++; continue;
+ }
+ i++;
+ }
+ if (href[0] && label[0]) {
+ ncxent *ne = (ncxent *)realloc(e->toc, (size_t)(e->n_toc + 1) * sizeof(ncxent));
+ if (ne) { e->toc = ne; ncxent *dst = &e->toc[e->n_toc++]; stl(dst->label, sizeof dst->label, label); stl(dst->src, sizeof dst->src, href); }
+ }
+}
+
+/* ---------------- 注册 ---------------- */
+static int epub_probe(const uint8_t *head, size_t n)
+{
+ return (n >= 4 && head[0] == 'P' && head[1] == 'K' && head[2] == 3 && head[3] == 4);
+}
+static const rd_src_vt epub_vt = {
+ epub_free, epub_reset, epub_next, epub_seg,
+ epub_toc_n, epub_toc, epub_img, epub_pages, epub_page,
+};
+const rd_src_loader rd_epub_loader = { RD_FMT_EPUB, ".epub", epub_probe, epub_open, &epub_vt };
diff --git a/patch_v2/reader_app/core/rdfb2.c b/patch_v2/reader_app/core/rdfb2.c
new file mode 100644
index 0000000..68b0c17
--- /dev/null
+++ b/patch_v2/reader_app/core/rdfb2.c
@@ -0,0 +1,280 @@
+/* rdfb2.c — FB2 loader:严格 XML,用自写 rdxml 回调解析器抽段落。
+ * 结构: → (元数据跳过) → (主) → … → ;
+ *
段题(目录);BASE64 内嵌图(FBReader 式 base64 解码存 list);
+ * 在文内落 "[图:ref]" 占位。
+ *
+ * 内存纪律:全文读入解析一次后即释;段文本存段表(可读正文),内嵌图解码一次存 image 列表。
+ */
+#include "rdsrc_int.h"
+#include "rdxml.h"
+#include "rdsb.h"
+#include
+#include
+#include
+
+static void stl(char *d, int sz, const char *s)
+{
+ size_t n = s ? strlen(s) : 0;
+ if (sz > 0) { if (n >= (size_t)sz) n = (size_t)sz - 1; memcpy(d, s, n); d[n] = 0; }
+}
+typedef struct fb2_img_t { char id[64]; uint8_t *data; size_t size; } fb2_img_t;
+typedef struct fb2_seg_t { char *text; size_t len; char title[160]; } fb2_seg_t;
+typedef struct fb2_toc_t { char title[160]; int seg; } fb2_toc_t;
+
+typedef struct {
+ char *path;
+ fb2_seg_t *segs; int n_seg, cap_seg;
+ fb2_toc_t *toc; int n_toc, cap_toc;
+ fb2_img_t *imgs; int n_img, cap_img;
+ int cur;
+} fb2_impl;
+
+/* 解析 ctx:section 栈(text/title 各一个 rdsb),加文本模式标志 */
+typedef struct {
+ fb2_impl *m;
+ rdsb text[64], title[64]; int sp;
+ int in_main, seen_body;
+ int cur_title, in_binary, in_desc;
+ char bin_id[64]; rdsb b64;
+} fb2c;
+
+static void fb2_free(rd_src *s)
+{
+ fb2_impl *m = (fb2_impl *)s->impl;
+ if (!m) return;
+ for (int i = 0; i < m->n_seg; i++) free(m->segs[i].text);
+ free(m->segs);
+ for (int i = 0; i < m->n_img; i++) free(m->imgs[i].data);
+ free(m->imgs);
+ free(m->toc);
+ free(m);
+ s->impl = NULL;
+}
+static int seg_push(fb2_impl *m, rdsb *text, rdsb *title)
+{
+ if (m->n_seg >= m->cap_seg) {
+ int nc = m->cap_seg ? m->cap_seg * 2 : 64;
+ fb2_seg_t *ns = (fb2_seg_t *)realloc(m->segs, (size_t)nc * sizeof(fb2_seg_t));
+ if (!ns) return -1;
+ m->segs = ns; m->cap_seg = nc;
+ }
+ fb2_seg_t *sg = &m->segs[m->n_seg];
+ sg->text = (char *)malloc(text->len ? text->len : 1);
+ if (!sg->text) return -1;
+ if (text->len) memcpy(sg->text, text->buf, text->len);
+ sg->len = text->len;
+ size_t tl = title->len, k = 0;
+ while (k < tl && (title->buf[k] == '\n' || title->buf[k] == ' ' || title->buf[k] == '\t')) k++;
+ size_t s0 = k; while (k < tl && title->buf[k] != '\n') k++;
+ if (k - s0 >= sizeof sg->title) k = s0 + sizeof sg->title - 1;
+ memcpy(sg->title, title->buf + s0, k - s0); sg->title[k - s0] = 0;
+ m->n_seg++;
+ return 0;
+}
+static int toc_push(fb2_impl *m, const char *title, int seg)
+{
+ if (m->n_toc >= m->cap_toc) {
+ int nc = m->cap_toc ? m->cap_toc * 2 : 32;
+ fb2_toc_t *nt = (fb2_toc_t *)realloc(m->toc, (size_t)nc * sizeof(fb2_toc_t));
+ if (!nt) return -1;
+ m->toc = nt; m->cap_toc = nc;
+ }
+ fb2_toc_t *t = &m->toc[m->n_toc++];
+ stl(t->title, sizeof t->title, title);
+ t->seg = seg;
+ return 0;
+}
+static int img_add(fb2_impl *m, const char *id, uint8_t *data, size_t size)
+{
+ if (m->n_img >= m->cap_img) {
+ int nc = m->cap_img ? m->cap_img * 2 : 8;
+ fb2_img_t *ni = (fb2_img_t *)realloc(m->imgs, (size_t)nc * sizeof(fb2_img_t));
+ if (!ni) return -1;
+ m->imgs = ni; m->cap_img = nc;
+ }
+ fb2_img_t *im = &m->imgs[m->n_img++];
+ stl(im->id, sizeof im->id, id);
+ im->data = data; im->size = size;
+ return 0;
+}
+
+static void fb2_xml_open(const char *n, const rdxml_attr *a, int na, void *ctx)
+{
+ fb2c *c = (fb2c *)ctx;
+ if (strcmp(n, "body") == 0) { if (!c->seen_body) { c->seen_body = 1; c->in_main = 1; } else c->in_main = 0; return; }
+ if (strcmp(n, "description") == 0) { c->in_desc = 1; return; }
+ if (strcmp(n, "section") == 0 && c->in_main) {
+ if (c->sp < 64) { rdsb_init(&c->text[c->sp]); rdsb_init(&c->title[c->sp]); c->sp++; }
+ return;
+ }
+ if (strcmp(n, "title") == 0 && c->in_main && c->sp > 0) { c->cur_title = 1; return; }
+ if (strcmp(n, "binary") == 0) {
+ c->in_binary = 1; c->bin_id[0] = 0;
+ for (int i = 0; i < na; i++) if (strcmp(a[i].name, "id") == 0) { stl(c->bin_id, sizeof c->bin_id, a[i].value); }
+ rdsb_init(&c->b64);
+ return;
+ }
+ if (strcmp(n, "image") == 0 && c->in_main && !c->in_binary && c->sp > 0) {
+ char ref[96] = {0};
+ for (int i = 0; i < na; i++)
+ if (strcmp(a[i].name, "href") == 0 || strcmp(a[i].name, "xlink:href") == 0 || strcmp(a[i].name, "l:href") == 0)
+ stl(ref, sizeof ref, a[i].value);
+ if (ref[0] == '#') memmove(ref, ref + 1, strlen(ref));
+ if (ref[0]) {
+ rdsb *tb = c->cur_title ? &c->title[c->sp - 1] : &c->text[c->sp - 1];
+ rdsb_put(tb, (const uint8_t *)"\n[图:", 4);
+ rdsb_put(tb, (const uint8_t *)ref, strlen(ref));
+ rdsb_put(tb, (const uint8_t *)"]\n", 2);
+ }
+ return;
+ }
+}
+static void fb2_xml_text(const char *t, size_t len, void *ctx)
+{
+ fb2c *c = (fb2c *)ctx;
+ if (c->in_binary) { rdsb_put(&c->b64, (const uint8_t *)t, len); return; }
+ if (c->in_desc || !c->in_main || c->sp <= 0) return;
+ rdsb *dst = c->cur_title ? &c->title[c->sp - 1] : &c->text[c->sp - 1];
+ uint8_t dec[512];
+ size_t done = 0;
+ while (done < len) {
+ size_t want = len - done; if (want > sizeof dec - 2) want = sizeof dec - 2;
+ size_t n = rd_ent_decode_to((const uint8_t *)t + done, want, dec, sizeof dec);
+ rdsb_put(dst, dec, n);
+ done += want;
+ }
+}
+static void fb2_xml_close(const char *n, void *ctx)
+{
+ fb2c *c = (fb2c *)ctx; fb2_impl *m = c->m;
+ if (strcmp(n, "body") == 0) { c->in_main = 0; return; }
+ if (strcmp(n, "description") == 0) { c->in_desc = 0; return; }
+ if (strcmp(n, "title") == 0) {
+ c->cur_title = 0;
+ /* 段题也进入正文(标题作段首),保证「第一章」出现在渲染文本中 */
+ if (c->in_main && c->sp > 0 && c->title[c->sp - 1].len > 0) {
+ rdsb_put(&c->text[c->sp - 1], c->title[c->sp - 1].buf, c->title[c->sp - 1].len);
+ rdsb_nl(&c->text[c->sp - 1]);
+ }
+ return;
+ }
+ if (strcmp(n, "p") == 0 && c->in_main && c->sp > 0) {
+ rdsb *dst = c->cur_title ? &c->title[c->sp - 1] : &c->text[c->sp - 1];
+ rdsb_nl(dst);
+ return;
+ }
+ if (strcmp(n, "binary") == 0 && c->in_binary) {
+ c->in_binary = 0;
+ size_t dlen = 0; uint8_t *dec = NULL;
+ if (c->b64.len > 0) {
+ size_t cap = c->b64.len / 4 * 3 + 4;
+ dec = (uint8_t *)malloc(cap);
+ if (dec && rd_b64_decode(c->b64.buf, c->b64.len, dec, cap, &dlen) != 0) { free(dec); dec = NULL; }
+ }
+ if (c->bin_id[0]) { if (!dec) { dec = (uint8_t *)malloc(1); dlen = 0; } img_add(m, c->bin_id, dec, dlen); }
+ else free(dec);
+ rdsb_free(&c->b64);
+ return;
+ }
+ if (strcmp(n, "section") == 0 && c->in_main && c->sp > 0) {
+ c->sp--;
+ if (c->text[c->sp].len > 0 || c->title[c->sp].len > 1) {
+ int idx = m->n_seg;
+ if (seg_push(m, &c->text[c->sp], &c->title[c->sp]) == 0) {
+ if (m->segs[idx].title[0]) toc_push(m, m->segs[idx].title, idx);
+ }
+ }
+ rdsb_free(&c->text[c->sp]); rdsb_free(&c->title[c->sp]);
+ return;
+ }
+}
+
+/* ---- vtable ---- */
+static int fb2_reset(rd_src *s) { ((fb2_impl *)s->impl)->cur = 0; return 0; }
+static int fb2_next(rd_src *s, uint8_t *buf, size_t cap, size_t *len)
+{
+ fb2_impl *m = (fb2_impl *)s->impl;
+ if (m->cur >= m->n_seg) return 1;
+ fb2_seg_t *sg = &m->segs[m->cur++];
+ size_t n = sg->len;
+ if (n > cap) n = cap;
+ if (n) memcpy(buf, sg->text, n);
+ *len = n;
+ return 0;
+}
+static int fb2_seg(rd_src *s, int idx, uint8_t *buf, size_t cap, size_t *len)
+{
+ fb2_impl *m = (fb2_impl *)s->impl;
+ if (idx < 0 || idx >= m->n_seg) return 1;
+ fb2_seg_t *sg = &m->segs[idx];
+ size_t n = sg->len; if (n > cap) n = cap;
+ if (n) memcpy(buf, sg->text, n);
+ *len = n;
+ return 0;
+}
+static int fb2_toc_n(rd_src *s) { return ((fb2_impl *)s->impl)->n_toc; }
+static int fb2_toc(rd_src *s, int i, rd_toc_entry *t)
+{
+ fb2_impl *m = (fb2_impl *)s->impl;
+ if (i < 0 || i >= m->n_toc) return -1;
+ stl(t->title, sizeof t->title, m->toc[i].title);
+ t->seg = m->toc[i].seg; t->ch = 0;
+ return 0;
+}
+static int fb2_img(rd_src *s, const char *ref, uint8_t **data, size_t *size)
+{
+ fb2_impl *m = (fb2_impl *)s->impl;
+ if (!data || !size || !ref) return -1;
+ for (int i = 0; i < m->n_img; i++) if (strcmp(m->imgs[i].id, ref) == 0) {
+ *data = (uint8_t *)malloc(m->imgs[i].size ? m->imgs[i].size : 1);
+ if (!*data) return -6;
+ if (m->imgs[i].size) memcpy(*data, m->imgs[i].data, m->imgs[i].size);
+ *size = m->imgs[i].size;
+ return 0;
+ }
+ return -1;
+}
+static int fb2_pages(rd_src *s) { (void)s; return 0; }
+static int fb2_page(rd_src *s, int i, uint8_t **data, size_t *size) { (void)s;(void)i;(void)data;(void)size; return -1; }
+
+static int fb2_open(rd_src *s)
+{
+ fb2_impl *m = (fb2_impl *)calloc(1, sizeof *m);
+ if (!m) return -6;
+ s->impl = m;
+ m->path = strdup(s->path);
+ FILE *f = fopen(s->path, "rb");
+ if (!f) { fb2_free(s); return -1; }
+ fseek(f, 0, SEEK_END); long sz = ftell(f); fseek(f, 0, SEEK_SET);
+ if (sz <= 0) { fclose(f); fb2_free(s); return -2; }
+ uint8_t *buf = (uint8_t *)malloc((size_t)sz);
+ if (!buf) { fclose(f); fb2_free(s); return -6; }
+ size_t got = fread(buf, 1, (size_t)sz, f);
+ fclose(f);
+
+ fb2c c; memset(&c, 0, sizeof c); c.m = m;
+ rdxml_handler h = { fb2_xml_open, fb2_xml_close, fb2_xml_text };
+ rdxml_parse(buf, got, &h, &c);
+ free(buf);
+ stl(s->title, sizeof s->title, "FB2");
+ /* 书名用第一个 section title?默认文件名 */
+ if (m->n_toc > 0) stl(s->title, sizeof s->title, m->toc[0].title);
+ return 0;
+}
+
+static int fb2_probe(const uint8_t *head, size_t n)
+{
+ /* '
+#include
+
+typedef struct {
+ const rdhtml_out *out; void *ctx;
+ uint8_t run[1024]; int run_len;
+ int pending_space;
+ int inbody; int seen_body;
+ int in_heading; /* 当前在 h1..h6 内 */
+ uint8_t hbuf[256]; int hlen; /* 标题文本缓冲 */
+} hx;
+static int is_heading_name(const uint8_t *nm, size_t nl)
+{
+ if (nl < 2 || nl > 3 || nm[0] != 'h') return 0;
+ uint8_t c = nm[1];
+ if (c < '1' || c > '6') return 0;
+ if (nl == 3 && nm[2] != '\0') return 0;
+ return 1;
+}
+static void hx_heading_flush(hx *x)
+{
+ /* 去掉首尾空白后回调 */
+ if (x->in_heading && x->out && x->out->heading) {
+ int s = 0, e = x->hlen;
+ while (s < e && (x->hbuf[s] == ' ' || x->hbuf[s] == '\n' || x->hbuf[s] == '\t')) s++;
+ while (e > s && (x->hbuf[e-1] == ' ' || x->hbuf[e-1] == '\n' || x->hbuf[e-1] == '\t')) e--;
+ if (e > s) x->out->heading(x->hbuf + s, (size_t)(e - s), x->ctx);
+ }
+ x->hlen = 0;
+}
+
+static void hx_flush(hx *x)
+{
+ if (x->run_len > 0) {
+ if (x->out && x->out->text) x->out->text(x->run, (size_t)x->run_len, x->ctx);
+ x->run_len = 0;
+ }
+}
+static void hx_space(hx *x)
+{
+ hx_flush(x);
+ if (x->pending_space) return;
+ if (x->out && x->out->text) {
+ static const uint8_t sp = ' ';
+ x->out->text(&sp, 1, x->ctx);
+ }
+ if (x->in_heading && x->hlen < (int)sizeof x->hbuf) x->hbuf[x->hlen] = ' ', x->hlen++;
+ x->pending_space = 1;
+}
+static void hx_byte(hx *x, uint8_t b)
+{
+ if (x->pending_space) x->pending_space = 0;
+ if (x->run_len >= (int)sizeof x->run) hx_flush(x);
+ x->run[x->run_len++] = b;
+ if (x->in_heading && x->hlen < (int)sizeof x->hbuf) x->hbuf[x->hlen++] = b;
+}
+static void hx_newline(hx *x)
+{
+ hx_flush(x);
+ x->pending_space = 0;
+ if (x->out && x->out->newline) x->out->newline(x->ctx);
+}
+
+/* 在 tag 子串(ta[0..tlen),不含首 '<')里查属性 attr(局部名),写解码值到 val(≤vsz)。
+ * 返回 1=找到。宽松:把 namespace 前缀剥离(':' 或 ':' 取后段)。 */
+static int attr_lookup(const uint8_t *ta, size_t tlen, const char *attr, char *val, size_t vsz)
+{
+ size_t i = 0;
+ size_t alen = strlen(attr);
+ while (i < tlen) {
+ /* 跳空白与 '/' '>' */
+ while (i < tlen && (ta[i] == ' ' || ta[i] == '\t' || ta[i] == '\r' || ta[i] == '\n')) i++;
+ if (i >= tlen || ta[i] == '>' || ta[i] == '/') break;
+ size_t ns = i;
+ while (i < tlen && ta[i] != ' ' && ta[i] != '\t' && ta[i] != '=' && ta[i] != '>' && ta[i] != '/') i++;
+ size_t ne = i;
+ /* 局部名:剥 ':' 前缀 */
+ size_t lstart = ns, lend = ne;
+ for (size_t k = ns; k < ne; k++) if (ta[k] == ':') { lstart = k + 1; }
+ size_t ln = lend - lstart;
+ while (i < tlen && (ta[i] == ' ' || ta[i] == '\t')) i++;
+ if (i < tlen && ta[i] == '=') {
+ i++;
+ while (i < tlen && (ta[i] == ' ' || ta[i] == '\t')) i++;
+ if (i < tlen && (ta[i] == '"' || ta[i] == '\'')) {
+ uint8_t q = ta[i]; i++;
+ size_t vs = i;
+ while (i < tlen && ta[i] != q) i++;
+ size_t ve = i;
+ if (i < tlen && ta[i] == q) i++;
+ if (ln == alen && memcmp(ta + lstart, attr, alen) == 0) {
+ size_t dec = rd_ent_decode_to(ta + vs, ve - vs, (uint8_t *)val, vsz);
+ if (dec >= vsz) dec = vsz - 1;
+ val[dec] = 0;
+ return 1;
+ }
+ } else {
+ size_t vs = i;
+ while (i < tlen && ta[i] != ' ' && ta[i] != '\t' && ta[i] != '>') i++;
+ if (ln == alen && memcmp(ta + lstart, attr, alen) == 0) {
+ size_t vlen = i - vs; if (vlen >= vsz) vlen = vsz - 1;
+ memcpy(val, ta + vs, vlen); val[vlen] = 0;
+ return 1;
+ }
+ }
+ }
+ }
+ return 0;
+}
+
+static int name_eq_cs(const uint8_t *nm, size_t nl, const char *s)
+{
+ size_t sl = strlen(s);
+ if (nl != sl) return 0;
+ for (size_t k = 0; k < nl; k++) {
+ uint8_t a = nm[k]; if (a >= 'A' && a <= 'Z') a += 32;
+ if (a != (uint8_t)s[k]) return 0;
+ }
+ return 1;
+}
+static int name_eq_css(const uint8_t *nm, size_t nl, const char *s) /* 已小写 s */
+{
+ return name_eq_cs(nm, nl, s);
+}
+
+/* 判定是否块级断行 tag(局部名小写)。 */
+static int is_block_tag(const uint8_t *nm, size_t nl, int is_close)
+{
+ /* 开/闭都断行:p div li h1..h6 blockquote section article table tr td ul ol dl dt dd
+ * center hr br(br 行断) */
+ static const char *blocks[] = { "p","div","li","ul","ol","dl","dt","dd","h1","h2","h3",
+ "h4","h5","h6","blockquote","section","article","tr","td",
+ "th","table","center","hr","br","address","pre" };
+ (void)is_close;
+ for (size_t k = 0; k < sizeof blocks / sizeof blocks[0]; k++)
+ if (name_eq_css(nm, nl, blocks[k])) return 1;
+ return 0;
+}
+
+int rdhtml_parse(const uint8_t *html, size_t len, const rdhtml_out *out, void *ctx, int emit_all)
+{
+ if (!html) return 0;
+ hx x; memset(&x, 0, sizeof x);
+ x.out = out; x.ctx = ctx;
+ x.inbody = emit_all; /* emit_all: 无 body 门控 */
+ int raw_until = 0; /* 0=无;>0=处于 raw 区(1=script,2=style) */
+ size_t i = 0;
+
+ while (i < len) {
+ uint8_t c = html[i];
+ if (raw_until) {
+ /* 在 ";
+ size_t etl = (raw_until == 1) ? 8 : 7;
+ while (j + etl <= len && !(tolower(html[j]) == '<' && tolower(html[j + 1]) == '/' &&
+ memcmp(html + j + 2, endtag + 2, etl - 2) == 0)) j++;
+ if (j + etl <= len) { i = j; raw_until = 0; }
+ else i = len;
+ continue;
+ }
+ if (c == '<') {
+ /* 处理 PI/注释/doctype(粗略跳过) */
+ if (i + 1 < len && html[i + 1] == '?') { while (i < len && !(html[i] == '?' && i + 1 < len && html[i + 1] == '>')) i++; i += (i < len) ? 2 : 0; continue; }
+ if (i + 3 < len && memcmp(html + i, "