LVGLImage.py: # RLE encoder can emit an invalid stream (control byte `0x80` = literal run of 0 blocks)
LVGL version
v9.5
Platform
Host-side image converter (LVGLImage.py), Python 3.x, Windows 11.
Reproduced both through the CLI (--ofmt BIN --cf ARGB8888) and by calling RLEImage.rle_compress() directly.
What happened?
The encoder sometimes emits the control byte 0x80 ("literal run of 0 blocks") and then still appends 128 blocks of payload, so the control byte and the payload disagree. The stream is not self-consistent: LVGL's own lv_rle_decompress() rejects it (returns 0), and some hardware RLE decoders get stuck on it.
Root cause — get_nonrepeat_count() clamps nonrepeat_count on the "threshold reached" path but not on the "end of data" path:
while True:
value = data[index:index + blksize]
if value == pre_value:
repeat_cnt += 1
if repeat_cnt > threshold:
# repeat found.
break
else:
pre_value = value
nonrepeat_count += 1 + repeat_cnt
repeat_cnt = 0
if nonrepeat_count >= 127: # <-- clamped only here
nonrepeat_count = 127
break
index += blksize # move to next position
if index >= len(data): # data end
nonrepeat_count += repeat_cnt # <-- can exceed 127 here
break
return nonrepeat_countOn the end-of-data path the count can become 126 + repeat_cnt (up to ~142). The caller then does:
ctrl_byte = uint8_t(nonrepeat_cnt | 0x80) # 128 | 0x80 -> 0x80 (truncated to 7 bits)
compressed_data.append(ctrl_byte)
compressed_data.append(memview[index:index + nonrepeat_cnt * blksize]) # still 128 blocksi.e. control byte says 0 blocks, payload carries 128 blocks.
How to reproduce?
How to reproduce
Minimal case (512 bytes of input), attached as repro_minimal.py:
import sys; sys.path.insert(0, '<lvgl>/scripts')
from LVGLImage import RLEImage, ColorFormat
BLK, TAIL = 4, bytes([0x11, 0x22, 0x33, 0xFF])
data = bytearray()
for i in range(125): # 125 pairwise different pixels
data += bytes([i, (i * 3) & 0xFF, (i * 5) & 0xFF, 0xFF])
data += TAIL * 3 # 3 identical pixels at the very end
img = RLEImage(cf=ColorFormat.ARGB8888, w=128, h=1, data=bytes(data))
print(img.rle_compress(data, BLK)[:8].hex(" "))Observed:
80 00 00 00 ff 01 03 05 <- first control byte 0x80 = literal run of 0 blocks
stream size: 513 B (1 control byte + 512 B payload)
decoder walk: INPUT OVERRUN: ctrl 0xFF at 16 claims 508 byte(s), 496 leftExpected: ff (literal run of 127 blocks) followed by one more control byte for the remaining block.
Real-world sample: img_282.png (234x173 ARGB8888, converted with the official tool) contains the same defect at stream offset 161059 (80 00 00 00 ff ...); lv_rle_decompress() returns 0 for it. The converted .bin is attached as img_282.bin.
Suggested fix
Clamp on every exit path, e.g.:
- return nonrepeat_count
+ return min(nonrepeat_count, 127)The bitstream format is unchanged and previously valid streams stay byte-identical.
Attached files
LVGLImage_fixed.py— patched copy ofscripts/LVGLImage.py(v9.5) containing only the single-hunk fix above; it can also be used as a drop-in replacement for testing. Verified before posting: convertingimg_282.pngbelow with it produces a stream that LVGL's ownlv_rle_decompress()decodes completely (161928/161928B, previously0) and that matches the source PNG pixel for pixel (0 differing pixels).repro_minimal.py— self-contained minimal reproducer, printsBUG REPRODUCEDwith the unpatched script andno anomaly in this runwith the patched one. Pass the LVGLscriptsfolder through theLVGL_SCRIPTSenvironment variable.img_282.png— real-world sample whose conversion triggers the defect (0x80at stream offset 161059); enough to regenerate the bad.binwith either script.
Source: lvgl/lvgl