SDK: 图片解码真缩解后端 —— JPEG DCT 域 / PNG 逐行 / BMP 跳行(M23)

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。
This commit is contained in:
AskaEth committed 2026-11-05 21:33:54 +08:00
1 parent 88f3d861b0
commit d84dd949c7
8 files changed
+2320 -15

No files matched your search

+1 -1
View File
@@ -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 =="
+14 -5
View File
@@ -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);
+974
View File
@@ -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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
/* ================================================================== */
/* 公共:目标尺寸(与 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;
}
+48
View File
@@ -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 <stddef.h>
#include <stdio.h>
#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 */
+492
View File
@@ -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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
/* ---------------- zlib ABI ---------------- */
#if defined(__has_include)
# if __has_include(<zlib.h>)
# include <zlib.h>
# 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;
}
+20 -2
View File
@@ -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 <stdio.h>
#include <stdlib.h>
@@ -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;
+9 -7
View File
@@ -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);
}
+762
View File
@@ -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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <sys/stat.h>
#include <time.h> /* clock():解码耗时打印 */
#include <math.h> /* 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;
}