P1-1(内存安全):mj_huff_build 构造 9 位快表前校验规范码空间 (code + bits[l] > (1<<l) → 表非法),填表时再夹一道 <512 边界;mj_dht 失败 即返回 0 → 后端拒绝 → 回退 stb。此前过订阅 DHT(如「长度 2 的码 100 个」)会把 fast[512] 下标推到 12799,28~220 字节的畸形 JPEG 就栈溢出(ASan 实测 stack-buffer-overflow,非 ASan 直接 SIGSEGV/SIGBUS)。 回归:码长 1..4 × 符号数 3..255 共 1012 组畸形 DHT 全部后端拒绝、无崩溃; 「有效 JPEG + 注入坏 DHT」走拒绝 + 回退(不花屏)。 P1-2(能力):三处扫描循环的区间重同步统一到 mj_boundary,**只在 total == rst (编码器区间长度)时重同步**。原实现一见「over && marker∈D0..D7」就重同步,但熵码 位缓冲有最多 9 位前瞻,over 会在区间真正结束前 1~2 个 MCU 置位——提前重同步会丢掉 bitbuf 里本区间剩余块的有效比特,随即失步(渐进式 DC 扫描每块仅 ~1 bit,前瞻能跨过 整个 MCU)。这正是 Pillow restart_marker_blocks=1/2/3/4/5/8/16/64/200 的渐进式 100% 回退 stb(25.8MiB)的根因。另修 mj_restart 裸扫把 FF 00(数据字节 0xFF 的填充) 里的 00 误当 marker。 回归:9 个渐进式 + restart 夹具全部 BACKEND=1、峰值 1.86MiB(与无 restart 相同)、 与同编码无 restart 文件逐字节一致(mad=0.000);baseline + restart 仍逐字节一致。 附带(评审 P2 两项,改动极小且已验证): - PNG 8bit gray/RGB 的 tRNS 透明接通(8bit 采样值在 tRNS 低字节),与 stb 逐点 alpha 一致; - 拒绝 16bit 量化表(只属 12bit 精度;收下会让 F*qt 在 mj_accim 里 int32 溢出,UB)。 - meow_img_load_file_step 的 JPEG 走真缩解后端(PNG/BMP 暂留 stb:阅读器预算模型仍是 M22「整幅物化」口径,待 M27 同步后放开)。 验证:ASan 构建全 PASS;test_meow_img_stream 111 项全 PASS;test_meow_img_budget / test_meow_img_color 全 PASS;tuku run_tests.sh rc=0(zip 106 / cbz 296,含 membudget)、 run_webtoon_tests.sh rc=0;build_sdk.sh 零警告,MIPS32 rel2 目标码。 峰值不变(修复不增加分配):2000x3000 baseline/progressive 1.86MiB、PNG 1.33MiB。 体积:meow_jpeg.o 17,284B + meow_png_stream.o 8,456B = 25.1KB。
297 lines
12 KiB
C
297 lines
12 KiB
C
/*
|
||
* 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 反推 —— 内存预算紧时可选更粗的档位。 */
|
||
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);
|
||
/* 追加(M23):真缩解后端优先(本入口只接 JPEG)。step 是「输出别比这个档位更细」的提示:
|
||
* 折算成目标长边 ceil(长边/step) 传给后端(后端按该长边自动选 DCT 阶),与下面
|
||
* stbi_load_scaled(step) 的语义一致,峰值只与输出尺寸相关;maxdim<=0 时不精修,
|
||
* 结果仍是档位尺寸。后端不支持(CMYK/算术编码/交错…)时回退 stb,行为与 M22 一致。
|
||
*
|
||
* PNG/BMP 在本入口暂不接后端:阅读器的单次解码峰值**预算模型**还是 M22 的「整幅物化」
|
||
* 口径,实测峰值骤降会让它的 model≤2×measured 自检失真(详见 M23 r1 报告的 M27 待办)。
|
||
* 待 M27 同步模型后,把这里的 JPEG 判定去掉即可。 */
|
||
{
|
||
unsigned char sig[3];
|
||
int is_jpeg = 0;
|
||
FILE *fp = fopen(path, "rb");
|
||
if (fp) {
|
||
if (fread(sig, 1, 3, fp) == 3 &&
|
||
sig[0] == 0xFF && sig[1] == 0xD8 && sig[2] == 0xFF)
|
||
is_jpeg = 1;
|
||
fclose(fp);
|
||
}
|
||
if (is_jpeg) {
|
||
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)
|
||
return img_finish_dim(bp, bw, bh, maxdim > 0 ? maxdim : longside);
|
||
}
|
||
}
|
||
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);
|
||
/* 追加(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);
|
||
}
|