sdk: 图片层追加预算解码入口,JPEG 解码期缩解(峰值 43.7→14.0 MiB)

M6 finding(vuln/high):meow_img_load_file 必先整幅 RGBA 解码,1600×2400 漫画页
瞬时峰值 20.2 MiB(15.4 MiB 输出 + 5.5 MiB YCbCr 平面),超设备 ~14MB 口径,
App 侧无法规避。

调研(vendored stb v2.30):无解码期缩放能力——JPEG 走固定 hs/vs 重采样到全尺寸,
PNG 一次性 inflate 整幅 raw_image,公共 API 只有 load/load_16/loadf/info。
完整缩小 IDCT 需改编 1/2、1/4、1/8 IDCT + 进升级系数存储 + MCU 尺寸,改动面大且
易出静默像素错误,故不重写解码器。

改为受控的最小追加(sdk/stb_image.h):
- 新增 stbi_load_scaled / stbi_load_scaled_from_memory:JPEG 输出级按步长 s 抽取
  行/列(resample 状态机逐行推进,仅采样行做颜色转换),整幅原尺寸输出缓冲从不
  物化;scale<=1 与旧 API 逐字节等价,非 JPEG 忽略。
- 进升级 JPEG:各分量系数 IDCT 进像素平面后立即释放(原来拖到输出分配之后)。
- 旧公共 API 行为不变。

SDK 追加式入口(旧 API 一字未改):
- meow_img_load_file_dim / meow_img_load_mem_dim(path/buf, maxdim):stbi_info 头探测
  选 1/2、1/4、1/8 整数档(取「长边/s ≥ maxdim」最大档,不欠采样),再交既有
  meow_img_downscale 精修;小图/maxdim<=0 走旧路径。
- meow_img_probe_file:只读头取尺寸,供 App 在 OOM 前提示。

实测(宿主 test_meow_img_budget,--wrap malloc 打点,全部 PASS):
  8.32MP JPEG   43.70 → 13.96 MiB(新/旧 0.32)
  1600×2400 页  20.16 →  9.18 MiB
  progressive   43.70 → 35.83 MiB(系数阵列扫描期驻留,残余上限)
  PNG 2400×1600 25.65 → 25.65 MiB(stb 无 PNG 缩解,如实记录)
  BMP 2400×1600 14.65 → 15.23 MiB(同上,多一次降采样小缓冲)
缩解结果与「整幅解码+downscale」网格平均差 6.28;对 PIL 逐像素比对与全幅解码
同误差量级(mean ≤0.02, max ≤3)。

验证:build_sdk.sh 重编 meow_ui.a 0 warning;examples/hello 重链 0 warning;
tuku 用 MEOW_SDK_DIR 指向本分支 sdk 重链 0 warning;既有 test_meow_img_color 仍全 PASS。
This commit is contained in:
AskaEth committed 2026-11-04 21:02:39 +08:00
1 parent 5f40d6fd87
commit 21bffa1aeb
5 files changed
+684 -12

No files matched your search

+69
View File
@@ -0,0 +1,69 @@
#!/usr/bin/env python3
"""test_meow_img_budget_gen.py — 为宿主测试 test_meow_img_budget.c 生成图片夹具。
用法:python3 test_meow_img_budget_gen.py <输出目录>
依赖:Pillow(PIL)。夹具只含结构化图案(白底/黑线/灰块),体积极小。
生成:
big.jpg 3200x2600 baseline 4:2:0 JPEG(8.32MP,峰值对比主用例)
bigprog.jpg 3200x2600 progressive JPEG(验证进升级解码走同一缩解路径)
page.jpg 1600x2400 baseline JPEG(finding 里 15.4MB 的漫画页原尺寸)
small.jpg 64x64 baseline JPEG(「小图不动」用例)
big.png 2400x1600 RGB PNG(stb 无解码期缩放:峰值如实记录,不作改善断言)
big.bmp 2400x1600 24bpp BMP(同上)
broken.jpg JPEG 魔数 + 垃圾字节(头部解析失败,坏图用例)
"""
import os
import sys
from PIL import Image, ImageDraw
def page_pattern(w, h):
"""漫画页近似:白底 + 分镜框线 + 灰阶网点带。内容稀疏 → JPEG 体积小。"""
im = Image.new("RGB", (w, h), (250, 249, 245))
d = ImageDraw.Draw(im)
for k in range(0, min(w, h) - 400, 60):
d.rectangle([100 + k, 100 + k, 400 + k, 300 + k], outline=(12, 12, 14), width=5)
for band in range(14):
y0 = band * h // 14
for x in range(0, w, 9):
d.line([(x, y0), (x + 4, y0 + 7)], fill=(110, 110, 116))
return im
def main():
if len(sys.argv) != 2:
print(__doc__)
return 2
out = sys.argv[1]
os.makedirs(out, exist_ok=True)
big = page_pattern(3200, 2600)
big.save(os.path.join(out, "big.jpg"), "JPEG", quality=55, subsampling=2)
big.save(os.path.join(out, "bigprog.jpg"), "JPEG", quality=55, subsampling=2,
progressive=True)
small = page_pattern(64, 64)
small.save(os.path.join(out, "small.jpg"), "JPEG", quality=80, subsampling=2)
page = page_pattern(1600, 2400)
page.save(os.path.join(out, "page.jpg"), "JPEG", quality=55, subsampling=2)
mid = page_pattern(2400, 1600)
mid.save(os.path.join(out, "big.png"), "PNG")
mid.save(os.path.join(out, "big.bmp"), "BMP")
# 坏图:JPEG 魔数 + 垃圾字节 → 头部解析失败(stb 对「截断但头部完整」很宽容,不能只截断)
garbage = bytes((i * 37 + 11) & 0xFF for i in range(256))
with open(os.path.join(out, "broken.jpg"), "wb") as f:
f.write(b"\xff\xd8\xff\xe0" + garbage)
for name in ("big.jpg", "bigprog.jpg", "page.jpg", "small.jpg", "big.png",
"big.bmp", "broken.jpg"):
print(" %-12s %8d B" % (name, os.path.getsize(os.path.join(out, name))))
return 0
if __name__ == "__main__":
sys.exit(main())