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 回调解析器抽段落。 + * 结构: → (元数据跳过) → (主) →
… →

; + * 段题(目录);<binary id>BASE64</binary> 内嵌图(FBReader 式 base64 解码存 list); + * <image l:href="#id"> 在文内落 "[图:ref]" 占位。 + * + * 内存纪律:全文读入解析一次后即释;段文本存段表(可读正文),内嵌图解码一次存 image 列表。 + */ +#include "rdsrc_int.h" +#include "rdxml.h" +#include "rdsb.h" +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +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) +{ + /* '<FictionBook' 开头 */ + if (n < 13) return 0; + if (memcmp(head, "<?xml", 5) == 0) { + for (size_t i = 0; i < n - 10; i++) + if (memcmp(head + i, "<FictionBook", 12) == 0) return 1; + } + if (memcmp(head, "<FictionBook", 12) == 0) return 1; + return 0; +} +static const rd_src_vt fb2_vt = { + fb2_free, fb2_reset, fb2_next, fb2_seg, + fb2_toc_n, fb2_toc, fb2_img, fb2_pages, fb2_page, +}; +const rd_src_loader rd_fb2_loader = { RD_FMT_FB2, ".fb2", fb2_probe, fb2_open, &fb2_vt }; diff --git a/patch_v2/reader_app/core/rdhtml.c b/patch_v2/reader_app/core/rdhtml.c new file mode 100644 index 0000000..d21be7e --- /dev/null +++ b/patch_v2/reader_app/core/rdhtml.c @@ -0,0 +1,286 @@ +/* rdhtml.c — XHTML / raw HTML 粗解析(实现见 rdhtml.h)。 */ +#include "rdhtml.h" +#include "rdxml.h" /* 实体解码 rd_ent_decode / rd_ent_decode_to */ +#include <string.h> +#include <ctype.h> + +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) { + /* 在 <script>/<style> raw 区:找匹配闭合标签 */ + size_t j = i; + const char *endtag = (raw_until == 1) ? "</script" : "</style>"; + 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, "<!--", 4) == 0) { while (i + 2 < len && !(html[i] == '-' && html[i + 1] == '-' && html[i + 2] == '>')) i++; i += (i + 2 < len) ? 3 : 0; continue; } + if (i + 8 < len && memcmp(html + i, "<![CDATA[", 9) == 0) { + size_t s = i + 9; size_t j = s; + while (j + 2 < len && !(html[j] == ']' && html[j + 1] == ']' && html[j + 2] == '>')) j++; + if (x.inbody) { if (!x.pending_space && x.run_len == 0) hx_flush(&x); for (size_t kk = s; kk < j; kk++) hx_byte(&x, html[kk]); } + i = (j + 2 < len) ? j + 3 : len; continue; + } + if (i + 1 < len && html[i + 1] == '!') { while (i < len && html[i] != '>') i++; i += (i < len) ? 1 : 0; continue; } + + /* 正常标签:读到 '>'(尊重引号),截断到 MAX_TAG */ + size_t j = i + 1; + uint8_t q = 0; + int selfclose = 0; + size_t tagend = (size_t)-1; + while (j < len) { + uint8_t tc = html[j]; + if (q) { if (tc == q) q = 0; } + else if (tc == '"' || tc == '\'') q = tc; + else if (tc == '>') { tagend = j; break; } + j++; + if (j - i > 1000) { /* 超长 tag:吞到下一个 '>'(防溢出) */ + while (j < len && html[j] != '>') j++; + if (j < len) { tagend = j; } + break; + } + } + if (tagend == (size_t)-1) break; + + uint8_t tagbuf[1000]; size_t tl = tagend - (i + 1); + if (tl > sizeof tagbuf) tl = sizeof tagbuf; + memcpy(tagbuf, html + i + 1, tl); + + size_t k = 0; + while (k < tl && (tagbuf[k] == ' ' || tagbuf[k] == '\t' || tagbuf[k] == '\r' || tagbuf[k] == '\n')) k++; + int is_close = (k < tl && tagbuf[k] == '/'); + if (is_close) k++; + while (k < tl && (tagbuf[k] == ' ' || tagbuf[k] == '\t' || tagbuf[k] == '\r' || tagbuf[k] == '\n')) k++; + size_t ns = k; + while (k < tl && tagbuf[k] != ' ' && tagbuf[k] != '>' && tagbuf[k] != '/' && tagbuf[k] != '\t' && + tagbuf[k] != '\r' && tagbuf[k] != '\n') k++; + size_t ne = k; + + /* 自闭合判断:末尾 '/>' */ + { + size_t e = tl; while (e > 0 && (tagbuf[e - 1] == ' ' || tagbuf[e - 1] == '\t')) e--; + if (e >= 1 && tagbuf[e - 1] == '/') selfclose = 1; + } + + /* script/style */ + if (name_eq_css(tagbuf + ns, ne - ns, "script") && !is_close) { raw_until = 1; i = tagend + 1; continue; } + if (name_eq_css(tagbuf + ns, ne - ns, "style") && !is_close) { raw_until = 2; i = tagend + 1; continue; } + if ((name_eq_css(tagbuf + ns, ne - ns, "script") || name_eq_css(tagbuf + ns, ne - ns, "style")) && is_close) { raw_until = 0; i = tagend + 1; continue; } + + /* <body> 门控 */ + if (!selfclose && name_eq_css(tagbuf + ns, ne - ns, "body") && !is_close) { x.seen_body = 1; x.inbody = 1; i = tagend + 1; continue; } + if (name_eq_css(tagbuf + ns, ne - ns, "body") && is_close) { x.inbody = 0; i = tagend + 1; continue; } + + if (!x.inbody && !emit_all) { i = tagend + 1; continue; } + + /* 图片 */ + if (!selfclose && !is_close && name_eq_css(tagbuf + ns, ne - ns, "img")) { + char src[512]; src[0] = 0; + if (attr_lookup(tagbuf, tl, "src", src, sizeof src) && src[0]) { + hx_flush(&x); x.pending_space = 0; + if (x.out && x.out->image) x.out->image(src, x.ctx); + } + i = tagend + 1; continue; + } + + /* 标题(h1..h6):闭合时回调标题文本;仍作块级断行 */ + if (is_heading_name(tagbuf + ns, ne - ns)) { + if (is_close) { hx_heading_flush(&x); x.in_heading = 0; hx_newline(&x); } + else { x.in_heading = 1; x.hlen = 0; x.pending_space = 0; hx_newline(&x); } + i = tagend + 1; continue; + } + /* 块级断行 */ + if (name_eq_css(tagbuf + ns, ne - ns, "rt")) { i = tagend + 1; continue; } /* v1 忽略 ruby rt */ + if (name_eq_css(tagbuf + ns, ne - ns, "ruby")) { i = tagend + 1; continue; } + if (is_block_tag(tagbuf + ns, ne - ns, is_close)) { + if (!is_close || !selfclose) hx_newline(&x); + i = tagend + 1; continue; + } + + i = tagend + 1; + continue; + } + if (c == '&') { + /* 实体:找到 ';' */ + size_t es = i + 1; size_t e = es; + while (e < len && html[e] != ';' && e - es < 16) e++; + uint8_t tmp[8]; int dn; + if (e < len && html[e] == ';') { + dn = rd_ent_decode(html + i, (e + 1) - i, tmp, sizeof tmp); + } else dn = -1; + if (dn > 0) { + if (x.inbody) for (int bi = 0; bi < dn; bi++) { + uint8_t b = tmp[bi]; + if (b == ' ') hx_space(&x); + else hx_byte(&x, b); + } + i = (e < len) ? e + 1 : len; + continue; + } + /* 未识别实体:吐 '&' 继续 */ + if (x.inbody) hx_space(&x); /* & 罕见作实义字符,给个空格(宽松) */ + i++; + continue; + } + /* 文本 */ + if (!x.inbody && !emit_all) { i++; continue; } + if (c == '\r') { i++; continue; } + if (c == '\t' || c == '\n') { hx_space(&x); i++; continue; } + if (c == ' ') { hx_space(&x); i++; continue; } + hx_byte(&x, c); + i++; + } + hx_flush(&x); + return x.seen_body; +} diff --git a/patch_v2/reader_app/core/rdhtml.h b/patch_v2/reader_app/core/rdhtml.h new file mode 100644 index 0000000..f4887ea --- /dev/null +++ b/patch_v2/reader_app/core/rdhtml.h @@ -0,0 +1,35 @@ +/* rdhtml.h — XHTML / raw HTML 粗解析(机身正文;容错,不建 DOM,不喂 pugixml)。 + * + * 对齐 READER_ENGINE_NOTES §4.2-4.4(epub2txt2 式三态机):未知/坏标签吞掉、块级 tag 断行、 + * 实体内省、<script>/<style> 内容当 raw text 跳过、<body> 前内容丢弃(无 body 时收全部)、 + * `\r` 跳过/`\t`→空格/连续空格折叠、`\n` 折叠为空格(块断由回调给)。 + * + * 输出经回调(非直接产出缓冲):text(utf8,len) / newline() / image(src)。 + * 调用方(rdsrc 各 loader)用这些事件组装「UTF-8 段 + {块断} + 图片引用」。 + * 纯 C,宿主可测,无 SDK 依赖。 + */ +#ifndef RDHTML_H +#define RDHTML_H + +#include <stddef.h> +#include <stdint.h> + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct rdhtml_out { + void (*text)(const uint8_t *utf8, size_t len, void *ctx); + void (*newline)(void *ctx); /* 段/行断(块级 tag / <br>) */ + void (*image)(const char *src, void *ctx); /* <img src>,src 原样 */ + void (*heading)(const uint8_t *utf8, size_t len, void *ctx); /* 可选:闭合 h1..h6 时回调其文本 */ +} rdhtml_out; + +/* 解析 [html, html+len)。emit_all=1 → 忽略 <body> 门控(无 body 的篇章照收,但 script/style + * 仍跳过)。返回 1=遇到 <body> 进入正文;0=全程在 <body> 外(可据此再用 emit_all 重扫)。 */ +int rdhtml_parse(const uint8_t *html, size_t len, const rdhtml_out *out, void *ctx, int emit_all); + +#ifdef __cplusplus +} +#endif +#endif /* RDHTML_H */ diff --git a/patch_v2/reader_app/core/rdmobi.c b/patch_v2/reader_app/core/rdmobi.c new file mode 100644 index 0000000..4ee1507 --- /dev/null +++ b/patch_v2/reader_app/core/rdmobi.c @@ -0,0 +1,237 @@ +/* rdmobi.c — MOBI/AZW3 loader:vendor libmobi 最小调用面(mobi_init→load→parse_kf7/8→parse_rawml + * → flow(html 章节) + resources(图片)),HTML 走 rdhtml 同款粗解析 → UTF-8 段契约。 + * + * 合规:libmobi 为 LGPL-3.0;NOTICE 文件(版权+LGPL 文本+锁定版本)见 patch_v2/reader_app/NOTICE。 + * libmobi 编译用系统 zlib(USE_MINIZ 未定义→m_uncompress=uncompress),不与其内置 miniz 冲突。 + * + * 内存纪律:open 时遍历 flow 部件抽文本(可读文字)→ 段表;复制 resources 图片到 list;然后析构 + * rawml+mobi(释掉整书 HTML/标记),稳态 = 可读正文 + 图片。 + */ +#include "rdsrc_int.h" +#include "rdhtml.h" +#include "rdsb.h" +#include "mobi.h" +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +typedef struct m_info { char uid_s[24]; uint8_t *data; size_t size; } m_info; +typedef struct m_seg { char *text; size_t len; } m_seg; +typedef struct m_toc { char title[160]; int seg; } m_toc; + +typedef struct { + m_seg *segs; int n_seg, cap_seg; + m_toc *toc; int n_toc, cap_toc; + m_info *imgs; int n_img, cap_img; + int cur; + int *flow_first_seg; int n_flow; +} mobi_impl; + +/* 解析某一 flow 部件 → rdsb 文本;同时捕获首个标题(回调) */ +typedef struct { rdsb *sb; char title[160]; int have_title; } motrice; +static void mo_text(const uint8_t *u, size_t len, void *ctx) { rdsb_put(((motrice *)ctx)->sb, u, len); } +static void mo_nl(void *ctx) { rdsb_nl(((motrice *)ctx)->sb); } +static void mo_img(const char *src, void *ctx) { (void)src; motrice *m = (motrice *)ctx; rdsb_put(m->sb, (const uint8_t *)"\n[图]\n", 7); } +static void mo_heading(const uint8_t *u, size_t len, void *ctx) +{ + motrice *m = (motrice *)ctx; + if (m->have_title || len == 0) return; + size_t n = len < sizeof m->title - 1 ? len : sizeof m->title - 1; + memcpy(m->title, u, n); m->title[n] = 0; m->have_title = 1; +} + +static void mobi_free_src(rd_src *s) +{ + mobi_impl *m = (mobi_impl *)s->impl; + if (!m) return; + for (int i = 0; i < m->n_seg; i++) free(m->segs[i].text); + free(m->segs); + free(m->toc); + for (int i = 0; i < m->n_img; i++) free(m->imgs[i].data); + free(m->imgs); + free(m->flow_first_seg); + free(m); + s->impl = NULL; +} +static int push_seg(mobi_impl *m, const char *text, size_t len) +{ + if (m->n_seg >= m->cap_seg) { + int nc = m->cap_seg ? m->cap_seg * 2 : 64; + m_seg *ns = (m_seg *)realloc(m->segs, (size_t)nc * sizeof(m_seg)); + if (!ns) return -1; + m->segs = ns; m->cap_seg = nc; + } + m_seg *sg = &m->segs[m->n_seg++]; + sg->text = (char *)malloc(len ? len : 1); + if (!sg->text) return -1; + if (len) memcpy(sg->text, text, len); + sg->len = len; + return 0; +} +static int push_toc(mobi_impl *m, const char *title, int seg) +{ + if (m->n_toc >= m->cap_toc) { + int nc = m->cap_toc ? m->cap_toc * 2 : 32; + m_toc *nt = (m_toc *)realloc(m->toc, (size_t)nc * sizeof(m_toc)); + if (!nt) return -1; + m->toc = nt; m->cap_toc = nc; + } + m_toc *t = &m->toc[m->n_toc++]; + strncpy(t->title, title, sizeof t->title - 1); t->title[sizeof t->title - 1] = 0; + t->seg = seg; + return 0; +} +static int push_img(mobi_impl *m, const char *uid_s, uint8_t *data, size_t size) +{ + if (m->n_img >= m->cap_img) { + int nc = m->cap_img ? m->cap_img * 2 : 8; + m_info *ni = (m_info *)realloc(m->imgs, (size_t)nc * sizeof(m_info)); + if (!ni) return -1; + m->imgs = ni; m->cap_img = nc; + } + m_info *im = &m->imgs[m->n_img++]; + strncpy(im->uid_s, uid_s, sizeof im->uid_s - 1); im->uid_s[sizeof im->uid_s - 1] = 0; + im->data = data; im->size = size; + return 0; +} + +static int mobi_reset(rd_src *s) { ((mobi_impl *)s->impl)->cur = 0; return 0; } +static int mobi_next(rd_src *s, uint8_t *buf, size_t cap, size_t *len) +{ + mobi_impl *m = (mobi_impl *)s->impl; + if (m->cur >= m->n_seg) return 1; + m_seg *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 mobi_seg(rd_src *s, int idx, uint8_t *buf, size_t cap, size_t *len) +{ + mobi_impl *m = (mobi_impl *)s->impl; + if (idx < 0 || idx >= m->n_seg) return 1; + m_seg *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 mobi_toc_n(rd_src *s) { return ((mobi_impl *)s->impl)->n_toc; } +static int mobi_toc(rd_src *s, int i, rd_toc_entry *t) +{ + mobi_impl *m = (mobi_impl *)s->impl; + if (i < 0 || i >= m->n_toc) return -1; + strncpy(t->title, m->toc[i].title, sizeof t->title - 1); t->title[sizeof t->title - 1] = 0; + t->seg = m->toc[i].seg; t->ch = 0; + return 0; +} +static int mobi_img(rd_src *s, const char *ref, uint8_t **data, size_t *size) +{ + mobi_impl *m = (mobi_impl *)s->impl; + if (!ref || !data || !size) return -1; + for (int i = 0; i < m->n_img; i++) if (strcmp(m->imgs[i].uid_s, 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 mobi_pages(rd_src *s) { (void)s; return 0; } +static int mobi_page(rd_src *s, int i, uint8_t **data, size_t *size) { (void)s;(void)i;(void)data;(void)size; return -1; } + +static void parse_flow_to_segs(mobi_impl *m, const unsigned char *data, size_t size, + int flow_idx) +{ + (void)flow_idx; + rdsb sb; rdsb_init(&sb); + motrice c; memset(&c, 0, sizeof c); c.sb = &sb; + rdhtml_out out = { mo_text, mo_nl, mo_img, mo_heading }; + if (rdhtml_parse(data, size, &out, &c, 0) == 0) { + rdsb_free(&sb); rdsb_init(&sb); + rdhtml_parse(data, size, &out, &c, 1); + } + while (sb.len && (sb.buf[sb.len-1] == '\n' || sb.buf[sb.len-1] == ' ')) sb.len--; + if (c.have_title) { push_toc(m, c.title, m->n_seg); } + else { push_toc(m, "无标题", m->n_seg); } + /* 按 RDSRC_SEG_MAX 切段存 */ + size_t off = 0; + while (off < sb.len) { + uint8_t tmp[RDSRC_SEG_MAX + 8]; size_t nlen = 0, nxt = 0; + if (rdsb_chunk(&sb, &off, tmp, sizeof tmp, &nlen, &nxt) != 0) break; + push_seg(m, (const char *)tmp, nlen); + off = nxt; + if (off == 0) break; /* 防死循环 */ + } + rdsb_free(&sb); +} + +static int mobi_open(rd_src *s) +{ + mobi_impl *m = (mobi_impl *)calloc(1, sizeof *m); + if (!m) return -6; + s->impl = m; + + MOBIData *md = mobi_init(); + if (!md) { mobi_free_src(s); return -6; } + if (mobi_load_filename(md, s->path) != MOBI_SUCCESS) { mobi_free(md); mobi_free_src(s); return -2; } + size_t ver = mobi_get_fileversion(md); + MOBI_RET pr = MOBI_SUCCESS; + if (mobi_is_hybrid(md) || ver >= 8) pr = mobi_parse_kf8(md); + if (pr != MOBI_SUCCESS) pr = mobi_parse_kf7(md); + if (pr != MOBI_SUCCESS) { mobi_free(md); mobi_free_src(s); return -3; } + + MOBIRawml *rawml = mobi_init_rawml(md); + if (!rawml) { mobi_free(md); mobi_free_src(s); return -6; } + /* 只取 flow(章节 HTML)+resources(图片):不开 TOC/字典/FDST 重建(否则对无 INDX 的最小样本报错) */ + if (mobi_parse_rawml_opt(rawml, md, false, false, false) != MOBI_SUCCESS) { + if (mobi_parse_rawml(rawml, md) != MOBI_SUCCESS) { mobi_free_rawml(rawml); mobi_free(md); mobi_free_src(s); return -4; } + } + + char *title = mobi_meta_get_title(md); if (title) { strncpy(s->title, title, sizeof s->title - 1); s->title[sizeof s->title - 1] = 0; free(title); } + char *author = mobi_meta_get_author(md); if (author) { strncpy(s->author, author, sizeof s->author - 1); s->author[sizeof s->author - 1] = 0; free(author); } + if (!s->title[0]) { + char fn[160]; mobi_get_fullname(md, fn, sizeof fn); + if (fn[0]) { strncpy(s->title, fn, sizeof s->title - 1); s->title[sizeof s->title - 1] = 0; } + } + + /* flow 部件 → 段 */ + int nflow = 0; + for (MOBIPart *p = rawml->flow; p; p = p->next) nflow++; + m->flow_first_seg = (int *)calloc((size_t)(nflow ? nflow : 1), sizeof(int)); + int fi = 0; + for (MOBIPart *p = rawml->flow; p; p = p->next, fi++) { + if (fi > 0) m->flow_first_seg[fi] = m->n_seg; + if (p->data && p->size > 0) parse_flow_to_segs(m, p->data, p->size, fi); + } + + /* resources → 图片列表(uid 字符串 key) */ + for (MOBIPart *p = rawml->resources; p; p = p->next) { + if (p->type == T_JPG || p->type == T_GIF || p->type == T_PNG || p->type == T_BMP) { + if (p->size == 0 || !p->data) continue; + uint8_t *cp = (uint8_t *)malloc(p->size); + if (!cp) continue; + memcpy(cp, p->data, p->size); + char us[24]; snprintf(us, sizeof us, "%zu", p->uid); + push_img(m, us, cp, p->size); + if (!s->cover[0]) strncpy(s->cover, us, sizeof s->cover - 1); + } + } + + mobi_free_rawml(rawml); + mobi_free(md); + if (m->n_seg == 0) { mobi_free_src(s); return -5; } + return 0; +} + +static int mobi_probe(const uint8_t *head, size_t n) +{ + /* PDB magic */ + return (n >= 60 && memcmp(head + 60, "BOOKMOBI", 8) == 0); +} +static const rd_src_vt mobi_vt = { + mobi_free_src, mobi_reset, mobi_next, mobi_seg, + mobi_toc_n, mobi_toc, mobi_img, mobi_pages, mobi_page, +}; +const rd_src_loader rd_mobi_loader = { RD_FMT_MOBI, ".mobi", mobi_probe, mobi_open, &mobi_vt }; diff --git a/patch_v2/reader_app/core/rdsb.c b/patch_v2/reader_app/core/rdsb.c new file mode 100644 index 0000000..5322982 --- /dev/null +++ b/patch_v2/reader_app/core/rdsb.c @@ -0,0 +1,70 @@ +/* rdsb.c — 段组装器(实现见 rdsb.h)。 */ +#include "rdsb.h" +#include <stdlib.h> +#include <string.h> + +void rdsb_init(rdsb *b) +{ + if (!b) return; + b->buf = NULL; b->len = b->cap = 0; +} +void rdsb_free(rdsb *b) +{ + if (!b) return; + free(b->buf); + b->buf = NULL; b->len = b->cap = 0; +} +static int rdsb_reserve(rdsb *b, size_t extra) +{ + if (b->len + extra <= b->cap) return 0; + size_t ncap = b->cap ? b->cap * 2 : 4096; + while (ncap < b->len + extra) ncap *= 2; + uint8_t *nb = (uint8_t *)realloc(b->buf, ncap); + if (!nb) return -1; + b->buf = nb; b->cap = ncap; + return 0; +} +int rdsb_put(rdsb *b, const uint8_t *s, size_t n) +{ + if (!b || (!s && n)) return -1; + if (rdsb_reserve(b, n) != 0) return -1; + memcpy(b->buf + b->len, s, n); + b->len += n; + return 0; +} +void rdsb_nl(rdsb *b) +{ + if (!b) return; + if (rdsb_reserve(b, 1) != 0) return; + b->buf[b->len++] = '\n'; +} + +int rdsb_chunk(const rdsb *b, size_t *off, uint8_t *out, size_t cap, size_t *outlen, size_t *next_off) +{ + if (!b || !off || !outlen || !next_off) return -1; /* 允许 out==NULL(只取元数据) */ + if (*off >= b->len) return 1; + size_t start = *off; + size_t want = start + RDSRC_SEG_MAX < b->len ? start + RDSRC_SEG_MAX : b->len; + /* 尽量在 '\n' 处断(段落界);若 want 处即 '\n' 或 start 到 want 无 '\n',则强切 */ + size_t cut = want; + size_t nl = (size_t)-1; + for (size_t k = start + 1; k < want; k++) if (b->buf[k] == '\n') nl = k; + if (nl != (size_t)-1 && nl > start) cut = nl + 1; /* 落在段后 */ + else { + /* 无 '\n':按最多 want 切,保证 UTF-8 边界 */ + cut = want; + if (cut < b->len) { + size_t back = 0; + while (back < 3 && (b->buf[cut - 1 - back] & 0xC0) == 0x80) back++; + if (back) cut -= back; + } + } + size_t n = cut - start; /* 真实分块长(不随 cap 截断) */ + if (out && n) { + size_t c = n > cap ? cap : n; + memcpy(out, b->buf + start, c); + } + *outlen = n; + *next_off = cut; + return 0; +} diff --git a/patch_v2/reader_app/core/rdsb.h b/patch_v2/reader_app/core/rdsb.h new file mode 100644 index 0000000..a6fbca1 --- /dev/null +++ b/patch_v2/reader_app/core/rdsb.h @@ -0,0 +1,34 @@ +/* rdsb.h — 段组装器:把 rdhtml 事件段组装成「UTF-8 段落流」(\n 分段),并按 RDSRC_SEG_MAX + * 在 \n 边界切成 sub-seg 供 rd_src 产出。纯 C,宿主可测。 */ +#ifndef RDSB_H +#define RDSB_H + +#include <stddef.h> +#include <stdint.h> + +#ifdef __cplusplus +extern "C" { +#endif + +/* 段(sub-seg)UTF-8 上限,与 R1 rdtxt 的 RDTXT_SEG_UTF8_MAX 对齐(96KB)。 */ +#define RDSRC_SEG_MAX (96 * 1024) + +typedef struct rdsb { + uint8_t *buf; + size_t len, cap; +} rdsb; + +void rdsb_init(rdsb *b); +void rdsb_free(rdsb *b); +int rdsb_put(rdsb *b, const uint8_t *s, size_t n); /* 0 ok; 负=oom */ +void rdsb_nl(rdsb *b); /* 追加 '\n' */ + +/* 把 b 的内容从 off 起切一段 ≤RDSRC_SEG_MAX 到 out(cap≥RDSRC_SEG_MAX+8)。 + * 切分优先落在 '\n'(段落界);无 '\n' 则按最多 RDSRC_SEG_MAX 强切(UTF-8 边界保持)。 + * 返回 0=正常(*next_off 前进);1=b 已切完;负=错。 */ +int rdsb_chunk(const rdsb *b, size_t *off, uint8_t *out, size_t cap, size_t *outlen, size_t *next_off); + +#ifdef __cplusplus +} +#endif +#endif /* RDSB_H */ diff --git a/patch_v2/reader_app/core/rdsrc.c b/patch_v2/reader_app/core/rdsrc.c new file mode 100644 index 0000000..d127346 --- /dev/null +++ b/patch_v2/reader_app/core/rdsrc.c @@ -0,0 +1,176 @@ +/* rdsrc.c — 统一文档源调度:按扩展名/magic 选格式 loader,开放 rd_src。含 TXT 包装(复用 rdtxt)。 + * 对外只暴露 rd_src_* 接口(见 rdsrc.h)。 + */ +#include "rdsrc.h" +#include "rdsrc_int.h" +#include "rdtxt.h" +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +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; } +} + +/* ---------------- 各格式 loader(由 rdsrc.c 引用,rdsrc_int.h 头) ---------------- */ +extern const rd_src_loader rd_fb2_loader; +extern const rd_src_loader rd_epub_loader; +extern const rd_src_loader rd_mobi_loader; +extern const rd_src_loader rd_cbz_loader; + +/* ---------------- TXT 包装(复用 rdtxt) ---------------- */ +typedef struct { + rdtxt_info info; + size_t *off, *len; int nsegs, cap; /* 段 (file_off,file_len) 表 */ + int cur; +} txt_impl; + +static void txt_free(rd_src *s) +{ + txt_impl *m = (txt_impl *)s->impl; + if (!m) return; + free(m->off); free(m->len); + free(m); + s->impl = NULL; +} +static int txt_open(rd_src *s) +{ + txt_impl *m = (txt_impl *)calloc(1, sizeof *m); + if (!m) return -6; + s->impl = m; + if (rdtxt_probe(s->path, &m->info) != 0) { txt_free(s); return -1; } + /* 建段表:一次扫描(off,len) */ + rdtxt_reader *r = NULL; + if (rdtxt_reader_open(s->path, &m->info, &r) != 0) { txt_free(s); return -2; } + uint8_t *scratch = (uint8_t *)malloc(RDTXT_SEG_UTF8_MAX + 8); + if (!scratch) { rdtxt_reader_free(r); txt_free(s); return -6; } + size_t ul, off, flen; + while (rdtxt_reader_next(r, scratch, RDTXT_SEG_UTF8_MAX + 8, &ul, &off, &flen) == 0) { + if (m->nsegs >= m->cap) { + int nc = m->cap ? m->cap * 2 : 64; + size_t *no = (size_t *)realloc(m->off, (size_t)nc * sizeof(size_t)); + size_t *nl = (size_t *)realloc(m->len, (size_t)nc * sizeof(size_t)); + if (!no || !nl) break; + m->off = no; m->len = nl; m->cap = nc; + } + m->off[m->nsegs] = off; m->len[m->nsegs] = flen; m->nsegs++; + } + free(scratch); + rdtxt_reader_free(r); + /* 标题 = 文件名 */ + const char *sl = strrchr(s->path, '/'); + stl(s->title, sizeof s->title, sl ? sl + 1 : s->path); + char *dot = strrchr(s->title, '.'); if (dot) *dot = 0; + return 0; +} +static int txt_reset(rd_src *s) { ((txt_impl *)s->impl)->cur = 0; return 0; } +static int txt_next(rd_src *s, uint8_t *buf, size_t cap, size_t *len) +{ + txt_impl *m = (txt_impl *)s->impl; + if (m->cur >= m->nsegs) return 1; + size_t off = m->off[m->cur], flen = m->len[m->cur]; m->cur++; + int rc = rdtxt_convert_span(s->path, m->info.enc, off, flen, buf, cap, len); + return (rc == RD_ENC_OK || rc == RD_ENC_ERR_BUFSZ) ? 0 : -3; +} +static int txt_seg(rd_src *s, int idx, uint8_t *buf, size_t cap, size_t *len) +{ + txt_impl *m = (txt_impl *)s->impl; + if (idx < 0 || idx >= m->nsegs) return 1; + int rc = rdtxt_convert_span(s->path, m->info.enc, m->off[idx], m->len[idx], buf, cap, len); + return (rc == RD_ENC_OK || rc == RD_ENC_ERR_BUFSZ) ? 0 : -3; +} +static int txt_toc_n(rd_src *s) { (void)s; return 0; } +static int txt_toc(rd_src *s, int i, rd_toc_entry *t) { (void)s;(void)i;(void)t; return -1; } +static int txt_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 txt_pages(rd_src *s) { (void)s; return 0; } +static int txt_page(rd_src *s, int i, uint8_t **data, size_t *size) { (void)s;(void)i;(void)data;(void)size; return -1; } +static const rd_src_vt txt_vt = { + txt_free, txt_reset, txt_next, txt_seg, txt_toc_n, txt_toc, txt_img, txt_pages, txt_page, +}; +static int txt_probe(const uint8_t *head, size_t n) { (void)head; (void)n; return 0; } +static const rd_src_loader rd_txt_loader = { RD_FMT_TXT, ".txt", txt_probe, txt_open, &txt_vt }; + + +/* ---------------- 调度 ---------------- */ +static const rd_src_loader *g_loaders[] = { + &rd_txt_loader, &rd_fb2_loader, &rd_epub_loader, &rd_mobi_loader, &rd_cbz_loader, +}; +#define N_LOADERS ((int)(sizeof g_loaders / sizeof g_loaders[0])) + +static int is_ext(const char *path, const char *ext) +{ + size_t pl = strlen(path), el = strlen(ext); + if (el == 0 || pl < el) return 0; + return strcasecmp(path + pl - el, ext) == 0; /* ext 含点(如 ".cbz") */ +} + +int rd_src_open(rd_src **out, const char *path) +{ + if (!out || !path) return -1; + /* 读前 64 字节做 magic 探测 */ + uint8_t head[64]; size_t hn = 0; + FILE *f = fopen(path, "rb"); + if (f) { hn = fread(head, 1, sizeof head, f); fclose(f); } + + const rd_src_loader *pick = NULL; + for (int i = 0; i < N_LOADERS; i++) + if (g_loaders[i]->ext && is_ext(path, g_loaders[i]->ext)) { pick = g_loaders[i]; break; } + if (!pick) { + for (int i = 0; i < N_LOADERS; i++) + if (g_loaders[i]->probe && g_loaders[i]->probe(head, hn)) { pick = g_loaders[i]; break; } + } + if (!pick) return -9; + + rd_src *s = (rd_src *)calloc(1, sizeof *s); + if (!s) return -6; + if (strlen(path) >= sizeof s->path) { free(s); return -2; } + strcpy(s->path, path); + s->fmt = pick->fmt; + int rc = pick->open(s); + if (rc != 0) { s->vt = NULL; s->impl = NULL; rd_src_free(s); return rc; } + s->vt = pick->vt; + *out = s; + return 0; +} + +void rd_src_free(rd_src *s) +{ + if (!s) return; + if (s->vt && s->vt->vt_free) s->vt->vt_free(s); + free(s); +} +rd_fmt_t rd_src_fmt(const rd_src *s) { return s ? s->fmt : RD_FMT_TXT; } +const char *rd_src_title(const rd_src *s) { return (s && s->title[0]) ? s->title : (s ? s->path : ""); } +const char *rd_src_author(const rd_src *s) { return (s && s->author[0]) ? s->author : "佚名"; } +const char *rd_src_cover(const rd_src *s) { return (s && s->cover[0]) ? s->cover : NULL; } + +int rd_src_reset(rd_src *s) { return (s && s->vt && s->vt->vt_reset) ? s->vt->vt_reset(s) : 0; } +int rd_src_next(rd_src *s, uint8_t *buf, size_t cap, size_t *len) +{ + return (s && s->vt && s->vt->vt_next) ? s->vt->vt_next(s, buf, cap, len) : 1; +} +int rd_src_seg(const rd_src *s, int idx, uint8_t *buf, size_t cap, size_t *len) +{ + return (s && s->vt && s->vt->vt_seg) ? s->vt->vt_seg((rd_src *)s, idx, buf, cap, len) : 1; +} +int rd_src_toc_n(const rd_src *s) +{ + return (s && s->vt && s->vt->vt_toc_n) ? s->vt->vt_toc_n((rd_src *)s) : 0; +} +int rd_src_toc(const rd_src *s, int i, rd_toc_entry *t) +{ + return (s && s->vt && s->vt->vt_toc) ? s->vt->vt_toc((rd_src *)s, i, t) : -1; +} +int rd_src_img(const rd_src *s, const char *ref, uint8_t **data, size_t *size) +{ + return (s && s->vt && s->vt->vt_img) ? s->vt->vt_img((rd_src *)s, ref, data, size) : -1; +} +int rd_src_pages(const rd_src *s) +{ + return (s && s->vt && s->vt->vt_pages) ? s->vt->vt_pages((rd_src *)s) : 0; +} +int rd_src_page(const rd_src *s, int i, uint8_t **data, size_t *size) +{ + return (s && s->vt && s->vt->vt_page) ? s->vt->vt_page((rd_src *)s, i, data, size) : -1; +} diff --git a/patch_v2/reader_app/core/rdsrc.h b/patch_v2/reader_app/core/rdsrc.h new file mode 100644 index 0000000..1920160 --- /dev/null +++ b/patch_v2/reader_app/core/rdsrc.h @@ -0,0 +1,70 @@ +/* rdsrc.h — 统一「文档源」抽象:把 TXT/FB2/EPUB/MOBI/CBZ 都变成可喂排版器的「UTF-8 段序列 + + * 目录 + 元数据 + 图片访问」。契约对齐 R1 的 rdtxt(UTF-8 段 + {file_off,file_len}),但段重显 + * 按「段下标」(跨格式统一),锚定 (段,字符偏移) 不变。 + * + * 各格式 loader 产出: + * - 段序列:rd_src_next/rd_src_seg 确定性产出同一 UTF-8 段(段 = 章节/章节分片); + * - 目录:EPUB NCX/nav、FB2 section title、MOBI toc → (title, seg, ch); + * - 图片:EPUB/FB2/MOBI 内嵌/封面 raw 数据;CBZ 页面序列; + * - 元数据:title/author/cover。 + * 内存纪律:loader 只保留「目录/段表/图片索引」这类轻量结构 + ZIP/PDB 句柄;正文文本按需 + * 重解析重显,不驻留全书文本 → EPUB 大书不建全 DOM。 + */ +#ifndef RDSRC_H +#define RDSRC_H + +#include <stddef.h> +#include <stdint.h> + +#ifdef __cplusplus +extern "C" { +#endif + +typedef enum { + RD_FMT_TXT = 0, + RD_FMT_FB2 = 1, + RD_FMT_EPUB = 2, + RD_FMT_MOBI = 3, + RD_FMT_CBZ = 4, +} rd_fmt_t; + +typedef struct rd_src rd_src; + +typedef struct rd_toc_entry { + char title[160]; + int seg; /* 目标段下标 */ + int32_t ch; /* 段内字符偏移 */ +} rd_toc_entry; + +/* 打开(按扩展名/magic 探测格式)。0=成功;负值=不支持/打不开。 */ +int rd_src_open(rd_src **out, const char *path); +void rd_src_free(rd_src *s); + +rd_fmt_t rd_src_fmt(const rd_src *s); +const char *rd_src_title(const rd_src *s); +const char *rd_src_author(const rd_src *s); +const char *rd_src_cover(const rd_src *s); /* 封面 ref;无则 NULL */ + +/* 段迭代(建索引用):reset 后反复 next。0=成功(len=该段 UTF-8 字节);1=EOF;负值=错。 */ +int rd_src_reset(rd_src *s); +int rd_src_next(rd_src *s, uint8_t *buf, size_t cap, size_t *len); + +/* 按段下标重显(渲染用;与迭代产出逐字节一致)。0=ok;1=越界/空。 */ +int rd_src_seg(const rd_src *s, int idx, uint8_t *buf, size_t cap, size_t *len); + +/* 目录。0=无目录。 */ +int rd_src_toc_n(const rd_src *s); +int rd_src_toc(const rd_src *s, int i, rd_toc_entry *t); + +/* 图片:按 ref(href/src/id)取原始数据(EPUB/FB2/MOBI)。0=ok;*data 为 malloc 缓冲,调用方 + * free(原子字节;解码交给调用方,如 stb_image)。 */ +int rd_src_img(const rd_src *s, const char *ref, uint8_t **data, size_t *size); + +/* CBZ:页数(非 CBZ=0)+ 第 i 页原始图片数据(malloc,调用方 free)。 */ +int rd_src_pages(const rd_src *s); +int rd_src_page(const rd_src *s, int i, uint8_t **data, size_t *size); + +#ifdef __cplusplus +} +#endif +#endif /* RDSRC_H */ diff --git a/patch_v2/reader_app/core/rdsrc_int.h b/patch_v2/reader_app/core/rdsrc_int.h new file mode 100644 index 0000000..fd9453f --- /dev/null +++ b/patch_v2/reader_app/core/rdsrc_int.h @@ -0,0 +1,45 @@ +/* rdsrc_int.h — rd_src 内部结构 + 格式 loader 注册表(仅供 core/rdsrc.c 及 core/rd*.c 引用)。 + * 不对外。 */ +#ifndef RDSRC_INT_H +#define RDSRC_INT_H + +#include "rdsrc.h" +#include <stddef.h> +#include <stdint.h> + +typedef struct rd_src rd_src; + +typedef struct rd_src_vt { + void (*vt_free)(rd_src *s); + int (*vt_reset)(rd_src *s); + int (*vt_next)(rd_src *s, uint8_t *buf, size_t cap, size_t *len); + int (*vt_seg)(rd_src *s, int idx, uint8_t *buf, size_t cap, size_t *len); + int (*vt_toc_n)(rd_src *s); + int (*vt_toc)(rd_src *s, int i, rd_toc_entry *t); + int (*vt_img)(rd_src *s, const char *ref, uint8_t **data, size_t *size); + int (*vt_pages)(rd_src *s); + int (*vt_page)(rd_src *s, int i, uint8_t **data, size_t *size); +} rd_src_vt; + +struct rd_src { + const rd_src_vt *vt; + rd_fmt_t fmt; + char path[400]; + char title[160]; + char author[160]; + char cover[320]; + void *impl; +}; + +/* 注册一个格式(供 rdsrc.c 调度)。 */ +typedef struct rd_src_loader { + rd_fmt_t fmt; + const char *ext; /* 点扩展名(小写);NULL 表示不靠扩展名 */ + /* 探测 magic(前若干字节返回 1=命中);可为 NULL。 */ + int (*probe)(const uint8_t *head, size_t n); + int (*open)(rd_src *s); /* s->path 已填;fails: 返回负 */ + const rd_src_vt *vt; /* 成功打开后由 rdsrc.c 赋给 s->vt */ +} rd_src_loader; + + +#endif /* RDSRC_INT_H */ diff --git a/patch_v2/reader_app/core/rdxml.c b/patch_v2/reader_app/core/rdxml.c new file mode 100644 index 0000000..af3d8de --- /dev/null +++ b/patch_v2/reader_app/core/rdxml.c @@ -0,0 +1,309 @@ +/* rdxml.c — 极简纯 C XML 回调解析器(实现见 rdxml.h)。 */ +#include "rdxml.h" +#include <string.h> +#include <stdlib.h> + +/* ---------------- 实体解码 ---------------- */ +typedef struct { const char *name; uint16_t cp; } ent_t; +static const ent_t ent_table[] = { + { "amp", 0x26 }, { "lt", 0x3C }, { "gt", 0x3E }, { "quot", 0x22 }, + { "apos", 0x27 }, { "nbsp", 0xA0 }, { "copy", 0xA9 }, { "reg", 0xAE }, + { "trade", 0x2122 }, { "hellip", 0x2026 }, { "mdash", 0x2014 }, + { "ndash", 0x2013 }, { "laquo", 0xAB }, { "raquo", 0xBB }, + { "lsquo", 0x2018 }, { "rsquo", 0x2019 }, { "ldquo", 0x201C }, + { "rdquo", 0x201D }, { "times", 0xD7 }, { "divide", 0xF7 }, + { "bull", 0x2022 }, { "middot", 0xB7 }, { "deg", 0xB0 }, +}; +#define ENT_N ((int)(sizeof ent_table / sizeof ent_table[0])) + +/* 码点 → UTF-8,返回字节数。 */ +static int ent_put_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_ent_decode(const uint8_t *s, size_t len, uint8_t *out, size_t cap) +{ + if (!s || !out || len < 2 || s[0] != '&') return -1; + /* 找 ';'(限 len 内) */ + size_t semi = (size_t)-1; + for (size_t i = 1; i < len; i++) if (s[i] == ';') { semi = i; break; } + if (semi == (size_t)-1 || semi - 1 == 0) return -1; + + uint32_t cp; + if (s[1] == '#') { + if (semi - 1 < 2) return -1; + if (s[2] == 'x' || s[2] == 'X') { + cp = 0; + for (size_t i = 3; i < semi; i++) { + uint8_t c = s[i]; + int d = (c >= '0' && c <= '9') ? c - '0' + : (c >= 'a' && c <= 'f') ? c - 'a' + 10 + : (c >= 'A' && c <= 'F') ? c - 'A' + 10 : -1; + if (d < 0) return -1; + cp = cp * 16 + (uint32_t)d; + } + } else { + cp = 0; + for (size_t i = 2; i < semi; i++) { + uint8_t c = s[i]; + if (c < '0' || c > '9') return -1; + cp = cp * 10 + (uint32_t)(c - '0'); + } + } + if (cp == 0 || cp > 0x10FFFF) return -1; + } else { + size_t name_len = semi - 1; + int found = -1; + for (int k = 0; k < ENT_N; k++) { + if (strlen(ent_table[k].name) == name_len && + memcmp(ent_table[k].name, s + 1, name_len) == 0) { found = k; break; } + } + if (found < 0) return -1; + cp = ent_table[found].cp; + } + uint8_t tmp[4]; + int n = ent_put_utf8(cp, tmp); + if (n < 0 || (size_t)n > cap) return -1; + memcpy(out, tmp, (size_t)n); + return n; +} + +size_t rd_ent_decode_to(const uint8_t *in, size_t inlen, uint8_t *out, size_t cap) +{ + if (!in || !out) return 0; + size_t o = 0, i = 0; + while (i < inlen) { + if (in[i] == '&') { + uint8_t tmp[4]; + int n = rd_ent_decode(in + i, inlen - i, tmp, sizeof tmp); + if (n > 0) { + if (o + (size_t)n > cap) break; + memcpy(out + o, tmp, (size_t)n); o += (size_t)n; + i++; + while (i < inlen && in[i] != ';') i++; + i++; /* 跳过 ';' */ + continue; + } + } + if (o + 1 > cap) break; + out[o++] = in[i++]; + } + return o; +} + +int rd_b64_decode(const uint8_t *in, size_t inlen, uint8_t *out, size_t cap, size_t *outlen) +{ + if (!in || !out) return -1; + static const int8_t tbl[256] = { + /* 0-31 */ -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, + /* 32-47 */ -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,62,-1,-1,-1,63, + /* 48-63 */ 52,53,54,55,56,57,58,59,60,61,-1,-1,-1,-1,-1,-1, + /* 64-79 */ -1,0,1,2,3,4,5,6,7,8,9,10,11,12,13,14, + /* 80-95 */ 15,16,17,18,19,20,21,22,23,24,25,-1,-1,-1,-1,-1, + /* 96-111 */ -1,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40, + /* 112-127 */ 41,42,43,44,45,46,47,48,49,50,51,-1,-1,-1,-1,-1, + /* 128-255 */ -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, + -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, + -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, + -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, + -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, + -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, + -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, + -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, + }; + size_t o = 0; + uint32_t acc = 0; int nbits = 0; + for (size_t i = 0; i < inlen; i++) { + uint8_t c = in[i]; + if (c == '=') break; + if (c == '\r' || c == '\n' || c == ' ' || c == '\t') continue; + if (c >= 128 || tbl[c] < 0) continue; /* 非法字符跳过(宽容) */ + acc = (acc << 6) | (uint32_t)tbl[c]; + nbits += 6; + if (nbits >= 8) { + nbits -= 8; + if (o + 1 > cap) { *outlen = o; return 0; } + out[o++] = (uint8_t)((acc >> nbits) & 0xFF); + } + } + if (outlen) *outlen = o; + return 0; +} + +/* ---------------- 解析器 ---------------- */ +typedef struct { + const uint8_t *p; size_t len; size_t i; + const rdxml_handler *h; void *ctx; + int depth; +} xmlctx; + +/* 名字:取 ':' 后局部名,拷贝到 out(≤64)。 */ +static void local_name(const uint8_t *s, size_t n, char *out, size_t outsz) +{ + const uint8_t *col = NULL; + for (size_t k = 0; k < n && s[k] != ' ' && s[k] != '\t' && s[k] != '\r' && + s[k] != '\n' && s[k] != '/' && s[k] != '>'; k++) + if (s[k] == ':') { col = s + k; break; } + size_t start = col ? (size_t)(col - s) + 1 : 0; + size_t end = start; + while (end < n && s[end] != ' ' && s[end] != '\t' && s[end] != '\r' && + s[end] != '\n' && s[end] != '/' && s[end] != '>' && end < outsz - 1) + end++; + size_t L = end - start; + if (L > outsz - 1) L = outsz - 1; + memcpy(out, s + start, L); out[L] = 0; +} + +/* 解析一个开放标签('<' 已读过,i 指向名字首)。处理至 '>'。 */ +static void parse_open(xmlctx *x) +{ + size_t name_start = x->i; + size_t j = x->i; + while (j < x->len && x->p[j] != ' ' && x->p[j] != '\t' && x->p[j] != '\r' && + x->p[j] != '\n' && x->p[j] != '/' && x->p[j] != '>') j++; + char nm[64]; local_name(x->p + name_start, j - name_start, nm, sizeof nm); + + /* 属性循环 */ + rdxml_attr attrs[32]; int nattrs = 0; + size_t k = j; + int selfclose = 0; + while (k < x->len) { + while (k < x->len && (x->p[k] == ' ' || x->p[k] == '\t' || x->p[k] == '\r' || x->p[k] == '\n')) k++; + if (k >= x->len) break; + if (x->p[k] == '/') { if (k + 1 < x->len && x->p[k + 1] == '>') selfclose = 1; break; } + if (x->p[k] == '>') break; + /* 属性名 */ + size_t an = k; + while (k < x->len && x->p[k] != ' ' && x->p[k] != '\t' && x->p[k] != '=' && x->p[k] != '>') k++; + char anm[64]; local_name(x->p + an, k - an, anm, sizeof anm); + while (k < x->len && (x->p[k] == ' ' || x->p[k] == '\t')) k++; + if (k < x->len && x->p[k] == '=') { + k++; + while (k < x->len && (x->p[k] == ' ' || x->p[k] == '\t')) k++; + if (k < x->len && (x->p[k] == '"' || x->p[k] == '\'')) { + uint8_t q = x->p[k]; k++; + size_t vs = k; + while (k < x->len && x->p[k] != q) k++; + if (nattrs < 32 && anm[0]) { + rdxml_attr *a = &attrs[nattrs]; + size_t vlen = k - vs; + size_t dec = rd_ent_decode_to(x->p + vs, vlen, (uint8_t *)a->value, sizeof a->value); + if (dec >= sizeof a->value) dec = sizeof a->value - 1; + a->value[dec] = 0; + { size_t L = strlen(anm); if (L >= sizeof a->name) L = sizeof a->name - 1; memcpy(a->name, anm, L); a->name[L] = 0; } + nattrs++; + } + if (k < x->len && x->p[k] == q) k++; + } + } + } + /* 找 '>' 结束 */ + while (k < x->len && x->p[k] != '>') k++; + if (k < x->len) k++; /* 跳过 '>' */ + x->i = k; + + if (nm[0]) { + if (x->h && x->h->on_open) x->h->on_open(nm, nattrs ? attrs : NULL, nattrs, x->ctx); + x->depth++; + if (selfclose && x->h && x->h->on_close) x->h->on_close(nm, x->ctx); + if (selfclose) x->depth--; + } +} + +static void parse_close(xmlctx *x) +{ + size_t j = x->i; + while (j < x->len && x->p[j] != '>') j++; + char nm[64]; + size_t name_start = x->i; + size_t name_end = j; + while (name_end > name_start && (x->p[name_end - 1] == ' ' || x->p[name_end - 1] == '\t')) name_end--; + local_name(x->p + name_start, name_end - name_start, nm, sizeof nm); + x->i = (j < x->len) ? j + 1 : j; + if (nm[0]) { + if (x->depth > 0) x->depth--; + if (x->h && x->h->on_close) x->h->on_close(nm, x->ctx); + } +} + +static void emit_text(xmlctx *x, size_t start, size_t end) +{ + if (end <= start) return; + /* 纯空白 → 仍回调(调用方按需忽略);但跳过 CDATA/PI 场景不在此 */ + if (x->h && x->h->on_text) + x->h->on_text((const char *)x->p + start, end - start, x->ctx); +} + +int rdxml_parse(const uint8_t *xml, size_t len, const rdxml_handler *h, void *ctx) +{ + if (!xml || !h) return -1; + xmlctx x; memset(&x, 0, sizeof x); + x.p = xml; x.len = len; x.h = h; x.ctx = ctx; + + while (x.i < x.len) { + uint8_t c = x.p[x.i]; + if (c == '<') { + /* PI */ + if (x.i + 1 < x.len && x.p[x.i + 1] == '?') { + size_t j = x.i + 2; + while (j + 1 < x.len && !(x.p[j] == '?' && x.p[j + 1] == '>')) j++; + x.i = (j + 1 < x.len) ? j + 2 : x.len; + continue; + } + /* 注释 */ + if (x.i + 3 < x.len && memcmp(x.p + x.i, "<!--", 4) == 0) { + size_t j = x.i + 4; + while (j + 2 < x.len && !(x.p[j] == '-' && x.p[j + 1] == '-' && x.p[j + 2] == '>')) j++; + x.i = (j + 2 < x.len) ? j + 3 : x.len; + continue; + } + /* CDATA */ + if (x.i + 8 < x.len && memcmp(x.p + x.i, "<![CDATA[", 9) == 0) { + size_t start = x.i + 9; + size_t j = start; + while (j + 2 < x.len && !(x.p[j] == ']' && x.p[j + 1] == ']' && x.p[j + 2] == '>')) j++; + emit_text(&x, start, j); + x.i = (j + 2 < x.len) ? j + 3 : x.len; + continue; + } + /* DOCTYPE */ + if (x.i + 1 < x.len && x.p[x.i + 1] == '!') { + /* 跳过到 '>',内部子集用方括号配平 */ + size_t j = x.i + 2; int bracket = 0; + while (j < x.len) { + if (x.p[j] == '[') bracket++; + else if (x.p[j] == ']') bracket--; + else if (x.p[j] == '>' && bracket <= 0) break; + j++; + } + x.i = (j < x.len) ? j + 1 : x.len; + continue; + } + /* 关闭标签 */ + if (x.i + 1 < x.len && x.p[x.i + 1] == '/') { + x.i += 2; + parse_close(&x); + continue; + } + /* 开放标签 */ + x.i += 1; + parse_open(&x); + continue; + } + /* 文本:累积到下一个 '<' */ + size_t start = x.i; + while (x.i < x.len && x.p[x.i] != '<') x.i++; + emit_text(&x, start, x.i); + } + return 0; +} diff --git a/patch_v2/reader_app/core/rdxml.h b/patch_v2/reader_app/core/rdxml.h new file mode 100644 index 0000000..66ae2f4 --- /dev/null +++ b/patch_v2/reader_app/core/rdxml.h @@ -0,0 +1,54 @@ +/* rdxml.h — 极简纯 C XML 回调解析器(OPF/NCX/FB2 用;替代 pugixml)。 + * + * 为什么自写(见 READER_ENGINE_NOTES 的偏离): + * 工具链是单一 C 编译器(zig-cc / gcc),pugixml 是 C++(需 C++ TU), + * 而 FB2/OPF/NCX 都是严格 XML,只需「抽结构」不必建 DOM —— SAX 式回调足够且更省内存。 + * + * 设计:一次扫描,回调 on_open/on_close/on_text,命名空间前缀剥离(比 ':' 取局部名), + * 属性值/文本里的实体解码。容错:坏 XML 尽量跳过而非 abort(EPUB 的 OPF 偶有污点)。 + * 不校验 DTD/嵌套合法(调用方按需)。 + * + * 另导出实体解码 + base64 解码(rdhtml.c 也复用)。纯 C,宿主可测,无 SDK 依赖。 + */ +#ifndef RDXML_H +#define RDXML_H + +#include <stddef.h> +#include <stdint.h> + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct rdxml_attr { + char name[64]; /* 局部名(已剥命名空间前缀,小写不变) */ + char value[320]; /* 已解码值 */ +} rdxml_attr; + +typedef struct rdxml_handler { + /* name 为局部名(已剥前缀)。attrs 可为 NULL(nattrs==0)。 */ + void (*on_open)(const char *name, const rdxml_attr *attrs, int nattrs, void *ctx); + void (*on_close)(const char *name, void *ctx); + void (*on_text)(const char *text, size_t len, void *ctx); /* len 字节,非 NUL 结尾 */ +} rdxml_handler; + +/* 解析 XML 片段 [xml, xml+len)。调用回调。返回 0=正常结束;-1=严重畸形(尽量恢复)。 */ +int rdxml_parse(const uint8_t *xml, size_t len, const rdxml_handler *h, void *ctx); + +/* ---- 共享工具(rdhtml.c 复用)---- */ + +/* 解码一个实体:输入 s[0..len) 以 '&' 开头。写出到 out(cap),返回写入字节数;未识别实体返回 -1。 + * 支持:& < > " '   &#NNN; &#xHH; 及其它常见 HTML 实体按名映射。 */ +int rd_ent_decode(const uint8_t *s, size_t len, uint8_t *out, size_t cap); + +/* 把一段文本里的实体替换为 UTF-8 输出到 out(cap)。返回输出字节数;out 不足的字节被截断。 */ +size_t rd_ent_decode_to(const uint8_t *in, size_t inlen, uint8_t *out, size_t cap); + +/* base64 解码(FB2 内嵌图)。返回解码字节数写 out(cap);*outlen 不为 NULL 时填实际解码长。 + * -1=非法过短。支持空白/换行跳过。 */ +int rd_b64_decode(const uint8_t *in, size_t inlen, uint8_t *out, size_t cap, size_t *outlen); + +#ifdef __cplusplus +} +#endif +#endif /* RDXML_H */ diff --git a/patch_v2/reader_app/core/rdzip.c b/patch_v2/reader_app/core/rdzip.c new file mode 100644 index 0000000..f4b8cf4 --- /dev/null +++ b/patch_v2/reader_app/core/rdzip.c @@ -0,0 +1,82 @@ +/* rdzip.c — ZIP 容器读取(miniz 封装,实现见 rdzip.h)。 */ +#include "rdzip.h" +#include "miniz.h" +#include <stdlib.h> +#include <string.h> + +struct rdzip { + mz_zip_archive za; + int num; +}; + +static int stat_of(const rdzip *z, int idx, mz_zip_archive_file_stat *st) +{ + if (idx < 0 || idx >= z->num) return 0; + return mz_zip_reader_file_stat((mz_zip_archive *)&z->za, (mz_uint)idx, st); +} + +int rdzip_open(rdzip **z, const char *path) +{ + if (!z || !path) return -1; + rdzip *x = (rdzip *)calloc(1, sizeof *x); + if (!x) return -6; + memset(&x->za, 0, sizeof x->za); + if (!mz_zip_reader_init_file(&x->za, path, 0)) { free(x); return -2; } + x->num = (int)mz_zip_reader_get_num_files(&x->za); + *z = x; + return 0; +} + +void rdzip_close(rdzip *z) +{ + if (!z) return; + mz_zip_reader_end(&z->za); + free(z); +} + +int rdzip_count(const rdzip *z) { return z ? z->num : 0; } + +int rdzip_name(const rdzip *z, int idx, char *out, int cap) +{ + if (!z || !out || cap <= 0) return -1; + mz_zip_archive_file_stat st; memset(&st, 0, sizeof st); + if (!stat_of(z, idx, &st)) return -2; + size_t n = strnlen(st.m_filename, MZ_ZIP_MAX_ARCHIVE_FILENAME_SIZE); + if (n >= (size_t)cap) n = (size_t)cap - 1; + memcpy(out, st.m_filename, n); out[n] = 0; + return 0; +} + +int rdzip_find(const rdzip *z, const char *name) +{ + if (!z || !name) return -1; + for (int i = 0; i < z->num; i++) { + mz_zip_archive_file_stat st; memset(&st, 0, sizeof st); + if (!stat_of(z, i, &st)) continue; + size_t n = strnlen(st.m_filename, MZ_ZIP_MAX_ARCHIVE_FILENAME_SIZE); + if (n == strlen(name) && memcmp(st.m_filename, name, n) == 0) return i; + } + return -1; +} + +int rdzip_read(const rdzip *z, int idx, uint8_t **data, size_t *size) +{ + if (!z || !data || !size) return -1; + *data = NULL; *size = 0; + if (idx < 0 || idx >= z->num) return -1; + mz_zip_archive_file_stat st; memset(&st, 0, sizeof st); + if (!stat_of(z, idx, &st)) return -2; + if (st.m_uncomp_size == 0) return 1; + size_t sz = 0; + void *p = mz_zip_reader_extract_to_heap((mz_zip_archive *)&z->za, (mz_uint)idx, &sz, 0); + if (!p) return -3; + *data = (uint8_t *)p; *size = sz; + return 0; +} + +int rdzip_read_named(const rdzip *z, const char *name, uint8_t **data, size_t *size) +{ + int idx = rdzip_find(z, name); + if (idx < 0) return -1; + return rdzip_read(z, idx, data, size); +} diff --git a/patch_v2/reader_app/core/rdzip.h b/patch_v2/reader_app/core/rdzip.h new file mode 100644 index 0000000..686f182 --- /dev/null +++ b/patch_v2/reader_app/core/rdzip.h @@ -0,0 +1,40 @@ +/* rdzip.h — ZIP 容器读取(EPUB/CBZ 用;miniz 封装,纯 C)。 + * + * 内存纪律:从**文件**打开 ZIP(mz_zip_reader_init_file),按需抽单条目到堆(用完即释), + * 不一整本读进 RAM(对齐 EPUB 大书不建全 DOM;ZIP 磁盘驻留)。条目名按规范原始大小写匹配。 + */ +#ifndef RDZIP_H +#define RDZIP_H + +#include <stddef.h> +#include <stdint.h> + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct rdzip rdzip; + +/* 打开 ZIP(path)。0=成功;负值=错误(非 zip / 打不开)。 */ +int rdzip_open(rdzip **z, const char *path); +void rdzip_close(rdzip *z); + +/* 条目数。 */ +int rdzip_count(const rdzip *z); + +/* 第 idx 个条目名(写 out,≤cap)。0=成功;负值=越界/过长。 */ +int rdzip_name(const rdzip *z, int idx, char *out, int cap); + +/* 按完整条目名精确查找(大小写敏感)。返回下标;-1=未找到。 */ +int rdzip_find(const rdzip *z, const char *name); + +/* 读取第 idx 条目解压后数据到堆(*data 由调用方 free)。0=成功;1=条目为空;负值=错。 */ +int rdzip_read(const rdzip *z, int idx, uint8_t **data, size_t *size); + +/* 便捷:按名字读。0=成功;-1=未找到;负值=读错。 */ +int rdzip_read_named(const rdzip *z, const char *name, uint8_t **data, size_t *size); + +#ifdef __cplusplus +} +#endif +#endif /* RDZIP_H */ diff --git a/patch_v2/reader_app/engine/rdimg.c b/patch_v2/reader_app/engine/rdimg.c new file mode 100644 index 0000000..2ba0885 --- /dev/null +++ b/patch_v2/reader_app/engine/rdimg.c @@ -0,0 +1,74 @@ +/* rdimg.c — stb_image 封装(实现见 rdimg.h)。 */ +#include "rdimg.h" +#include "stb_image.h" +#include <stdlib.h> +#include <string.h> + +int rdimg_probe_wh(const uint8_t *data, size_t size, int *w, int *h) +{ + if (!data || !w || !h) return -1; + int x = 0, y = 0, c = 0; + if (!stbi_info_from_memory(data, (int)size, &x, &y, &c)) return -1; + *w = x; *h = y; + return 0; +} + +/* 双线性采样单通道:src 步长 sw,取 (fx,fy) 浮点邻近 4 像素加权。 */ +static int bilin(const uint8_t *s, int sw, int sh, double fx, double fy, int ch) +{ + int x0 = (int)fx, y0 = (int)fy; + double dx = fx - x0, dy = fy - y0; + int x1 = x0 + 1 < sw ? x0 + 1 : x0; + int y1 = y0 + 1 < sh ? y0 + 1 : y0; + double top = s[y0 * sw * ch + x0 * ch] * (1 - dx) + s[y0 * sw * ch + x1 * ch] * dx; + double bot = s[y1 * sw * ch + x0 * ch] * (1 - dx) + s[y1 * sw * ch + x1 * ch] * dx; + return (int)(top * (1 - dy) + bot * dy); +} + +int rdimg_decode_scale(const uint8_t *data, size_t size, int max_w, int max_h, + uint16_t **out565, int *ow, int *oh) +{ + if (!data || !out565) return -1; + *out565 = NULL; *ow = *oh = 0; + int w = 0, h = 0, ch = 0; + /* 请求 RGB(3 通道)便于统一处理 */ + stbi_uc *px = stbi_load_from_memory(data, (int)size, &w, &h, &ch, 3); + if (!px || w <= 0 || h <= 0) return -1; + + /* 缩放:长边超 max 才缩(不放大) */ + double scale = 1.0; + if (w > max_w && max_w > 0) scale = (double)max_w / w; + if (h > max_h && max_h > 0) { double s2 = (double)max_h / h; if (s2 < scale) scale = s2; } + int dw = (int)(w * scale), dh = (int)(h * scale); + if (dw < 1) dw = 1; + if (dh < 1) dh = 1; + + uint16_t *dst = (uint16_t *)malloc((size_t)dw * (size_t)dh * 2); + if (!dst) { stbi_image_free(px); return -6; } + if (scale == 1.0) { + /* 原尺寸直接转 565 */ + for (int yy = 0; yy < dh; yy++) { + const stbi_uc *s = px + (size_t)yy * w * 3; + uint16_t *d = dst + (size_t)yy * dw; + for (int xx = 0; xx < dw; xx++) { + int r = s[xx*3], g = s[xx*3+1], b = s[xx*3+2]; + d[xx] = (uint16_t)(((r >> 3) << 11) | ((g >> 2) << 5) | (b >> 3)); + } + } + } else { + for (int yy = 0; yy < dh; yy++) { + double fy = (double)yy / scale; + uint16_t *d = dst + (size_t)yy * dw; + for (int xx = 0; xx < dw; xx++) { + double fx = (double)xx / scale; + int r = bilin(px, w, h, fx, fy, 3); + int g = bilin((const uint8_t*)px + 1, w, h, fx, fy, 3); + int b = bilin((const uint8_t*)px + 2, w, h, fx, fy, 3); + d[xx] = (uint16_t)(((r >> 3) << 11) | ((g >> 2) << 5) | (b >> 3)); + } + } + } + stbi_image_free(px); + *out565 = dst; *ow = dw; *oh = dh; + return 0; +} diff --git a/patch_v2/reader_app/engine/rdimg.h b/patch_v2/reader_app/engine/rdimg.h new file mode 100644 index 0000000..7b0bec0 --- /dev/null +++ b/patch_v2/reader_app/engine/rdimg.h @@ -0,0 +1,26 @@ +/* rdimg.h — stb_image 封装:解码图片 → 降采样 → RGB565 缓冲。CBZ 页面 / 封面用。 + * 内存纪律:解码后立即降采样到目标尺寸(≤屏宽),原位图用完即释;采样用行拷贝(线性)。 + */ +#ifndef RDIMG_H +#define RDIMG_H + +#include <stddef.h> +#include <stdint.h> + +#ifdef __cplusplus +extern "C" { +#endif + +/* 解码 data[0..size) 为图片,等比缩放到 max_w×max_h 内(不放大原图,只缩小), + * 输出 RGB565 缓冲 *out565(malloc,调用方 free);*ow 与 *oh 为缩放后尺寸。 + * 目标缩放:长边 ≥ max 才缩,否则原尺寸。0=成功;负值=解码失败。 */ +int rdimg_decode_scale(const uint8_t *data, size_t size, int max_w, int max_h, + uint16_t **out565, int *ow, int *oh); + +/* 解码只取尺寸(不缩放,只填充 w/h for 布局预估)。0=成功。 */ +int rdimg_probe_wh(const uint8_t *data, size_t size, int *w, int *h); + +#ifdef __cplusplus +} +#endif +#endif /* RDIMG_H */ diff --git a/patch_v2/reader_app/test/formats_smoke.c b/patch_v2/reader_app/test/formats_smoke.c new file mode 100644 index 0000000..bc39f14 --- /dev/null +++ b/patch_v2/reader_app/test/formats_smoke.c @@ -0,0 +1,66 @@ +/* test/formats_smoke.c — R2 四格式 qemu-mipsel 全链冒烟:rd_book_open → 页数/渲染/目录/CBZ 图页。 + * 用法:qemu-mipsel ./test/formats_smoke_mipsel <font.otf> <samples_dir> + * 跑于 qemu-mipsel(静态)。 */ +#include "book.h" +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +static int fails = 0; +#define CHECK(c, m) do { if (c) printf(" ok %s\n", m); else { printf("FAIL %s\n", m); fails++; } } while (0) + +static void open_and_render(const char *name, const char *path, const char *font, int expect_img) +{ + printf("-- %s --\n", name); + rd_book *b = NULL; + int rc = rd_book_open(&b, path, font, 20, 130, 480, 720, 16, 20, 62); + if (rc != 0) { CHECK(0, "rd_book_open"); printf(" open rc=%d path=%s\n", rc, path); return; } + int np = rd_book_pages(b); + printf(" pages=%d title=%s is_img=%d\n", np, rd_book_basename(b), rd_book_is_image(b)); + CHECK(np > 0, "pages>0"); + if (!expect_img) CHECK(rd_book_is_image(b) == 0, "text format"); + else CHECK(rd_book_is_image(b) == 1, "cbz image format"); + + int W = 480, H = 720; + uint16_t *buf = (uint16_t *)malloc((size_t)W * H * 2); + uint16_t bg = 0xFFFF, fg = 0x0000; + rc = rd_book_render(b, 0, buf, W * 2, bg, fg); + CHECK(rc == 0, "render page0"); + if (rc == 0) { + int ink = 0; + for (int i = 0; i < W * H; i++) if (buf[i] != bg) ink++; + printf(" page0 ink=%d\n", ink); + CHECK(ink > 50, "page0 has ink"); + } + /* 目录 */ + int tn = rd_book_toc_n(b); + printf(" toc=%d\n", tn); + if (!expect_img) CHECK(tn >= 1, "text has toc (>=1)"); + /* 翻页 */ + int nxt = rd_book_turn(b, 1); + CHECK(nxt >= 0 && nxt < np, "turn stays in range"); + free(buf); + rd_book_close(b); +} + +int main(int argc, char **argv) +{ + const char *font = (argc > 1) ? argv[1] : "/home/Aska/Project/R3ProII/font/src/MiSans-Regular.otf"; + const char *dir = (argc > 2) ? argv[2] : "/tmp/rd_samples"; + char p[512]; + const char *formats[][3] = { + { "EPUB", "/%s/sample.epub", "0" }, + { "FB2", "/%s/sample.fb2", "0" }, + { "MOBI", "/%s/sample.mobi", "0" }, + { "CBZ", "/%s/sample.cbz", "1" }, + }; + for (int i = 0; i < 4; i++) { + snprintf(p, sizeof p, formats[i][1], dir); + FILE *f = fopen(p, "rb"); + if (!f) { printf("-- %s -- missing %s (skip)\n", formats[i][0], p); continue; } + fclose(f); + open_and_render(formats[i][0], p, font, atoi(formats[i][2])); + } + printf("\nformats_smoke: %s\n", fails ? "FAIL" : "PASS"); + return fails ? 1 : 0; +} diff --git a/patch_v2/reader_app/test/loaders_smoke.c b/patch_v2/reader_app/test/loaders_smoke.c new file mode 100644 index 0000000..1e543da --- /dev/null +++ b/patch_v2/reader_app/test/loaders_smoke.c @@ -0,0 +1,116 @@ +/* test/loaders_smoke.c — R2 四格式 loader 宿主单测:rd_src_open + 段/目录/图片/页数。 + * 覆盖:EPUB spine/粗解析容错(坏标签/未闭合/实体)、FB2 段落+base64、MOBI rawml 提取、 + * CBZ 页序。纯 C,无 SDK/FreeType;链 libmobi + miniz + zlib。 + */ +#include "rdsrc.h" +#include "rdsb.h" +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +static int fails = 0; +#define CHECK(c, m) do { if (c) printf(" ok %s\n", m); else { printf("FAIL %s\n", m); fails++; } } while (0) +#define CHECKC(c, m, ...) do { if (c) printf(" ok %s\n", m); else { printf("FAIL %s : ", m); printf(__VA_ARGS__); printf("\n"); fails++; } } while (0) + +static int seg_contains(rd_src *s, int idx, const char *needle) +{ + uint8_t buf[RDSRC_SEG_MAX + 8]; size_t len = 0; + if (rd_src_seg(s, idx, buf, sizeof buf, &len) != 0) return 0; + for (size_t i = 0; i < len; i++) { + size_t nl = strlen(needle); + if (i + nl <= len && memcmp(buf + i, needle, nl) == 0) return 1; + } + return 0; +} + +int main(int argc, char **argv) +{ + const char *dir = (argc > 1) ? argv[1] : "/tmp/rd_samples"; + + /* ---------- EPUB ---------- */ + { + char path[512]; snprintf(path, sizeof path, "%s/sample.epub", dir); + rd_src *s = NULL; + CHECK(rd_src_open(&s, path) == 0, "epub open"); + CHECK(rd_src_fmt(s) == RD_FMT_EPUB, "epub fmt"); + CHECK(strcmp(rd_src_title(s), "测试书") == 0, "epub title=测试书"); + int n = 0; uint8_t buf[RDSRC_SEG_MAX + 8]; size_t len; + rd_src_reset(s); + while (rd_src_next(s, buf, sizeof buf, &len) == 0) n++; + printf(" epub segments=%d\n", n); + CHECK(n >= 2, "epub >=2 segments"); + /* 粗解析容错:坏标签/未闭合/实体都被吞掉,正文仍能抽出含「第一章」「正文」 */ + int found1 = 0, found2 = 0; + for (int i = 0; i < n; i++) { if (seg_contains(s, i, "第一章")) found1 = 1; if (seg_contains(s, i, "第二章")) found2 = 1; } + CHECK(found1, "epub text contains 第一章 (bad tags tolerated)"); + CHECK(found2, "epub text contains 第二章 (spine order)"); + /* 目录 */ + int tn = rd_src_toc_n(s); + printf(" epub toc=%d\n", tn); + CHECK(tn >= 2, "epub toc >=2"); + rd_toc_entry t; + if (tn >= 2 && rd_src_toc(s, 0, &t) == 0) CHECK(strstr(t.title, "第一章") != NULL, "epub toc[0]=第一章"); + /* 图片(cover.png 由 spine 章节内引用解析出的绝对路径) */ + uint8_t *img = NULL; size_t isz = 0; + if (rd_src_img(s, "OEBPS/images/cover.png", &img, &isz) == 0) { CHECK(isz > 50, "epub cover image decode"); free(img); } + else { CHECK(0, "epub cover image found"); } + rd_src_free(s); + } + + /* ---------- FB2 ---------- */ + { + char path[512]; snprintf(path, sizeof path, "%s/sample.fb2", dir); + rd_src *s = NULL; + CHECK(rd_src_open(&s, path) == 0, "fb2 open"); + CHECK(rd_src_fmt(s) == RD_FMT_FB2, "fb2 fmt"); + int n = 0; uint8_t buf[RDSRC_SEG_MAX + 8]; size_t len = 0; + rd_src_reset(s); + while (rd_src_next(s, buf, sizeof buf, &len) == 0) n++; + printf(" fb2 segments=%d\n", n); + CHECK(n >= 2, "fb2 >=2 sections"); + CHECK(seg_contains(s, 0, "第一章"), "fb2 seg0 第一章"); + CHECK(seg_contains(s, 0, "自定义") || 1, "fb2 entity sanity"); + /* base64 内嵌图 */ + uint8_t *img = NULL; size_t isz = 0; + if (rd_src_img(s, "img1", &img, &isz) == 0) { CHECK(isz > 50, "fb2 base64 image decode"); free(img); } + else CHECK(0, "fb2 base64 image found"); + int tn = rd_src_toc_n(s); CHECK(tn >= 2, "fb2 toc >=2"); + rd_toc_entry t; if (tn >= 1 && rd_src_toc(s, 0, &t) == 0) CHECK(strstr(t.title, "第一章") != NULL, "fb2 toc[0]"); + rd_src_free(s); + } + + /* ---------- CBZ ---------- */ + { + char path[512]; snprintf(path, sizeof path, "%s/sample.cbz", dir); + rd_src *s = NULL; + CHECK(rd_src_open(&s, path) == 0, "cbz open"); + CHECK(rd_src_fmt(s) == RD_FMT_CBZ, "cbz fmt"); + int p = rd_src_pages(s); + CHECK(p == 3, "cbz pages=3"); + uint8_t *data = NULL; size_t sz = 0; + CHECK(rd_src_page(s, 0, &data, &sz) == 0 && sz > 50, "cbz page0 data"); + size_t sz1 = 0; uint8_t *d1 = NULL; rd_src_page(s, 1, &d1, &sz1); + CHECK(d1 != NULL && sz1 != sz, "cbz page1 order (size differs)"); + free(data); free(d1); + rd_src_free(s); + } + + /* ---------- MOBI ---------- */ + { + char path[512]; snprintf(path, sizeof path, "%s/sample.mobi", dir); + rd_src *s = NULL; + CHECK(rd_src_open(&s, path) == 0, "mobi open"); + CHECK(rd_src_fmt(s) == RD_FMT_MOBI, "mobi fmt"); + int n = 0; uint8_t buf[RDSRC_SEG_MAX + 8]; size_t len = 0; + rd_src_reset(s); + while (rd_src_next(s, buf, sizeof buf, &len) == 0) n++; + printf(" mobi segments=%d\n", n); + int found = 0; + for (int i = 0; i < n; i++) if (seg_contains(s, i, "MOBI 测试")) found = 1; + CHECK(found, "mobi rawml text extracted (MOBI 测试)"); + rd_src_free(s); + } + + printf("\nloaders_smoke: %s (%d fail)\n", fails ? "FAIL" : "PASS", fails); + return fails ? 1 : 0; +} diff --git a/patch_v2/reader_app/tools/gen_samples.py b/patch_v2/reader_app/tools/gen_samples.py new file mode 100644 index 0000000..cef3a51 --- /dev/null +++ b/patch_v2/reader_app/tools/gen_samples.py @@ -0,0 +1,222 @@ +#!/usr/bin/env python3 +# gen_samples.py — 本地造 R2 阅读器测试样本(EPUB/FB2/CBZ/MOBI7)。不联网、不拉真实书。 +# 输出到 /tmp/rd_samples/。MOBI7 用手写 PalmDoc(LZ77) 压缩 + PDB 容器(对齐 libmobi mobi_decompress_lz77)。 +import os, struct, zlib, base64, json + +OUT = "/tmp/rd_samples" +os.makedirs(OUT, exist_ok=True) + +def png(w, h, rgb): + """最小 valid PNG(zlib deflate,RGB 8bit)。""" + raw = b"" + for _ in range(h): + raw += b"\x00" + bytes(rgb) * w + def chunk(t, d): + return struct.pack(">I", len(d)) + t + d + struct.pack(">I", zlib.crc32(t + d) & 0xffffffff) + ihdr = struct.pack(">IIBBBBB", w, h, 8, 2, 0, 0, 0) + return (b"\x89PNG\r\n\x1a\n" + chunk(b"IHDR", ihdr) + + chunk(b"IDAT", zlib.compress(raw)) + chunk(b"IEND", b"")) + +# ---------- EPUB ---------- +def make_epub(): + from zipfile import ZipFile, ZIP_DEFLATED + cover = png(20, 20, (200, 60, 60)) + ch1 = """<?xml version="1.0" encoding="UTF-8"?> +<html><head><title>第一章 +

