ra8-firmware 0.1.0
Bare-metal firmware for the Renesas RA8 family (RA8D2 / RA8P1)
Loading...
Searching...
No Matches
make_cover_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 epub_cover_fixture.h: a minimal EPUB3 with a real PNG cover.
5
6The manifest declares the cover with `properties="cover-image"`, and the
7on-device ereader_cover gate exercises the whole chain against it: open the
8blob in memory with `epub_open()`, pull the cover bytes with
9`epub_get_cover_image()`, decode, scale and blit with
10`ra8_img_decode_blit()`, then CRC-gate the framebuffer.
11
12The cover is a REAL RGB PNG, and that is the point of this fixture existing
13alongside apps/shared_libs/epub/tests/src/test_epub.c. That test uses a 4-byte stand-in, which
14exercises only the byte-copy path and never the decoder. Here stb_image has to
15actually decode something.
16
17Output is pure 7-bit ASCII -- a C array of the .epub bytes -- baked at 16 bytes
18per row so clang-format leaves it byte-identical rather than reflowing it.
19
20Changing the cover changes the framebuffer CRC. Re-run this, then read the CRC
21the board prints and update hil.conf in the SAME change, or the gate fails on a
22fixture that is perfectly correct.
23
24Usage:
25 python3 examples/ek_ra8d2/hw_pending/ereader_cover/scripts/make_cover_fixture.py
26"""
27
28from __future__ import annotations
29
30import hashlib
31import io
32import zipfile
33from pathlib import Path
34
35# Canonical portrait cover: 96x144 (2:3, a real book-cover ratio), with four
36# horizontal colour bands. These are the approved fixture's exact encoded PNG
37# bytes. Freezing them avoids delegating reproducibility to a host image encoder
38# or zlib version while still making stb_image decode a real RGB PNG.
39COVER_PNG_SHA256 = "e15408252d202bc39e8b00286e730a07f4d410c3141c716c7e7747d8148f9a2f"
40COVER_PNG = bytes.fromhex(
41 "89504E470D0A1A0A0000000D49484452000000600000009008020000007868F9760000014949444154789CEDD2411583"
42 "40100541160539724402129018099182844842019480EDBACE9CFAFD717DF625CFD6975B0A642D080A040582024181A0"
43 "405020281014080A040582024181A0405020281014080A040582024181A0405020281014080A040582024181A0405020"
44 "28101408C6F63DF433B516040582024181A0405020281014080A040582024181A0405020281014080A040582024181A0"
45 "405020281014080A040582024181A0405020281014080A04058202C1D8CF4B3F536B415020281014080A040582024181"
46 "A0405020281014080A040582024181A0405020281014080A040582024181A0405020281014080A040582024181A04050"
47 "2028108CFF6FD3CFD45A1014080A040582024181A0405020281014080A040582024181A0405020281014080A04058202"
48 "4181A0405020281014080A040582024181A0405020281014080AB4BCBB012704058D7A1374B60000000049454E44AE42"
49 "6082"
50)
51
52
53def make_cover_png() -> bytes:
54 """Return the canonical 96x144 four-band portrait cover PNG.
55
56 The 2:3 aspect is a real book-cover ratio, so the gate exercises the
57 scaler's aspect handling rather than a convenient square.
58
59 The digest check prevents a hand edit from silently changing the canonical
60 image bytes and therefore the framebuffer CRC contract.
61
62 Returns:
63 The byte-identical canonical PNG.
64
65 Raises:
66 RuntimeError: The embedded PNG no longer has its approved digest.
67 """
68 actual_digest = hashlib.sha256(COVER_PNG).hexdigest()
69 if actual_digest != COVER_PNG_SHA256:
70 _error = f"cover PNG digest {actual_digest} != {COVER_PNG_SHA256}"
71 raise RuntimeError(_error)
72 return COVER_PNG
73
74
75# --- Minimal EPUB3 parts (shape mirrors apps/shared_libs/epub/tests/src/test_epub.c) ---
76CONTAINER_XML = (
77 '<?xml version="1.0"?>\n'
78 '<container version="1.0" '
79 'xmlns="urn:oasis:names:tc:opendocument:xmlns:container">\n'
80 " <rootfiles>\n"
81 ' <rootfile full-path="OEBPS/content.opf" '
82 'media-type="application/oebps-package+xml"/>\n'
83 " </rootfiles>\n"
84 "</container>\n"
85)
86
87CONTENT_OPF = (
88 '<?xml version="1.0" encoding="UTF-8"?>\n'
89 '<package xmlns="http://www.idpf.org/2007/opf" version="3.0" '
90 'unique-identifier="id">\n'
91 ' <metadata xmlns:dc="http://purl.org/dc/elements/1.1/">\n'
92 " <dc:title>Cover Art Demo</dc:title>\n"
93 " <dc:creator>Brighton Sikarskie</dc:creator>\n"
94 " <dc:language>en</dc:language>\n"
95 ' <dc:identifier id="id">urn:test:cover</dc:identifier>\n'
96 ' <meta name="cover" content="cover"/>\n'
97 " </metadata>\n"
98 " <manifest>\n"
99 ' <item id="ch1" href="ch1.xhtml" media-type="application/xhtml+xml"/>\n'
100 ' <item id="cover" href="cover.png" media-type="image/png" '
101 'properties="cover-image"/>\n'
102 " </manifest>\n"
103 " <spine>\n"
104 ' <itemref idref="ch1"/>\n'
105 " </spine>\n"
106 "</package>\n"
107)
108
109CH1_XHTML = (
110 '<?xml version="1.0"?><html><body><h1>Cover Art Demo</h1>'
111 "<p>The cover is decoded from the manifest.</p></body></html>"
112)
113
114
115def make_epub() -> bytes:
116 """Assemble the minimal EPUB3 archive in memory and return its bytes.
117
118 Reproducible byte for byte: every entry carries a fixed 2026-01-01 timestamp
119 and fixed permissions, and every member is ZIP_STORED. No host compressor
120 participates, so regenerating on a clean tree yields an identical header.
121
122 `mimetype` is first and uncompressed as required by the EPUB spec. Storing
123 the other four small members costs little and removes zlib-version drift.
124
125 Returns:
126 The complete .epub archive as bytes.
127 """
128 cover_png = make_cover_png()
129 out = io.BytesIO()
130 fixed = (2026, 1, 1, 0, 0, 0)
131
132 def add(zf: zipfile.ZipFile, name: str, data: bytes | str) -> None:
133 info = zipfile.ZipInfo(name, date_time=fixed)
134 info.compress_type = zipfile.ZIP_STORED
135 info.external_attr = 0o600 << 16
136 zf.writestr(info, data)
137
138 with zipfile.ZipFile(out, "w") as zf:
139 # mimetype MUST be the first entry and stored (uncompressed).
140 add(zf, "mimetype", b"application/epub+zip")
141 add(zf, "META-INF/container.xml", CONTAINER_XML.encode("ascii"))
142 add(zf, "OEBPS/content.opf", CONTENT_OPF.encode("ascii"))
143 add(zf, "OEBPS/ch1.xhtml", CH1_XHTML.encode("ascii"))
144 add(zf, "OEBPS/cover.png", cover_png)
145 return out.getvalue()
146
147
148def bake_header(epub: bytes) -> str:
149 """Render the EPUB bytes as a C header with a `static const uint8_t` table.
150
151 The array is sized from a generated `enum : size_t`, so the declared length
152 and the data cannot drift apart.
153
154 The table sits inside a `clang-format off`/`on` guard at 16 bytes per row.
155 That is what keeps the formatter from reflowing thousands of bytes and
156 turning every regeneration into a large spurious diff.
157
158 Args:
159 epub: Archive bytes to bake, emitted as `0xNN`.
160
161 Returns:
162 The complete header source, pure 7-bit ASCII.
163 """
164 rows = []
165 for i in range(0, len(epub), 16):
166 chunk = epub[i : i + 16]
167 rows.append(" " + ", ".join(f"0x{b:02X}" for b in chunk) + ",")
168 body = "\n".join(rows)
169 return (
170 "/**\n"
171 " * @file epub_cover_fixture.h\n"
172 " * @brief Baked minimal EPUB3 with a real PNG cover (cover-image manifest item).\n"
173 " *\n"
174 " * @details A 96x144 four-band RGB PNG cover wrapped in a one-chapter EPUB3,\n"
175 " * byte-identical run to run. The ereader_cover gate opens it in memory\n"
176 " * (epub_open), extracts the cover (epub_get_cover_image), and\n"
177 " * decode+scale+blits it (ra8_img_decode_blit); the framebuffer hash in\n"
178 " * hil.conf pins the result. Generated by examples/ek_ra8d2/hw_pending/\n"
179 " * ereader_cover/scripts/make_cover_fixture.py. Pure ASCII.\n"
180 " *\n"
181 " * @copyright Copyright (c) 2026 Brighton Sikarskie\n"
182 " * SPDX-License-Identifier: MIT\n"
183 " */\n"
184 "#pragma once\n"
185 "\n"
186 "#include <stddef.h>\n"
187 "#include <stdint.h>\n"
188 "\n"
189 "/** @brief Length of the baked cover EPUB blob, bytes. */\n"
190 f"enum : size_t {{ k_epub_cover_fixture_len = {len(epub)}U "
191 "/**< EPUB cover fixture length. */ };\n"
192 "\n"
193 "/** @brief Baked cover-art EPUB3 byte stream. */\n"
194 "static const uint8_t k_epub_cover_fixture[k_epub_cover_fixture_len] = {\n"
195 f"{body}\n"
196 "};\n"
197 )
198
199
200def main() -> int:
201 """Regenerate the owning component's inc/epub_cover_fixture.h.
202
203 Remember the two-step: this refreshes the fixture, but the framebuffer CRC
204 in hil.conf is pinned separately and must be re-read from the board and
205 updated in the same change.
206 """
207 epub = make_epub()
208 header = bake_header(epub)
209 output = Path(__file__).resolve().parent.parent / "inc" / "epub_cover_fixture.h"
210 with output.open("w", encoding="ascii") as f:
211 f.write(header)
212 print(f"wrote {output} ({len(epub)} epub bytes)")
213
214
215if __name__ == "__main__":
216 main()
void main(void)
The application entry point Reset_Handler hands control to.
Definition main.c:298