"""LVGL v9 RLE codec (LV_IMAGE_COMPRESS_RLE). Control byte grammar (upstream `lv_rle_decompress`): high bit CLEAR (0x00..0x7F) : repeat the next single `blk`-byte block `ctrl` times high bit SET (0x80|count) : copy `count` blocks verbatim (literal run) count is 1..=127 For palette formats (I8) the stream decodes to 256*4 palette bytes followed by w*h palette indices. """ import struct from PIL import Image MAGIC = 0x19 FLAG_COMPRESSED = 0x08 METHOD_NONE, METHOD_RLE, METHOD_LZ4 = 0, 1, 2 def rle_decompress(data, blk, out_len=None): out = bytearray() i, n = 0, len(data) while i < n: ctrl = data[i]; i += 1 if ctrl & 0x80: cnt = ctrl & 0x7F nb = cnt * blk if i + nb > n: raise ValueError('truncated literal run') out += data[i:i + nb] i += nb else: cnt = ctrl if i + blk > n: raise ValueError('truncated repeat unit') unit = data[i:i + blk] i += blk out += unit * cnt if out_len is not None and len(out) >= out_len: break return bytes(out) def rle_compress(raw, max_run=127): """LVGL RLE 编码器(rle_decompress 的逆向,blk=1)。 贪心:先找同字节游程(>=2 用「重复」指令),否则攒「原样」指令。 输出长度总是 <= 原图像素流,因为最坏情况每条 literal 只多 1 个控制字节。 """ out = bytearray() i, n = 0, len(raw) while i < n: v = raw[i] j = i + 1 while j < n and raw[j] == v and j - i < max_run: j += 1 run = j - i if run >= 2: out.append(run); out.append(v); i = j continue lit = bytearray() k = i while k < n and len(lit) < max_run: if k + 1 < n and raw[k] == raw[k + 1]: break lit.append(raw[k]); k += 1 if not lit: # 理论上到不了这里 lit.append(v); k = i + 1 out.append(0x80 | len(lit)); out += lit i = k return bytes(out) def pad_rle(comp, want): """把 RLE 流补到 want 字节:用 ctrl=0(重复 0 次 = 产出 0 像素)的 2 字节空指令。 ctrl=0 是合法指令(`len = 0`,两个越界判断都不触发),所以设备端解码器读完 csize 字节既不会越界、也不会返回 INVALID —— 这是「等长覆盖」能成立的关键。 差值为奇数时先把一条 literal 拆成两条(payload 不变、多 1 个控制字节)。 """ d = want - len(comp) if d < 0: return None if d % 2: b = bytearray(comp) for idx in range(len(b) - 1, -1, -1): if b[idx] & 0x80 and (b[idx] & 0x7F) >= 2: cnt = b[idx] & 0x7F lit = bytes(b[idx + 1: idx + 1 + cnt]) half = cnt // 2 b[idx: idx + 1 + cnt] = (bytes([0x80 | half]) + lit[:half] + bytes([0x80 | (cnt - half)]) + lit[half:]) break else: return None comp = bytes(b) d -= 1 return comp + b'\x00\x00' * (d // 2) def parse_header(blob): if len(blob) < 12 or blob[0] != MAGIC: return None magic, cf, flags, w, h, stride, rsv = struct.unpack('