Files
HaloKeymind/scripts/generate_compact_noto.py
T
mikecarper 42d6bfd560 Reduce nRF52 infrastructure flash usage without trimming sensors
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.
2026-10-02 15:52:23 -07:00

155 lines
7.0 KiB
Python

"""Losslessly compress the selected Noto Condensed production bitmap fonts.
The checked-in, licensed mode7 assets in FontFamilyTrials.h are authoritative;
regeneration needs Google Zopfli but no Pillow, FreeType or installed font file.
Every glyph's pixels, advance width, blank slot and font metric are preserved.
Normal firmware builds use the checked-in header without running this script.
"""
from __future__ import annotations
import argparse
import hashlib
from pathlib import Path
import re
from generate_compact_arial import (
Font, Glyph, HEADER_END as ARIAL_DECODER, ROOT, array, chunk_font, descriptor,
)
SOURCE = ROOT / "src/helpers/ui/FontFamilyTrials.h"
OUTPUT = ROOT / "src/helpers/ui/CompactNotoFonts.h"
def read_fonts(path: Path = SOURCE) -> tuple[tuple[Font, ...], str]:
source = path.read_text(encoding="utf-8")
match = re.search(r"#if MESH_ARIAL_EXPERIMENT == 7\s*(.*?)\n#endif", source, re.S)
if not match:
raise ValueError("Authoritative raw Noto Condensed mode7 assets are missing")
body = match[1]
fonts = []
for id, encoded in re.findall(r"static const uint8_t TRIAL_([12])\[\] = \{(.*?)\};", body, re.S):
data = bytes(int(value, 16) for value in re.findall(r"0x([0-9a-f]{2})", encoded))
index = int(id)
expected_header = bytes((16, 19, 32, 224)) if index == 1 else bytes((24, 28, 32, 224))
if data[:4] != expected_header:
raise ValueError("Production Noto dimensions/character coverage changed")
base = 4 + 4 * data[3]
if len(data) < base:
raise ValueError("Incomplete raw Noto character table")
glyphs = []
consumed = 0
for slot in range(data[3]):
record = data[4 + 4 * slot:8 + 4 * slot]
offset = int.from_bytes(record[:2], "big")
length, width = record[2:]
if offset == 0xffff:
if length:
raise ValueError("A blank Noto slot contains bitmap bytes")
bitmap = b""
else:
if not length or offset != consumed or base + offset + length > len(data):
raise ValueError("Invalid or non-contiguous raw Noto glyph")
bitmap = data[base + offset:base + offset + length]
consumed += length
glyphs.append(Glyph(width, bitmap))
if base + consumed != len(data) or sum(bool(glyph.bitmap) for glyph in glyphs) != 189:
raise ValueError("Raw Noto glyph coverage/trailing bytes changed")
fonts.append(Font(f"MeshFontProduction9_{16 if index == 1 else 24}", data, tuple(glyphs)))
if len(fonts) != 2 or fonts[0].header[0] != 16 or fonts[1].header[0] != 24:
raise ValueError("Expected ordered Noto 16/24 assets")
license = re.search(r"/\*(.*?)\*/", body, re.S)
if not license or "SIL OPEN FONT LICENSE Version 1.1" not in license[1]:
raise ValueError("Required source Noto copyright/OFL notice is missing")
return tuple(fonts), license[1].strip()
def chunk_decoder() -> str:
"""Reuse the tested Arial codec's exact C++ DEFLATE branch and pointer API.
Extract at generation time, not with firmware macros: production contains
neither the RLE decoder nor an experimental-mode dependency. Tests compile
the result in isolation and check every authoritative raw Noto glyph.
"""
api = ARIAL_DECODER[:ARIAL_DECODER.index("// Writes only to caller scratch.")]
start = ARIAL_DECODER.index("inline Glyph glyph(")
common = ARIAL_DECODER[start:ARIAL_DECODER.index("#if MESH_ARIAL_EXPERIMENT == 2", start)]
deflate = ARIAL_DECODER.split("#elif MESH_ARIAL_EXPERIMENT == 3\n", 1)[1].split("#endif\n", 1)[0]
return (api + "// Caller-owned 512-byte scratch; no cache, heap or mutable global state.\n"
+ common + deflate + "}\n}}} // namespace mesh::ui::noto")
HEADER_START = '''// Generated by scripts/generate_compact_noto.py; do not edit.
// MeshFontProduction9: lossless storage of the licensed MeshFontTrial7 bitmaps.
// All 224 slots/189 drawable glyphs preserved at 12/17 px H cap height;
// H tops 3/5 px, line boxes 19/28 px. The unused initial Arial10 is not stored.
// Google Zopfli 1000; glyph-aligned chunks decode to at most 512 bytes.
// Source rasterizer: Pillow 12.3.0 / FreeType 2.14.3, threshold 128.
// Nominal source sizes 16/24 px, baselines 15/22 px; existing mode7 placement.
// Normal builds need no compressor, rasterizer or source TrueType font.
#pragma once
#include <stddef.h>
#include <stdint.h>
#include "../ota/tinf/tinf.h"
namespace mesh { namespace ui { namespace noto {
static constexpr size_t kScratchSize = 512;
struct Font {
const uint8_t* descriptor;
const uint8_t* data;
const uint16_t* offsets;
const uint16_t* rawLengths;
uint16_t chunkCount;
uint16_t dataLength;
};
struct Glyph {
const uint8_t* data;
uint8_t width;
uint8_t length;
bool drawable;
bool valid;
};'''
def generate_header(fonts: tuple[Font, ...], license: str) -> str:
output = [HEADER_START, "/*\nCompression derivative named MeshFontProduction9.\n"
"The following notice identifies the upstream rasterized font source.\n"
+ "\n".join(line.rstrip() for line in license.splitlines()) + "\n*/"]
for id, font in enumerate(fonts, 1):
packed, rows, offsets, lengths = chunk_font(font)
prefix = f"FONT_{id}"
output.append(f"// Authoritative raw bitmap SHA256: {hashlib.sha256(font.original).hexdigest()}")
output.append(f"inline const Font& fontStorage{id}() {{")
output.append(array(prefix + "_DATA", packed))
output.append(array(prefix + "_DESCRIPTOR", descriptor(font, rows)))
output.append(array(prefix + "_OFFSETS", offsets, "uint16_t"))
output.append(array(prefix + "_LENGTHS", lengths, "uint16_t"))
output.append(f"static const Font {prefix} = {{ {prefix}_DESCRIPTOR, {prefix}_DATA, "
f"{prefix}_OFFSETS, {prefix}_LENGTHS, {len(lengths)}, {len(packed)} }};")
output.append(f"return {prefix};\n}}")
output.append(chunk_decoder())
return "\n\n".join(output) + "\n"
def main() -> None:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--check", action="store_true", help="Verify the generated header without writing")
args = parser.parse_args()
fonts, license = read_fonts()
header = generate_header(fonts, license)
if args.check:
if not OUTPUT.exists() or OUTPUT.read_text(encoding="utf-8") != header:
raise SystemExit("CompactNotoFonts.h is stale; regenerate the production font assets")
else:
OUTPUT.write_text(header, encoding="utf-8", newline="\n")
total = 0
for font in fonts:
packed, records, offsets, lengths = chunk_font(font)
size = len(packed) + len(descriptor(font, records)) + 2 * (len(offsets) + len(lengths))
total += size
print(f"{font.name}: raw={len(font.original)}, compressed+metadata={size}, chunks={len(lengths)}")
print(f"Combined storage={total} bytes (runtime accessor code excluded)")
if __name__ == "__main__":
main()