reader(R2): EPUB/FB2/MOBI/CBZ 加载器落地
- rdsrc 统一文档源抽象:TXT/FB2/EPUB/MOBI/CBZ 都产出「UTF-8 段序列+目录+元数据+图片」, 与 R1 rdtxt 段契约一致;book.c 改喂 rd_src(文本)或图页(CBZ)。 - EPUB:miniz 解 ZIP → 自写 rdxml 回调解析 OPF(spine/manifest)+NCX/nav → 正文 XHTML 用 rdhtml 逐行粗解析(坏标签/未闭合/实体容错,不建 DOM),章节按 RDSRC_SEG_MAX 分片。 - FB2:rdxml 严格解析抽 section 段落 + base64 内嵌图;段题进正文+目录。 - MOBI/AZW3:vendor libmobi 最小调用面(mobi_init→load→parse_kf7/8→parse_rawml_opt→ flow/resources),HTML 走 rdhtml 同款粗解析;LGPL-3.0 合规见 NOTICE(版权+全文+锁定 commit)。 - CBZ:miniz 解 ZIP → 自然序图片序列,rdimg(stb_image) 解码+降采样,阅读页图页模式。 - 目录显示与跳转(EPUB NCX/FB2 section/MOBI 标题);书架开放四格式;进度按 (段,字符偏移)(TXT/FB2/EPUB/MOBI)/页号(CBZ)。 - 宿主单测 loaders_smoke(四格式抽取/目录/图片/base64/页序)+ qemu-mipsel formats_smoke/book_smoke;R1 test_reader_core 58/0 不回归。 - build_reader.sh 集成 miniz/libmobi/stb/zlib 头;NOTICE 入仓;交叉 -Wall -Wextra 0 warning。 新 reader md5: e29036599300eb277b1c960a75b65dbf
This commit is contained in:
1 parent
10690bc348
commit
0294553096
27 files changed
+3440
-193
No files matched your search
@@ -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
|
||||
@@ -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. <http://fsf.org/>
|
||||
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).
|
||||
|
||||
+170
-118
@@ -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 <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -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=当前页) */
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
#include <time.h>
|
||||
|
||||
#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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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"
|
||||
@@ -0,0 +1,128 @@
|
||||
/* rdcbz.c — CBZ loader:miniz 解 ZIP → 图片序列(自然序)→ rd_src_page 逐页原始数据(调用方
|
||||
* stb_image 解码+降采样)。纯图阅读,不走排版器。无正文/目录元数据(标题=文件名)。
|
||||
*/
|
||||
#include "rdsrc_int.h"
|
||||
#include "rdzip.h"
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <ctype.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 {
|
||||
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 };
|
||||
@@ -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 <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <ctype.h>
|
||||
#include <strings.h>
|
||||
|
||||
/* 有界拷贝(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 抽 <a href>text</a> */
|
||||
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 抽取:<a href=..>…text…</a> 一个扁平目录 */
|
||||
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 };
|
||||
@@ -0,0 +1,280 @@
|
||||
/* rdfb2.c — FB2 loader:严格 XML,用自写 rdxml 回调解析器抽段落。
|
||||
* 结构:<FictionBook> → <description>(元数据跳过) → <body>(主) → <section>… → <p>;
|
||||
* <title> 段题(目录);<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 };
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 */
|
||||
@@ -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 };
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 */
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 */
|
||||
@@ -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 */
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 */
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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 */
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 */
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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>第一章</title></head><body>
|
||||
<h1>第一章 测试</h1>
|
||||
<p>这是一段<em>带格式</em>的正文,包含实体 & 和标点符号。English words mixed here.</p>
|
||||
<p>第二段:测试坏标签容错 <b>未闭合 <i>粗斜体</p>
|
||||
<p><img src="../images/cover.png" alt="cover"/></p>
|
||||
<p>以上是数值实体引用的中文。</p>
|
||||
</body></html>"""
|
||||
ch2 = """<?xml version="1.0"?>
|
||||
<html><head><title>第二章</title></head><body>
|
||||
<h1>第二章 测试</h1>
|
||||
<p>第二章正文,用于验证多章节 spine 顺序。</p>
|
||||
<p>结尾</p>
|
||||
</body></html>"""
|
||||
container = """<?xml version="1.0"?>
|
||||
<container version="1.0" xmlns="urn:oasis:names:tc:opendocument:xmlns:container">
|
||||
<rootfiles><rootfile full-path="OEBPS/content.opf" media-type="application/oebps-package+xml"/></rootfiles>
|
||||
</container>"""
|
||||
opf = """<?xml version="1.0"?>
|
||||
<package xmlns="http://www.idpf.org/2007/opf" version="3.0" unique-identifier="bookid">
|
||||
<metadata xmlns:dc="http://purl.org/dc/elements/1.1/">
|
||||
<dc:title>测试书</dc:title><dc:creator>MeowOS</dc:creator>
|
||||
</metadata>
|
||||
<manifest>
|
||||
<item id="ncx" href="toc.ncx" media-type="application/x-dtbncx+xml"/>
|
||||
<item id="ch1" href="text/ch1.xhtml" media-type="application/xhtml+xml"/>
|
||||
<item id="ch2" href="text/ch2.xhtml" media-type="application/xhtml+xml"/>
|
||||
<item id="cover-img" href="images/cover.png" media-type="image/png"/>
|
||||
</manifest>
|
||||
<spine toc="ncx"><itemref idref="ch1"/><itemref idref="ch2"/></spine>
|
||||
</package>"""
|
||||
ncx = """<?xml version="1.0"?>
|
||||
<ncx xmlns="http://www.daisy.org/z3986/2005/ncx/" version="2005-1">
|
||||
<navMap>
|
||||
<navPoint id="n1"><navLabel><text>第一章</text></navLabel><content src="text/ch1.xhtml"/></navPoint>
|
||||
<navPoint id="n2"><navLabel><text>第二章</text></navLabel><content src="text/ch2.xhtml"/></navPoint>
|
||||
</navMap>
|
||||
</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"""<?xml version="1.0" encoding="UTF-8"?>
|
||||
<FictionBook xmlns="http://www.gribuser.ru/xml/fictionbook/2.0">
|
||||
<description><title-info><book-title>FB2测试</book-title><author><first-name>Meo</first-name><last-name>OS</last-name></author></title-info></description>
|
||||
<body>
|
||||
<section><title><p>第一章</p></title>
|
||||
<p>这是 FB2 段落,含实体 & 与 正文。</p>
|
||||
<p><image l:href="#img1"/></p>
|
||||
</section>
|
||||
<section><title><p>第二章</p></title>
|
||||
<p>第二章内容。</p>
|
||||
</section>
|
||||
</body>
|
||||
<binary id="img1" content-type="image/png">{img_data}</binary>
|
||||
</FictionBook>"""
|
||||
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 = (
|
||||
"<html><body>"
|
||||
"<h1>MOBI 测试</h1>"
|
||||
"<p>这是 Mobi 正文,用于验证 libmobi rawml 抽取与粗解析。</p>"
|
||||
"<p>第二段,含 entity & 与标点。</p>"
|
||||
"</body></html>"
|
||||
)
|
||||
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("<HHIHHI",
|
||||
2, # compression = 2 -> 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("<B", 0) # attr (1)
|
||||
pdb += (i & 0xFFFFFF).to_bytes(3, "little") # unique ID (3 字节)
|
||||
for r in records:
|
||||
pdb += r
|
||||
# 补 appInfo/sortInfo? libmobi 可能要求 record0 后紧跟 appInfo。先不加,看解析结果。
|
||||
path = os.path.join(OUT, "sample.mobi")
|
||||
open(path, "wb").write(bytes(pdb))
|
||||
print(" wrote", path, len(pdb), "bytes")
|
||||
|
||||
make_epub(); make_fb2(); make_cbz(); make_mobi()
|
||||
print("samples written to", OUT)
|
||||
for f in sorted(os.listdir(OUT)):
|
||||
print(" ", f, os.path.getsize(os.path.join(OUT, f)), "bytes")
|
||||
Reference in new issue
Block a user