/* 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 */ #include /* size_t(mr_probe_size_mem) */ #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); /* 同上,但从内存缓冲探测(档案页解压后不需要落盘再读一遍)。 */ int mr_probe_size_mem(const void *buf, size_t len, int *w, int *h); /* ---- 追加(M22):解码内存预算需要的头部信息(仍然零像素内存) ---- * 解析器按**内容**嗅探(PNG → JPEG → BMP),不信任扩展名:一个 .jpg 后缀的 PNG 也能 * 得到正确结论。planes 是 stb 解码时**除最终 RGBA 输出缓冲以外**的中间面字节数: * - JPEG:各分量面 w2*h2(按 MCU 对齐 + 采样因子,与 stb 同式),渐进式再加 short 系数面; * - PNG :源像素缓冲 ch*W*H(16bit 再 ×2,ch==4 时就是输出缓冲本身,故记 0); * - BMP :0(stb 对 24/32bpp 直接写 RGBA 目标,不另开源缓冲)。 * 调用方(阅读器)再叠加「抽取输出 + 精修/落框临时图」得到真实峰值。 * 1=已知(info 已填满),0=未知/损坏(info 清零且 codec=MR_IMG_CODEC_UNKNOWN)。 */ #define MR_IMG_CODEC_UNKNOWN 0 #define MR_IMG_CODEC_JPEG 1 #define MR_IMG_CODEC_PNG 2 #define MR_IMG_CODEC_BMP 3 typedef struct mr_img_info { int w, h; int codec; /* MR_IMG_CODEC_* */ int progressive; /* 1=渐进式 JPEG(SOF2/SOF6/SOF10/SOF14) */ int channels; /* 源通道数(JPEG 分量数 / PNG 颜色类型 / BMP 位深/8) */ long long planes; /* 见上:stb 中间面字节(不含最终 RGBA 输出) */ /* ---- 追加(M27):真缩解后端(M23)支持判定所需的头字段。 * 缺省 0 = 未知;判定按「不支持」处理(保守回退 stb 门控),绝不因缺字段误走后端。 ---- */ int depth; /* JPEG 精度 / PNG 位深 / BMP 位深(bpp) */ int color; /* PNG 颜色类型(0/2/3/4/6);非 PNG 为 0 */ int sof; /* JPEG SOF marker(0xC0/0xC1/0xC2 才可能走后端) */ int interlace; /* PNG 1=Adam7 交错(后端不支持) */ int compression; /* PNG 压缩法 / BMP BI_*(0 才可能走后端) */ int filter; /* PNG 滤波法(0 才可能走后端) */ int dib; /* BMP DIB 头长度(<40 后端不支持) */ int ncomp; /* JPEG 分量数(1..3;4=CMYK → 后端不支持) */ unsigned char comp_h[3], comp_v[3], comp_tq[3]; /* JPEG 采样因子/量化表号 */ } mr_img_info; int mr_probe_info(const char *path, mr_img_info *info); int mr_probe_info_mem(const void *buf, size_t len, mr_img_info *info); /* ---- 追加(M27):解码峰值模型 + 门控的唯一实现 ---- * 阅读器(ui/reader)与页源(本层)都只调 reader_estimate_peak,不得各写一份模型。 * 一次调用同时回答三件事: * path 走后端(MR_DECODE_PATH_BACKEND)还是回退 stb(MR_DECODE_PATH_STB); * skip 是否占位(MR_DECODE_OK / SKIP_LOWMEM / SKIP_PEAK); * peak_bytes + tw/th 预估峰值与产出尺寸(日志/测试直接对照实测)。 * * 判定顺序(M27 核心设计): * 1. 头部特征满足 M23 后端支持矩阵(JPEG SOF0/1/2、≤3 分量、8bit、N=1..7; * PNG 非交错/标准压缩;BMP DIB≥40/BI_RGB/8·16·24·32bpp)→ **走后端**: * 峰值 = 目标图字节 + 后端工作面 + 固定开销,与源分辨率无关 → 不占位(不做内存门控)。 * 2. 否则回退 stb:按 M22 的 stb 真实分配模型算峰值,超预算 → SKIP_PEAK; * MemAvailable < 6MB → SKIP_LOWMEM。这两道门控只作用于回退路径。 * * 参数:step 与 meow_img_load_file_step 同义(JPEG 档位提示);box_w/box_h 是阅读器 * 最终显示框;memfree_kb/memavail_kb 读不到时传 -1。out 必须非 NULL。 * 估算精度口径:**后端命中页 / 典型页**保证估算 ≥ 实测(--wrap malloc)且 ≤1.5×; * 回退 stb 的小图只保证门控安全(可能保守,不强求 1.5× 上界)。断言见 tuku/test。 */ #define MR_DECODE_PATH_STB 0 #define MR_DECODE_PATH_BACKEND 1 #define MR_DECODE_OK 0 #define MR_DECODE_SKIP_LOWMEM 1 #define MR_DECODE_SKIP_PEAK 2 typedef struct mr_decode_decision { int path; /* MR_DECODE_PATH_* */ int skip; /* MR_DECODE_OK / SKIP_* */ int step; /* 实际档位(<=1 按 1) */ int tw, th; /* 预估最终产出尺寸(含阅读器落框精修) */ long long peak_bytes; /* 预估峰值(字节,含固定开销) */ long long budget_bytes; /* 本次预算(字节;SKIP_PEAK 的比较基准) */ /* ---- 追加(M32):页槽改 RGB565 后的「落槽暂存」---- * 解码仍产出 RGBA 的 meow_img;阅读器把它一次性转成 565 再释放 RGBA,转换期间 * RGBA(4B/px)+ 目标 565(2B/px)同时存在。这笔暂存(= 2×tw×th)与解码峰值不同时 * 发生(解码中间面此时已释放),但同属「一次取页操作」的内存,必须一起进预算: * page_source 的门控按 peak_bytes + convert_bytes 比较,阅读器也按两者之和做日志/断言 * (与 peak_bytes 分开,保持解码模型的原有口径)。 */ long long convert_bytes; } mr_decode_decision; void reader_estimate_peak(const mr_img_info *in, int step, int box_w, int box_h, long memfree_kb, long memavail_kb, mr_decode_decision *out); /* 预算策略(reader_estimate_peak 内部用的就是它):常态 3MB;MemFree 富余时放宽到 * min(12MB, MemFree-4MB);读不到 MemFree(<0)只用常态。暴露给日志/测试,避免复制。 */ long long reader_peak_budget_bytes(long memfree_kb); /* ============================================================ */ /* 档案册(M11):.cbz / .zip 当一本书 */ /* ------------------------------------------------------------ * 目录册之外的第二种 mr_book 实现,契约完全相同。条目名按 zip 原始字节 * 透传(通常 UTF-8):只收「图片后缀 + 路径安全 + 非隐藏 + 非目录」的条目, * 跨子目录按 mr_natural_cmp 自然排序。open(i) 先把条目解压进 * logic/reader/page_cache(磁盘 LRU),再用注入的解码回调按路径解码—— * 与目录册走同一条 UI 解码/预算路径,翻页与条漫两种模式零改动复用。 * * 条目名永远不会被拼成文件系统路径(缓存文件名是哈希),路径安全过滤是 * 纵深防御:拒绝绝对路径、'..' 段、反斜杠与内嵌 NUL。 * ============================================================ */ #define MR_ZIP_MAX_ENTRY_NAME 1024 /* 超长条目名跳过(防病态包) */ #define MR_ZIP_PAGE_MAX_BYTES (64LL * 1024 * 1024) /* 单页解压上限(防 zip 炸弹) */ typedef struct mr_zip_book { char path[MR_PATH_MAX]; void *arch; /* mz_archive*(zip 层句柄;不对外暴露) */ void *lock; /* pthread_mutex_t*:mz_read* 串行化(open/meta 共用) */ int *order; /* 页 i → zip 条目下标(堆分配) */ int *meta_w; /* 页原始宽/高(按需读条目 4KB 前缀探测;NULL=未探测) */ int *meta_h; int count; int maxdim; long long mtime; /* 档案 mtime/size:解压缓存键的一部分 */ long long size; mr_book_load_fn load; /* 可为 NULL(只取封面/页路径时不需要解码) */ mr_book_unload_fn unload; void *ud; } mr_zip_book; /* 打开档案并建立页表。返回页数(>=0);参数非法/坏档/空档案不可读返回 -1 * (zb 已清空、可安全 free)。0 页的合法档案返回 0(zb 仍持有打开的 archive)。 */ int mr_zip_book_init(mr_zip_book *zb, const char *path, int maxdim, mr_book_load_fn load, mr_book_unload_fn unload, void *ud); void mr_zip_book_free(mr_zip_book *zb); /* 把档案册绑定到通用 mr_book(zb 生命周期由调用方持有)。 */ void mr_zip_book_bind(mr_book *b, mr_zip_book *zb); /* 第 index 页在 zip 内的条目名(原始字节);越界输出空串。 */ void mr_zip_book_page_name(const mr_zip_book *zb, int index, char *out, int outsz); /* 确保第 index 页已解压到缓存并返回缓存文件路径;越界/坏档/缓存不可用 -1。 */ int mr_zip_book_page_path(mr_zip_book *zb, int index, char *out, int outsz); /* 只数页(书库扫描/邻册判断用;不解压、不建缓存)。0=不是档案或没有图片页。 */ int mr_zip_count_pages(const char *path); /* 档案封面:档内自然序第一张图解到缓存,路径写入 out。1=成功。 */ int mr_zip_cover_path(const char *path, char *out, int outsz); /* 条目是否算一页(图片后缀 + 路径安全 + 非隐藏 + 非目录)。1=是。 */ int mr_zip_entry_is_page(const char *name, unsigned name_len); /* ============================================================ */ /* 统一开册(目录册 / 档案册) */ /* ------------------------------------------------------------ * 书库点入的行既可能是目录也可能是 .cbz/.zip;阅读器只调这里,不关心 provider。 * 句柄由调用方提供(阅读器用静态变量),生命周期 = 打开到 close_auto。 * ============================================================ */ #define MR_BOOK_KIND_NONE 0 #define MR_BOOK_KIND_DIR 1 #define MR_BOOK_KIND_ZIP 2 typedef struct mr_book_handle { int kind; union { mr_dir_book dir; mr_zip_book zip; } u; } mr_book_handle; /* 按路径自动选 provider 并绑定 b。返回页数(>=0)或 -1(打不开)。 * 页数为 0 时 b 不绑定(空册),句柄仍可安全 mr_book_close_auto。 */ int mr_book_open_auto(mr_book *b, mr_book_handle *h, const char *path, int maxdim, mr_book_load_fn load, mr_book_unload_fn unload, void *ud); /* 释放 provider(先 mr_book_destroy 再释放各自索引);可重复调用。 */ void mr_book_close_auto(mr_book *b, mr_book_handle *h); #ifdef __cplusplus } #endif #endif /* MR_PAGE_SOURCE_H */