diff --git a/sdk/meow_jpeg.c b/sdk/meow_jpeg.c index 934056c..0330333 100644 --- a/sdk/meow_jpeg.c +++ b/sdk/meow_jpeg.c @@ -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; } diff --git a/sdk/meow_jpeg.h b/sdk/meow_jpeg.h index 11364d8..5aac22b 100644 --- a/sdk/meow_jpeg.h +++ b/sdk/meow_jpeg.h @@ -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 等。 diff --git a/sdk/test_meow_img_stream.c b/sdk/test_meow_img_stream.c index 2cfabad..4b1534d 100644 --- a/sdk/test_meow_img_stream.c +++ b/sdk/test_meow_img_stream.c @@ -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"); {