ra8-firmware 0.1.0
Bare-metal firmware for the Renesas RA8 family (RA8D2 / RA8P1)
Loading...
Searching...
No Matches
make_imgfmt_fixtures.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2# SPDX-License-Identifier: MIT
3# Copyright (c) 2026 Brighton Sikarskie
4"""Generate imgfmt_fixtures.h: deterministic BMP and GIF images for the gate.
5
6These close a coverage hole. The firmware links four stb_image formats, and
7STBI_ONLY_BMP and STBI_ONLY_GIF were the two with no example exercising them --
8PNG and JPEG already have ereader_image and ereader_jpeg. A decoder nobody
9calls is a decoder nobody knows is broken.
10
11The on-device ereader_imgfmt gate decodes both through `ra8_img_decode_blit`
12(zero-heap) into a 160x120 framebuffer and CRC-gates each independently, so a
13regression in one format cannot be masked by the other passing.
14
15Determinism is the requirement that shapes everything here: the CRCs are pinned
16in the gate, so a fixture that varied run to run would break the gate rather
17than detect anything. Output is pure 7-bit ASCII at 16 bytes per row inside a
18clang-format-off guard, so the formatter leaves it byte-identical.
19
20Usage:
21 python3 examples/ek_ra8d2/hw_validated/hil/ereader_imgfmt/scripts/make_imgfmt_fixtures.py
22"""
23
24from __future__ import annotations
25
26import io
27from pathlib import Path
28
29from PIL import Image
30
31W, H = 40, 30
32QUAD = [(0xC0, 0x10, 0x20), (0x18, 0x90, 0x30), (0x20, 0x40, 0xC0), (0xD0, 0xA0, 0x18)]
33
34
35def make_image(layout: str) -> Image.Image:
36 """Render a 40x30 RGB test pattern in one of two layouts.
37
38 The two layouts exist so BMP and GIF decode to DIFFERENT pixels and
39 therefore pin two different CRCs. Identical images would let a gate pass
40 while decoding the wrong buffer -- a copy-paste bug in the gate would be
41 invisible.
42
43 Colors come from QUAD, chosen to be far apart so GIF's 4-color adaptive
44 palette reproduces them exactly rather than approximating.
45
46 Args:
47 layout: "quad" for four quadrants, anything else for horizontal bands.
48
49 Returns:
50 A 40x30 RGB Pillow Image.
51 """
52 # Two distinct patterns so the BMP and GIF decode to different pixels and
53 # therefore pin two different CRCs (each path independently gated).
54 img = Image.new("RGB", (W, H))
55 px = img.load()
56 for y in range(H):
57 for x in range(W):
58 if layout == "quad":
59 idx = (1 if x >= W // 2 else 0) + (2 if y >= H // 2 else 0)
60 else: # horizontal bands
61 idx = min(y // (H // len(QUAD)), len(QUAD) - 1)
62 px[x, y] = QUAD[idx]
63 return img
64
65
66def encode(fmt: str) -> bytes:
67 """Render and encode the test image for one format.
68
69 Pairs each format with its own layout -- BMP gets quadrants, anything else
70 gets bands -- so the two fixtures never collide. GIF is additionally
71 converted to a 4-color adaptive palette: GIF is palette-only anyway, and
72 pinning the color count keeps the encoder from choosing a different palette
73 size and shifting the bytes.
74
75 Args:
76 fmt: A Pillow format name, in practice "BMP" or "GIF". Any other value
77 takes the bands layout and is passed to Pillow unchanged.
78
79 Returns:
80 The encoded image bytes; identical across runs for a given format.
81
82 Raises:
83 KeyError: Pillow does not know `fmt`.
84 """
85 # BMP gets the quadrant layout; GIF gets horizontal bands.
86 img = make_image("quad" if fmt == "BMP" else "bands")
87 out = io.BytesIO()
88 if fmt == "GIF":
89 # Palette mode keeps the GIF tiny and deterministic.
90 img = img.convert("P", palette=Image.ADAPTIVE, colors=4)
91 img.save(out, format=fmt)
92 return out.getvalue()
93
94
95def bake_array(name: str, data: bytes) -> str:
96 """Render bytes as a C length enum plus a `static const uint8_t` array.
97
98 The length is emitted as `enum : size_t` and used to size the array, so the
99 declared size and the data can never disagree.
100
101 The table is wrapped in `clang-format off`/`on` and laid out 16 bytes per
102 row. That keeps the formatter from reflowing it, which is what makes a
103 regenerated fixture diff empty when nothing actually changed.
104
105 Args:
106 name: Lowercase stem for the identifiers; produces `k_<name>` and
107 `k_<name>_len`, and is upper-cased for the doc comments. Used
108 unquoted, so it must already be a valid C identifier fragment.
109 data: Bytes to bake.
110
111 Returns:
112 The declarations as C source text.
113 """
114 rows = []
115 for i in range(0, len(data), 16):
116 chunk = data[i : i + 16]
117 rows.append(" " + ", ".join(f"0x{b:02X}" for b in chunk) + ",")
118 body = "\n".join(rows)
119 return (
120 f"/** @brief Length of the baked {name.upper()} image, bytes. */\n"
121 f"enum : size_t {{ k_{name}_len = {len(data)}U "
122 f"/**< {name.capitalize()} length. */ }};\n"
123 "\n"
124 f"/** @brief Baked 40x30 four-quadrant {name.upper()} image. */\n"
125 "/* Suppression rationale: generated bytes stay at 16 per row for stable diffs. */\n"
126 "/* clang-format off */\n"
127 f"static const uint8_t k_{name}[k_{name}_len] = {{\n"
128 f"{body}\n"
129 "};\n"
130 "/* clang-format on */\n"
131 )
132
133
134def main() -> int:
135 """Regenerate the owning component's inc/imgfmt_fixtures.h.
136
137 Regenerating changes the baked bytes only if Pillow's encoders change. If it
138 does, the CRCs pinned in the on-device gate no longer match and must be
139 re-pinned in the same commit -- otherwise the gate fails on a fixture that
140 is actually correct.
141 """
142 bmp = encode("BMP")
143 gif = encode("GIF")
144 header = (
145 "/**\n"
146 " * @file imgfmt_fixtures.h\n"
147 " * @brief Baked BMP + GIF test images for the ereader_imgfmt gate.\n"
148 " *\n"
149 " * @details The two stb_image formats (STBI_ONLY_BMP / STBI_ONLY_GIF) that\n"
150 " * had no example. Each is a 40x30 four-quadrant image, byte-identical run to\n"
151 " * run. Generated by examples/ek_ra8d2/hw_validated/hil/ereader_imgfmt/scripts/\n"
152 " * make_imgfmt_fixtures.py. Pure ASCII, 16 bytes per row.\n"
153 " *\n"
154 " * @copyright Copyright (c) 2026 Brighton Sikarskie\n"
155 " * SPDX-License-Identifier: MIT\n"
156 " */\n"
157 "#pragma once\n"
158 "\n"
159 "#include <stddef.h>\n"
160 "#include <stdint.h>\n"
161 "\n"
162 f"{bake_array('bmp', bmp)}"
163 "\n"
164 f"{bake_array('gif', gif)}"
165 )
166 output = Path(__file__).resolve().parent.parent / "inc" / "imgfmt_fixtures.h"
167 with output.open("w", encoding="ascii") as f:
168 f.write(header)
169 print(f"wrote {output} (bmp {len(bmp)} B, gif {len(gif)} B)")
170
171
172if __name__ == "__main__":
173 main()
void main(void)
The application entry point Reset_Handler hands control to.
Definition main.c:298
#define min(x, y)
Untyped minimum shim used by the SOUP's buffer clamping.
Definition xz_config.h:157