mirror of
https://github.com/mikecarper/MeshCore.git
synced 2026-10-20 12:41:58 +00:00
Keep Companion and Terminal Chat profiles unchanged; retain full-sensor and reduced-sensor OTA builds. Add compact crypto and OTA names, qualified display asset compression, font license packaging, and scope regression tests.
152 lines
7.0 KiB
Python
152 lines
7.0 KiB
Python
"""Lossless experimental Arial compression with the existing C tinf decoder."""
|
|
from pathlib import Path
|
|
import random
|
|
import re
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
import unittest
|
|
import zlib
|
|
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
sys.path.insert(0, str(ROOT / "scripts"))
|
|
import generate_compact_arial as generate
|
|
|
|
|
|
HEADER = ROOT / "src/helpers/ui/CompactArialFonts.h"
|
|
HARNESS = ROOT / "test/fixtures/compact_arial/test.cpp"
|
|
|
|
|
|
def unpack_rle(data):
|
|
output = bytearray()
|
|
index = 0
|
|
while index < len(data):
|
|
token = data[index]
|
|
index += 1
|
|
if token < 128:
|
|
count = token + 1
|
|
output.extend(data[index:index + count])
|
|
index += count
|
|
else:
|
|
output.extend(bytes((data[index],)) * ((token & 127) + 3))
|
|
index += 1
|
|
return bytes(output)
|
|
|
|
|
|
class CompactArialTest(unittest.TestCase):
|
|
@classmethod
|
|
def setUpClass(cls):
|
|
cls.fonts = generate.read_fonts()
|
|
cls.header = HEADER.read_text(encoding="utf-8")
|
|
|
|
def compile_and_run(self, mode, header=None, invalid=False):
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
folder = Path(directory)
|
|
if header is not None:
|
|
include = folder / "helpers/ui"
|
|
include.mkdir(parents=True)
|
|
(include / "CompactArialFonts.h").write_text(header, encoding="utf-8")
|
|
(folder / "helpers/ota/tinf").mkdir(parents=True)
|
|
(folder / "helpers/ota/tinf/tinf.h").write_text(
|
|
(ROOT / "src/helpers/ota/tinf/tinf.h").read_text(encoding="utf-8"), encoding="utf-8")
|
|
decoder = folder / "tinf.o"
|
|
subprocess.run([
|
|
"cc", "-Os", "-DMESHCORE_TINF_IMPLEMENTATION=1", "-c",
|
|
str(ROOT / "src/helpers/ota/tinf/tinflate.c"), "-o", str(decoder),
|
|
], check=True)
|
|
executable = folder / "fonts"
|
|
subprocess.run([
|
|
"c++", "-std=c++11", "-O2", "-Wall", "-Wextra", "-Werror",
|
|
f"-DMESH_ARIAL_EXPERIMENT={mode}", f"-I{folder}", f"-I{ROOT / 'src'}",
|
|
str(HARNESS), str(HARNESS.with_name("other.cpp")), str(decoder), "-o", str(executable),
|
|
], check=True)
|
|
return subprocess.run(
|
|
[str(executable)] + (["invalid"] if invalid else []),
|
|
text=True, capture_output=True, check=True).stdout.splitlines()
|
|
|
|
def test_generated_header_is_reproducible_and_omits_unused_10(self):
|
|
self.assertEqual(self.header, generate.generate_header(self.fonts))
|
|
self.assertNotIn("FONT_0", self.header)
|
|
self.assertEqual([len(font.original) for font in self.fonts], [2731, 5049, 9643])
|
|
self.assertEqual(max(len(glyph.bitmap) for font in self.fonts for glyph in font.glyphs), 91)
|
|
|
|
def test_rle_and_raw_bypass_roundtrip_every_glyph_and_metric(self):
|
|
for font in self.fonts:
|
|
packed, rows = generate.rle_font(font)
|
|
descriptor = generate.descriptor(font, rows)
|
|
self.assertEqual(descriptor[:4], font.header)
|
|
for index, (glyph, record) in enumerate(zip(font.glyphs, rows)):
|
|
location, length, width = record
|
|
self.assertEqual((length, width), (len(glyph.bitmap), glyph.width))
|
|
self.assertEqual(descriptor[6 + 4 * index:8 + 4 * index], font.original[6 + 4 * index:8 + 4 * index])
|
|
if not length:
|
|
self.assertEqual(location, 0xffff)
|
|
continue
|
|
first = location & 0x7fff
|
|
end = next((next_row[0] & 0x7fff for next_row in rows[index + 1:] if next_row[1]), len(packed))
|
|
encoded = packed[first:end]
|
|
self.assertEqual(encoded if location & 0x8000 else unpack_rle(encoded), glyph.bitmap)
|
|
|
|
def test_rle_literal_and_run_boundaries(self):
|
|
cases = [bytes(range(256)), b"x" * 130, b"x" * 131, b"x" * 260,
|
|
b"abc" + b"x" * 130 + b"def", b"", b"x", b"xx"]
|
|
randomizer = random.Random(1234)
|
|
cases += [bytes(randomizer.randrange(8) for _ in range(length)) for length in range(300)]
|
|
for data in cases:
|
|
self.assertEqual(unpack_rle(generate.rle_encode(data)), data)
|
|
|
|
def test_zopfli_chunks_roundtrip_every_glyph_with_512_byte_cap(self):
|
|
original_total = 0
|
|
compressed_total = 0
|
|
for font in self.fonts[1:]:
|
|
packed, rows, offsets, lengths = generate.chunk_font(font)
|
|
raw = [zlib.decompress(packed[offsets[index]:offsets[index + 1]], -15) for index in range(len(lengths))]
|
|
self.assertEqual(list(map(len, raw)), list(lengths))
|
|
self.assertLessEqual(max(lengths), 512)
|
|
for glyph, (location, length, width) in zip(font.glyphs, rows):
|
|
self.assertEqual((length, width), (len(glyph.bitmap), glyph.width))
|
|
if length:
|
|
self.assertEqual(raw[location >> 9][(location & 511):(location & 511) + length], glyph.bitmap)
|
|
else:
|
|
self.assertEqual(location, 0xffff)
|
|
original_total += len(font.original)
|
|
compressed_total += len(packed) + len(generate.descriptor(font, rows)) + 2 * (len(offsets) + len(lengths))
|
|
self.assertEqual(original_total - compressed_total, 7555)
|
|
|
|
def test_google_zopfli_uses_1000_iterations(self):
|
|
from unittest.mock import patch
|
|
generate.chunk_font.cache_clear()
|
|
original = generate.raw_deflate_compress
|
|
with patch.object(generate, "raw_deflate_compress", wraps=original) as compressor:
|
|
generate.chunk_font(self.fonts[0])
|
|
self.assertEqual(compressor.call_count, 4)
|
|
self.assertTrue(all(call.kwargs == {"numiterations": 1000} for call in compressor.call_args_list))
|
|
|
|
def test_cpp_both_variants_preserve_all_448_glyphs_and_metadata(self):
|
|
expected = []
|
|
for id, font in enumerate(self.fonts[1:], 1):
|
|
expected.append("F " + " ".join(map(str, (id, *font.header))))
|
|
for slot, glyph in enumerate(font.glyphs):
|
|
expected.append(f"G {id} {slot + font.header[2]} {glyph.width} {len(glyph.bitmap)} {glyph.bitmap.hex()}")
|
|
for mode in (2, 3):
|
|
with self.subTest(mode=mode):
|
|
self.assertEqual(self.compile_and_run(mode), expected)
|
|
|
|
def test_cpp_corrupt_descriptor_or_deflate_stream_fails_closed(self):
|
|
# Glyph 33 is the first bitmap; glyph32 is a blank space.
|
|
for mode in (2, 3):
|
|
bad = re.sub(
|
|
r"(static const uint8_t FONT_1_DESCRIPTOR\[\] = \{\s*"
|
|
r"(?:0x[0-9a-f]{2},\s*){8})0x[0-9a-f]{2},\s*0x[0-9a-f]{2}",
|
|
lambda match: match[1] + "0xff, 0xfe", self.header)
|
|
self.assertNotEqual(bad, self.header)
|
|
self.compile_and_run(mode, bad, invalid=True)
|
|
bad = re.sub(r"(static const uint8_t FONT_1_DATA\[\] = \{\s*)0x[0-9a-f]{2}",
|
|
lambda match: match[1] + "0x07", self.header)
|
|
self.compile_and_run(3, bad, invalid=True)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|