- 页源契约(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 零警告
453 lines
18 KiB
C
453 lines
18 KiB
C
/* 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;
|
||
}
|