P1-A(像素错误):mj_boundary() 在「over 且 marker 非 RSTn」时 return 0 提前退出
扫描。熵码位缓冲最多 9 位前瞻,会在最后一个 MCU/块**之前**把扫描结束后的 marker
(DHT/EOI)吃掉;此时最后 1~2 个 MCU 的有效比特还在 bitbuf 里,return 0 把它们丢掉,
分量平面 memset(128) 的灰底直接露出来(低熵/纯白渐进式右下角 #808080)。
修:扫描循环本来就知道确定的 MCU/块数 —— 一律继续解完(位耗尽后 mj_eb 返回 0 做
零填充),marker 交给外层主循环。实测(纯白 2000x3000 progressive 无 restart):
修复前全图最小通道 128 / 与 stb 最大差 127 / >40 差值的通道 9 个;修复后
最小通道 253~255、最大差 ≤2、>40 通道 0。
测试加固(评审指出旧断言是抽样蒙混):
- S12「与无 restart 逐字节一致」改为**全图 memcmp**(去掉 64×64 网格 MAD<0.1 的兜底);
修复后 9 档 restart 全部逐字节一致。
- 新增 S15:纯白 2000x3000 progressive(q50/60/70/80,无 restart)+ mostly-white q95,
断言全图最小通道/右下角 24×24 最小通道 ≥250(丢 MCU 必露 128)、与 stb 全图逐像素比较。
用 HEAD(11a4fcd) 的 meow_jpeg.c 重编同一测试:4 档纯白全部 FAIL(min=128,
>40 通道 9),修复后全 PASS。
P1-B(内存放大/安全):mj_sof() 按 SOF 声明尺寸分配平面,降档循环在 base_n 已为 1
时不再生效,17~23 字节的 `SOI+SOF0(65535×65535)+EOI` 会让公开入口 _dim(720)
分配并 memset 64MiB(1 分量)/96MiB(3 分量)后才返回 NULL。
修:降档循环之后补硬闸 `if (plane_bytes + target_bytes > MJ_PEAK_CAP) return 0;`
→ 回退 stb(同文件 stb 是 0.02MiB 安全失败)。
回归 S16:构造 SOF 65535²(1/3 分量)→ 后端返回 0、公开入口 NULL、峰值 0.02MiB
(修复前 64.00 / 96.00MiB)。
meow_jpeg.h 注明「峰值只与输出尺寸相关」的适用范围:畸形/超大声明尺寸直接拒绝(安全失败)。
验证:test_meow_img_stream ALL PASS(含 ASan 整轮 rc=0 无报告);test_meow_img_budget /
test_meow_img_color ALL PASS;run_tests.sh rc=0(zip 106 / cbz 296 / membudget PASS);
run_webtoon_tests.sh rc=0;build_sdk.sh 零警告,MIPS32 rel2;体积 meow_jpeg.o 17,212B
+ meow_png_stream.o 8,456B。峰值不变(修复不增加分配)。
1183 lines
52 KiB
C
1183 lines
52 KiB
C
/* 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;
|
||
}
|