feat(tuku): 阅读器左右翻页核心——页源契约/预解码窗口/跟手翻页/方向感知 tap
- 页源契约(logic/reader):count/meta/open/close + 目录册(文件夹即书)为第一个实现; meta 由纯 C 图片头探测(PNG IHDR/JPEG SOF/BMP)提供原始尺寸,条漫可先排版再取图, zip 册(M9/M11)只需另写 mr_book_ops,两种消费方式零改动复用 - 翻页引擎:pthread 后台解码窗口=当前±1 页(目录册 downscale 到屏幕长边),翻页只 blit 缓存页图 + meow_anim 150ms 滑动吸附;未命中立即切页显示加载态、不在翻页路径解码; 只 flush 内容区;decode/flip 首帧/翻页完成/进出内存全部 meow_log 打点 - 阅读页 UI:fit 三档(适宽/适高/原始,TopAppBar 动作循环,标题显示页码 x/y)+ 双击以点击点为锚放大(临时缓冲超上限自动降倍率)+ 单指拖拽/滑动步进平移; tap 三分区方向感知(RTL 默认:左=下一页,LTR 反之),中区切 chrome; 左缘返回交给 app.c、屏底热区交给 SDK 回桌面,两者都不拦截 - 跟手:app.c 仅补 2 处极小路由(ACT_READER 的 down/move),reader 自行算位移; 屏底 28px/左缘 24px 不进拖动,竖向主导放弃横滑 - 内存纪律:驻留 ≤3 张 downscale 页图;放大时窗口收缩到当前页;on_leave 停解码线程、 释放全部页图/册索引/TopAppBar,并记录 VmRSS/MemAvailable 前后对比 - 测试/构建:新增 test_reader_core(契约边界/空册/越界/重复 open-close/首末页/头探测/ 方向语义/fit/平移夹取,108 项);run_tests.sh 与 build_tuku.sh 源列表接线; 宿主 46+108 全 PASS,交叉构建静态 MIPS32r2 ELF、-Wall -Wextra 零警告
This commit is contained in:
1 parent
28b67a7b8d
commit
23c9db73df
10 files changed
+2088
-69
No files matched your search
@@ -0,0 +1,282 @@
|
||||
/* logic/reader/page_source.c — 页源契约实现(纯 C):目录册 + 图片头探测。
|
||||
*
|
||||
* 契约与两种消费方式的说明见 page_source.h。本文件不含 SDK 依赖:解码/释放由
|
||||
* UI 层注回回调,因此可在宿主测试里用假回调覆盖「空册/越界/重复 open-close/首末页」。
|
||||
*/
|
||||
#include "logic/reader/page_source.h"
|
||||
|
||||
#include <dirent.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/stat.h>
|
||||
|
||||
/* ============================================================ */
|
||||
/* 契约包装 */
|
||||
/* ============================================================ */
|
||||
int mr_book_valid(const mr_book *b)
|
||||
{
|
||||
return b && b->ops && b->ops->count && b->ops->open;
|
||||
}
|
||||
|
||||
int mr_book_count(const mr_book *b)
|
||||
{
|
||||
if (!mr_book_valid(b)) return 0;
|
||||
int n = b->ops->count(b->ctx);
|
||||
return n > 0 ? n : 0;
|
||||
}
|
||||
|
||||
int mr_book_meta(const mr_book *b, int index, int *w, int *h)
|
||||
{
|
||||
if (w) *w = 0;
|
||||
if (h) *h = 0;
|
||||
if (!mr_book_valid(b) || !b->ops->meta) return 0;
|
||||
if (index < 0 || index >= mr_book_count(b)) return 0;
|
||||
return b->ops->meta(b->ctx, index, w, h) ? 1 : 0;
|
||||
}
|
||||
|
||||
meow_img *mr_book_open(mr_book *b, int index)
|
||||
{
|
||||
if (!mr_book_valid(b)) return NULL;
|
||||
if (index < 0 || index >= mr_book_count(b)) return NULL; /* 越界:契约边界 */
|
||||
return b->ops->open(b->ctx, index);
|
||||
}
|
||||
|
||||
void mr_book_close(mr_book *b, meow_img *img)
|
||||
{
|
||||
if (!img) return;
|
||||
if (!mr_book_valid(b) || !b->ops->close) return;
|
||||
b->ops->close(b->ctx, img);
|
||||
}
|
||||
|
||||
void mr_book_destroy(mr_book *b)
|
||||
{
|
||||
if (!b) return;
|
||||
if (b->ops && b->ops->destroy && b->ctx) b->ops->destroy(b->ctx);
|
||||
b->ops = NULL;
|
||||
b->ctx = NULL;
|
||||
}
|
||||
|
||||
/* ============================================================ */
|
||||
/* 目录册 */
|
||||
/* ============================================================ */
|
||||
static int name_cmp_qsort(const void *pa, const void *pb)
|
||||
{
|
||||
const char *a = *(const char *const *)pa;
|
||||
const char *b = *(const char *const *)pb;
|
||||
return mr_natural_cmp(a, b);
|
||||
}
|
||||
|
||||
void mr_dir_book_path(const mr_dir_book *db, int index, char *out, int outsz)
|
||||
{
|
||||
if (!out || outsz <= 0) return;
|
||||
out[0] = '\0';
|
||||
if (!db || index < 0 || index >= db->count || !db->names[index]) return;
|
||||
size_t dl = strlen(db->dir);
|
||||
if (dl > 0 && db->dir[dl - 1] == '/')
|
||||
snprintf(out, (size_t)outsz, "%s%s", db->dir, db->names[index]);
|
||||
else
|
||||
snprintf(out, (size_t)outsz, "%s/%s", db->dir, db->names[index]);
|
||||
}
|
||||
|
||||
void mr_dir_book_free(mr_dir_book *db)
|
||||
{
|
||||
if (!db) return;
|
||||
for (int i = 0; i < db->count; i++) free(db->names[i]);
|
||||
free(db->names);
|
||||
db->names = NULL;
|
||||
db->count = 0;
|
||||
}
|
||||
|
||||
int mr_dir_book_init(mr_dir_book *db, const char *dir, int maxdim,
|
||||
mr_book_load_fn load, mr_book_unload_fn unload, void *ud)
|
||||
{
|
||||
if (!db) return -1;
|
||||
memset(db, 0, sizeof *db);
|
||||
if (!dir || !dir[0] || !load || !unload) return -1;
|
||||
if (maxdim <= 0) maxdim = 720;
|
||||
if (strlen(dir) >= sizeof db->dir) return -1;
|
||||
|
||||
DIR *d = opendir(dir);
|
||||
if (!d) return -1;
|
||||
|
||||
snprintf(db->dir, sizeof db->dir, "%s", dir);
|
||||
db->maxdim = maxdim;
|
||||
db->load = load;
|
||||
db->unload = unload;
|
||||
db->ud = ud;
|
||||
|
||||
int cap = 0;
|
||||
struct dirent *de;
|
||||
while ((de = readdir(d)) != NULL) {
|
||||
const char *name = de->d_name;
|
||||
if (strcmp(name, ".") == 0 || strcmp(name, "..") == 0) continue;
|
||||
if (name[0] == '.') continue; /* 隐藏文件不参与成册 */
|
||||
if (!mr_path_is_image(name)) continue;
|
||||
|
||||
char full[MR_PATH_MAX];
|
||||
size_t dl = strlen(db->dir);
|
||||
int n = (dl > 0 && db->dir[dl - 1] == '/')
|
||||
? snprintf(full, sizeof full, "%s%s", db->dir, name)
|
||||
: snprintf(full, sizeof full, "%s/%s", db->dir, name);
|
||||
if (n <= 0 || n >= (int)sizeof full) continue;
|
||||
struct stat st;
|
||||
if (lstat(full, &st) != 0 || !S_ISREG(st.st_mode)) continue; /* 跳过符号链接/目录 */
|
||||
|
||||
if (db->count >= MR_BOOK_MAX_PAGES) break;
|
||||
if (db->count >= cap) {
|
||||
int nc = cap ? cap * 2 : 32;
|
||||
char **nn = (char **)realloc(db->names, (size_t)nc * sizeof *nn);
|
||||
if (!nn) { mr_dir_book_free(db); closedir(d); return -1; }
|
||||
db->names = nn;
|
||||
cap = nc;
|
||||
}
|
||||
size_t len = strlen(name);
|
||||
db->names[db->count] = (char *)malloc(len + 1);
|
||||
if (!db->names[db->count]) { mr_dir_book_free(db); closedir(d); return -1; }
|
||||
memcpy(db->names[db->count], name, len + 1);
|
||||
db->count++;
|
||||
}
|
||||
closedir(d);
|
||||
|
||||
if (db->count > 1)
|
||||
qsort(db->names, (size_t)db->count, sizeof *db->names, name_cmp_qsort);
|
||||
return db->count;
|
||||
}
|
||||
|
||||
/* ============================================================ */
|
||||
/* 图片头探测(不解码像素) */
|
||||
/* ============================================================ */
|
||||
static unsigned probe_be32(const unsigned char *p)
|
||||
{
|
||||
return ((unsigned)p[0] << 24) | ((unsigned)p[1] << 16) | ((unsigned)p[2] << 8) | p[3];
|
||||
}
|
||||
|
||||
static int probe_png(FILE *f, int *w, int *h)
|
||||
{
|
||||
unsigned char hdr[24];
|
||||
if (fread(hdr, 1, sizeof hdr, f) != sizeof hdr) return 0;
|
||||
static const unsigned char sig[8] = { 0x89, 'P', 'N', 'G', 0x0D, 0x0A, 0x1A, 0x0A };
|
||||
if (memcmp(hdr, sig, 8) != 0 || memcmp(hdr + 12, "IHDR", 4) != 0) return 0;
|
||||
unsigned ww = probe_be32(hdr + 16), hh = probe_be32(hdr + 20);
|
||||
if (ww == 0 || hh == 0) return 0;
|
||||
*w = (int)ww;
|
||||
*h = (int)hh;
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int probe_jpeg(FILE *f, int *w, int *h)
|
||||
{
|
||||
unsigned char b[2];
|
||||
if (fread(b, 1, 2, f) != 2 || b[0] != 0xFF || b[1] != 0xD8) return 0;
|
||||
for (;;) {
|
||||
int c = fgetc(f);
|
||||
if (c == EOF) return 0;
|
||||
if (c != 0xFF) continue;
|
||||
int m = fgetc(f);
|
||||
while (m == 0xFF) m = fgetc(f); /* 填充字节 */
|
||||
if (m == EOF) return 0;
|
||||
if (m == 0xD8 || m == 0x01 || (m >= 0xD0 && m <= 0xD7)) continue; /* 无长度段 */
|
||||
if (m == 0xD9 || m == 0xDA) return 0; /* EOI/SOS 前没等到 SOF */
|
||||
unsigned char lb[2];
|
||||
if (fread(lb, 1, 2, f) != 2) return 0;
|
||||
int len = ((int)lb[0] << 8) | lb[1];
|
||||
if (len < 2) return 0;
|
||||
int is_sof = (m >= 0xC0 && m <= 0xCF) && m != 0xC4 && m != 0xC8 && m != 0xCC;
|
||||
if (is_sof) {
|
||||
unsigned char so[5];
|
||||
if (len < 7 || fread(so, 1, 5, f) != 5) return 0;
|
||||
int hh = ((int)so[1] << 8) | so[2];
|
||||
int ww = ((int)so[3] << 8) | so[4];
|
||||
if (ww <= 0 || hh <= 0) return 0;
|
||||
*w = ww;
|
||||
*h = hh;
|
||||
return 1;
|
||||
}
|
||||
if (fseek(f, len - 2, SEEK_CUR) != 0) return 0;
|
||||
}
|
||||
}
|
||||
|
||||
static int probe_bmp(FILE *f, int *w, int *h)
|
||||
{
|
||||
unsigned char hdr[26];
|
||||
if (fread(hdr, 1, sizeof hdr, f) != sizeof hdr) return 0;
|
||||
if (hdr[0] != 'B' || hdr[1] != 'M') return 0;
|
||||
int ww = (int)((unsigned)hdr[18] | ((unsigned)hdr[19] << 8) |
|
||||
((unsigned)hdr[20] << 16) | ((unsigned)hdr[21] << 24));
|
||||
int hh = (int)((unsigned)hdr[22] | ((unsigned)hdr[23] << 8) |
|
||||
((unsigned)hdr[24] << 16) | ((unsigned)hdr[25] << 24));
|
||||
if (hh < 0) hh = -hh; /* 自底向上 BMP */
|
||||
if (ww <= 0 || hh <= 0) return 0;
|
||||
*w = ww;
|
||||
*h = hh;
|
||||
return 1;
|
||||
}
|
||||
|
||||
int mr_probe_size(const char *path, int *w, int *h)
|
||||
{
|
||||
if (w) *w = 0;
|
||||
if (h) *h = 0;
|
||||
if (!path || !path[0] || !w || !h) return 0;
|
||||
FILE *f = fopen(path, "rb");
|
||||
if (!f) return 0;
|
||||
int ok = probe_png(f, w, h);
|
||||
if (!ok) { rewind(f); ok = probe_jpeg(f, w, h); }
|
||||
if (!ok) { rewind(f); ok = probe_bmp(f, w, h); }
|
||||
fclose(f);
|
||||
if (ok && (*w <= 0 || *h <= 0)) { *w = *h = 0; return 0; }
|
||||
return ok;
|
||||
}
|
||||
|
||||
/* ============================================================ */
|
||||
/* mr_book_ops 适配器(目录册) */
|
||||
/* ============================================================ */
|
||||
static int dir_ops_count(void *ctx)
|
||||
{
|
||||
mr_dir_book *db = (mr_dir_book *)ctx;
|
||||
return db ? db->count : 0;
|
||||
}
|
||||
|
||||
static int dir_ops_meta(void *ctx, int index, int *w, int *h)
|
||||
{
|
||||
mr_dir_book *db = (mr_dir_book *)ctx;
|
||||
if (w) *w = 0;
|
||||
if (h) *h = 0;
|
||||
if (!db || index < 0 || index >= db->count) return 0;
|
||||
char path[MR_PATH_MAX];
|
||||
mr_dir_book_path(db, index, path, (int)sizeof path);
|
||||
return mr_probe_size(path, w, h);
|
||||
}
|
||||
|
||||
static meow_img *dir_ops_open(void *ctx, int index)
|
||||
{
|
||||
mr_dir_book *db = (mr_dir_book *)ctx;
|
||||
if (!db || index < 0 || index >= db->count) return NULL;
|
||||
char path[MR_PATH_MAX];
|
||||
mr_dir_book_path(db, index, path, (int)sizeof path);
|
||||
if (!path[0]) return NULL;
|
||||
return db->load(path, db->ud, db->maxdim);
|
||||
}
|
||||
|
||||
static void dir_ops_close(void *ctx, meow_img *img)
|
||||
{
|
||||
mr_dir_book *db = (mr_dir_book *)ctx;
|
||||
if (db && db->unload && img) db->unload(img);
|
||||
}
|
||||
|
||||
static void dir_ops_destroy(void *ctx)
|
||||
{
|
||||
mr_dir_book *db = (mr_dir_book *)ctx;
|
||||
if (db) mr_dir_book_free(db);
|
||||
}
|
||||
|
||||
static const mr_book_ops g_dir_ops = {
|
||||
dir_ops_count, dir_ops_meta, dir_ops_open, dir_ops_close, dir_ops_destroy,
|
||||
};
|
||||
|
||||
void mr_dir_book_bind(mr_book *b, mr_dir_book *db)
|
||||
{
|
||||
if (!b) return;
|
||||
b->ops = db ? &g_dir_ops : NULL;
|
||||
b->ctx = db;
|
||||
}
|
||||
Reference in new issue
Block a user