diff --git a/sdk/include/meow_ui_image.h b/sdk/include/meow_ui_image.h index 36376b6..4427376 100644 --- a/sdk/include/meow_ui_image.h +++ b/sdk/include/meow_ui_image.h @@ -22,6 +22,18 @@ meow_img *meow_img_load_file(const char *path); /* 内部读文件 + stb 解 meow_img *meow_img_load_mem(const void *buf, int len); void meow_img_free(meow_img *img); +/* 追加(M12)预算解码:只读文件头取原始尺寸,不解码像素(零像素内存)。成功 1,失败 0。 + * 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 报告)。 + * 小图(长边 ≤ 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); + int meow_img_w(const meow_img *img); int meow_img_h(const meow_img *img); /* 访问器:取 RGBA 像素指针(4 通道 straight alpha;供取色/直读像素)。meow_img 对外 opaque, diff --git a/sdk/meow_ui_img.c b/sdk/meow_ui_img.c index baf3791..9571800 100644 --- a/sdk/meow_ui_img.c +++ b/sdk/meow_ui_img.c @@ -42,6 +42,65 @@ meow_img *meow_img_load_file(const char *path) return img_alloc(w, h, p); } +/* 追加(M12):预算解码。选整数缩解档位 s ∈ {1,2,4,8}——取满足「长边/s ≥ maxdim」的最大档, + * 保证缩解结果不欠采样(不丢细节),再由 meow_img_downscale 精修到 maxdim。长边 < 2*maxdim + * 时无档可用(s=1),退回整幅解码后精修——此时本就接近目标尺寸,无需牺牲。 */ +static int img_pick_scale(int longside, int maxdim) +{ + int s = 1; + while (s < 8 && (longside / (s * 2)) >= maxdim) s *= 2; + return s; +} + +/* 追加(M12):缩解结果落地 + 需要时精修。p 所有权转交。 */ +static meow_img *img_finish_dim(unsigned char *p, int w, int h, int maxdim) +{ + if (!p) return NULL; + meow_img *im = img_alloc(w, h, p); + if (!im) return NULL; + if (im->w <= maxdim && im->h <= maxdim) return im; + meow_img *small = meow_img_downscale(im, maxdim); + if (!small) return im; /* 精修失败:缩解结果已小于原图,仍可用 */ + meow_img_free(im); + return small; +} + +int meow_img_probe_file(const char *path, int *w, int *h, int *channels) +{ + int pw = 0, ph = 0, pc = 0; + if (!path || !stbi_info(path, &pw, &ph, &pc)) return 0; + if (w) *w = pw; + if (h) *h = ph; + if (channels) *channels = pc; + return 1; +} + +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); /* 不限尺寸 = 旧入口 */ + int w = 0, h = 0, c = 0; + if (!stbi_info(path, &w, &h, &c)) return NULL; + int longside = w > h ? w : h; + if (longside <= maxdim) return meow_img_load_file(path); /* 小图不动:旧路径 */ + unsigned char *p = stbi_load_scaled(path, &w, &h, &c, 4, img_pick_scale(longside, maxdim)); + return img_finish_dim(p, w, h, maxdim); +} + +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); + 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; + if (longside <= maxdim) return meow_img_load_mem(buf, len); + unsigned char *p = stbi_load_scaled_from_memory((const unsigned char *)buf, len, + &w, &h, &c, 4, + img_pick_scale(longside, maxdim)); + return img_finish_dim(p, w, h, maxdim); +} + void meow_img_free(meow_img *im) { if (!im) return; diff --git a/sdk/stb_image.h b/sdk/stb_image.h index 9eedabe..e1ef827 100644 --- a/sdk/stb_image.h +++ b/sdk/stb_image.h @@ -429,6 +429,14 @@ STBIDEF stbi_uc *stbi_load_from_file (FILE *f, int *x, int *y, int *channels_in // for stbi_load_from_file, file pointer is left pointing immediately after image #endif +// 追加(MeowOS SDK M12):JPEG 解码期降采样。scale>=2 时按 s 抽取输出行/列, +// 不物化整幅原尺寸输出缓冲(*x,*y 返回 ceil(原尺寸/s))。scale<=1 与 +// stbi_load / stbi_load_from_memory 完全等价;非 JPEG 格式忽略 scale。 +STBIDEF stbi_uc *stbi_load_scaled_from_memory(stbi_uc const *buffer, int len, int *x, int *y, int *channels_in_file, int desired_channels, int scale); +#ifndef STBI_NO_STDIO +STBIDEF stbi_uc *stbi_load_scaled (char const *filename, int *x, int *y, int *channels_in_file, int desired_channels, int scale); +#endif + #ifndef STBI_NO_GIF STBIDEF stbi_uc *stbi_load_gif_from_memory(stbi_uc const *buffer, int len, int **delays, int *x, int *y, int *z, int *comp, int req_comp); #endif @@ -907,7 +915,7 @@ typedef struct #ifndef STBI_NO_JPEG static int stbi__jpeg_test(stbi__context *s); -static void *stbi__jpeg_load(stbi__context *s, int *x, int *y, int *comp, int req_comp, stbi__result_info *ri); +static void *stbi__jpeg_load_ex(stbi__context *s, int *x, int *y, int *comp, int req_comp, stbi__result_info *ri, int scale); static int stbi__jpeg_info(stbi__context *s, int *x, int *y, int *comp); #endif @@ -1134,7 +1142,7 @@ STBIDEF void stbi_set_flip_vertically_on_load_thread(int flag_true_if_should_fli : stbi__vertically_flip_on_load_global) #endif // STBI_THREAD_LOCAL -static void *stbi__load_main(stbi__context *s, int *x, int *y, int *comp, int req_comp, stbi__result_info *ri, int bpc) +static void *stbi__load_main(stbi__context *s, int *x, int *y, int *comp, int req_comp, stbi__result_info *ri, int bpc, int jpeg_scale) { memset(ri, 0, sizeof(*ri)); // make sure it's initialized if we add new fields ri->bits_per_channel = 8; // default is 8 so most paths don't have to be changed @@ -1165,7 +1173,9 @@ static void *stbi__load_main(stbi__context *s, int *x, int *y, int *comp, int re // bytes matching expectations; these are prone to false positives, so // try them later #ifndef STBI_NO_JPEG - if (stbi__jpeg_test(s)) return stbi__jpeg_load(s,x,y,comp,req_comp, ri); + if (stbi__jpeg_test(s)) return stbi__jpeg_load_ex(s,x,y,comp,req_comp, ri, jpeg_scale); + #else + STBI_NOTUSED(jpeg_scale); #endif #ifndef STBI_NO_PNM if (stbi__pnm_test(s)) return stbi__pnm_load(s,x,y,comp,req_comp, ri); @@ -1257,10 +1267,10 @@ static void stbi__vertical_flip_slices(void *image, int w, int h, int z, int byt } #endif -static unsigned char *stbi__load_and_postprocess_8bit(stbi__context *s, int *x, int *y, int *comp, int req_comp) +static unsigned char *stbi__load_and_postprocess_8bit_ex(stbi__context *s, int *x, int *y, int *comp, int req_comp, int jpeg_scale) { stbi__result_info ri; - void *result = stbi__load_main(s, x, y, comp, req_comp, &ri, 8); + void *result = stbi__load_main(s, x, y, comp, req_comp, &ri, 8, jpeg_scale); if (result == NULL) return NULL; @@ -1283,10 +1293,15 @@ static unsigned char *stbi__load_and_postprocess_8bit(stbi__context *s, int *x, return (unsigned char *) result; } +static unsigned char *stbi__load_and_postprocess_8bit(stbi__context *s, int *x, int *y, int *comp, int req_comp) +{ + return stbi__load_and_postprocess_8bit_ex(s, x, y, comp, req_comp, 1); +} + static stbi__uint16 *stbi__load_and_postprocess_16bit(stbi__context *s, int *x, int *y, int *comp, int req_comp) { stbi__result_info ri; - void *result = stbi__load_main(s, x, y, comp, req_comp, &ri, 16); + void *result = stbi__load_main(s, x, y, comp, req_comp, &ri, 16, 1); if (result == NULL) return NULL; @@ -1409,6 +1424,19 @@ STBIDEF stbi_us *stbi_load_16(char const *filename, int *x, int *y, int *comp, i return result; } +// 追加(M12):JPEG 解码期降采样(见头文件声明)。非 JPEG 等价于 stbi_load。 +STBIDEF stbi_uc *stbi_load_scaled(char const *filename, int *x, int *y, int *comp, int req_comp, int scale) +{ + FILE *f = stbi__fopen(filename, "rb"); + stbi_uc *result; + stbi__context s; + if (!f) return stbi__errpuc("can't fopen", "Unable to open file"); + stbi__start_file(&s,f); + result = stbi__load_and_postprocess_8bit_ex(&s,x,y,comp,req_comp, scale); + if (result) fseek(f, - (int) (s.img_buffer_end - s.img_buffer), SEEK_CUR); + fclose(f); + return result; +} #endif //!STBI_NO_STDIO @@ -1433,6 +1461,14 @@ STBIDEF stbi_uc *stbi_load_from_memory(stbi_uc const *buffer, int len, int *x, i return stbi__load_and_postprocess_8bit(&s,x,y,comp,req_comp); } +// 追加(M12):JPEG 解码期降采样(见头文件声明)。非 JPEG 等价于 stbi_load_from_memory。 +STBIDEF stbi_uc *stbi_load_scaled_from_memory(stbi_uc const *buffer, int len, int *x, int *y, int *comp, int req_comp, int scale) +{ + stbi__context s; + stbi__start_mem(&s,buffer,len); + return stbi__load_and_postprocess_8bit_ex(&s,x,y,comp,req_comp, scale); +} + STBIDEF stbi_uc *stbi_load_from_callbacks(stbi_io_callbacks const *clbk, void *user, int *x, int *y, int *comp, int req_comp) { stbi__context s; @@ -1982,6 +2018,7 @@ typedef struct int nomore; // flag if we saw a marker so must stop int progressive; + int scale; // 追加(M12):输出抽取步长,默认 1=原尺寸 int spec_start; int spec_end; int succ_high; @@ -3092,6 +3129,12 @@ static void stbi__jpeg_finish(stbi__jpeg *z) z->idct_block_kernel(z->img_comp[n].data+z->img_comp[n].w2*j*8+i*8, z->img_comp[n].w2, data); } } + // 追加(M12):该分量系数已全部 IDCT 进像素平面,立即释放,降低进升级 JPEG 的峰值 + if (z->img_comp[n].raw_coeff) { + STBI_FREE(z->img_comp[n].raw_coeff); + z->img_comp[n].raw_coeff = 0; + z->img_comp[n].coeff = 0; + } } } } @@ -3893,6 +3936,10 @@ static stbi_uc *load_jpeg_image(stbi__jpeg *z, int *out_x, int *out_y, int *comp unsigned int i,j; stbi_uc *output; stbi_uc *coutput[4] = { NULL, NULL, NULL, NULL }; + stbi_uc *rowbuf = NULL; // 追加(M12):整行临时缓冲,仅 dec>1 时使用 + int dec = z->scale > 1 ? z->scale : 1; + int out_w = (z->s->img_x + dec - 1) / dec; + int out_h = (z->s->img_y + dec - 1) / dec; stbi__resample res_comp[4]; @@ -3918,13 +3965,19 @@ static stbi_uc *load_jpeg_image(stbi__jpeg *z, int *out_x, int *out_y, int *comp else r->resample = stbi__resample_row_generic; } + // 追加(M12):dec>1 时整行先转成像素再按步长抽取,不物化整幅原尺寸输出缓冲 + if (dec > 1) { + rowbuf = (stbi_uc *) stbi__malloc_mad2(n, z->s->img_x, 0); + if (!rowbuf) { stbi__cleanup_jpeg(z); return stbi__errpuc("outofmem", "Out of memory"); } + } + // can't error after this so, this is safe - output = (stbi_uc *) stbi__malloc_mad3(n, z->s->img_x, z->s->img_y, 1); - if (!output) { stbi__cleanup_jpeg(z); return stbi__errpuc("outofmem", "Out of memory"); } + output = (stbi_uc *) stbi__malloc_mad3(n, out_w, out_h, 1); + if (!output) { STBI_FREE(rowbuf); stbi__cleanup_jpeg(z); return stbi__errpuc("outofmem", "Out of memory"); } // now go ahead and resample for (j=0; j < z->s->img_y; ++j) { - stbi_uc *out = output + n * z->s->img_x * j; + stbi_uc *out; for (k=0; k < decode_n; ++k) { stbi__resample *r = &res_comp[k]; int y_bot = r->ystep >= (r->vs >> 1); @@ -3939,6 +3992,9 @@ static stbi_uc *load_jpeg_image(stbi__jpeg *z, int *out_x, int *out_y, int *comp r->line1 += z->img_comp[k].w2; } } + // 追加(M12):resample 状态机须逐行推进(上面已完成);非采样行直接丢弃 + if (dec > 1 && (j % (unsigned int) dec) != 0) continue; + out = (dec > 1) ? rowbuf : output + (size_t) n * out_w * j; if (n >= 3) { stbi_uc *y = coutput[0]; if (z->s->img_n == 3) { @@ -4016,16 +4072,27 @@ static stbi_uc *load_jpeg_image(stbi__jpeg *z, int *out_x, int *out_y, int *comp for (i=0; i < z->s->img_x; ++i) { *out++ = y[i]; *out++ = 255; } } } + // 追加(M12):rowbuf 已按整行转好像素,按步长抽取写入输出行 + if (dec > 1) { + stbi_uc *dst = output + (size_t) n * out_w * (j / (unsigned int) dec); + unsigned int x; + for (x=0; x < (unsigned int) out_w; ++x) { + int c; + for (c=0; c < n; ++c) + dst[(size_t) x * n + c] = rowbuf[(size_t) x * dec * n + c]; + } + } } + STBI_FREE(rowbuf); stbi__cleanup_jpeg(z); - *out_x = z->s->img_x; - *out_y = z->s->img_y; + *out_x = out_w; + *out_y = out_h; if (comp) *comp = z->s->img_n >= 3 ? 3 : 1; // report original components, not output return output; } } -static void *stbi__jpeg_load(stbi__context *s, int *x, int *y, int *comp, int req_comp, stbi__result_info *ri) +static void *stbi__jpeg_load_ex(stbi__context *s, int *x, int *y, int *comp, int req_comp, stbi__result_info *ri, int scale) { unsigned char* result; stbi__jpeg* j = (stbi__jpeg*) stbi__malloc(sizeof(stbi__jpeg)); @@ -4033,6 +4100,7 @@ static void *stbi__jpeg_load(stbi__context *s, int *x, int *y, int *comp, int re memset(j, 0, sizeof(stbi__jpeg)); STBI_NOTUSED(ri); j->s = s; + j->scale = scale; // 追加(M12):输出抽取步长 stbi__setup_jpeg(j); result = load_jpeg_image(j, x,y,comp,req_comp); STBI_FREE(j); diff --git a/sdk/test_meow_img_budget.c b/sdk/test_meow_img_budget.c new file mode 100644 index 0000000..275760a --- /dev/null +++ b/sdk/test_meow_img_budget.c @@ -0,0 +1,464 @@ +/* test_meow_img_budget.c — M12 预算解码(meow_img_load_file_dim / _mem_dim)宿主验证。 + * + * 覆盖矩阵(对齐 M12 任务 3/4): + * B1 探针:meow_img_probe_file 只读头取原始尺寸(8.32MP 图),坏图/不存在返回 0 + * B2 小图不动:64x64 走旧路径,与 meow_img_load_file 逐字节相同;maxdim<=0 等价旧入口 + * B3 长边约束:8.32MP baseline JPEG 在 maxdim=480/300/128/64 下结果两维都 ≤ maxdim,长宽比保持 + * B4 峰值对比:同一 8.32MP JPEG「旧入口 vs 新入口」瞬时分配峰值(--wrap malloc 打点), + * 断言新入口峰值 < 旧入口 1/2;B4b 极端 maxdim=100;B4c finding 原始场景 1600x2400 页 + * B5 内容正确:新入口结果与「旧入口整幅解码 + meow_img_downscale」网格采样比对(平均差阈值) + * B6 进升级:progressive JPEG 走同一缩解路径(峰值记录 + 长边约束) + * B7 内存入口:meow_img_load_mem_dim 与文件入口等价(峰值 < 旧内存入口 1/2) + * B8 坏图:坏 JPEG(魔数+垃圾)/ 空内存 / 不存在路径 → NULL(不崩) + * B9 PNG/BMP:尺寸约束成立,峰值如实记录(stb 无解码期缩放 → 与旧入口同级) + * + * 夹具(生成一次): + * python3 test_meow_img_budget_gen.py "$PREFIX/tmp/meow_img_budget" + * 编译(本 sdk 目录下;--wrap 用于给 malloc/free 家族打点): + * 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 \ + * -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 脚本生成。 + */ +#define _GNU_SOURCE +#include "meow_ui.h" +#include "meow_ui_internal.h" /* meow_gfx_blit_rgba 原型(本测试只给桩) */ + +#include +#include +#include +#include +#include + +/* ------------------------------------------------------------------ */ +/* 分配打点:--wrap malloc/free 家族,记录「存活字节」与峰值 */ +/* ------------------------------------------------------------------ */ +#define AHDR 32 /* 头部长度:保留原始大小,保证 16 字节对齐 */ + +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) + +static void peak_report(const char *tag, size_t bytes) +{ + printf(" peak %-28s %8.2f MiB (%zu B)\n", tag, (double)bytes / (1024.0 * 1024.0), bytes); +} + +/* ------------------------------------------------------------------ */ +/* 路径与夹具 */ +/* ------------------------------------------------------------------ */ +static const char *g_dir = NULL; + +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 *big = path_join("big.jpg"); + if (file_exists(big)) { free(big); return; } + free(big); + printf("夹具缺失,尝试 python3 test_meow_img_budget_gen.py %s\n", g_dir); + { + char cmd[1024]; + snprintf(cmd, sizeof cmd, "python3 test_meow_img_budget_gen.py '%s'", g_dir); + if (system(cmd) != 0) + printf(" 生成失败(缺 Pillow?)——请按头注释手动生成\n"); + } +} + +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; } + fclose(f); + *len = (size_t)n; + 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 mean_abs_diff(const meow_img *a, const meow_img *b, int grid) +{ + 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++; + } + } + } + return cnt ? (double)acc / (double)cnt : 1e9; +} + +/* ================================================================== */ +int main(int argc, char **argv) +{ + char *p_big, *p_prog, *p_small, *p_png, *p_bmp, *p_broken, *p_missing; + size_t peak_old, peak_new; + + if (argc > 1) { + g_dir = argv[1]; + } else { + /* Termux 的 /tmp 不可写:优先 $TMPDIR,其次 $PREFIX/tmp。 */ + static char dflt[600]; + const char *tmp = getenv("TMPDIR"); + const char *pre = getenv("PREFIX"); + if (tmp && *tmp) snprintf(dflt, sizeof dflt, "%s/meow_img_budget", tmp); + else if (pre && *pre) snprintf(dflt, sizeof dflt, "%s/tmp/meow_img_budget", pre); + else snprintf(dflt, sizeof dflt, "/tmp/meow_img_budget"); + g_dir = dflt; + } + ensure_fixtures(); + + p_big = path_join("big.jpg"); + p_prog = path_join("bigprog.jpg"); + p_small = path_join("small.jpg"); + p_png = path_join("big.png"); + p_bmp = path_join("big.bmp"); + p_broken = path_join("broken.jpg"); + p_missing = path_join("no_such_file.jpg"); + + if (!file_exists(p_big)) { + printf("FATAL 夹具 %s 不存在(先跑 test_meow_img_budget_gen.py)\n", p_big); + return 1; + } + + /* ---- B1 探针 ---- */ + printf("[B1] 头探测(零像素内存)\n"); + { + int w = 0, h = 0, c = 0; + CHECK(meow_img_probe_file(p_big, &w, &h, &c) == 1, "probe 成功"); + CHECK(w == 3200 && h == 2600, "probe 原始尺寸 3200x2600"); + CHECK(c == 3, "probe 通道数 3"); + printf(" probe → %dx%d ch=%d\n", w, h, c); + CHECK(meow_img_probe_file(p_broken, NULL, NULL, NULL) == 0, "probe 截断图 → 0"); + CHECK(meow_img_probe_file(p_missing, NULL, NULL, NULL) == 0, "probe 不存在 → 0"); + CHECK(meow_img_probe_file(NULL, NULL, NULL, NULL) == 0, "probe NULL → 0"); + } + + /* ---- B2 小图不动 ---- */ + printf("[B2] 小图不动 / maxdim<=0 等价旧入口\n"); + { + 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(a && b && z, "小图三条路径都成功"); + CHECK(meow_img_w(b) == meow_img_w(a) && meow_img_h(b) == meow_img_h(a), + "maxdim=64 → 尺寸不变"); + CHECK(b && a && meow_img_w(a) * meow_img_h(a) > 0 && + memcmp(meow_img_pixels(a), meow_img_pixels(b), + (size_t)meow_img_w(a) * meow_img_h(a) * 4) == 0, + "小图逐字节与旧入口一致"); + CHECK(z && a && memcmp(meow_img_pixels(a), meow_img_pixels(z), + (size_t)meow_img_w(a) * meow_img_h(a) * 4) == 0, + "maxdim=0 等价旧入口(逐字节)"); + meow_img_free(a); meow_img_free(b); meow_img_free(z); + } + + /* ---- B3 长边约束 ---- */ + printf("[B3] 8.32MP baseline JPEG 长边约束\n"); + { + static const int dims[] = { 480, 300, 128, 64 }; + int wi = 0, hi = 0; + meow_img *full = meow_img_load_file(p_big); + CHECK(full != NULL, "旧入口解码成功"); + if (full) { wi = meow_img_w(full); hi = meow_img_h(full); } + printf(" 原图 %dx%d (%.2f MP)\n", wi, hi, (double)wi * hi / 1e6); + for (size_t i = 0; i < sizeof dims / sizeof dims[0]; i++) { + meow_img *s = meow_img_load_file_dim(p_big, dims[i]); + char name[96]; + snprintf(name, sizeof name, "maxdim=%d → 两维 ≤ maxdim 且非空", dims[i]); + CHECK(s && meow_img_w(s) <= dims[i] && meow_img_h(s) <= dims[i] && nonblank(s), name); + if (s) { + double ar0 = (double)wi / hi, ar1 = (double)meow_img_w(s) / meow_img_h(s); + CHECK(ar1 > ar0 * 0.98 && ar1 < ar0 * 1.02, "长宽比保持(±2%)"); + printf(" maxdim=%-4d → %dx%d\n", dims[i], meow_img_w(s), meow_img_h(s)); + } + meow_img_free(s); + } + meow_img_free(full); + } + + /* ---- B4 峰值对比(JPEG) ---- */ + printf("[B4] 瞬时分配峰值:旧入口 vs 新入口(同一 8.32MP JPEG)\n"); + { + size_t base = peak_reset(); + meow_img *old = meow_img_load_file(p_big); + peak_old = peak_since(base); + + base = peak_reset(); + meow_img *nw = meow_img_load_file_dim(p_big, 480); + peak_new = peak_since(base); + + CHECK(old && nw, "两条路径都成功"); + peak_report("旧入口 meow_img_load_file", peak_old); + peak_report("新入口 _dim(480)", peak_new); + printf(" 整幅 RGBA 理论值 = %.2f MiB;新/旧 = %.2f\n", + (double)meow_img_w(old) * meow_img_h(old) * 4 / (1024.0 * 1024.0), + (double)peak_new / (double)peak_old); + CHECK(peak_new * 2 < peak_old, "新入口峰值 < 旧入口 1/2(不物化整幅 RGBA)"); + CHECK(peak_new < (size_t)20 * 1024 * 1024, "新入口峰值 < 20 MiB(8.32MP 页可加载)"); + + /* B5 内容正确性:新 vs 旧整幅 + downscale 精修 */ + if (old && nw) { + meow_img *ref = meow_img_downscale(old, 480); + double d = mean_abs_diff(nw, ref ? ref : old, 64); + printf(" 与「旧入口+downscale」网格平均差 = %.2f\n", d); + CHECK(d < 20.0, "缩解内容结构一致(平均差 < 20)"); + meow_img_free(ref); + } + meow_img_free(old); + meow_img_free(nw); + } + + /* ---- B4b 极端 maxdim ---- */ + printf("[B4b] maxdim=100(8 倍缩解档位 + 精修)\n"); + { + size_t base = peak_reset(); + meow_img *s = meow_img_load_file_dim(p_big, 100); + size_t peak = peak_since(base); + CHECK(s && meow_img_w(s) <= 100 && meow_img_h(s) <= 100 && nonblank(s), "结果 ≤100 且非空"); + peak_report("_dim(100)", peak); + CHECK(peak * 2 < peak_old, "峰值仍 < 旧入口 1/2"); + meow_img_free(s); + } + + /* ---- B4c finding 原始场景 ---- */ + printf("[B4c] finding 原始场景:1600x2400 漫画页\n"); + { + char *p_page = path_join("page.jpg"); + if (file_exists(p_page)) { + size_t base = peak_reset(); + meow_img *old = meow_img_load_file(p_page); + size_t p_old = peak_since(base); + base = peak_reset(); + meow_img *nw = meow_img_load_file_dim(p_page, 720); + size_t p_new = peak_since(base); + CHECK(old && nw, "1600x2400 两条路径都成功"); + if (nw) + CHECK(meow_img_w(nw) <= 720 && meow_img_h(nw) <= 720 && nonblank(nw), + "结果 ≤720 且非空"); + if (old) + printf(" 整幅 RGBA 理论值 = %.2f MiB\n", + (double)meow_img_w(old) * meow_img_h(old) * 4 / (1024.0 * 1024.0)); + peak_report("旧入口 page.jpg", p_old); + peak_report("新入口 _dim(720)", p_new); + CHECK(p_new < (size_t)10 * 1024 * 1024, "新入口峰值 < 10 MiB(设备 ~14MB 口径内)"); + meow_img_free(old); + meow_img_free(nw); + } else { + printf(" SKIP page.jpg(夹具缺失)\n"); + } + free(p_page); + } + + /* ---- B6 进升级 JPEG ---- */ + printf("[B6] progressive JPEG(同一缩解路径)\n"); + { + size_t base = peak_reset(); + meow_img *old = meow_img_load_file(p_prog); + size_t p_old = peak_since(base); + base = peak_reset(); + meow_img *nw = meow_img_load_file_dim(p_prog, 480); + size_t p_new = peak_since(base); + CHECK(old && nw, "进升级两条路径都成功"); + CHECK(nw && meow_img_w(nw) <= 480 && meow_img_h(nw) <= 480 && nonblank(nw), + "进升级结果 ≤480 且非空"); + peak_report("旧入口 progressive", p_old); + peak_report("新入口 _dim(480) progressive", p_new); + CHECK(p_new < p_old, "进升级峰值下降(系数阵列仍在扫描期驻留,见报告)"); + meow_img_free(old); + meow_img_free(nw); + } + + /* ---- B7 内存入口 ---- */ + printf("[B7] 内存入口 meow_img_load_mem_dim(CBZ/档案页场景)\n"); + { + size_t len = 0; + uint8_t *buf = read_whole(p_big, &len); + CHECK(buf != NULL, "读入 8.32MP JPEG 到内存"); + if (buf) { + size_t base = peak_reset(); + meow_img *old = meow_img_load_mem(buf, (int)len); + size_t p_old = peak_since(base); + base = peak_reset(); + meow_img *nw = meow_img_load_mem_dim(buf, (int)len, 480); + size_t p_new = peak_since(base); + CHECK(old && nw, "内存入口两条路径都成功"); + CHECK(nw && meow_img_w(nw) <= 480 && nonblank(nw), "内存入口结果 ≤480 且非空"); + peak_report("旧 meow_img_load_mem", p_old); + peak_report("新 meow_img_load_mem_dim", p_new); + CHECK(p_new * 2 < p_old, "内存入口峰值 < 旧入口 1/2"); + meow_img_free(old); + meow_img_free(nw); + free(buf); + } + } + + /* ---- B8 坏图 ---- */ + printf("[B8] 坏图容错\n"); + { + size_t len = 0; + uint8_t *buf; + CHECK(meow_img_load_file_dim(p_broken, 480) == NULL, "截断 JPEG → NULL"); + CHECK(meow_img_load_file_dim(p_missing, 480) == NULL, "不存在路径 → NULL"); + CHECK(meow_img_load_file_dim(NULL, 480) == NULL, "NULL 路径 → NULL"); + buf = read_whole(p_broken, &len); + CHECK(buf && meow_img_load_mem_dim(buf, (int)len, 480) == NULL, "截断 JPEG 内存入口 → NULL"); + free(buf); + CHECK(meow_img_load_mem_dim("x", 3, 480) == NULL, "垃圾内存 → NULL"); + } + + /* ---- B9 PNG / BMP(stb 无解码期缩放,如实记录峰值) ---- */ + printf("[B9] PNG / BMP:尺寸约束成立,峰值如实记录(stb 无解码期缩放)\n"); + { + struct { const char *tag; const char *path; } fx[] = { + { "PNG", p_png }, { "BMP", p_bmp }, + }; + for (size_t i = 0; i < sizeof fx / sizeof fx[0]; i++) { + if (!file_exists(fx[i].path)) { + printf(" SKIP %s(夹具缺失)\n", fx[i].tag); + continue; + } + { + size_t base = peak_reset(); + meow_img *old = meow_img_load_file(fx[i].path); + size_t p_old = peak_since(base); + meow_img *nw; + char name[96]; + base = peak_reset(); + nw = meow_img_load_file_dim(fx[i].path, 480); + { + size_t p_new = peak_since(base); + snprintf(name, sizeof name, "%s _dim(480) 两维 ≤480 且非空", fx[i].tag); + 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); + if (old) printf(" %s 原图 %dx%d\n", fx[i].tag, meow_img_w(old), meow_img_h(old)); + meow_img_free(nw); + } + meow_img_free(old); + } + } + } + + if (g_fail) printf("\nRESULT: FAIL (%d)\n", g_fail); + else printf("\nRESULT: ALL PASS\n"); + free(p_big); free(p_prog); free(p_small); free(p_png); + free(p_bmp); free(p_broken); free(p_missing); + return g_fail ? 1 : 0; +} diff --git a/sdk/test_meow_img_budget_gen.py b/sdk/test_meow_img_budget_gen.py new file mode 100644 index 0000000..715c6ad --- /dev/null +++ b/sdk/test_meow_img_budget_gen.py @@ -0,0 +1,69 @@ +#!/usr/bin/env python3 +"""test_meow_img_budget_gen.py — 为宿主测试 test_meow_img_budget.c 生成图片夹具。 + +用法:python3 test_meow_img_budget_gen.py <输出目录> +依赖:Pillow(PIL)。夹具只含结构化图案(白底/黑线/灰块),体积极小。 + +生成: + big.jpg 3200x2600 baseline 4:2:0 JPEG(8.32MP,峰值对比主用例) + bigprog.jpg 3200x2600 progressive JPEG(验证进升级解码走同一缩解路径) + page.jpg 1600x2400 baseline JPEG(finding 里 15.4MB 的漫画页原尺寸) + small.jpg 64x64 baseline JPEG(「小图不动」用例) + big.png 2400x1600 RGB PNG(stb 无解码期缩放:峰值如实记录,不作改善断言) + big.bmp 2400x1600 24bpp BMP(同上) + broken.jpg JPEG 魔数 + 垃圾字节(头部解析失败,坏图用例) +""" +import os +import sys + +from PIL import Image, ImageDraw + + +def page_pattern(w, h): + """漫画页近似:白底 + 分镜框线 + 灰阶网点带。内容稀疏 → JPEG 体积小。""" + im = Image.new("RGB", (w, h), (250, 249, 245)) + d = ImageDraw.Draw(im) + for k in range(0, min(w, h) - 400, 60): + d.rectangle([100 + k, 100 + k, 400 + k, 300 + k], outline=(12, 12, 14), width=5) + for band in range(14): + y0 = band * h // 14 + for x in range(0, w, 9): + d.line([(x, y0), (x + 4, y0 + 7)], fill=(110, 110, 116)) + return im + + +def main(): + if len(sys.argv) != 2: + print(__doc__) + return 2 + out = sys.argv[1] + os.makedirs(out, exist_ok=True) + + big = page_pattern(3200, 2600) + big.save(os.path.join(out, "big.jpg"), "JPEG", quality=55, subsampling=2) + big.save(os.path.join(out, "bigprog.jpg"), "JPEG", quality=55, subsampling=2, + progressive=True) + + small = page_pattern(64, 64) + small.save(os.path.join(out, "small.jpg"), "JPEG", quality=80, subsampling=2) + + page = page_pattern(1600, 2400) + page.save(os.path.join(out, "page.jpg"), "JPEG", quality=55, subsampling=2) + + mid = page_pattern(2400, 1600) + mid.save(os.path.join(out, "big.png"), "PNG") + mid.save(os.path.join(out, "big.bmp"), "BMP") + + # 坏图:JPEG 魔数 + 垃圾字节 → 头部解析失败(stb 对「截断但头部完整」很宽容,不能只截断) + garbage = bytes((i * 37 + 11) & 0xFF for i in range(256)) + with open(os.path.join(out, "broken.jpg"), "wb") as f: + f.write(b"\xff\xd8\xff\xe0" + garbage) + + for name in ("big.jpg", "bigprog.jpg", "page.jpg", "small.jpg", "big.png", + "big.bmp", "broken.jpg"): + print(" %-12s %8d B" % (name, os.path.getsize(os.path.join(out, name)))) + return 0 + + +if __name__ == "__main__": + sys.exit(main())