ra8-firmware 0.1.0
Bare-metal firmware for the Renesas RA8 family (RA8D2 / RA8P1)
Loading...
Searching...
No Matches
gen_comic_large_fixture.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 the baked oversized single-page CBZ fixture for ereader_comic (#344).
5
6Emits a pure 7-bit-ASCII C header (comic_large_fixture.h) holding one CBZ with a
7single deliberately-large grayscale PNG page. Decoded at full resolution the page
8far exceeds the app's whole-decode arena, so the reader cannot open it that way;
9it exercises the tile path (comic_tiles -> JOF atlas -> ra8_tile_cache)
10instead. The art is flat-shaded horizontal bands plus a frame and a left gutter,
11so it DEFLATEs to a few hundred bytes and the baked array stays tiny on disk
12while the decoded image is multiple megabytes.
13
14The output is deterministic (fixed art, fixed zlib level), so the tile digest the
15app prints is identical on host, ra8_emulator, and silicon.
16
17Usage:
18 python3 scripts/gen/gen_comic_large_fixture.py \
19 examples/ek_ra8d2/hw_pending/ereader_comic/inc/comic_large_fixture.h
20"""
21
22from __future__ import annotations
23
24import struct
25import sys
26import zlib
27from pathlib import Path
28
29# Oversized on purpose: 1024x1400 decodes to ~4.3 MiB RGB (or ~5.7 MiB at the
30# reader's 4 bpp footprint estimate), well past the 2 MiB whole-decode arena.
31PAGE_W = 1024
32PAGE_H = 1400
33
34PNG_BIT_DEPTH_8 = 8
35PNG_COLOR_GRAY = 0
36DEFAULT_OUTPUT = (
37 Path(__file__).resolve().parents[2]
38 / "examples"
39 / "ek_ra8d2"
40 / "hw_pending"
41 / "ereader_comic"
42 / "inc"
43 / "comic_large_fixture.h"
44)
45ZIP_LOCAL_MAGIC = 0x04034B50
46ZIP_CENTRAL_MAGIC = 0x02014B50
47ZIP_EOCD_MAGIC = 0x06054B50
48ZIP_VERSION = 20
49ZIP_METHOD_DEFLATE = 8
50DEFLATE_RAW_WBITS = -15
51ZLIB_BEST = 9
52BYTES_PER_ROW = 16
53
54BORDER = 8
55GUTTER = 48
56BAND_COUNT = 8
57BAND_BASE = 40
58BAND_STEP = 24
59GUTTER_GRAY = 20
60
61
62def gray(x: int, y: int) -> int:
63 """Deterministic, DEFLATE-friendly grayscale sample for one pixel."""
64 if x < BORDER or x >= PAGE_W - BORDER or y < BORDER or y >= PAGE_H - BORDER:
65 return 0 # black frame
66 if x < GUTTER:
67 return GUTTER_GRAY # dark left gutter
68 band = (y * BAND_COUNT) // PAGE_H
69 return min(255, BAND_BASE + BAND_STEP * band)
70
71
72def render_page() -> bytearray:
73 """Render the whole page into a flat grayscale buffer."""
74 out = bytearray(PAGE_W * PAGE_H)
75 for y in range(PAGE_H):
76 row = y * PAGE_W
77 for x in range(PAGE_W):
78 out[row + x] = gray(x, y)
79 return out
80
81
82def png_chunk(tag: bytes, data: bytes) -> bytes:
83 """Wrap a payload in one PNG chunk: length, type, data, CRC-32."""
84 out = struct.pack(">I", len(data)) + tag + data
85 crc = zlib.crc32(tag + data) & 0xFFFFFFFF
86 return out + struct.pack(">I", crc)
87
88
89def encode_png(g: bytes | bytearray) -> bytes:
90 """8-bit grayscale PNG (color type 0), one filter-0 byte per scanline."""
91 sig = b"\x89PNG\r\n\x1a\n"
92 ihdr = struct.pack(">IIBBBBB", PAGE_W, PAGE_H, PNG_BIT_DEPTH_8, PNG_COLOR_GRAY, 0, 0, 0)
93 raw = bytearray()
94 for y in range(PAGE_H):
95 raw.append(0)
96 raw += g[y * PAGE_W : (y + 1) * PAGE_W]
97 idat = zlib.compress(bytes(raw), ZLIB_BEST)
98 return sig + png_chunk(b"IHDR", ihdr) + png_chunk(b"IDAT", idat) + png_chunk(b"IEND", b"")
99
100
101def build_cbz(png: bytes) -> bytes:
102 """Minimal deterministic single-member ZIP (DEFLATE) that miniz reads."""
103 name = b"01_large.png"
104 comp = zlib.compressobj(ZLIB_BEST, zlib.DEFLATED, DEFLATE_RAW_WBITS)
105 body = comp.compress(png) + comp.flush()
106 crc = zlib.crc32(png) & 0xFFFFFFFF
107 local = (
108 struct.pack(
109 "<IHHHHHIIIHH",
110 ZIP_LOCAL_MAGIC,
111 ZIP_VERSION,
112 0,
113 ZIP_METHOD_DEFLATE,
114 0,
115 0,
116 crc,
117 len(body),
118 len(png),
119 len(name),
120 0,
121 )
122 + name
123 + body
124 )
125 central = (
126 struct.pack(
127 "<IHHHHHHIIIHHHHHII",
128 ZIP_CENTRAL_MAGIC,
129 ZIP_VERSION,
130 ZIP_VERSION,
131 0,
132 ZIP_METHOD_DEFLATE,
133 0,
134 0,
135 crc,
136 len(body),
137 len(png),
138 len(name),
139 0,
140 0,
141 0,
142 0,
143 0,
144 0,
145 )
146 + name
147 )
148 eocd = struct.pack(
149 "<IHHHHIIH",
150 ZIP_EOCD_MAGIC,
151 0,
152 0,
153 1,
154 1,
155 len(central),
156 len(local),
157 0,
158 )
159 return local + central + eocd
160
161
162def emit_header(path: str | Path, cbz: bytes) -> None:
163 """Write the generated pure-ASCII C header holding the CBZ byte array."""
164 header = (
165 "/**\n"
166 " * @file comic_large_fixture.h\n"
167 " * @brief Baked oversized single-page CBZ for the ereader_comic tile self-check\n"
168 " * (#344; generated by scripts/gen/gen_comic_large_fixture.py -- do not edit).\n"
169 " *\n"
170 f" * @details A miniz-decodable ZIP of one {PAGE_W}x{PAGE_H} grayscale PNG page whose\n"
171 " * decoded size far exceeds the reader's whole-decode arena, so it can only\n"
172 " * be opened through the JOF tile path (comic_tiles). Flat-shaded art\n"
173 " * keeps the baked array tiny while the decoded image is multiple MiB.\n"
174 " *\n"
175 " * @copyright Copyright (c) 2026 Brighton Sikarskie\n"
176 " * SPDX-License-Identifier: MIT\n"
177 " */\n"
178 "#pragma once\n\n"
179 "#include <stdint.h>\n\n"
180 f"/** @brief Baked {len(cbz)}-byte single-page oversized CBZ. */\n"
181 "static const uint8_t k_comic_large_cbz[] = {\n"
182 )
183 rows = []
184 for start in range(0, len(cbz), BYTES_PER_ROW):
185 chunk = cbz[start : start + BYTES_PER_ROW]
186 rows.append(" " + " ".join(f"0x{byte:02X}," for byte in chunk))
187 # The length is derived from the array rather than emitted as a literal,
188 # and the geometry is a typed enum: check_magic_numbers.py now reads
189 # headers too, and both spellings are what the project rule asks for.
190 footer = (
191 "\n};\n\n"
192 "/** @brief Length of ::k_comic_large_cbz in bytes. */\n"
193 "static const uint32_t k_comic_large_cbz_len = (uint32_t)sizeof(k_comic_large_cbz);\n\n"
194 "/**\n"
195 " * @brief Decoded geometry of the baked oversized page.\n"
196 " * @details These are properties of the baked bytes above, not tunables: the\n"
197 " * decoder must reproduce exactly this size or the fixture is wrong.\n"
198 " */\n"
199 "typedef enum : uint32_t {\n"
200 f" k_comic_large_w = {PAGE_W}U, /**< Decoded width in pixels. */\n"
201 f" k_comic_large_h = {PAGE_H}U, /**< Decoded height in pixels. */\n"
202 "} comic_large_geometry_t;\n"
203 )
204 Path(path).write_text(header + "\n".join(rows) + footer, encoding="ascii")
205
206
207def main() -> None:
208 """Render the oversized page, pack it into a CBZ, write the C header."""
209 out = Path(sys.argv[1]) if len(sys.argv) > 1 else DEFAULT_OUTPUT
210 cbz = build_cbz(encode_png(render_page()))
211 emit_header(out, cbz)
212 sys.stderr.write(f"wrote {out} ({len(cbz)} bytes CBZ, {PAGE_W}x{PAGE_H})\n")
213
214
215if __name__ == "__main__":
216 main()
static bool render_page(uint8_t *fb, const char *text, uint32_t len)
Render the collected page text into the SDRAM framebuffer via ra8_gfx.
Definition cpu1_main.c:370
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