Files
MeowOS-SDK/tuku/logic/reader/page_source.h
T
AskaEth 11e09af7a4 图库: 修 M27 评审 P1 门控旁路 + 三个 P2(r1)
P1-1:判定为 backend 的页不再无条件放行 + 后端中途失败不再静默回退 stb。
- meow_img_load_file_step_ex 增加 allow_stb_fallback 参数:0 = 只试后端,
  失败返回 NULL(坏图占位),绝不回落 stb。阅读器按 dec.path 传入。
- 旧行为实测:截断 12MP JPEG 18,403kB、3000×3000 24bpp 头-only BMP 36,056kB、
  非法位深 PNG 16,409kB(stb 都"成功"出图)。修复后:
  截断 JPEG 275kB→NULL、头-only BMP 908kB→NULL、非法位深 PNG 门控占位(0 分配)。

P2-1:PNG 支持矩阵补 png_depth_ok 同式(color 2/4/6 仅 8/16;color 3 无 16;
color 0 允许 1/2/4/8/16),不再出现「矩阵说支持、后端拒绝」。
P2-2:后端命中页也保留两道安全阀:MemAvailable<6MB 占位、估算 >12MB 硬上限占位。
P2-3:B8 指标改为「窗口稳定后只 reset 一次」,量整个 30 翻窗口内相对稳态基线的
最大存活增量(淘汰不再掩盖峰值),并打印稳态/驻留/增量。
P2-4:page_source.h 的估算精度口径限定为「后端命中/典型页」。

回归:
- B7c 新增截断 12MP JPEG / 24bpp 头-only BMP / 非法位深 PNG,断言 backend-only
  NULL 且峰值 ≤3MB、或 stb 门控占位;B7 典型页与 B7b 回退矩阵保持全绿。
- test_reader_many_pages 恢复 24bpp 头-only 夹具(1bpp 保留):24bpp 判 failed、
  1bpp 判 skipped;断言 failed=2 / skipped=1。
- run_tests.sh / run_webtoon_tests.sh EXIT=0;sdk/test_meow_img_stream ALL PASS;
  交叉构建(SDK + tuku)零警告。
2026-11-06 11:46:16 +08:00

247 lines
14 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* 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 <stddef.h> /* 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 的比较基准) */
} 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; /* 页原始宽/高(按需解压头探测;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 */