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
6
The manifest declares the cover with `properties="cover-image"`, and the
7
on-device ereader_cover gate exercises the whole chain against it: open the
8
blob 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
12
The cover is a REAL RGB PNG, and that is the point of this fixture existing
13
alongside apps/shared_libs/epub/tests/src/test_epub.c. That test uses a 4-byte stand-in, which
14
exercises only the byte-copy path and never the decoder. Here stb_image has to
15
actually decode something.
16
17
Output is pure 7-bit ASCII -- a C array of the .epub bytes -- baked at 16 bytes
18
per row so clang-format leaves it byte-identical rather than reflowing it.
19
20
Changing the cover changes the framebuffer CRC. Re-run this, then read the CRC
21
the board prints and update hil.conf in the SAME change, or the gate fails on a
22
fixture that is perfectly correct.
23
24
Usage:
25
python3 examples/ek_ra8d2/hw_pending/ereader_cover/scripts/make_cover_fixture.py
26
"""
27
28
from
__future__
import
annotations
29
30
import
hashlib
31
import
io
32
import
zipfile
33
from
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.
39
COVER_PNG_SHA256 =
"e15408252d202bc39e8b00286e730a07f4d410c3141c716c7e7747d8148f9a2f"
40
COVER_PNG = bytes.fromhex(
41
"89504E470D0A1A0A0000000D49484452000000600000009008020000007868F9760000014949444154789CEDD2411583"
42
"40100541160539724402129018099182844842019480EDBACE9CFAFD717DF625CFD6975B0A642D080A040582024181A0"
43
"405020281014080A040582024181A0405020281014080A040582024181A0405020281014080A040582024181A0405020"
44
"28101408C6F63DF433B516040582024181A0405020281014080A040582024181A0405020281014080A040582024181A0"
45
"405020281014080A040582024181A0405020281014080A04058202C1D8CF4B3F536B415020281014080A040582024181"
46
"A0405020281014080A040582024181A0405020281014080A040582024181A0405020281014080A040582024181A04050"
47
"2028108CFF6FD3CFD45A1014080A040582024181A0405020281014080A040582024181A0405020281014080A04058202"
48
"4181A0405020281014080A040582024181A0405020281014080AB4BCBB012704058D7A1374B60000000049454E44AE42"
49
"6082"
50
)
51
52
53
def
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) ---
76
CONTAINER_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
87
CONTENT_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
109
CH1_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
115
def
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
148
def
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
200
def
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
215
if
__name__ ==
"__main__"
:
216
main
()
main
void main(void)
The application entry point Reset_Handler hands control to.
Definition
main.c:298
examples
ek_ra8d2
hw_pending
ereader_cover
scripts
make_cover_fixture.py
Generated by
1.16.1