From d84dd949c78069b8160e524192df10803e6e6bd0 Mon Sep 17 00:00:00 2001 From: AskaEth Date: Thu, 5 Nov 2026 21:07:59 +0800 Subject: [PATCH] =?UTF-8?q?SDK:=20=E5=9B=BE=E7=89=87=E8=A7=A3=E7=A0=81?= =?UTF-8?q?=E7=9C=9F=E7=BC=A9=E8=A7=A3=E5=90=8E=E7=AB=AF=20=E2=80=94?= =?UTF-8?q?=E2=80=94=20JPEG=20DCT=20=E5=9F=9F=20/=20PNG=20=E9=80=90?= =?UTF-8?q?=E8=A1=8C=20/=20BMP=20=E8=B7=B3=E8=A1=8C=EF=BC=88M23=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit stb 的 stbi_load_scaled 是「先整幅解、再重采样」:2000x3000 baseline 峰值 10.07MB、 渐进式 25.84MB、1600x2400 PNG 25.65MB,真机解码余量只有 ~4.2MB。新增解码后端在 解码期只算目标分辨率,峰值只与输出尺寸相关: - meow_jpeg.c:自写纯 C JPEG 解码(baseline + progressive,Huffman)。按 ceil(8*maxdim/长边) 选 N 点 DCT 缩解(N=1..7),基函数为「8 点 IDCT 按像素区间 盒式平均」的 Q12 定点(DC 恒为 F00/8,N 之间亮度一致);系数边解边累加进分量平面, 不留整幅系数阵列;渐进式跳过精细化扫描(Ah>0)、只解各频带首次扫描 —— 渐进式与 baseline 内存足迹相同(这是 libjpeg 系列做不到的:它对渐进式必须整幅驻留 JBLOCK)。 不支持项(4 分量 CMYK/YCCK、算术/无损/分层、12bit 精度)返回 0 回退 stb。 - meow_png_stream.c:libz 逐行 inflate + 反滤波,只把需要的列写进目标行; gray/gray+A/RGB/RGBA/调色板,1/2/4/8/16bit(16→8 取高字节);Adam7 等回退 stb。 BMP 按目标行 seek + 行内跳列(8bpp 调色板/16bpp RGB555/24/32bpp),不读全图。 - meow_ui_img.c:load_file_dim / load_mem_dim 先进后端,失败/不支持回落 stb 旧路径 (stbi_load_scaled + 精修);输出尺寸与 meow_img_downscale 同公式,对外语义不变。 宿主实测(--wrap malloc,maxdim=720,M23 前后同一夹具): 2000x3000 baseline 10.07 → 1.86 MiB 2000x3000 progressive 25.84 → 1.86 MiB 1600x2400 PNG 25.65 → 1.33 MiB 1600x2400 BMP 15.97 → 1.32 MiB 4MP/8MP/12MP JPEG 9.67/13.37/20.12 → 2.30/2.20/2.29 MiB(不随分辨率线性增长) sdk/test_meow_img_stream.c:峰值断言(≤2.5MiB)、与 stb 整幅+降采样的像素比对 (平均差 4.4-13.7/255,块均值结构相关 ≥0.88)、内存入口逐字节一致、回退矩阵 (CMYK/1bpp BMP/Adam7 PNG)、restart marker、缩解阶扫描 N=4/5/7、坏图容错。 build_sdk.sh 增加 meow_jpeg / meow_png_stream 两个 TU(MIPS32 -Os 零警告)。 体积:目标码 +24.9KB(meow_ui.a +25.5KB),picbrowser 静态二进制 +18.2KB。 --- sdk/build_sdk.sh | 2 +- sdk/include/meow_ui_image.h | 19 +- sdk/meow_jpeg.c | 974 ++++++++++++++++++++++++++++++++++++ sdk/meow_jpeg.h | 48 ++ sdk/meow_png_stream.c | 492 ++++++++++++++++++ sdk/meow_ui_img.c | 22 +- sdk/test_meow_img_budget.c | 16 +- sdk/test_meow_img_stream.c | 762 ++++++++++++++++++++++++++++ 8 files changed, 2320 insertions(+), 15 deletions(-) create mode 100644 sdk/meow_jpeg.c create mode 100644 sdk/meow_jpeg.h create mode 100644 sdk/meow_png_stream.c create mode 100644 sdk/test_meow_img_stream.c diff --git a/sdk/build_sdk.sh b/sdk/build_sdk.sh index d03749f..de53531 100755 --- a/sdk/build_sdk.sh +++ b/sdk/build_sdk.sh @@ -23,7 +23,7 @@ rm -f meow_ui.a *.o # select/net_ipc)随主仓增量并入。 # meow_ui_time.c 必须**编进归档**且排在前面:它提供 meow_ui_time_ms(gfx 已不再实现)与 # __clock_gettime* 强定义,归档成员随 meow_ui_time_ms 的引用被拉入,从而在 -lc 之前解析。 -OBJS="meow_ui_time meow_ui_gfx meow_ui_log meow_ui_notify meow_notify_ipc meow_ui_img meow_ui_widget meow_ui_input meow_ui_theme meow_ui_ime meow_pinyin meow_ui_sys meow_ui_vol meow_ui_activity meow_ui_global meow_ui_dialog meow_ui_config meow_ui_draw meow_ui_view meow_ui_nav meow_ui_media meow_ui_audio stb_image meow_font meow_ui_box meow_ui_anim meow_ui_scroll meow_ui_str meow_cachegov meow_lazygrid meow_select meow_net_ipc" +OBJS="meow_ui_time meow_ui_gfx meow_ui_log meow_ui_notify meow_notify_ipc meow_ui_img meow_jpeg meow_png_stream meow_ui_widget meow_ui_input meow_ui_theme meow_ui_ime meow_pinyin meow_ui_sys meow_ui_vol meow_ui_activity meow_ui_global meow_ui_dialog meow_ui_config meow_ui_draw meow_ui_view meow_ui_nav meow_ui_media meow_ui_audio stb_image meow_font meow_ui_box meow_ui_anim meow_ui_scroll meow_ui_str meow_cachegov meow_lazygrid meow_select meow_net_ipc" CFLAGS="-Os -static -Iinclude -I." for f in $OBJS; do echo "== cc $f ==" diff --git a/sdk/include/meow_ui_image.h b/sdk/include/meow_ui_image.h index 7cde639..8d3a642 100644 --- a/sdk/include/meow_ui_image.h +++ b/sdk/include/meow_ui_image.h @@ -1,7 +1,8 @@ /* * meow_ui_image.h — SDK 图片层(§4,B1)。 * - * 原 meow_ui.h 拆分的一部分(image 模块)。内部 stb_image(单头文件,静态编入 meow_ui.a) + * 原 meow_ui.h 拆分的一部分(image 模块)。解码后端:M23 真缩解(JPEG DCT 域 / PNG 逐行 / + * BMP 跳行,见 meow_jpeg.h)优先,回退 stb_image(单头文件,静态编入 meow_ui.a)。 * 解 PNG/JPEG/BMP,统一转 RGBA。meow_img 为不透明句柄(前向声明见 meow_ui_types.h)。 */ #ifndef MEOW_UI_IMAGE_H @@ -26,10 +27,18 @@ void meow_img_free(meow_img *img); * App 可用它在加载前判断「图太大」并给用户提示,而不必先撞 OOM。 */ int meow_img_probe_file(const char *path, int *w, int *h, int *channels); -/* 追加(M12)预算解码:解码期受长边约束,返回图最长边 ≤ maxdim。 - * JPEG:按 1/2、1/4、1/8 整数步长抽取解码(stbi_load_scaled),整幅原尺寸 RGBA 缓冲从不物化, - * 峰值 ≈ 亮度分量平面 + 缩解输出 + 单行缓冲;随后用 meow_img_downscale 精修到 maxdim。 - * PNG/BMP 等 stb 无解码期缩放能力的格式:整幅解码后立即降采样(峰值同旧入口,见 M12 报告)。 +/* 追加(M12/M23)预算解码:解码期受长边约束,返回图最长边 ≤ maxdim,尺寸与 + * 「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)——低频带已完整,不再整幅驻留系数阵列; + * PNG :libz 逐行 inflate + 反滤波,按步长丢行丢列直写目标尺寸(gray/gray+A/RGB/RGBA/ + * 调色板/16bit,16bit 取高字节); + * 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。 */ 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); diff --git a/sdk/meow_jpeg.c b/sdk/meow_jpeg.c new file mode 100644 index 0000000..4b05372 --- /dev/null +++ b/sdk/meow_jpeg.c @@ -0,0 +1,974 @@ +/* + * meow_jpeg.c — M23 真缩解后端(一):JPEG DCT 域缩解 + 格式分派。 + * + * 背景(M20 审计实测):stb 的 stbi_load_scaled 是「先整幅解、再重采样」——baseline 分量面 + * 1.5·W·H、渐进式再加 3·W·H,2000×3000 baseline 峰值 +10.1MB、渐进 +23.5MB,真机 kill 点 + * 只剩 ~4.2MB 余量。本文件换成「解码期只算目标分辨率」: + * + * - 按目标显示尺寸选 DCT 缩解档 N ∈ {1,2,4}(1/8、1/4、1/2 分辨率),每 8×8 块只解出 + * N×N 个像素——N 点基函数是「8 点 IDCT 按像素区间盒式平均」的定点近似,DC 项恒为 F00/8, + * 与「整幅解完再降采样」结构一致(差异只在高频细节)。 + * - 系数边解边累加进分量平面,不留整幅系数阵列:baseline/渐进式内存足迹相同。 + * - 渐进式只解到首个精细化扫描(首个 Ah>0)为止:此时各频带的首次扫描(Ah=0)已到齐, + * 低频信息完整;再往后只是同一批系数的量化精度细化。因此**不需要**整幅系数的 + * successive-approximation 缓存——这正是 libjpeg 系列做不到 2.5MB 的根因 + * (渐进式必须整幅驻留 JBLOCK,≈1 字节/源像素/分量)。 + * - 输出直接是目标尺寸 RGBA,不再「缩解结果 + 精修输出」两份同时驻留。 + * + * 不支持 → 返回 0 让 meow_ui_img.c 回退 stb(不改变对外语义):4 分量 CMYK/YCCK、 + * 算术编码(SOF9/10)、无损(SOF3)、分层(SOF5-7,13-15)、12bit 精度。 + * + * 许可证:MeowOS 自写(MIT,随 SDK 发布);未引入第三方解码器源码。 + * 参考资料:ITU-T T.81 (JPEG) 语法与 Huffman 解码、libjpeg 的 marker/DQT/DHT 布局; + * 缩解基函数见文件内 mj_basis_* 的推导注释。 + */ +#include "meow_jpeg.h" + +#include +#include +#include +#include + +/* ================================================================== */ +/* 公共:目标尺寸(与 meow_img_downscale 同一公式,保证对外表现不变) */ +/* ================================================================== */ +void meow_img_target_dims(int w, int h, int maxdim, int *tw, int *th) +{ + if (w >= h) { + *tw = maxdim; + *th = (int)((long)h * maxdim / w); + } else { + *th = maxdim; + *tw = (int)((long)w * maxdim / h); + } + if (*tw < 1) *tw = 1; + if (*th < 1) *th = 1; +} + +/* ================================================================== */ +/* 字节源:文件流(4KB 环形读)或内存(零拷贝,直接用调用方缓冲) */ +/* ================================================================== */ +#define MJ_IOBUF 4096 + +typedef struct { + FILE *f; + const unsigned char *mem; + size_t memlen, mempos; + unsigned char buf[MJ_IOBUF]; + int buflen, bufpos; + unsigned int bitbuf; /* 熵流位缓冲(MSB 在前) */ + int bitcnt; + int over; /* 本熵段已到尽头(遇到 marker 或 EOF) */ + int marker; /* over 时已吃掉的 marker 码(0=无) */ +} mj_src; + +static int mj_raw(mj_src *s) +{ + if (s->mem) { + if (s->mempos >= s->memlen) return -1; + return s->mem[s->mempos++]; + } + if (s->bufpos >= s->buflen) { + s->buflen = (int)fread(s->buf, 1, MJ_IOBUF, s->f); + s->bufpos = 0; + if (s->buflen <= 0) { s->buflen = 0; return -1; } + } + return s->buf[s->bufpos++]; +} + +static int mj_u16(mj_src *s) +{ + int a = mj_raw(s), b = mj_raw(s); + return (a < 0 || b < 0) ? -1 : ((a << 8) | b); +} + +static int mj_read(mj_src *s, unsigned char *dst, size_t n) +{ + while (n--) { + int c = mj_raw(s); + if (c < 0) return 0; + *dst++ = (unsigned char)c; + } + return 1; +} + +static int mj_skip(mj_src *s, long n) +{ + while (n-- > 0) if (mj_raw(s) < 0) return 0; + return 1; +} + +/* 熵段内的字节:FF00 → FF;遇到真 marker(含 RSTn)→ 置 over 并记录,返回 0。 */ +static int mj_eb(mj_src *s) +{ + int c; + if (s->over) return 0; + c = mj_raw(s); + if (c < 0) { s->over = 1; return 0; } + if (c != 0xFF) return c; + for (;;) { + int d = mj_raw(s); + if (d < 0) { s->over = 1; return 0; } + if (d == 0x00) return 0xFF; + if (d == 0xFF) continue; /* 填充 FF */ + s->marker = d; s->over = 1; return 0; + } +} + +static void mj_fill(mj_src *s, int n) +{ + while (s->bitcnt < n) { + s->bitbuf = (s->bitbuf << 8) | (unsigned)mj_eb(s); + s->bitcnt += 8; + } +} + +static int mj_getbits(mj_src *s, int n) +{ + if (n <= 0) return 0; + if (s->bitcnt < n) mj_fill(s, n); + s->bitcnt -= n; + return (int)((s->bitbuf >> s->bitcnt) & ((1u << n) - 1u)); +} + +static int mj_extend(int v, int n) +{ + return v < (1 << (n - 1)) ? v - (1 << n) + 1 : v; +} + +/* ================================================================== */ +/* Huffman 表:9 位快表 + 慢路径(最长码 16 位) */ +/* ================================================================== */ +typedef struct { + unsigned char bits[17]; /* 各码长符号数 */ + unsigned char vals[256]; + unsigned short fast[512]; /* (len<<8)|sym,0 = 走慢路径 */ + int maxcode[17]; + int mincode[17]; + int valptr[17]; +} mj_huff; + +static void 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; + 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]); + for (j = 0; j < (1 << shift); j++) + h->fast[(code << shift) | j] = e; + } + code++; + k++; + } + h->maxcode[l] = h->bits[l] ? code - 1 : -1; + code <<= 1; + } +} + +static int mj_hdecode(mj_src *s, const mj_huff *h) +{ + if (s->bitcnt < 9) mj_fill(s, 9); + if (s->bitcnt >= 9) { + unsigned idx = (s->bitbuf >> (s->bitcnt - 9)) & 511u; + unsigned e = h->fast[idx]; + if (e) { + s->bitcnt -= (int)(e >> 8); + return (int)(e & 0xFFu); + } + } + { + int code = 0, l; + for (l = 1; l <= 16; l++) { + code = (code << 1) | mj_getbits(s, 1); + if (code <= h->maxcode[l]) + return h->vals[h->valptr[l] + code - h->mincode[l]]; + if (s->over) return 0; + } + } + s->over = 1; + return 0; +} + +/* zigzag 序号 → 自然序(行优先 v*8+u) */ +static const unsigned char mj_zigzag[64] = { + 0, 1, 8, 16, 9, 2, 3, 10, + 17, 24, 32, 25, 18, 11, 4, 5, + 12, 19, 26, 33, 40, 48, 41, 34, + 27, 20, 13, 6, 7, 14, 21, 28, + 35, 42, 49, 56, 57, 50, 43, 36, + 29, 22, 15, 23, 30, 37, 44, 51, + 58, 59, 52, 45, 38, 31, 39, 46, + 53, 60, 61, 54, 47, 55, 62, 63 +}; + +/* ================================================================== */ +/* 缩解基(Q12):B(u,x) = 「8 点 IDCT 在输出像素区间的盒式平均」 */ +/* B(u,y) = (N/8)·Σ_{x} w(y,x)·(1/2)·C(u)·cos((2x+1)uπ/16), C(0)=1/√2 */ +/* w(y,x) = 像素 x 的单位区间与输出区间 [y·8/N, (y+1)·8/N) 的重叠长度 */ +/* 于是:DC 单频重建恒为 F00/8(与 8×8 IDCT 的均值一致);N=8 时退化为 */ +/* 精确 IDCT;任意 N 之间的亮度/对比度一致(同一套离散平均口径)。 */ +/* 只列 N=1..7:N=8(≈不缩解)交回 stb 旧路径,保持「档位」对外表现。 */ +/* ================================================================== */ +static const short mj_basis_1[1] = { 1448 }; +static const short mj_basis_2[4] = { + 1448, 1448, + 1312, -1312 +}; +static const short mj_basis_3[9] = { + 1448, 1448, 1448, + 1676, 0, -1676, + 808, -1615, 808 +}; +static const short mj_basis_4[16] = { + 1448, 1448, 1448, 1448, + 1856, 769, -769, -1856, + 1338, -1338, -1338, 1338, + 652, -1573, 1573, -652 +}; +static const short mj_basis_5[25] = { + 1448, 1448, 1448, 1448, 1448, + 1894, 1187, 0, -1187, -1894, + 1476, -530, -1892, -530, 1476, + 914, -1498, 0, 1498, -914, + 362, -1086, 1448, -1086, 362 +}; +static const short mj_basis_6[36] = { + 1448, 1448, 1448, 1448, 1448, 1448, + 1932, 1420, 584, -584, -1420, -1932, + 1615, 0, -1615, -1615, 0, 1615, + 1177, -1204, -1356, 1356, 1204, -1177, + 724, -1448, 724, 724, -1448, 724, + 351, -805, 1377, -1377, 805, -351 +}; +static const short mj_basis_7[49] = { + 1448, 1448, 1448, 1448, 1448, 1448, 1448, + 1970, 1562, 861, 0, -861, -1562, -1970, + 1754, 392, -1199, -1892, -1199, 392, 1754, + 1440, -802, -1682, 0, 1682, 802, -1440, + 1086, -1448, -362, 1448, -362, -1448, 1086, + 745, -1407, 888, 0, -888, 1407, -745, + 449, -946, 889, -784, 889, -946, 449 +}; + +static const short *mj_basis(int n) +{ + switch (n) { + case 1: return mj_basis_1; + case 2: return mj_basis_2; + case 3: return mj_basis_3; + case 4: return mj_basis_4; + case 5: return mj_basis_5; + case 6: return mj_basis_6; + default: return mj_basis_7; + } +} + +/* ================================================================== */ +/* 分量与图像 */ +/* ================================================================== */ +typedef struct { + int id, h, v, tq; + int n; /* 缩解阶:每块 n×n 像素 */ + int pw, ph; /* 平面(含 MCU padding)尺寸 */ + int vw, vh; /* 有效像素尺寸(最近邻采样用) */ + int ow, oh; /* 本分量自己的块网格(非交织扫描用) */ + int td, ta; /* 当前扫描的 Huffman 表号 */ + int pred; /* DC 预测值(量化域) */ + unsigned char *plane; +} mj_comp; + +typedef struct { + int width, height; + int ncomp; + int hmax, vmax; + int mcus_x, mcus_y; + int progressive; + int rst; /* 重启间隔(MCU 数),0 = 无 */ + int adobe_transform; /* -1 未声明;0=RGB/CMYK;1=YCbCr */ + int blocks; /* 已解块数(判「有没有解出东西」) */ + mj_comp c[3]; + int qt[4][64]; + int have_qt[4]; + mj_huff dc[4], ac[4]; + int have_dc[4], have_ac[4]; + mj_src src; +} mj_img; + +#define MJ_PEAK_CAP (8u * 1024u * 1024u) /* 防御性上限:异常巨图不至于一次吃掉几十 MB */ + +static void mj_free(mj_img *img) +{ + int i; + for (i = 0; i < img->ncomp; i++) { + free(img->c[i].plane); + img->c[i].plane = NULL; + } +} + +/* 把一个(已反量化的)系数按 DCT 缩解累加进平面。k 为自然序下标。 */ +static void mj_accum(mj_comp *c, int bx, int by, int k, int32_t F) +{ + int u = k & 7, v = k >> 3, n = c->n, x, y; + const short *W; + unsigned char *base; + if (!F || u >= n || v >= n) return; + W = mj_basis(n); + base = c->plane + (size_t)(by * n) * c->pw + (size_t)(bx * n); + for (y = 0; y < n; y++) { + int32_t t = (F * W[v * n + y]) >> 12; + unsigned char *row = base + (size_t)y * c->pw; + for (x = 0; x < n; x++) { + int nv = (int)row[x] + (int)((t * W[u * n + x]) >> 12); + row[x] = (unsigned char)(nv < 0 ? 0 : (nv > 255 ? 255 : nv)); + } + } +} + +/* ------------------------------------------------------------------ */ +/* 表解析 */ +/* ------------------------------------------------------------------ */ +static int mj_dqt(mj_img *img, const unsigned char *d, int len) +{ + int p = 0; + while (p < 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; + } + img->have_qt[tq] = 1; + } + return 1; +} + +static int mj_dht(mj_img *img, const unsigned char *d, int len) +{ + int p = 0; + while (p < len) { + int tc, th, i, total = 0; + if (p + 17 > len) return 0; + tc = d[p] >> 4; th = d[p] & 15; p++; + if (th > 3 || tc > 1) return 0; + { + mj_huff *h = tc ? &img->ac[th] : &img->dc[th]; + for (i = 1; i <= 16; i++) { h->bits[i] = d[p + i - 1]; total += d[p + i - 1]; } + p += 16; + 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 (tc) img->have_ac[th] = 1; else img->have_dc[th] = 1; + } + } + return 1; +} + +/* ------------------------------------------------------------------ */ +/* SOF:尺寸/采样因子/分量的平面分配 */ +/* ------------------------------------------------------------------ */ +static int mj_ceil_div(int a, int b) { return (a + b - 1) / b; } + +/* 缩解阶 N:每 8×8 块只解出 N×N 像素(N/8 = 输出/源分辨率)。 + * 取「不欠采样」的最小 N:ceil(8·maxdim/longside)。N=8 表示几乎不缩解(≈原尺寸), + * 这种图上 stb 旧路径的峰值本来就受控,直接交回它,档位对外表现不变。 */ +static int mj_pick_n(int longside, int maxdim) +{ + int n = (8 * maxdim + longside - 1) / longside; + if (n < 1) n = 1; + if (n > 7) return 0; + return n; +} + +/* 按各分量当前的 n 计算块网格与平面尺寸,返回平面总字节。 */ +static unsigned mj_layout(mj_img *img) +{ + unsigned total = 0; + int i; + for (i = 0; i < img->ncomp; i++) { + mj_comp *c = &img->c[i]; + int sw = mj_ceil_div(img->width * c->h, img->hmax); + int sh = mj_ceil_div(img->height * c->v, img->vmax); + c->ow = mj_ceil_div(sw, 8); + c->oh = mj_ceil_div(sh, 8); + c->pw = (img->mcus_x * c->h > c->ow ? img->mcus_x * c->h : c->ow) * c->n; + c->ph = (img->mcus_y * c->v > c->oh ? img->mcus_y * c->v : c->oh) * c->n; + c->vw = mj_ceil_div(sw * c->n, 8); + c->vh = mj_ceil_div(sh * c->n, 8); + if (c->vw > c->pw) c->vw = c->pw; + if (c->vh > c->ph) c->vh = c->ph; + total += (unsigned)c->pw * (unsigned)c->ph; + } + return total; +} + +static int mj_sof(mj_img *img, int marker, const unsigned char *d, int len, int maxdim) +{ + int i, n, longside, base_n; + unsigned int plane_bytes, target_bytes; + int tw, th; + + if (marker != 0xC0 && marker != 0xC1 && marker != 0xC2) return 0; /* 算术/无损/差分 */ + if (len < 6) return 0; + if (d[0] != 8) return 0; /* 12bit 精度不支持 */ + img->height = (d[1] << 8) | d[2]; + img->width = (d[3] << 8) | d[4]; + n = d[5]; + if (img->width <= 0 || img->height <= 0 || n < 1 || n > 3) return 0; /* 4 分量 CMYK → stb */ + if (len < 6 + n * 3) return 0; + img->ncomp = n; + img->progressive = (marker == 0xC2); + + longside = img->width > img->height ? img->width : img->height; + if (longside <= maxdim) return 0; /* 小图走旧入口 */ + base_n = mj_pick_n(longside, maxdim); + if (!base_n) return 0; /* 几乎等尺寸:交回 stb 旧路径 */ + + img->hmax = img->vmax = 1; + for (i = 0; i < n; i++) { + const unsigned char *p = d + 6 + i * 3; + mj_comp *c = &img->c[i]; + c->id = p[0]; + c->h = p[1] >> 4; + c->v = p[1] & 15; + c->tq = p[2]; + if (c->h < 1 || c->v < 1 || c->h > 4 || c->v > 4 || c->tq > 3) return 0; + if (c->h > img->hmax) img->hmax = c->h; + if (c->v > img->vmax) img->vmax = c->v; + } + img->mcus_x = mj_ceil_div(img->width, 8 * img->hmax); + img->mcus_y = mj_ceil_div(img->height, 8 * img->vmax); + + meow_img_target_dims(img->width, img->height, maxdim, &tw, &th); + target_bytes = (unsigned)tw * (unsigned)th * 4u; + + for (i = 0; i < n; i++) img->c[i].n = base_n; + plane_bytes = mj_layout(img); + + /* 4:4:4 的色度平面与亮度等大(正常 4:2:0 只 1/4):亮度平面超过 400KB 时色度降一档, + * 视觉近无损,省下的正是色度要多占的两份。 */ + if (n == 3 && img->c[0].h == img->c[1].h && img->c[0].v == img->c[1].v && + plane_bytes / 3 > 400u * 1024u) { + for (i = 1; i < n; i++) if (img->c[i].n > 1) img->c[i].n >>= 1; + plane_bytes = mj_layout(img); + } + + /* 预算 2.4MB(平面 + 目标图):超了先降色度(亮度保持清晰),再整体降档。 + * 12MP 4:2:0 一档:1.13MB→0.94MB,输出仍是 480×720。 */ + if (plane_bytes + target_bytes > 2400u * 1024u) { + int ch = 0; + for (i = 1; i < n; i++) if (img->c[i].n > 1) { img->c[i].n >>= 1; ch = 1; } + if (ch) plane_bytes = mj_layout(img); + } + /* 最后一道防线:异常巨图整体降档(最多降到 N=1),不至于一次吃掉几十 MB。 */ + while (plane_bytes + target_bytes > MJ_PEAK_CAP && base_n > 1) { + base_n >>= 1; + for (i = 0; i < n; i++) if (img->c[i].n > base_n) img->c[i].n = base_n; + plane_bytes = mj_layout(img); + } + + for (i = 0; i < n; i++) { + mj_comp *c = &img->c[i]; + size_t bytes = (size_t)c->pw * (size_t)c->ph; + c->plane = (unsigned char *)malloc(bytes); + if (!c->plane) return 0; + memset(c->plane, 128, bytes); /* 中性灰底,系数累加即 IDCT */ + c->pred = 0; + } + return 1; +} + +/* ------------------------------------------------------------------ */ +/* 熵解码:块 */ +/* ------------------------------------------------------------------ */ +static int mj_block_seq(mj_img *img, mj_comp *c, int bx, int by) +{ + mj_src *s = &img->src; + int t = mj_hdecode(s, &img->dc[c->td]); + int k; + if (t > 16) return 0; + c->pred += t ? mj_extend(mj_getbits(s, t), t) : 0; + if (c->pred > (1 << 20) || c->pred < -(1 << 20)) c->pred = 0; + mj_accum(c, bx, by, 0, (int32_t)c->pred * img->qt[c->tq][0]); + for (k = 1; k < 64; ) { + int rs = mj_hdecode(s, &img->ac[c->ta]); + int r = rs >> 4, sz = rs & 15; + if (sz == 0) { + if (r < 15) break; /* EOB */ + k += 16; /* ZRL */ + continue; + } + k += r; + if (k > 63) break; + { + int v = mj_extend(mj_getbits(s, sz), sz); + int nk = mj_zigzag[k]; + if (v > 1023) v = 1023; + if (v < -1023) v = -1023; + mj_accum(c, bx, by, nk, (int32_t)v * img->qt[c->tq][nk]); + k++; + } + } + img->blocks++; + return 1; +} + +static int mj_block_dc_prog(mj_img *img, mj_comp *c, int bx, int by, int al) +{ + mj_src *s = &img->src; + int t = mj_hdecode(s, &img->dc[c->td]); + if (t > 16) return 0; + c->pred += t ? (mj_extend(mj_getbits(s, t), t) << al) : 0; + if (c->pred > (1 << 20) || c->pred < -(1 << 20)) c->pred = 0; + mj_accum(c, bx, by, 0, (int32_t)c->pred * img->qt[c->tq][0]); + img->blocks++; + return 1; +} + +/* 跳过一段熵编码数据(不解码),停在下一个非 RST 的 marker 上。 + * 用途:渐进式的精细化扫描(Ah>0)对缩略图无价值,但编码器可能把频带扫描与精细化交错, + * 所以只跳过、不中止——后续的 Ah=0 扫描仍会解码。 */ +static void mj_skip_entropy(mj_img *img) +{ + mj_src *s = &img->src; + s->bitcnt = 0; + s->bitbuf = 0; + for (;;) { + while (!s->over) (void)mj_eb(s); + if (s->over && s->marker >= 0xD0 && s->marker <= 0xD7) { + s->over = 0; /* RSTn:重同步后继续扫 */ + s->marker = 0; + continue; + } + break; + } +} + +/* 重启标记:对齐位缓冲并吃掉 RSTn,重置 DC 预测。 */ +static void mj_restart(mj_img *img) +{ + mj_src *s = &img->src; + int i; + s->bitcnt = 0; + s->bitbuf = 0; + if (s->over) { + if (s->marker >= 0xD0 && s->marker <= 0xD7) { s->over = 0; s->marker = 0; } + } else { + int c; + for (;;) { + 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 (i = 0; i < img->ncomp; i++) img->c[i].pred = 0; +} + +typedef struct { + int ns; + int ci[4]; + int td[4], ta[4]; + int ss, se, ah, al; +} mj_scan; + +/* 顺序(baseline / SOF1)扫描 */ +static void mj_scan_seq(mj_img *img, const mj_scan *sc) +{ + int single = (sc->ns == 1); + int mcus_x = single ? img->c[sc->ci[0]].ow : img->mcus_x; + int mcus_y = single ? img->c[sc->ci[0]].oh : img->mcus_y; + int mx, my, total = 0; + 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 (single) { + mj_comp *c = &img->c[sc->ci[0]]; + c->td = sc->td[0]; c->ta = sc->ta[0]; + mj_block_seq(img, c, mx, my); + } else { + for (j = 0; j < sc->ns; j++) { + mj_comp *c = &img->c[sc->ci[j]]; + int by, bx; + c->td = sc->td[j]; c->ta = sc->ta[j]; + for (by = 0; by < c->v; by++) + for (bx = 0; bx < c->h; bx++) + mj_block_seq(img, c, mx * c->h + bx, my * c->v + by); + } + } + total++; + } + } +} + +/* 渐进式 DC 扫描(Ah=0) */ +static void mj_scan_dc_prog(mj_img *img, const mj_scan *sc) +{ + int single = (sc->ns == 1); + int mcus_x = single ? img->c[sc->ci[0]].ow : img->mcus_x; + int mcus_y = single ? img->c[sc->ci[0]].oh : img->mcus_y; + int mx, my, total = 0; + 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 (single) { + mj_comp *c = &img->c[sc->ci[0]]; + c->td = sc->td[0]; + mj_block_dc_prog(img, c, mx, my, sc->al); + } else { + for (j = 0; j < sc->ns; j++) { + mj_comp *c = &img->c[sc->ci[j]]; + int by, bx; + c->td = sc->td[j]; + for (by = 0; by < c->v; by++) + for (bx = 0; bx < c->h; bx++) + mj_block_dc_prog(img, c, mx * c->h + bx, my * c->v + by, sc->al); + } + } + total++; + } + } +} + +/* 渐进式 AC 扫描(Ah=0,单分量) */ +static void mj_scan_ac_prog(mj_img *img, const mj_scan *sc) +{ + mj_comp *c = &img->c[sc->ci[0]]; + mj_src *s = &img->src; + int eobrun = 0, bx, by, total = 0; + 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; + if (eobrun > 0) { + eobrun--; + } else { + int k = sc->ss; + while (k <= sc->se) { + int rs = mj_hdecode(s, &img->ac[c->ta]); + int r = rs >> 4, sz = rs & 15; + if (sz == 0) { + if (r < 15) { + eobrun = (1 << r) - 1; + if (r) eobrun += mj_getbits(s, r); + break; + } + k += 16; + continue; + } + k += r; + if (k > sc->se) break; + { + int v = mj_extend(mj_getbits(s, sz), sz); + int nk = mj_zigzag[k]; + int32_t sv; + if (v > 1023) v = 1023; + if (v < -1023) v = -1023; + sv = (int32_t)v * img->qt[c->tq][nk]; + if (sc->al > 6) sv <<= 6; else sv <<= sc->al; /* Al 上限防御,防 int32 溢出 */ + mj_accum(c, bx, by, nk, sv); + } + k++; + } + } + total++; + img->blocks++; + if (s->over) return; + } + } +} + +/* ------------------------------------------------------------------ */ +/* 输出:分量平面 → 目标尺寸 RGBA(最近邻,与 meow_img_downscale 同口径)*/ +/* ------------------------------------------------------------------ */ +#define MJ_FIX_R 5743 /* 1.402 * 4096 */ +#define MJ_FIX_G 1410 /* 0.34414 * 4096 */ +#define MJ_FIX_G2 2925 /* 0.71414 * 4096 */ +#define MJ_FIX_B 7258 /* 1.772 * 4096 */ + +static unsigned char *mj_output(mj_img *img, int tw, int th, int rgb_mode) +{ + unsigned char *out = (unsigned char *)malloc((size_t)tw * th * 4); + mj_comp *cy = &img->c[0]; + mj_comp *cb = img->ncomp >= 3 ? &img->c[1] : NULL; + mj_comp *cr = img->ncomp >= 3 ? &img->c[2] : NULL; + const unsigned char *py = cy->plane; + const unsigned char *pb = cb ? cb->plane : NULL; + const unsigned char *pc = cr ? cr->plane : NULL; + int x, y; + if (!out) return NULL; + for (y = 0; y < th; y++) { + int sy = (int)((long)y * cy->vh / th); + const unsigned char *ry = py + (size_t)sy * cy->pw; + const unsigned char *rb = pb ? pb + (size_t)((long)y * cb->vh / th) * cb->pw : NULL; + const unsigned char *rc = pc ? pc + (size_t)((long)y * cr->vh / th) * cr->pw : NULL; + unsigned char *dst = out + (size_t)y * tw * 4; + for (x = 0; x < tw; x++) { + int yv = ry[(int)((long)x * cy->vw / tw)]; + unsigned char *o = dst + (size_t)x * 4; + if (!rb) { + o[0] = o[1] = o[2] = (unsigned char)yv; + o[3] = 255; + } else if (rgb_mode) { + o[0] = (unsigned char)yv; + o[1] = rb[(int)((long)x * cb->vw / tw)]; + o[2] = rc[(int)((long)x * cr->vw / tw)]; + o[3] = 255; + } else { + int u = (int)rb[(int)((long)x * cb->vw / tw)] - 128; + int v = (int)rc[(int)((long)x * cr->vw / tw)] - 128; + int r = yv + ((MJ_FIX_R * v) >> 12); + int g = yv - ((MJ_FIX_G * u + MJ_FIX_G2 * v) >> 12); + int b = yv + ((MJ_FIX_B * u) >> 12); + o[0] = (unsigned char)(r < 0 ? 0 : (r > 255 ? 255 : r)); + o[1] = (unsigned char)(g < 0 ? 0 : (g > 255 ? 255 : g)); + o[2] = (unsigned char)(b < 0 ? 0 : (b > 255 ? 255 : b)); + o[3] = 255; + } + } + } + return out; +} + +/* ------------------------------------------------------------------ */ +/* 主解析循环 */ +/* ------------------------------------------------------------------ */ +static int mj_decode(mj_img *img, int maxdim, int *tw, int *th, unsigned char **rgba) +{ + mj_src *s = &img->src; + int have_sof = 0, rgb_mode = 0; + int pending = -1; /* 熵段结束时被位读取器吃掉的 marker(回填给主循环) */ + + if (mj_u16(s) != 0xFFD8) return 0; /* SOI */ + for (;;) { + int code, len; + if (pending >= 0) { + code = pending; + pending = -1; + } else { + /* 找下一个 marker(跳过 FF 填充字节) */ + do { code = mj_raw(s); if (code < 0) return 0; } while (code != 0xFF); + do { code = mj_raw(s); if (code < 0) return 0; } while (code == 0xFF); + } + if (code == 0xD9) break; /* EOI */ + if (code == 0xD8 || code == 0x01 || (code >= 0xD0 && code <= 0xD7)) continue; + len = mj_u16(s); + if (len < 2) return 0; + len -= 2; + if (code == 0xDA) { + int ns, i; + mj_scan sc; + unsigned char hdr[64]; + if (len > (int)sizeof hdr) return 0; + if (!mj_read(s, hdr, (size_t)len)) return 0; + if (len < 1) return 0; + ns = hdr[0]; + if (ns < 1 || ns > 3 || len < 1 + ns * 2 + 3) return 0; + memset(&sc, 0, sizeof sc); + sc.ns = ns; + for (i = 0; i < ns; i++) { + int cid = hdr[1 + i * 2], tt = hdr[2 + i * 2], j, found = -1; + for (j = 0; j < img->ncomp; j++) + if (img->c[j].id == cid) { found = j; break; } + if (found < 0) return 0; + sc.ci[i] = found; + sc.td[i] = tt >> 4; + sc.ta[i] = tt & 15; + if (sc.td[i] > 3 || sc.ta[i] > 3) return 0; + } + sc.ss = hdr[1 + ns * 2]; + sc.se = hdr[2 + ns * 2]; + sc.ah = hdr[3 + ns * 2] >> 4; + sc.al = hdr[3 + ns * 2] & 15; + if (!have_sof) return 0; + if (sc.ss < 0 || sc.se > 63 || sc.ss > sc.se || sc.al > 13) return 0; + for (i = 0; i < ns; i++) { + mj_comp *c = &img->c[sc.ci[i]]; + if (!img->have_qt[c->tq]) return 0; + if (sc.ss == 0 && !img->have_dc[sc.td[i]]) return 0; + if (sc.se > 0 && !img->have_ac[sc.ta[i]]) return 0; + } + if (img->progressive && sc.ah > 0) { + /* 精细化扫描:跳过(不解码),继续找后续 Ah=0 扫描。低频带首次扫描到齐 + * 就够出一张清晰缩略图;整幅系数的 successive-approximation 缓存正是 + * 渐进式峰值失控的根因,这里根本不留。 */ + mj_skip_entropy(img); + s->bitcnt = 0; + s->bitbuf = 0; + if (s->over) { + if (s->marker) pending = s->marker; + s->marker = 0; + s->over = 0; + } + continue; + } + if (!img->progressive) { + mj_scan_seq(img, &sc); + } else if (sc.ss == 0) { + mj_scan_dc_prog(img, &sc); + } else { + if (sc.ns != 1) return 0; + mj_scan_ac_prog(img, &sc); + } + s->bitcnt = 0; + s->bitbuf = 0; + /* 一个熵段结束:位读取器可能已把下一个 marker 吃掉,回填给主循环, + * 否则紧跟其后的 DHT/DQT/SOS 会被漏掉。 */ + if (s->over) { + if (s->marker) pending = s->marker; + s->marker = 0; + s->over = 0; + } + continue; + } + if (len > 0 && code != 0xDA) { + /* APPn/COM 只跳过(APP14 取 Adobe transform,很短);其它段读进 2KB 栈缓冲。 + * 不在 marker 循环里 malloc:EXIF 缩略图/ICC 这类段可能有几百 KB,会把解码峰值抬上去。 */ + if (code >= 0xE0 && code <= 0xEF) { + if (code == 0xEE && len >= 12 && len <= 64) { + unsigned char ad[64]; + if (!mj_read(s, ad, (size_t)len)) return 0; + if (memcmp(ad, "Adobe", 5) == 0) img->adobe_transform = ad[11]; + } else if (!mj_skip(s, len)) { + return 0; + } + continue; + } + if (code == 0xFE) { /* COM */ + if (!mj_skip(s, len)) return 0; + continue; + } + { + unsigned char pbuf[2048]; + if (len > (int)sizeof pbuf) return 0; /* DQT/DHT/SOF 不可能这么长 */ + if (!mj_read(s, pbuf, (size_t)len)) return 0; + switch (code) { + case 0xC0: case 0xC1: case 0xC2: + if (have_sof) return 0; + if (!mj_sof(img, code, pbuf, len, maxdim)) return 0; + have_sof = 1; + break; + case 0xC4: + if (!mj_dht(img, pbuf, len)) return 0; + break; + case 0xDB: + if (!mj_dqt(img, pbuf, len)) return 0; + break; + case 0xDD: + if (len >= 2) img->rst = (pbuf[0] << 8) | pbuf[1]; + break; + default: + break; + } + } + continue; + } + if (len > 0) { + if (!mj_skip(s, len)) return 0; + } + } + + if (!have_sof || img->blocks <= 0) return 0; + + /* 色彩空间:3 分量默认 YCbCr;Adobe transform=0 或分量 ID 为 'R','G','B' 时按 RGB。 */ + if (img->ncomp == 3) { + if (img->adobe_transform == 0) rgb_mode = 1; + if (img->c[0].id == 'R' && img->c[1].id == 'G' && img->c[2].id == 'B') rgb_mode = 1; + } + meow_img_target_dims(img->width, img->height, maxdim, tw, th); + *rgba = mj_output(img, *tw, *th, rgb_mode); + return *rgba ? 1 : 0; +} + +/* ================================================================== */ +/* 后端分派 */ +/* ================================================================== */ +static int mj_is_jpeg(const unsigned char *p, size_t n) +{ + return n >= 3 && p[0] == 0xFF && p[1] == 0xD8 && p[2] == 0xFF; +} + +int meow_img_backend_mem(const void *buf, size_t len, int maxdim, int *tw, int *th, unsigned char **rgba) +{ + const unsigned char *p = (const unsigned char *)buf; + if (!buf || len < 8 || maxdim <= 0) return 0; + if (mj_is_jpeg(p, len)) { + mj_img img; + int ok; + memset(&img, 0, sizeof img); + img.adobe_transform = -1; + img.src.mem = p; + img.src.memlen = len; + ok = mj_decode(&img, maxdim, tw, th, rgba); + mj_free(&img); + return ok; + } + if (p[0] == 0x89 && p[1] == 'P' && p[2] == 'N' && p[3] == 'G') + return meow_png_decode_stream(NULL, p, len, maxdim, tw, th, rgba); + if (p[0] == 'B' && p[1] == 'M') + return meow_bmp_decode_stream(NULL, p, len, maxdim, tw, th, rgba); + return 0; +} + +int meow_img_backend_file(const char *path, int maxdim, int *tw, int *th, unsigned char **rgba) +{ + FILE *f; + unsigned char sig[8]; + size_t n; + int ok = 0; + if (!path || maxdim <= 0) return 0; + f = fopen(path, "rb"); + if (!f) return 0; + n = fread(sig, 1, sizeof sig, f); + if (n < 4 || fseek(f, 0, SEEK_SET) != 0) { fclose(f); return 0; } + if (mj_is_jpeg(sig, n)) { + mj_img img; + memset(&img, 0, sizeof img); + img.adobe_transform = -1; + img.src.f = f; + ok = mj_decode(&img, maxdim, tw, th, rgba); + mj_free(&img); + } else if (sig[0] == 0x89 && sig[1] == 'P' && sig[2] == 'N' && sig[3] == 'G') { + ok = meow_png_decode_stream(f, NULL, 0, maxdim, tw, th, rgba); + } else if (sig[0] == 'B' && sig[1] == 'M') { + ok = meow_bmp_decode_stream(f, NULL, 0, maxdim, tw, th, rgba); + } + fclose(f); + return ok; +} diff --git a/sdk/meow_jpeg.h b/sdk/meow_jpeg.h new file mode 100644 index 0000000..11364d8 --- /dev/null +++ b/sdk/meow_jpeg.h @@ -0,0 +1,48 @@ +/* + * meow_jpeg.h — M23 真缩解后端内部接口(**不对外导出**,不在 meow_ui.h 伞头里)。 + * + * 后端直接产出「目标显示尺寸」的 RGBA,不做「先整幅解、再重采样」: + * - JPEG(meow_jpeg.c):DCT 域缩解 1/8、1/4、1/2(N=1/2/4 点 IDCT),渐进式只解到 + * 首个精细化扫描(首个 Ah>0)为止——低频带此时已完整,再往后只是为了更细的量化精度。 + * - PNG(meow_png_stream.c):libz inflate 逐行 defilter,按步长丢行丢列直写目标尺寸。 + * - BMP(meow_png_stream.c):按行 seek + 行内跳列(同一套「只碰需要的行」思路)。 + * 三者峰值都只与「输出尺寸 + 目标图像」相关,不随源图分辨率线性增长。 + * + * 不支持的特性一律返回 0,由 meow_ui_img.c 回退 stb(行为与 M23 之前完全一致): + * 4 分量 CMYK/YCCK、算术编码/无损/分层 JPEG、Adam7 交错 PNG、1/2/4bpp 与 RLE BMP 等。 + * + * 许可证:本文件与 meow_jpeg.c / meow_png_stream.c 为 MeowOS 自写代码(MIT,随 SDK 发布)。 + * 不引入第三方解码器源码——理由见 M23 报告「为什么不用 IJG libjpeg」:libjpeg 系列对 + * 渐进式 JPEG 必须整幅驻留系数阵列(≈1 字节/源像素/分量),正是审计里 +23.5MB 的来源, + * 换成它也无法把渐进式峰值压到 2.5MB 以内。 + */ +#ifndef MEOW_JPEG_H +#define MEOW_JPEG_H + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* 解码 path / 内存里的图,直接产出「长边 ≤ maxdim 且按 meow_img_downscale 同一公式等比缩放」 + * 的 tw×th RGBA(4 通道 straight alpha,malloc 分配,调用方 free)。 + * 返回 1 成功(tw/th/rgba 已填);0 = 不支持/失败/不需要(调用方回退 stb 或旧入口)。 */ +int meow_img_backend_file(const char *path, int maxdim, int *tw, int *th, unsigned char **rgba); +int meow_img_backend_mem(const void *buf, size_t len, int maxdim, int *tw, int *th, unsigned char **rgba); + +/* 目标尺寸:与 meow_img_downscale 同一公式(长边 = maxdim,短边按比例截断,下限 1)。 */ +void meow_img_target_dims(int w, int h, int maxdim, int *tw, int *th); + +/* meow_png_stream.c 提供(PNG 逐行 inflate / BMP 按行跳采样)。f 与 mem 二选一: + * f != NULL 走文件流(只读需要的行/块),否则从 mem[0..len) 读。 */ +int meow_png_decode_stream(FILE *f, const unsigned char *mem, size_t len, + int maxdim, int *tw, int *th, unsigned char **rgba); +int meow_bmp_decode_stream(FILE *f, const unsigned char *mem, size_t len, + int maxdim, int *tw, int *th, unsigned char **rgba); + +#ifdef __cplusplus +} +#endif +#endif /* MEOW_JPEG_H */ diff --git a/sdk/meow_png_stream.c b/sdk/meow_png_stream.c new file mode 100644 index 0000000..b505191 --- /dev/null +++ b/sdk/meow_png_stream.c @@ -0,0 +1,492 @@ +/* + * meow_png_stream.c — M23 真缩解后端(二):PNG 逐行 inflate + BMP 按行跳采样。 + * + * 为什么不用 stb 的 PNG 路径:stb 先整幅 inflate + 反滤波成 raw,再展开成 RGBA 整幅, + * 最后才降采样——1600×2400 PNG 实测 +26.2MB。这里改成「按行流式」: + * - IDAT 边到边 inflate,一次只持有一行(filter 字节 + rowbytes)与上一行; + * - 反滤波后**只把需要的列**写进目标行(最近邻,与 meow_img_downscale 同口径); + * - 不需要的行整行丢弃(但仍要反滤波,filter 依赖上一行); + * - 峰值 ≈ 两行缓冲 + zlib 窗口(32KB) + 目标 RGBA,与源图分辨率无关。 + * 支持:gray / gray+A / RGB / RGBA / 调色板,bit depth 1/2/4/8/16(16bit 取高字节降 8bit), + * tRNS 透明。不支持 → 返回 0 回退 stb:Adam7 交错、非 0 压缩/滤波方法、异常色型组合。 + * + * BMP:文件流按目标行直接 seek 到源行(不读全图),行内跳列采样。支持 8bpp 调色板 / + * 16bpp RGB555 / 24bpp / 32bpp(BI_RGB);RLE、BITFIELDS、1/2/4bpp → 回退 stb。 + * + * 许可证:MeowOS 自写(MIT,随 SDK 发布)。PNG/zlib 语义参考 RFC 2083 / zlib 手册; + * BMP 布局参考 stb_image 的解析口径(同为本仓已有依赖,行为对齐)。 + */ +#include "meow_jpeg.h" + +#include +#include +#include +#include + +/* ---------------- zlib ABI ---------------- */ +#if defined(__has_include) +# if __has_include() +# include +# define MZP_HAVE_H 1 +# endif +#endif +#ifndef MZP_HAVE_H +/* 设备 musl sysroot 没有 zlib.h(meow_font.c 同此口径):按 libz.a 的 ABI 声明最小 API。 + * 指针/unsigned long 交给编译器定尺寸,宿主 64 位与设备 32 位都成立。 */ +typedef unsigned char mzp_Bytef; +typedef unsigned int mzp_uInt; +typedef unsigned long mzp_uLong; +typedef void *mzp_voidpf; +typedef struct mzp_stream_s { + mzp_Bytef *next_in; mzp_uInt avail_in; mzp_uLong total_in; + mzp_Bytef *next_out; mzp_uInt avail_out; mzp_uLong total_out; + char *msg; + void *state; + void *zalloc; + void *zfree; + mzp_voidpf opaque; + int data_type; + mzp_uLong adler; + mzp_uLong reserved; +} mzp_stream; +extern int inflateInit_(mzp_stream *, const char *, int); +extern int inflate(mzp_stream *, int); +extern int inflateEnd(mzp_stream *); +extern const char *zlibVersion(void); +# define z_stream mzp_stream +# define Z_OK 0 +# define Z_STREAM_END 1 +# define Z_NO_FLUSH 0 +#endif + +/* ---------------- 字节源 ---------------- */ +typedef struct { + FILE *f; + const unsigned char *mem; + size_t memlen, mempos; +} png_src; + +static int ps_read(png_src *s, void *dst, size_t n) +{ + if (s->mem) { + if (s->mempos + n > s->memlen) return 0; + memcpy(dst, s->mem + s->mempos, n); + s->mempos += n; + return 1; + } + return fread(dst, 1, n, s->f) == n; +} + +static int ps_skip(png_src *s, size_t n) +{ + if (s->mem) { + if (s->mempos + n > s->memlen) return 0; + s->mempos += n; + return 1; + } + return fseek(s->f, (long)n, SEEK_CUR) == 0; +} + +/* ================================================================== */ +/* PNG */ +/* ================================================================== */ +typedef struct { + png_src src; + z_stream z; + int z_ready, z_end; + unsigned w, h; + int depth, color; + int bits; /* 每像素位数 */ + size_t rowbytes; + unsigned char *line; /* 1 filter 字节 + rowbytes */ + unsigned char *prev; /* 上一行的像素字节 */ + unsigned char *cur; /* 本行的像素字节 */ + size_t need, got; + unsigned char pal[256][3]; + unsigned char trns_a[256]; /* 调色板透明度 */ + int pal_n; + int have_trns; + unsigned trns[3]; /* gray / RGB 的 tRNS 值(已降到 8bit) */ + unsigned char *out; + int tw, th; + int next_ty; + unsigned y; +} png_ctx; + +/* 从行里取 px 列的 8bit 通道;返回通道数(写入 o[0..3] 的 RGB(A) 语义由调用方定)。 */ +static void png_get_px(const png_ctx *c, const unsigned char *row, int x, unsigned char *o) +{ + int d = c->depth; + if (d < 8) { + int per = 8 / d; /* 每字节像素数 */ + int byte = x / per; + int shift = 8 - d * (x % per + 1); + int v = (row[byte] >> shift) & ((1 << d) - 1); + if (c->color == 3) { /* 调色板:子字节值即索引 */ + o[0] = c->pal[v][0]; o[1] = c->pal[v][1]; o[2] = c->pal[v][2]; + o[3] = c->trns_a[v]; + } else { + unsigned char g = (unsigned char)(v * 255 / ((1 << d) - 1)); + o[0] = o[1] = o[2] = g; + o[3] = 255; + } + return; + } + if (d == 8) { + if (c->color == 0) { /* gray */ + o[0] = o[1] = o[2] = row[x]; + o[3] = 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; + } 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]; + o[3] = c->trns_a[i]; + } else if (c->color == 4) { /* gray + A */ + o[0] = o[1] = o[2] = row[x * 2]; + o[3] = row[x * 2 + 1]; + } else { /* RGBA */ + o[0] = row[x * 4]; o[1] = row[x * 4 + 1]; + o[2] = row[x * 4 + 2]; o[3] = row[x * 4 + 3]; + } + return; + } + /* 16bit:取高字节 */ + if (c->color == 0) { + o[0] = o[1] = o[2] = row[x * 2]; + o[3] = (c->have_trns && row[x * 2] == c->trns[0]) ? 0 : 255; + } else if (c->color == 2) { + o[0] = row[x * 6]; o[1] = row[x * 6 + 2]; o[2] = row[x * 6 + 4]; + o[3] = (c->have_trns && row[x * 6] == c->trns[0] && + row[x * 6 + 2] == c->trns[1] && row[x * 6 + 4] == c->trns[2]) ? 0 : 255; + } else if (c->color == 4) { + o[0] = o[1] = o[2] = row[x * 4]; + o[3] = row[x * 4 + 2]; + } else { + o[0] = row[x * 8]; o[1] = row[x * 8 + 2]; + o[2] = row[x * 8 + 4]; o[3] = row[x * 8 + 6]; + } +} + +/* 一行反滤波完成后:按需写目标行。 */ +static int png_emit_row(png_ctx *c) +{ + unsigned char ft = c->line[0]; + const unsigned char *src = c->line + 1; + unsigned char *dst = c->cur; + size_t x, bpp = (size_t)((c->bits + 7) / 8); + if (bpp < 1) bpp = 1; + if (ft > 4) return 0; + for (x = 0; x < c->rowbytes; x++) { + unsigned char left = (x >= bpp) ? dst[x - bpp] : 0; + unsigned char up = c->prev ? c->prev[x] : 0; + unsigned char ul = (c->prev && x >= bpp) ? c->prev[x - bpp] : 0; + unsigned char v = src[x]; + switch (ft) { + case 0: dst[x] = v; break; + case 1: dst[x] = (unsigned char)(v + left); break; + case 2: dst[x] = (unsigned char)(v + up); break; + case 3: dst[x] = (unsigned char)(v + (((unsigned)left + (unsigned)up) >> 1)); break; + default: { + int p = (int)left + (int)up - (int)ul; + int pa = p - (int)left; if (pa < 0) pa = -pa; + int pb = p - (int)up; if (pb < 0) pb = -pb; + int pc = p - (int)ul; if (pc < 0) pc = -pc; + unsigned char pred = (pa <= pb && pa <= pc) ? left : (pb <= pc ? up : ul); + dst[x] = (unsigned char)(v + pred); + break; + } + } + } + if (c->next_ty < c->th) { + long sy = (long)c->next_ty * (long)c->h / c->th; + if (sy == (long)c->y) { + unsigned char *o = c->out + (size_t)c->next_ty * c->tw * 4; + int tx; + for (tx = 0; tx < c->tw; tx++) { + int sx = (int)((long)tx * (long)c->w / c->tw); + unsigned char px[4]; + png_get_px(c, dst, sx, px); + o[tx * 4] = px[0]; o[tx * 4 + 1] = px[1]; + o[tx * 4 + 2] = px[2]; o[tx * 4 + 3] = px[3]; + } + c->next_ty++; + } + } + /* 交换 prev/cur(prev 直接成为反滤波基准) */ + { + unsigned char *t = c->prev; + c->prev = c->cur; + c->cur = t; + } + c->y++; + return 1; +} + +#define PNG_NEED_MORE 2 + +static int png_pump(png_ctx *c) +{ + while (c->got < c->need) { + int ret; + c->z.next_out = c->line + c->got; + c->z.avail_out = (unsigned)(c->need - c->got); + ret = inflate(&c->z, Z_NO_FLUSH); + c->got = c->need - c->z.avail_out; + if (ret == Z_STREAM_END) c->z_end = 1; + else if (ret != Z_OK) return 0; + if (c->got < c->need && (c->z.avail_in == 0 || c->z_end)) + return c->z_end ? 0 : PNG_NEED_MORE; + } + if (!png_emit_row(c)) return 0; + c->got = 0; + return 1; +} + +static int png_bits_of(int color, int depth) +{ + switch (color) { + case 0: return depth; /* gray */ + case 2: return depth * 3; /* rgb */ + case 3: return depth; /* palette */ + case 4: return depth * 2; /* gray + a */ + case 6: return depth * 4; /* rgba */ + default: return 0; + } +} + +static int png_depth_ok(int color, int depth) +{ + if (color == 0) return depth == 1 || depth == 2 || depth == 4 || depth == 8 || depth == 16; + if (color == 3) return depth == 1 || depth == 2 || depth == 4 || depth == 8; + if (color == 2 || color == 4 || color == 6) return depth == 8 || depth == 16; + return 0; +} + +int meow_png_decode_stream(FILE *f, const unsigned char *mem, size_t len, + int maxdim, int *tw, int *th, unsigned char **rgba) +{ + png_ctx c; + unsigned char sig[8], hdr[8]; + unsigned char ibuf[4096]; + int have_ihdr = 0, i, ok = 0; + + if (maxdim <= 0) return 0; + memset(&c, 0, sizeof c); + c.src.f = f; + c.src.mem = mem; + c.src.memlen = len; + if (!ps_read(&c.src, sig, 8)) return 0; + if (memcmp(sig, "\x89PNG\r\n\x1a\n", 8) != 0) return 0; + + while (ps_read(&c.src, hdr, 8)) { + unsigned clen = ((unsigned)hdr[0] << 24) | ((unsigned)hdr[1] << 16) | + ((unsigned)hdr[2] << 8) | hdr[3]; + const unsigned char *type = hdr + 4; + if (clen > 0x7FFFFFFFu) return 0; + + if (memcmp(type, "IHDR", 4) == 0) { + unsigned char d[13]; + if (clen < 13 || have_ihdr) return 0; + if (!ps_read(&c.src, d, 13)) return 0; + c.w = ((unsigned)d[0] << 24) | ((unsigned)d[1] << 16) | ((unsigned)d[2] << 8) | d[3]; + c.h = ((unsigned)d[4] << 24) | ((unsigned)d[5] << 16) | ((unsigned)d[6] << 8) | d[7]; + c.depth = d[8]; + c.color = d[9]; + if (d[10] != 0 || d[11] != 0 || d[12] != 0) return 0; /* 只支持非交错 + 标准压缩/滤波 */ + if (c.w == 0 || c.h == 0 || c.w > 65535u || c.h > 65535u) return 0; + if (!png_depth_ok(c.color, c.depth)) return 0; + c.bits = png_bits_of(c.color, c.depth); + c.rowbytes = ((size_t)c.w * c.bits + 7) / 8; + if (c.rowbytes == 0) return 0; + /* 小图不动:与 meow_img_load_file_dim 的「旧入口」判定一致 */ + if ((long)(c.w > c.h ? c.w : c.h) <= maxdim) return 0; + if (clen > 13 && !ps_skip(&c.src, clen - 13)) return 0; + have_ihdr = 1; + c.need = c.rowbytes + 1; + c.line = (unsigned char *)malloc(c.need); + c.prev = (unsigned char *)malloc(c.rowbytes); + c.cur = (unsigned char *)malloc(c.rowbytes); + if (!c.line || !c.prev || !c.cur) goto done; + memset(c.prev, 0, c.rowbytes); + memset(c.cur, 0, c.rowbytes); + for (i = 0; i < 256; i++) c.trns_a[i] = 255; + meow_img_target_dims((int)c.w, (int)c.h, maxdim, &c.tw, &c.th); + c.out = (unsigned char *)malloc((size_t)c.tw * c.th * 4); + if (!c.out) goto done; + memset(c.out, 0, (size_t)c.tw * c.th * 4); + memset(&c.z, 0, sizeof c.z); + if (inflateInit_(&c.z, zlibVersion(), (int)sizeof c.z) != Z_OK) goto done; + c.z_ready = 1; + } else if (memcmp(type, "PLTE", 4) == 0) { + unsigned char d[256 * 3]; + if (clen > sizeof d || clen % 3) return 0; + if (!ps_read(&c.src, d, clen)) return 0; + c.pal_n = (int)(clen / 3); + memcpy(c.pal, d, clen); + } else if (memcmp(type, "tRNS", 4) == 0) { + unsigned char d[256]; + if (clen > sizeof d) return 0; + if (!ps_read(&c.src, d, clen)) return 0; + c.have_trns = 1; + 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) */ + } else if (c.color == 2 && clen >= 6) { + c.trns[0] = d[0]; c.trns[1] = d[2]; c.trns[2] = d[4]; + } + } else if (memcmp(type, "IDAT", 4) == 0) { + unsigned remain = clen; + if (!have_ihdr || !c.out) return 0; + while (remain) { + unsigned n = remain > sizeof ibuf ? (unsigned)sizeof ibuf : remain; + if (!ps_read(&c.src, ibuf, n)) goto done; + remain -= n; + c.z.next_in = ibuf; + c.z.avail_in = n; + while (c.z.avail_in && c.next_ty < c.th) { + int r = png_pump(&c); + if (r == 0) goto done; + if (r == PNG_NEED_MORE) break; + } + } + } else if (memcmp(type, "IEND", 4) == 0) { + break; + } else { + if (clen && !ps_skip(&c.src, clen)) return 0; + } + /* 跳过 CRC */ + { + unsigned char crc[4]; + if (!ps_read(&c.src, crc, 4)) break; + } + if (c.out && c.next_ty >= c.th) break; /* 目标行都拿到了,不必读完 */ + } + + if (!have_ihdr || !c.out || c.next_ty < c.th) goto done; + if (c.color == 3 && c.pal_n <= 0) goto done; + *tw = c.tw; + *th = c.th; + *rgba = c.out; + c.out = NULL; + ok = 1; +done: + if (c.z_ready) inflateEnd(&c.z); + free(c.line); + free(c.prev); + free(c.cur); + free(c.out); + return ok; +} + +/* ================================================================== */ +/* BMP */ +/* ================================================================== */ +static unsigned le16(const unsigned char *p) { return (unsigned)p[0] | ((unsigned)p[1] << 8); } +static unsigned le32(const unsigned char *p) +{ + return (unsigned)p[0] | ((unsigned)p[1] << 8) | ((unsigned)p[2] << 16) | ((unsigned)p[3] << 24); +} + +int meow_bmp_decode_stream(FILE *f, const unsigned char *mem, size_t len, + int maxdim, int *tw, int *th, unsigned char **rgba) +{ + png_src src; + unsigned char fh[14], dh[4]; + unsigned char dib[1024]; + unsigned dibsize, dataoff, bpp, compression, colors; + long w, h, rowsize; + int topdown, i; + unsigned char pal[256][3]; + unsigned char *row = NULL, *out = NULL; + int tw_ = 0, th_ = 0, ok = 0; + + if (maxdim <= 0) return 0; + memset(&src, 0, sizeof src); + src.f = f; + src.mem = mem; + src.memlen = len; + if (!ps_read(&src, fh, 14)) return 0; + if (fh[0] != 'B' || fh[1] != 'M') return 0; + dataoff = le32(fh + 10); + if (!ps_read(&src, dh, 4)) return 0; + dibsize = le32(dh); + if (dibsize < 40 || dibsize > sizeof dib) return 0; /* BITMAPCOREHEADER/OS2 → stb */ + memcpy(dib, dh, 4); + if (!ps_read(&src, dib + 4, dibsize - 4)) return 0; + w = (long)(int)le32(dib + 4); + h = (long)(int)le32(dib + 8); + bpp = le16(dib + 14); + compression = le32(dib + 16); + colors = (dibsize >= 36) ? le32(dib + 32) : 0; + if (compression != 0) return 0; /* RLE/BITFIELDS → stb */ + if (w <= 0 || h == 0) return 0; + if (bpp != 8 && bpp != 16 && bpp != 24 && bpp != 32) return 0; + topdown = h < 0; + if (h < 0) h = -h; + if ((long)(w > h ? w : h) <= maxdim) return 0; /* 小图走旧入口 */ + + if (bpp <= 8) { + unsigned n = colors ? colors : 256; + unsigned char e[4]; + if (n > 256) n = 256; + for (i = 0; (unsigned)i < n; i++) { + if (!ps_read(&src, e, 4)) return 0; + pal[i][0] = e[2]; pal[i][1] = e[1]; pal[i][2] = e[0]; + } + for (; i < 256; i++) pal[i][0] = pal[i][1] = pal[i][2] = 0; + } + + rowsize = (w * bpp + 31) / 32 * 4; + meow_img_target_dims((int)w, (int)h, maxdim, &tw_, &th_); + row = (unsigned char *)malloc((size_t)rowsize); + out = (unsigned char *)malloc((size_t)tw_ * th_ * 4); + if (!row || !out) goto done; + + for (int ty = 0; ty < th_; ty++) { + int sy = (int)((long)ty * h / th_); + long file_row = topdown ? sy : (h - 1 - sy); + long off = (long)dataoff + file_row * rowsize; + unsigned char *o = out + (size_t)ty * tw_ * 4; + if (src.mem) { + if (off < 0 || (size_t)off + (size_t)rowsize > src.memlen) goto done; + memcpy(row, src.mem + off, (size_t)rowsize); + } else { + if (fseek(src.f, off, SEEK_SET) != 0) goto done; + if (fread(row, 1, (size_t)rowsize, src.f) != (size_t)rowsize) goto done; + } + for (int tx = 0; tx < tw_; tx++) { + int sx = (int)((long)tx * w / tw_); + unsigned char *p = o + (size_t)tx * 4; + if (bpp == 24) { + const unsigned char *q = row + sx * 3; + p[0] = q[2]; p[1] = q[1]; p[2] = q[0]; p[3] = 255; + } else if (bpp == 32) { + const unsigned char *q = row + sx * 4; + p[0] = q[2]; p[1] = q[1]; p[2] = q[0]; p[3] = 255; /* BI_RGB:alpha 不作为语义 */ + } else if (bpp == 16) { + unsigned v = le16(row + sx * 2); + unsigned r = (v >> 10) & 31, g = (v >> 5) & 31, b = v & 31; + p[0] = (unsigned char)((r << 3) | (r >> 2)); + p[1] = (unsigned char)((g << 3) | (g >> 2)); + p[2] = (unsigned char)((b << 3) | (b >> 2)); + p[3] = 255; + } else { + unsigned idx = row[sx]; + p[0] = pal[idx & 255][0]; p[1] = pal[idx & 255][1]; + p[2] = pal[idx & 255][2]; p[3] = 255; + } + } + } + *tw = tw_; + *th = th_; + *rgba = out; + out = NULL; + ok = 1; +done: + free(row); + free(out); + return ok; +} diff --git a/sdk/meow_ui_img.c b/sdk/meow_ui_img.c index 31c33c2..8f20682 100644 --- a/sdk/meow_ui_img.c +++ b/sdk/meow_ui_img.c @@ -1,10 +1,11 @@ /* - * meow_ui_img.c — 图片层(B1.2)。基于 stb_image(单头文件、静态编入), - * 解 PNG/JPEG/BMP → RGBA,统一最近邻缩放/居中裁方。 + * meow_ui_img.c — 图片层(B1.2)。解码:M23 真缩解后端(meow_jpeg.c / meow_png_stream.c) + * 优先,不支持/失败回退 stb_image(单头文件、静态编入);统一输出 RGBA,最近邻缩放/居中裁方。 */ #include "meow_ui.h" #include "meow_ui_internal.h" #include "stb_image.h" +#include "meow_jpeg.h" /* M23 真缩解后端(内部头,不对外导出) */ #include #include @@ -79,6 +80,16 @@ meow_img *meow_img_load_file_dim(const char *path, int maxdim) { if (!path) return NULL; if (maxdim <= 0) return meow_img_load_file(path); /* 不限尺寸 = 旧入口 */ + /* 追加(M23):真缩解后端直接产出目标尺寸(JPEG 按 DCT 档位;PNG 逐行 inflate 丢行丢列; + * BMP 按行 seek 跳采样)。峰值只与输出尺寸相关。 + * 不支持/失败/几乎等尺寸(4 分量 CMYK、算术编码、交错 PNG、RLE BMP、小图…)返回 0 → + * 落回下面的 stb 旧路径,对外语义与档位表现不变。 */ + { + unsigned char *bp = NULL; + int bw = 0, bh = 0; + if (meow_img_backend_file(path, maxdim, &bw, &bh, &bp) && bp) + return img_finish_dim(bp, bw, bh, maxdim); + } int w = 0, h = 0, c = 0; if (!stbi_info(path, &w, &h, &c)) return NULL; int longside = w > h ? w : h; @@ -121,6 +132,13 @@ meow_img *meow_img_load_mem_dim(const void *buf, int len, int maxdim) { if (!buf || len <= 0) return NULL; if (maxdim <= 0) return meow_img_load_mem(buf, len); + /* 追加(M23):同文件入口,后端直接从调用方缓冲解码(不再整幅解 + 重采样)。 */ + { + unsigned char *bp = NULL; + int bw = 0, bh = 0; + if (meow_img_backend_mem(buf, (size_t)len, maxdim, &bw, &bh, &bp) && bp) + return img_finish_dim(bp, bw, bh, maxdim); + } int w = 0, h = 0, c = 0; if (!stbi_info_from_memory((const unsigned char *)buf, len, &w, &h, &c)) return NULL; int longside = w > h ? w : h; diff --git a/sdk/test_meow_img_budget.c b/sdk/test_meow_img_budget.c index 275760a..79ab86b 100644 --- a/sdk/test_meow_img_budget.c +++ b/sdk/test_meow_img_budget.c @@ -10,13 +10,15 @@ * B6 进升级:progressive JPEG 走同一缩解路径(峰值记录 + 长边约束) * B7 内存入口:meow_img_load_mem_dim 与文件入口等价(峰值 < 旧内存入口 1/2) * B8 坏图:坏 JPEG(魔数+垃圾)/ 空内存 / 不存在路径 → NULL(不崩) - * B9 PNG/BMP:尺寸约束成立,峰值如实记录(stb 无解码期缩放 → 与旧入口同级) + * B9 PNG/BMP:尺寸约束成立;M23 起 PNG/BMP 也走解码期缩解(逐行 inflate / 按行 seek), + * 峰值应显著低于旧入口(本用例只断言 ≤ maxdim + 非空,并打印峰值供报告引用) * * 夹具(生成一次): * python3 test_meow_img_budget_gen.py "$PREFIX/tmp/meow_img_budget" - * 编译(本 sdk 目录下;--wrap 用于给 malloc/free 家族打点): + * 编译(本 sdk 目录下;--wrap 用于给 malloc/free 家族打点;-lz 供 PNG 后端 inflate): * gcc -Wall -Wextra -I. -Iinclude -DMEOW_UI_HOST_TEST -o test_meow_img_budget \ - * test_meow_img_budget.c meow_ui_img.c stb_image.c -lm \ + * test_meow_img_budget.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_budget "$PREFIX/tmp/meow_img_budget" (exit 0=PASS / 1=FAIL) * 夹具目录可省略:默认 $PREFIX/tmp/meow_img_budget;缺失时自动调 gen 脚本生成。 @@ -421,8 +423,8 @@ int main(int argc, char **argv) CHECK(meow_img_load_mem_dim("x", 3, 480) == NULL, "垃圾内存 → NULL"); } - /* ---- B9 PNG / BMP(stb 无解码期缩放,如实记录峰值) ---- */ - printf("[B9] PNG / BMP:尺寸约束成立,峰值如实记录(stb 无解码期缩放)\n"); + /* ---- B9 PNG / BMP(M23 起也走解码期缩解;只断言尺寸/非空,峰值打印) ---- */ + printf("[B9] PNG / BMP:尺寸约束成立,峰值记录(M23 逐行/跳行缩解,应远低于旧入口)\n"); { struct { const char *tag; const char *path; } fx[] = { { "PNG", p_png }, { "BMP", p_bmp }, @@ -446,8 +448,8 @@ int main(int argc, char **argv) CHECK(nw && meow_img_w(nw) <= 480 && meow_img_h(nw) <= 480 && nonblank(nw), name); peak_report(fx[i].tag, p_old); peak_report(" ↑ 新入口", p_new); - snprintf(name, sizeof name, "%s 峰值与旧入口同级(stb 无解码期缩放)", fx[i].tag); - CHECK(p_new <= p_old + p_old / 8, name); + snprintf(name, sizeof name, "%s 新入口峰值 ≤ 旧入口(不再整幅物化)", fx[i].tag); + CHECK(p_new <= p_old, name); if (old) printf(" %s 原图 %dx%d\n", fx[i].tag, meow_img_w(old), meow_img_h(old)); meow_img_free(nw); } diff --git a/sdk/test_meow_img_stream.c b/sdk/test_meow_img_stream.c new file mode 100644 index 0000000..509b3ac --- /dev/null +++ b/sdk/test_meow_img_stream.c @@ -0,0 +1,762 @@ +/* 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 +#include +#include +#include +#include +#include /* clock():解码耗时打印 */ +#include /* 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" +"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 *mark = path_join("base2000.jpg"); + int have = file_exists(mark); + free(mark); + 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; +} + +/* ================================================================== */ +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(); + meow_img *got = meow_img_load_file_dim(prs, 720); + size_t pk = peak_since(base); + meow_img *ref = ref_thumb(prs, 720); + double corr = 0.0; + double d = (got && ref) ? mean_abs_diff(got, ref, 64, &corr) : 1e9; + peak_report("restart marker 2000x3000", pk); + 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); + } + free(psw); + free(prs); + } + + /* ---- 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; +}