/* * meow_ui_img.c — 图片层(B1.2)。基于 stb_image(单头文件、静态编入), * 解 PNG/JPEG/BMP → RGBA,统一最近邻缩放/居中裁方。 */ #include "meow_ui.h" #include "meow_ui_internal.h" #include "stb_image.h" #include #include #include struct meow_img { int w, h; unsigned char *rgba; /* 4 通道 straight alpha */ }; static meow_img *img_alloc(int w, int h, unsigned char *rgba) { if (w <= 0 || h <= 0 || !rgba) { if (rgba) stbi_image_free(rgba); return NULL; } meow_img *im = (meow_img *)malloc(sizeof *im); if (!im) { stbi_image_free(rgba); return NULL; } im->w = w; im->h = h; im->rgba = rgba; return im; } meow_img *meow_img_load_mem(const void *buf, int len) { if (!buf || len <= 0) return NULL; int w = 0, h = 0, c = 0; unsigned char *p = stbi_load_from_memory((const unsigned char *)buf, len, &w, &h, &c, 4); if (!p) return NULL; return img_alloc(w, h, p); } meow_img *meow_img_load_file(const char *path) { if (!path) return NULL; int w = 0, h = 0, c = 0; unsigned char *p = stbi_load(path, &w, &h, &c, 4); if (!p) return NULL; return img_alloc(w, h, p); } /* 追加(M12):预算解码。选整数缩解档位 s ∈ {1,2,4,8}——取满足「长边/s ≥ maxdim」的最大档, * 保证缩解结果不欠采样(不丢细节),再由 meow_img_downscale 精修到 maxdim。长边 < 2*maxdim * 时无档可用(s=1),退回整幅解码后精修——此时本就接近目标尺寸,无需牺牲。 */ int meow_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, meow_img_pick_scale(longside, maxdim)); return img_finish_dim(p, w, h, maxdim); } /* 追加(M22):档位由调用方指定。与 meow_img_load_file_dim 同语义(结果长边 ≤ maxdim, * 超了再精修一次),只是档位不再由 maxdim 反推 —— 内存预算紧时可选更粗的档位。 */ meow_img *meow_img_load_file_step(const char *path, int maxdim, int step) { if (!path) return NULL; if (step <= 1 && maxdim <= 0) return meow_img_load_file(path); if (step <= 0) step = 1; if (step > 8) step = 8; int w = 0, h = 0, c = 0; if (!stbi_info(path, &w, &h, &c)) return NULL; int longside = w > h ? w : h; if (step == 1 && maxdim <= 0) return meow_img_load_file(path); unsigned char *p = stbi_load_scaled(path, &w, &h, &c, 4, step); /* maxdim<=0:不精修(保留档位输出的原始尺寸)。 */ return img_finish_dim(p, w, h, maxdim > 0 ? maxdim : longside); } /* 追加(M22):精确重采样到目标尺寸(最近邻,双向)。阅读器把缩解档位的结果一次性 * 落到显示框,之后命中页就是单次原生 blit(M17 的热路径纪律不被粗档位破坏)。 */ static unsigned char *scale_buf(const meow_img *im, int dw, int dh, int cover); meow_img *meow_img_resample(meow_img *im, int w, int h) { if (!im || w <= 0 || h <= 0) return NULL; if (im->w == w && im->h == h) return NULL; unsigned char *tmp = scale_buf(im, w, h, 0); if (!tmp) return NULL; return img_alloc(w, h, tmp); } 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, meow_img_pick_scale(longside, maxdim)); return img_finish_dim(p, w, h, maxdim); } void meow_img_free(meow_img *im) { if (!im) return; if (im->rgba) stbi_image_free(im->rgba); free(im); } int meow_img_w(const meow_img *im) { return im ? im->w : 0; } int meow_img_h(const meow_img *im) { return im ? im->h : 0; } /* 访问器:取 RGBA 像素指针(meow_img 对外 opaque,本函数为唯一像素读入口;供取色/直读像素)。 * NULL 返回 NULL。 */ const unsigned char *meow_img_pixels(const meow_img *im) { return im ? im->rgba : NULL; } /* 最近邻采样缩放(cover=1 居中裁方),返回 dw*dh*4 临时 RGBA 缓冲 */ static unsigned char *scale_buf(const meow_img *im, int dw, int dh, int cover) { if (!im || dw <= 0 || dh <= 0) return NULL; unsigned char *out = (unsigned char *)malloc((size_t)dw * dh * 4); if (!out) return NULL; float s = 1.0f; if (cover) { /* 真正铺满(cover):每目标像素对应 s 个源像素,s = min(W/dw, H/dh)——缩放后图像 * 宽/高两维都不小于目标(可能外溢),居中裁方。居中裁剪偏移(源像素)= (W-dw*s)/2 * 恒 ≥ 0。曾误作 `s = max(sx,sy)` 且采样 `(dx+ox)/s`(除以缩放比):把整块输出映射到 * 源左上角 ~dw/s 像素的透明角——图标(透明角 logo)整块透明、不绘制,即 * 「icon: ... OK 但渲染不出(只显首字母占位)」根因。正确:源索引 = dx*s + 裁剪偏移 * (与下方非 cover 分支 `dx*im->w/dw` 同一乘向)。 */ float sx = (float)im->w / dw, sy = (float)im->h / dh; s = sx < sy ? sx : sy; /* 源像素/目标像素(铺满 → 取小) */ float oxs = ((float)im->w - (float)dw * s) * 0.5f; /* 居中裁剪起点(源像素,≥0) */ float oys = ((float)im->h - (float)dh * s) * 0.5f; if (oxs < 0) oxs = 0; if (oys < 0) oys = 0; for (int dy = 0; dy < dh; dy++) { int syi = (int)((float)dy * s + oys); if (syi < 0) syi = 0; if (syi >= im->h) syi = im->h - 1; for (int dx = 0; dx < dw; dx++) { int sxi = (int)((float)dx * s + oxs); if (sxi < 0) sxi = 0; if (sxi >= im->w) sxi = im->w - 1; memcpy(out + ((size_t)dy * dw + dx) * 4, im->rgba + ((size_t)syi * im->w + sxi) * 4, 4); } } } else { for (int dy = 0; dy < dh; dy++) { int syi = (int)((float)dy * im->h / dh); if (syi >= im->h) syi = im->h - 1; for (int dx = 0; dx < dw; dx++) { int sxi = (int)((float)dx * im->w / dw); if (sxi >= im->w) sxi = im->w - 1; memcpy(out + ((size_t)dy * dw + dx) * 4, im->rgba + ((size_t)syi * im->w + sxi) * 4, 4); } } } return out; } /* 内存瘦身:把大图按最近邻等比缩放到「最长边 ≤ maxdim」,返回新图;调用方负责 * meow_img_free 旧图 + 新图。原图已 ≤maxdim → 返回 NULL(无需缩放)。 * 典型:网易云全尺寸封面 1448×1448 RGBA=8MB,显示只需 ≤480px(屏幕 480 宽)→ * 480×480=0.9MB,省 ~7MB anon(设备 RAM 仅 56MB,OOM 真机被杀)。 * 复用 scale_buf(cover=0,最近邻锁比例),不做插值(App 封面/背景无需高质量缩放)。 */ meow_img *meow_img_downscale(meow_img *im, int maxdim) { if (!im || maxdim <= 0) return NULL; if (im->w <= maxdim && im->h <= maxdim) return NULL; /* 已够小,不缩放 */ int dw, dh; if (im->w >= im->h) { dw = maxdim; dh = (int)((long)im->h * maxdim / im->w); } else { dh = maxdim; dw = (int)((long)im->w * maxdim / im->h); } if (dw < 1) dw = 1; if (dh < 1) dh = 1; unsigned char *tmp = scale_buf(im, dw, dh, 0); if (!tmp) return NULL; return img_alloc(dw, dh, tmp); } void meow_ui_img(int x, int y, meow_img *im) { if (!im) return; meow_gfx_blit_rgba(x, y, im->w, im->h, im->rgba, 255); } void meow_ui_img_scaled(int x, int y, int w, int h, meow_img *im) { if (!im) return; unsigned char *tmp = scale_buf(im, w, h, 0); if (!tmp) return; meow_gfx_blit_rgba(x, y, w, h, tmp, 255); free(tmp); } void meow_ui_img_alpha(int x, int y, int w, int h, meow_img *im, int alpha) { if (!im) return; unsigned char *tmp = scale_buf(im, w, h, 0); if (!tmp) return; meow_gfx_blit_rgba(x, y, w, h, tmp, alpha); free(tmp); } void meow_ui_img_cover(int x, int y, int size, meow_img *im) { if (!im) return; unsigned char *tmp = scale_buf(im, size, size, 1); if (!tmp) return; meow_gfx_blit_rgba(x, y, size, size, tmp, 255); free(tmp); }