"""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 #include #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()