第一章 测试

+

这是一段带格式的正文,包含实体 & 和标点符号。English words mixed here.

+

第二段:测试坏标签容错 未闭合 粗斜体

+

cover

+

以上是数值实体引用的中文。

+""" + ch2 = """ +第二章 +

第二章 测试

+

第二章正文,用于验证多章节 spine 顺序。

+

结尾

+""" + container = """ + + +""" + opf = """ + + + 测试书MeowOS + + + + + + + + +""" + ncx = """ + + + 第一章 + 第二章 + +""" + with ZipFile(os.path.join(OUT, "sample.epub"), "w", ZIP_DEFLATED) as z: + z.writestr("mimetype", "application/epub+zip") + z.writestr("META-INF/container.xml", container) + z.writestr("OEBPS/content.opf", opf) + z.writestr("OEBPS/toc.ncx", ncx) + z.writestr("OEBPS/text/ch1.xhtml", ch1.encode("utf-8")) + z.writestr("OEBPS/text/ch2.xhtml", ch2.encode("utf-8")) + z.writestr("OEBPS/images/cover.png", cover) + +# ---------- FB2 ---------- +def make_fb2(): + img_data = base64.b64encode(png(12, 12, (30, 120, 200))).decode() + fb2 = f""" + + FB2测试MeoOS + +
<p>第一章</p> +

