fix: 畸形 DHT 拒绝 + 渐进式 restart 重同步(M23 r1 P1-1/P1-2)

P1-1(内存安全):mj_huff_build 构造 9 位快表前校验规范码空间
(code + bits[l] > (1<<l) → 表非法),填表时再夹一道 <512 边界;mj_dht 失败
即返回 0 → 后端拒绝 → 回退 stb。此前过订阅 DHT(如「长度 2 的码 100 个」)会把
fast[512] 下标推到 12799,28~220 字节的畸形 JPEG 就栈溢出(ASan 实测
stack-buffer-overflow,非 ASan 直接 SIGSEGV/SIGBUS)。
回归:码长 1..4 × 符号数 3..255 共 1012 组畸形 DHT 全部后端拒绝、无崩溃;
「有效 JPEG + 注入坏 DHT」走拒绝 + 回退(不花屏)。

P1-2(能力):三处扫描循环的区间重同步统一到 mj_boundary,**只在 total == rst
(编码器区间长度)时重同步**。原实现一见「over && marker∈D0..D7」就重同步,但熵码
位缓冲有最多 9 位前瞻,over 会在区间真正结束前 1~2 个 MCU 置位——提前重同步会丢掉
bitbuf 里本区间剩余块的有效比特,随即失步(渐进式 DC 扫描每块仅 ~1 bit,前瞻能跨过
整个 MCU)。这正是 Pillow restart_marker_blocks=1/2/3/4/5/8/16/64/200 的渐进式
100% 回退 stb(25.8MiB)的根因。另修 mj_restart 裸扫把 FF 00(数据字节 0xFF 的填充)
里的 00 误当 marker。
回归:9 个渐进式 + restart 夹具全部 BACKEND=1、峰值 1.86MiB(与无 restart 相同)、
与同编码无 restart 文件逐字节一致(mad=0.000);baseline + restart 仍逐字节一致。

附带(评审 P2 两项,改动极小且已验证):
- PNG 8bit gray/RGB 的 tRNS 透明接通(8bit 采样值在 tRNS 低字节),与 stb 逐点 alpha 一致;
- 拒绝 16bit 量化表(只属 12bit 精度;收下会让 F*qt 在 mj_accim 里 int32 溢出,UB)。
- meow_img_load_file_step 的 JPEG 走真缩解后端(PNG/BMP 暂留 stb:阅读器预算模型仍是
  M22「整幅物化」口径,待 M27 同步后放开)。

验证:ASan 构建全 PASS;test_meow_img_stream 111 项全 PASS;test_meow_img_budget /
test_meow_img_color 全 PASS;tuku run_tests.sh rc=0(zip 106 / cbz 296,含 membudget)、
run_webtoon_tests.sh rc=0;build_sdk.sh 零警告,MIPS32 rel2 目标码。
峰值不变(修复不增加分配):2000x3000 baseline/progressive 1.86MiB、PNG 1.33MiB。
体积:meow_jpeg.o 17,284B + meow_png_stream.o 8,456B = 25.1KB。
This commit is contained in:
AskaEth committed 2026-11-05 22:39:09 +08:00
1 parent e9c0749609
commit 11a4fcd730
6 files changed
+377 -54

No files matched your search

