Merge branch 'feat/tuku-fluid-viewer-core'
This commit is contained in:
commit
5f40d6fd87
10 files changed
+2088
-69
No files matched your search
@@ -10,6 +10,7 @@
|
|||||||
* 各页 on_draw / SDK 控件下一帧即用新语义色重绘(不缓存颜色)。
|
* 各页 on_draw / SDK 控件下一帧即用新语义色重绘(不缓存颜色)。
|
||||||
*/
|
*/
|
||||||
#include "tuku_internal.h"
|
#include "tuku_internal.h"
|
||||||
|
#include "ui/reader/reader.h"
|
||||||
#include "meow_ui_nav.h"
|
#include "meow_ui_nav.h"
|
||||||
|
|
||||||
#include <signal.h>
|
#include <signal.h>
|
||||||
@@ -89,14 +90,18 @@ static void app_on_swipe_progress(int x_start, int dx)
|
|||||||
meow_ui_swipe_hint(x_start + dx);
|
meow_ui_swipe_hint(x_start + dx);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 跟手拖动路由(M6):ACT_READER 需要原始 down/move 算位移;其余页保持原语义。
|
||||||
|
* 屏底热区/左缘的避让由 reader 侧按同一口径判断(reader_on_touch_down)。 */
|
||||||
static void app_on_touch_down(int x, int y)
|
static void app_on_touch_down(int x, int y)
|
||||||
{
|
{
|
||||||
if (app_active() == ACT_LIBRARY) library_on_touch_down(x, y);
|
if (app_active() == ACT_LIBRARY) library_on_touch_down(x, y);
|
||||||
|
else if (app_active() == ACT_READER) reader_on_touch_down(x, y);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void app_on_touch_move(int x, int y)
|
static void app_on_touch_move(int x, int y)
|
||||||
{
|
{
|
||||||
if (app_active() == ACT_LIBRARY) library_on_touch_move(x, y);
|
if (app_active() == ACT_LIBRARY) library_on_touch_move(x, y);
|
||||||
|
else if (app_active() == ACT_READER) reader_on_touch_move(x, y);
|
||||||
}
|
}
|
||||||
|
|
||||||
int main(void)
|
int main(void)
|
||||||
|
|||||||
+1
-1
@@ -12,7 +12,7 @@ SDK="${MEOW_SDK_DIR:-$HOME/Proj/MeowOS-SDK/sdk}"
|
|||||||
|
|
||||||
"$CC" -Os -static -Wall -Wextra -I"$SDK" -I. \
|
"$CC" -Os -static -Wall -Wextra -I"$SDK" -I. \
|
||||||
app.c ui/library/library.c ui/reader/reader.c ui/settings/settings.c ui/lock/lock.c \
|
app.c ui/library/library.c ui/reader/reader.c ui/settings/settings.c ui/lock/lock.c \
|
||||||
logic/library.c logic/reader.c \
|
logic/library.c logic/reader.c logic/reader/page_source.c logic/reader/nav.c \
|
||||||
"$SDK/meow_ui.a" "$SYSROOT/lib/libz.a" -lpthread -o picbrowser
|
"$SDK/meow_ui.a" "$SYSROOT/lib/libz.a" -lpthread -o picbrowser
|
||||||
ls -la picbrowser
|
ls -la picbrowser
|
||||||
file picbrowser
|
file picbrowser
|
||||||
@@ -0,0 +1,120 @@
|
|||||||
|
/* logic/reader/nav.c — 阅读导航纯逻辑实现(见 nav.h 的方向语义说明)。 */
|
||||||
|
#include "logic/reader/nav.h"
|
||||||
|
|
||||||
|
int mr_nav_tap_zone(int x, int view_w)
|
||||||
|
{
|
||||||
|
if (view_w <= 0) return MR_ZONE_MIDDLE;
|
||||||
|
if (x < view_w / 3) return MR_ZONE_LEFT;
|
||||||
|
if (x >= (view_w * 2) / 3) return MR_ZONE_RIGHT;
|
||||||
|
return MR_ZONE_MIDDLE;
|
||||||
|
}
|
||||||
|
|
||||||
|
int mr_nav_tap_delta(int rtl, int zone)
|
||||||
|
{
|
||||||
|
if (zone == MR_ZONE_MIDDLE) return 0;
|
||||||
|
if (rtl) return (zone == MR_ZONE_LEFT) ? 1 : -1; /* RTL:左进右退 */
|
||||||
|
return (zone == MR_ZONE_LEFT) ? -1 : 1; /* LTR:右进左退 */
|
||||||
|
}
|
||||||
|
|
||||||
|
int mr_nav_swipe_delta(int rtl, int dx)
|
||||||
|
{
|
||||||
|
if (dx == 0) return 0;
|
||||||
|
if (rtl) return dx > 0 ? 1 : -1;
|
||||||
|
return dx > 0 ? -1 : 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int mr_nav_clamp_page(int page, int count)
|
||||||
|
{
|
||||||
|
if (count <= 0) return -1;
|
||||||
|
if (page < 0) return 0;
|
||||||
|
if (page >= count) return count - 1;
|
||||||
|
return page;
|
||||||
|
}
|
||||||
|
|
||||||
|
int mr_nav_commit_dist(int view_w)
|
||||||
|
{
|
||||||
|
int t = view_w / 4;
|
||||||
|
if (t < 60) t = 60;
|
||||||
|
if (t > 160) t = 160;
|
||||||
|
return t;
|
||||||
|
}
|
||||||
|
|
||||||
|
int mr_nav_commit(int dx, int view_w)
|
||||||
|
{
|
||||||
|
int adx = dx < 0 ? -dx : dx;
|
||||||
|
return adx >= mr_nav_commit_dist(view_w);
|
||||||
|
}
|
||||||
|
|
||||||
|
int mr_nav_fit_size(int fit, int src_w, int src_h, int vw, int vh,
|
||||||
|
int *out_w, int *out_h)
|
||||||
|
{
|
||||||
|
if (!out_w || !out_h || src_w <= 0 || src_h <= 0 || vw <= 0 || vh <= 0) return -1;
|
||||||
|
long long w, h;
|
||||||
|
if (fit == MR_FIT_ORIGINAL) {
|
||||||
|
w = src_w;
|
||||||
|
h = src_h;
|
||||||
|
} else if (fit == MR_FIT_HEIGHT) {
|
||||||
|
h = vh;
|
||||||
|
w = (long long)src_w * vh / src_h;
|
||||||
|
} else {
|
||||||
|
w = vw;
|
||||||
|
h = (long long)src_h * vw / src_w;
|
||||||
|
}
|
||||||
|
if (w < 1) w = 1;
|
||||||
|
if (h < 1) h = 1;
|
||||||
|
*out_w = (int)w;
|
||||||
|
*out_h = (int)h;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int mr_nav_zoom_size(int w, int h, int zoom_pct, int *out_w, int *out_h)
|
||||||
|
{
|
||||||
|
if (!out_w || !out_h || w <= 0 || h <= 0 || zoom_pct <= 0) return -1;
|
||||||
|
long long ww = (long long)w * zoom_pct / 100;
|
||||||
|
long long hh = (long long)h * zoom_pct / 100;
|
||||||
|
if (ww < 1) ww = 1;
|
||||||
|
if (hh < 1) hh = 1;
|
||||||
|
*out_w = (int)ww;
|
||||||
|
*out_h = (int)hh;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void mr_nav_pan_limit(int content_w, int content_h, int vw, int vh,
|
||||||
|
int *lim_x, int *lim_y)
|
||||||
|
{
|
||||||
|
int lx = content_w - vw;
|
||||||
|
int ly = content_h - vh;
|
||||||
|
if (lx < 0) lx = 0;
|
||||||
|
if (ly < 0) ly = 0;
|
||||||
|
/* 平移量是「中心偏移」:可左右各移动溢出量的一半,两边才都看得到。 */
|
||||||
|
if (lim_x) *lim_x = lx / 2;
|
||||||
|
if (lim_y) *lim_y = ly / 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
void mr_nav_pan_clamp(int content_w, int content_h, int vw, int vh,
|
||||||
|
int *pan_x, int *pan_y)
|
||||||
|
{
|
||||||
|
int lx, ly;
|
||||||
|
mr_nav_pan_limit(content_w, content_h, vw, vh, &lx, &ly);
|
||||||
|
if (pan_x) {
|
||||||
|
if (*pan_x > lx) *pan_x = lx;
|
||||||
|
if (*pan_x < -lx) *pan_x = -lx;
|
||||||
|
}
|
||||||
|
if (pan_y) {
|
||||||
|
if (*pan_y > ly) *pan_y = ly;
|
||||||
|
if (*pan_y < -ly) *pan_y = -ly;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int mr_nav_slide_from(int rtl, int delta, int view_w)
|
||||||
|
{
|
||||||
|
if (!delta) return 0;
|
||||||
|
int from = rtl ? -delta : delta;
|
||||||
|
return from * view_w;
|
||||||
|
}
|
||||||
|
|
||||||
|
int mr_nav_fit_next(int fit)
|
||||||
|
{
|
||||||
|
if (fit < 0 || fit >= MR_FIT_COUNT) return MR_FIT_WIDTH;
|
||||||
|
return (fit + 1) % MR_FIT_COUNT;
|
||||||
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
/* logic/reader/nav.h — 阅读导航纯逻辑(方向语义 / tap 分区 / fit / 平移 clamp)。
|
||||||
|
*
|
||||||
|
* 纯 C、无 SDK 依赖:reader.c 与宿主测试共用同一实现(logic/reader/nav.c)。
|
||||||
|
*
|
||||||
|
* 方向语义(mr_reader_get_direction,默认 RTL):
|
||||||
|
* - LTR(左→右读):页码沿屏幕向右递增;屏左 1/3 = 上一页,屏右 1/3 = 下一页;
|
||||||
|
* 手指向右拖露出左边那页 = 上一页。
|
||||||
|
* - RTL(日漫,默认):页码沿屏幕向左递增;屏左 1/3 = **下一页**,屏右 1/3 = 上一页;
|
||||||
|
* 手指向右拖露出左边那页 = 下一页。
|
||||||
|
* 图像本身不做镜像(RTL 只影响翻页语义)。
|
||||||
|
*/
|
||||||
|
#ifndef MR_READER_NAV_H
|
||||||
|
#define MR_READER_NAV_H
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/* ---- fit 三档 ---- */
|
||||||
|
#define MR_FIT_WIDTH 0 /* 适宽:撑满屏宽 */
|
||||||
|
#define MR_FIT_HEIGHT 1 /* 适高:撑满屏高 */
|
||||||
|
#define MR_FIT_ORIGINAL 2 /* 原始:1:1 像素 */
|
||||||
|
#define MR_FIT_COUNT 3
|
||||||
|
|
||||||
|
/* ---- tap 分区(按屏幕横向三分) ---- */
|
||||||
|
#define MR_ZONE_LEFT 0
|
||||||
|
#define MR_ZONE_MIDDLE 1
|
||||||
|
#define MR_ZONE_RIGHT 2
|
||||||
|
|
||||||
|
/* 屏宽三分:左/中/右。view_w<=0 返回 MIDDLE。 */
|
||||||
|
int mr_nav_tap_zone(int x, int view_w);
|
||||||
|
|
||||||
|
/* tap 分区的页增量:+1=下一页 / -1=上一页 / 0=chrome 显隐(中区)。 */
|
||||||
|
int mr_nav_tap_delta(int rtl, int zone);
|
||||||
|
|
||||||
|
/* 中部横向拖动的页增量。dx>0 = 手指向右(露出屏幕左侧那页)。
|
||||||
|
* 首/末页不在这里判断——调用方用 mr_nav_clamp_page 落到实际目标页。 */
|
||||||
|
int mr_nav_swipe_delta(int rtl, int dx);
|
||||||
|
|
||||||
|
/* 页下标 clamp;count<=0 返回 -1。 */
|
||||||
|
int mr_nav_clamp_page(int page, int count);
|
||||||
|
|
||||||
|
/* 翻页提交阈值:视口宽的 1/4,夹在 [60,160] px。 */
|
||||||
|
int mr_nav_commit_dist(int view_w);
|
||||||
|
|
||||||
|
/* 松手位移是否达到翻页阈值。 */
|
||||||
|
int mr_nav_commit(int dx, int view_w);
|
||||||
|
|
||||||
|
/* fit 目标尺寸(未含 zoom)。0=成功;参数非法 -1。 */
|
||||||
|
int mr_nav_fit_size(int fit, int src_w, int src_h, int vw, int vh, int *out_w, int *out_h);
|
||||||
|
|
||||||
|
/* 按百分比放大(zoom_pct=100 原样;200=2×)。0=成功。 */
|
||||||
|
int mr_nav_zoom_size(int w, int h, int zoom_pct, int *out_w, int *out_h);
|
||||||
|
|
||||||
|
/* 平移半幅:内容比视口每侧多出的像素(>=0)。内容不溢出该轴 → 0(该轴不可平移)。 */
|
||||||
|
void mr_nav_pan_limit(int content_w, int content_h, int vw, int vh, int *lim_x, int *lim_y);
|
||||||
|
|
||||||
|
/* 把平移量夹到 ±半幅。 */
|
||||||
|
void mr_nav_pan_clamp(int content_w, int content_h, int vw, int vh, int *pan_x, int *pan_y);
|
||||||
|
|
||||||
|
/* 翻页动画的进场起点(px):下一页在 RTL 从左侧进、LTR 从右侧进。delta=0 返回 0。 */
|
||||||
|
int mr_nav_slide_from(int rtl, int delta, int view_w);
|
||||||
|
|
||||||
|
/* fit 档循环:适宽 → 适高 → 原始 → 适宽。 */
|
||||||
|
int mr_nav_fit_next(int fit);
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
#endif /* MR_READER_NAV_H */
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
/* logic/reader/page_source.h — 页源契约(纯 C,不依赖 meow_ui.h)。
|
||||||
|
*
|
||||||
|
* 一册 = 有序页序列。契约只有四件事:
|
||||||
|
* count 总页数
|
||||||
|
* meta(i) 页的原始像素尺寸(可选;条漫连续滚动先用它排版,未知时返回 0)
|
||||||
|
* open(i) 取第 i 页:解码 + downscale 后返回 meow_img*(调用方拥有,必须 close)
|
||||||
|
* close(img) 释放 open 返回的图像
|
||||||
|
*
|
||||||
|
* 两种消费方式共用同一契约,后续实现零改动:
|
||||||
|
* - 左右翻页(本任务):预解码窗口 = 当前 ±1 页,翻页只 blit 窗口内已备好的图;
|
||||||
|
* - 条漫连续滚动(M7):先 meta(i) 累加高度排版,再按滚动位置 open(视口±1 页)。
|
||||||
|
* zip 册(M9/M11)只需另写一套 mr_book_ops,目录册是第一个实现。
|
||||||
|
*
|
||||||
|
* meow_img 在 logic 层是前向声明的不透明句柄;解码/释放由 UI 层注入回调
|
||||||
|
* (logic 不 include SDK,宿主测试用假回调即可覆盖契约边界)。
|
||||||
|
*/
|
||||||
|
#ifndef MR_PAGE_SOURCE_H
|
||||||
|
#define MR_PAGE_SOURCE_H
|
||||||
|
|
||||||
|
#include "logic/library.h" /* mr_path_is_image / mr_natural_cmp / MR_PATH_MAX / MR_NAME_MAX */
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
|
struct meow_img;
|
||||||
|
typedef struct meow_img meow_img; /* SDK 不透明句柄;logic 层只透传 */
|
||||||
|
|
||||||
|
#define MR_BOOK_MAX_PAGES 4096 /* 单册页数上限(防坏卡/巨目录撑爆内存) */
|
||||||
|
|
||||||
|
/* 解码回调(UI 层注入):解码 path,长边 >maxdim 时等比 downscale;失败返回 NULL。
|
||||||
|
* 必须可在后台线程调用(宿主测试用假实现)。 */
|
||||||
|
typedef meow_img *(*mr_book_load_fn)(const char *path, void *ud, int maxdim);
|
||||||
|
/* 释放 open 返回的图像(UI 层注入 meow_img_free;宿主测试用假实现计数)。 */
|
||||||
|
typedef void (*mr_book_unload_fn)(meow_img *img);
|
||||||
|
|
||||||
|
typedef struct mr_book_ops {
|
||||||
|
int (*count)(void *ctx);
|
||||||
|
/* 页原始尺寸(未 downscale);1=已知,0=未知。可为 NULL。 */
|
||||||
|
int (*meta)(void *ctx, int index, int *w, int *h);
|
||||||
|
/* 取页:越界/解码失败返回 NULL。返回值的所有权交给调用方。 */
|
||||||
|
meow_img *(*open)(void *ctx, int index);
|
||||||
|
/* 释放 open 返回的图像;须容忍 NULL。 */
|
||||||
|
void (*close)(void *ctx, meow_img *img);
|
||||||
|
/* 释放 provider 自身(册内索引等)。可为 NULL。 */
|
||||||
|
void (*destroy)(void *ctx);
|
||||||
|
} mr_book_ops;
|
||||||
|
|
||||||
|
typedef struct mr_book {
|
||||||
|
const mr_book_ops *ops;
|
||||||
|
void *ctx;
|
||||||
|
} mr_book;
|
||||||
|
|
||||||
|
/* ---- 契约包装:所有入口对 NULL/越界安全(越界 open 返回 NULL) ---- */
|
||||||
|
int mr_book_valid(const mr_book *b);
|
||||||
|
int mr_book_count(const mr_book *b);
|
||||||
|
int mr_book_meta(const mr_book *b, int index, int *w, int *h);
|
||||||
|
meow_img *mr_book_open(mr_book *b, int index);
|
||||||
|
void mr_book_close(mr_book *b, meow_img *img);
|
||||||
|
/* 释放 provider 自身(不含已 open 的图像——那些由调用方先 close)。幂等。 */
|
||||||
|
void mr_book_destroy(mr_book *b);
|
||||||
|
|
||||||
|
/* ---- 目录册(文件夹即书):目录直接子文件里的图片按自然序成册 ---- */
|
||||||
|
typedef struct mr_dir_book {
|
||||||
|
char dir[MR_PATH_MAX];
|
||||||
|
char **names; /* 自然序文件名(堆分配) */
|
||||||
|
int count;
|
||||||
|
int maxdim; /* open 时传给解码回调的长边上限 */
|
||||||
|
mr_book_load_fn load;
|
||||||
|
mr_book_unload_fn unload;
|
||||||
|
void *ud; /* 解码回调的宿主上下文 */
|
||||||
|
} mr_dir_book;
|
||||||
|
|
||||||
|
/* 扫描 dir 的直接子文件,收图片名并自然排序。
|
||||||
|
* 返回页数(>=0);参数非法 / 目录不可读 / 内存不足返回 -1(db 已清空、可安全 free)。 */
|
||||||
|
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);
|
||||||
|
void mr_dir_book_free(mr_dir_book *db);
|
||||||
|
/* 第 index 页的完整路径;越界时输出空串。 */
|
||||||
|
void mr_dir_book_path(const mr_dir_book *db, int index, char *out, int outsz);
|
||||||
|
/* 把目录册绑定到通用 mr_book(db 生命周期由调用方持有)。 */
|
||||||
|
void mr_dir_book_bind(mr_book *b, mr_dir_book *db);
|
||||||
|
|
||||||
|
/* 纯 C 头解析:取图片原始像素尺寸(不解码像素)。
|
||||||
|
* PNG(IHDR)/ JPEG(SOF0-15)/ BMP(BITMAPINFOHEADER+)。1=已知,0=未知。 */
|
||||||
|
int mr_probe_size(const char *path, int *w, int *h);
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
#endif /* MR_PAGE_SOURCE_H */
|
||||||
@@ -6,3 +6,8 @@ gcc -Wall -Wextra -O1 -I. \
|
|||||||
test/test_library.c logic/library.c logic/reader.c \
|
test/test_library.c logic/library.c logic/reader.c \
|
||||||
-o test/test_library
|
-o test/test_library
|
||||||
./test/test_library
|
./test/test_library
|
||||||
|
gcc -Wall -Wextra -O1 -I. \
|
||||||
|
test/test_reader_core.c logic/library.c logic/reader.c \
|
||||||
|
logic/reader/page_source.c logic/reader/nav.c \
|
||||||
|
-o test/test_reader_core
|
||||||
|
./test/test_reader_core
|
||||||
@@ -0,0 +1,452 @@
|
|||||||
|
/* test_reader_core.c — 阅读器纯逻辑宿主测试(无 SDK/硬件)。
|
||||||
|
*
|
||||||
|
* 覆盖:
|
||||||
|
* R1 页源契约:空册 / 不可读目录 / 越界 open / 重复 open-close / 首末页 / 自然序 /
|
||||||
|
* 隐藏文件与子目录排除 / 销毁后失效;
|
||||||
|
* R2 契约对两种消费方式通用:meta() 给条漫连续滚动排高度,open() 给左右翻页取图;
|
||||||
|
* R3 图片头探测:PNG/JPEG/BMP 原始尺寸(不解码);
|
||||||
|
* R4 方向语义:tap 分区 / 拖动增量(RTL 默认与 LTR)/ clamp / 提交阈值;
|
||||||
|
* R5 fit 三档 / 缩放 / 平移夹取 / 进场方向 / 档位循环;
|
||||||
|
* R6 阅读方向状态(M3 API)。
|
||||||
|
*
|
||||||
|
* 编译(在 tuku/ 下):
|
||||||
|
* gcc -Wall -Wextra -O1 -I. test/test_reader_core.c logic/library.c logic/reader.c \
|
||||||
|
* logic/reader/page_source.c logic/reader/nav.c -o test/test_reader_core
|
||||||
|
* 或直接 test/run_tests.sh。
|
||||||
|
*/
|
||||||
|
#include "logic/library.h"
|
||||||
|
#include "logic/reader.h"
|
||||||
|
#include "logic/reader/nav.h"
|
||||||
|
#include "logic/reader/page_source.h"
|
||||||
|
|
||||||
|
#include <dirent.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <sys/stat.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
|
static int g_pass, g_fail;
|
||||||
|
#define CHECK(cond, ...) do { \
|
||||||
|
if (cond) { g_pass++; } \
|
||||||
|
else { g_fail++; printf("FAIL %s:%d ", __FILE__, __LINE__); printf(__VA_ARGS__); printf("\n"); } \
|
||||||
|
} while (0)
|
||||||
|
|
||||||
|
/* ============================================================ */
|
||||||
|
/* 假解码器:meow_img* 用堆上假句柄代替,记录 open/close 次数 */
|
||||||
|
/* ============================================================ */
|
||||||
|
typedef struct fake_img { int id; } fake_img;
|
||||||
|
|
||||||
|
static int g_loads, g_frees;
|
||||||
|
static int g_last_maxdim;
|
||||||
|
static char g_last_path[MR_PATH_MAX];
|
||||||
|
|
||||||
|
static meow_img *fake_load(const char *path, void *ud, int maxdim)
|
||||||
|
{
|
||||||
|
(void)ud;
|
||||||
|
g_loads++;
|
||||||
|
g_last_maxdim = maxdim;
|
||||||
|
snprintf(g_last_path, sizeof g_last_path, "%s", path ? path : "");
|
||||||
|
fake_img *f = (fake_img *)malloc(sizeof *f);
|
||||||
|
if (!f) return NULL;
|
||||||
|
f->id = g_loads;
|
||||||
|
return (meow_img *)f;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void fake_unload(meow_img *img)
|
||||||
|
{
|
||||||
|
g_frees++;
|
||||||
|
free(img);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============================================================ */
|
||||||
|
/* 临时目录工具 */
|
||||||
|
/* ============================================================ */
|
||||||
|
static int mkdir_p(const char *path)
|
||||||
|
{
|
||||||
|
char tmp[MR_PATH_MAX];
|
||||||
|
snprintf(tmp, sizeof tmp, "%s", path);
|
||||||
|
for (size_t i = 1; i <= strlen(tmp); i++) {
|
||||||
|
if (tmp[i] == '/' || tmp[i] == '\0') {
|
||||||
|
char ch = tmp[i];
|
||||||
|
tmp[i] = '\0';
|
||||||
|
if (mkdir(tmp, 0755) != 0) {
|
||||||
|
struct stat st;
|
||||||
|
if (stat(tmp, &st) != 0 || !S_ISDIR(st.st_mode)) return -1;
|
||||||
|
}
|
||||||
|
tmp[i] = ch;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int write_bytes(const char *path, const unsigned char *buf, int len)
|
||||||
|
{
|
||||||
|
FILE *f = fopen(path, "wb");
|
||||||
|
if (!f) return -1;
|
||||||
|
int ok = fwrite(buf, 1, (size_t)len, f) == (size_t)len;
|
||||||
|
if (fclose(f) != 0) ok = 0;
|
||||||
|
return ok ? 0 : -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int touch_file(const char *path)
|
||||||
|
{
|
||||||
|
return write_bytes(path, (const unsigned char *)"x", 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int rm_rf(const char *path)
|
||||||
|
{
|
||||||
|
struct stat st;
|
||||||
|
if (lstat(path, &st) != 0) return -1;
|
||||||
|
if (!S_ISDIR(st.st_mode)) return unlink(path);
|
||||||
|
DIR *d = opendir(path);
|
||||||
|
if (!d) return -1;
|
||||||
|
struct dirent *de;
|
||||||
|
while ((de = readdir(d)) != NULL) {
|
||||||
|
if (strcmp(de->d_name, ".") == 0 || strcmp(de->d_name, "..") == 0) continue;
|
||||||
|
char full[MR_PATH_MAX];
|
||||||
|
snprintf(full, sizeof full, "%s/%s", path, de->d_name);
|
||||||
|
rm_rf(full);
|
||||||
|
}
|
||||||
|
closedir(d);
|
||||||
|
return rmdir(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- 头文件样例(只写头,不写像素) ---- */
|
||||||
|
static int write_png(const char *path, int w, int h)
|
||||||
|
{
|
||||||
|
unsigned char b[24] = {
|
||||||
|
0x89, 'P', 'N', 'G', 0x0D, 0x0A, 0x1A, 0x0A,
|
||||||
|
0, 0, 0, 13, 'I', 'H', 'D', 'R',
|
||||||
|
(unsigned char)(w >> 24), (unsigned char)(w >> 16),
|
||||||
|
(unsigned char)(w >> 8), (unsigned char)w,
|
||||||
|
(unsigned char)(h >> 24), (unsigned char)(h >> 16),
|
||||||
|
(unsigned char)(h >> 8), (unsigned char)h,
|
||||||
|
};
|
||||||
|
return write_bytes(path, b, (int)sizeof b);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int write_jpeg(const char *path, int w, int h)
|
||||||
|
{
|
||||||
|
unsigned char b[16] = {
|
||||||
|
0xFF, 0xD8, /* SOI */
|
||||||
|
0xFF, 0xC0, 0x00, 0x11, /* SOF0, len=17 */
|
||||||
|
0x08, /* precision */
|
||||||
|
(unsigned char)(h >> 8), (unsigned char)h,
|
||||||
|
(unsigned char)(w >> 8), (unsigned char)w,
|
||||||
|
0x03, 0x01, 0x11, 0x00, /* 3 分量(长度不足也无妨:只读到 w/h) */
|
||||||
|
};
|
||||||
|
return write_bytes(path, b, (int)sizeof b);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int write_bmp(const char *path, int w, int h)
|
||||||
|
{
|
||||||
|
unsigned char b[26] = { 0 };
|
||||||
|
b[0] = 'B';
|
||||||
|
b[1] = 'M';
|
||||||
|
b[18] = (unsigned char)w; b[19] = (unsigned char)(w >> 8);
|
||||||
|
b[20] = (unsigned char)(w >> 16); b[21] = (unsigned char)(w >> 24);
|
||||||
|
b[22] = (unsigned char)h; b[23] = (unsigned char)(h >> 8);
|
||||||
|
b[24] = (unsigned char)(h >> 16); b[25] = (unsigned char)(h >> 24);
|
||||||
|
return write_bytes(path, b, (int)sizeof b);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============================================================ */
|
||||||
|
/* R1/R2 页源契约 */
|
||||||
|
/* ============================================================ */
|
||||||
|
static void test_contract(void)
|
||||||
|
{
|
||||||
|
char tmpl[] = "./mr_reader_XXXXXX";
|
||||||
|
char *root = mkdtemp(tmpl);
|
||||||
|
CHECK(root != NULL, "mkdtemp");
|
||||||
|
if (!root) return;
|
||||||
|
|
||||||
|
char p[MR_PATH_MAX];
|
||||||
|
snprintf(p, sizeof p, "%s/page10.png", root); write_png(p, 600, 900);
|
||||||
|
snprintf(p, sizeof p, "%s/page2.jpg", root); write_jpeg(p, 1200, 1800);
|
||||||
|
snprintf(p, sizeof p, "%s/page1.bmp", root); write_bmp(p, 800, 1200);
|
||||||
|
snprintf(p, sizeof p, "%s/notes.txt", root); touch_file(p);
|
||||||
|
snprintf(p, sizeof p, "%s/.hidden.png", root); touch_file(p);
|
||||||
|
snprintf(p, sizeof p, "%s/sub.png", root); mkdir_p(p); /* 目录不参与成册 */
|
||||||
|
snprintf(p, sizeof p, "%s/sub/inside.png", root); touch_file(p);
|
||||||
|
|
||||||
|
mr_dir_book db;
|
||||||
|
mr_book book;
|
||||||
|
int n = mr_dir_book_init(&db, root, 640, fake_load, fake_unload, NULL);
|
||||||
|
CHECK(n == 3, "init count=%d (want 3)", n);
|
||||||
|
CHECK(db.count == 3, "db.count=%d", db.count);
|
||||||
|
mr_dir_book_bind(&book, &db);
|
||||||
|
|
||||||
|
CHECK(mr_book_valid(&book) == 1, "book valid");
|
||||||
|
CHECK(mr_book_count(&book) == 3, "book count=3");
|
||||||
|
CHECK(mr_book_count(NULL) == 0, "NULL count safe");
|
||||||
|
CHECK(mr_book_valid(NULL) == 0, "NULL invalid");
|
||||||
|
CHECK(mr_book_open(NULL, 0) == NULL, "NULL open safe");
|
||||||
|
mr_book_close(NULL, NULL); /* 全程 NULL 安全,不崩 */
|
||||||
|
mr_book_destroy(NULL);
|
||||||
|
|
||||||
|
/* 自然序 + 首末页 */
|
||||||
|
char path[MR_PATH_MAX];
|
||||||
|
mr_dir_book_path(&db, 0, path, sizeof path);
|
||||||
|
CHECK(strstr(path, "page1.bmp") != NULL, "page 0 = page1 got %s", path);
|
||||||
|
mr_dir_book_path(&db, 1, path, sizeof path);
|
||||||
|
CHECK(strstr(path, "page2.jpg") != NULL, "page 1 = page2 got %s", path);
|
||||||
|
mr_dir_book_path(&db, 2, path, sizeof path);
|
||||||
|
CHECK(strstr(path, "page10.png") != NULL, "page 2 = page10 got %s", path);
|
||||||
|
|
||||||
|
/* 越界:open/meta/path 全部安全 */
|
||||||
|
CHECK(mr_book_open(&book, -1) == NULL, "open(-1) NULL");
|
||||||
|
CHECK(mr_book_open(&book, 3) == NULL, "open(count) NULL");
|
||||||
|
CHECK(mr_book_open(&book, 9999) == NULL, "open(9999) NULL");
|
||||||
|
int mw = -1, mh = -1;
|
||||||
|
CHECK(mr_book_meta(&book, -1, &mw, &mh) == 0 && mw == 0 && mh == 0, "meta(-1) unknown");
|
||||||
|
CHECK(mr_book_meta(&book, 3, &mw, &mh) == 0, "meta(count) unknown");
|
||||||
|
mr_dir_book_path(&db, 9, path, sizeof path);
|
||||||
|
CHECK(path[0] == '\0', "path out of range empty");
|
||||||
|
|
||||||
|
/* 首末页 open + 重复 open-close(每次 open 的句柄独立,各 close 一次) */
|
||||||
|
int loads0 = g_loads, frees0 = g_frees;
|
||||||
|
meow_img *a = mr_book_open(&book, 0);
|
||||||
|
CHECK(a != NULL, "open(0) ok");
|
||||||
|
CHECK(strstr(g_last_path, "page1.bmp") != NULL, "open(0) path %s", g_last_path);
|
||||||
|
CHECK(g_last_maxdim == 640, "open passes maxdim=%d", g_last_maxdim);
|
||||||
|
meow_img *a2 = mr_book_open(&book, 0);
|
||||||
|
CHECK(a2 != NULL && a2 != a, "re-open(0) gives independent handle");
|
||||||
|
CHECK(g_loads == loads0 + 2, "two loads");
|
||||||
|
mr_book_close(&book, a);
|
||||||
|
mr_book_close(&book, a2);
|
||||||
|
CHECK(g_frees == frees0 + 2, "frees=%d", g_frees - frees0);
|
||||||
|
|
||||||
|
/* 再开再关一轮:契约允许同一页反复 open/close */
|
||||||
|
a = mr_book_open(&book, 0);
|
||||||
|
CHECK(a != NULL && g_loads == loads0 + 3, "re-open after close");
|
||||||
|
mr_book_close(&book, a);
|
||||||
|
CHECK(g_frees == frees0 + 3, "re-close frees");
|
||||||
|
|
||||||
|
meow_img *last = mr_book_open(&book, 2);
|
||||||
|
CHECK(last != NULL, "open(last) ok");
|
||||||
|
CHECK(strstr(g_last_path, "page10.png") != NULL, "open(last) path %s", g_last_path);
|
||||||
|
mr_book_close(&book, last);
|
||||||
|
|
||||||
|
/* meta:PNG/JPEG/BMP 头解析 */
|
||||||
|
CHECK(mr_book_meta(&book, 0, &mw, &mh) == 1 && mw == 800 && mh == 1200,
|
||||||
|
"meta bmp=%dx%d", mw, mh);
|
||||||
|
CHECK(mr_book_meta(&book, 1, &mw, &mh) == 1 && mw == 1200 && mh == 1800,
|
||||||
|
"meta jpeg=%dx%d", mw, mh);
|
||||||
|
CHECK(mr_book_meta(&book, 2, &mw, &mh) == 1 && mw == 600 && mh == 900,
|
||||||
|
"meta png=%dx%d", mw, mh);
|
||||||
|
|
||||||
|
/* R2:契约对条漫通用——用 meta 累加高度,按滚动位置定位页(M7 可直接复用)。 */
|
||||||
|
long y = 0, heights[3];
|
||||||
|
for (int i = 0; i < 3; i++) {
|
||||||
|
int sw = 0, sh = 0;
|
||||||
|
mr_book_meta(&book, i, &sw, &sh);
|
||||||
|
int fw = 0, fh = 0;
|
||||||
|
mr_nav_fit_size(MR_FIT_WIDTH, sw, sh, 480, 720, &fw, &fh);
|
||||||
|
heights[i] = fh;
|
||||||
|
y += fh;
|
||||||
|
}
|
||||||
|
CHECK(y == 720 + 720 + 720, "column height=%ld", y);
|
||||||
|
CHECK(heights[0] == 720 && heights[1] == 720, "meta-derived fit heights");
|
||||||
|
|
||||||
|
/* 销毁:provider 失效,二次 destroy 幂等 */
|
||||||
|
mr_book_destroy(&book);
|
||||||
|
CHECK(mr_book_valid(&book) == 0, "destroyed invalid");
|
||||||
|
CHECK(mr_book_open(&book, 0) == NULL, "open after destroy NULL");
|
||||||
|
mr_book_destroy(&book);
|
||||||
|
mr_dir_book_free(&db);
|
||||||
|
|
||||||
|
rm_rf(root);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_empty_and_bad(void)
|
||||||
|
{
|
||||||
|
char tmpl[] = "./mr_reader_empty_XXXXXX";
|
||||||
|
char *root = mkdtemp(tmpl);
|
||||||
|
CHECK(root != NULL, "mkdtemp empty");
|
||||||
|
|
||||||
|
/* 空目录:可读但 0 页(≠ 错误) */
|
||||||
|
mr_dir_book db;
|
||||||
|
int n = mr_dir_book_init(&db, root, 720, fake_load, fake_unload, NULL);
|
||||||
|
CHECK(n == 0, "empty dir count=0 got %d", n);
|
||||||
|
CHECK(db.count == 0, "empty db count");
|
||||||
|
mr_book book;
|
||||||
|
mr_dir_book_bind(&book, &db);
|
||||||
|
CHECK(mr_book_count(&book) == 0, "empty book count 0");
|
||||||
|
CHECK(mr_book_open(&book, 0) == NULL, "empty open NULL");
|
||||||
|
CHECK(mr_book_meta(&book, 0, NULL, NULL) == 0, "empty meta NULL-safe");
|
||||||
|
mr_dir_book_free(&db);
|
||||||
|
|
||||||
|
/* 目录不存在:-1,且 db 可安全 free */
|
||||||
|
mr_dir_book bad;
|
||||||
|
CHECK(mr_dir_book_init(&bad, "/no/such/dir/tuku-reader", 720, fake_load, fake_unload,
|
||||||
|
NULL) == -1, "missing dir -> -1");
|
||||||
|
CHECK(bad.count == 0 && bad.names == NULL, "missing dir cleared");
|
||||||
|
mr_dir_book_free(&bad);
|
||||||
|
|
||||||
|
/* 参数非法 */
|
||||||
|
CHECK(mr_dir_book_init(NULL, root, 720, fake_load, fake_unload, NULL) == -1, "NULL db");
|
||||||
|
CHECK(mr_dir_book_init(&bad, root, 720, NULL, fake_unload, NULL) == -1, "NULL load");
|
||||||
|
CHECK(mr_dir_book_init(&bad, root, 720, fake_load, NULL, NULL) == -1, "NULL unload");
|
||||||
|
CHECK(mr_dir_book_init(&bad, "", 720, fake_load, fake_unload, NULL) == -1, "empty dirname");
|
||||||
|
|
||||||
|
rm_rf(root);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============================================================ */
|
||||||
|
/* R4 方向语义 */
|
||||||
|
/* ============================================================ */
|
||||||
|
static void test_direction_nav(void)
|
||||||
|
{
|
||||||
|
const int W = 480;
|
||||||
|
CHECK(mr_nav_tap_zone(0, W) == MR_ZONE_LEFT, "zone 0");
|
||||||
|
CHECK(mr_nav_tap_zone(159, W) == MR_ZONE_LEFT, "zone 159");
|
||||||
|
CHECK(mr_nav_tap_zone(160, W) == MR_ZONE_MIDDLE, "zone 160");
|
||||||
|
CHECK(mr_nav_tap_zone(319, W) == MR_ZONE_MIDDLE, "zone 319");
|
||||||
|
CHECK(mr_nav_tap_zone(320, W) == MR_ZONE_RIGHT, "zone 320");
|
||||||
|
CHECK(mr_nav_tap_zone(479, W) == MR_ZONE_RIGHT, "zone 479");
|
||||||
|
CHECK(mr_nav_tap_zone(10, 0) == MR_ZONE_MIDDLE, "zone bad width");
|
||||||
|
|
||||||
|
/* RTL(默认):屏左=下一页,屏右=上一页 */
|
||||||
|
CHECK(mr_nav_tap_delta(1, MR_ZONE_LEFT) == 1, "RTL left=next");
|
||||||
|
CHECK(mr_nav_tap_delta(1, MR_ZONE_RIGHT) == -1, "RTL right=prev");
|
||||||
|
CHECK(mr_nav_tap_delta(1, MR_ZONE_MIDDLE) == 0, "RTL middle=chrome");
|
||||||
|
/* LTR:屏左=上一页,屏右=下一页 */
|
||||||
|
CHECK(mr_nav_tap_delta(0, MR_ZONE_LEFT) == -1, "LTR left=prev");
|
||||||
|
CHECK(mr_nav_tap_delta(0, MR_ZONE_RIGHT) == 1, "LTR right=next");
|
||||||
|
CHECK(mr_nav_tap_delta(0, MR_ZONE_MIDDLE) == 0, "LTR middle=chrome");
|
||||||
|
|
||||||
|
/* 拖动:向右(dx>0)露出左侧那页 */
|
||||||
|
CHECK(mr_nav_swipe_delta(1, 120) == 1, "RTL drag right=next");
|
||||||
|
CHECK(mr_nav_swipe_delta(1, -120) == -1, "RTL drag left=prev");
|
||||||
|
CHECK(mr_nav_swipe_delta(0, 120) == -1, "LTR drag right=prev");
|
||||||
|
CHECK(mr_nav_swipe_delta(0, -120) == 1, "LTR drag left=next");
|
||||||
|
CHECK(mr_nav_swipe_delta(1, 0) == 0, "drag 0 no-op");
|
||||||
|
|
||||||
|
CHECK(mr_nav_clamp_page(-5, 10) == 0, "clamp low");
|
||||||
|
CHECK(mr_nav_clamp_page(20, 10) == 9, "clamp high");
|
||||||
|
CHECK(mr_nav_clamp_page(3, 10) == 3, "clamp inside");
|
||||||
|
CHECK(mr_nav_clamp_page(0, 0) == -1, "clamp empty");
|
||||||
|
|
||||||
|
CHECK(mr_nav_commit_dist(480) == 120, "commit dist 480 -> 120");
|
||||||
|
CHECK(mr_nav_commit_dist(200) == 60, "commit dist 200 -> 60");
|
||||||
|
CHECK(mr_nav_commit_dist(2000) == 160, "commit dist 2000 -> 160");
|
||||||
|
CHECK(mr_nav_commit(119, 480) == 0, "119 not committed");
|
||||||
|
CHECK(mr_nav_commit(120, 480) == 1, "120 committed");
|
||||||
|
CHECK(mr_nav_commit(-120, 480) == 1, "-120 committed");
|
||||||
|
|
||||||
|
/* 进场方向:RTL 下一页从左侧进,LTR 下一页从右侧进 */
|
||||||
|
CHECK(mr_nav_slide_from(1, 1, W) == -W, "RTL next enters left");
|
||||||
|
CHECK(mr_nav_slide_from(1, -1, W) == W, "RTL prev enters right");
|
||||||
|
CHECK(mr_nav_slide_from(0, 1, W) == W, "LTR next enters right");
|
||||||
|
CHECK(mr_nav_slide_from(0, -1, W) == -W, "LTR prev enters left");
|
||||||
|
CHECK(mr_nav_slide_from(1, 0, W) == 0, "slide 0");
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============================================================ */
|
||||||
|
/* R5 fit / 缩放 / 平移 */
|
||||||
|
/* ============================================================ */
|
||||||
|
static void test_fit_zoom_pan(void)
|
||||||
|
{
|
||||||
|
int w = 0, h = 0;
|
||||||
|
/* 竖图 1000×1500,视口 480×720 */
|
||||||
|
CHECK(mr_nav_fit_size(MR_FIT_WIDTH, 1000, 1500, 480, 720, &w, &h) == 0 &&
|
||||||
|
w == 480 && h == 720, "fit width %dx%d", w, h);
|
||||||
|
CHECK(mr_nav_fit_size(MR_FIT_HEIGHT, 1000, 1500, 480, 720, &w, &h) == 0 &&
|
||||||
|
w == 480 && h == 720, "fit height %dx%d", w, h);
|
||||||
|
CHECK(mr_nav_fit_size(MR_FIT_ORIGINAL, 1000, 1500, 480, 720, &w, &h) == 0 &&
|
||||||
|
w == 1000 && h == 1500, "fit original %dx%d", w, h);
|
||||||
|
|
||||||
|
/* 横图 1500×1000:适宽缩到 480×320,适高放大到 1080×720(可左右平移) */
|
||||||
|
CHECK(mr_nav_fit_size(MR_FIT_WIDTH, 1500, 1000, 480, 720, &w, &h) == 0 &&
|
||||||
|
w == 480 && h == 320, "landscape fit width %dx%d", w, h);
|
||||||
|
CHECK(mr_nav_fit_size(MR_FIT_HEIGHT, 1500, 1000, 480, 720, &w, &h) == 0 &&
|
||||||
|
w == 1080 && h == 720, "landscape fit height %dx%d", w, h);
|
||||||
|
|
||||||
|
CHECK(mr_nav_fit_size(MR_FIT_WIDTH, 0, 100, 480, 720, &w, &h) == -1, "fit bad src");
|
||||||
|
CHECK(mr_nav_fit_size(MR_FIT_WIDTH, 100, 100, 0, 720, &w, &h) == -1, "fit bad viewport");
|
||||||
|
CHECK(mr_nav_fit_size(MR_FIT_WIDTH, 100, 100, 480, 720, NULL, &h) == -1, "fit NULL out");
|
||||||
|
|
||||||
|
CHECK(mr_nav_zoom_size(480, 720, 200, &w, &h) == 0 && w == 960 && h == 1440,
|
||||||
|
"zoom 200 %dx%d", w, h);
|
||||||
|
CHECK(mr_nav_zoom_size(480, 720, 150, &w, &h) == 0 && w == 720 && h == 1080,
|
||||||
|
"zoom 150 %dx%d", w, h);
|
||||||
|
CHECK(mr_nav_zoom_size(0, 720, 200, &w, &h) == -1, "zoom bad size");
|
||||||
|
|
||||||
|
int lx = -1, ly = -1;
|
||||||
|
mr_nav_pan_limit(960, 1440, 480, 720, &lx, &ly);
|
||||||
|
CHECK(lx == 240 && ly == 360, "pan limit %d,%d", lx, ly);
|
||||||
|
mr_nav_pan_limit(480, 720, 480, 720, &lx, &ly);
|
||||||
|
CHECK(lx == 0 && ly == 0, "no overflow no pan");
|
||||||
|
|
||||||
|
int px = 9999, py = -9999;
|
||||||
|
mr_nav_pan_clamp(960, 1440, 480, 720, &px, &py);
|
||||||
|
CHECK(px == 240 && py == -360, "pan clamp %d,%d", px, py);
|
||||||
|
px = 10; py = 20;
|
||||||
|
mr_nav_pan_clamp(480, 720, 480, 720, &px, &py);
|
||||||
|
CHECK(px == 0 && py == 0, "pan clamp no overflow");
|
||||||
|
|
||||||
|
CHECK(mr_nav_fit_next(MR_FIT_WIDTH) == MR_FIT_HEIGHT, "fit cycle w->h");
|
||||||
|
CHECK(mr_nav_fit_next(MR_FIT_HEIGHT) == MR_FIT_ORIGINAL, "fit cycle h->o");
|
||||||
|
CHECK(mr_nav_fit_next(MR_FIT_ORIGINAL) == MR_FIT_WIDTH, "fit cycle o->w");
|
||||||
|
CHECK(mr_nav_fit_next(99) == MR_FIT_WIDTH, "fit cycle bad");
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============================================================ */
|
||||||
|
/* R3 图片头探测 */
|
||||||
|
/* ============================================================ */
|
||||||
|
static void test_probe(void)
|
||||||
|
{
|
||||||
|
char tmpl[] = "./mr_reader_probe_XXXXXX";
|
||||||
|
char *root = mkdtemp(tmpl);
|
||||||
|
CHECK(root != NULL, "mkdtemp probe");
|
||||||
|
if (!root) return;
|
||||||
|
|
||||||
|
char p[MR_PATH_MAX];
|
||||||
|
int w = 0, h = 0;
|
||||||
|
snprintf(p, sizeof p, "%s/a.png", root);
|
||||||
|
write_png(p, 1234, 5678);
|
||||||
|
CHECK(mr_probe_size(p, &w, &h) == 1 && w == 1234 && h == 5678, "probe png %dx%d", w, h);
|
||||||
|
|
||||||
|
snprintf(p, sizeof p, "%s/a.jpg", root);
|
||||||
|
write_jpeg(p, 1600, 2400);
|
||||||
|
CHECK(mr_probe_size(p, &w, &h) == 1 && w == 1600 && h == 2400, "probe jpeg %dx%d", w, h);
|
||||||
|
|
||||||
|
snprintf(p, sizeof p, "%s/a.bmp", root);
|
||||||
|
write_bmp(p, 640, 480);
|
||||||
|
CHECK(mr_probe_size(p, &w, &h) == 1 && w == 640 && h == 480, "probe bmp %dx%d", w, h);
|
||||||
|
|
||||||
|
snprintf(p, sizeof p, "%s/notes.txt", root);
|
||||||
|
touch_file(p);
|
||||||
|
CHECK(mr_probe_size(p, &w, &h) == 0 && w == 0 && h == 0, "probe unknown");
|
||||||
|
CHECK(mr_probe_size("/no/such/file.png", &w, &h) == 0, "probe missing");
|
||||||
|
CHECK(mr_probe_size(NULL, &w, &h) == 0, "probe NULL");
|
||||||
|
|
||||||
|
rm_rf(root);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============================================================ */
|
||||||
|
/* R6 阅读方向状态 */
|
||||||
|
/* ============================================================ */
|
||||||
|
static void test_direction_state(void)
|
||||||
|
{
|
||||||
|
mr_reader_set_direction(MR_DIR_RTL);
|
||||||
|
CHECK(mr_reader_get_direction() == MR_DIR_RTL, "default RTL");
|
||||||
|
mr_reader_set_direction(MR_DIR_LTR);
|
||||||
|
CHECK(mr_reader_get_direction() == MR_DIR_LTR, "switch LTR");
|
||||||
|
mr_reader_set_direction(42);
|
||||||
|
CHECK(mr_reader_get_direction() == MR_DIR_LTR, "invalid ignored");
|
||||||
|
mr_reader_set_direction(MR_DIR_RTL);
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(void)
|
||||||
|
{
|
||||||
|
test_contract();
|
||||||
|
test_empty_and_bad();
|
||||||
|
test_probe();
|
||||||
|
test_direction_nav();
|
||||||
|
test_fit_zoom_pan();
|
||||||
|
test_direction_state();
|
||||||
|
printf("%s: %d passed, %d failed\n", g_fail ? "FAIL" : "PASS", g_pass, g_fail);
|
||||||
|
return g_fail ? 1 : 0;
|
||||||
|
}
|
||||||
+1043
-68
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,19 @@
|
|||||||
|
/* ui/reader/reader.h — 阅读页给 app.c 的事件接缝(跟手拖动/平移)。
|
||||||
|
*
|
||||||
|
* app.c 只做最小路由:ACT_READER 时的 on_touch_down/move 转给这里(镜像 library 分支)。
|
||||||
|
* 其余入口(on_tap / view on_event / mr_reader_open_dir)仍走 tuku_internal.h 的既有声明。
|
||||||
|
*/
|
||||||
|
#ifndef TUKU_UI_READER_READER_H
|
||||||
|
#define TUKU_UI_READER_READER_H
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
|
void reader_on_touch_down(int x, int y);
|
||||||
|
void reader_on_touch_move(int x, int y);
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
#endif /* TUKU_UI_READER_READER_H */
|
||||||
Reference in new issue
Block a user