4"""Generate the baked multi-page CBZ fixture for the viewable ereader_comic demo.
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.
14The output is deterministic (fixed art, fixed zlib level), so the render hash
15the app prints is identical on host, ra8_emulator, and silicon.
18 python3 scripts/gen/gen_comic_fixture.py \
19 examples/ek_ra8d2/hw_pending/ereader_comic/inc/comic_pages_fixture.h
22from __future__
import annotations
27from pathlib
import Path
33 Path(__file__).resolve().parents[2]
39 /
"comic_pages_fixture.h"
47PAGE_PAPER = [244, 232, 250, 224, 238]
48PAGE_FILL = [200, 150, 120, 175, 95]
53ZIP_LOCAL_MAGIC = 0x04034B50
54ZIP_CENTRAL_MAGIC = 0x02014B50
55ZIP_EOCD_MAGIC = 0x06054B50
58DEFLATE_RAW_WBITS = -15
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),
84 """A flat 8-bit grayscale page the drawing helpers paint into."""
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)
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.
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.
99 for y
in range(max(0, y0),
min(PAGE_H, y0 + h)):
101 for x
in range(max(0, x0),
min(PAGE_W, x0 + w)):
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``.
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``.
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)
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``.
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.
123 a, b, c, d, e, f, g = SEG_MAP[digit]
125 mid = y + (h - t) // 2
128 self.fill_rect((x, y, w, t), INK)
130 self.fill_rect((x, mid, w, t), INK)
132 self.fill_rect((x, y + h - t, w, t), INK)
134 self.fill_rect((x, y, t, half), INK)
136 self.fill_rect((x + w - t, y, t, half), INK)
138 self.fill_rect((x, mid, t, half), INK)
140 self.fill_rect((x + w - t, mid, t, half), INK)
143def draw_layout(canvas: Canvas, page_idx: int, fill: int) ->
None:
144 """Draw a distinct comic panel arrangement (ink gutters) over a gray fill."""
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)
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)
173def png_chunk(tag: bytes, data: bytes) -> bytes:
174 """Wrap a payload in one PNG chunk: length, type, data, CRC-32.
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.
180 out = struct.pack(
">I", len(data)) + tag + data
181 crc = zlib.crc32(tag + data) & 0xFFFFFFFF
182 return out + struct.pack(
">I", crc)
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)
190 for y
in range(PAGE_H):
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
"")
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.
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.
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
226 return record, (name, crc, len(body), len(data), 0)
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
234 "<IHHHHHHIIIHHHHHII",
257def _zip_eocd(count: int, central_len: int, cd_off: int) -> bytes:
258 """End-of-central-directory record closing the archive."""
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]] = []
276 for idx, data
in enumerate(pngs):
278 record, entry = _zip_local_entry(idx, data)
280 files.append((*entry[:4], offset))
281 central = bytearray()
283 central += _zip_central_record(entry)
285 return bytes(local) + bytes(central) + _zip_eocd(len(files), len(central), cd_off)
288def emit_header(path: str | Path, cbz: bytes) ->
None:
289 """Write the generated pure-ASCII C header holding the CBZ byte array."""
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"
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"
302 " * @copyright Copyright (c) 2026 Brighton Sikarskie\n"
303 " * SPDX-License-Identifier: MIT\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"
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))
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"
321 Path(path).write_text(header +
"\n".join(rows) + footer, encoding=
"ascii")
325 """Render every page, pack them into a CBZ, and write the C header.
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.
332 The progress line goes to stderr so stdout stays free for the generated
333 header if this is ever redirected.
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")
342if __name__ ==
"__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.
void main(void)
The application entry point Reset_Handler hands control to.
#define min(x, y)
Untyped minimum shim used by the SOUP's buffer clamping.