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:
AskaEth committed 2026-09-02 03:11:55 +08:00
1 parent 10690bc348
commit 0294553096
27 files changed
+3440 -193

No files matched your search

+2
View File
@@ -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
+209
View File
@@ -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
View File
@@ -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;
}
+5
View File
@@ -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=当前页) */
+68 -12
View File
@@ -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;
}
+2 -4
View File
@@ -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);
+103 -59
View File
@@ -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"
+128
View File
@@ -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 };
+531
View File
@@ -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 };
+280
View File
@@ -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 };
+286
View File
@@ -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;
}
+35
View File
@@ -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 */
+237
View File
@@ -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 };
+70
View File
@@ -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;
}
+34
View File
@@ -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 */
+176
View File
@@ -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;
}
+70
View File
@@ -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 */
+45
View File
@@ -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 */
+309
View File
@@ -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;
}
+54
View File
@@ -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。
* 支持:&amp; &lt; &gt; &quot; &apos; &nbsp; &#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 */
+82
View File
@@ -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);
}
+40
View File
@@ -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 */
+74
View File
@@ -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;
}
+26
View File
@@ -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 */
+66
View File
@@ -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;
}
+116
View File
@@ -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;
}
+222
View File
@@ -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>的正文,包含实体 &amp; 和标点符号。English words mixed here.</p>
<p>第二段:测试坏标签容错 <b>未闭合 <i>粗斜体</p>
<p><img src="../images/cover.png" alt="cover"/></p>
<p>&#20197;&#19978;是数值实体引用的中文。</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 段落,含实体 &amp; 与 &#27491;&#25991;。</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 &amp; 与标点。</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")