Files
MeowOS-For-R3P2/patch_v2/sdk/meow_png_stream.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

500 lines
19 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_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;
/* tRNS 存的是原始采样值(0..2^d-1),不是展开后的 0..255 */
o[3] = (c->color == 0 && c->have_trns && (unsigned)v == c->trns[0]) ? 0 : 255;
}
return;
}
if (d == 8) {
if (c->color == 0) { /* gray */
o[0] = o[1] = o[2] = row[x];
o[3] = (c->have_trns && row[x] == c->trns[0]) ? 0 : 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] = (c->have_trns && row[x * 3] == c->trns[0] &&
row[x * 3 + 1] == c->trns[1] && row[x * 3 + 2] == c->trns[2]) ? 0 : 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) {
/* 8bit/低位深:采样值存在低字节;16bit:与像素一样取高字节比较 */
c.trns[0] = (c.depth == 16) ? d[0] : d[1];
} else if (c.color == 2 && clen >= 6) {
if (c.depth == 16) {
c.trns[0] = d[0]; c.trns[1] = d[2]; c.trns[2] = d[4];
} else {
c.trns[0] = d[1]; c.trns[1] = d[3]; c.trns[2] = d[5];
}
}
} 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;
}