From ee5796747df317a25e6674075c5204127a1b078e Mon Sep 17 00:00:00 2001 From: AskaEth Date: Fri, 6 Nov 2026 20:44:22 +0800 Subject: [PATCH 1/2] =?UTF-8?q?feat(sdk):=20M26=20=E5=85=AC=E5=BC=80?= =?UTF-8?q?=E4=BB=93=E7=9C=9F=E7=BC=A9=E8=A7=A3=E5=9B=BE=E5=83=8F=E6=A0=88?= =?UTF-8?q?=E5=B9=B6=E5=85=A5=E4=B8=BB=E4=BB=93=20=E2=80=94=20JPEG=20DCT/P?= =?UTF-8?q?NG=20=E9=80=90=E8=A1=8C/BMP=20=E8=B7=B3=E8=A1=8C=E5=90=8E?= =?UTF-8?q?=E7=AB=AF=20+=20RGB565=20=E9=A1=B5=E5=9B=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 主仓 SDK 的图片层原本是 220 行 stb 直通(整幅解码后重采样)。公开仓 MeowOS-SDK 的同一子系统已被真实 App(tuku 阅读器)驱动并做过峰值实测与像素比对,故以公开仓 为权威方向并入主仓 meow_ui.a,两个方向的新功能都保留(逐文件 diff 确认公开仓是 严格超集:无一处「双方互有增减」)。 - 新增 meow_jpeg.c / meow_jpeg.h:DCT 域缩解 1/8、1/4、1/2(N=1/2/4 点 IDCT), 渐进式只解到首个精细化扫描;不支持项(CMYK/YCCK、算术编码、过订阅 DHT、16bit 量化表)返回 0 由调用方回退 stb - 新增 meow_png_stream.c:libz 逐行 inflate + 反滤波,按步长丢行丢列直写目标尺寸; BMP 按目标行 seek + 行内跳列(8/16/24/32bpp) - meow_ui_img.c + include/meow_ui_image.h:新增 meow_img_pick_scale / load_file_step / load_file_step_ex / resample 四个公开入口;_dim 与 _mem_dim 先进后端,失败/不支持才回退 stb(对外语义不变) - include/meow_ui_draw.h + meow_ui_gfx.c:M32 RGB565 页图 5 函数 (meow_gfx_rgb565 / rgba_to_rgb565 / blit_rgb565 / _scaled)。gfx 保留主仓的 `#include "../meow_launcherd/meow_font.h"` 与 (公开仓自 vendored meow_font.h 且无 ,机械覆盖会丢这两行) - meow_jpeg.h 保持内部:不进 meow_ui.h 伞头,include/ 下无任何头暴露 meow_jpeg_* - build_sdk.sh:OBJS 插入 meow_jpeg / meow_png_stream(32→34);成员数 76→78(+meow_font.o +43 freetype),--verify 全为 ELF32 MIPS32 rel2/o32 - 测试:新增 test_meow_img_stream.c(S1-S16),test_meow_img_budget.c 同步公开仓 版(B9 断言收紧为 p_new <= p_old);22 个既有宿主测试的编译注释行补 meow_jpeg.c / meow_png_stream.c / -lz(meow_ui_img.c 现引用后端符号,不加即 undefined reference) 宿主实测:S1-S16 全绿(S7 与 stb 网格像素一致:JPEG mad 4.35-13.69、PNG/BMP mad 0.00;S9 回退矩阵 CMYK/1bpp BMP/Adam7 均拒绝且 stb 仍成功),B1-B9 全绿 (B9 PNG 峰值 25.65→0.61 MiB、BMP 14.65→0.59 MiB)。回归 test_meow_ui_widgets / scroll / ime_fix / test_umbrella 全过。zlib 在应用链接期解析:meow_ui.a 只留 inflate* 未定义符号,四个 App 构建脚本原本就带 $SYSROOT/lib/libz.a,静态 linkcheck(meow_ui.a + libz.a -lpthread)通过。 注:主仓 patch_v2/reader_app 仍自带 vendored stb、不用 meow_img_*,本单不改变 阅读器的内存占用;RGB565 页图接入 reader_app 是另一个任务。 --- patch_v2/sdk/build_sdk.sh | 2 +- patch_v2/sdk/demo_box_scroll.c | 4 +- patch_v2/sdk/include/meow_ui_draw.h | 18 + patch_v2/sdk/include/meow_ui_image.h | 55 +- patch_v2/sdk/meow_jpeg.c | 1005 ++++++++++++++++++ patch_v2/sdk/meow_jpeg.h | 50 + patch_v2/sdk/meow_png_stream.c | 499 +++++++++ patch_v2/sdk/meow_ui_gfx.c | 103 ++ patch_v2/sdk/meow_ui_img.c | 93 +- patch_v2/sdk/test_ime_fix.c | 6 +- patch_v2/sdk/test_meow_img_budget.c | 16 +- patch_v2/sdk/test_meow_img_color.c | 4 +- patch_v2/sdk/test_meow_img_stream.c | 1182 +++++++++++++++++++++ patch_v2/sdk/test_meow_lyric_view.c | 4 +- patch_v2/sdk/test_meow_md3.c | 4 +- patch_v2/sdk/test_meow_ui_anim.c | 4 +- patch_v2/sdk/test_meow_ui_box.c | 4 +- patch_v2/sdk/test_meow_ui_dispatch.c | 4 +- patch_v2/sdk/test_meow_ui_home_gesture.c | 4 +- patch_v2/sdk/test_meow_ui_iconbtn.c | 4 +- patch_v2/sdk/test_meow_ui_lazygrid.c | 4 +- patch_v2/sdk/test_meow_ui_listview_rowh.c | 4 +- patch_v2/sdk/test_meow_ui_listvirt.c | 4 +- patch_v2/sdk/test_meow_ui_scroll.c | 4 +- patch_v2/sdk/test_meow_ui_select.c | 4 +- patch_v2/sdk/test_meow_ui_swipe.c | 4 +- patch_v2/sdk/test_meow_ui_theme.c | 4 +- patch_v2/sdk/test_meow_ui_theme_export.c | 4 +- patch_v2/sdk/test_meow_ui_view_event.c | 4 +- patch_v2/sdk/test_meow_ui_widgets.c | 6 +- patch_v2/sdk/test_p1p2p3_repro.c | 4 +- patch_v2/sdk/test_pinyin.c | 2 +- 32 files changed, 3049 insertions(+), 64 deletions(-) create mode 100644 patch_v2/sdk/meow_jpeg.c create mode 100644 patch_v2/sdk/meow_jpeg.h create mode 100644 patch_v2/sdk/meow_png_stream.c create mode 100644 patch_v2/sdk/test_meow_img_stream.c diff --git a/patch_v2/sdk/build_sdk.sh b/patch_v2/sdk/build_sdk.sh index 90b4706..d4e65a3 100755 --- a/patch_v2/sdk/build_sdk.sh +++ b/patch_v2/sdk/build_sdk.sh @@ -44,7 +44,7 @@ FT_INCS="-I$FT_BUILD -I$FT_BUILD/include -I$FT_SRC/include" rm -f meow_ui.a *.o # -I../meow_launcherd:meow_notify_ipc.c 复用共享协议头 meow_ipc.h(同一份、杜绝两端漂移) -OBJS="meow_ui_time meow_ui_gfx meow_ui_log meow_ui_notify meow_notify_ipc meow_ui_img meow_ui_widget meow_ui_input meow_ui_theme meow_ui_ime meow_pinyin meow_ui_sys meow_ui_vol meow_ui_activity meow_ui_global meow_ui_dialog meow_ui_config meow_ui_draw meow_ui_view meow_ui_nav meow_ui_media meow_ui_audio stb_image meow_ui_box meow_ui_anim meow_ui_scroll meow_lazygrid meow_select meow_ui_str meow_ui_ft meow_net_ipc meow_cachegov" +OBJS="meow_ui_time meow_ui_gfx meow_ui_log meow_ui_notify meow_notify_ipc meow_ui_img meow_jpeg meow_png_stream meow_ui_widget meow_ui_input meow_ui_theme meow_ui_ime meow_pinyin meow_ui_sys meow_ui_vol meow_ui_activity meow_ui_global meow_ui_dialog meow_ui_config meow_ui_draw meow_ui_view meow_ui_nav meow_ui_media meow_ui_audio stb_image meow_ui_box meow_ui_anim meow_ui_scroll meow_lazygrid meow_select meow_ui_str meow_ui_ft meow_net_ipc meow_cachegov" CFLAGS="-Os -static -I$ROOT/patch_v2/meow_launcherd" for f in $OBJS; do echo "== cc $f ==" diff --git a/patch_v2/sdk/demo_box_scroll.c b/patch_v2/sdk/demo_box_scroll.c index c36e1ad..3b761a1 100644 --- a/patch_v2/sdk/demo_box_scroll.c +++ b/patch_v2/sdk/demo_box_scroll.c @@ -7,8 +7,8 @@ * meow_ui_gfx.c meow_ui_sys.c meow_ui_theme.c meow_ui_vol.c meow_ui_log.c \ * meow_ui_widget.c meow_ui_input.c meow_ui_activity.c meow_ui_dialog.c meow_ui_ime.c \ * meow_pinyin.c meow_ui_draw.c meow_ui_view.c meow_ui_box.c meow_ui_anim.c meow_ui_scroll.c \ - * meow_ui_img.c meow_ui_media.c meow_notify_ipc.c stb_image.c \ - * -I. -I../meow_launcherd -DMEOW_UI_HOST_TEST -pthread -lm + * meow_ui_img.c meow_ui_media.c meow_notify_ipc.c stb_image.c meow_jpeg.c meow_png_stream.c \ + * -I. -I../meow_launcherd -DMEOW_UI_HOST_TEST -pthread -lm -lz * 运行:MEOW_UI_DUMP=/tmp/demo_box.ppm ./demo_box_scroll */ #include "meow_ui.h" diff --git a/patch_v2/sdk/include/meow_ui_draw.h b/patch_v2/sdk/include/meow_ui_draw.h index e6ff303..21a2389 100644 --- a/patch_v2/sdk/include/meow_ui_draw.h +++ b/patch_v2/sdk/include/meow_ui_draw.h @@ -50,6 +50,24 @@ void meow_ui_flush_region(int x, int y, int w, int h); /* 只刷新指定区 * /* 半透明(blend565:前景按 alpha 与背景混叠) */ void meow_ui_fill_rect_alpha(int x, int y, int w, int h, uint32_t color, int alpha); /* 0-255 */ +/* ---- RGB565 页图(M32)---- + * 页槽常驻 RGB565(2 字节/像素):真机 480×853 页 1599KB → 800KB。 + * 量化与 back_buf 用同一个 565 转换(逐位截断),565 屏上最终像素与 RGBA blit + * 路径逐像素一致——不引入色偏、不会比现在多出色带。 + * 命中页(页图尺寸 == 显示尺寸)走 meow_gfx_blit_rgb565:每行一次拷贝,无重采样。 + * + * alpha:565 页图**按不透明**处理(alpha 被忽略)。漫画页解码后 alpha 恒为 255, + * 与旧路径等价;带透明像素的 PNG(调色板/tRNS/color type 6、或 32bpp BMP)会有可见 + * 差异:旧 RGBA 路径把透明像素混合到阅读器底色,565 路径直接画它的 RGB(通常是黑)。 + * 若将来要支持透明页,需要在上屏时按 alpha 混合(而不是在落槽时丢通道)。 */ +uint16_t meow_gfx_rgb565(uint32_t rgb); /* 24-bit RGB → 565(与 back_buf 同式) */ +void meow_gfx_rgba_to_rgb565(const uint8_t *rgba, uint16_t *out, int npx); /* 不透明像素(忽略 alpha) */ +/* 原生尺寸 blit(不透明):16bpp back_buf 上是每行一次 memcpy。 */ +void meow_gfx_blit_rgb565(int x, int y, int w, int h, const uint16_t *px); +/* 最近邻缩放 blit(几何不符时的兜底;采样点与 meow_img_resample 同式)。 */ +void meow_gfx_blit_rgb565_scaled(int x, int y, int w, int h, + int sw, int sh, const uint16_t *px); + /* ============================================================ */ /* §7 动画/时间(B1 最小可用) */ /* ============================================================ */ diff --git a/patch_v2/sdk/include/meow_ui_image.h b/patch_v2/sdk/include/meow_ui_image.h index 4427376..028a8f8 100644 --- a/patch_v2/sdk/include/meow_ui_image.h +++ b/patch_v2/sdk/include/meow_ui_image.h @@ -1,7 +1,8 @@ /* * meow_ui_image.h — SDK 图片层(§4,B1)。 * - * 原 meow_ui.h 拆分的一部分(image 模块)。内部 stb_image(单头文件,静态编入 meow_ui.a) + * 原 meow_ui.h 拆分的一部分(image 模块)。解码后端:M23 真缩解(JPEG DCT 域 / PNG 逐行 / + * BMP 跳行,见 meow_jpeg.h)优先,回退 stb_image(单头文件,静态编入 meow_ui.a)。 * 解 PNG/JPEG/BMP,统一转 RGBA。meow_img 为不透明句柄(前向声明见 meow_ui_types.h)。 */ #ifndef MEOW_UI_IMAGE_H @@ -26,14 +27,56 @@ void meow_img_free(meow_img *img); * App 可用它在加载前判断「图太大」并给用户提示,而不必先撞 OOM。 */ int meow_img_probe_file(const char *path, int *w, int *h, int *channels); -/* 追加(M12)预算解码:解码期受长边约束,返回图最长边 ≤ maxdim。 - * JPEG:按 1/2、1/4、1/8 整数步长抽取解码(stbi_load_scaled),整幅原尺寸 RGBA 缓冲从不物化, - * 峰值 ≈ 亮度分量平面 + 缩解输出 + 单行缓冲;随后用 meow_img_downscale 精修到 maxdim。 - * PNG/BMP 等 stb 无解码期缩放能力的格式:整幅解码后立即降采样(峰值同旧入口,见 M12 报告)。 - * 小图(长边 ≤ maxdim)走旧路径,与原 API 完全一致;maxdim<=0 等价旧入口。失败返回 NULL。 */ +/* 追加(M12/M23)预算解码:解码期受长边约束,返回图最长边 ≤ maxdim,尺寸与 + * 「meow_img_load_file + meow_img_downscale(maxdim)」逐字节同口径(公式一致)。 + * + * 实现(M23 起):先进真缩解后端(内部 meow_jpeg.c / meow_png_stream.c),解码期只算目标分辨率: + * JPEG:DCT 域缩解(每 8×8 块只解出 N×N 像素,N 由 8·maxdim/长边 取「不欠采样」的最小值; + * N=1..7,N≥8 交回 stb),渐进式跳过精细化扫描、只解各频带首次扫描 —— 低频带已完整, + * 不整幅驻留系数阵列; + * PNG :libz 逐行 inflate + 反滤波,按步长丢行丢列直写目标尺寸(gray/gray+A/RGB/RGBA/ + * 调色板/1-16bit,16bit 取高字节,tRNS 透明含 8bit 灰/RGB); + * BMP :按目标行 seek 到源行 + 行内跳列(8bpp 调色板 / 16bpp RGB555 / 24bpp / 32bpp)。 + * 峰值只与输出尺寸相关,不随源图分辨率/压缩体积线性增长(2000×3000 页 ≤2.5MB 量级)。 + * 不支持的特性(CMYK/YCCK 4 分量 JPEG、算术编码、过订阅 Huffman 表、16bit 量化表、Adam7 + * 交错 PNG、RLE/1-4bpp BMP、几乎等尺寸)自动回退 stb 旧路径,对外语义完全不变。 + * 小图(长边 ≤ maxdim)走旧路径,与原 API 完全一致;maxdim<=0 等价旧入口。失败返回 NULL。 + * + * 尺寸口径说明(M23 评审 P2-2):本入口按「原图 + meow_img_downscale」公式算目标尺寸;M12 旧 + * 实现经 stb 的 ceil(w/s) 缩解再精修,源尺寸不是档位整数倍时可能与本入口差 1px(实测 1999×2999: + * 旧 320×480 vs 新 319×480)。阅读器按显示框缩放,无实际影响;PNG/BMP 无此差异。 */ meow_img *meow_img_load_file_dim(const char *path, int maxdim); meow_img *meow_img_load_mem_dim(const void *buf, int len, int maxdim); +/* 追加(M22)缩解档位选择:取满足「长边/档位 ≥ maxdim」的最大整数档 s ∈ {1,2,4,8}, + * 保证缩解结果不欠采样。meow_img_load_file_dim 内部用的就是这个函数;调用方(阅读器) + * 用它枚举候选档位做内存预算,避免两处各写一份而漂移。 */ +int meow_img_pick_scale(int longside, int maxdim); + +/* 追加(M22)显式档位解码:语义与 meow_img_load_file_dim 相同(结果长边 ≤ maxdim, + * 超了就再精修一次),只是缩解档位由调用方决定。内存吃紧时可以选更粗的档位——输出更小、 + * 峰值更低,代价是细节变软。step <= 1 等价长边约束入口;step > 8 按 8 处理。 + * M23 起:先走真缩解后端(step 折算成目标长边 ceil(长边/step),后端按该长边选 DCT 阶), + * 峰值只与输出尺寸相关;不支持的特性回退 stb(step 原样传入)。 + * M27 起 PNG/BMP 也走同一后端入口(阅读器峰值模型已同步为「目标图 + 工作面」口径); + * 断言与实测见 tuku/test/test_reader_membudget.c 的 B7。 */ +meow_img *meow_img_load_file_step(const char *path, int maxdim, int step); + +/* 追加(M27):与 meow_img_load_file_step 相同,额外控制回退并回报**实际**路径: + * allow_stb_fallback = 1:后端不支持/失败时回退 stb(与 _step 完全一致); + * allow_stb_fallback = 0:只尝试后端,失败返回 NULL——**绝不**回落 stb。阅读器的门控 + * 按「后端峰值」放行时必须用 0,否则头部判定通过、解码中途失败的文件会静默走 stb, + * 实测可放出 18–36MB(截断 12MP JPEG / 头-only BMP / 非法位深 PNG)。失败页由调用方 + * 按坏图占位。 + * used_backend = 1(M23 后端直出目标尺寸)/ 0(未走成后端)。NULL 时等价 _step(允许回退)。 */ +meow_img *meow_img_load_file_step_ex(const char *path, int maxdim, int step, + int allow_stb_fallback, int *used_backend); + +/* 追加(M22)精确重采样到 w×h(最近邻,缩小/放大都支持)。阅读器用它把「缩解档位」的 + * 结果一次性落到显示框,使命中页仍是单次原生 blit(不做每帧重采样)。同尺寸返回 NULL + * (调用方保留原图);失败返回 NULL。 */ +meow_img *meow_img_resample(meow_img *im, int w, int h); + int meow_img_w(const meow_img *img); int meow_img_h(const meow_img *img); /* 访问器:取 RGBA 像素指针(4 通道 straight alpha;供取色/直读像素)。meow_img 对外 opaque, diff --git a/patch_v2/sdk/meow_jpeg.c b/patch_v2/sdk/meow_jpeg.c new file mode 100644 index 0000000..0330333 --- /dev/null +++ b/patch_v2/sdk/meow_jpeg.c @@ -0,0 +1,1005 @@ +/* + * meow_jpeg.c — M23 真缩解后端(一):JPEG DCT 域缩解 + 格式分派。 + * + * 背景(M20 审计实测):stb 的 stbi_load_scaled 是「先整幅解、再重采样」——baseline 分量面 + * 1.5·W·H、渐进式再加 3·W·H,2000×3000 baseline 峰值 +10.1MB、渐进 +23.5MB,真机 kill 点 + * 只剩 ~4.2MB 余量。本文件换成「解码期只算目标分辨率」: + * + * - 按目标显示尺寸选 DCT 缩解档 N ∈ {1,2,4}(1/8、1/4、1/2 分辨率),每 8×8 块只解出 + * N×N 个像素——N 点基函数是「8 点 IDCT 按像素区间盒式平均」的定点近似,DC 项恒为 F00/8, + * 与「整幅解完再降采样」结构一致(差异只在高频细节)。 + * - 系数边解边累加进分量平面,不留整幅系数阵列:baseline/渐进式内存足迹相同。 + * - 渐进式只解到首个精细化扫描(首个 Ah>0)为止:此时各频带的首次扫描(Ah=0)已到齐, + * 低频信息完整;再往后只是同一批系数的量化精度细化。因此**不需要**整幅系数的 + * successive-approximation 缓存——这正是 libjpeg 系列做不到 2.5MB 的根因 + * (渐进式必须整幅驻留 JBLOCK,≈1 字节/源像素/分量)。 + * - 输出直接是目标尺寸 RGBA,不再「缩解结果 + 精修输出」两份同时驻留。 + * + * 不支持 → 返回 0 让 meow_ui_img.c 回退 stb(不改变对外语义):4 分量 CMYK/YCCK、 + * 算术编码(SOF9/10)、无损(SOF3)、分层(SOF5-7,13-15)、12bit 精度。 + * + * 许可证:MeowOS 自写(MIT,随 SDK 发布);未引入第三方解码器源码。 + * 参考资料:ITU-T T.81 (JPEG) 语法与 Huffman 解码、libjpeg 的 marker/DQT/DHT 布局; + * 缩解基函数见文件内 mj_basis_* 的推导注释。 + */ +#include "meow_jpeg.h" + +#include +#include +#include +#include + +/* ================================================================== */ +/* 公共:目标尺寸(与 meow_img_downscale 同一公式,保证对外表现不变) */ +/* ================================================================== */ +void meow_img_target_dims(int w, int h, int maxdim, int *tw, int *th) +{ + if (w >= h) { + *tw = maxdim; + *th = (int)((long)h * maxdim / w); + } else { + *th = maxdim; + *tw = (int)((long)w * maxdim / h); + } + if (*tw < 1) *tw = 1; + if (*th < 1) *th = 1; +} + +/* ================================================================== */ +/* 字节源:文件流(4KB 环形读)或内存(零拷贝,直接用调用方缓冲) */ +/* ================================================================== */ +#define MJ_IOBUF 4096 + +typedef struct { + FILE *f; + const unsigned char *mem; + size_t memlen, mempos; + unsigned char buf[MJ_IOBUF]; + int buflen, bufpos; + unsigned int bitbuf; /* 熵流位缓冲(MSB 在前) */ + int bitcnt; + int over; /* 本熵段已到尽头(遇到 marker 或 EOF) */ + int marker; /* over 时已吃掉的 marker 码(0=无) */ +} mj_src; + +static int mj_raw(mj_src *s) +{ + if (s->mem) { + if (s->mempos >= s->memlen) return -1; + return s->mem[s->mempos++]; + } + if (s->bufpos >= s->buflen) { + s->buflen = (int)fread(s->buf, 1, MJ_IOBUF, s->f); + s->bufpos = 0; + if (s->buflen <= 0) { s->buflen = 0; return -1; } + } + return s->buf[s->bufpos++]; +} + +static int mj_u16(mj_src *s) +{ + int a = mj_raw(s), b = mj_raw(s); + return (a < 0 || b < 0) ? -1 : ((a << 8) | b); +} + +static int mj_read(mj_src *s, unsigned char *dst, size_t n) +{ + while (n--) { + int c = mj_raw(s); + if (c < 0) return 0; + *dst++ = (unsigned char)c; + } + return 1; +} + +static int mj_skip(mj_src *s, long n) +{ + while (n-- > 0) if (mj_raw(s) < 0) return 0; + return 1; +} + +/* 熵段内的字节:FF00 → FF;遇到真 marker(含 RSTn)→ 置 over 并记录,返回 0。 */ +static int mj_eb(mj_src *s) +{ + int c; + if (s->over) return 0; + c = mj_raw(s); + if (c < 0) { s->over = 1; return 0; } + if (c != 0xFF) return c; + for (;;) { + int d = mj_raw(s); + if (d < 0) { s->over = 1; return 0; } + if (d == 0x00) return 0xFF; + if (d == 0xFF) continue; /* 填充 FF */ + s->marker = d; s->over = 1; return 0; + } +} + +static void mj_fill(mj_src *s, int n) +{ + while (s->bitcnt < n) { + s->bitbuf = (s->bitbuf << 8) | (unsigned)mj_eb(s); + s->bitcnt += 8; + } +} + +static int mj_getbits(mj_src *s, int n) +{ + if (n <= 0) return 0; + if (s->bitcnt < n) mj_fill(s, n); + s->bitcnt -= n; + return (int)((s->bitbuf >> s->bitcnt) & ((1u << n) - 1u)); +} + +static int mj_extend(int v, int n) +{ + return v < (1 << (n - 1)) ? v - (1 << n) + 1 : v; +} + +/* ================================================================== */ +/* Huffman 表:9 位快表 + 慢路径(最长码 16 位) */ +/* ================================================================== */ +typedef struct { + unsigned char bits[17]; /* 各码长符号数 */ + unsigned char vals[256]; + unsigned short fast[512]; /* (len<<8)|sym,0 = 走慢路径 */ + int maxcode[17]; + int mincode[17]; + int valptr[17]; +} mj_huff; + +/* 构造 9 位快表 + 规范码表。返回 0 = 表非法(过订阅/码空间越界),调用方必须拒绝该表。 + * 过订阅输入(如「长度 2 的码有 100 个」)此前会让 code << shift 把快表下标推到 512 之外, + * 28~220 字节的畸形 JPEG 就能栈溢出 → 这里在填表前校验码空间,填表时再加一道下标边界。 */ +static int mj_huff_build(mj_huff *h) +{ + int code = 0, k = 0, l; + memset(h->fast, 0, sizeof h->fast); + h->maxcode[0] = -1; + for (l = 1; l <= 16; l++) { + int i; + if (code + h->bits[l] > (1 << l)) return 0; /* 码空间溢出:过订阅表 */ + h->valptr[l] = k; + h->mincode[l] = code; + for (i = 0; i < h->bits[l]; i++) { + if (l <= 9) { + int shift = 9 - l, j; + unsigned short e = (unsigned short)((l << 8) | h->vals[k]); + int base = code << shift; + for (j = 0; j < (1 << shift); j++) + if ((base | j) < 512) h->fast[base | j] = e; + } + code++; + k++; + } + h->maxcode[l] = h->bits[l] ? code - 1 : -1; + code <<= 1; + } + return 1; +} + +static int mj_hdecode(mj_src *s, const mj_huff *h) +{ + if (s->bitcnt < 9) mj_fill(s, 9); + if (s->bitcnt >= 9) { + unsigned idx = (s->bitbuf >> (s->bitcnt - 9)) & 511u; + unsigned e = h->fast[idx]; + if (e) { + s->bitcnt -= (int)(e >> 8); + return (int)(e & 0xFFu); + } + } + { + int code = 0, l; + for (l = 1; l <= 16; l++) { + code = (code << 1) | mj_getbits(s, 1); + if (code <= h->maxcode[l]) + return h->vals[h->valptr[l] + code - h->mincode[l]]; + if (s->over) return 0; + } + } + s->over = 1; + return 0; +} + +/* zigzag 序号 → 自然序(行优先 v*8+u) */ +static const unsigned char mj_zigzag[64] = { + 0, 1, 8, 16, 9, 2, 3, 10, + 17, 24, 32, 25, 18, 11, 4, 5, + 12, 19, 26, 33, 40, 48, 41, 34, + 27, 20, 13, 6, 7, 14, 21, 28, + 35, 42, 49, 56, 57, 50, 43, 36, + 29, 22, 15, 23, 30, 37, 44, 51, + 58, 59, 52, 45, 38, 31, 39, 46, + 53, 60, 61, 54, 47, 55, 62, 63 +}; + +/* ================================================================== */ +/* 缩解基(Q12):B(u,x) = 「8 点 IDCT 在输出像素区间的盒式平均」 */ +/* B(u,y) = (N/8)·Σ_{x} w(y,x)·(1/2)·C(u)·cos((2x+1)uπ/16), C(0)=1/√2 */ +/* w(y,x) = 像素 x 的单位区间与输出区间 [y·8/N, (y+1)·8/N) 的重叠长度 */ +/* 于是:DC 单频重建恒为 F00/8(与 8×8 IDCT 的均值一致);N=8 时退化为 */ +/* 精确 IDCT;任意 N 之间的亮度/对比度一致(同一套离散平均口径)。 */ +/* 只列 N=1..7:N=8(≈不缩解)交回 stb 旧路径,保持「档位」对外表现。 */ +/* ================================================================== */ +static const short mj_basis_1[1] = { 1448 }; +static const short mj_basis_2[4] = { + 1448, 1448, + 1312, -1312 +}; +static const short mj_basis_3[9] = { + 1448, 1448, 1448, + 1676, 0, -1676, + 808, -1615, 808 +}; +static const short mj_basis_4[16] = { + 1448, 1448, 1448, 1448, + 1856, 769, -769, -1856, + 1338, -1338, -1338, 1338, + 652, -1573, 1573, -652 +}; +static const short mj_basis_5[25] = { + 1448, 1448, 1448, 1448, 1448, + 1894, 1187, 0, -1187, -1894, + 1476, -530, -1892, -530, 1476, + 914, -1498, 0, 1498, -914, + 362, -1086, 1448, -1086, 362 +}; +static const short mj_basis_6[36] = { + 1448, 1448, 1448, 1448, 1448, 1448, + 1932, 1420, 584, -584, -1420, -1932, + 1615, 0, -1615, -1615, 0, 1615, + 1177, -1204, -1356, 1356, 1204, -1177, + 724, -1448, 724, 724, -1448, 724, + 351, -805, 1377, -1377, 805, -351 +}; +static const short mj_basis_7[49] = { + 1448, 1448, 1448, 1448, 1448, 1448, 1448, + 1970, 1562, 861, 0, -861, -1562, -1970, + 1754, 392, -1199, -1892, -1199, 392, 1754, + 1440, -802, -1682, 0, 1682, 802, -1440, + 1086, -1448, -362, 1448, -362, -1448, 1086, + 745, -1407, 888, 0, -888, 1407, -745, + 449, -946, 889, -784, 889, -946, 449 +}; + +static const short *mj_basis(int n) +{ + switch (n) { + case 1: return mj_basis_1; + case 2: return mj_basis_2; + case 3: return mj_basis_3; + case 4: return mj_basis_4; + case 5: return mj_basis_5; + case 6: return mj_basis_6; + default: return mj_basis_7; + } +} + +/* ================================================================== */ +/* 分量与图像 */ +/* ================================================================== */ +typedef struct { + int id, h, v, tq; + int n; /* 缩解阶:每块 n×n 像素 */ + int pw, ph; /* 平面(含 MCU padding)尺寸 */ + int vw, vh; /* 有效像素尺寸(最近邻采样用) */ + int ow, oh; /* 本分量自己的块网格(非交织扫描用) */ + int td, ta; /* 当前扫描的 Huffman 表号 */ + int pred; /* DC 预测值(量化域) */ + unsigned char *plane; +} mj_comp; + +typedef struct { + int width, height; + int ncomp; + int hmax, vmax; + int mcus_x, mcus_y; + int progressive; + int rst; /* 重启间隔(MCU 数),0 = 无 */ + int adobe_transform; /* -1 未声明;0=RGB/CMYK;1=YCbCr */ + int blocks; /* 已解块数(判「有没有解出东西」) */ + mj_comp c[3]; + int qt[4][64]; + int have_qt[4]; + mj_huff dc[4], ac[4]; + int have_dc[4], have_ac[4]; + mj_src src; +} mj_img; + +#define MJ_PEAK_CAP (8u * 1024u * 1024u) /* 防御性上限:异常巨图不至于一次吃掉几十 MB */ + +static void mj_free(mj_img *img) +{ + int i; + for (i = 0; i < img->ncomp; i++) { + free(img->c[i].plane); + img->c[i].plane = NULL; + } +} + +/* 把一个(已反量化的)系数按 DCT 缩解累加进平面。k 为自然序下标。 */ +static void mj_accum(mj_comp *c, int bx, int by, int k, int32_t F) +{ + int u = k & 7, v = k >> 3, n = c->n, x, y; + const short *W; + unsigned char *base; + if (!F || u >= n || v >= n) return; + W = mj_basis(n); + base = c->plane + (size_t)(by * n) * c->pw + (size_t)(bx * n); + for (y = 0; y < n; y++) { + int32_t t = (F * W[v * n + y]) >> 12; + unsigned char *row = base + (size_t)y * c->pw; + for (x = 0; x < n; x++) { + int nv = (int)row[x] + (int)((t * W[u * n + x]) >> 12); + row[x] = (unsigned char)(nv < 0 ? 0 : (nv > 255 ? 255 : nv)); + } + } +} + +/* ------------------------------------------------------------------ */ +/* 表解析 */ +/* ------------------------------------------------------------------ */ +static int mj_dqt(mj_img *img, const unsigned char *d, int len) +{ + int p = 0; + while (p < len) { + int pq = d[p] >> 4, tq = d[p] & 15, i; + p++; + if (tq > 3) return 0; + /* 16bit 量化表只出现在 12bit 精度(本模块不支持);收下会让 F*qt 在 mj_accum 里 + * int32 溢出(UB)。直接拒绝 → 后端返回 0 → 回退 stb。 */ + if (pq != 0) return 0; + if (p + 64 > len) return 0; + for (i = 0; i < 64; i++) img->qt[tq][mj_zigzag[i]] = d[p + i]; + p += 64; + img->have_qt[tq] = 1; + } + return 1; +} + +static int mj_dht(mj_img *img, const unsigned char *d, int len) +{ + int p = 0; + while (p < len) { + int tc, th, i, total = 0; + if (p + 17 > len) return 0; + tc = d[p] >> 4; th = d[p] & 15; p++; + if (th > 3 || tc > 1) return 0; + { + mj_huff *h = tc ? &img->ac[th] : &img->dc[th]; + for (i = 1; i <= 16; i++) { h->bits[i] = d[p + i - 1]; total += d[p + i - 1]; } + p += 16; + if (total > 256 || p + total > len) return 0; + for (i = 0; i < total; i++) h->vals[i] = d[p + i]; + p += total; + if (!mj_huff_build(h)) return 0; /* 过订阅/码空间越界:拒绝整段 → 回退 stb */ + if (tc) img->have_ac[th] = 1; else img->have_dc[th] = 1; + } + } + return 1; +} + +/* ------------------------------------------------------------------ */ +/* SOF:尺寸/采样因子/分量的平面分配 */ +/* ------------------------------------------------------------------ */ +static int mj_ceil_div(int a, int b) { return (a + b - 1) / b; } + +/* 缩解阶 N:每 8×8 块只解出 N×N 像素(N/8 = 输出/源分辨率)。 + * 取「不欠采样」的最小 N:ceil(8·maxdim/longside)。N=8 表示几乎不缩解(≈原尺寸), + * 这种图上 stb 旧路径的峰值本来就受控,直接交回它,档位对外表现不变。 */ +static int mj_pick_n(int longside, int maxdim) +{ + int n = (8 * maxdim + longside - 1) / longside; + if (n < 1) n = 1; + if (n > 7) return 0; + return n; +} + +/* 按各分量当前的 n 计算块网格与平面尺寸,返回平面总字节。 */ +static unsigned mj_layout(mj_img *img) +{ + unsigned total = 0; + int i; + for (i = 0; i < img->ncomp; i++) { + mj_comp *c = &img->c[i]; + int sw = mj_ceil_div(img->width * c->h, img->hmax); + int sh = mj_ceil_div(img->height * c->v, img->vmax); + c->ow = mj_ceil_div(sw, 8); + c->oh = mj_ceil_div(sh, 8); + c->pw = (img->mcus_x * c->h > c->ow ? img->mcus_x * c->h : c->ow) * c->n; + c->ph = (img->mcus_y * c->v > c->oh ? img->mcus_y * c->v : c->oh) * c->n; + c->vw = mj_ceil_div(sw * c->n, 8); + c->vh = mj_ceil_div(sh * c->n, 8); + if (c->vw > c->pw) c->vw = c->pw; + if (c->vh > c->ph) c->vh = c->ph; + total += (unsigned)c->pw * (unsigned)c->ph; + } + return total; +} + +static int mj_sof(mj_img *img, int marker, const unsigned char *d, int len, int maxdim) +{ + int i, n, longside, base_n; + unsigned int plane_bytes, target_bytes; + int tw, th; + + if (marker != 0xC0 && marker != 0xC1 && marker != 0xC2) return 0; /* 算术/无损/差分 */ + if (len < 6) return 0; + if (d[0] != 8) return 0; /* 12bit 精度不支持 */ + img->height = (d[1] << 8) | d[2]; + img->width = (d[3] << 8) | d[4]; + n = d[5]; + if (img->width <= 0 || img->height <= 0 || n < 1 || n > 3) return 0; /* 4 分量 CMYK → stb */ + if (len < 6 + n * 3) return 0; + img->ncomp = n; + img->progressive = (marker == 0xC2); + + longside = img->width > img->height ? img->width : img->height; + if (longside <= maxdim) return 0; /* 小图走旧入口 */ + base_n = mj_pick_n(longside, maxdim); + if (!base_n) return 0; /* 几乎等尺寸:交回 stb 旧路径 */ + + img->hmax = img->vmax = 1; + for (i = 0; i < n; i++) { + const unsigned char *p = d + 6 + i * 3; + mj_comp *c = &img->c[i]; + c->id = p[0]; + c->h = p[1] >> 4; + c->v = p[1] & 15; + c->tq = p[2]; + if (c->h < 1 || c->v < 1 || c->h > 4 || c->v > 4 || c->tq > 3) return 0; + if (c->h > img->hmax) img->hmax = c->h; + if (c->v > img->vmax) img->vmax = c->v; + } + img->mcus_x = mj_ceil_div(img->width, 8 * img->hmax); + img->mcus_y = mj_ceil_div(img->height, 8 * img->vmax); + + meow_img_target_dims(img->width, img->height, maxdim, &tw, &th); + target_bytes = (unsigned)tw * (unsigned)th * 4u; + + for (i = 0; i < n; i++) img->c[i].n = base_n; + plane_bytes = mj_layout(img); + + /* 4:4:4 的色度平面与亮度等大(正常 4:2:0 只 1/4):亮度平面超过 400KB 时色度降一档, + * 视觉近无损,省下的正是色度要多占的两份。 */ + if (n == 3 && img->c[0].h == img->c[1].h && img->c[0].v == img->c[1].v && + plane_bytes / 3 > 400u * 1024u) { + for (i = 1; i < n; i++) if (img->c[i].n > 1) img->c[i].n >>= 1; + plane_bytes = mj_layout(img); + } + + /* 预算 2.4MB(平面 + 目标图):超了先降色度(亮度保持清晰),再整体降档。 + * 12MP 4:2:0 一档:1.13MB→0.94MB,输出仍是 480×720。 */ + if (plane_bytes + target_bytes > 2400u * 1024u) { + int ch = 0; + for (i = 1; i < n; i++) if (img->c[i].n > 1) { img->c[i].n >>= 1; ch = 1; } + if (ch) plane_bytes = mj_layout(img); + } + /* 最后一道防线:异常巨图整体降档(最多降到 N=1),不至于一次吃掉几十 MB。 */ + while (plane_bytes + target_bytes > MJ_PEAK_CAP && base_n > 1) { + base_n >>= 1; + for (i = 0; i < n; i++) if (img->c[i].n > base_n) img->c[i].n = base_n; + plane_bytes = mj_layout(img); + } + /* 硬闸:降到 N=1 仍超上限(畸形/超大声明尺寸,如 23 字节的 SOF 65535² → 亮度面 + * 8192² = 64MiB)直接拒绝 → 返回 0 → 回退 stb(stb 对这类文件是 KB 级安全失败)。 + * 没有这道闸,公开入口 meow_img_load_file_dim(720) 会被一个头文件打爆设备内存。 */ + if (plane_bytes + target_bytes > MJ_PEAK_CAP) return 0; + + for (i = 0; i < n; i++) { + mj_comp *c = &img->c[i]; + size_t bytes = (size_t)c->pw * (size_t)c->ph; + c->plane = (unsigned char *)malloc(bytes); + if (!c->plane) return 0; + memset(c->plane, 128, bytes); /* 中性灰底,系数累加即 IDCT */ + c->pred = 0; + } + return 1; +} + +/* ------------------------------------------------------------------ */ +/* 熵解码:块 */ +/* ------------------------------------------------------------------ */ +static int mj_block_seq(mj_img *img, mj_comp *c, int bx, int by) +{ + mj_src *s = &img->src; + int t = mj_hdecode(s, &img->dc[c->td]); + int k; + if (t > 16) return 0; + c->pred += t ? mj_extend(mj_getbits(s, t), t) : 0; + if (c->pred > (1 << 20) || c->pred < -(1 << 20)) c->pred = 0; + mj_accum(c, bx, by, 0, (int32_t)c->pred * img->qt[c->tq][0]); + for (k = 1; k < 64; ) { + int rs = mj_hdecode(s, &img->ac[c->ta]); + int r = rs >> 4, sz = rs & 15; + if (sz == 0) { + if (r < 15) break; /* EOB */ + k += 16; /* ZRL */ + continue; + } + k += r; + if (k > 63) break; + { + int v = mj_extend(mj_getbits(s, sz), sz); + int nk = mj_zigzag[k]; + if (v > 1023) v = 1023; + if (v < -1023) v = -1023; + mj_accum(c, bx, by, nk, (int32_t)v * img->qt[c->tq][nk]); + k++; + } + } + img->blocks++; + return 1; +} + +static int mj_block_dc_prog(mj_img *img, mj_comp *c, int bx, int by, int al) +{ + mj_src *s = &img->src; + int t = mj_hdecode(s, &img->dc[c->td]); + if (t > 16) return 0; + c->pred += t ? (mj_extend(mj_getbits(s, t), t) << al) : 0; + if (c->pred > (1 << 20) || c->pred < -(1 << 20)) c->pred = 0; + mj_accum(c, bx, by, 0, (int32_t)c->pred * img->qt[c->tq][0]); + img->blocks++; + return 1; +} + +/* 跳过一段熵编码数据(不解码),停在下一个非 RST 的 marker 上。 + * 用途:渐进式的精细化扫描(Ah>0)对缩略图无价值,但编码器可能把频带扫描与精细化交错, + * 所以只跳过、不中止——后续的 Ah=0 扫描仍会解码。 */ +static void mj_skip_entropy(mj_img *img) +{ + mj_src *s = &img->src; + s->bitcnt = 0; + s->bitbuf = 0; + for (;;) { + while (!s->over) (void)mj_eb(s); + if (s->over && s->marker >= 0xD0 && s->marker <= 0xD7) { + s->over = 0; /* RSTn:重同步后继续扫 */ + s->marker = 0; + continue; + } + break; + } +} + +/* 重启标记:对齐位缓冲并吃掉 RSTn,重置 DC 预测。 + * + * 裸扫时必须跳过 FF 00(数据字节 0xFF 的填充):区间末尾的填充字节常常正好是 0xFF, + * 它在码流里写成 `FF 00 FF Dn`;把中间那个 00 当成 marker 会让扫描提前结束并失步。 */ +static void mj_restart(mj_img *img) +{ + mj_src *s = &img->src; + int i; + s->bitcnt = 0; + s->bitbuf = 0; + if (s->over) { + if (s->marker >= 0xD0 && s->marker <= 0xD7) { s->over = 0; s->marker = 0; } + } else { + int c; + for (;;) { + c = mj_raw(s); + if (c < 0) { s->over = 1; break; } + if (c != 0xFF) continue; + for (;;) { + c = mj_raw(s); + if (c < 0) { s->over = 1; break; } + if (c == 0xFF) continue; /* 填充 FF */ + if (c >= 0xD0 && c <= 0xD7) goto rst_found; + if (c == 0x00) break; /* FF00 = 数据字节,继续扫 */ + s->marker = c; s->over = 1; /* 别的 marker:熵段结束 */ + goto rst_done; + } + if (s->over) break; + } + } +rst_found: + for (i = 0; i < img->ncomp; i++) img->c[i].pred = 0; +rst_done: + ; +} + +/* 区间边界处理(三处扫描循环共用)。返回:1 = 继续;2 = 刚做过区间重同步(AC 需清 EOBRUN)。 + * + * 只在 total == rst(编码器的区间长度)时重同步。**不能**一看到 over&&RSTn 就重同步: + * 熵码位缓冲有最多 9 位前瞻,`over` 会在区间/扫描真正结束前 1~2 个 MCU 就被置位, + * 此时 bitbuf 里还压着剩余块的有效比特——提前重同步会把它们丢掉并失步 + * (渐进式 DC 扫描每块仅 ~1 bit,前瞻能跨过整个 MCU;这是 rst=2..200 花屏的根因)。 + * 同理,over 且 marker 非 RSTn(扫描结束后的 DHT/EOI 已被前瞻吃掉)时**也不能退出**: + * 扫描循环本来就知道确定的 MCU/块数,把剩余的解完即可(位耗尽后 mj_eb 返回 0 做零填充), + * marker 交给外层主循环处理。曾在此 `return 0` 提前退出,丢掉最后一个 MCU, + * 平面 memset(128) 的灰底直接露出来(低熵/纯白渐进式右下角 3 像素 #808080)。 */ +static int mj_boundary(mj_img *img, int *total) +{ + mj_src *s = &img->src; + (void)s; + if (img->rst && *total == img->rst) { + mj_restart(img); + *total = 0; + return 2; + } + return 1; +} + +typedef struct { + int ns; + int ci[4]; + int td[4], ta[4]; + int ss, se, ah, al; +} mj_scan; + +/* 顺序(baseline / SOF1)扫描 */ +static void mj_scan_seq(mj_img *img, const mj_scan *sc) +{ + int single = (sc->ns == 1); + int mcus_x = single ? img->c[sc->ci[0]].ow : img->mcus_x; + int mcus_y = single ? img->c[sc->ci[0]].oh : img->mcus_y; + int mx, my, total = 0; + for (my = 0; my < mcus_y; my++) { + for (mx = 0; mx < mcus_x; mx++) { + int j; + if (!mj_boundary(img, &total)) return; + if (single) { + mj_comp *c = &img->c[sc->ci[0]]; + c->td = sc->td[0]; c->ta = sc->ta[0]; + mj_block_seq(img, c, mx, my); + } else { + for (j = 0; j < sc->ns; j++) { + mj_comp *c = &img->c[sc->ci[j]]; + int by, bx; + c->td = sc->td[j]; c->ta = sc->ta[j]; + for (by = 0; by < c->v; by++) + for (bx = 0; bx < c->h; bx++) + mj_block_seq(img, c, mx * c->h + bx, my * c->v + by); + } + } + total++; + } + } +} + +/* 渐进式 DC 扫描(Ah=0) */ +static void mj_scan_dc_prog(mj_img *img, const mj_scan *sc) +{ + int single = (sc->ns == 1); + int mcus_x = single ? img->c[sc->ci[0]].ow : img->mcus_x; + int mcus_y = single ? img->c[sc->ci[0]].oh : img->mcus_y; + int mx, my, total = 0; + for (my = 0; my < mcus_y; my++) { + for (mx = 0; mx < mcus_x; mx++) { + int j; + if (!mj_boundary(img, &total)) { + return; + } + if (single) { + mj_comp *c = &img->c[sc->ci[0]]; + c->td = sc->td[0]; + mj_block_dc_prog(img, c, mx, my, sc->al); + } else { + for (j = 0; j < sc->ns; j++) { + mj_comp *c = &img->c[sc->ci[j]]; + int by, bx; + c->td = sc->td[j]; + for (by = 0; by < c->v; by++) + for (bx = 0; bx < c->h; bx++) + mj_block_dc_prog(img, c, mx * c->h + bx, my * c->v + by, sc->al); + } + } + total++; + } + } +} + +/* 渐进式 AC 扫描(Ah=0,单分量) */ +static void mj_scan_ac_prog(mj_img *img, const mj_scan *sc) +{ + mj_comp *c = &img->c[sc->ci[0]]; + mj_src *s = &img->src; + int eobrun = 0, bx, by, total = 0; + c->ta = sc->ta[0]; + for (by = 0; by < c->oh; by++) { + for (bx = 0; bx < c->ow; bx++) { + int rb = mj_boundary(img, &total); + if (!rb) return; + if (rb == 2) eobrun = 0; /* 区间重同步:EOB 游程不能跨 RST */ + if (eobrun > 0) { + eobrun--; + } else { + int k = sc->ss; + while (k <= sc->se) { + int rs = mj_hdecode(s, &img->ac[c->ta]); + int r = rs >> 4, sz = rs & 15; + if (sz == 0) { + if (r < 15) { + eobrun = (1 << r) - 1; + if (r) eobrun += mj_getbits(s, r); + break; + } + k += 16; + continue; + } + k += r; + if (k > sc->se) break; + { + int v = mj_extend(mj_getbits(s, sz), sz); + int nk = mj_zigzag[k]; + int32_t sv; + if (v > 1023) v = 1023; + if (v < -1023) v = -1023; + sv = (int32_t)v * img->qt[c->tq][nk]; + if (sc->al > 6) sv <<= 6; else sv <<= sc->al; /* Al 上限防御,防 int32 溢出 */ + mj_accum(c, bx, by, nk, sv); + } + k++; + } + } + total++; + img->blocks++; + /* 这里**不能**再 `if (s->over) return`:块末的位前瞻可能已吃掉本区间的 RSTn + * (over=1),必须留给下一轮 mj_boundary 做区间重同步,否则渐进式 + restart 的 + * AC 扫描会在第一个区间边界处静默退出、把后续 RST 当独立 marker 跳过 → 失步。 */ + } + } +} + +/* ------------------------------------------------------------------ */ +/* 输出:分量平面 → 目标尺寸 RGBA(最近邻,与 meow_img_downscale 同口径)*/ +/* ------------------------------------------------------------------ */ +#define MJ_FIX_R 5743 /* 1.402 * 4096 */ +#define MJ_FIX_G 1410 /* 0.34414 * 4096 */ +#define MJ_FIX_G2 2925 /* 0.71414 * 4096 */ +#define MJ_FIX_B 7258 /* 1.772 * 4096 */ + +static unsigned char *mj_output(mj_img *img, int tw, int th, int rgb_mode) +{ + unsigned char *out = (unsigned char *)malloc((size_t)tw * th * 4); + mj_comp *cy = &img->c[0]; + mj_comp *cb = img->ncomp >= 3 ? &img->c[1] : NULL; + mj_comp *cr = img->ncomp >= 3 ? &img->c[2] : NULL; + const unsigned char *py = cy->plane; + const unsigned char *pb = cb ? cb->plane : NULL; + const unsigned char *pc = cr ? cr->plane : NULL; + int x, y; + if (!out) return NULL; + for (y = 0; y < th; y++) { + int sy = (int)((long)y * cy->vh / th); + const unsigned char *ry = py + (size_t)sy * cy->pw; + const unsigned char *rb = pb ? pb + (size_t)((long)y * cb->vh / th) * cb->pw : NULL; + const unsigned char *rc = pc ? pc + (size_t)((long)y * cr->vh / th) * cr->pw : NULL; + unsigned char *dst = out + (size_t)y * tw * 4; + for (x = 0; x < tw; x++) { + int yv = ry[(int)((long)x * cy->vw / tw)]; + unsigned char *o = dst + (size_t)x * 4; + if (!rb) { + o[0] = o[1] = o[2] = (unsigned char)yv; + o[3] = 255; + } else if (rgb_mode) { + o[0] = (unsigned char)yv; + o[1] = rb[(int)((long)x * cb->vw / tw)]; + o[2] = rc[(int)((long)x * cr->vw / tw)]; + o[3] = 255; + } else { + int u = (int)rb[(int)((long)x * cb->vw / tw)] - 128; + int v = (int)rc[(int)((long)x * cr->vw / tw)] - 128; + int r = yv + ((MJ_FIX_R * v) >> 12); + int g = yv - ((MJ_FIX_G * u + MJ_FIX_G2 * v) >> 12); + int b = yv + ((MJ_FIX_B * u) >> 12); + o[0] = (unsigned char)(r < 0 ? 0 : (r > 255 ? 255 : r)); + o[1] = (unsigned char)(g < 0 ? 0 : (g > 255 ? 255 : g)); + o[2] = (unsigned char)(b < 0 ? 0 : (b > 255 ? 255 : b)); + o[3] = 255; + } + } + } + return out; +} + +/* ------------------------------------------------------------------ */ +/* 主解析循环 */ +/* ------------------------------------------------------------------ */ +static int mj_decode(mj_img *img, int maxdim, int *tw, int *th, unsigned char **rgba) +{ + mj_src *s = &img->src; + int have_sof = 0, rgb_mode = 0; + int pending = -1; /* 熵段结束时被位读取器吃掉的 marker(回填给主循环) */ + + if (mj_u16(s) != 0xFFD8) return 0; /* SOI */ + for (;;) { + int code, len; + if (pending >= 0) { + code = pending; + pending = -1; + } else { + /* 找下一个 marker(跳过 FF 填充字节) */ + do { code = mj_raw(s); if (code < 0) return 0; } while (code != 0xFF); + do { code = mj_raw(s); if (code < 0) return 0; } while (code == 0xFF); + } + if (code == 0xD9) break; /* EOI */ + if (code == 0xD8 || code == 0x01 || (code >= 0xD0 && code <= 0xD7)) continue; + len = mj_u16(s); + if (len < 2) return 0; + len -= 2; + if (code == 0xDA) { + int ns, i; + mj_scan sc; + unsigned char hdr[64]; + if (len > (int)sizeof hdr) return 0; + if (!mj_read(s, hdr, (size_t)len)) return 0; + if (len < 1) return 0; + ns = hdr[0]; + if (ns < 1 || ns > 3 || len < 1 + ns * 2 + 3) return 0; + memset(&sc, 0, sizeof sc); + sc.ns = ns; + for (i = 0; i < ns; i++) { + int cid = hdr[1 + i * 2], tt = hdr[2 + i * 2], j, found = -1; + for (j = 0; j < img->ncomp; j++) + if (img->c[j].id == cid) { found = j; break; } + if (found < 0) return 0; + sc.ci[i] = found; + sc.td[i] = tt >> 4; + sc.ta[i] = tt & 15; + if (sc.td[i] > 3 || sc.ta[i] > 3) return 0; + } + sc.ss = hdr[1 + ns * 2]; + sc.se = hdr[2 + ns * 2]; + sc.ah = hdr[3 + ns * 2] >> 4; + sc.al = hdr[3 + ns * 2] & 15; + if (!have_sof) return 0; + if (sc.ss < 0 || sc.se > 63 || sc.ss > sc.se || sc.al > 13) return 0; + for (i = 0; i < ns; i++) { + mj_comp *c = &img->c[sc.ci[i]]; + if (!img->have_qt[c->tq]) return 0; + if (sc.ss == 0 && !img->have_dc[sc.td[i]]) return 0; + if (sc.se > 0 && !img->have_ac[sc.ta[i]]) return 0; + } + if (img->progressive && sc.ah > 0) { + /* 精细化扫描:跳过(不解码),继续找后续 Ah=0 扫描。低频带首次扫描到齐 + * 就够出一张清晰缩略图;整幅系数的 successive-approximation 缓存正是 + * 渐进式峰值失控的根因,这里根本不留。 */ + mj_skip_entropy(img); + s->bitcnt = 0; + s->bitbuf = 0; + if (s->over) { + if (s->marker) pending = s->marker; + s->marker = 0; + s->over = 0; + } + continue; + } + { + if (!img->progressive) { + mj_scan_seq(img, &sc); + } else if (sc.ss == 0) { + mj_scan_dc_prog(img, &sc); + } else { + if (sc.ns != 1) return 0; + mj_scan_ac_prog(img, &sc); + } + } + s->bitcnt = 0; + s->bitbuf = 0; + /* 一个熵段结束:位读取器可能已把下一个 marker 吃掉,回填给主循环, + * 否则紧跟其后的 DHT/DQT/SOS 会被漏掉。 */ + if (s->over) { + if (s->marker) pending = s->marker; + s->marker = 0; + s->over = 0; + } + continue; + } + if (len > 0 && code != 0xDA) { + /* APPn/COM 只跳过(APP14 取 Adobe transform,很短);其它段读进 2KB 栈缓冲。 + * 不在 marker 循环里 malloc:EXIF 缩略图/ICC 这类段可能有几百 KB,会把解码峰值抬上去。 */ + if (code >= 0xE0 && code <= 0xEF) { + if (code == 0xEE && len >= 12 && len <= 64) { + unsigned char ad[64]; + if (!mj_read(s, ad, (size_t)len)) return 0; + if (memcmp(ad, "Adobe", 5) == 0) img->adobe_transform = ad[11]; + } else if (!mj_skip(s, len)) { + return 0; + } + continue; + } + if (code == 0xFE) { /* COM */ + if (!mj_skip(s, len)) return 0; + continue; + } + { + unsigned char pbuf[2048]; + if (len > (int)sizeof pbuf) return 0; /* DQT/DHT/SOF 不可能这么长 */ + if (!mj_read(s, pbuf, (size_t)len)) return 0; + switch (code) { + case 0xC0: case 0xC1: case 0xC2: + if (have_sof) return 0; + if (!mj_sof(img, code, pbuf, len, maxdim)) return 0; + have_sof = 1; + break; + case 0xC4: + if (!mj_dht(img, pbuf, len)) return 0; + break; + case 0xDB: + if (!mj_dqt(img, pbuf, len)) return 0; + break; + case 0xDD: + if (len >= 2) img->rst = (pbuf[0] << 8) | pbuf[1]; + break; + default: + break; + } + } + continue; + } + if (len > 0) { + if (!mj_skip(s, len)) return 0; + } + } + + if (!have_sof || img->blocks <= 0) return 0; + + /* 色彩空间:3 分量默认 YCbCr;Adobe transform=0 或分量 ID 为 'R','G','B' 时按 RGB。 */ + if (img->ncomp == 3) { + if (img->adobe_transform == 0) rgb_mode = 1; + if (img->c[0].id == 'R' && img->c[1].id == 'G' && img->c[2].id == 'B') rgb_mode = 1; + } + meow_img_target_dims(img->width, img->height, maxdim, tw, th); + *rgba = mj_output(img, *tw, *th, rgb_mode); + return *rgba ? 1 : 0; +} + +/* ================================================================== */ +/* 后端分派 */ +/* ================================================================== */ +static int mj_is_jpeg(const unsigned char *p, size_t n) +{ + return n >= 3 && p[0] == 0xFF && p[1] == 0xD8 && p[2] == 0xFF; +} + +int meow_img_backend_mem(const void *buf, size_t len, int maxdim, int *tw, int *th, unsigned char **rgba) +{ + const unsigned char *p = (const unsigned char *)buf; + if (!buf || len < 8 || maxdim <= 0) return 0; + if (mj_is_jpeg(p, len)) { + mj_img img; + int ok; + memset(&img, 0, sizeof img); + img.adobe_transform = -1; + img.src.mem = p; + img.src.memlen = len; + ok = mj_decode(&img, maxdim, tw, th, rgba); + mj_free(&img); + return ok; + } + if (p[0] == 0x89 && p[1] == 'P' && p[2] == 'N' && p[3] == 'G') + return meow_png_decode_stream(NULL, p, len, maxdim, tw, th, rgba); + if (p[0] == 'B' && p[1] == 'M') + return meow_bmp_decode_stream(NULL, p, len, maxdim, tw, th, rgba); + return 0; +} + +int meow_img_backend_file(const char *path, int maxdim, int *tw, int *th, unsigned char **rgba) +{ + FILE *f; + unsigned char sig[8]; + size_t n; + int ok = 0; + if (!path || maxdim <= 0) return 0; + f = fopen(path, "rb"); + if (!f) return 0; + n = fread(sig, 1, sizeof sig, f); + if (n < 4 || fseek(f, 0, SEEK_SET) != 0) { fclose(f); return 0; } + if (mj_is_jpeg(sig, n)) { + mj_img img; + memset(&img, 0, sizeof img); + img.adobe_transform = -1; + img.src.f = f; + ok = mj_decode(&img, maxdim, tw, th, rgba); + mj_free(&img); + } else if (sig[0] == 0x89 && sig[1] == 'P' && sig[2] == 'N' && sig[3] == 'G') { + ok = meow_png_decode_stream(f, NULL, 0, maxdim, tw, th, rgba); + } else if (sig[0] == 'B' && sig[1] == 'M') { + ok = meow_bmp_decode_stream(f, NULL, 0, maxdim, tw, th, rgba); + } + fclose(f); + return ok; +} diff --git a/patch_v2/sdk/meow_jpeg.h b/patch_v2/sdk/meow_jpeg.h new file mode 100644 index 0000000..5aac22b --- /dev/null +++ b/patch_v2/sdk/meow_jpeg.h @@ -0,0 +1,50 @@ +/* + * meow_jpeg.h — M23 真缩解后端内部接口(**不对外导出**,不在 meow_ui.h 伞头里)。 + * + * 后端直接产出「目标显示尺寸」的 RGBA,不做「先整幅解、再重采样」: + * - JPEG(meow_jpeg.c):DCT 域缩解 1/8、1/4、1/2(N=1/2/4 点 IDCT),渐进式只解到 + * 首个精细化扫描(首个 Ah>0)为止——低频带此时已完整,再往后只是为了更细的量化精度。 + * - PNG(meow_png_stream.c):libz inflate 逐行 defilter,按步长丢行丢列直写目标尺寸。 + * - BMP(meow_png_stream.c):按行 seek + 行内跳列(同一套「只碰需要的行」思路)。 + * 三者峰值都只与「输出尺寸 + 目标图像」相关,不随源图分辨率线性增长——**前提是文件头 + * 声明的尺寸在合理范围内**:畸形/超大声明尺寸(例如 23 字节的 SOF 声明 65535×65535)会被 + * 直接拒绝(返回 0 → 回退 stb,安全失败),不会按声明尺寸分配平面。 + * + * 不支持的特性一律返回 0,由 meow_ui_img.c 回退 stb(行为与 M23 之前完全一致): + * 4 分量 CMYK/YCCK、算术编码/无损/分层 JPEG、Adam7 交错 PNG、1/2/4bpp 与 RLE BMP 等。 + * + * 许可证:本文件与 meow_jpeg.c / meow_png_stream.c 为 MeowOS 自写代码(MIT,随 SDK 发布)。 + * 不引入第三方解码器源码——理由见 M23 报告「为什么不用 IJG libjpeg」:libjpeg 系列对 + * 渐进式 JPEG 必须整幅驻留系数阵列(≈1 字节/源像素/分量),正是审计里 +23.5MB 的来源, + * 换成它也无法把渐进式峰值压到 2.5MB 以内。 + */ +#ifndef MEOW_JPEG_H +#define MEOW_JPEG_H + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* 解码 path / 内存里的图,直接产出「长边 ≤ maxdim 且按 meow_img_downscale 同一公式等比缩放」 + * 的 tw×th RGBA(4 通道 straight alpha,malloc 分配,调用方 free)。 + * 返回 1 成功(tw/th/rgba 已填);0 = 不支持/失败/不需要(调用方回退 stb 或旧入口)。 */ +int meow_img_backend_file(const char *path, int maxdim, int *tw, int *th, unsigned char **rgba); +int meow_img_backend_mem(const void *buf, size_t len, int maxdim, int *tw, int *th, unsigned char **rgba); + +/* 目标尺寸:与 meow_img_downscale 同一公式(长边 = maxdim,短边按比例截断,下限 1)。 */ +void meow_img_target_dims(int w, int h, int maxdim, int *tw, int *th); + +/* meow_png_stream.c 提供(PNG 逐行 inflate / BMP 按行跳采样)。f 与 mem 二选一: + * f != NULL 走文件流(只读需要的行/块),否则从 mem[0..len) 读。 */ +int meow_png_decode_stream(FILE *f, const unsigned char *mem, size_t len, + int maxdim, int *tw, int *th, unsigned char **rgba); +int meow_bmp_decode_stream(FILE *f, const unsigned char *mem, size_t len, + int maxdim, int *tw, int *th, unsigned char **rgba); + +#ifdef __cplusplus +} +#endif +#endif /* MEOW_JPEG_H */ diff --git a/patch_v2/sdk/meow_png_stream.c b/patch_v2/sdk/meow_png_stream.c new file mode 100644 index 0000000..4da2594 --- /dev/null +++ b/patch_v2/sdk/meow_png_stream.c @@ -0,0 +1,499 @@ +/* + * meow_png_stream.c — M23 真缩解后端(二):PNG 逐行 inflate + BMP 按行跳采样。 + * + * 为什么不用 stb 的 PNG 路径:stb 先整幅 inflate + 反滤波成 raw,再展开成 RGBA 整幅, + * 最后才降采样——1600×2400 PNG 实测 +26.2MB。这里改成「按行流式」: + * - IDAT 边到边 inflate,一次只持有一行(filter 字节 + rowbytes)与上一行; + * - 反滤波后**只把需要的列**写进目标行(最近邻,与 meow_img_downscale 同口径); + * - 不需要的行整行丢弃(但仍要反滤波,filter 依赖上一行); + * - 峰值 ≈ 两行缓冲 + zlib 窗口(32KB) + 目标 RGBA,与源图分辨率无关。 + * 支持:gray / gray+A / RGB / RGBA / 调色板,bit depth 1/2/4/8/16(16bit 取高字节降 8bit), + * tRNS 透明。不支持 → 返回 0 回退 stb:Adam7 交错、非 0 压缩/滤波方法、异常色型组合。 + * + * BMP:文件流按目标行直接 seek 到源行(不读全图),行内跳列采样。支持 8bpp 调色板 / + * 16bpp RGB555 / 24bpp / 32bpp(BI_RGB);RLE、BITFIELDS、1/2/4bpp → 回退 stb。 + * + * 许可证:MeowOS 自写(MIT,随 SDK 发布)。PNG/zlib 语义参考 RFC 2083 / zlib 手册; + * BMP 布局参考 stb_image 的解析口径(同为本仓已有依赖,行为对齐)。 + */ +#include "meow_jpeg.h" + +#include +#include +#include +#include + +/* ---------------- zlib ABI ---------------- */ +#if defined(__has_include) +# if __has_include() +# include +# define MZP_HAVE_H 1 +# endif +#endif +#ifndef MZP_HAVE_H +/* 设备 musl sysroot 没有 zlib.h(meow_font.c 同此口径):按 libz.a 的 ABI 声明最小 API。 + * 指针/unsigned long 交给编译器定尺寸,宿主 64 位与设备 32 位都成立。 */ +typedef unsigned char mzp_Bytef; +typedef unsigned int mzp_uInt; +typedef unsigned long mzp_uLong; +typedef void *mzp_voidpf; +typedef struct mzp_stream_s { + mzp_Bytef *next_in; mzp_uInt avail_in; mzp_uLong total_in; + mzp_Bytef *next_out; mzp_uInt avail_out; mzp_uLong total_out; + char *msg; + void *state; + void *zalloc; + void *zfree; + mzp_voidpf opaque; + int data_type; + mzp_uLong adler; + mzp_uLong reserved; +} mzp_stream; +extern int inflateInit_(mzp_stream *, const char *, int); +extern int inflate(mzp_stream *, int); +extern int inflateEnd(mzp_stream *); +extern const char *zlibVersion(void); +# define z_stream mzp_stream +# define Z_OK 0 +# define Z_STREAM_END 1 +# define Z_NO_FLUSH 0 +#endif + +/* ---------------- 字节源 ---------------- */ +typedef struct { + FILE *f; + const unsigned char *mem; + size_t memlen, mempos; +} png_src; + +static int ps_read(png_src *s, void *dst, size_t n) +{ + if (s->mem) { + if (s->mempos + n > s->memlen) return 0; + memcpy(dst, s->mem + s->mempos, n); + s->mempos += n; + return 1; + } + return fread(dst, 1, n, s->f) == n; +} + +static int ps_skip(png_src *s, size_t n) +{ + if (s->mem) { + if (s->mempos + n > s->memlen) return 0; + s->mempos += n; + return 1; + } + return fseek(s->f, (long)n, SEEK_CUR) == 0; +} + +/* ================================================================== */ +/* PNG */ +/* ================================================================== */ +typedef struct { + png_src src; + z_stream z; + int z_ready, z_end; + unsigned w, h; + int depth, color; + int bits; /* 每像素位数 */ + size_t rowbytes; + unsigned char *line; /* 1 filter 字节 + rowbytes */ + unsigned char *prev; /* 上一行的像素字节 */ + unsigned char *cur; /* 本行的像素字节 */ + size_t need, got; + unsigned char pal[256][3]; + unsigned char trns_a[256]; /* 调色板透明度 */ + int pal_n; + int have_trns; + unsigned trns[3]; /* gray / RGB 的 tRNS 值(已降到 8bit) */ + unsigned char *out; + int tw, th; + int next_ty; + unsigned y; +} png_ctx; + +/* 从行里取 px 列的 8bit 通道;返回通道数(写入 o[0..3] 的 RGB(A) 语义由调用方定)。 */ +static void png_get_px(const png_ctx *c, const unsigned char *row, int x, unsigned char *o) +{ + int d = c->depth; + if (d < 8) { + int per = 8 / d; /* 每字节像素数 */ + int byte = x / per; + int shift = 8 - d * (x % per + 1); + int v = (row[byte] >> shift) & ((1 << d) - 1); + if (c->color == 3) { /* 调色板:子字节值即索引 */ + o[0] = c->pal[v][0]; o[1] = c->pal[v][1]; o[2] = c->pal[v][2]; + o[3] = c->trns_a[v]; + } else { + unsigned char g = (unsigned char)(v * 255 / ((1 << d) - 1)); + o[0] = o[1] = o[2] = g; + /* tRNS 存的是原始采样值(0..2^d-1),不是展开后的 0..255 */ + o[3] = (c->color == 0 && c->have_trns && (unsigned)v == c->trns[0]) ? 0 : 255; + } + return; + } + if (d == 8) { + if (c->color == 0) { /* gray */ + o[0] = o[1] = o[2] = row[x]; + o[3] = (c->have_trns && row[x] == c->trns[0]) ? 0 : 255; + } else if (c->color == 2) { /* RGB */ + o[0] = row[x * 3]; o[1] = row[x * 3 + 1]; o[2] = row[x * 3 + 2]; + o[3] = (c->have_trns && row[x * 3] == c->trns[0] && + row[x * 3 + 1] == c->trns[1] && row[x * 3 + 2] == c->trns[2]) ? 0 : 255; + } else if (c->color == 3) { /* palette */ + int i = row[x]; + o[0] = c->pal[i][0]; o[1] = c->pal[i][1]; o[2] = c->pal[i][2]; + o[3] = c->trns_a[i]; + } else if (c->color == 4) { /* gray + A */ + o[0] = o[1] = o[2] = row[x * 2]; + o[3] = row[x * 2 + 1]; + } else { /* RGBA */ + o[0] = row[x * 4]; o[1] = row[x * 4 + 1]; + o[2] = row[x * 4 + 2]; o[3] = row[x * 4 + 3]; + } + return; + } + /* 16bit:取高字节 */ + if (c->color == 0) { + o[0] = o[1] = o[2] = row[x * 2]; + o[3] = (c->have_trns && row[x * 2] == c->trns[0]) ? 0 : 255; + } else if (c->color == 2) { + o[0] = row[x * 6]; o[1] = row[x * 6 + 2]; o[2] = row[x * 6 + 4]; + o[3] = (c->have_trns && row[x * 6] == c->trns[0] && + row[x * 6 + 2] == c->trns[1] && row[x * 6 + 4] == c->trns[2]) ? 0 : 255; + } else if (c->color == 4) { + o[0] = o[1] = o[2] = row[x * 4]; + o[3] = row[x * 4 + 2]; + } else { + o[0] = row[x * 8]; o[1] = row[x * 8 + 2]; + o[2] = row[x * 8 + 4]; o[3] = row[x * 8 + 6]; + } +} + +/* 一行反滤波完成后:按需写目标行。 */ +static int png_emit_row(png_ctx *c) +{ + unsigned char ft = c->line[0]; + const unsigned char *src = c->line + 1; + unsigned char *dst = c->cur; + size_t x, bpp = (size_t)((c->bits + 7) / 8); + if (bpp < 1) bpp = 1; + if (ft > 4) return 0; + for (x = 0; x < c->rowbytes; x++) { + unsigned char left = (x >= bpp) ? dst[x - bpp] : 0; + unsigned char up = c->prev ? c->prev[x] : 0; + unsigned char ul = (c->prev && x >= bpp) ? c->prev[x - bpp] : 0; + unsigned char v = src[x]; + switch (ft) { + case 0: dst[x] = v; break; + case 1: dst[x] = (unsigned char)(v + left); break; + case 2: dst[x] = (unsigned char)(v + up); break; + case 3: dst[x] = (unsigned char)(v + (((unsigned)left + (unsigned)up) >> 1)); break; + default: { + int p = (int)left + (int)up - (int)ul; + int pa = p - (int)left; if (pa < 0) pa = -pa; + int pb = p - (int)up; if (pb < 0) pb = -pb; + int pc = p - (int)ul; if (pc < 0) pc = -pc; + unsigned char pred = (pa <= pb && pa <= pc) ? left : (pb <= pc ? up : ul); + dst[x] = (unsigned char)(v + pred); + break; + } + } + } + if (c->next_ty < c->th) { + long sy = (long)c->next_ty * (long)c->h / c->th; + if (sy == (long)c->y) { + unsigned char *o = c->out + (size_t)c->next_ty * c->tw * 4; + int tx; + for (tx = 0; tx < c->tw; tx++) { + int sx = (int)((long)tx * (long)c->w / c->tw); + unsigned char px[4]; + png_get_px(c, dst, sx, px); + o[tx * 4] = px[0]; o[tx * 4 + 1] = px[1]; + o[tx * 4 + 2] = px[2]; o[tx * 4 + 3] = px[3]; + } + c->next_ty++; + } + } + /* 交换 prev/cur(prev 直接成为反滤波基准) */ + { + unsigned char *t = c->prev; + c->prev = c->cur; + c->cur = t; + } + c->y++; + return 1; +} + +#define PNG_NEED_MORE 2 + +static int png_pump(png_ctx *c) +{ + while (c->got < c->need) { + int ret; + c->z.next_out = c->line + c->got; + c->z.avail_out = (unsigned)(c->need - c->got); + ret = inflate(&c->z, Z_NO_FLUSH); + c->got = c->need - c->z.avail_out; + if (ret == Z_STREAM_END) c->z_end = 1; + else if (ret != Z_OK) return 0; + if (c->got < c->need && (c->z.avail_in == 0 || c->z_end)) + return c->z_end ? 0 : PNG_NEED_MORE; + } + if (!png_emit_row(c)) return 0; + c->got = 0; + return 1; +} + +static int png_bits_of(int color, int depth) +{ + switch (color) { + case 0: return depth; /* gray */ + case 2: return depth * 3; /* rgb */ + case 3: return depth; /* palette */ + case 4: return depth * 2; /* gray + a */ + case 6: return depth * 4; /* rgba */ + default: return 0; + } +} + +static int png_depth_ok(int color, int depth) +{ + if (color == 0) return depth == 1 || depth == 2 || depth == 4 || depth == 8 || depth == 16; + if (color == 3) return depth == 1 || depth == 2 || depth == 4 || depth == 8; + if (color == 2 || color == 4 || color == 6) return depth == 8 || depth == 16; + return 0; +} + +int meow_png_decode_stream(FILE *f, const unsigned char *mem, size_t len, + int maxdim, int *tw, int *th, unsigned char **rgba) +{ + png_ctx c; + unsigned char sig[8], hdr[8]; + unsigned char ibuf[4096]; + int have_ihdr = 0, i, ok = 0; + + if (maxdim <= 0) return 0; + memset(&c, 0, sizeof c); + c.src.f = f; + c.src.mem = mem; + c.src.memlen = len; + if (!ps_read(&c.src, sig, 8)) return 0; + if (memcmp(sig, "\x89PNG\r\n\x1a\n", 8) != 0) return 0; + + while (ps_read(&c.src, hdr, 8)) { + unsigned clen = ((unsigned)hdr[0] << 24) | ((unsigned)hdr[1] << 16) | + ((unsigned)hdr[2] << 8) | hdr[3]; + const unsigned char *type = hdr + 4; + if (clen > 0x7FFFFFFFu) return 0; + + if (memcmp(type, "IHDR", 4) == 0) { + unsigned char d[13]; + if (clen < 13 || have_ihdr) return 0; + if (!ps_read(&c.src, d, 13)) return 0; + c.w = ((unsigned)d[0] << 24) | ((unsigned)d[1] << 16) | ((unsigned)d[2] << 8) | d[3]; + c.h = ((unsigned)d[4] << 24) | ((unsigned)d[5] << 16) | ((unsigned)d[6] << 8) | d[7]; + c.depth = d[8]; + c.color = d[9]; + if (d[10] != 0 || d[11] != 0 || d[12] != 0) return 0; /* 只支持非交错 + 标准压缩/滤波 */ + if (c.w == 0 || c.h == 0 || c.w > 65535u || c.h > 65535u) return 0; + if (!png_depth_ok(c.color, c.depth)) return 0; + c.bits = png_bits_of(c.color, c.depth); + c.rowbytes = ((size_t)c.w * c.bits + 7) / 8; + if (c.rowbytes == 0) return 0; + /* 小图不动:与 meow_img_load_file_dim 的「旧入口」判定一致 */ + if ((long)(c.w > c.h ? c.w : c.h) <= maxdim) return 0; + if (clen > 13 && !ps_skip(&c.src, clen - 13)) return 0; + have_ihdr = 1; + c.need = c.rowbytes + 1; + c.line = (unsigned char *)malloc(c.need); + c.prev = (unsigned char *)malloc(c.rowbytes); + c.cur = (unsigned char *)malloc(c.rowbytes); + if (!c.line || !c.prev || !c.cur) goto done; + memset(c.prev, 0, c.rowbytes); + memset(c.cur, 0, c.rowbytes); + for (i = 0; i < 256; i++) c.trns_a[i] = 255; + meow_img_target_dims((int)c.w, (int)c.h, maxdim, &c.tw, &c.th); + c.out = (unsigned char *)malloc((size_t)c.tw * c.th * 4); + if (!c.out) goto done; + memset(c.out, 0, (size_t)c.tw * c.th * 4); + memset(&c.z, 0, sizeof c.z); + if (inflateInit_(&c.z, zlibVersion(), (int)sizeof c.z) != Z_OK) goto done; + c.z_ready = 1; + } else if (memcmp(type, "PLTE", 4) == 0) { + unsigned char d[256 * 3]; + if (clen > sizeof d || clen % 3) return 0; + if (!ps_read(&c.src, d, clen)) return 0; + c.pal_n = (int)(clen / 3); + memcpy(c.pal, d, clen); + } else if (memcmp(type, "tRNS", 4) == 0) { + unsigned char d[256]; + if (clen > sizeof d) return 0; + if (!ps_read(&c.src, d, clen)) return 0; + c.have_trns = 1; + if (c.color == 3) { + for (i = 0; i < (int)clen && i < 256; i++) c.trns_a[i] = d[i]; + } else if (c.color == 0 && clen >= 2) { + /* 8bit/低位深:采样值存在低字节;16bit:与像素一样取高字节比较 */ + c.trns[0] = (c.depth == 16) ? d[0] : d[1]; + } else if (c.color == 2 && clen >= 6) { + if (c.depth == 16) { + c.trns[0] = d[0]; c.trns[1] = d[2]; c.trns[2] = d[4]; + } else { + c.trns[0] = d[1]; c.trns[1] = d[3]; c.trns[2] = d[5]; + } + } + } else if (memcmp(type, "IDAT", 4) == 0) { + unsigned remain = clen; + if (!have_ihdr || !c.out) return 0; + while (remain) { + unsigned n = remain > sizeof ibuf ? (unsigned)sizeof ibuf : remain; + if (!ps_read(&c.src, ibuf, n)) goto done; + remain -= n; + c.z.next_in = ibuf; + c.z.avail_in = n; + while (c.z.avail_in && c.next_ty < c.th) { + int r = png_pump(&c); + if (r == 0) goto done; + if (r == PNG_NEED_MORE) break; + } + } + } else if (memcmp(type, "IEND", 4) == 0) { + break; + } else { + if (clen && !ps_skip(&c.src, clen)) return 0; + } + /* 跳过 CRC */ + { + unsigned char crc[4]; + if (!ps_read(&c.src, crc, 4)) break; + } + if (c.out && c.next_ty >= c.th) break; /* 目标行都拿到了,不必读完 */ + } + + if (!have_ihdr || !c.out || c.next_ty < c.th) goto done; + if (c.color == 3 && c.pal_n <= 0) goto done; + *tw = c.tw; + *th = c.th; + *rgba = c.out; + c.out = NULL; + ok = 1; +done: + if (c.z_ready) inflateEnd(&c.z); + free(c.line); + free(c.prev); + free(c.cur); + free(c.out); + return ok; +} + +/* ================================================================== */ +/* BMP */ +/* ================================================================== */ +static unsigned le16(const unsigned char *p) { return (unsigned)p[0] | ((unsigned)p[1] << 8); } +static unsigned le32(const unsigned char *p) +{ + return (unsigned)p[0] | ((unsigned)p[1] << 8) | ((unsigned)p[2] << 16) | ((unsigned)p[3] << 24); +} + +int meow_bmp_decode_stream(FILE *f, const unsigned char *mem, size_t len, + int maxdim, int *tw, int *th, unsigned char **rgba) +{ + png_src src; + unsigned char fh[14], dh[4]; + unsigned char dib[1024]; + unsigned dibsize, dataoff, bpp, compression, colors; + long w, h, rowsize; + int topdown, i; + unsigned char pal[256][3]; + unsigned char *row = NULL, *out = NULL; + int tw_ = 0, th_ = 0, ok = 0; + + if (maxdim <= 0) return 0; + memset(&src, 0, sizeof src); + src.f = f; + src.mem = mem; + src.memlen = len; + if (!ps_read(&src, fh, 14)) return 0; + if (fh[0] != 'B' || fh[1] != 'M') return 0; + dataoff = le32(fh + 10); + if (!ps_read(&src, dh, 4)) return 0; + dibsize = le32(dh); + if (dibsize < 40 || dibsize > sizeof dib) return 0; /* BITMAPCOREHEADER/OS2 → stb */ + memcpy(dib, dh, 4); + if (!ps_read(&src, dib + 4, dibsize - 4)) return 0; + w = (long)(int)le32(dib + 4); + h = (long)(int)le32(dib + 8); + bpp = le16(dib + 14); + compression = le32(dib + 16); + colors = (dibsize >= 36) ? le32(dib + 32) : 0; + if (compression != 0) return 0; /* RLE/BITFIELDS → stb */ + if (w <= 0 || h == 0) return 0; + if (bpp != 8 && bpp != 16 && bpp != 24 && bpp != 32) return 0; + topdown = h < 0; + if (h < 0) h = -h; + if ((long)(w > h ? w : h) <= maxdim) return 0; /* 小图走旧入口 */ + + if (bpp <= 8) { + unsigned n = colors ? colors : 256; + unsigned char e[4]; + if (n > 256) n = 256; + for (i = 0; (unsigned)i < n; i++) { + if (!ps_read(&src, e, 4)) return 0; + pal[i][0] = e[2]; pal[i][1] = e[1]; pal[i][2] = e[0]; + } + for (; i < 256; i++) pal[i][0] = pal[i][1] = pal[i][2] = 0; + } + + rowsize = (w * bpp + 31) / 32 * 4; + meow_img_target_dims((int)w, (int)h, maxdim, &tw_, &th_); + row = (unsigned char *)malloc((size_t)rowsize); + out = (unsigned char *)malloc((size_t)tw_ * th_ * 4); + if (!row || !out) goto done; + + for (int ty = 0; ty < th_; ty++) { + int sy = (int)((long)ty * h / th_); + long file_row = topdown ? sy : (h - 1 - sy); + long off = (long)dataoff + file_row * rowsize; + unsigned char *o = out + (size_t)ty * tw_ * 4; + if (src.mem) { + if (off < 0 || (size_t)off + (size_t)rowsize > src.memlen) goto done; + memcpy(row, src.mem + off, (size_t)rowsize); + } else { + if (fseek(src.f, off, SEEK_SET) != 0) goto done; + if (fread(row, 1, (size_t)rowsize, src.f) != (size_t)rowsize) goto done; + } + for (int tx = 0; tx < tw_; tx++) { + int sx = (int)((long)tx * w / tw_); + unsigned char *p = o + (size_t)tx * 4; + if (bpp == 24) { + const unsigned char *q = row + sx * 3; + p[0] = q[2]; p[1] = q[1]; p[2] = q[0]; p[3] = 255; + } else if (bpp == 32) { + const unsigned char *q = row + sx * 4; + p[0] = q[2]; p[1] = q[1]; p[2] = q[0]; p[3] = 255; /* BI_RGB:alpha 不作为语义 */ + } else if (bpp == 16) { + unsigned v = le16(row + sx * 2); + unsigned r = (v >> 10) & 31, g = (v >> 5) & 31, b = v & 31; + p[0] = (unsigned char)((r << 3) | (r >> 2)); + p[1] = (unsigned char)((g << 3) | (g >> 2)); + p[2] = (unsigned char)((b << 3) | (b >> 2)); + p[3] = 255; + } else { + unsigned idx = row[sx]; + p[0] = pal[idx & 255][0]; p[1] = pal[idx & 255][1]; + p[2] = pal[idx & 255][2]; p[3] = 255; + } + } + } + *tw = tw_; + *th = th_; + *rgba = out; + out = NULL; + ok = 1; +done: + free(row); + free(out); + return ok; +} diff --git a/patch_v2/sdk/meow_ui_gfx.c b/patch_v2/sdk/meow_ui_gfx.c index 3ed6115..de16eea 100644 --- a/patch_v2/sdk/meow_ui_gfx.c +++ b/patch_v2/sdk/meow_ui_gfx.c @@ -172,6 +172,9 @@ static void refresh_governor(unsigned long long cost_ms, int period) } /* ---- 工具 ---- */ +/* 时钟统一由 meow_ui_time.c 提供(meow_ui_time_ms,声明见 include/meow_ui_draw.h): + * mipsel 上 zig musl 的 time64 ABI 自相矛盾,取时必须走内核 o32 ABI 再升位, + * 故本 TU 不再保留取时实现(详见 sdk/CLOCK_ABI.md)。 */ static uint16_t rgb565(uint32_t c) { return (uint16_t)(((c >> 8) & 0xF800) | ((c >> 5) & 0x07E0) | ((c >> 3) & 0x001F)); @@ -854,6 +857,106 @@ void meow_gfx_blit_rgba(int x, int y, int w, int h, const uint8_t *rgba, int alp dirty_add(x1, y1, x2 - x1, y2 - y1); } +/* ================= RGB565 页图(M32) ================= + * 阅读器页槽常驻 RGB565(2B/px)而不是 RGBA:真机 480×853 页 1599KB → 800KB。 + * 量化与 back_buf 用同一个 rgb565()(逐位截断),所以 565 屏上落下的像素与旧的 + * RGBA blit 路径**逐像素一致**——不引入色偏,也不会比现在多出色带。 + * 命中页(解码/落框尺寸 == 显示尺寸)走 meow_gfx_blit_rgb565:每行一次 memcpy, + * 单次拷贝、没有每帧重采样。alpha 按不透明处理(页图 alpha 恒为 255;带透明像素的 + * PNG/32bpp BMP 页会与旧 RGBA 路径不同——旧路径混合到底色,这里直接画 RGB)。 */ +uint16_t meow_gfx_rgb565(uint32_t rgb) +{ + return rgb565(rgb); +} + +void meow_gfx_rgba_to_rgb565(const uint8_t *rgba, uint16_t *out, int npx) +{ + if (!rgba || !out || npx <= 0) return; + for (int i = 0; i < npx; i++) { + const uint8_t *p = rgba + (size_t)i * 4; + out[i] = rgb565(((uint32_t)p[0] << 16) | ((uint32_t)p[1] << 8) | (uint32_t)p[2]); + } +} + +/* 目标矩形与 clip 求交;返回 1=还有可画区域。 */ +static int blit565_clip(int x, int y, int w, int h, int *x1, int *y1, int *x2, int *y2) +{ + if (!g_init_ok || w <= 0 || h <= 0) return 0; + *x1 = x; *y1 = y; *x2 = x + w; *y2 = y + h; + if (*x1 < 0) *x1 = 0; + if (*y1 < 0) *y1 = 0; + if (*x2 > g_xres) *x2 = g_xres; + if (*y2 > g_yres) *y2 = g_yres; + if (g_clip_n > 0) { + int *c = g_clip_stack[g_clip_n - 1]; + if (*x1 < c[0]) *x1 = c[0]; + if (*y1 < c[1]) *y1 = c[1]; + if (*x2 > c[2]) *x2 = c[2]; + if (*y2 > c[3]) *y2 = c[3]; + } + return *x2 > *x1 && *y2 > *y1; +} + +void meow_gfx_blit_rgb565(int x, int y, int w, int h, const uint16_t *px) +{ + if (!px) return; + int x1, y1, x2, y2; + if (!blit565_clip(x, y, w, h, &x1, &y1, &x2, &y2)) return; + int n = x2 - x1; + for (int py = y1; py < y2; py++) { + const uint16_t *src = px + (size_t)(py - y) * w + (x1 - x); + unsigned char *dst = g_fb + (size_t)py * g_line + (size_t)x1 * (size_t)(g_bpp / 8); + if (g_bpp == 16) { + memcpy(dst, src, (size_t)n * 2); /* 命中页:每行一次拷贝 */ + } else { + uint32_t *d32 = (uint32_t *)dst; + for (int i = 0; i < n; i++) { + uint16_t c = src[i]; + d32[i] = ((uint32_t)ex5((c >> 11) & 0x1F) << 24) | + ((uint32_t)ex6((c >> 5) & 0x3F) << 16) | + ((uint32_t)ex5(c & 0x1F) << 8) | 0xFFu; + } + } + } + dirty_add(x1, y1, n, y2 - y1); +} + +/* 最近邻缩放(仅几何不符时的兜底路径;采样点与 meow_img_resample 同式)。 + * 命中页不会走到这里——那是热路径纪律(M17/M32)。 */ +void meow_gfx_blit_rgb565_scaled(int x, int y, int w, int h, + int sw, int sh, const uint16_t *px) +{ + if (!px || sw <= 0 || sh <= 0) return; + int x1, y1, x2, y2; + if (!blit565_clip(x, y, w, h, &x1, &y1, &x2, &y2)) return; + int n = x2 - x1; + for (int py = y1; py < y2; py++) { + int sy = (int)((long long)(py - y) * sh / h); + if (sy >= sh) sy = sh - 1; + const uint16_t *row = px + (size_t)sy * sw; + unsigned char *dst = g_fb + (size_t)py * g_line + (size_t)x1 * (size_t)(g_bpp / 8); + if (g_bpp == 16) { + uint16_t *d16 = (uint16_t *)dst; + for (int i = 0; i < n; i++) { + int sx = (int)((long long)(x1 + i - x) * sw / w); + if (sx >= sw) sx = sw - 1; + d16[i] = row[sx]; + } + } else { + uint32_t *d32 = (uint32_t *)dst; + for (int i = 0; i < n; i++) { + int sx = (int)((long long)(x1 + i - x) * sw / w); + if (sx >= sw) sx = sw - 1; + uint16_t c = row[sx]; + d32[i] = ((uint32_t)ex5((c >> 11) & 0x1F) << 24) | + ((uint32_t)ex6((c >> 5) & 0x3F) << 16) | + ((uint32_t)ex5(c & 0x1F) << 8) | 0xFFu; + } + } + } + dirty_add(x1, y1, n, y2 - y1); +} + /* ================= present / 上屏 ================= */ static void dump_ppm(void) { diff --git a/patch_v2/sdk/meow_ui_img.c b/patch_v2/sdk/meow_ui_img.c index 9571800..4de3d31 100644 --- a/patch_v2/sdk/meow_ui_img.c +++ b/patch_v2/sdk/meow_ui_img.c @@ -1,10 +1,11 @@ /* - * meow_ui_img.c — 图片层(B1.2)。基于 stb_image(单头文件、静态编入), - * 解 PNG/JPEG/BMP → RGBA,统一最近邻缩放/居中裁方。 + * meow_ui_img.c — 图片层(B1.2)。解码:M23 真缩解后端(meow_jpeg.c / meow_png_stream.c) + * 优先,不支持/失败回退 stb_image(单头文件、静态编入);统一输出 RGBA,最近邻缩放/居中裁方。 */ #include "meow_ui.h" #include "meow_ui_internal.h" #include "stb_image.h" +#include "meow_jpeg.h" /* M23 真缩解后端(内部头,不对外导出) */ #include #include @@ -45,7 +46,7 @@ meow_img *meow_img_load_file(const char *path) /* 追加(M12):预算解码。选整数缩解档位 s ∈ {1,2,4,8}——取满足「长边/s ≥ maxdim」的最大档, * 保证缩解结果不欠采样(不丢细节),再由 meow_img_downscale 精修到 maxdim。长边 < 2*maxdim * 时无档可用(s=1),退回整幅解码后精修——此时本就接近目标尺寸,无需牺牲。 */ -static int img_pick_scale(int longside, int maxdim) +int meow_img_pick_scale(int longside, int maxdim) { int s = 1; while (s < 8 && (longside / (s * 2)) >= maxdim) s *= 2; @@ -79,25 +80,107 @@ meow_img *meow_img_load_file_dim(const char *path, int maxdim) { if (!path) return NULL; if (maxdim <= 0) return meow_img_load_file(path); /* 不限尺寸 = 旧入口 */ + /* 追加(M23):真缩解后端直接产出目标尺寸(JPEG 按 DCT 档位;PNG 逐行 inflate 丢行丢列; + * BMP 按行 seek 跳采样)。峰值只与输出尺寸相关。 + * 不支持/失败/几乎等尺寸(4 分量 CMYK、算术编码、交错 PNG、RLE BMP、小图…)返回 0 → + * 落回下面的 stb 旧路径,对外语义与档位表现不变。 */ + { + unsigned char *bp = NULL; + int bw = 0, bh = 0; + if (meow_img_backend_file(path, maxdim, &bw, &bh, &bp) && bp) + return img_finish_dim(bp, bw, bh, maxdim); + } int w = 0, h = 0, c = 0; if (!stbi_info(path, &w, &h, &c)) return NULL; int longside = w > h ? w : h; if (longside <= maxdim) return meow_img_load_file(path); /* 小图不动:旧路径 */ - unsigned char *p = stbi_load_scaled(path, &w, &h, &c, 4, img_pick_scale(longside, maxdim)); + unsigned char *p = stbi_load_scaled(path, &w, &h, &c, 4, meow_img_pick_scale(longside, maxdim)); return img_finish_dim(p, w, h, maxdim); } +/* 追加(M22):档位由调用方指定。与 meow_img_load_file_dim 同语义(结果长边 ≤ maxdim, + * 超了再精修一次),只是档位不再由 maxdim 反推 —— 内存预算紧时可选更粗的档位。 + * M27:真缩解后端优先(step 折算成目标长边 ceil(长边/step),后端按该长边选 DCT 阶, + * 峰值只与输出尺寸相关);后端不支持时回退 stbi_load_scaled(step),语义与 M22 一致。 + * + * M27 r1(P1-1):allow_stb_fallback=0 时只尝试后端,**绝不回退 stb**——阅读器的门控按 + * 「后端峰值」放行时必须走这里,否则「头部说支持、解码中途失败」会静默落到 stb, + * 实测可放出 18–36MB(截断 12MP JPEG / 头-only BMP / 非法位深 PNG),即 M22 的 SIGKILL 场景。 + * 后端失败返回 NULL(调用方按坏图占位)。 */ +meow_img *meow_img_load_file_step_ex(const char *path, int maxdim, int step, + int allow_stb_fallback, int *used_backend) +{ + if (used_backend) *used_backend = 0; + if (!path) return NULL; + if (step <= 1 && maxdim <= 0) + return allow_stb_fallback ? meow_img_load_file(path) : NULL; + if (step <= 0) step = 1; + if (step > 8) step = 8; + int w = 0, h = 0, c = 0; + if (!stbi_info(path, &w, &h, &c)) return NULL; + int longside = w > h ? w : h; + if (step == 1 && maxdim <= 0) + return allow_stb_fallback ? meow_img_load_file(path) : NULL; + /* 追加(M23/M27):真缩解后端优先(JPEG/PNG/BMP 全部格式)。step 是「输出别比这个档位 + * 更细」的提示:折算成目标长边 ceil(长边/step) 传给后端(后端按该长边自动选 DCT 阶), + * 与下面 stbi_load_scaled(step) 的语义一致,峰值只与输出尺寸相关;maxdim<=0 时不精修, + * 结果仍是档位尺寸。后端不支持(CMYK/算术编码/交错/RLE…)时回退 stb,行为与 M22 一致。 + * M27 起阅读器的峰值模型已与后端同口径(page_source 的 reader_estimate_peak), + * 所以 PNG/BMP 也一并接后端——这正是「修好了却用不上」的最后一处缝。 */ + { + int eff = maxdim > 0 ? maxdim : longside; + int step_cap = (longside + step - 1) / step; + unsigned char *bp = NULL; + int bw = 0, bh = 0; + if (step_cap > 0 && step_cap < eff) eff = step_cap; + if (eff > 0 && meow_img_backend_file(path, eff, &bw, &bh, &bp) && bp) { + if (used_backend) *used_backend = 1; + return img_finish_dim(bp, bw, bh, maxdim > 0 ? maxdim : longside); + } + } + if (!allow_stb_fallback) return NULL; /* P1-1:门控按后端放行的页,失败即坏图 */ + unsigned char *p = stbi_load_scaled(path, &w, &h, &c, 4, step); + /* maxdim<=0:不精修(保留档位输出的原始尺寸)。 */ + return img_finish_dim(p, w, h, maxdim > 0 ? maxdim : longside); +} + +meow_img *meow_img_load_file_step(const char *path, int maxdim, int step) +{ + /* 通用入口保持 M22 语义:后端失败允许回退 stb。 */ + return meow_img_load_file_step_ex(path, maxdim, step, 1, NULL); +} + +/* 追加(M22):精确重采样到目标尺寸(最近邻,双向)。阅读器把缩解档位的结果一次性 + * 落到显示框,之后命中页就是单次原生 blit(M17 的热路径纪律不被粗档位破坏)。 */ +static unsigned char *scale_buf(const meow_img *im, int dw, int dh, int cover); + +meow_img *meow_img_resample(meow_img *im, int w, int h) +{ + if (!im || w <= 0 || h <= 0) return NULL; + if (im->w == w && im->h == h) return NULL; + unsigned char *tmp = scale_buf(im, w, h, 0); + if (!tmp) return NULL; + return img_alloc(w, h, tmp); +} + meow_img *meow_img_load_mem_dim(const void *buf, int len, int maxdim) { if (!buf || len <= 0) return NULL; if (maxdim <= 0) return meow_img_load_mem(buf, len); + /* 追加(M23):同文件入口,后端直接从调用方缓冲解码(不再整幅解 + 重采样)。 */ + { + unsigned char *bp = NULL; + int bw = 0, bh = 0; + if (meow_img_backend_mem(buf, (size_t)len, maxdim, &bw, &bh, &bp) && bp) + return img_finish_dim(bp, bw, bh, maxdim); + } int w = 0, h = 0, c = 0; if (!stbi_info_from_memory((const unsigned char *)buf, len, &w, &h, &c)) return NULL; int longside = w > h ? w : h; if (longside <= maxdim) return meow_img_load_mem(buf, len); unsigned char *p = stbi_load_scaled_from_memory((const unsigned char *)buf, len, &w, &h, &c, 4, - img_pick_scale(longside, maxdim)); + meow_img_pick_scale(longside, maxdim)); return img_finish_dim(p, w, h, maxdim); } diff --git a/patch_v2/sdk/test_ime_fix.c b/patch_v2/sdk/test_ime_fix.c index 9ca0b9b..5fd4cad 100644 --- a/patch_v2/sdk/test_ime_fix.c +++ b/patch_v2/sdk/test_ime_fix.c @@ -10,10 +10,10 @@ * gcc -Wall -Wextra -o test_ime_fix test_ime_fix.c test_host_stub.c \ * meow_ui_gfx.c meow_ui_time.c meow_ui_sys.c meow_ui_theme.c meow_ui_vol.c meow_ui_log.c \ * meow_ui_widget.c meow_ui_input.c meow_ui_activity.c meow_ui_dialog.c meow_ui_ime.c meow_ui_str.c \ - * meow_ui_draw.c meow_ui_view.c meow_ui_img.c meow_notify_ipc.c stb_image.c \ + * meow_ui_draw.c meow_ui_view.c meow_ui_img.c meow_notify_ipc.c stb_image.c meow_jpeg.c meow_png_stream.c \ * meow_ui_anim.c meow_ui_media.c meow_ui_nav.c meow_pinyin.c meow_ui_box.c meow_ui_scroll.c \ - * -I. -I../meow_launcherd -DMEOW_UI_HOST_TEST -pthread -lm - * (meow_ui_widget.c 新 W_LISTVIEW 引用 meow_ui_img_scaled → 需链 meow_ui_img.c+stb_image.c+-lm; + * -I. -I../meow_launcherd -DMEOW_UI_HOST_TEST -pthread -lm -lz + * (meow_ui_widget.c 新 W_LISTVIEW 引用 meow_ui_img_scaled → 需链 meow_ui_img.c+stb_image.c+-lm -lz; * widget/input/ime/view 后续又引入 anim/media/nav/pinyin/box/scroll 依赖,一并链接) */ #include "meow_ui.h" diff --git a/patch_v2/sdk/test_meow_img_budget.c b/patch_v2/sdk/test_meow_img_budget.c index 275760a..79ab86b 100644 --- a/patch_v2/sdk/test_meow_img_budget.c +++ b/patch_v2/sdk/test_meow_img_budget.c @@ -10,13 +10,15 @@ * B6 进升级:progressive JPEG 走同一缩解路径(峰值记录 + 长边约束) * B7 内存入口:meow_img_load_mem_dim 与文件入口等价(峰值 < 旧内存入口 1/2) * B8 坏图:坏 JPEG(魔数+垃圾)/ 空内存 / 不存在路径 → NULL(不崩) - * B9 PNG/BMP:尺寸约束成立,峰值如实记录(stb 无解码期缩放 → 与旧入口同级) + * B9 PNG/BMP:尺寸约束成立;M23 起 PNG/BMP 也走解码期缩解(逐行 inflate / 按行 seek), + * 峰值应显著低于旧入口(本用例只断言 ≤ maxdim + 非空,并打印峰值供报告引用) * * 夹具(生成一次): * python3 test_meow_img_budget_gen.py "$PREFIX/tmp/meow_img_budget" - * 编译(本 sdk 目录下;--wrap 用于给 malloc/free 家族打点): + * 编译(本 sdk 目录下;--wrap 用于给 malloc/free 家族打点;-lz 供 PNG 后端 inflate): * gcc -Wall -Wextra -I. -Iinclude -DMEOW_UI_HOST_TEST -o test_meow_img_budget \ - * test_meow_img_budget.c meow_ui_img.c stb_image.c -lm \ + * test_meow_img_budget.c meow_ui_img.c meow_jpeg.c meow_png_stream.c stb_image.c \ + * -lm -lz \ * -Wl,--wrap=malloc -Wl,--wrap=calloc -Wl,--wrap=realloc -Wl,--wrap=free * 运行:./test_meow_img_budget "$PREFIX/tmp/meow_img_budget" (exit 0=PASS / 1=FAIL) * 夹具目录可省略:默认 $PREFIX/tmp/meow_img_budget;缺失时自动调 gen 脚本生成。 @@ -421,8 +423,8 @@ int main(int argc, char **argv) CHECK(meow_img_load_mem_dim("x", 3, 480) == NULL, "垃圾内存 → NULL"); } - /* ---- B9 PNG / BMP(stb 无解码期缩放,如实记录峰值) ---- */ - printf("[B9] PNG / BMP:尺寸约束成立,峰值如实记录(stb 无解码期缩放)\n"); + /* ---- B9 PNG / BMP(M23 起也走解码期缩解;只断言尺寸/非空,峰值打印) ---- */ + printf("[B9] PNG / BMP:尺寸约束成立,峰值记录(M23 逐行/跳行缩解,应远低于旧入口)\n"); { struct { const char *tag; const char *path; } fx[] = { { "PNG", p_png }, { "BMP", p_bmp }, @@ -446,8 +448,8 @@ int main(int argc, char **argv) CHECK(nw && meow_img_w(nw) <= 480 && meow_img_h(nw) <= 480 && nonblank(nw), name); peak_report(fx[i].tag, p_old); peak_report(" ↑ 新入口", p_new); - snprintf(name, sizeof name, "%s 峰值与旧入口同级(stb 无解码期缩放)", fx[i].tag); - CHECK(p_new <= p_old + p_old / 8, name); + snprintf(name, sizeof name, "%s 新入口峰值 ≤ 旧入口(不再整幅物化)", fx[i].tag); + CHECK(p_new <= p_old, name); if (old) printf(" %s 原图 %dx%d\n", fx[i].tag, meow_img_w(old), meow_img_h(old)); meow_img_free(nw); } diff --git a/patch_v2/sdk/test_meow_img_color.c b/patch_v2/sdk/test_meow_img_color.c index f414afd..76fd2d7 100644 --- a/patch_v2/sdk/test_meow_img_color.c +++ b/patch_v2/sdk/test_meow_img_color.c @@ -16,8 +16,8 @@ * meow_ui_gfx.c meow_ui_time.c meow_ui_sys.c meow_ui_theme.c meow_ui_vol.c meow_ui_log.c \ * meow_ui_widget.c meow_ui_input.c meow_ui_activity.c meow_ui_dialog.c meow_ui_ime.c meow_ui_str.c \ * meow_pinyin.c meow_ui_draw.c meow_ui_view.c meow_ui_box.c meow_ui_anim.c meow_ui_scroll.c \ - * meow_ui_img.c meow_ui_media.c meow_notify_ipc.c stb_image.c \ - * -I. -I../meow_launcherd -DMEOW_UI_HOST_TEST -pthread -lm + * meow_ui_img.c meow_ui_media.c meow_notify_ipc.c stb_image.c meow_jpeg.c meow_png_stream.c \ + * -I. -I../meow_launcherd -DMEOW_UI_HOST_TEST -pthread -lm -lz * 运行:./test_meow_img_color */ #include "meow_ui.h" diff --git a/patch_v2/sdk/test_meow_img_stream.c b/patch_v2/sdk/test_meow_img_stream.c new file mode 100644 index 0000000..4b1534d --- /dev/null +++ b/patch_v2/sdk/test_meow_img_stream.c @@ -0,0 +1,1182 @@ +/* test_meow_img_stream.c — M23 真缩解后端宿主验证(峰值 + 像素一致性 + 回退)。 + * + * 覆盖矩阵(对齐 M23 任务 3/4/5): + * S1 探针/入口透明:meow_img_probe_file、load_file_dim / load_mem_dim 对新后端透明; + * 小图逐字节等价旧入口;maxdim<=0 等价旧入口 + * S2 baseline 2000×3000:maxdim=720/480 峰值 ≤ 2.5MiB(旧入口如实并列打印) + * S3 progressive 2000×3000:同一断言(渐进式不再整幅驻留系数) + * S4 PNG 1600×2400:峰值 ≤ 2.5MiB(逐行 inflate,无整幅 raw/RGBA) + * S5 BMP 1600×2400:峰值 ≤ 2.5MiB(按行 seek,不读全图) + * S6 4MP/8MP/12MP JPEG:都能解且峰值 ≤ 2.5MiB;峰值增长远小于像素数增长(非线性) + * S7 像素一致性:与「stb 整幅解码 + meow_img_downscale」网格比对(容差内) + * S8 内存入口 == 文件入口(逐字节),峰值同级 + * S9 回退:CMYK JPEG(4 分量)、1bpp BMP、Adam7 交错 PNG → 后端拒绝,stb 仍成功; + * 截断/垃圾 → NULL(不崩) + * + * 夹具:本文件内置生成脚本(Pillow),首次运行时写到夹具目录并执行。 + * 编译(本 sdk 目录下;--wrap 用于给 malloc/free 家族打点): + * gcc -Wall -Wextra -O2 -I. -Iinclude -DMEOW_UI_HOST_TEST -o test_meow_img_stream \ + * test_meow_img_stream.c meow_ui_img.c meow_jpeg.c meow_png_stream.c stb_image.c \ + * -lm -lz \ + * -Wl,--wrap=malloc -Wl,--wrap=calloc -Wl,--wrap=realloc -Wl,--wrap=free + * 运行:./test_meow_img_stream "$PREFIX/tmp/meow_img_stream" (exit 0=PASS / 1=FAIL) + */ +#define _GNU_SOURCE +#include "meow_ui.h" +#include "meow_ui_internal.h" /* meow_gfx_blit_rgba 原型(本测试只给桩) */ +#include "meow_jpeg.h" /* 后端入口(内部头):直接比对「后端 vs 回退」 */ +#include "stb_image.h" /* 旧 _dim 路径基线(stbi_load_scaled,M23 之前的行为) */ + +#include +#include +#include +#include +#include +#include /* clock():解码耗时打印 */ +#include /* sqrt:结构相关系数 */ + +/* ------------------------------------------------------------------ */ +/* 分配打点 */ +/* ------------------------------------------------------------------ */ +#define AHDR 32 + +static size_t g_live = 0; +static size_t g_peak = 0; + +extern void *__real_malloc(size_t); +extern void *__real_calloc(size_t, size_t); +extern void *__real_realloc(void *, size_t); +extern void __real_free(void *); + +static void acc_alloc(size_t n) +{ + g_live += n; + if (g_live > g_peak) g_peak = g_live; +} + +void *__wrap_malloc(size_t n) +{ + size_t *h = (size_t *)__real_malloc(n + AHDR); + if (!h) return NULL; + h[0] = n; + acc_alloc(n); + return (char *)h + AHDR; +} + +void *__wrap_calloc(size_t a, size_t b) +{ + size_t n = a * b; + void *p = __wrap_malloc(n); + if (p) memset(p, 0, n); + return p; +} + +void __wrap_free(void *p) +{ + size_t *h; + if (!p) return; + h = (size_t *)((char *)p - AHDR); + g_live -= h[0]; + __real_free(h); +} + +void *__wrap_realloc(void *p, size_t n) +{ + size_t old = p ? ((size_t *)((char *)p - AHDR))[0] : 0; + void *q = __wrap_malloc(n); + if (!q) return NULL; + if (p) { + memcpy(q, p, old < n ? old : n); + __wrap_free(p); + } + return q; +} + +static size_t peak_reset(void) { g_peak = g_live; return g_live; } +static size_t peak_since(size_t base) { return g_peak - base; } + +/* meow_ui_img.c 的绘制函数需要的 gfx 符号:本测试只测解码/内存,桩即可。 */ +void meow_gfx_blit_rgba(int x, int y, int w, int h, const uint8_t *rgba, int alpha) +{ + (void)x; (void)y; (void)w; (void)h; (void)rgba; (void)alpha; +} + +/* ------------------------------------------------------------------ */ +/* 断言与打印 */ +/* ------------------------------------------------------------------ */ +static int g_fail = 0; +#define CHECK(cond, name) do { \ + if (cond) printf(" PASS %s\n", name); \ + else { printf(" FAIL %s (%s:%d)\n", name, __FILE__, __LINE__); g_fail++; } } while (0) + +#define PEAK_2500KB ((size_t)2560 * 1024) /* 2.5 MiB */ + +static void peak_report(const char *tag, size_t bytes) +{ + printf(" peak %-32s %7.2f MiB (%zu B)\n", tag, (double)bytes / (1024.0 * 1024.0), bytes); +} + +/* ------------------------------------------------------------------ */ +/* 夹具 */ +/* ------------------------------------------------------------------ */ +static const char *g_dir = NULL; + +static const char *g_gen_py = +"import os, sys, zlib, struct\n" +"from PIL import Image, ImageDraw\n" +"out = sys.argv[1]\n" +"os.makedirs(out, exist_ok=True)\n" +"def pat(w, h, mode='RGB'):\n" +" im = Image.new(mode, (w, h), (250, 249, 245) if mode == 'RGB' else 200)\n" +" d = ImageDraw.Draw(im)\n" +" for k in range(0, min(w, h) - 200, 40):\n" +" d.rectangle([40 + k, 40 + k, 200 + k, 180 + k], outline=(12, 12, 14), width=3)\n" +" for band in range(14):\n" +" y0 = band * h // 14\n" +" for x in range(0, w, 7):\n" +" d.line([(x, y0), (x + 3, y0 + 5)], fill=(110, 110, 116))\n" +" for i, c in enumerate([(220, 30, 40), (30, 180, 60), (40, 60, 220), (240, 200, 20)]):\n" +" d.rectangle([w // 2 + i * 30, h // 2, w // 2 + i * 30 + 25, h // 2 + 60], fill=c)\n" +" return im\n" +"big = pat(2000, 3000)\n" +"big.save(os.path.join(out, 'base2000.jpg'), 'JPEG', quality=60, subsampling=2)\n" +"big.save(os.path.join(out, 'prog2000.jpg'), 'JPEG', quality=60, subsampling=2, progressive=True)\n" +"big.save(os.path.join(out, 'base2000_444.jpg'), 'JPEG', quality=60, subsampling=0)\n" +"big.save(os.path.join(out, 'base2000_422.jpg'), 'JPEG', quality=60, subsampling=1)\n" +"big.save(os.path.join(out, 'restart2000.jpg'), 'JPEG', quality=60, subsampling=2,\n" +" restart_marker_blocks=8)\n" +"pat(640, 480).save(os.path.join(out, 'sweep640.jpg'), 'JPEG', quality=70, subsampling=2)\n" +"big.convert('L').save(os.path.join(out, 'gray2000.jpg'), 'JPEG', quality=60)\n" +"photo = Image.blend(big, Image.effect_noise((2000, 3000), 40).convert('RGB'), 0.35)\n" +"photo.save(os.path.join(out, 'photo2000.jpg'), 'JPEG', quality=75, subsampling=2)\n" +"photo.save(os.path.join(out, 'photo2000p.jpg'), 'JPEG', quality=75, subsampling=2, progressive=True)\n" +"pat(1600, 2400).save(os.path.join(out, 'page1600.png'), 'PNG')\n" +"pat(1600, 2400).save(os.path.join(out, 'page1600.bmp'), 'BMP')\n" +"pat(2320, 1740).save(os.path.join(out, 'mp4.jpg'), 'JPEG', quality=60, subsampling=2)\n" +"pat(3264, 2448).save(os.path.join(out, 'mp8.jpg'), 'JPEG', quality=60, subsampling=2)\n" +"pat(4000, 3000).save(os.path.join(out, 'mp12.jpg'), 'JPEG', quality=60, subsampling=2)\n" +"pat(64, 64).save(os.path.join(out, 'small64.jpg'), 'JPEG', quality=80)\n" +"Image.open(os.path.join(out, 'page1600.png')).convert('L').save(os.path.join(out, 'gray.png'))\n" +"pat(800, 600).convert('P', palette=Image.ADAPTIVE, colors=64).save(os.path.join(out, 'pal.png'))\n" +"pat(800, 600).convert('RGBA').save(os.path.join(out, 'rgba.png'))\n" +"big.convert('CMYK').save(os.path.join(out, 'cmyk.jpg'), 'JPEG', quality=60)\n" +"Image.open(os.path.join(out, 'page1600.png')).convert('1').save(os.path.join(out, 'mono1.bmp'))\n" +"def chunk(t, d):\n" +" c = struct.pack('>I', len(d)) + t + d\n" +" return c + struct.pack('>I', zlib.crc32(t + d) & 0xffffffff)\n" +"def adam7(path, w, h):\n" +" raw = bytearray()\n" +" passes = [(0,0,8,8),(4,0,8,8),(0,4,4,8),(2,0,4,4),(0,2,2,4),(1,0,2,2),(0,1,1,2)]\n" +" for (x0, y0, dx, dy) in passes:\n" +" xs = list(range(x0, w, dx)); ys = list(range(y0, h, dy))\n" +" if not xs or not ys:\n" +" continue\n" +" for y in ys:\n" +" raw.append(0)\n" +" for x in xs:\n" +" raw += bytes(((x * 3) & 255, (y * 3) & 255, ((x + y) * 2) & 255))\n" +" ihdr = struct.pack('>IIBBBBB', w, h, 8, 2, 0, 0, 1)\n" +" data = (b'\\x89PNG\\r\\n\\x1a\\n' + chunk(b'IHDR', ihdr) +\n" +" chunk(b'IDAT', zlib.compress(bytes(raw))) + chunk(b'IEND', b''))\n" +" open(path, 'wb').write(data)\n" +"adam7(os.path.join(out, 'adam7.png'), 1600, 2400)\n" +"for rb in (1, 2, 3, 4, 5, 8, 16, 64, 200):\n" +" big.save(os.path.join(out, 'prog_rst%d.jpg' % rb), 'JPEG', quality=60,\n" +" subsampling=2, progressive=True, restart_marker_blocks=rb)\n" +"white = Image.new('RGB', (2000, 3000), (255, 255, 255))\n" +"white.save(os.path.join(out, 'white2000p.jpg'), 'JPEG', quality=60,\n" +" subsampling=2, progressive=True)\n" +"for _q in (50, 70, 80):\n" +" white.save(os.path.join(out, 'white2000p%d.jpg' % _q), 'JPEG', quality=_q,\n" +" subsampling=2, progressive=True)\n" +"mw = Image.new('RGB', (2000, 3000), (255, 255, 255))\n" +"dmw = ImageDraw.Draw(mw)\n" +"dmw.rectangle([60, 60, 260, 180], fill=(0, 0, 0))\n" +"dmw.line([(300, 200), (1900, 200)], fill=(20, 20, 20), width=2)\n" +"mw.save(os.path.join(out, 'mostlywhite2000p.jpg'), 'JPEG', quality=95,\n" +" subsampling=2, progressive=True)\n" +"g8 = Image.new('L', (640, 480), 0)\n" +"dg = ImageDraw.Draw(g8)\n" +"dg.rectangle([100, 100, 400, 300], fill=255)\n" +"g8.save(os.path.join(out, 'gray8_trns.png'), 'PNG', transparency=0)\n" +"r8 = Image.new('RGB', (640, 480), (255, 0, 255))\n" +"dr = ImageDraw.Draw(r8)\n" +"dr.rectangle([100, 100, 400, 300], fill=(10, 200, 30))\n" +"r8.save(os.path.join(out, 'rgb8_trns.png'), 'PNG', transparency=(255, 0, 255))\n" +"open(os.path.join(out, 'broken.jpg'), 'wb').write(b'\\xff\\xd8\\xff\\xe0' +\n" +" bytes((i * 37 + 11) & 0xFF for i in range(256)))\n" +"for n in sorted(os.listdir(out)):\n" +" print(' %-14s %9d B' % (n, os.path.getsize(os.path.join(out, n))))\n"; + +static char *path_join(const char *name) +{ + size_t n = strlen(g_dir) + strlen(name) + 2; + char *p = (char *)malloc(n); + if (!p) { printf("OOM\n"); exit(1); } + snprintf(p, n, "%s/%s", g_dir, name); + return p; +} + +static int file_exists(const char *p) +{ + struct stat st; + return stat(p, &st) == 0 && st.st_size > 0; +} + +static void ensure_fixtures(void) +{ + /* 标记文件:新增夹具后改这里,旧夹具目录会自动补齐(生成脚本幂等) */ + char *m1 = path_join("base2000.jpg"); + char *m2 = path_join("white2000p.jpg"); + int have = file_exists(m1) && file_exists(m2); + free(m1); + free(m2); + if (have) return; + mkdir(g_dir, 0755); + printf("夹具缺失,生成到 %s(Pillow)\n", g_dir); + { + char *py = path_join("gen_stream_fixtures.py"); + char cmd[1200]; + FILE *f = fopen(py, "w"); + int rc; + if (!f) { printf("FATAL 无法写 %s\n", py); exit(1); } + fputs(g_gen_py, f); + fclose(f); + snprintf(cmd, sizeof cmd, "python3 '%s' '%s'", py, g_dir); + rc = system(cmd); + free(py); + if (rc != 0) { + printf("FATAL 夹具生成失败(缺 Pillow?)\n"); + exit(1); + } + } +} + +static uint8_t *read_whole(const char *p, size_t *len) +{ + FILE *f = fopen(p, "rb"); + uint8_t *buf; + long n; + if (!f) return NULL; + fseek(f, 0, SEEK_END); + n = ftell(f); + fseek(f, 0, SEEK_SET); + if (n <= 0) { fclose(f); return NULL; } + buf = (uint8_t *)malloc((size_t)n); + if (!buf) { fclose(f); return NULL; } + if (fread(buf, 1, (size_t)n, f) != (size_t)n) { free(buf); fclose(f); return NULL; } + *len = (size_t)n; + fclose(f); + return buf; +} + +/* ------------------------------------------------------------------ */ +/* 图像辅助 */ +/* ------------------------------------------------------------------ */ +static int nonblank(const meow_img *im) +{ + int w = meow_img_w(im), h = meow_img_h(im); + const unsigned char *p = meow_img_pixels(im); + long sum = 0; + if (!p || w <= 0 || h <= 0) return 0; + for (long i = 0; i < (long)w * h; i += 97) + sum += p[i * 4] + p[i * 4 + 1] + p[i * 4 + 2]; + return sum > 0; +} + +/* 求矩形区域的亮度均值(用于块均值相关:消除采样相位差与高频噪声,只看结构)。 */ +static double block_mean(const meow_img *im, int x0, int y0, int x1, int y1) +{ + int w = meow_img_w(im), h = meow_img_h(im); + const unsigned char *p = meow_img_pixels(im); + double s = 0; + long n = 0; + if (!p) return 0; + if (x1 > w) x1 = w; + if (y1 > h) y1 = h; + for (int y = y0; y < y1; y++) + for (int x = x0; x < x1; x++) { + const unsigned char *q = p + ((size_t)y * w + x) * 4; + s += (q[0] + q[1] + q[2]) / 3.0; + n++; + } + return n ? s / n : 0; +} + +/* 与「整幅 stb 解码 + meow_img_downscale」比对:按比例网格采样,返回各通道平均绝对差; + * corr 非空时同时给出「网格块均值」的亮度相关系数(结构一致性)。 */ +static double mean_abs_diff(const meow_img *a, const meow_img *b, int grid, double *corr) +{ + int wa = meow_img_w(a), ha = meow_img_h(a); + int wb = meow_img_w(b), hb = meow_img_h(b); + const unsigned char *pa = meow_img_pixels(a), *pb = meow_img_pixels(b); + long acc = 0, cnt = 0; + if (!pa || !pb || wa <= 0 || hb <= 0) return 1e9; + for (int gy = 0; gy < grid; gy++) { + int ya = (int)((long)gy * ha / grid); + int yb = (int)((long)gy * hb / grid); + for (int gx = 0; gx < grid; gx++) { + int xa = (int)((long)gx * wa / grid); + int xb = (int)((long)gx * wb / grid); + const unsigned char *qa = pa + ((size_t)ya * wa + xa) * 4; + const unsigned char *qb = pb + ((size_t)yb * wb + xb) * 4; + for (int k = 0; k < 3; k++) { + int d = (int)qa[k] - (int)qb[k]; + acc += d < 0 ? -d : d; + cnt++; + } + } + } + if (corr && cnt) { + double s1 = 0, s2 = 0, s11 = 0, s22 = 0, s12 = 0; + long n = 0; + for (int gy = 0; gy < grid; gy++) { + int ya0 = (int)((long)gy * ha / grid), ya1 = (int)((long)(gy + 1) * ha / grid); + int yb0 = (int)((long)gy * hb / grid), yb1 = (int)((long)(gy + 1) * hb / grid); + if (ya1 <= ya0) ya1 = ya0 + 1; + if (yb1 <= yb0) yb1 = yb0 + 1; + for (int gx = 0; gx < grid; gx++) { + int xa0 = (int)((long)gx * wa / grid), xa1 = (int)((long)(gx + 1) * wa / grid); + int xb0 = (int)((long)gx * wb / grid), xb1 = (int)((long)(gx + 1) * wb / grid); + double la, lb; + if (xa1 <= xa0) xa1 = xa0 + 1; + if (xb1 <= xb0) xb1 = xb0 + 1; + la = block_mean(a, xa0, ya0, xa1, ya1); + lb = block_mean(b, xb0, yb0, xb1, yb1); + s1 += la; s2 += lb; s11 += la * la; s22 += lb * lb; s12 += la * lb; + n++; + } + } + if (n > 1) { + double va = s11 - s1 * s1 / n, vb = s22 - s2 * s2 / n; + double cv = s12 - s1 * s2 / n; + *corr = (va > 1e-9 && vb > 1e-9) ? cv / sqrt(va * vb) : 1.0; + } else { + *corr = 1.0; + } + } + return cnt ? (double)acc / (double)cnt : 1e9; +} + +/* 旧入口整幅解码 → 参考缩略图(downscale 到 maxdim),用于像素比对。 */ +static meow_img *ref_thumb(const char *path, int maxdim) +{ + meow_img *full = meow_img_load_file(path); + meow_img *small; + if (!full) return NULL; + if (meow_img_w(full) <= maxdim && meow_img_h(full) <= maxdim) return full; + small = meow_img_downscale(full, maxdim); + meow_img_free(full); + return small; +} + +/* 一次「新入口」解码的峰值。 */ +static size_t peak_of_dim(const char *path, int maxdim, meow_img **out) +{ + size_t base = peak_reset(); + meow_img *im = meow_img_load_file_dim(path, maxdim); + size_t peak = peak_since(base); + if (out) *out = im; else meow_img_free(im); + return peak; +} + +/* 旧 _dim 路径基线(M23 之前的实现):stbi_load_scaled + 最近邻精修到 maxdim 的瞬时峰值。 + * 只用于报告里的「前后对比表」——这就是审计里 +10.1MB / +23.5MB / +26.2MB 的那条路径。 */ +static size_t old_dim_peak(const char *path, int maxdim) +{ + int w = 0, h = 0, c = 0, s = 1, longside; + size_t base, peak; + unsigned char *p, *small = NULL; + if (!stbi_info(path, &w, &h, &c)) return 0; + longside = w > h ? w : h; + base = peak_reset(); + while (s < 8 && (longside / (s * 2)) >= maxdim) s *= 2; + p = stbi_load_scaled(path, &w, &h, &c, 4, s); + if (!p) { peak = peak_since(base); return peak; } + if (w > maxdim || h > maxdim) { + int dw, dh; + if (w >= h) { dw = maxdim; dh = (int)((long)h * maxdim / w); } + else { dh = maxdim; dw = (int)((long)w * maxdim / h); } + if (dw < 1) dw = 1; + if (dh < 1) dh = 1; + small = (unsigned char *)malloc((size_t)dw * dh * 4); + if (small) { + for (int dy = 0; dy < dh; dy++) { + int sy = (int)((long)dy * h / dh); + for (int dx = 0; dx < dw; dx++) { + int sx = (int)((long)dx * w / dw); + memcpy(small + ((size_t)dy * dw + dx) * 4, p + ((size_t)sy * w + sx) * 4, 4); + } + } + } + } + peak = peak_since(base); + free(p); + free(small); + return peak; +} + +static long alpha_zero_count(const meow_img *im) +{ + int w = meow_img_w(im), h = meow_img_h(im); + const unsigned char *p = meow_img_pixels(im); + long n = 0; + if (!p) return 0; + for (long i = 0; i < (long)w * h; i++) if (p[i * 4 + 3] == 0) n++; + return n; +} + +/* 目标图 vs 整幅参考图的 alpha 一致性:按 nearest 反向映射采样源像素,返回不一致像素数 + * (-1 = 参数错误)。PNG 的采样公式与后端一致(tx*W/tw、ty*H/th),应逐点相等。 */ +static long alpha_mismatch(const meow_img *got, const meow_img *ref) +{ + int gw = meow_img_w(got), gh = meow_img_h(got); + int rw = meow_img_w(ref), rh = meow_img_h(ref); + const unsigned char *pg = meow_img_pixels(got), *pr = meow_img_pixels(ref); + long bad = 0; + if (!pg || !pr || gw <= 0 || gh <= 0) return -1; + for (int y = 0; y < gh; y++) { + int sy = (int)((long)y * rh / gh); + for (int x = 0; x < gw; x++) { + int sx = (int)((long)x * rw / gw); + if (pg[((size_t)y * gw + x) * 4 + 3] != pr[((size_t)sy * rw + sx) * 4 + 3]) + bad++; + } + } + return bad; +} + +/* ------------------------------------------------------------------ */ +/* P1-1 回归:过订阅 Huffman 表(畸形 DHT) */ +/* ------------------------------------------------------------------ */ +/* 构造「SOI + DHT(码长 lc 有 nsym 个符号) + EOI」的最小 JPEG(≤ 280B)。 */ +static size_t build_badhuff(unsigned char *buf, int lc, int nsym) +{ + size_t p = 0; + int i; + int seglen = 2 + 1 + 16 + nsym; /* DHT 长度字段计入自身 2 字节 */ + buf[p++] = 0xFF; buf[p++] = 0xD8; /* SOI */ + buf[p++] = 0xFF; buf[p++] = 0xC4; /* DHT */ + buf[p++] = (unsigned char)(seglen >> 8); + buf[p++] = (unsigned char)(seglen & 0xFF); + buf[p++] = 0x00; /* Tc=0, Th=0 */ + for (i = 1; i <= 16; i++) + buf[p++] = (unsigned char)(i == lc ? nsym : 0); + for (i = 0; i < nsym; i++) buf[p++] = (unsigned char)i; + buf[p++] = 0xFF; buf[p++] = 0xD9; /* EOI */ + return p; +} + +/* 把同一段坏 DHT 插到真实 JPEG 的 SOI 之后(其余字节原样):验证「有效文件 + 畸形 + * 码表」走的是「拒绝 + 回退」,而不是花屏/崩溃。 */ +static unsigned char *inject_badhuff(const unsigned char *orig, size_t n, + int lc, int nsym, size_t *outn) +{ + unsigned char seg[320]; + size_t all = build_badhuff(seg, lc, nsym); + size_t seglen = all - 4; /* 去掉首 FFD8 与尾 FFD9 */ + size_t total = n + seglen; + unsigned char *out = (unsigned char *)malloc(total); + if (!out) return NULL; + memcpy(out, orig, 2); /* SOI */ + memcpy(out + 2, seg + 2, seglen); /* 坏 DHT 段 */ + memcpy(out + 2 + seglen, orig + 2, n - 2); /* 原文件其余部分 */ + *outn = total; + return out; +} + +/* 全图逐像素比较(不是抽样):返回平均绝对差;*maxd = 最大单通道差; + * *big = 差值 > thr 的通道数。尺寸必须一致。 */ +static double full_diff(const meow_img *a, const meow_img *b, int *maxd, long *big, int thr) +{ + int w = meow_img_w(a), h = meow_img_h(a); + const unsigned char *pa = meow_img_pixels(a), *pb = meow_img_pixels(b); + long acc = 0, cnt = 0, nb = 0; + int mx = 0; + if (!pa || !pb || w <= 0 || h <= 0 || meow_img_w(b) != w || meow_img_h(b) != h) { + if (maxd) *maxd = 999; + if (big) *big = -1; + return 1e9; + } + for (long i = 0; i < (long)w * h; i++) + for (int k = 0; k < 3; k++) { + int d = (int)pa[i * 4 + k] - (int)pb[i * 4 + k]; + if (d < 0) d = -d; + if (d > mx) mx = d; + if (d > thr) nb++; + acc += d; + cnt++; + } + if (maxd) *maxd = mx; + if (big) *big = nb; + return cnt ? (double)acc / (double)cnt : 1e9; +} + +/* 全图最小通道值(丢 MCU 会露出 128 灰底,纯白图上立刻可见)。 */ +static int min_channel(const meow_img *im) +{ + int w = meow_img_w(im), h = meow_img_h(im); + const unsigned char *p = meow_img_pixels(im); + int mn = 255; + if (!p || w <= 0 || h <= 0) return -1; + for (long i = 0; i < (long)w * h; i++) + for (int k = 0; k < 3; k++) + if (p[i * 4 + k] < mn) mn = p[i * 4 + k]; + return mn; +} + +/* 矩形区域的最小通道值(丢 MCU 的灰块只出现在最后一块附近,看角落最灵敏)。 */ +static int region_min(const meow_img *im, int x0, int y0, int x1, int y1) +{ + int w = meow_img_w(im), h = meow_img_h(im); + const unsigned char *p = meow_img_pixels(im); + int mn = 255; + if (!p) return -1; + if (x0 < 0) x0 = 0; + if (y0 < 0) y0 = 0; + if (x1 > w) x1 = w; + if (y1 > h) y1 = h; + for (int y = y0; y < y1; y++) + for (int x = x0; x < x1; x++) + for (int k = 0; k < 3; k++) + if (p[((size_t)y * w + x) * 4 + k] < mn) mn = p[((size_t)y * w + x) * 4 + k]; + return mn; +} + +/* 构造「SOI + SOF0(W×H, ncomp) + EOI」的最小 JPEG(无扫描数据)。 */ +static size_t build_sof_only(unsigned char *buf, int w, int h, int ncomp) +{ + size_t p = 0; + int i; + int seglen = 2 + 6 + 3 * ncomp; + buf[p++] = 0xFF; buf[p++] = 0xD8; /* SOI */ + buf[p++] = 0xFF; buf[p++] = 0xC0; /* SOF0 */ + buf[p++] = (unsigned char)(seglen >> 8); + buf[p++] = (unsigned char)(seglen & 0xFF); + buf[p++] = 8; /* precision */ + buf[p++] = (unsigned char)((h >> 8) & 0xFF); buf[p++] = (unsigned char)(h & 0xFF); + buf[p++] = (unsigned char)((w >> 8) & 0xFF); buf[p++] = (unsigned char)(w & 0xFF); + buf[p++] = (unsigned char)ncomp; + for (i = 0; i < ncomp; i++) { + buf[p++] = (unsigned char)(i + 1); + buf[p++] = (unsigned char)((i == 0 && ncomp > 1) ? 0x22 : 0x11); /* 4:2:0 / 4:4:4 */ + buf[p++] = (unsigned char)(i ? 1 : 0); /* 量化表号 */ + } + buf[p++] = 0xFF; buf[p++] = 0xD9; /* EOI */ + return p; +} + +/* ================================================================== */ +int main(int argc, char **argv) +{ + char *p_base, *p_prog, *p_png, *p_bmp, *p_mp4, *p_mp8, *p_mp12; + char *p_small, *p_gray, *p_pal, *p_rgba, *p_cmyk, *p_mono, *p_adam7, *p_broken; + char *p_444, *p_422, *p_grayjpg, *p_photo, *p_photop; + + if (argc > 1) { + g_dir = argv[1]; + } else { + static char dflt[600]; + const char *tmp = getenv("TMPDIR"); + const char *pre = getenv("PREFIX"); + if (tmp && *tmp) snprintf(dflt, sizeof dflt, "%s/meow_img_stream", tmp); + else if (pre && *pre) snprintf(dflt, sizeof dflt, "%s/tmp/meow_img_stream", pre); + else snprintf(dflt, sizeof dflt, "/tmp/meow_img_stream"); + g_dir = dflt; + } + ensure_fixtures(); + + p_base = path_join("base2000.jpg"); + p_prog = path_join("prog2000.jpg"); + p_png = path_join("page1600.png"); + p_bmp = path_join("page1600.bmp"); + p_mp4 = path_join("mp4.jpg"); + p_mp8 = path_join("mp8.jpg"); + p_mp12 = path_join("mp12.jpg"); + p_small = path_join("small64.jpg"); + p_gray = path_join("gray.png"); + p_pal = path_join("pal.png"); + p_rgba = path_join("rgba.png"); + p_cmyk = path_join("cmyk.jpg"); + p_mono = path_join("mono1.bmp"); + p_adam7 = path_join("adam7.png"); + p_broken = path_join("broken.jpg"); + p_444 = path_join("base2000_444.jpg"); + p_422 = path_join("base2000_422.jpg"); + p_grayjpg = path_join("gray2000.jpg"); + p_photo = path_join("photo2000.jpg"); + p_photop = path_join("photo2000p.jpg"); + + if (!file_exists(p_base)) { printf("FATAL 夹具 %s 缺失\n", p_base); return 1; } + + /* ---- S1 入口透明 ---- */ + printf("[S1] 探针 / 小图 / maxdim<=0\n"); + { + int w = 0, h = 0, c = 0; + meow_img *a = meow_img_load_file(p_small); + meow_img *b = meow_img_load_file_dim(p_small, 64); + meow_img *z = meow_img_load_file_dim(p_small, 0); + CHECK(meow_img_probe_file(p_base, &w, &h, &c) == 1 && w == 2000 && h == 3000, + "probe base2000 → 2000x3000"); + CHECK(a && b && z, "小图三条路径都成功"); + CHECK(a && b && memcmp(meow_img_pixels(a), meow_img_pixels(b), + (size_t)meow_img_w(a) * meow_img_h(a) * 4) == 0, "小图逐字节与旧入口一致"); + CHECK(a && z && memcmp(meow_img_pixels(a), meow_img_pixels(z), + (size_t)meow_img_w(a) * meow_img_h(a) * 4) == 0, "maxdim=0 等价旧入口"); + CHECK(meow_img_probe_file(p_broken, NULL, NULL, NULL) == 0, "截断图 probe → 0"); + meow_img_free(a); meow_img_free(b); meow_img_free(z); + } + + /* ---- S2 baseline 2000×3000 ---- */ + printf("[S2] baseline 2000x3000(maxdim=720/480)\n"); + { + meow_img *im = NULL; + size_t pk720 = peak_of_dim(p_base, 720, &im); + size_t pk480, pk_old; + peak_report("新入口 _dim(720)", pk720); + CHECK(im && meow_img_w(im) == 480 && meow_img_h(im) == 720 && nonblank(im), + "结果 480x720 且非空"); + CHECK(pk720 <= PEAK_2500KB, "峰值 ≤ 2.5 MiB"); + meow_img_free(im); + pk480 = peak_of_dim(p_base, 480, NULL); + peak_report("新入口 _dim(480)", pk480); + CHECK(pk480 <= PEAK_2500KB, "_dim(480) 峰值 ≤ 2.5 MiB"); + { + size_t base = peak_reset(); + meow_img *old = meow_img_load_file(p_base); + pk_old = peak_since(base); + peak_report("旧入口 meow_img_load_file", pk_old); + CHECK(old && pk_old > 6 * pk720, "新入口峰值 < 旧入口 1/6(整幅不再物化)"); + meow_img_free(old); + } + } + + /* ---- S2b 其它色度采样/灰度/照片:峰值与压缩体积无关 ---- */ + printf("[S2b] 4:4:4 / 4:2:2 / 灰度 / 照片级噪声(同一 2000x3000, maxdim=720)\n"); + { + char *pe = path_join("base2000_444.jpg"); + char *p4 = path_join("base2000_422.jpg"); + char *pg = path_join("gray2000.jpg"); + char *pp = path_join("photo2000.jpg"); + char *pq = path_join("photo2000p.jpg"); + struct { const char *tag; const char *path; } fx[] = { + { "4:4:4", pe }, { "4:2:2", p4 }, { "gray", pg }, + { "photo baseline", pp }, { "photo progressive", pq }, + }; + for (size_t i = 0; i < sizeof fx / sizeof fx[0]; i++) { + meow_img *im = NULL; + size_t base = peak_reset(); + size_t pk; + long t0 = (long)clock(); + im = meow_img_load_file_dim(fx[i].path, 720); + pk = peak_since(base); + { + char name[128]; + struct stat st; + stat(fx[i].path, &st); + peak_report(fx[i].tag, pk); + printf(" 源文件 %ld KB,解码 %ld ms,输出 %dx%d\n", + (long)st.st_size / 1024, + (long)((clock() - t0) * 1000 / CLOCKS_PER_SEC), + meow_img_w(im), meow_img_h(im)); + snprintf(name, sizeof name, "%s 峰值 ≤ 2.5 MiB 且非空", fx[i].tag); + CHECK(im && nonblank(im) && pk <= PEAK_2500KB, name); + } + meow_img_free(im); + } + free(pe); free(p4); free(pg); free(pp); free(pq); + } + + /* ---- S3 progressive 2000×3000 ---- */ + printf("[S3] progressive 2000x3000\n"); + { + meow_img *im = NULL; + size_t pk = peak_of_dim(p_prog, 720, &im); + peak_report("新入口 _dim(720) progressive", pk); + CHECK(im && meow_img_w(im) == 480 && meow_img_h(im) == 720 && nonblank(im), + "结果 480x720 且非空"); + CHECK(pk <= PEAK_2500KB, "渐进式峰值 ≤ 2.5 MiB(不整幅驻留系数)"); + { + size_t base = peak_reset(); + meow_img *old = meow_img_load_file(p_prog); + size_t pk_old = peak_since(base); + peak_report("旧入口 progressive", pk_old); + CHECK(old && pk_old > 8 * pk, "渐进式新入口峰值 < 旧入口 1/8"); + meow_img_free(old); + } + meow_img_free(im); + } + + /* ---- S4 PNG ---- */ + printf("[S4] PNG 1600x2400\n"); + { + meow_img *im = NULL; + size_t pk = peak_of_dim(p_png, 720, &im); + peak_report("新入口 _dim(720) PNG", pk); + CHECK(im && meow_img_w(im) == 480 && meow_img_h(im) == 720 && nonblank(im), + "结果 480x720 且非空"); + CHECK(pk <= PEAK_2500KB, "PNG 峰值 ≤ 2.5 MiB(逐行 inflate)"); + { + size_t base = peak_reset(); + meow_img *old = meow_img_load_file(p_png); + size_t pk_old = peak_since(base); + peak_report("旧入口 PNG", pk_old); + CHECK(old && pk_old > 6 * pk, "PNG 新入口峰值 < 旧入口 1/6"); + meow_img_free(old); + } + meow_img_free(im); + } + + /* ---- S5 BMP ---- */ + printf("[S5] BMP 1600x2400\n"); + { + meow_img *im = NULL; + size_t pk = peak_of_dim(p_bmp, 720, &im); + peak_report("新入口 _dim(720) BMP", pk); + CHECK(im && meow_img_w(im) == 480 && meow_img_h(im) == 720 && nonblank(im), + "结果 480x720 且非空"); + CHECK(pk <= PEAK_2500KB, "BMP 峰值 ≤ 2.5 MiB(按行 seek)"); + meow_img_free(im); + } + + /* ---- S6 4/8/12MP:峰值不随分辨率线性增长 ---- */ + printf("[S6] 4MP/8MP/12MP JPEG(同一 maxdim=720)\n"); + { + size_t p4 = peak_of_dim(p_mp4, 720, NULL); + size_t p8 = peak_of_dim(p_mp8, 720, NULL); + size_t p12 = peak_of_dim(p_mp12, 720, NULL); + meow_img *big = NULL; + size_t pb = peak_of_dim(p_mp12, 720, &big); + (void)pb; + peak_report("4MP _dim(720)", p4); + peak_report("8MP _dim(720)", p8); + peak_report("12MP _dim(720)", p12); + CHECK(big && nonblank(big), "12MP 能解且非空"); + meow_img_free(big); + CHECK(p4 && p8 && p12, "4/8/12MP 全部解码成功"); + CHECK(p4 <= PEAK_2500KB && p8 <= PEAK_2500KB && p12 <= PEAK_2500KB, + "4/8/12MP 峰值均 ≤ 2.5 MiB"); + /* 像素数 3× 增长(4→12MP),线性则峰值 3×;DCT 缩解下应远小于。 */ + CHECK(p12 < p4 * 2, "12MP/4MP 峰值 < 2×(远非线性 3×)"); + printf(" 12MP/4MP 峰值比 = %.2f(像素比 3.00,旧入口约同上)\n", + (double)p12 / (double)p4); + } + + /* ---- S6b 前后对比表 ---- */ + printf("[S6b] 峰值前后对比(旧 _dim 路径 = stbi_load_scaled + 精修)\n"); + { + struct { const char *tag; const char *path; int md; } fx[] = { + { "2000x3000 baseline @720", p_base, 720 }, + { "2000x3000 progressive @720", p_prog, 720 }, + { "2000x3000 baseline @480", p_base, 480 }, + { "1600x2400 PNG @720", p_png, 720 }, + { "1600x2400 BMP @720", p_bmp, 720 }, + { "4MP JPEG @720", p_mp4, 720 }, + { "8MP JPEG @720", p_mp8, 720 }, + { "12MP JPEG @720", p_mp12, 720 }, + }; + for (size_t i = 0; i < sizeof fx / sizeof fx[0]; i++) { + size_t old = old_dim_peak(fx[i].path, fx[i].md); + meow_img *im = NULL; + size_t nw = peak_of_dim(fx[i].path, fx[i].md, &im); + char name[128]; + printf(" %-28s 旧 %6.2f MiB → 新 %5.2f MiB(÷%.1f)\n", fx[i].tag, + (double)old / 1048576.0, (double)nw / 1048576.0, + nw ? (double)old / (double)nw : 0.0); + snprintf(name, sizeof name, "%s 新峰值 ≤ 旧 1/3", fx[i].tag); + CHECK(old && nw && nw * 3 <= old, name); + meow_img_free(im); + } + } + + /* ---- S7 像素一致性 ---- */ + printf("[S7] 像素一致性(vs stb 整幅 + meow_img_downscale)\n"); + { + struct { const char *tag; const char *path; int md; double tol; double corr_min; } cases[] = { + { "baseline JPEG", p_base, 720, 24.0, 0.85 }, + { "progressive JPEG", p_prog, 720, 24.0, 0.85 }, + { "baseline JPEG", p_base, 300, 26.0, 0.85 }, + { "4:4:4 JPEG", p_444, 720, 24.0, 0.85 }, + { "4:2:2 JPEG", p_422, 720, 24.0, 0.85 }, + { "gray JPEG", p_grayjpg, 720, 24.0, 0.85 }, + { "photo baseline", p_photo, 720, 24.0, 0.85 }, + { "photo progressive", p_photop, 720, 26.0, 0.85 }, + { "PNG RGB", p_png, 720, 8.0, 0.999 }, + { "PNG gray", p_gray, 300, 8.0, 0.999 }, + { "PNG palette", p_pal, 300, 8.0, 0.999 }, + { "PNG RGBA", p_rgba, 300, 8.0, 0.999 }, + { "BMP", p_bmp, 720, 8.0, 0.999 }, + { "12MP JPEG", p_mp12, 720, 24.0, 0.85 }, + }; + for (size_t i = 0; i < sizeof cases / sizeof cases[0]; i++) { + meow_img *got = meow_img_load_file_dim(cases[i].path, cases[i].md); + meow_img *ref = ref_thumb(cases[i].path, cases[i].md); + char name[128]; + if (!got || !ref) { + snprintf(name, sizeof name, "%s 两侧都成功", cases[i].tag); + CHECK(0, name); + meow_img_free(got); meow_img_free(ref); + continue; + } + { + double corr = 0.0; + double d = mean_abs_diff(got, ref, 64, &corr); + snprintf(name, sizeof name, "%s _dim(%d) 平均差 %.2f < %.1f", + cases[i].tag, cases[i].md, d, cases[i].tol); + CHECK(d < cases[i].tol, name); + snprintf(name, sizeof name, "%s _dim(%d) 结构相关 %.3f > %.3f(非错位/反色/错位通道)", + cases[i].tag, cases[i].md, corr, cases[i].corr_min); + CHECK(corr > cases[i].corr_min, name); + snprintf(name, sizeof name, "%s _dim(%d) 尺寸与旧入口精修一致", + cases[i].tag, cases[i].md); + CHECK(meow_img_w(got) == meow_img_w(ref) && meow_img_h(got) == meow_img_h(ref), + name); + } + meow_img_free(got); + meow_img_free(ref); + } + } + + /* ---- S10 缩解阶扫描 + 重启标记(验证 N=4/5/7 基表与 RST 重同步) ---- */ + printf("[S10] 缩解阶扫描(N=4/5/7)与 restart marker\n"); + { + char *psw = path_join("sweep640.jpg"); + char *prs = path_join("restart2000.jpg"); + static const int dims[] = { 320, 400, 560 }; + for (size_t i = 0; i < sizeof dims / sizeof dims[0]; i++) { + meow_img *got = meow_img_load_file_dim(psw, dims[i]); + meow_img *ref = ref_thumb(psw, dims[i]); + char name[128]; + double corr = 0.0; + double d = (got && ref) ? mean_abs_diff(got, ref, 32, &corr) : 1e9; + snprintf(name, sizeof name, "640x480 _dim(%d) → %dx%d 平均差 %.2f 相关 %.3f", + dims[i], meow_img_w(got), meow_img_h(got), d, corr); + CHECK(got && ref && d < 24.0 && corr > 0.85 && nonblank(got), name); + meow_img_free(got); + meow_img_free(ref); + } + { + size_t base = peak_reset(); + int tw = 0, th = 0; + unsigned char *px = NULL; + int handled = meow_img_backend_file(prs, 720, &tw, &th, &px); + meow_img *got; + size_t pk; + meow_img *ref; + double corr = 0.0; + double d; + if (px) free(px); + base = peak_reset(); + got = meow_img_load_file_dim(prs, 720); + pk = peak_since(base); + ref = ref_thumb(prs, 720); + d = (got && ref) ? mean_abs_diff(got, ref, 64, &corr) : 1e9; + peak_report("restart marker 2000x3000", pk); + CHECK(handled == 1, "baseline + restart 走后端(BACKEND=1,不回退 stb)"); + CHECK(got && nonblank(got), "restart marker JPEG 解码成功"); + CHECK(d < 24.0 && corr > 0.85, "restart marker JPEG 与 stb 结构一致(RST 重同步正确)"); + CHECK(pk <= PEAK_2500KB, "restart marker JPEG 峰值 ≤ 2.5 MiB"); + meow_img_free(got); + meow_img_free(ref); + } + /* baseline + restart 与同参数无 restart 文件:RST 只切分熵段,解码结果应逐字节相同 */ + { + char *pbase = path_join("base2000.jpg"); + meow_img *a = meow_img_load_file_dim(prs, 720); + meow_img *b = meow_img_load_file_dim(pbase, 720); + CHECK(a && b && meow_img_w(a) == meow_img_w(b) && meow_img_h(a) == meow_img_h(b) && + memcmp(meow_img_pixels(a), meow_img_pixels(b), + (size_t)meow_img_w(a) * meow_img_h(a) * 4) == 0, + "baseline restart 与无 restart 结果逐字节一致(mad=0)"); + meow_img_free(a); + meow_img_free(b); + free(pbase); + } + free(psw); + free(prs); + } + + /* ---- S11 畸形 DHT(过订阅 Huffman 表):拒绝 + 回退,不崩 ---- */ + printf("[S11] 畸形 DHT 矩阵(码长 1..4 × 符号数 3..255)\n"); + { + unsigned char buf[320]; + int cases = 0, rejected = 0; + int lc, nsym; + for (lc = 1; lc <= 4; lc++) { + for (nsym = 3; nsym <= 255; nsym++) { + size_t n = build_badhuff(buf, lc, nsym); + int tw = 0, th = 0; + unsigned char *px = NULL; + cases++; + /* 崩溃即测试进程死;返回 1 说明过订阅表被当成合法表收下(缺陷) */ + if (meow_img_backend_mem(buf, n, 720, &tw, &th, &px) == 0) rejected++; + if (px) free(px); + } + } + { + char name[128]; + snprintf(name, sizeof name, "过订阅 DHT 全部拒绝(%d/%d,无崩溃)", rejected, cases); + CHECK(rejected == cases, name); + } + /* 有效 JPEG + 注入坏 DHT:后端必须拒绝;公共入口不得崩(stb 兜底或返回 NULL) */ + { + size_t flen = 0, ilen = 0; + uint8_t *real = read_whole(p_base, &flen); + CHECK(real != NULL, "读入 baseline JPEG 用于注入"); + if (real) { + uint8_t *inj = inject_badhuff(real, flen, 2, 100, &ilen); + int tw = 0, th = 0; + unsigned char *px = NULL; + meow_img *im; + CHECK(inj && meow_img_backend_mem(inj, ilen, 720, &tw, &th, &px) == 0, + "有效 JPEG + 注入过订阅 DHT → 后端拒绝"); + if (px) free(px); + im = inj ? meow_img_load_mem_dim(inj, (int)ilen, 720) : NULL; + CHECK(im == NULL || nonblank(im), "注入坏表后公共入口不崩、不花屏"); + meow_img_free(im); + free(inj); + free(real); + } + } + } + + /* ---- S12 渐进式 + restart marker(P1-2 回归) ---- */ + printf("[S12] progressive + restart marker(BACKEND 必须为 1,峰值 ≤2.5MiB)\n"); + { + static const int rb[] = { 1, 2, 3, 4, 5, 8, 16, 64, 200 }; + meow_img *ref = meow_img_load_file_dim(p_prog, 720); + int all_backend = 1, all_peak = 1, all_equal = 1; + for (size_t i = 0; i < sizeof rb / sizeof rb[0]; i++) { + char fname[48]; + char *p; + int tw = 0, th = 0; + unsigned char *px = NULL; + meow_img *im; + size_t base, pk; + snprintf(fname, sizeof fname, "prog_rst%d.jpg", rb[i]); + p = path_join(fname); + if (meow_img_backend_file(p, 720, &tw, &th, &px) != 1) all_backend = 0; + if (px) free(px); + base = peak_reset(); + im = meow_img_load_file_dim(p, 720); + pk = peak_since(base); + if (!(im && nonblank(im) && pk <= PEAK_2500KB)) all_peak = 0; + /* 严格全图逐字节比较(不是网格抽样:64×64=4096 个采样点看不到右下角)。 + * restart 只切分熵段、不改系数流,任何差异都是解码器错误。 */ + if (!(im && ref && meow_img_w(im) == meow_img_w(ref) && meow_img_h(im) == meow_img_h(ref) && + memcmp(meow_img_pixels(im), meow_img_pixels(ref), + (size_t)meow_img_w(ref) * meow_img_h(ref) * 4) == 0)) { + all_equal = 0; + printf(" RST=%-3d 全图 memcmp 失败(与无 restart 不同)\n", rb[i]); + } + /* 关键正确性:与本文件自己的 stb 整幅解码比对(不同编码器脚本下 restart 文件的 + * 系数可能不同,所以拿「无 restart 文件」当基准只能作参考) */ + { + meow_img *rr = ref_thumb(p, 720); + double c3 = 0.0; + double d3 = (im && rr) ? mean_abs_diff(im, rr, 64, &c3) : 1e9; + if (!(d3 < 24.0 && c3 > 0.85)) all_peak = 0; + printf(" RST=%-3d vs stb 平均差 %.2f 相关 %.3f\n", rb[i], d3, c3); + meow_img_free(rr); + } + printf(" RST=%-3d → %dx%d peak %.2f MiB\n", rb[i], + meow_img_w(im), meow_img_h(im), (double)pk / 1048576.0); + free(p); + meow_img_free(im); + } + CHECK(all_backend == 1, "9 个 progressive+restart 全部走后端(BACKEND=1)"); + CHECK(all_peak == 1, "9 个 progressive+restart 全部解码成功且峰值 ≤2.5MiB"); + CHECK(all_equal == 1, "与无 restart 的同编码 progressive 结果逐字节一致(mad=0)"); + meow_img_free(ref); + } + + /* ---- S13 8bit gray/RGB PNG 的 tRNS 透明(P2-1 回归) ---- */ + printf("[S13] PNG 8bit tRNS(gray / RGB)\n"); + { + struct { const char *tag; const char *name; } fx[] = { + { "gray8 tRNS", "gray8_trns.png" }, + { "rgb8 tRNS", "rgb8_trns.png" }, + }; + for (size_t i = 0; i < sizeof fx / sizeof fx[0]; i++) { + char *p = path_join(fx[i].name); + int tw = 0, th = 0; + unsigned char *px = NULL; + int handled = meow_img_backend_file(p, 320, &tw, &th, &px); + meow_img *got; + meow_img *full; + char name[128]; + if (px) free(px); + got = meow_img_load_file_dim(p, 320); + full = meow_img_load_file(p); /* stb 整幅(tRNS 处理正确) */ + snprintf(name, sizeof name, "%s 走后端(BACKEND=1)", fx[i].tag); + CHECK(handled == 1, name); + snprintf(name, sizeof name, "%s 目标图有透明像素(%ld 个)", fx[i].tag, + alpha_zero_count(got)); + CHECK(got && alpha_zero_count(got) > 0, name); + snprintf(name, sizeof name, "%s alpha 与 stb 整幅逐点一致(不一致 %ld)", fx[i].tag, + alpha_mismatch(got, full)); + CHECK(got && full && alpha_mismatch(got, full) == 0, name); + meow_img_free(got); + meow_img_free(full); + free(p); + } + } + + /* ---- S14 M22 显式档位入口 _step:JPEG 走真缩解(PNG/BMP 待 M27 同步预算模型) ---- */ + printf("[S14] meow_img_load_file_step(M22 档位入口)JPEG 走后端\n"); + { + size_t base = peak_reset(); + meow_img *im = meow_img_load_file_step(p_base, 720, 8); + size_t pk = peak_since(base); + char name[128]; + snprintf(name, sizeof name, "_step(720,8) → %dx%d(长边 ≤720,非空)", + meow_img_w(im), meow_img_h(im)); + CHECK(im && nonblank(im) && meow_img_w(im) <= 720 && meow_img_h(im) <= 720, name); + peak_report("_step(720, 8)", pk); + CHECK(pk <= PEAK_2500KB, "_step 峰值 ≤2.5MiB(走后端,不整幅物化)"); + meow_img_free(im); + base = peak_reset(); + im = meow_img_load_file_step(p_base, 720, 1); + pk = peak_since(base); + CHECK(im && nonblank(im) && meow_img_w(im) <= 720, "_step(720,1) 成功且长边 ≤720"); + CHECK(pk <= PEAK_2500KB, "_step(720,1) 峰值 ≤2.5MiB"); + meow_img_free(im); + } + + /* ---- S15 低熵/纯白 progressive(P1-A 回归:最后一个 MCU 不许丢) ---- */ + printf("[S15] 低熵/纯白 progressive 全图逐像素(丢 MCU 会露出 128 灰块)\n"); + { + struct { const char *tag; const char *name; int all_white; } fx[] = { + { "pure white q60", "white2000p.jpg", 1 }, + { "pure white q50", "white2000p50.jpg", 1 }, + { "pure white q70", "white2000p70.jpg", 1 }, + { "pure white q80", "white2000p80.jpg", 1 }, + { "mostly white q95", "mostlywhite2000p.jpg", 0 }, + }; + for (size_t i = 0; i < sizeof fx / sizeof fx[0]; i++) { + char *p = path_join(fx[i].name); + meow_img *im = meow_img_load_file_dim(p, 720); + meow_img *ref = ref_thumb(p, 720); /* stb 整幅 + 同公式降采样 */ + char name[128]; + int mn = min_channel(im); + int cw = meow_img_w(im), ch = meow_img_h(im); + int corner = region_min(im, cw - 24, ch - 24, cw, ch); /* 最后一个 MCU 落点 */ + int maxd = 0; + long big = -1; + double mean = (im && ref) ? full_diff(im, ref, &maxd, &big, 40) : 1e9; + snprintf(name, sizeof name, "%s 解码成功且 480x720", fx[i].tag); + CHECK(im && nonblank(im) && cw == 480 && ch == 720, name); + /* 丢 MCU 会露出平面 memset(128) 的灰底:右下角与全图最小值都会掉到 128 附近。 + * 纯白图全图最小值 ≥250;mostly-white 图有黑色矩形,只看右下角这块。 */ + if (fx[i].all_white) { + snprintf(name, sizeof name, "%s 全图最小通道 = %d(丢块会露 128)", fx[i].tag, mn); + CHECK(mn >= 250, name); + } + snprintf(name, sizeof name, "%s 右下角 24x24 最小通道 = %d(丢块会露 128)", + fx[i].tag, corner); + CHECK(corner >= 250, name); + snprintf(name, sizeof name, "%s 与 stb 全图比较:平均差 %.2f,最大差 %d,>40 的通道 %ld", + fx[i].tag, mean, maxd, big); + CHECK(mean < 24.0 && big >= 0 && big * 200 < (long)720 * 480 * 3, name); + meow_img_free(im); + meow_img_free(ref); + free(p); + } + } + + /* ---- S16 畸形 SOF 声明尺寸(P1-B 回归:不得按声明尺寸分配) ---- */ + printf("[S16] 畸形 SOF 65535x65535(1/3 分量)\n"); + { + unsigned char buf[64]; + static const int nc[2] = { 1, 3 }; + for (size_t i = 0; i < sizeof nc / sizeof nc[0]; i++) { + size_t n = build_sof_only(buf, 65535, 65535, nc[i]); + int tw = 0, th = 0; + unsigned char *px = NULL; + char name[128]; + char *fp = path_join(nc[i] == 1 ? "sof65535_1.jpg" : "sof65535_3.jpg"); + FILE *f; + size_t base, pk; + meow_img *im; + snprintf(name, sizeof name, "SOF 65535² ncomp=%d → 后端拒绝(%zu 字节)", nc[i], n); + CHECK(meow_img_backend_mem(buf, n, 720, &tw, &th, &px) == 0, name); + if (px) free(px); + f = fopen(fp, "wb"); + CHECK(f != NULL, "写临时畸形 SOF 文件"); + if (f) { fwrite(buf, 1, n, f); fclose(f); } + base = peak_reset(); + im = meow_img_load_file_dim(fp, 720); + pk = peak_since(base); + peak_report("_dim(720) SOF 65535²", pk); + snprintf(name, sizeof name, "ncomp=%d 公开入口返回 NULL(安全失败)", nc[i]); + CHECK(im == NULL, name); + snprintf(name, sizeof name, "ncomp=%d 峰值 < 2.5MiB(不按声明尺寸分配)", nc[i]); + CHECK(pk < PEAK_2500KB, name); + meow_img_free(im); + free(fp); + } + } + + /* ---- S8 内存入口 ---- */ + printf("[S8] 内存入口 meow_img_load_mem_dim\n"); + { + size_t len = 0; + uint8_t *buf = read_whole(p_base, &len); + CHECK(buf != NULL, "读入 baseline JPEG"); + if (buf) { + size_t base = peak_reset(); + meow_img *m = meow_img_load_mem_dim(buf, (int)len, 720); + size_t pk = peak_since(base); + meow_img *f = meow_img_load_file_dim(p_base, 720); + peak_report("_mem_dim(720)", pk); + CHECK(m && f && meow_img_w(m) == meow_img_w(f) && meow_img_h(m) == meow_img_h(f), + "内存入口尺寸一致"); + CHECK(m && f && memcmp(meow_img_pixels(m), meow_img_pixels(f), + (size_t)meow_img_w(f) * meow_img_h(f) * 4) == 0, "内存入口逐字节一致"); + CHECK(pk <= PEAK_2500KB, "内存入口峰值 ≤ 2.5 MiB"); + meow_img_free(m); + meow_img_free(f); + free(buf); + } + } + + /* ---- S9 回退与容错 ---- */ + printf("[S9] 回退(新后端拒绝 → stb)与坏图\n"); + { + int tw = 0, th = 0; + unsigned char *px = NULL; + meow_img *im; + /* CMYK(4 分量):后端必须拒绝,stb 必须兜住 */ + CHECK(meow_img_backend_file(p_cmyk, 720, &tw, &th, &px) == 0 && px == NULL, + "CMYK JPEG → 后端拒绝"); + im = meow_img_load_file_dim(p_cmyk, 720); + CHECK(im && nonblank(im), "CMYK JPEG → stb 回退成功"); + meow_img_free(im); + /* 1bpp BMP */ + CHECK(meow_img_backend_file(p_mono, 720, &tw, &th, &px) == 0 && px == NULL, + "1bpp BMP → 后端拒绝"); + im = meow_img_load_file_dim(p_mono, 720); + CHECK(im && nonblank(im), "1bpp BMP → stb 回退成功"); + meow_img_free(im); + /* Adam7 交错 PNG */ + CHECK(meow_img_backend_file(p_adam7, 720, &tw, &th, &px) == 0 && px == NULL, + "Adam7 PNG → 后端拒绝"); + im = meow_img_load_file_dim(p_adam7, 720); + CHECK(im && nonblank(im), "Adam7 PNG → stb 回退成功"); + meow_img_free(im); + /* 坏图 */ + CHECK(meow_img_load_file_dim(p_broken, 480) == NULL, "截断 JPEG → NULL"); + CHECK(meow_img_load_file_dim("/no/such/file.jpg", 480) == NULL, "不存在路径 → NULL"); + CHECK(meow_img_load_file_dim(NULL, 480) == NULL, "NULL 路径 → NULL"); + CHECK(meow_img_load_mem_dim("x", 3, 480) == NULL, "垃圾内存 → NULL"); + } + + if (g_fail) printf("\nRESULT: FAIL (%d)\n", g_fail); + else printf("\nRESULT: ALL PASS\n"); + free(p_base); free(p_prog); free(p_png); free(p_bmp); + free(p_mp4); free(p_mp8); free(p_mp12); free(p_small); + free(p_gray); free(p_pal); free(p_rgba); free(p_cmyk); + free(p_mono); free(p_adam7); free(p_broken); + free(p_444); free(p_422); free(p_grayjpg); free(p_photo); free(p_photop); + return g_fail ? 1 : 0; +} diff --git a/patch_v2/sdk/test_meow_lyric_view.c b/patch_v2/sdk/test_meow_lyric_view.c index 02e849f..eaabbc7 100644 --- a/patch_v2/sdk/test_meow_lyric_view.c +++ b/patch_v2/sdk/test_meow_lyric_view.c @@ -8,8 +8,8 @@ * meow_ui_gfx.c meow_ui_time.c meow_ui_sys.c meow_ui_theme.c meow_ui_vol.c meow_ui_log.c \ * meow_ui_widget.c meow_ui_input.c meow_ui_activity.c meow_ui_dialog.c meow_ui_ime.c meow_ui_str.c \ * meow_pinyin.c meow_ui_draw.c meow_ui_view.c meow_ui_box.c meow_ui_anim.c meow_ui_scroll.c \ - * meow_ui_img.c meow_ui_media.c meow_notify_ipc.c stb_image.c \ - * -I. -I../meow_launcherd -DMEOW_UI_HOST_TEST -pthread -lm + * meow_ui_img.c meow_ui_media.c meow_notify_ipc.c stb_image.c meow_jpeg.c meow_png_stream.c \ + * -I. -I../meow_launcherd -DMEOW_UI_HOST_TEST -pthread -lm -lz * 运行:./test_meow_lyric_view * * 覆盖矩阵(对齐任务批2:活动行纯白/灰阶、翻译行占位不叠行、偏上锚定、折行中西文、点行 seek 命中、 diff --git a/patch_v2/sdk/test_meow_md3.c b/patch_v2/sdk/test_meow_md3.c index 58363c7..60730c9 100644 --- a/patch_v2/sdk/test_meow_md3.c +++ b/patch_v2/sdk/test_meow_md3.c @@ -9,8 +9,8 @@ * meow_ui_gfx.c meow_ui_time.c meow_ui_sys.c meow_ui_theme.c meow_ui_vol.c meow_ui_log.c \ * meow_ui_widget.c meow_ui_input.c meow_ui_activity.c meow_ui_dialog.c meow_ui_ime.c meow_ui_str.c \ * meow_pinyin.c meow_ui_draw.c meow_ui_view.c meow_ui_box.c meow_ui_anim.c meow_ui_scroll.c \ - * meow_ui_img.c meow_ui_media.c meow_notify_ipc.c stb_image.c \ - * -I. -I../meow_launcherd -DMEOW_UI_HOST_TEST -pthread -lm + * meow_ui_img.c meow_ui_media.c meow_notify_ipc.c stb_image.c meow_jpeg.c meow_png_stream.c \ + * -I. -I../meow_launcherd -DMEOW_UI_HOST_TEST -pthread -lm -lz * 运行:./test_meow_md3 */ #include "meow_ui.h" diff --git a/patch_v2/sdk/test_meow_ui_anim.c b/patch_v2/sdk/test_meow_ui_anim.c index bf913ef..323ea3a 100644 --- a/patch_v2/sdk/test_meow_ui_anim.c +++ b/patch_v2/sdk/test_meow_ui_anim.c @@ -7,8 +7,8 @@ * meow_ui_gfx.c meow_ui_time.c meow_ui_sys.c meow_ui_theme.c meow_ui_vol.c meow_ui_log.c \ * meow_ui_widget.c meow_ui_input.c meow_ui_activity.c meow_ui_dialog.c meow_ui_ime.c meow_ui_str.c \ * meow_pinyin.c meow_ui_draw.c meow_ui_view.c meow_ui_box.c meow_ui_anim.c meow_ui_scroll.c \ - * meow_ui_img.c meow_ui_media.c meow_notify_ipc.c stb_image.c \ - * -I. -I../meow_launcherd -DMEOW_UI_HOST_TEST -pthread -lm + * meow_ui_img.c meow_ui_media.c meow_notify_ipc.c stb_image.c meow_jpeg.c meow_png_stream.c \ + * -I. -I../meow_launcherd -DMEOW_UI_HOST_TEST -pthread -lm -lz * 运行:./test_meow_ui_anim */ #include "meow_ui.h" diff --git a/patch_v2/sdk/test_meow_ui_box.c b/patch_v2/sdk/test_meow_ui_box.c index 2d89199..95ad0c7 100644 --- a/patch_v2/sdk/test_meow_ui_box.c +++ b/patch_v2/sdk/test_meow_ui_box.c @@ -7,8 +7,8 @@ * meow_ui_gfx.c meow_ui_time.c meow_ui_sys.c meow_ui_theme.c meow_ui_vol.c meow_ui_log.c \ * meow_ui_widget.c meow_ui_input.c meow_ui_activity.c meow_ui_dialog.c meow_ui_ime.c meow_ui_str.c \ * meow_pinyin.c meow_ui_draw.c meow_ui_view.c meow_ui_box.c meow_ui_anim.c meow_ui_scroll.c \ - * meow_ui_img.c meow_ui_media.c meow_notify_ipc.c stb_image.c \ - * -I. -I../meow_launcherd -DMEOW_UI_HOST_TEST -pthread -lm + * meow_ui_img.c meow_ui_media.c meow_notify_ipc.c stb_image.c meow_jpeg.c meow_png_stream.c \ + * -I. -I../meow_launcherd -DMEOW_UI_HOST_TEST -pthread -lm -lz * 运行:./test_meow_ui_box */ #include "meow_ui.h" diff --git a/patch_v2/sdk/test_meow_ui_dispatch.c b/patch_v2/sdk/test_meow_ui_dispatch.c index 1265e31..685327c 100644 --- a/patch_v2/sdk/test_meow_ui_dispatch.c +++ b/patch_v2/sdk/test_meow_ui_dispatch.c @@ -16,8 +16,8 @@ * meow_ui_gfx.c meow_ui_time.c meow_ui_sys.c meow_ui_theme.c meow_ui_vol.c meow_ui_log.c \ * meow_ui_widget.c meow_ui_input.c meow_ui_activity.c meow_ui_dialog.c meow_ui_ime.c meow_ui_str.c \ * meow_pinyin.c meow_ui_draw.c meow_ui_view.c meow_ui_box.c meow_ui_anim.c meow_ui_scroll.c \ - * meow_ui_img.c meow_ui_media.c meow_notify_ipc.c stb_image.c \ - * -I. -I../meow_launcherd -DMEOW_UI_HOST_TEST -pthread -lm + * meow_ui_img.c meow_ui_media.c meow_notify_ipc.c stb_image.c meow_jpeg.c meow_png_stream.c \ + * -I. -I../meow_launcherd -DMEOW_UI_HOST_TEST -pthread -lm -lz * 运行:./test_meow_ui_dispatch (无设备,纯宿主) */ #include "meow_ui.h" diff --git a/patch_v2/sdk/test_meow_ui_home_gesture.c b/patch_v2/sdk/test_meow_ui_home_gesture.c index 0fbe5eb..dd3be89 100644 --- a/patch_v2/sdk/test_meow_ui_home_gesture.c +++ b/patch_v2/sdk/test_meow_ui_home_gesture.c @@ -19,8 +19,8 @@ * meow_ui_gfx.c meow_ui_time.c meow_ui_sys.c meow_ui_theme.c meow_ui_vol.c meow_ui_log.c \ * meow_ui_widget.c meow_ui_input.c meow_ui_activity.c meow_ui_dialog.c meow_ui_ime.c meow_ui_str.c \ * meow_pinyin.c meow_ui_draw.c meow_ui_view.c meow_ui_box.c meow_ui_anim.c meow_ui_scroll.c \ - * meow_ui_img.c meow_ui_media.c meow_notify_ipc.c stb_image.c \ - * -I. -I../meow_launcherd -DMEOW_UI_HOST_TEST -pthread -lm + * meow_ui_img.c meow_ui_media.c meow_notify_ipc.c stb_image.c meow_jpeg.c meow_png_stream.c \ + * -I. -I../meow_launcherd -DMEOW_UI_HOST_TEST -pthread -lm -lz * 运行:./test_meow_ui_home_gesture */ #include "meow_ui.h" diff --git a/patch_v2/sdk/test_meow_ui_iconbtn.c b/patch_v2/sdk/test_meow_ui_iconbtn.c index 7014c68..35ad8cc 100644 --- a/patch_v2/sdk/test_meow_ui_iconbtn.c +++ b/patch_v2/sdk/test_meow_ui_iconbtn.c @@ -12,8 +12,8 @@ * meow_ui_gfx.c meow_ui_time.c meow_ui_sys.c meow_ui_theme.c meow_ui_vol.c meow_ui_log.c \ * meow_ui_widget.c meow_ui_input.c meow_ui_activity.c meow_ui_dialog.c meow_ui_ime.c meow_ui_str.c \ * meow_pinyin.c meow_ui_draw.c meow_ui_view.c meow_ui_box.c meow_ui_anim.c meow_ui_scroll.c \ - * meow_ui_img.c meow_ui_media.c meow_notify_ipc.c stb_image.c \ - * -I. -I../meow_launcherd -DMEOW_UI_HOST_TEST -pthread -lm + * meow_ui_img.c meow_ui_media.c meow_notify_ipc.c stb_image.c meow_jpeg.c meow_png_stream.c \ + * -I. -I../meow_launcherd -DMEOW_UI_HOST_TEST -pthread -lm -lz * 运行:./test_meow_ui_iconbtn */ #include "meow_ui.h" diff --git a/patch_v2/sdk/test_meow_ui_lazygrid.c b/patch_v2/sdk/test_meow_ui_lazygrid.c index f063bca..2038b94 100644 --- a/patch_v2/sdk/test_meow_ui_lazygrid.c +++ b/patch_v2/sdk/test_meow_ui_lazygrid.c @@ -11,8 +11,8 @@ * meow_ui_gfx.c meow_ui_time.c meow_ui_sys.c meow_ui_theme.c meow_ui_vol.c meow_ui_log.c \ * meow_ui_widget.c meow_ui_input.c meow_ui_activity.c meow_ui_dialog.c meow_ui_ime.c meow_ui_str.c \ * meow_pinyin.c meow_ui_draw.c meow_ui_view.c meow_ui_box.c meow_ui_anim.c meow_ui_scroll.c \ - * meow_lazygrid.c meow_ui_img.c meow_ui_media.c meow_notify_ipc.c stb_image.c \ - * -I. -I../meow_launcherd -DMEOW_UI_HOST_TEST -pthread -lm + * meow_lazygrid.c meow_ui_img.c meow_ui_media.c meow_notify_ipc.c stb_image.c meow_jpeg.c meow_png_stream.c \ + * -I. -I../meow_launcherd -DMEOW_UI_HOST_TEST -pthread -lm -lz * 运行:./test_meow_ui_lazygrid */ #include "meow_ui.h" diff --git a/patch_v2/sdk/test_meow_ui_listview_rowh.c b/patch_v2/sdk/test_meow_ui_listview_rowh.c index b0b688f..1f9d9cc 100644 --- a/patch_v2/sdk/test_meow_ui_listview_rowh.c +++ b/patch_v2/sdk/test_meow_ui_listview_rowh.c @@ -20,8 +20,8 @@ * meow_ui_gfx.c meow_ui_time.c meow_ui_sys.c meow_ui_theme.c meow_ui_vol.c meow_ui_log.c \ * meow_ui_widget.c meow_ui_input.c meow_ui_activity.c meow_ui_dialog.c meow_ui_ime.c meow_ui_str.c \ * meow_pinyin.c meow_ui_draw.c meow_ui_view.c meow_ui_box.c meow_ui_anim.c meow_ui_scroll.c \ - * meow_ui_img.c meow_ui_media.c meow_notify_ipc.c stb_image.c meow_ui_nav.c \ - * -I. -I../meow_launcherd -DMEOW_UI_HOST_TEST -pthread -lm \ + * meow_ui_img.c meow_ui_media.c meow_notify_ipc.c stb_image.c meow_jpeg.c meow_png_stream.c meow_ui_nav.c \ + * -I. -I../meow_launcherd -DMEOW_UI_HOST_TEST -pthread -lm -lz \ * -Wl,--wrap=meow_ui_text_height * 运行:./test_meow_ui_listview_rowh */ diff --git a/patch_v2/sdk/test_meow_ui_listvirt.c b/patch_v2/sdk/test_meow_ui_listvirt.c index 010ba62..e05a0b7 100644 --- a/patch_v2/sdk/test_meow_ui_listvirt.c +++ b/patch_v2/sdk/test_meow_ui_listvirt.c @@ -13,8 +13,8 @@ * meow_ui_gfx.c meow_ui_time.c meow_ui_sys.c meow_ui_theme.c meow_ui_vol.c meow_ui_log.c \ * meow_ui_widget.c meow_ui_input.c meow_ui_activity.c meow_ui_dialog.c meow_ui_ime.c meow_ui_str.c \ * meow_pinyin.c meow_ui_draw.c meow_ui_view.c meow_ui_box.c meow_ui_anim.c meow_ui_scroll.c \ - * meow_ui_img.c meow_ui_media.c meow_notify_ipc.c stb_image.c \ - * -I. -I../meow_launcherd -DMEOW_UI_HOST_TEST -pthread -lm + * meow_ui_img.c meow_ui_media.c meow_notify_ipc.c stb_image.c meow_jpeg.c meow_png_stream.c \ + * -I. -I../meow_launcherd -DMEOW_UI_HOST_TEST -pthread -lm -lz * 运行:./test_meow_ui_listvirt */ #include "meow_ui.h" diff --git a/patch_v2/sdk/test_meow_ui_scroll.c b/patch_v2/sdk/test_meow_ui_scroll.c index b49cf2e..484493a 100644 --- a/patch_v2/sdk/test_meow_ui_scroll.c +++ b/patch_v2/sdk/test_meow_ui_scroll.c @@ -7,8 +7,8 @@ * meow_ui_gfx.c meow_ui_time.c meow_ui_sys.c meow_ui_theme.c meow_ui_vol.c meow_ui_log.c \ * meow_ui_widget.c meow_ui_input.c meow_ui_activity.c meow_ui_dialog.c meow_ui_ime.c meow_ui_str.c \ * meow_pinyin.c meow_ui_draw.c meow_ui_view.c meow_ui_box.c meow_ui_anim.c meow_ui_scroll.c \ - * meow_ui_img.c meow_ui_media.c meow_notify_ipc.c stb_image.c meow_ui_nav.c \ - * -I. -I../meow_launcherd -DMEOW_UI_HOST_TEST -pthread -lm + * meow_ui_img.c meow_ui_media.c meow_notify_ipc.c stb_image.c meow_jpeg.c meow_png_stream.c meow_ui_nav.c \ + * -I. -I../meow_launcherd -DMEOW_UI_HOST_TEST -pthread -lm -lz * 运行:./test_meow_ui_scroll */ #include "meow_ui.h" diff --git a/patch_v2/sdk/test_meow_ui_select.c b/patch_v2/sdk/test_meow_ui_select.c index cb5d96b..a408b12 100644 --- a/patch_v2/sdk/test_meow_ui_select.c +++ b/patch_v2/sdk/test_meow_ui_select.c @@ -9,8 +9,8 @@ * meow_ui_gfx.c meow_ui_time.c meow_ui_sys.c meow_ui_theme.c meow_ui_vol.c meow_ui_log.c \ * meow_ui_widget.c meow_ui_input.c meow_ui_activity.c meow_ui_dialog.c meow_ui_ime.c meow_ui_str.c \ * meow_pinyin.c meow_ui_draw.c meow_ui_view.c meow_ui_box.c meow_ui_anim.c meow_ui_scroll.c \ - * meow_select.c meow_ui_img.c meow_ui_media.c meow_notify_ipc.c stb_image.c \ - * -I. -I../meow_launcherd -DMEOW_UI_HOST_TEST -pthread -lm + * meow_select.c meow_ui_img.c meow_ui_media.c meow_notify_ipc.c stb_image.c meow_jpeg.c meow_png_stream.c \ + * -I. -I../meow_launcherd -DMEOW_UI_HOST_TEST -pthread -lm -lz * 运行:./test_meow_ui_select */ #include "meow_ui.h" diff --git a/patch_v2/sdk/test_meow_ui_swipe.c b/patch_v2/sdk/test_meow_ui_swipe.c index ad7b692..f84c202 100644 --- a/patch_v2/sdk/test_meow_ui_swipe.c +++ b/patch_v2/sdk/test_meow_ui_swipe.c @@ -11,8 +11,8 @@ * meow_ui_gfx.c meow_ui_time.c meow_ui_sys.c meow_ui_theme.c meow_ui_vol.c meow_ui_log.c \ * meow_ui_widget.c meow_ui_input.c meow_ui_activity.c meow_ui_dialog.c meow_ui_ime.c meow_ui_str.c \ * meow_pinyin.c meow_ui_draw.c meow_ui_view.c meow_ui_box.c meow_ui_anim.c meow_ui_scroll.c \ - * meow_ui_img.c meow_ui_media.c meow_notify_ipc.c stb_image.c \ - * -I. -I../meow_launcherd -DMEOW_UI_HOST_TEST -pthread -lm + * meow_ui_img.c meow_ui_media.c meow_notify_ipc.c stb_image.c meow_jpeg.c meow_png_stream.c \ + * -I. -I../meow_launcherd -DMEOW_UI_HOST_TEST -pthread -lm -lz * 运行:./test_meow_ui_swipe */ #include "meow_ui.h" diff --git a/patch_v2/sdk/test_meow_ui_theme.c b/patch_v2/sdk/test_meow_ui_theme.c index 01f014f..04b9f2f 100644 --- a/patch_v2/sdk/test_meow_ui_theme.c +++ b/patch_v2/sdk/test_meow_ui_theme.c @@ -10,8 +10,8 @@ * gcc -o test_meow_ui_theme test_meow_ui_theme.c test_host_stub.c \ * meow_ui_gfx.c meow_ui_time.c meow_ui_sys.c meow_ui_theme.c meow_ui_vol.c meow_ui_log.c \ * meow_ui_widget.c meow_ui_input.c meow_ui_activity.c meow_ui_dialog.c meow_ui_ime.c meow_ui_str.c \ - * meow_ui_draw.c meow_ui_view.c meow_ui_img.c meow_notify_ipc.c stb_image.c \ - * -I. -I../meow_launcherd -DMEOW_UI_HOST_TEST -pthread -lm + * meow_ui_draw.c meow_ui_view.c meow_ui_img.c meow_notify_ipc.c stb_image.c meow_jpeg.c meow_png_stream.c \ + * -I. -I../meow_launcherd -DMEOW_UI_HOST_TEST -pthread -lm -lz * * 验证要点:四套主题 primary/radius 各不相同(MD3 蓝 / MIUIx 橙 + 更大圆角), * 同 style 下 light/dark 的 surface 成对切换;图形用 MEOW_UI_DUMP 导出 PPM 目检。 diff --git a/patch_v2/sdk/test_meow_ui_theme_export.c b/patch_v2/sdk/test_meow_ui_theme_export.c index b3c9d96..5696427 100644 --- a/patch_v2/sdk/test_meow_ui_theme_export.c +++ b/patch_v2/sdk/test_meow_ui_theme_export.c @@ -14,8 +14,8 @@ * meow_ui_gfx.c meow_ui_time.c meow_ui_sys.c meow_ui_theme.c meow_ui_vol.c meow_ui_log.c \ * meow_ui_widget.c meow_ui_input.c meow_ui_activity.c meow_ui_dialog.c meow_ui_ime.c meow_ui_str.c \ * meow_pinyin.c meow_ui_draw.c meow_ui_view.c meow_ui_box.c meow_ui_anim.c meow_ui_scroll.c \ - * meow_ui_img.c meow_ui_media.c meow_notify_ipc.c stb_image.c \ - * -I. -I../meow_launcherd -DMEOW_UI_HOST_TEST -pthread -lm + * meow_ui_img.c meow_ui_media.c meow_notify_ipc.c stb_image.c meow_jpeg.c meow_png_stream.c \ + * -I. -I../meow_launcherd -DMEOW_UI_HOST_TEST -pthread -lm -lz * 运行:./test_meow_ui_theme_export */ #include "meow_ui.h" diff --git a/patch_v2/sdk/test_meow_ui_view_event.c b/patch_v2/sdk/test_meow_ui_view_event.c index 3c6f336..1517cb5 100644 --- a/patch_v2/sdk/test_meow_ui_view_event.c +++ b/patch_v2/sdk/test_meow_ui_view_event.c @@ -11,8 +11,8 @@ * meow_ui_gfx.c meow_ui_time.c meow_ui_sys.c meow_ui_theme.c meow_ui_vol.c meow_ui_log.c \ * meow_ui_widget.c meow_ui_input.c meow_ui_activity.c meow_ui_dialog.c meow_ui_ime.c meow_ui_str.c \ * meow_pinyin.c meow_ui_draw.c meow_ui_view.c meow_ui_box.c meow_ui_anim.c meow_ui_scroll.c \ - * meow_ui_img.c meow_ui_media.c meow_notify_ipc.c stb_image.c \ - * -I. -I../meow_launcherd -DMEOW_UI_HOST_TEST -pthread -lm + * meow_ui_img.c meow_ui_media.c meow_notify_ipc.c stb_image.c meow_jpeg.c meow_png_stream.c \ + * -I. -I../meow_launcherd -DMEOW_UI_HOST_TEST -pthread -lm -lz * 运行:./test_meow_ui_view_event */ #include "meow_ui.h" diff --git a/patch_v2/sdk/test_meow_ui_widgets.c b/patch_v2/sdk/test_meow_ui_widgets.c index a45ab96..f44d96d 100644 --- a/patch_v2/sdk/test_meow_ui_widgets.c +++ b/patch_v2/sdk/test_meow_ui_widgets.c @@ -5,11 +5,11 @@ * gcc -o test_meow_ui_widgets test_meow_ui_widgets.c test_host_stub.c \ * meow_ui_gfx.c meow_ui_time.c meow_ui_sys.c meow_ui_theme.c meow_ui_vol.c meow_ui_log.c \ * meow_ui_widget.c meow_ui_input.c meow_ui_activity.c meow_ui_dialog.c meow_ui_ime.c meow_ui_str.c \ - * meow_ui_draw.c meow_ui_view.c meow_ui_img.c meow_notify_ipc.c stb_image.c \ - * -I. -I../meow_launcherd -DMEOW_UI_HOST_TEST -pthread -lm + * meow_ui_draw.c meow_ui_view.c meow_ui_img.c meow_notify_ipc.c stb_image.c meow_jpeg.c meow_png_stream.c \ + * -I. -I../meow_launcherd -DMEOW_UI_HOST_TEST -pthread -lm -lz * 运行:MEOW_UI_DUMP=/tmp/b2.ppm ./test_meow_ui_widgets (可选,dump 屏幕) * - * 注:W_LISTVIEW 的 MEOW_LIST_ICON_IMG 分支调用 meow_ui_img_scaled → 宿主链 meow_ui_img.c+stb_image.c+-lm。 + * 注:W_LISTVIEW 的 MEOW_LIST_ICON_IMG 分支调用 meow_ui_img_scaled → 宿主链 meow_ui_img.c+stb_image.c+-lm -lz。 * 结构化列表断言用 back_buf 像素(region_has/px_near),画完即读,非目检。 */ #include "meow_ui.h" diff --git a/patch_v2/sdk/test_p1p2p3_repro.c b/patch_v2/sdk/test_p1p2p3_repro.c index cfe23ab..f36739a 100644 --- a/patch_v2/sdk/test_p1p2p3_repro.c +++ b/patch_v2/sdk/test_p1p2p3_repro.c @@ -25,11 +25,11 @@ * test_host_stub.c meow_ui_gfx.c meow_ui_time.c meow_ui_sys.c meow_ui_theme.c meow_ui_vol.c meow_ui_log.c \ * meow_ui_widget.c meow_ui_input.c meow_ui_activity.c meow_ui_dialog.c meow_ui_ime.c meow_ui_str.c \ * meow_ui_img.c meow_ui_config.c meow_ui_notify.c meow_notify_ipc.c meow_ui_draw.c \ - * meow_ui_view.c meow_ui_nav.c meow_ui_media.c stb_image.c \ + * meow_ui_view.c meow_ui_nav.c meow_ui_media.c stb_image.c meow_jpeg.c meow_png_stream.c \ * ../hello_app/ui/home/home.c ../hello_app/ui/full.c ../hello_app/ui/demo/demo.c \ * ../hello_app/ui/demo/list/list.c ../hello_app/ui/common/chrome.c \ * ../hello_app/logic/notify.c ../hello_app/logic/state.c \ - * -I. -I../hello_app -I../meow_launcherd -pthread -lm + * -I. -I../hello_app -I../meow_launcherd -pthread -lm -lz * 运行:MEOW_UI_DUMP=/tmp/repro.ppm ./test_p1p2p3_repro (optional,dump 最终屏幕) * * 预期 RESULT: ALL PASS(P1c/P2/P3 断言真实有效,未弱化)。 diff --git a/patch_v2/sdk/test_pinyin.c b/patch_v2/sdk/test_pinyin.c index 418bd45..0928ed8 100644 --- a/patch_v2/sdk/test_pinyin.c +++ b/patch_v2/sdk/test_pinyin.c @@ -11,7 +11,7 @@ * meow_ui_gfx.c meow_ui_time.c meow_ui_sys.c meow_ui_theme.c meow_ui_vol.c meow_ui_log.c \ * meow_ui_widget.c meow_ui_input.c meow_ui_activity.c meow_ui_dialog.c meow_ui_ime.c meow_ui_str.c \ * meow_pinyin.c meow_ui_draw.c meow_ui_view.c meow_ui_img.c meow_ui_media.c \ - * meow_notify_ipc.c stb_image.c -I. -I../meow_launcherd -DMEOW_UI_HOST_TEST -pthread -lm + * meow_notify_ipc.c stb_image.c meow_jpeg.c meow_png_stream.c -I. -I../meow_launcherd -DMEOW_UI_HOST_TEST -pthread -lm -lz */ #include "meow_ui.h" #include "meow_ui_internal.h" From 281f4153eeca4c19646810704766255561b26358 Mon Sep 17 00:00:00 2001 From: AskaEth Date: Fri, 6 Nov 2026 21:27:03 +0800 Subject: [PATCH 2/2] =?UTF-8?q?fix(sdk):=20M26=20r1=20P1=20=E2=80=94=20ILP?= =?UTF-8?q?32=20=E5=B0=BA=E5=AF=B8=E7=AE=97=E6=9C=AF=E6=BA=A2=E5=87=BA?= =?UTF-8?q?=EF=BC=88=E7=95=B8=E5=BD=A2=20BMP=20=E7=BB=8F=E5=85=AC=E5=BC=80?= =?UTF-8?q?=E5=85=A5=E5=8F=A3=20SIGSEGV=EF=BC=89+=20=E5=8A=A0=20S17=20?= =?UTF-8?q?=E5=9B=9E=E5=BD=92?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 评审在 ARM32(ILP32,与 mipsel 同数据模型)实测:54 字节 BMP 头声明 w=2^27、bpp=32 时 w*bpp 回绕成 0 → rowsize=0 → malloc(0) → 采样索引 row+sx*4 越界读 → SIGSEGV, 经 meow_img_load_file_dim 端到端可触发;同一文件在 LP64 宿主返回 NULL(不溢出), 合并前走 stb 也是 NULL。即这是相对旧路径的鲁棒性回归。 修复(只加闸,正常路径数值不变): - meow_png_stream.c(P1 本体) · 新增 ILP32 守卫 MZP_MAX_DIM=1<<20 与 mzp_bytes_ok()(unsigned long long 验算, 超过 SIZE_MAX 即拒) · BMP:声明尺寸先升 int/long long 校验(w/h 单边 ≤1<<20;h=INT_MIN 用 -(long long)hi 取绝对值,不再有 -h 溢出);rowsize 用 64 位算并验; 行偏移 off / 采样下标 sx 改 long long,sx 夹在 [0,w-1];按行 fseek 前加 off > LONG_MAX 拒绝;目标 RGBA 字节数 64 位验后再 malloc · PNG:IHDR 尺寸原已 ≤65535,补上 tw*th*4 的 64 位校验(32768²×4 = 2^32 会回绕), emit_row 的 sy/sx 与输出行偏移改 64 位并夹紧 - meow_jpeg.c(顺手审计同类 32 位算术,评审要求「不要只修点名那处」) · mj_layout 返回 unsigned long long(单平面/三平面之和都可能越 2^32), 越界返回 MJ_LAYOUT_BAD → mj_sof 各处重新布局后都检查 · target_bytes 与分量 plane malloc 改 64 位验算;目标图字节超 MJ_PEAK_CAP 直接拒 · mj_output:输出缓冲 64 位验;y*vh / x*vw 采样改 long long 并夹紧 · meow_img_target_dims / mj_pick_n 的 (long)h*maxdim、8*maxdim 改 long long - test_meow_img_stream.c:新增 S17(10 组畸形 BMP 声明尺寸 + 2 个合法正例 + PNG 回绕点),断言「后端拒绝 + 公开入口不崩(越界尺寸另断言 NULL 且不分配)」; 文件头补 ILP32 交叉编译命令(宿主 LP64 看不到 32 位回绕,测试注释里明写这一点) 验证:同一测试在 zig arm-linux-musleabi(sizeof(long)=4)下 S1-S17 全绿(206 checks, exit 0);把后端换回 r1 提交的旧实现,同一 ARM32 二进制 exit 139(说明 S17 真能抓); 宿主 LP64 S1-S17 全绿、B1-B9 全绿;静态库 --verify 仍 78 成员 / 全 MIPS32r2。 同步提醒:这些行与公开仓 ~/Proj/MeowOS-SDK/sdk/{meow_png_stream.c,meow_jpeg.c} 逐字节 同源,同一补丁必须由控制塔同步过去,否则立刻产生双边漂移。 --- patch_v2/sdk/meow_jpeg.c | 101 +++++++++++++----- patch_v2/sdk/meow_png_stream.c | 89 ++++++++++++---- patch_v2/sdk/test_meow_img_stream.c | 157 ++++++++++++++++++++++++++++ 3 files changed, 300 insertions(+), 47 deletions(-) diff --git a/patch_v2/sdk/meow_jpeg.c b/patch_v2/sdk/meow_jpeg.c index 0330333..82d3bc4 100644 --- a/patch_v2/sdk/meow_jpeg.c +++ b/patch_v2/sdk/meow_jpeg.c @@ -36,10 +36,10 @@ void meow_img_target_dims(int w, int h, int maxdim, int *tw, int *th) { if (w >= h) { *tw = maxdim; - *th = (int)((long)h * maxdim / w); + *th = (int)((long long)h * maxdim / w); /* 64 位:ILP32 上 h*maxdim 会溢出 */ } else { *th = maxdim; - *tw = (int)((long)w * maxdim / h); + *tw = (int)((long long)w * maxdim / h); } if (*tw < 1) *tw = 1; if (*th < 1) *th = 1; @@ -390,21 +390,25 @@ static int mj_ceil_div(int a, int b) { return (a + b - 1) / b; } * 这种图上 stb 旧路径的峰值本来就受控,直接交回它,档位对外表现不变。 */ static int mj_pick_n(int longside, int maxdim) { - int n = (8 * maxdim + longside - 1) / longside; + long long n = ((long long)8 * maxdim + longside - 1) / longside; /* 64 位:maxdim 可到 INT_MAX */ if (n < 1) n = 1; if (n > 7) return 0; - return n; + return (int)n; } -/* 按各分量当前的 n 计算块网格与平面尺寸,返回平面总字节。 */ -static unsigned mj_layout(mj_img *img) +/* 按各分量当前的 n 计算块网格与平面尺寸,返回平面总字节。 + * 返回 64 位:ILP32 上单平面 pw*ph 与 3 个平面之和都可能越过 2^32。 + * MJ_LAYOUT_BAD 表示该乘积在 32 位 size_t 上无法表示(调用方直接拒绝)。 */ +#define MJ_LAYOUT_BAD ((unsigned long long)-1) +static unsigned long long mj_layout(mj_img *img) { - unsigned total = 0; + unsigned long long total = 0; int i; for (i = 0; i < img->ncomp; i++) { mj_comp *c = &img->c[i]; int sw = mj_ceil_div(img->width * c->h, img->hmax); int sh = mj_ceil_div(img->height * c->v, img->vmax); + unsigned long long plane; c->ow = mj_ceil_div(sw, 8); c->oh = mj_ceil_div(sh, 8); c->pw = (img->mcus_x * c->h > c->ow ? img->mcus_x * c->h : c->ow) * c->n; @@ -413,7 +417,9 @@ static unsigned mj_layout(mj_img *img) c->vh = mj_ceil_div(sh * c->n, 8); if (c->vw > c->pw) c->vw = c->pw; if (c->vh > c->ph) c->vh = c->ph; - total += (unsigned)c->pw * (unsigned)c->ph; + plane = (unsigned long long)c->pw * (unsigned long long)c->ph; + if (plane > (unsigned long long)(size_t)-1) return MJ_LAYOUT_BAD; + total += plane; } return total; } @@ -421,7 +427,7 @@ static unsigned mj_layout(mj_img *img) static int mj_sof(mj_img *img, int marker, const unsigned char *d, int len, int maxdim) { int i, n, longside, base_n; - unsigned int plane_bytes, target_bytes; + unsigned long long plane_bytes, target_bytes; int tw, th; if (marker != 0xC0 && marker != 0xC1 && marker != 0xC2) return 0; /* 算术/无损/差分 */ @@ -456,10 +462,14 @@ static int mj_sof(mj_img *img, int marker, const unsigned char *d, int len, int img->mcus_y = mj_ceil_div(img->height, 8 * img->vmax); meow_img_target_dims(img->width, img->height, maxdim, &tw, &th); - target_bytes = (unsigned)tw * (unsigned)th * 4u; + /* 64 位算目标图字节:ILP32 上 tw*th*4 在 tw=th=32768 时正好回绕成 0, + * 会让下面的预算闸失效并去 malloc(0)(M26 r1 顺手审计)。 */ + target_bytes = (unsigned long long)(unsigned)tw * (unsigned long long)(unsigned)th * 4u; + if (target_bytes > MJ_PEAK_CAP) return 0; for (i = 0; i < n; i++) img->c[i].n = base_n; plane_bytes = mj_layout(img); + if (plane_bytes == MJ_LAYOUT_BAD) return 0; /* 4:4:4 的色度平面与亮度等大(正常 4:2:0 只 1/4):亮度平面超过 400KB 时色度降一档, * 视觉近无损,省下的正是色度要多占的两份。 */ @@ -467,6 +477,7 @@ static int mj_sof(mj_img *img, int marker, const unsigned char *d, int len, int plane_bytes / 3 > 400u * 1024u) { for (i = 1; i < n; i++) if (img->c[i].n > 1) img->c[i].n >>= 1; plane_bytes = mj_layout(img); + if (plane_bytes == MJ_LAYOUT_BAD) return 0; } /* 预算 2.4MB(平面 + 目标图):超了先降色度(亮度保持清晰),再整体降档。 @@ -474,13 +485,17 @@ static int mj_sof(mj_img *img, int marker, const unsigned char *d, int len, int if (plane_bytes + target_bytes > 2400u * 1024u) { int ch = 0; for (i = 1; i < n; i++) if (img->c[i].n > 1) { img->c[i].n >>= 1; ch = 1; } - if (ch) plane_bytes = mj_layout(img); + if (ch) { + plane_bytes = mj_layout(img); + if (plane_bytes == MJ_LAYOUT_BAD) return 0; + } } /* 最后一道防线:异常巨图整体降档(最多降到 N=1),不至于一次吃掉几十 MB。 */ while (plane_bytes + target_bytes > MJ_PEAK_CAP && base_n > 1) { base_n >>= 1; for (i = 0; i < n; i++) if (img->c[i].n > base_n) img->c[i].n = base_n; plane_bytes = mj_layout(img); + if (plane_bytes == MJ_LAYOUT_BAD) return 0; } /* 硬闸:降到 N=1 仍超上限(畸形/超大声明尺寸,如 23 字节的 SOF 65535² → 亮度面 * 8192² = 64MiB)直接拒绝 → 返回 0 → 回退 stb(stb 对这类文件是 KB 级安全失败)。 @@ -489,7 +504,10 @@ static int mj_sof(mj_img *img, int marker, const unsigned char *d, int len, int for (i = 0; i < n; i++) { mj_comp *c = &img->c[i]; - size_t bytes = (size_t)c->pw * (size_t)c->ph; + unsigned long long bytes64 = (unsigned long long)c->pw * (unsigned long long)c->ph; + size_t bytes; + if (bytes64 == 0 || bytes64 > (unsigned long long)(size_t)-1) return 0; + bytes = (size_t)bytes64; c->plane = (unsigned char *)malloc(bytes); if (!c->plane) return 0; memset(c->plane, 128, bytes); /* 中性灰底,系数累加即 IDCT */ @@ -753,7 +771,8 @@ static void mj_scan_ac_prog(mj_img *img, const mj_scan *sc) static unsigned char *mj_output(mj_img *img, int tw, int th, int rgb_mode) { - unsigned char *out = (unsigned char *)malloc((size_t)tw * th * 4); + unsigned long long ob = (unsigned long long)tw * (unsigned long long)th * 4u; + unsigned char *out; mj_comp *cy = &img->c[0]; mj_comp *cb = img->ncomp >= 3 ? &img->c[1] : NULL; mj_comp *cr = img->ncomp >= 3 ? &img->c[2] : NULL; @@ -761,30 +780,62 @@ static unsigned char *mj_output(mj_img *img, int tw, int th, int rgb_mode) const unsigned char *pb = cb ? cb->plane : NULL; const unsigned char *pc = cr ? cr->plane : NULL; int x, y; + if (ob == 0 || ob > (unsigned long long)(size_t)-1) return NULL; + out = (unsigned char *)malloc((size_t)ob); if (!out) return NULL; for (y = 0; y < th; y++) { - int sy = (int)((long)y * cy->vh / th); + /* 64 位:ILP32 上 y*vh、x*vw 都可能越过 2^31(th/vh 各 3 万量级即可) */ + long long syl = (long long)y * cy->vh / th; + int sy = (int)(syl < 0 ? 0 : (syl >= (long long)cy->vh ? (long long)cy->vh - 1 : syl)); const unsigned char *ry = py + (size_t)sy * cy->pw; - const unsigned char *rb = pb ? pb + (size_t)((long)y * cb->vh / th) * cb->pw : NULL; - const unsigned char *rc = pc ? pc + (size_t)((long)y * cr->vh / th) * cr->pw : NULL; - unsigned char *dst = out + (size_t)y * tw * 4; + const unsigned char *rb = NULL, *rc = NULL; + if (pb) { + long long s = (long long)y * cb->vh / th; + if (s >= (long long)cb->vh) s = (long long)cb->vh - 1; + if (s < 0) s = 0; + rb = pb + (size_t)s * cb->pw; + } + if (pc) { + long long s = (long long)y * cr->vh / th; + if (s >= (long long)cr->vh) s = (long long)cr->vh - 1; + if (s < 0) s = 0; + rc = pc + (size_t)s * cr->pw; + } + unsigned char *dst = out + (size_t)((unsigned long long)y * (unsigned long long)tw * 4u); for (x = 0; x < tw; x++) { - int yv = ry[(int)((long)x * cy->vw / tw)]; + long long yxl = (long long)x * cy->vw / tw; + int yv; + if (yxl < 0) yxl = 0; + if (yxl >= (long long)cy->vw) yxl = (long long)cy->vw - 1; + yv = ry[(int)yxl]; unsigned char *o = dst + (size_t)x * 4; if (!rb) { o[0] = o[1] = o[2] = (unsigned char)yv; o[3] = 255; } else if (rgb_mode) { + long long ux = (long long)x * cb->vw / tw; + long long vx = (long long)x * cr->vw / tw; + if (ux < 0) ux = 0; + if (ux >= (long long)cb->vw) ux = (long long)cb->vw - 1; + if (vx < 0) vx = 0; + if (vx >= (long long)cr->vw) vx = (long long)cr->vw - 1; o[0] = (unsigned char)yv; - o[1] = rb[(int)((long)x * cb->vw / tw)]; - o[2] = rc[(int)((long)x * cr->vw / tw)]; + o[1] = rb[(int)ux]; + o[2] = rc[(int)vx]; o[3] = 255; } else { - int u = (int)rb[(int)((long)x * cb->vw / tw)] - 128; - int v = (int)rc[(int)((long)x * cr->vw / tw)] - 128; - int r = yv + ((MJ_FIX_R * v) >> 12); - int g = yv - ((MJ_FIX_G * u + MJ_FIX_G2 * v) >> 12); - int b = yv + ((MJ_FIX_B * u) >> 12); + long long ux = (long long)x * cb->vw / tw; + long long vx = (long long)x * cr->vw / tw; + int u, v, r, g, b; + if (ux < 0) ux = 0; + if (ux >= (long long)cb->vw) ux = (long long)cb->vw - 1; + if (vx < 0) vx = 0; + if (vx >= (long long)cr->vw) vx = (long long)cr->vw - 1; + u = (int)rb[(int)ux] - 128; + v = (int)rc[(int)vx] - 128; + r = yv + ((MJ_FIX_R * v) >> 12); + g = yv - ((MJ_FIX_G * u + MJ_FIX_G2 * v) >> 12); + b = yv + ((MJ_FIX_B * u) >> 12); o[0] = (unsigned char)(r < 0 ? 0 : (r > 255 ? 255 : r)); o[1] = (unsigned char)(g < 0 ? 0 : (g > 255 ? 255 : g)); o[2] = (unsigned char)(b < 0 ? 0 : (b > 255 ? 255 : b)); diff --git a/patch_v2/sdk/meow_png_stream.c b/patch_v2/sdk/meow_png_stream.c index 4da2594..ed1a12d 100644 --- a/patch_v2/sdk/meow_png_stream.c +++ b/patch_v2/sdk/meow_png_stream.c @@ -22,6 +22,7 @@ #include #include #include +#include /* LONG_MAX:BMP 按行 seek 的 32 位偏移上限 */ /* ---------------- zlib ABI ---------------- */ #if defined(__has_include) @@ -59,6 +60,21 @@ extern const char *zlibVersion(void); # define Z_NO_FLUSH 0 #endif +/* ---------------- ILP32 尺寸算术守卫 ---------------- + * mipsel / arm-linux-musleabi 上 long 与 size_t 都是 32 位:把「声明尺寸 × bpp」这类 + * 乘积直接放在 long 里会回绕成小值或 0。实测(ARM32):54 字节 BMP 头声明 w=2^27、 + * bpp=32 → w*bpp = 2^32 = 0 → rowsize = 0 → malloc(0) → 采样索引 row + sx*4 越界读 + * → SIGSEGV;同一文件在 LP64 宿主返回 NULL,所以宿主测试天然看不到这类。 + * 因此:凡「由文件头算出来的字节数/偏移」一律升到 unsigned long long 再验, + * 超限即返回 0,由调用方回退 stb(安全失败,与 M26 之前的行为一致)。 + * 单边上限 1<<20(1,048,576):远超真实 BMP/PNG 声明尺寸,同时保证行字节数 + * (≤4 MiB)、行偏移与目标 RGBA 都不会在 32 位溢出。 */ +#define MZP_MAX_DIM (1u << 20) +static int mzp_bytes_ok(unsigned long long n) +{ + return n > 0 && n <= (unsigned long long)(size_t)-1; +} + /* ---------------- 字节源 ---------------- */ typedef struct { FILE *f; @@ -202,12 +218,15 @@ static int png_emit_row(png_ctx *c) } } if (c->next_ty < c->th) { - long sy = (long)c->next_ty * (long)c->h / c->th; - if (sy == (long)c->y) { - unsigned char *o = c->out + (size_t)c->next_ty * c->tw * 4; + long long sy = (long long)c->next_ty * (long long)c->h / c->th; /* 64 位:ILP32 上 *h 会溢出 */ + if (sy == (long long)c->y) { + unsigned char *o = c->out + + (size_t)((unsigned long long)c->next_ty * (unsigned long long)c->tw * 4u); int tx; for (tx = 0; tx < c->tw; tx++) { - int sx = (int)((long)tx * (long)c->w / c->tw); + long long sxl = (long long)tx * (long long)c->w / c->tw; + int sx = (int)(sxl < 0 ? 0 + : (sxl >= (long long)c->w ? (long long)c->w - 1 : sxl)); unsigned char px[4]; png_get_px(c, dst, sx, px); o[tx * 4] = px[0]; o[tx * 4 + 1] = px[1]; @@ -315,9 +334,14 @@ int meow_png_decode_stream(FILE *f, const unsigned char *mem, size_t len, memset(c.cur, 0, c.rowbytes); for (i = 0; i < 256; i++) c.trns_a[i] = 255; meow_img_target_dims((int)c.w, (int)c.h, maxdim, &c.tw, &c.th); - c.out = (unsigned char *)malloc((size_t)c.tw * c.th * 4); - if (!c.out) goto done; - memset(c.out, 0, (size_t)c.tw * c.th * 4); + { + /* ILP32 上 tw*th*4 会回绕(32768×32768×4 = 2^32 → 0 → malloc(0) 后越界写) */ + unsigned long long ob = (unsigned long long)c.tw * (unsigned long long)c.th * 4u; + if (!mzp_bytes_ok(ob)) goto done; + c.out = (unsigned char *)malloc((size_t)ob); + if (!c.out) goto done; + memset(c.out, 0, (size_t)ob); + } memset(&c.z, 0, sizeof c.z); if (inflateInit_(&c.z, zlibVersion(), (int)sizeof c.z) != Z_OK) goto done; c.z_ready = 1; @@ -423,16 +447,25 @@ int meow_bmp_decode_stream(FILE *f, const unsigned char *mem, size_t len, if (dibsize < 40 || dibsize > sizeof dib) return 0; /* BITMAPCOREHEADER/OS2 → stb */ memcpy(dib, dh, 4); if (!ps_read(&src, dib + 4, dibsize - 4)) return 0; - w = (long)(int)le32(dib + 4); - h = (long)(int)le32(dib + 8); bpp = le16(dib + 14); compression = le32(dib + 16); colors = (dibsize >= 36) ? le32(dib + 32) : 0; if (compression != 0) return 0; /* RLE/BITFIELDS → stb */ - if (w <= 0 || h == 0) return 0; if (bpp != 8 && bpp != 16 && bpp != 24 && bpp != 32) return 0; - topdown = h < 0; - if (h < 0) h = -h; + /* 声明尺寸过 32 位算术闸(M26 r1 P1):w×bpp 与 tx×w 在 ILP32 上会回绕成小值/负数, + * 直接导致 malloc(0) + 越界读。h=INT_MIN 时 -h 同样溢出,故升到 long long 再取绝对值。 */ + { + int wi = (int)le32(dib + 4); + int hi = (int)le32(dib + 8); + long long hll; + if (wi <= 0 || hi == 0) return 0; + topdown = hi < 0; + hll = topdown ? -(long long)hi : (long long)hi; + if ((unsigned long long)wi > MZP_MAX_DIM || hll <= 0 || + (unsigned long long)hll > MZP_MAX_DIM) return 0; /* 离奇声明尺寸 → 回退 stb */ + w = (long)wi; + h = (long)hll; + } if ((long)(w > h ? w : h) <= maxdim) return 0; /* 小图走旧入口 */ if (bpp <= 8) { @@ -446,26 +479,38 @@ int meow_bmp_decode_stream(FILE *f, const unsigned char *mem, size_t len, for (; i < 256; i++) pal[i][0] = pal[i][1] = pal[i][2] = 0; } - rowsize = (w * bpp + 31) / 32 * 4; + { + /* 行字节数 64 位算(ILP32 上 w*bpp 在 w≥2^27/bpp 时回绕成 0) */ + unsigned long long rb = ((unsigned long long)w * bpp + 31u) / 32u * 4u; + if (!mzp_bytes_ok(rb)) return 0; + rowsize = (long)rb; /* w ≤ MZP_MAX_DIM → ≤ 4 MiB */ + } meow_img_target_dims((int)w, (int)h, maxdim, &tw_, &th_); row = (unsigned char *)malloc((size_t)rowsize); - out = (unsigned char *)malloc((size_t)tw_ * th_ * 4); + { + unsigned long long ob = (unsigned long long)tw_ * (unsigned long long)th_ * 4u; + if (mzp_bytes_ok(ob)) out = (unsigned char *)malloc((size_t)ob); + } if (!row || !out) goto done; for (int ty = 0; ty < th_; ty++) { - int sy = (int)((long)ty * h / th_); - long file_row = topdown ? sy : (h - 1 - sy); - long off = (long)dataoff + file_row * rowsize; - unsigned char *o = out + (size_t)ty * tw_ * 4; + long long sy = (long long)ty * h / th_; + long long file_row = topdown ? sy : ((long long)h - 1 - sy); + long long off = (long long)dataoff + file_row * (long long)rowsize; + unsigned char *o = out + (size_t)((unsigned long long)ty * (unsigned long long)tw_ * 4u); if (src.mem) { - if (off < 0 || (size_t)off + (size_t)rowsize > src.memlen) goto done; - memcpy(row, src.mem + off, (size_t)rowsize); + if (off < 0 || + (unsigned long long)off + (unsigned long long)rowsize > (unsigned long long)src.memlen) + goto done; + memcpy(row, src.mem + (size_t)off, (size_t)rowsize); } else { - if (fseek(src.f, off, SEEK_SET) != 0) goto done; + if (off < 0 || off > (long long)LONG_MAX) goto done; /* fseek 只有 32 位偏移 */ + if (fseek(src.f, (long)off, SEEK_SET) != 0) goto done; if (fread(row, 1, (size_t)rowsize, src.f) != (size_t)rowsize) goto done; } for (int tx = 0; tx < tw_; tx++) { - int sx = (int)((long)tx * w / tw_); + long long sxl = (long long)tx * w / tw_; + int sx = (int)(sxl < 0 ? 0 : (sxl >= (long long)w ? (long long)w - 1 : sxl)); unsigned char *p = o + (size_t)tx * 4; if (bpp == 24) { const unsigned char *q = row + sx * 3; diff --git a/patch_v2/sdk/test_meow_img_stream.c b/patch_v2/sdk/test_meow_img_stream.c index 4b1534d..ab80cf8 100644 --- a/patch_v2/sdk/test_meow_img_stream.c +++ b/patch_v2/sdk/test_meow_img_stream.c @@ -12,6 +12,9 @@ * S8 内存入口 == 文件入口(逐字节),峰值同级 * S9 回退:CMYK JPEG(4 分量)、1bpp BMP、Adam7 交错 PNG → 后端拒绝,stb 仍成功; * 截断/垃圾 → NULL(不崩) + * S10-S16 缩解阶/restart/DHT/tRNS/step/低熵/progressive-restart 等专项 + * S17 32 位尺寸算术:畸形 BMP 声明尺寸(w=2^27…、24bpp 回绕点、h=INT_MIN)与 PNG + * tw*th*4 回绕点 → 一律 NULL 不崩(ILP32 崩溃回归,M26 r1 P1) * * 夹具:本文件内置生成脚本(Pillow),首次运行时写到夹具目录并执行。 * 编译(本 sdk 目录下;--wrap 用于给 malloc/free 家族打点): @@ -20,6 +23,17 @@ * -lm -lz \ * -Wl,--wrap=malloc -Wl,--wrap=calloc -Wl,--wrap=realloc -Wl,--wrap=free * 运行:./test_meow_img_stream "$PREFIX/tmp/meow_img_stream" (exit 0=PASS / 1=FAIL) + * + * ILP32 回归(S17 的真覆盖;宿主 LP64 的 long/size_t 是 64 位,看不到 32 位回绕崩溃): + * 用 zig 的 arm-linux-musleabi 目标(与 mipsel 同数据模型:long/size_t 均 4 字节), + * 需要一份 32 位 libz.a(设备上的 libz.a 就是 mipsel 32 位;本机可从 zlib 源码 + * 用同一 zig cc --target=arm-linux-musleabi -c 各 .c 后 ar 打包)。命令: + * zig cc --target=arm-linux-musleabi -static -O2 -I. -Iinclude -DMEOW_UI_HOST_TEST \ + * -o test_meow_img_stream_arm test_meow_img_stream.c meow_ui_img.c meow_jpeg.c \ + * meow_png_stream.c stb_image.c -lm \ + * -Wl,--wrap=malloc -Wl,--wrap=calloc -Wl,--wrap=realloc -Wl,--wrap=free + * ./test_meow_img_stream_arm "$PREFIX/tmp/meow_img_stream" + * (aarch64 Android 设备可直跑 32 位 ARM 静态二进制;2026-11-06 实测 S1-S17 全绿。) */ #define _GNU_SOURCE #include "meow_ui.h" @@ -566,6 +580,60 @@ static size_t build_sof_only(unsigned char *buf, int w, int h, int ncomp) return p; } +/* ---- S17 用的畸形头构造器(54 字节 BMP / 仅 IHDR 的 PNG) ---- */ +static size_t bmp_build(unsigned char *b, unsigned w, unsigned hbits, unsigned bpp) +{ + size_t off = 54; + memset(b, 0, off); + b[0] = 'B'; b[1] = 'M'; + b[2] = (unsigned char)(off & 0xFF); b[3] = (unsigned char)((off >> 8) & 0xFF); + b[10] = (unsigned char)(off & 0xFF); b[11] = (unsigned char)((off >> 8) & 0xFF); + b[14] = 40; /* BITMAPINFOHEADER */ + b[18] = (unsigned char)(w & 0xFF); b[19] = (unsigned char)((w >> 8) & 0xFF); + b[20] = (unsigned char)((w >> 16) & 0xFF); b[21] = (unsigned char)((w >> 24) & 0xFF); + b[22] = (unsigned char)(hbits & 0xFF); b[23] = (unsigned char)((hbits >> 8) & 0xFF); + b[24] = (unsigned char)((hbits >> 16) & 0xFF); b[25] = (unsigned char)((hbits >> 24) & 0xFF); + b[26] = 1; /* planes */ + b[28] = (unsigned char)(bpp & 0xFF); + return off; +} + +/* 正例对照:合法的 64×64 32bpp BMP(topdown=1 时 h 写成负数) */ +static size_t bmp_build_64(unsigned char *b, int topdown) +{ + const unsigned w = 64, h = 64, bpp = 32; + size_t n = bmp_build(b, w, topdown ? (unsigned)(-64) : h, bpp); + unsigned x, y; + b[2] = (unsigned char)((n + w * h * 4) & 0xFF); /* 文件大小(解析器不校验) */ + b[3] = (unsigned char)(((n + w * h * 4) >> 8) & 0xFF); + for (y = 0; y < h; y++) + for (x = 0; x < w; x++, n += 4) { + b[n + 0] = (unsigned char)(x * 4); + b[n + 1] = (unsigned char)(y * 4); + b[n + 2] = 0x40; + b[n + 3] = 0xFF; + } + return n; +} + +static size_t png_ihdr_only(unsigned char *b, unsigned w, unsigned h) +{ + static const unsigned char sig[8] = { 0x89, 'P', 'N', 'G', '\r', '\n', 0x1a, '\n' }; + size_t p = 0; + memcpy(b, sig, 8); + p = 8; + b[p++] = 0; b[p++] = 0; b[p++] = 0; b[p++] = 13; /* IHDR 长度 */ + memcpy(b + p, "IHDR", 4); p += 4; + b[p++] = (unsigned char)(w >> 24); b[p++] = (unsigned char)((w >> 16) & 0xFF); + b[p++] = (unsigned char)((w >> 8) & 0xFF); b[p++] = (unsigned char)(w & 0xFF); + b[p++] = (unsigned char)(h >> 24); b[p++] = (unsigned char)((h >> 16) & 0xFF); + b[p++] = (unsigned char)((h >> 8) & 0xFF); b[p++] = (unsigned char)(h & 0xFF); + b[p++] = 8; /* bit depth */ + b[p++] = 2; /* color type RGB */ + b[p++] = 0; b[p++] = 0; b[p++] = 0; + return p; /* 故意不带后续块 */ +} + /* ================================================================== */ int main(int argc, char **argv) { @@ -1117,6 +1185,95 @@ int main(int argc, char **argv) } } + /* ---- S17 32 位尺寸算术(ILP32 崩溃回归,M26 r1 P1) ---- + * 54 字节 BMP 头声明 w=2^27、bpp=32 时,ILP32 上 w*bpp = 2^32 → rowsize 回绕成 0 → + * malloc(0) → 采样索引 row + sx*4 越界读 → SIGSEGV。LP64 宿主不会回绕,所以本节在 + * 宿主的价值是「契约固定」:这些声明尺寸必须一律返回 NULL(且不崩);真正的 32 位 + * 覆盖用法见文件头「ILP32 回归」那条交叉编译命令。 */ + printf("[S17] 32 位尺寸算术:BMP 声明尺寸 / 采样偏移溢出(ILP32 崩溃回归)\n"); + { + static const struct { const char *tag; unsigned w, hbits, bpp; int must_null; } sc[] = { + { "w=0x07FFFFFF bpp=32(回绕点前一点)", 0x07FFFFFFu, 1u, 32u, 1 }, + { "w=0x08000000 bpp=32(2^27,旧代码崩点)", 0x08000000u, 1u, 32u, 1 }, + { "w=0x10000000 bpp=32", 0x10000000u, 1u, 32u, 1 }, + { "w=0x40000000 bpp=32", 0x40000000u, 1u, 32u, 1 }, + { "w=0x0AAAAAAA bpp=24(w*24 回绕点前)", 0x0AAAAAAAu, 1u, 24u, 1 }, + { "w=0x0AAAAAAB bpp=24(w*24 ≥ 2^32)", 0x0AAAAAABu, 1u, 24u, 1 }, + /* 合法尺寸(≤1<<20)但无像素数据:后端必须拒绝;公开入口按设计回退 stb, + * stb 对截断 BMP 的行为不受本单约束,故只断言「不崩」,不要求 NULL。 */ + { "w=1000000 bpp=32(合法尺寸但无像素数据)", 1000000u, 1u, 32u, 0 }, + { "w=0x00100000 bpp=8(单边上限 1<<20)", 0x00100000u, 1u, 8u, 0 }, + { "w=0x00100001 bpp=8(超单边上限)", 0x00100001u, 1u, 8u, 1 }, + { "h=0x80000000(-INT_MIN 溢出点)", 0x01000000u, 0x80000000u, 32u, 1 }, + }; + for (size_t i = 0; i < sizeof sc / sizeof sc[0]; i++) { + unsigned char buf[54]; + int tw = 0, th = 0; + unsigned char *px = NULL; + meow_img *im; + char name[200]; + size_t base, pk; + char *fp = path_join("evil_dims.bmp"); + FILE *f; + + bmp_build(buf, sc[i].w, sc[i].hbits, sc[i].bpp); + snprintf(name, sizeof name, "%s → 后端拒绝", sc[i].tag); + base = peak_reset(); + CHECK(meow_bmp_decode_stream(NULL, buf, sizeof buf, 720, &tw, &th, &px) == 0, name); + pk = peak_since(base); + CHECK(px == NULL, "拒绝时不返回像素缓冲"); + if (sc[i].must_null) CHECK(pk < PEAK_2500KB, "越界声明在算术前就被拒(不分配)"); + + f = fopen(fp, "wb"); + CHECK(f != NULL, "写畸形 BMP 到夹具目录"); + if (f) { fwrite(buf, 1, sizeof buf, f); fclose(f); } + + im = meow_img_load_file_dim(fp, 720); + snprintf(name, sizeof name, "%s → _dim 不崩%s", sc[i].tag, + sc[i].must_null ? "且返回 NULL" : ""); + CHECK(im == NULL || !sc[i].must_null, name); + meow_img_free(im); + + im = meow_img_load_mem_dim(buf, (int)sizeof buf, 720); + snprintf(name, sizeof name, "%s → _mem_dim 不崩%s", sc[i].tag, + sc[i].must_null ? "且返回 NULL" : ""); + CHECK(im == NULL || !sc[i].must_null, name); + meow_img_free(im); + free(fp); + } + + /* 正例对照:合法 64×64 32bpp 必须仍能解(含 h<0 的正上方行序) */ + { + unsigned char *big = (unsigned char *)malloc(54 + 64 * 64 * 4); + int tw = 0, th = 0; + unsigned char *px = NULL; + size_t n; + CHECK(big != NULL, "分配正例 BMP 缓冲"); + if (big) { + n = bmp_build_64(big, 0); + CHECK(meow_bmp_decode_stream(NULL, big, n, 32, &tw, &th, &px) == 1 && + tw == 32 && th == 32, "正例:64x64 32bpp 正序 → 32x32"); + free(px); px = NULL; tw = th = 0; + n = bmp_build_64(big, 1); + CHECK(meow_bmp_decode_stream(NULL, big, n, 32, &tw, &th, &px) == 1 && + tw == 32 && th == 32, "正例:64x64 32bpp 倒序(h<0)→ 32x32"); + free(px); + free(big); + } + } + + /* PNG:tw*th*4 在 ILP32 上回绕成 0 的坐标点(65535² 头 + maxdim=32768) */ + { + unsigned char pb[64]; + int tw = 0, th = 0; + unsigned char *px = NULL; + size_t pn = png_ihdr_only(pb, 65535, 65535); + CHECK(meow_png_decode_stream(NULL, pb, pn, 32768, &tw, &th, &px) == 0, + "PNG 65535² maxdim=32768 → 拒绝(目标图字节数会回绕)"); + CHECK(px == NULL, "PNG 拒绝时不返回像素缓冲"); + } + } + /* ---- S8 内存入口 ---- */ printf("[S8] 内存入口 meow_img_load_mem_dim\n"); {