diff --git a/sdk/include/meow_ui_image.h b/sdk/include/meow_ui_image.h index 8d3a642..819257e 100644 --- a/sdk/include/meow_ui_image.h +++ b/sdk/include/meow_ui_image.h @@ -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)」逐字节同口径(公式一致)。 * * 实现(M23 起):先进真缩解后端(内部 meow_jpeg.c / meow_png_stream.c),解码期只算目标分辨率: - * JPEG:DCT 域缩解(每 8×8 块只解出 N×N 像素,N 由 8·maxdim/长边 取「不欠采样」的最小值), - * 渐进式只解到首个精细化扫描(首个 Ah>0)——低频带已完整,不再整幅驻留系数阵列; + * JPEG:DCT 域缩解(每 8×8 块只解出 N×N 像素,N 由 8·maxdim/长边 取「不欠采样」的最小值; + * N=1..7,N≥8 交回 stb),渐进式跳过精细化扫描、只解各频带首次扫描 —— 低频带已完整, + * 不整幅驻留系数阵列; * PNG :libz 逐行 inflate + 反滤波,按步长丢行丢列直写目标尺寸(gray/gray+A/RGB/RGBA/ - * 调色板/16bit,16bit 取高字节); + * 调色板/1-16bit,16bit 取高字节,tRNS 透明含 8bit 灰/RGB); * BMP :按目标行 seek 到源行 + 行内跳列(8bpp 调色板 / 16bpp RGB555 / 24bpp / 32bpp)。 * 峰值只与输出尺寸相关,不随源图分辨率/压缩体积线性增长(2000×3000 页 ≤2.5MB 量级)。 - * 不支持的特性(CMYK/YCCK 4 分量 JPEG、算术编码、Adam7 交错 PNG、RLE/1-4bpp BMP、几乎等尺寸) - * 自动回退 stb 旧路径(meow_img_load_file + 缩解/降采样),对外语义完全不变。 - * 小图(长边 ≤ maxdim)走旧路径,与原 API 完全一致;maxdim<=0 等价旧入口。失败返回 NULL。 */ + * 不支持的特性(CMYK/YCCK 4 分量 JPEG、算术编码、过订阅 Huffman 表、16bit 量化表、Adam7 + * 交错 PNG、RLE/1-4bpp BMP、几乎等尺寸)自动回退 stb 旧路径,对外语义完全不变。 + * 小图(长边 ≤ 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_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, * 超了就再精修一次),只是缩解档位由调用方决定。内存吃紧时可以选更粗的档位——输出更小、 * 峰值更低,代价是细节变软。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); /* 追加(M22)精确重采样到 w×h(最近邻,缩小/放大都支持)。阅读器用它把「缩解档位」的 diff --git a/sdk/meow_jpeg.c b/sdk/meow_jpeg.c index 4b05372..934056c 100644 --- a/sdk/meow_jpeg.c +++ b/sdk/meow_jpeg.c @@ -148,21 +148,26 @@ typedef struct { int valptr[17]; } 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; memset(h->fast, 0, sizeof h->fast); h->maxcode[0] = -1; for (l = 1; l <= 16; l++) { int i; + if (code + h->bits[l] > (1 << l)) return 0; /* 码空间溢出:过订阅表 */ h->valptr[l] = k; h->mincode[l] = code; for (i = 0; i < h->bits[l]; i++) { if (l <= 9) { int shift = 9 - l, j; unsigned short e = (unsigned short)((l << 8) | h->vals[k]); + int base = code << shift; for (j = 0; j < (1 << shift); j++) - h->fast[(code << shift) | j] = e; + if ((base | j) < 512) h->fast[base | j] = e; } code++; k++; @@ -170,6 +175,7 @@ static void mj_huff_build(mj_huff *h) h->maxcode[l] = h->bits[l] ? code - 1 : -1; code <<= 1; } + return 1; } 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; p++; if (tq > 3) return 0; - if (pq == 0) { - if (p + 64 > len) return 0; - for (i = 0; i < 64; i++) img->qt[tq][mj_zigzag[i]] = d[p + i]; - p += 64; - } else { - if (p + 128 > len) return 0; - for (i = 0; i < 64; i++) - img->qt[tq][mj_zigzag[i]] = (d[p + 2 * i] << 8) | d[p + 2 * i + 1]; - p += 128; - } + /* 16bit 量化表只出现在 12bit 精度(本模块不支持);收下会让 F*qt 在 mj_accum 里 + * int32 溢出(UB)。直接拒绝 → 后端返回 0 → 回退 stb。 */ + if (pq != 0) return 0; + if (p + 64 > len) return 0; + for (i = 0; i < 64; i++) img->qt[tq][mj_zigzag[i]] = d[p + i]; + p += 64; img->have_qt[tq] = 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; for (i = 0; i < total; i++) h->vals[i] = d[p + i]; 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; } } @@ -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) { mj_src *s = &img->src; @@ -573,14 +578,47 @@ static void mj_restart(mj_img *img) c = mj_raw(s); if (c < 0) { s->over = 1; break; } if (c != 0xFF) continue; - do { c = mj_raw(s); if (c < 0) { s->over = 1; break; } } while (c == 0xFF); - if (c < 0) break; - if (c >= 0xD0 && c <= 0xD7) break; - s->marker = c; s->over = 1; - break; + for (;;) { + c = mj_raw(s); + if (c < 0) { s->over = 1; break; } + if (c == 0xFF) continue; /* 填充 FF */ + 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; +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 { @@ -600,12 +638,7 @@ static void mj_scan_seq(mj_img *img, const mj_scan *sc) for (my = 0; my < mcus_y; my++) { for (mx = 0; mx < mcus_x; mx++) { int j; - if (img->rst && total == img->rst) { - if (img->src.over && !(img->src.marker >= 0xD0 && img->src.marker <= 0xD7)) return; - mj_restart(img); - total = 0; - } - if (img->src.over) return; + if (!mj_boundary(img, &total)) return; if (single) { mj_comp *c = &img->c[sc->ci[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 (mx = 0; mx < mcus_x; mx++) { int j; - if (img->rst && total == img->rst) { - if (img->src.over && !(img->src.marker >= 0xD0 && img->src.marker <= 0xD7)) return; - mj_restart(img); - total = 0; + if (!mj_boundary(img, &total)) { + return; } - if (img->src.over) return; if (single) { mj_comp *c = &img->c[sc->ci[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]; for (by = 0; by < c->oh; by++) { for (bx = 0; bx < c->ow; bx++) { - if (img->rst && total == img->rst) { - if (img->src.over && !(img->src.marker >= 0xD0 && img->src.marker <= 0xD7)) return; - mj_restart(img); - total = 0; - eobrun = 0; - } - if (s->over) return; + int rb = mj_boundary(img, &total); + if (!rb) return; + if (rb == 2) eobrun = 0; /* 区间重同步:EOB 游程不能跨 RST */ if (eobrun > 0) { eobrun--; } else { @@ -709,7 +735,9 @@ static void mj_scan_ac_prog(mj_img *img, const mj_scan *sc) } total++; 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; } + { if (!img->progressive) { mj_scan_seq(img, &sc); } 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; mj_scan_ac_prog(img, &sc); } + } s->bitcnt = 0; s->bitbuf = 0; /* 一个熵段结束:位读取器可能已把下一个 marker 吃掉,回填给主循环, diff --git a/sdk/meow_png_stream.c b/sdk/meow_png_stream.c index b505191..4da2594 100644 --- a/sdk/meow_png_stream.c +++ b/sdk/meow_png_stream.c @@ -128,17 +128,19 @@ static void png_get_px(const png_ctx *c, const unsigned char *row, int x, unsign } else { unsigned char g = (unsigned char)(v * 255 / ((1 << d) - 1)); 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; } if (d == 8) { if (c->color == 0) { /* gray */ 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 */ 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 */ int i = row[x]; 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) { for (i = 0; i < (int)clen && i < 256; i++) c.trns_a[i] = d[i]; } 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) { - 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) { unsigned remain = clen; diff --git a/sdk/meow_ui_img.c b/sdk/meow_ui_img.c index 8f20682..2a8c271 100644 --- a/sdk/meow_ui_img.c +++ b/sdk/meow_ui_img.c @@ -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; int longside = w > h ? w : h; 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); /* maxdim<=0:不精修(保留档位输出的原始尺寸)。 */ return img_finish_dim(p, w, h, maxdim > 0 ? maxdim : longside); diff --git a/sdk/test_meow_img_stream.c b/sdk/test_meow_img_stream.c index 509b3ac..2cfabad 100644 --- a/sdk/test_meow_img_stream.c +++ b/sdk/test_meow_img_stream.c @@ -180,6 +180,17 @@ static const char *g_gen_py = " 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" +"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" @@ -390,6 +401,75 @@ static size_t old_dim_peak(const char *path, int maxdim) 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) { @@ -681,22 +761,192 @@ int main(int argc, char **argv) } { size_t base = peak_reset(); - meow_img *got = meow_img_load_file_dim(prs, 720); - size_t pk = peak_since(base); - meow_img *ref = ref_thumb(prs, 720); + 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 = (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); + 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; + 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 内存入口 ---- */ printf("[S8] 内存入口 meow_img_load_mem_dim\n"); { diff --git a/tuku/test/run_tests.sh b/tuku/test/run_tests.sh index d27e491..89d730d 100755 --- a/tuku/test/run_tests.sh +++ b/tuku/test/run_tests.sh @@ -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_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_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_nav.c "$SDK"/stb_image.c "$SDK"/test_host_stub.c \ -pthread -lm -lz -o "$OUT"/test_reader_membudget \