ra8-firmware 0.1.0
Bare-metal firmware for the Renesas RA8 family (RA8D2 / RA8P1)
Loading...
Searching...
No Matches
gen_comic_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 multi-page CBZ fixture for the viewable ereader_comic demo.
5
6Emits a pure 7-bit-ASCII C header (comic_pages_fixture.h) holding one CBZ
7(a miniz-decodable ZIP of grayscale PNG pages) as a byte array. The pages are
8near-panel-size (aspect ~ 1024:600) and each is visually distinct -- a unique
9paper tint, a different comic panel layout, and a huge 7-segment page number --
10so a page turn on the panel is unmistakable. Grayscale PNGs of flat-shaded art
11DEFLATE to a few hundred bytes each, so the whole archive stays small enough to
12bake as an ASCII array under the repo's per-file line cap.
13
14The output is deterministic (fixed art, fixed zlib level), so the render hash
15the app prints is identical on host, ra8_emulator, and silicon.
16
17Usage:
18 python3 scripts/gen/gen_comic_fixture.py \
19 examples/ek_ra8d2/hw_pending/ereader_comic/inc/comic_pages_fixture.h
20"""
21
22from __future__ import annotations
23
24import struct
25import sys
26import zlib
27from pathlib import Path
28
29PAGE_W = 480
30PAGE_H = 240
31PAGE_COUNT = 5
32DEFAULT_OUTPUT = (
33 Path(__file__).resolve().parents[2]
34 / "examples"
35 / "ek_ra8d2"
36 / "hw_pending"
37 / "ereader_comic"
38 / "inc"
39 / "comic_pages_fixture.h"
40)
41
42# Grayscale palette (0 = black ink, 255 = white paper).
43INK = 0
44
45# Per-page paper tint + panel-fill grays, chosen far apart so the page turn is
46# visually obvious and every page framebuffer hashes differently.
47PAGE_PAPER = [244, 232, 250, 224, 238]
48PAGE_FILL = [200, 150, 120, 175, 95]
49
50# PNG / ZIP magic constants (named so the comparisons are self-documenting).
51PNG_BIT_DEPTH_8 = 8
52PNG_COLOR_GRAY = 0
53ZIP_LOCAL_MAGIC = 0x04034B50
54ZIP_CENTRAL_MAGIC = 0x02014B50
55ZIP_EOCD_MAGIC = 0x06054B50
56ZIP_VERSION = 20
57ZIP_METHOD_DEFLATE = 8
58DEFLATE_RAW_WBITS = -15
59ZLIB_BEST = 9
60BYTES_PER_ROW = 16
61
62# 7-segment layouts: single-vertical / mid-horizontal grids per page index.
63LAYOUT_VBAR = 0
64LAYOUT_HBAR = 1
65LAYOUT_CROSS = 2
66LAYOUT_TRIPTYCH = 3
67
68# Active 7-segment segments per decimal digit: (a, b, c, d, e, f, g).
69SEG_MAP = {
70 0: (1, 1, 1, 1, 1, 1, 0),
71 1: (0, 1, 1, 0, 0, 0, 0),
72 2: (1, 1, 0, 1, 1, 0, 1),
73 3: (1, 1, 1, 1, 0, 0, 1),
74 4: (0, 1, 1, 0, 0, 1, 1),
75 5: (1, 0, 1, 1, 0, 1, 1),
76 6: (1, 0, 1, 1, 1, 1, 1),
77 7: (1, 1, 1, 0, 0, 0, 0),
78 8: (1, 1, 1, 1, 1, 1, 1),
79 9: (1, 1, 1, 1, 0, 1, 1),
80}
81
82
83class Canvas:
84 """A flat 8-bit grayscale page the drawing helpers paint into."""
85
86 def __init__(self, fill: int) -> None:
87 """Allocate a full page pre-filled with one gray level (the paper tint)."""
88 self.px = bytearray([fill]) * (PAGE_W * PAGE_H)
89
90 def fill_rect(self, rect: tuple[int, int, int, int], v: int) -> None:
91 """Paint an ``(x, y, w, h)`` rectangle, clipped to the page.
92
93 Clipping is applied per axis rather than rejecting an out-of-range
94 rect, so callers may compute a rectangle that runs off the edge and get
95 the visible part -- ``frame_rect`` and ``draw_digit`` both rely on that
96 and would otherwise need bounds checks of their own.
97 """
98 x0, y0, w, h = rect
99 for y in range(max(0, y0), min(PAGE_H, y0 + h)):
100 row = y * PAGE_W
101 for x in range(max(0, x0), min(PAGE_W, x0 + w)):
102 self.px[row + x] = v
103
104 def frame_rect(self, rect: tuple[int, int, int, int], t: int, v: int) -> None:
105 """Draw a hollow rectangle of border thickness ``t``, inset into ``rect``.
106
107 The four sides are drawn as overlapping bars, so corners are painted
108 twice; harmless here because every bar uses the same value ``v``.
109 """
110 x0, y0, w, h = rect
111 self.fill_rect((x0, y0, w, t), v)
112 self.fill_rect((x0, y0 + h - t, w, t), v)
113 self.fill_rect((x0, y0, t, h), v)
114 self.fill_rect((x0 + w - t, y0, t, h), v)
115
116 def draw_digit(self, digit: int, rect: tuple[int, int, int, int], t: int) -> None:
117 """Render one decimal digit as a 7-segment figure filling ``rect``.
118
119 Always drawn in INK, so the digit stays legible over any paper tint or
120 panel fill; the segment thickness ``t`` is the caller's, not derived
121 from the rect, which keeps the stroke weight identical across pages.
122 """
123 a, b, c, d, e, f, g = SEG_MAP[digit]
124 x, y, w, h = rect
125 mid = y + (h - t) // 2
126 half = (h // 2) + t
127 if a:
128 self.fill_rect((x, y, w, t), INK)
129 if g:
130 self.fill_rect((x, mid, w, t), INK)
131 if d:
132 self.fill_rect((x, y + h - t, w, t), INK)
133 if f:
134 self.fill_rect((x, y, t, half), INK)
135 if b:
136 self.fill_rect((x + w - t, y, t, half), INK)
137 if e:
138 self.fill_rect((x, mid, t, half), INK)
139 if c:
140 self.fill_rect((x + w - t, mid, t, half), INK)
141
142
143def draw_layout(canvas: Canvas, page_idx: int, fill: int) -> None:
144 """Draw a distinct comic panel arrangement (ink gutters) over a gray fill."""
145 gut = 8
146 x0, y0 = 24, 24
147 w = PAGE_W - 2 * x0
148 h = PAGE_H - 2 * y0
149 canvas.fill_rect((x0, y0, w, h), fill)
150 layout = page_idx % PAGE_COUNT
151 if layout in (LAYOUT_VBAR, LAYOUT_CROSS):
152 canvas.fill_rect((x0 + w // 2 - gut // 2, y0, gut, h), INK)
153 if layout in (LAYOUT_HBAR, LAYOUT_CROSS):
154 canvas.fill_rect((x0, y0 + h // 2 - gut // 2, w, gut), INK)
155 if layout == LAYOUT_TRIPTYCH:
156 canvas.fill_rect((x0 + w // 3 - gut // 2, y0, gut, h), INK)
157 canvas.fill_rect((x0 + 2 * w // 3 - gut // 2, y0, gut, h), INK)
158 if layout not in (LAYOUT_VBAR, LAYOUT_HBAR, LAYOUT_CROSS, LAYOUT_TRIPTYCH):
159 canvas.fill_rect((x0, y0 + h // 3 - gut // 2, w, gut), INK)
160 canvas.fill_rect((x0 + w // 2 - gut // 2, y0 + h // 3, gut, 2 * h // 3), INK)
161
162
163def render_page(page_idx: int) -> bytearray:
164 """Build one distinct grayscale page: border, panels, and a huge number."""
165 canvas = Canvas(PAGE_PAPER[page_idx])
166 canvas.frame_rect((6, 6, PAGE_W - 12, PAGE_H - 12), 6, INK)
167 draw_layout(canvas, page_idx, PAGE_FILL[page_idx])
168 dw, dh, dt = 90, 150, 20
169 canvas.draw_digit(page_idx + 1, ((PAGE_W - dw) // 2, (PAGE_H - dh) // 2, dw, dh), dt)
170 return canvas.px
171
172
173def png_chunk(tag: bytes, data: bytes) -> bytes:
174 """Wrap a payload in one PNG chunk: length, type, data, CRC-32.
175
176 The CRC covers the type tag AND the data but NOT the length prefix, which
177 is what the PNG spec requires and the easiest detail to get wrong -- a
178 chunk whose CRC included the length decodes as corrupt in every reader.
179 """
180 out = struct.pack(">I", len(data)) + tag + data
181 crc = zlib.crc32(tag + data) & 0xFFFFFFFF
182 return out + struct.pack(">I", crc)
183
184
185def encode_png(gray: bytes | bytearray) -> bytes:
186 """8-bit grayscale PNG (color type 0), one filter-0 byte per scanline."""
187 sig = b"\x89PNG\r\n\x1a\n"
188 ihdr = struct.pack(">IIBBBBB", PAGE_W, PAGE_H, PNG_BIT_DEPTH_8, PNG_COLOR_GRAY, 0, 0, 0)
189 raw = bytearray()
190 for y in range(PAGE_H):
191 raw.append(0)
192 raw += gray[y * PAGE_W : (y + 1) * PAGE_W]
193 idat = zlib.compress(bytes(raw), ZLIB_BEST)
194 return sig + png_chunk(b"IHDR", ihdr) + png_chunk(b"IDAT", idat) + png_chunk(b"IEND", b"")
195
196
197def _zip_local_entry(idx: int, data: bytes) -> tuple[bytes, tuple[bytes, int, int, int, int]]:
198 """Deflate one page and return its local-header record plus its directory entry.
199
200 The offset in the returned entry is relative to the start of the local
201 section, so the caller must add records in the same order it later writes
202 the central directory -- a ZIP whose offsets disagree is unreadable.
203 """
204 name = f"page{idx + 1:02d}.png".encode("ascii")
205 comp = zlib.compressobj(ZLIB_BEST, zlib.DEFLATED, DEFLATE_RAW_WBITS)
206 body = comp.compress(data) + comp.flush()
207 crc = zlib.crc32(data) & 0xFFFFFFFF
208 record = (
209 struct.pack(
210 "<IHHHHHIIIHH",
211 ZIP_LOCAL_MAGIC,
212 ZIP_VERSION,
213 0,
214 ZIP_METHOD_DEFLATE,
215 0,
216 0,
217 crc,
218 len(body),
219 len(data),
220 len(name),
221 0,
222 )
223 + name
224 + body
225 )
226 return record, (name, crc, len(body), len(data), 0)
227
228
229def _zip_central_record(entry: tuple[bytes, int, int, int, int]) -> bytes:
230 """Central-directory record for one already-written local entry."""
231 name, crc, csize, usize, offset = entry
232 return (
233 struct.pack(
234 "<IHHHHHHIIIHHHHHII",
235 ZIP_CENTRAL_MAGIC,
236 ZIP_VERSION,
237 ZIP_VERSION,
238 0,
239 ZIP_METHOD_DEFLATE,
240 0,
241 0,
242 crc,
243 csize,
244 usize,
245 len(name),
246 0,
247 0,
248 0,
249 0,
250 0,
251 offset,
252 )
253 + name
254 )
255
256
257def _zip_eocd(count: int, central_len: int, cd_off: int) -> bytes:
258 """End-of-central-directory record closing the archive."""
259 return struct.pack(
260 "<IHHHHIIH",
261 ZIP_EOCD_MAGIC,
262 0,
263 0,
264 count,
265 count,
266 central_len,
267 cd_off,
268 0,
269 )
270
271
272def build_cbz(pngs: list[bytes]) -> bytes:
273 """Minimal deterministic ZIP writer (DEFLATE members) that miniz reads."""
274 files: list[tuple[bytes, int, int, int, int]] = []
275 local = bytearray()
276 for idx, data in enumerate(pngs):
277 offset = len(local)
278 record, entry = _zip_local_entry(idx, data)
279 local += record
280 files.append((*entry[:4], offset))
281 central = bytearray()
282 for entry in files:
283 central += _zip_central_record(entry)
284 cd_off = len(local)
285 return bytes(local) + bytes(central) + _zip_eocd(len(files), len(central), cd_off)
286
287
288def emit_header(path: str | Path, cbz: bytes) -> None:
289 """Write the generated pure-ASCII C header holding the CBZ byte array."""
290 header = (
291 "/**\n"
292 " * @file comic_pages_fixture.h\n"
293 " * @brief Baked multi-page CBZ fixture for the viewable ereader_comic demo\n"
294 " * (generated by scripts/gen/gen_comic_fixture.py -- do not edit).\n"
295 " *\n"
296 f" * @details A miniz-decodable ZIP of {PAGE_COUNT} near-panel grayscale PNG pages\n"
297 f" * ({PAGE_W}x{PAGE_H} each). Every page carries a distinct paper tint,\n"
298 " * panel layout, and a huge 7-segment page number, so a page turn on the\n"
299 " * 1024x600 panel is unmistakable. Pure 7-bit ASCII byte array, like the\n"
300 " * bundled font / cover blobs.\n"
301 " *\n"
302 " * @copyright Copyright (c) 2026 Brighton Sikarskie\n"
303 " * SPDX-License-Identifier: MIT\n"
304 " */\n"
305 "#pragma once\n\n"
306 "#include <stdint.h>\n\n"
307 f"/** @brief Baked {len(cbz)}-byte multi-page CBZ ({PAGE_COUNT} pages). */\n"
308 "static const uint8_t k_comic_cbz[] = {\n"
309 )
310 rows = []
311 for start in range(0, len(cbz), BYTES_PER_ROW):
312 chunk = cbz[start : start + BYTES_PER_ROW]
313 rows.append(" " + " ".join(f"0x{byte:02X}," for byte in chunk))
314 footer = (
315 "\n};\n\n"
316 "/** @brief Length of ::k_comic_cbz in bytes. */\n"
317 f"static const uint32_t k_comic_cbz_len = {len(cbz)}U;\n\n"
318 "/** @brief Page count baked into ::k_comic_cbz. */\n"
319 f"static const uint32_t k_comic_page_count = {PAGE_COUNT}U;\n"
320 )
321 Path(path).write_text(header + "\n".join(rows) + footer, encoding="ascii")
322
323
324def main() -> None:
325 """Render every page, pack them into a CBZ, and write the C header.
326
327 Takes the output path from argv[1], defaulting to the canonical app inc/
328 path. Returns None and exits 0 unconditionally: every failure mode here
329 (unwritable path, non-ASCII output) raises, and there is no partial-success
330 state a status code could describe.
331
332 The progress line goes to stderr so stdout stays free for the generated
333 header if this is ever redirected.
334 """
335 out = Path(sys.argv[1]) if len(sys.argv) > 1 else DEFAULT_OUTPUT
336 pngs = [encode_png(render_page(i)) for i in range(PAGE_COUNT)]
337 cbz = build_cbz(pngs)
338 emit_header(out, cbz)
339 sys.stderr.write(f"wrote {out} ({len(cbz)} bytes CBZ, {PAGE_COUNT} pages)\n")
340
341
342if __name__ == "__main__":
343 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