fix: 扫描结尾不再丢最后一个 MCU + 畸形 SOF 尺寸硬闸(M23 r2 P1-A/P1-B)

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。峰值不变(修复不增加分配)。
This commit is contained in:
AskaEth committed 2026-11-06 11:21:03 +08:00
1 parent 11a4fcd730
commit 6c8a1d3925
3 files changed
+192 -19

No files matched your search

+12 -11
View File
@@ -482,6 +482,10 @@ static int mj_sof(mj_img *img, int marker, const unsigned char *d, int len, int
for (i = 0; i < n; i++) if (img->c[i].n > base_n) img->c[i].n = base_n;
plane_bytes = mj_layout(img);
}
/* 硬闸:降到 N=1 仍超上限(畸形/超大声明尺寸,如 23 字节的 SOF 65535² → 亮度面
* 8192² = 64MiB)直接拒绝 → 返回 0 → 回退 stb(stb 对这类文件是 KB 级安全失败)。
* 没有这道闸,公开入口 meow_img_load_file_dim(720) 会被一个头文件打爆设备内存。 */
if (plane_bytes + target_bytes > MJ_PEAK_CAP) return 0;
for (i = 0; i < n; i++) {
mj_comp *c = &img->c[i];
@@ -596,28 +600,25 @@ rst_done:
;
}
/* 区间边界处理(三处扫描循环共用)。返回:0 = 熵段结束(调用方退出扫描);
* 1 = 继续;2 = 刚做过区间重同步(AC 扫描需清 EOBRUN)。
/* 区间边界处理(三处扫描循环共用)。返回:1 = 继续;2 = 刚做过区间重同步(AC 需清 EOBRUN)。
*
* 只在 total == rst(编码器的区间长度)时重同步。**不能**一看到 over&&RSTn 就重同步:
* 熵码位缓冲有最多 9 位前瞻,`over` 会在区间真正结束前 1~2 个 MCU 就被置位,
* 此时 bitbuf 里还压着本区间剩余块的有效比特——提前重同步会把它们丢掉并失步
* 熵码位缓冲有最多 9 位前瞻,`over` 会在区间/扫描真正结束前 1~2 个 MCU 就被置位,
* 此时 bitbuf 里还压着剩余块的有效比特——提前重同步会把它们丢掉并失步
* (渐进式 DC 扫描每块仅 ~1 bit,前瞻能跨过整个 MCU;这是 rst=2..200 花屏的根因)。
* 到达 total==rst 时,RSTn 要么已被前瞻吃掉(over/marker=RSTn,清掉即可),
* 要么还在流里(裸扫对齐)——mj_restart 两种都处理。 */
* 同理,over 且 marker 非 RSTn(扫描结束后的 DHT/EOI 已被前瞻吃掉)时**也不能退出**:
* 扫描循环本来就知道确定的 MCU/块数,把剩余的解完即可(位耗尽后 mj_eb 返回 0 做零填充),
* marker 交给外层主循环处理。曾在此 `return 0` 提前退出,丢掉最后一个 MCU,
* 平面 memset(128) 的灰底直接露出来(低熵/纯白渐进式右下角 3 像素 #808080)。 */
static int mj_boundary(mj_img *img, int *total)
{
mj_src *s = &img->src;
(void)s;
if (img->rst && *total == img->rst) {
mj_restart(img);
*total = 0;
return 2;
}
if (s->over) {
/* 前瞻已吃到 RSTn:本区间剩余块的有效比特还在 bitbuf 里,继续解。
* 只有「非 RST 的 marker」才是熵段真的结束(EOI/下一段的 DHT…)。 */
return (s->marker >= 0xD0 && s->marker <= 0xD7) ? 1 : 0;
}
return 1;
}
+3 -1
View File
@@ -6,7 +6,9 @@
* 首个精细化扫描(首个 Ah>0)为止——低频带此时已完整,再往后只是为了更细的量化精度。
* - PNG(meow_png_stream.c):libz inflate 逐行 defilter,按步长丢行丢列直写目标尺寸。
* - BMP(meow_png_stream.c):按行 seek + 行内跳列(同一套「只碰需要的行」思路)。
* 三者峰值都只与「输出尺寸 + 目标图像」相关,不随源图分辨率线性增长。
* 三者峰值都只与「输出尺寸 + 目标图像」相关,不随源图分辨率线性增长——**前提是文件头
* 声明的尺寸在合理范围内**:畸形/超大声明尺寸(例如 23 字节的 SOF 声明 65535×65535)会被
* 直接拒绝(返回 0 → 回退 stb,安全失败),不会按声明尺寸分配平面。
*
* 不支持的特性一律返回 0,由 meow_ui_img.c 回退 stb(行为与 M23 之前完全一致):
* 4 分量 CMYK/YCCK、算术编码/无损/分层 JPEG、Adam7 交错 PNG、1/2/4bpp 与 RLE BMP 等。
+177 -7
View File
@@ -183,6 +183,18 @@ static const char *g_gen_py =
"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"
@@ -213,9 +225,12 @@ static int file_exists(const char *p)
static void ensure_fixtures(void)
{
char *mark = path_join("base2000.jpg");
int have = file_exists(mark);
free(mark);
/* 标记文件:新增夹具后改这里,旧夹具目录会自动补齐(生成脚本幂等) */
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);
@@ -470,6 +485,87 @@ static unsigned char *inject_badhuff(const unsigned char *orig, size_t n,
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)
{
@@ -865,13 +961,13 @@ int main(int argc, char **argv)
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)) {
double c2 = 0.0;
double d2 = (im && ref) ? mean_abs_diff(im, ref, 64, &c2) : 1e9;
if (!(d2 < 0.1)) all_equal = 0; /* 允许极个别像素 ±1(编码器分表差异) */
printf(" RST=%-3d 与无 restart 平均差 %.4f\n", rb[i], d2);
all_equal = 0;
printf(" RST=%-3d 全图 memcmp 失败(与无 restart 不同)\n", rb[i]);
}
/* 关键正确性:与本文件自己的 stb 整幅解码比对(不同编码器脚本下 restart 文件的
* 系数可能不同,所以拿「无 restart 文件」当基准只能作参考) */
@@ -947,6 +1043,80 @@ int main(int argc, char **argv)
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");
{