Files
MeowOS-For-R3P2/patch_v2/sdk/meow_ui_img.c
T
AskaEth ee5796747d feat(sdk): M26 公开仓真缩解图像栈并入主仓 — JPEG DCT/PNG 逐行/BMP 跳行后端 + RGB565 页图
主仓 SDK 的图片层原本是 220 行 stb 直通(整幅解码后重采样)。公开仓 MeowOS-SDK
的同一子系统已被真实 App(tuku 阅读器)驱动并做过峰值实测与像素比对,故以公开仓
为权威方向并入主仓 meow_ui.a,两个方向的新功能都保留(逐文件 diff 确认公开仓是
严格超集:无一处「双方互有增减」)。

- 新增 meow_jpeg.c / meow_jpeg.h:DCT 域缩解 1/8、1/4、1/2(N=1/2/4 点 IDCT),
  渐进式只解到首个精细化扫描;不支持项(CMYK/YCCK、算术编码、过订阅 DHT、16bit
  量化表)返回 0 由调用方回退 stb
- 新增 meow_png_stream.c:libz 逐行 inflate + 反滤波,按步长丢行丢列直写目标尺寸;
  BMP 按目标行 seek + 行内跳列(8/16/24/32bpp)
- meow_ui_img.c + include/meow_ui_image.h:新增 meow_img_pick_scale /
  load_file_step / load_file_step_ex / resample 四个公开入口;_dim 与 _mem_dim
  先进后端,失败/不支持才回退 stb(对外语义不变)
- include/meow_ui_draw.h + meow_ui_gfx.c:M32 RGB565 页图 5 函数
  (meow_gfx_rgb565 / rgba_to_rgb565 / blit_rgb565 / _scaled)。gfx 保留主仓的
  `#include "../meow_launcherd/meow_font.h"` 与 <time.h>(公开仓自 vendored
  meow_font.h 且无 <time.h>,机械覆盖会丢这两行)
- meow_jpeg.h 保持内部:不进 meow_ui.h 伞头,include/ 下无任何头暴露 meow_jpeg_*
- build_sdk.sh:OBJS 插入 meow_jpeg / meow_png_stream(32→34);成员数
  76→78(+meow_font.o +43 freetype),--verify 全为 ELF32 MIPS32 rel2/o32
- 测试:新增 test_meow_img_stream.c(S1-S16),test_meow_img_budget.c 同步公开仓
  版(B9 断言收紧为 p_new <= p_old);22 个既有宿主测试的编译注释行补
  meow_jpeg.c / meow_png_stream.c / -lz(meow_ui_img.c 现引用后端符号,不加即
  undefined reference)

宿主实测:S1-S16 全绿(S7 与 stb 网格像素一致:JPEG mad 4.35-13.69、PNG/BMP
mad 0.00;S9 回退矩阵 CMYK/1bpp BMP/Adam7 均拒绝且 stb 仍成功),B1-B9 全绿
(B9 PNG 峰值 25.65→0.61 MiB、BMP 14.65→0.59 MiB)。回归 test_meow_ui_widgets /
scroll / ime_fix / test_umbrella 全过。zlib 在应用链接期解析:meow_ui.a 只留
inflate* 未定义符号,四个 App 构建脚本原本就带 $SYSROOT/lib/libz.a,静态
linkcheck(meow_ui.a + libz.a -lpthread)通过。

注:主仓 patch_v2/reader_app 仍自带 vendored stb、不用 meow_img_*,本单不改变
阅读器的内存占用;RGB565 页图接入 reader_app 是另一个任务。
2026-11-06 20:44:22 +08:00

304 lines
13 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* 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>
#include <string.h>
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); /* 不限尺寸 = 旧入口 */
/* 追加(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;
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 反推 —— 内存预算紧时可选更粗的档位。
* M27:真缩解后端优先(step 折算成目标长边 ceil(长边/step),后端按该长边选 DCT 阶,
* 峰值只与输出尺寸相关);后端不支持时回退 stbi_load_scaled(step),语义与 M22 一致。
*
* M27 r1(P1-1):allow_stb_fallback=0 时只尝试后端,**绝不回退 stb**——阅读器的门控按
* 「后端峰值」放行时必须走这里,否则「头部说支持、解码中途失败」会静默落到 stb,
* 实测可放出 18–36MB(截断 12MP JPEG / 头-only BMP / 非法位深 PNG),即 M22 的 SIGKILL 场景。
* 后端失败返回 NULL(调用方按坏图占位)。 */
meow_img *meow_img_load_file_step_ex(const char *path, int maxdim, int step,
int allow_stb_fallback, int *used_backend)
{
if (used_backend) *used_backend = 0;
if (!path) return NULL;
if (step <= 1 && maxdim <= 0)
return allow_stb_fallback ? meow_img_load_file(path) : NULL;
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 allow_stb_fallback ? meow_img_load_file(path) : NULL;
/* 追加(M23/M27):真缩解后端优先(JPEG/PNG/BMP 全部格式)。step 是「输出别比这个档位
* 更细」的提示:折算成目标长边 ceil(长边/step) 传给后端(后端按该长边自动选 DCT 阶),
* 与下面 stbi_load_scaled(step) 的语义一致,峰值只与输出尺寸相关;maxdim<=0 时不精修,
* 结果仍是档位尺寸。后端不支持(CMYK/算术编码/交错/RLE…)时回退 stb,行为与 M22 一致。
* M27 起阅读器的峰值模型已与后端同口径(page_source 的 reader_estimate_peak),
* 所以 PNG/BMP 也一并接后端——这正是「修好了却用不上」的最后一处缝。 */
{
int eff = maxdim > 0 ? maxdim : longside;
int step_cap = (longside + step - 1) / step;
unsigned char *bp = NULL;
int bw = 0, bh = 0;
if (step_cap > 0 && step_cap < eff) eff = step_cap;
if (eff > 0 && meow_img_backend_file(path, eff, &bw, &bh, &bp) && bp) {
if (used_backend) *used_backend = 1;
return img_finish_dim(bp, bw, bh, maxdim > 0 ? maxdim : longside);
}
}
if (!allow_stb_fallback) return NULL; /* P1-1:门控按后端放行的页,失败即坏图 */
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);
}
meow_img *meow_img_load_file_step(const char *path, int maxdim, int step)
{
/* 通用入口保持 M22 语义:后端失败允许回退 stb。 */
return meow_img_load_file_step_ex(path, maxdim, step, 1, NULL);
}
/* 追加(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);
/* 追加(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;
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);
}