+14 -7
View File
@@ -31,15 +31,20 @@ int meow_img_probe_file(const char *path, int *w, int *h, int *channels);
* 「meow_img_load_file + meow_img_downscale(maxdim)」逐字节同口径(公式一致)。 * 「meow_img_load_file + meow_img_downscale(maxdim)」逐字节同口径(公式一致)。
* *
* 实现(M23 起):先进真缩解后端(内部 meow_jpeg.c / meow_png_stream.c),解码期只算目标分辨率: * 实现(M23 起):先进真缩解后端(内部 meow_jpeg.c / meow_png_stream.c),解码期只算目标分辨率:
* JPEG:DCT 域缩解(每 8×8 块只解出 N×N 像素,N 由 8·maxdim/长边 取「不欠采样」的最小值), * JPEG:DCT 域缩解(每 8×8 块只解出 N×N 像素,N 由 8·maxdim/长边 取「不欠采样」的最小值;
* 渐进式只解到首个精细化扫描(首个 Ah>0)——低频带已完整,不再整幅驻留系数阵列; * N=1..7,N≥8 交回 stb),渐进式跳过精细化扫描、只解各频带首次扫描 —— 低频带已完整,
* 不整幅驻留系数阵列;
* PNG :libz 逐行 inflate + 反滤波,按步长丢行丢列直写目标尺寸(gray/gray+A/RGB/RGBA/ * PNG :libz 逐行 inflate + 反滤波,按步长丢行丢列直写目标尺寸(gray/gray+A/RGB/RGBA/
* 调色板/16bit,16bit 取高字节); * 调色板/1-16bit,16bit 取高字节,tRNS 透明含 8bit 灰/RGB);
* BMP :按目标行 seek 到源行 + 行内跳列(8bpp 调色板 / 16bpp RGB555 / 24bpp / 32bpp)。 * BMP :按目标行 seek 到源行 + 行内跳列(8bpp 调色板 / 16bpp RGB555 / 24bpp / 32bpp)。
* 峰值只与输出尺寸相关,不随源图分辨率/压缩体积线性增长(2000×3000 页 ≤2.5MB 量级)。 * 峰值只与输出尺寸相关,不随源图分辨率/压缩体积线性增长(2000×3000 页 ≤2.5MB 量级)。
* 不支持的特性(CMYK/YCCK 4 分量 JPEG、算术编码、Adam7 交错 PNG、RLE/1-4bpp BMP、几乎等尺寸) * 不支持的特性(CMYK/YCCK 4 分量 JPEG、算术编码、过订阅 Huffman 表、16bit 量化表、Adam7
* 自动回退 stb 旧路径(meow_img_load_file + 缩解/降采样),对外语义完全不变。 * 交错 PNG、RLE/1-4bpp BMP、几乎等尺寸)自动回退 stb 旧路径,对外语义完全不变。
* 小图(长边 ≤ maxdim)走旧路径,与原 API 完全一致;maxdim<=0 等价旧入口。失败返回 NULL。 */ * 小图(长边 ≤ maxdim)走旧路径,与原 API 完全一致;maxdim<=0 等价旧入口。失败返回 NULL。
*
* 尺寸口径说明(M23 评审 P2-2):本入口按「原图 + meow_img_downscale」公式算目标尺寸;M12 旧
* 实现经 stb 的 ceil(w/s) 缩解再精修,源尺寸不是档位整数倍时可能与本入口差 1px(实测 1999×2999:
* 旧 320×480 vs 新 319×480)。阅读器按显示框缩放,无实际影响;PNG/BMP 无此差异。 */
meow_img *meow_img_load_file_dim(const char *path, int maxdim); meow_img *meow_img_load_file_dim(const char *path, int maxdim);
meow_img *meow_img_load_mem_dim(const void *buf, int len, int maxdim); meow_img *meow_img_load_mem_dim(const void *buf, int len, int maxdim);
@@ -51,7 +56,9 @@ int meow_img_pick_scale(int longside, int maxdim);
/* 追加(M22)显式档位解码:语义与 meow_img_load_file_dim 相同(结果长边 ≤ maxdim, /* 追加(M22)显式档位解码:语义与 meow_img_load_file_dim 相同(结果长边 ≤ maxdim,
* 超了就再精修一次),只是缩解档位由调用方决定。内存吃紧时可以选更粗的档位——输出更小、 * 超了就再精修一次),只是缩解档位由调用方决定。内存吃紧时可以选更粗的档位——输出更小、
* 峰值更低,代价是细节变软。step <= 1 等价长边约束入口;step > 8 按 8 处理。 * 峰值更低,代价是细节变软。step <= 1 等价长边约束入口;step > 8 按 8 处理。
* 非 JPEG 格式 stb 没有解码期缩放,step 被忽略(整幅解码)。 */ * M23 起:JPEG 先走真缩解后端(step 折算成目标长边 ceil(长边/step),后端按该长边选 DCT 阶),
* 峰值只与输出尺寸相关;不支持的特性回退 stb(step 原样传入)。PNG/BMP 暂仍走 stb —— 阅读器的
* 单次解码预算模型(M22)还是「整幅物化」口径,待 M27 同步后放开(见 M23 r1 报告)。 */
meow_img *meow_img_load_file_step(const char *path, int maxdim, int step); meow_img *meow_img_load_file_step(const char *path, int maxdim, int step);
/* 追加(M22)精确重采样到 w×h(最近邻,缩小/放大都支持)。阅读器用它把「缩解档位」的 /* 追加(M22)精确重采样到 w×h(最近邻,缩小/放大都支持)。阅读器用它把「缩解档位」的
+68 -38
View File
@@ -148,21 +148,26 @@ typedef struct {
int valptr[17]; int valptr[17];
} mj_huff; } mj_huff;
static void mj_huff_build(mj_huff *h) /* 构造 9 位快表 + 规范码表。返回 0 = 表非法(过订阅/码空间越界),调用方必须拒绝该表。
* 过订阅输入(如「长度 2 的码有 100 个」)此前会让 code << shift 把快表下标推到 512 之外,
* 28~220 字节的畸形 JPEG 就能栈溢出 → 这里在填表前校验码空间,填表时再加一道下标边界。 */
static int mj_huff_build(mj_huff *h)
{ {
int code = 0, k = 0, l; int code = 0, k = 0, l;
memset(h->fast, 0, sizeof h->fast); memset(h->fast, 0, sizeof h->fast);
h->maxcode[0] = -1; h->maxcode[0] = -1;
for (l = 1; l <= 16; l++) { for (l = 1; l <= 16; l++) {
int i; int i;
if (code + h->bits[l] > (1 << l)) return 0; /* 码空间溢出:过订阅表 */
h->valptr[l] = k; h->valptr[l] = k;
h->mincode[l] = code; h->mincode[l] = code;
for (i = 0; i < h->bits[l]; i++) { for (i = 0; i < h->bits[l]; i++) {
if (l <= 9) { if (l <= 9) {
int shift = 9 - l, j; int shift = 9 - l, j;
unsigned short e = (unsigned short)((l << 8) | h->vals[k]); unsigned short e = (unsigned short)((l << 8) | h->vals[k]);
int base = code << shift;
for (j = 0; j < (1 << shift); j++) for (j = 0; j < (1 << shift); j++)
h->fast[(code << shift) | j] = e; if ((base | j) < 512) h->fast[base | j] = e;
} }
code++; code++;
k++; k++;
@@ -170,6 +175,7 @@ static void mj_huff_build(mj_huff *h)
h->maxcode[l] = h->bits[l] ? code - 1 : -1; h->maxcode[l] = h->bits[l] ? code - 1 : -1;
code <<= 1; code <<= 1;
} }
return 1;
} }
static int mj_hdecode(mj_src *s, const mj_huff *h) static int mj_hdecode(mj_src *s, const mj_huff *h)
@@ -341,16 +347,12 @@ static int mj_dqt(mj_img *img, const unsigned char *d, int len)
int pq = d[p] >> 4, tq = d[p] & 15, i; int pq = d[p] >> 4, tq = d[p] & 15, i;
p++; p++;
if (tq > 3) return 0; if (tq > 3) return 0;
if (pq == 0) { /* 16bit 量化表只出现在 12bit 精度(本模块不支持);收下会让 F*qt 在 mj_accum 里
if (p + 64 > len) return 0; * int32 溢出(UB)。直接拒绝 → 后端返回 0 → 回退 stb。 */
for (i = 0; i < 64; i++) img->qt[tq][mj_zigzag[i]] = d[p + i]; if (pq != 0) return 0;
p += 64; if (p + 64 > len) return 0;
} else { for (i = 0; i < 64; i++) img->qt[tq][mj_zigzag[i]] = d[p + i];
if (p + 128 > len) return 0; p += 64;
for (i = 0; i < 64; i++)
img->qt[tq][mj_zigzag[i]] = (d[p + 2 * i] << 8) | d[p + 2 * i + 1];
p += 128;
}
img->have_qt[tq] = 1; img->have_qt[tq] = 1;
} }
return 1; return 1;
@@ -371,7 +373,7 @@ static int mj_dht(mj_img *img, const unsigned char *d, int len)
if (total > 256 || p + total > len) return 0; if (total > 256 || p + total > len) return 0;
for (i = 0; i < total; i++) h->vals[i] = d[p + i]; for (i = 0; i < total; i++) h->vals[i] = d[p + i];
p += total; p += total;
mj_huff_build(h); if (!mj_huff_build(h)) return 0; /* 过订阅/码空间越界:拒绝整段 → 回退 stb */
if (tc) img->have_ac[th] = 1; else img->have_dc[th] = 1; if (tc) img->have_ac[th] = 1; else img->have_dc[th] = 1;
} }
} }
@@ -558,7 +560,10 @@ static void mj_skip_entropy(mj_img *img)
} }
} }
/* 重启标记:对齐位缓冲并吃掉 RSTn,重置 DC 预测。 */ /* 重启标记:对齐位缓冲并吃掉 RSTn,重置 DC 预测。
*
* 裸扫时必须跳过 FF 00(数据字节 0xFF 的填充):区间末尾的填充字节常常正好是 0xFF,
* 它在码流里写成 `FF 00 FF Dn`;把中间那个 00 当成 marker 会让扫描提前结束并失步。 */
static void mj_restart(mj_img *img) static void mj_restart(mj_img *img)
{ {
mj_src *s = &img->src; mj_src *s = &img->src;
@@ -573,14 +578,47 @@ static void mj_restart(mj_img *img)
c = mj_raw(s); c = mj_raw(s);
if (c < 0) { s->over = 1; break; } if (c < 0) { s->over = 1; break; }
if (c != 0xFF) continue; if (c != 0xFF) continue;
do { c = mj_raw(s); if (c < 0) { s->over = 1; break; } } while (c == 0xFF); for (;;) {
if (c < 0) break; c = mj_raw(s);
if (c >= 0xD0 && c <= 0xD7) break; if (c < 0) { s->over = 1; break; }
s->marker = c; s->over = 1; if (c == 0xFF) continue; /* 填充 FF */
break; if (c >= 0xD0 && c <= 0xD7) goto rst_found;
if (c == 0x00) break; /* FF00 = 数据字节,继续扫 */
s->marker = c; s->over = 1; /* 别的 marker:熵段结束 */
goto rst_done;
}
if (s->over) break;
} }
} }
rst_found:
for (i = 0; i < img->ncomp; i++) img->c[i].pred = 0; for (i = 0; i < img->ncomp; i++) img->c[i].pred = 0;
rst_done:
;
}
/* 区间边界处理(三处扫描循环共用)。返回:0 = 熵段结束(调用方退出扫描);
* 1 = 继续;2 = 刚做过区间重同步(AC 扫描需清 EOBRUN)。
*
* 只在 total == rst(编码器的区间长度)时重同步。**不能**一看到 over&&RSTn 就重同步:
* 熵码位缓冲有最多 9 位前瞻,`over` 会在区间真正结束前 1~2 个 MCU 就被置位,
* 此时 bitbuf 里还压着本区间剩余块的有效比特——提前重同步会把它们丢掉并失步
* (渐进式 DC 扫描每块仅 ~1 bit,前瞻能跨过整个 MCU;这是 rst=2..200 花屏的根因)。
* 到达 total==rst 时,RSTn 要么已被前瞻吃掉(over/marker=RSTn,清掉即可),
* 要么还在流里(裸扫对齐)——mj_restart 两种都处理。 */
static int mj_boundary(mj_img *img, int *total)
{
mj_src *s = &img->src;
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;
} }
typedef struct { typedef struct {
@@ -600,12 +638,7 @@ static void mj_scan_seq(mj_img *img, const mj_scan *sc)
for (my = 0; my < mcus_y; my++) { for (my = 0; my < mcus_y; my++) {
for (mx = 0; mx < mcus_x; mx++) { for (mx = 0; mx < mcus_x; mx++) {
int j; int j;
if (img->rst && total == img->rst) { if (!mj_boundary(img, &total)) return;
if (img->src.over && !(img->src.marker >= 0xD0 && img->src.marker <= 0xD7)) return;
mj_restart(img);
total = 0;
}
if (img->src.over) return;
if (single) { if (single) {
mj_comp *c = &img->c[sc->ci[0]]; mj_comp *c = &img->c[sc->ci[0]];
c->td = sc->td[0]; c->ta = sc->ta[0]; c->td = sc->td[0]; c->ta = sc->ta[0];
@@ -635,12 +668,9 @@ static void mj_scan_dc_prog(mj_img *img, const mj_scan *sc)
for (my = 0; my < mcus_y; my++) { for (my = 0; my < mcus_y; my++) {
for (mx = 0; mx < mcus_x; mx++) { for (mx = 0; mx < mcus_x; mx++) {
int j; int j;
if (img->rst && total == img->rst) { if (!mj_boundary(img, &total)) {
if (img->src.over && !(img->src.marker >= 0xD0 && img->src.marker <= 0xD7)) return; return;
mj_restart(img);
total = 0;
} }
if (img->src.over) return;
if (single) { if (single) {
mj_comp *c = &img->c[sc->ci[0]]; mj_comp *c = &img->c[sc->ci[0]];
c->td = sc->td[0]; c->td = sc->td[0];
@@ -669,13 +699,9 @@ static void mj_scan_ac_prog(mj_img *img, const mj_scan *sc)
c->ta = sc->ta[0]; c->ta = sc->ta[0];
for (by = 0; by < c->oh; by++) { for (by = 0; by < c->oh; by++) {
for (bx = 0; bx < c->ow; bx++) { for (bx = 0; bx < c->ow; bx++) {
if (img->rst && total == img->rst) { int rb = mj_boundary(img, &total);
if (img->src.over && !(img->src.marker >= 0xD0 && img->src.marker <= 0xD7)) return; if (!rb) return;
mj_restart(img); if (rb == 2) eobrun = 0; /* 区间重同步:EOB 游程不能跨 RST */
total = 0;
eobrun = 0;
}
if (s->over) return;
if (eobrun > 0) { if (eobrun > 0) {
eobrun--; eobrun--;
} else { } else {
@@ -709,7 +735,9 @@ static void mj_scan_ac_prog(mj_img *img, const mj_scan *sc)
} }
total++; total++;
img->blocks++; img->blocks++;
if (s->over) return; /* 这里**不能**再 `if (s->over) return`:块末的位前瞻可能已吃掉本区间的 RSTn
* (over=1),必须留给下一轮 mj_boundary 做区间重同步,否则渐进式 + restart 的
* AC 扫描会在第一个区间边界处静默退出、把后续 RST 当独立 marker 跳过 → 失步。 */
} }
} }
} }
@@ -838,6 +866,7 @@ static int mj_decode(mj_img *img, int maxdim, int *tw, int *th, unsigned char **
} }
continue; continue;
} }
{
if (!img->progressive) { if (!img->progressive) {
mj_scan_seq(img, &sc); mj_scan_seq(img, &sc);
} else if (sc.ss == 0) { } else if (sc.ss == 0) {
@@ -846,6 +875,7 @@ static int mj_decode(mj_img *img, int maxdim, int *tw, int *th, unsigned char **
if (sc.ns != 1) return 0; if (sc.ns != 1) return 0;
mj_scan_ac_prog(img, &sc); mj_scan_ac_prog(img, &sc);
} }
}
s->bitcnt = 0; s->bitcnt = 0;
s->bitbuf = 0; s->bitbuf = 0;
/* 一个熵段结束:位读取器可能已把下一个 marker 吃掉,回填给主循环, /* 一个熵段结束:位读取器可能已把下一个 marker 吃掉,回填给主循环,
+12 -5
View File
@@ -128,17 +128,19 @@ static void png_get_px(const png_ctx *c, const unsigned char *row, int x, unsign
} else { } else {
unsigned char g = (unsigned char)(v * 255 / ((1 << d) - 1)); unsigned char g = (unsigned char)(v * 255 / ((1 << d) - 1));
o[0] = o[1] = o[2] = g; o[0] = o[1] = o[2] = g;
o[3] = 255; /* tRNS 存的是原始采样值(0..2^d-1),不是展开后的 0..255 */
o[3] = (c->color == 0 && c->have_trns && (unsigned)v == c->trns[0]) ? 0 : 255;
} }
return; return;
} }
if (d == 8) { if (d == 8) {
if (c->color == 0) { /* gray */ if (c->color == 0) { /* gray */
o[0] = o[1] = o[2] = row[x]; o[0] = o[1] = o[2] = row[x];
o[3] = 255; o[3] = (c->have_trns && row[x] == c->trns[0]) ? 0 : 255;
} else if (c->color == 2) { /* RGB */ } else if (c->color == 2) { /* RGB */
o[0] = row[x * 3]; o[1] = row[x * 3 + 1]; o[2] = row[x * 3 + 2]; o[0] = row[x * 3]; o[1] = row[x * 3 + 1]; o[2] = row[x * 3 + 2];
o[3] = 255; o[3] = (c->have_trns && row[x * 3] == c->trns[0] &&
row[x * 3 + 1] == c->trns[1] && row[x * 3 + 2] == c->trns[2]) ? 0 : 255;
} else if (c->color == 3) { /* palette */ } else if (c->color == 3) { /* palette */
int i = row[x]; int i = row[x];
o[0] = c->pal[i][0]; o[1] = c->pal[i][1]; o[2] = c->pal[i][2]; o[0] = c->pal[i][0]; o[1] = c->pal[i][1]; o[2] = c->pal[i][2];
@@ -333,9 +335,14 @@ int meow_png_decode_stream(FILE *f, const unsigned char *mem, size_t len,
if (c.color == 3) { if (c.color == 3) {
for (i = 0; i < (int)clen && i < 256; i++) c.trns_a[i] = d[i]; for (i = 0; i < (int)clen && i < 256; i++) c.trns_a[i] = d[i];
} else if (c.color == 0 && clen >= 2) { } else if (c.color == 0 && clen >= 2) {
c.trns[0] = d[0]; /* 高字节(已降 8bit) */ /* 8bit/低位深:采样值存在低字节;16bit:与像素一样取高字节比较 */
c.trns[0] = (c.depth == 16) ? d[0] : d[1];
} else if (c.color == 2 && clen >= 6) { } else if (c.color == 2 && clen >= 6) {
c.trns[0] = d[0]; c.trns[1] = d[2]; c.trns[2] = d[4]; if (c.depth == 16) {
c.trns[0] = d[0]; c.trns[1] = d[2]; c.trns[2] = d[4];
} else {
c.trns[0] = d[1]; c.trns[1] = d[3]; c.trns[2] = d[5];
}
} }
} else if (memcmp(type, "IDAT", 4) == 0) { } else if (memcmp(type, "IDAT", 4) == 0) {
unsigned remain = clen; unsigned remain = clen;
+28
View File
@@ -110,6 +110,34 @@ meow_img *meow_img_load_file_step(const char *path, int maxdim, int step)
if (!stbi_info(path, &w, &h, &c)) return NULL; if (!stbi_info(path, &w, &h, &c)) return NULL;
int longside = w > h ? w : h; int longside = w > h ? w : h;
if (step == 1 && maxdim <= 0) return meow_img_load_file(path); if (step == 1 && maxdim <= 0) return meow_img_load_file(path);
/* 追加(M23):真缩解后端优先(本入口只接 JPEG)。step 是「输出别比这个档位更细」的提示:
* 折算成目标长边 ceil(长边/step) 传给后端(后端按该长边自动选 DCT 阶),与下面
* stbi_load_scaled(step) 的语义一致,峰值只与输出尺寸相关;maxdim<=0 时不精修,
* 结果仍是档位尺寸。后端不支持(CMYK/算术编码/交错…)时回退 stb,行为与 M22 一致。
*
* PNG/BMP 在本入口暂不接后端:阅读器的单次解码峰值**预算模型**还是 M22 的「整幅物化」
* 口径,实测峰值骤降会让它的 model≤2×measured 自检失真(详见 M23 r1 报告的 M27 待办)。
* 待 M27 同步模型后,把这里的 JPEG 判定去掉即可。 */
{
unsigned char sig[3];
int is_jpeg = 0;
FILE *fp = fopen(path, "rb");
if (fp) {
if (fread(sig, 1, 3, fp) == 3 &&
sig[0] == 0xFF && sig[1] == 0xD8 && sig[2] == 0xFF)
is_jpeg = 1;
fclose(fp);
}
if (is_jpeg) {
int eff = maxdim > 0 ? maxdim : longside;
int step_cap = (longside + step - 1) / step;
unsigned char *bp = NULL;
int bw = 0, bh = 0;
if (step_cap > 0 && step_cap < eff) eff = step_cap;
if (eff > 0 && meow_img_backend_file(path, eff, &bw, &bh, &bp) && bp)
return img_finish_dim(bp, bw, bh, maxdim > 0 ? maxdim : longside);
}
}
unsigned char *p = stbi_load_scaled(path, &w, &h, &c, 4, step); unsigned char *p = stbi_load_scaled(path, &w, &h, &c, 4, step);
/* maxdim<=0:不精修(保留档位输出的原始尺寸)。 */ /* maxdim<=0:不精修(保留档位输出的原始尺寸)。 */
return img_finish_dim(p, w, h, maxdim > 0 ? maxdim : longside); return img_finish_dim(p, w, h, maxdim > 0 ? maxdim : longside);
+254 -4
View File
@@ -180,6 +180,17 @@ static const char *g_gen_py =
" chunk(b'IDAT', zlib.compress(bytes(raw))) + chunk(b'IEND', b''))\n" " chunk(b'IDAT', zlib.compress(bytes(raw))) + chunk(b'IEND', b''))\n"
" open(path, 'wb').write(data)\n" " open(path, 'wb').write(data)\n"
"adam7(os.path.join(out, 'adam7.png'), 1600, 2400)\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"
"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" "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" " bytes((i * 37 + 11) & 0xFF for i in range(256)))\n"
"for n in sorted(os.listdir(out)):\n" "for n in sorted(os.listdir(out)):\n"
@@ -390,6 +401,75 @@ static size_t old_dim_peak(const char *path, int maxdim)
return peak; 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;
}
/* ================================================================== */ /* ================================================================== */
int main(int argc, char **argv) int main(int argc, char **argv)
{ {
@@ -681,22 +761,192 @@ int main(int argc, char **argv)
} }
{ {
size_t base = peak_reset(); size_t base = peak_reset();
meow_img *got = meow_img_load_file_dim(prs, 720); int tw = 0, th = 0;
size_t pk = peak_since(base); unsigned char *px = NULL;
meow_img *ref = ref_thumb(prs, 720); 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 corr = 0.0;
double d = (got && ref) ? mean_abs_diff(got, ref, 64, &corr) : 1e9; 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); peak_report("restart marker 2000x3000", pk);
CHECK(handled == 1, "baseline + restart 走后端(BACKEND=1,不回退 stb)");
CHECK(got && nonblank(got), "restart marker JPEG 解码成功"); CHECK(got && nonblank(got), "restart marker JPEG 解码成功");
CHECK(d < 24.0 && corr > 0.85, "restart marker JPEG 与 stb 结构一致(RST 重同步正确)"); CHECK(d < 24.0 && corr > 0.85, "restart marker JPEG 与 stb 结构一致(RST 重同步正确)");
CHECK(pk <= PEAK_2500KB, "restart marker JPEG 峰值 ≤ 2.5 MiB"); CHECK(pk <= PEAK_2500KB, "restart marker JPEG 峰值 ≤ 2.5 MiB");
meow_img_free(got); meow_img_free(got);
meow_img_free(ref); 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(psw);
free(prs); 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;
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);
}
/* 关键正确性:与本文件自己的 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);
}
/* ---- S8 内存入口 ---- */ /* ---- S8 内存入口 ---- */
printf("[S8] 内存入口 meow_img_load_mem_dim\n"); printf("[S8] 内存入口 meow_img_load_mem_dim\n");
{ {
+1
View File
@@ -165,6 +165,7 @@ if [ -f "$SDK/meow_ui_view.c" ] && [ -f "$SDK/meow_ui_config.c" ]; then
"$SDK"/meow_ui_ime.c "$SDK"/meow_ui_str.c "$SDK"/meow_pinyin.c \ "$SDK"/meow_ui_ime.c "$SDK"/meow_ui_str.c "$SDK"/meow_pinyin.c \
"$SDK"/meow_ui_draw.c "$SDK"/meow_ui_view.c "$SDK"/meow_ui_box.c \ "$SDK"/meow_ui_draw.c "$SDK"/meow_ui_view.c "$SDK"/meow_ui_box.c \
"$SDK"/meow_ui_anim.c "$SDK"/meow_ui_scroll.c "$SDK"/meow_ui_img.c \ "$SDK"/meow_ui_anim.c "$SDK"/meow_ui_scroll.c "$SDK"/meow_ui_img.c \
"$SDK"/meow_jpeg.c "$SDK"/meow_png_stream.c "$SDK"/meow_ui_time.c \
"$SDK"/meow_ui_media.c "$SDK"/meow_notify_ipc.c "$SDK"/meow_ui_config.c \ "$SDK"/meow_ui_media.c "$SDK"/meow_notify_ipc.c "$SDK"/meow_ui_config.c \
"$SDK"/meow_ui_nav.c "$SDK"/stb_image.c "$SDK"/test_host_stub.c \ "$SDK"/meow_ui_nav.c "$SDK"/stb_image.c "$SDK"/test_host_stub.c \
-pthread -lm -lz -o "$OUT"/test_reader_membudget \ -pthread -lm -lz -o "$OUT"/test_reader_membudget \