Files
MeowOS-For-R3P2/patch_v2/sdk/test_meow_img_stream.c
T
AskaEth ee5796747d feat(sdk): M26 公开仓真缩解图像栈并入主仓 — JPEG DCT/PNG 逐行/BMP 跳行后端 + RGB565 页图
主仓 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"` 与 <time.h>(公开仓自 vendored
  meow_font.h 且无 <time.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 是另一个任务。
2026-11-06 20:44:22 +08:00

1183 lines
52 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* 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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <sys/stat.h>
#include <time.h> /* clock():解码耗时打印 */
#include <math.h> /* 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;
}