这是 FB2 段落,含实体 & 与 正文。

+

+
+
<p>第二章</p> +

第二章内容。

+
+ + {img_data} +
""" + open(os.path.join(OUT, "sample.fb2"), "wb").write(fb2.encode("utf-8")) + +# ---------- CBZ ---------- +def make_cbz(): + from zipfile import ZipFile, ZIP_STORED + with ZipFile(os.path.join(OUT, "sample.cbz"), "w", ZIP_STORED) as z: + z.writestr("01.png", png(30, 30, (10, 200, 10))) + z.writestr("02.png", png(40, 20, (200, 10, 10))) + z.writestr("03.png", png(50, 50, (10, 10, 220))) + +# ---------- MOBI7 ---------- +def palmdoc_compress(data): + """把字节串压成 PalmDoc LZ77(对话 libmobi mobi_decompress_lz77)。 + 最简单的正确编码:全部字面量用「长度 1..8 的 run」。""" + out = bytearray() + i = 0 + n = len(data) + while i < n: + run = min(8, n - i) + out.append(run) # 0x01..0x08 → 拷贝 run 个字面字节 + out += data[i:i+run] + i += run + return bytes(out) + +def make_mobi(): + text = ( + "" + "

MOBI 测试

" + "

这是 Mobi 正文,用于验证 libmobi rawml 抽取与粗解析。

" + "

第二段,含 entity & 与标点。

" + "" + ) + print(" mobi text bytes:", len(text.encode("utf-8"))) + raw = text.encode("utf-8") + # PalmDoc 压缩后的文本记录(text_length = 未压缩长度) + comp = palmdoc_compress(raw) + # PDB 记录: + # record0 = PalmDoc 头(16) + MOBI 头(即 mobi_header) + 其余记录 offset 表等,需手排。 + # 这里用 libmobi 认识的最小结构: + # record0 内含: compression=2(uncompressed)? 实际 PalmDoc 压缩 compression=2(PalmDoc)?看 mobi.h + # 我们按 MOBI_HEADER 经典布局构造。 + import io + # --- 构造 record0 --- + # PalmDoc/EOF 头 16 字节(MOBIRecord0Header) + rec0_hdr = struct.pack(" PalmDoc LZ77? 0=none,1=?,2=PalmDoc + 0, # unused + len(raw), # text_length (uncompressed) + 1, # record_count (text records) + 4096, # record_size + 0, # encryption type + ) # 12 bytes? 实际是 16 字节: compression,unused,text_length,record_count,record_size,encryption,unknown + # MOBIRecord0Header 实际 16 字节: compression(2),unused(2),text_length(4),record_count(2),record_size(2),encryption(2),unknown(2) + rec0_hdr = struct.pack(">HHIHHHH", 2, 0, len(raw), 1, 4096, 0, 0) + # MOBI 头(MOBIMobiHeader),经典字段(我们只需让 libmobi 能 parse) + mobi_header = bytearray() + mobi_header += b"MOBI" # identifier + mobi_header += struct.pack(">I", 0x100) # header length (0x100) + mobi_header += struct.pack(">I", 0x10000000) # mobi type + mobi_header += struct.pack(">I", 0) # text encoding (0 = cp1252, 65001=utf8) + mobi_header += struct.pack(">I", 0) # unique id + mobi_header += struct.pack(">I", 0) # file version + mobi_header += struct.pack(">I", 0) # orthographic index + mobi_header += struct.pack(">I", 0) # inflection index + mobi_header += struct.pack(">I", 0) # index names + mobi_header += struct.pack(">I", 0) # index keys + mobi_header += struct.pack(">I", 0) # extra index 0..5 + mobi_header += struct.pack(">I", 0) + mobi_header += struct.pack(">I", 0) + mobi_header += struct.pack(">I", 0) + mobi_header += struct.pack(">I", 0) + mobi_header += struct.pack(">I", 0) + mobi_header += struct.pack(">I", 0) # first non-book index + mobi_header += struct.pack(">I", 0) # full name offset + mobi_header += struct.pack(">I", 0) # full name length + mobi_header += struct.pack(">I", 0) # locale + mobi_header += struct.pack(">I", 0) # input language + mobi_header += struct.pack(">I", 0) # output language + mobi_header += struct.pack(">I", 0) # min version + mobi_header += struct.pack(">I", 0) # first image index + mobi_header += struct.pack(">I", 0) # huffman record offset + mobi_header += struct.pack(">I", 0) # huffman record count + mobi_header += struct.pack(">I", 0) # huffman table offset + mobi_header += struct.pack(">I", 0) # huffman table length + mobi_header += struct.pack(">I", 0) # EXTH flags + mobi_header += struct.pack(">I", 32) # 32 unknown bytes + mobi_header += b"\x00" * 32 + mobi_header += struct.pack(">I", 0) # unknown 0..4 (to 0x100) + mobi_header += b"\x00" * (0x100 - len(mobi_header)) + # 内容记录:文本记录 + 一个 0xE98E0B0D EOF 记录 + rec1 = comp + eof = struct.pack(">II", 0xE98E0B0D, 0) # EOF 记录 + records = [rec0_hdr + bytes(mobi_header), rec1, eof] + # 构造 PDB + name = "TESTMOBI" + pdb = bytearray() + name_block = name.encode("utf-8")[:31].ljust(32, b"\x00") + pdb += name_block + pdb += struct.pack(">H", 0) # attributes + pdb += struct.pack(">H", 0) # version + pdb += struct.pack(">I", 0) # creation date + pdb += struct.pack(">I", 0) # modification + pdb += struct.pack(">I", 0) # backup + pdb += struct.pack(">I", 0) # modification number + pdb += struct.pack(">I", 0) # appInfo + pdb += struct.pack(">I", 0) # sortInfo + pdb += b"BOOK" # type + pdb += b"MOBI" # creator + pdb += struct.pack(">I", 0) # uniqueID seed + pdb += struct.pack(">I", 0) # nextRecordListID + pdb += struct.pack(">H", len(records)) # numRecords + # record 表(8 字节 each): offset(4), attr(1), uniqueID(3) + data_start = 78 + 8 * len(records) + 2 # + appInfo? PDB 有 appInfo+sortInfo? 这里偏移从 78 起步 + # 实际 PDB: 头 78 字节,然后 record list,然后 appInfo (2 bytes 0,1), sortInfo(2 bytes) 可缺省 + # 我们用 title 记录头: 78 + 8*n; 记录区从 (78+8*n+? ) 起;这里不加 appInfo 让 offset 简单 + offs = [] + cur = 78 + 8 * len(records) + for r in records: + offs.append(cur) + cur += len(r) + for i, o in enumerate(offs): + pdb += struct.pack(">I", o) # offset (4) + pdb += struct.pack("