diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index e9d24d3..bdd782f 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -69,7 +69,7 @@ jobs:
' CHANGELOG.md > release-notes.md
# Fall back to a generic line if the changelog had no matching section.
if [ ! -s release-notes.md ]; then
- echo "Firefly for Java by Example — release ${TAG_NAME}." > release-notes.md
+ echo "jafly by example — release ${TAG_NAME}." > release-notes.md
fi
echo "----- release-notes.md -----"
cat release-notes.md
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0562f9a..6a344a4 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,11 +1,24 @@
# Changelog
-All notable changes to *Firefly for Java by Example* are documented here.
+All notable changes to *jafly by example* are documented here.
The project uses CalVer (`YY.MM.PATCH`). The `release.yml` workflow extracts the
section matching a `v*.*.*` tag as the GitHub Release notes.
## [Unreleased]
+### Changed
+
+- Adopt the shared Framework by Firefly identity for English and Spanish front and back covers.
+- Introduce *jafly by example* as the collection title across both editions, with localized enterprise-architecture subtitles. Existing package identifiers and download filenames remain unchanged.
+- Place PDF covers on full trim pages without running headers or page numbers.
+- Include localized front and back cover documents in EPUB reading order, and retain chapter navigation.
+- Include publisher metadata in both formats and rights metadata in EPUB.
+- Replace the retired cover generator with canonical artwork validation and import.
+- Apply the shared logo and palette to title pages, part dividers and chapter openers.
+- Render localized chapter and prelude titles when the manuscript relies on manifest headings.
+
+The technical manuscript and companion application are unchanged.
+
## [26.09.01] — 2026-09-24
### Fixed
diff --git a/README.md b/README.md
index fc18899..978741a 100644
--- a/README.md
+++ b/README.md
@@ -1,8 +1,13 @@
-# Firefly for Java by Example
+# jafly by example
-> *Reactive Microservices with Spring Boot, WebFlux & the Firefly Framework.*
+
+
+
+
-The canonical, build-it-yourself guide to the **Java Firefly Framework** — a metaframework on top of Spring Boot (WebFlux · Project Reactor · R2DBC). You build one real application, a personal-loan origination service (**Lumen Lending**), from an empty folder to a secured, observable, event-driven, three-tier system — making every concept concrete before the next.
+> *From Spring Boot to a connected enterprise architecture.*
+
+The canonical, build-it-yourself guide to **jafly**, the Java member of **Framework by Firefly** — a metaframework on top of Spring Boot (WebFlux · Project Reactor · R2DBC). You build one real application, a personal-loan origination service (**Lumen Lending**), from an empty folder to a secured, observable, event-driven, three-tier system — making every concept concrete before the next.
Published by the **Firefly Software Foundation** under Apache-2.0, in **American English** and **Spain Spanish (es-ES)**.
@@ -38,13 +43,52 @@ mvn -f samples/lumen-lending/pom.xml verify
./build/.venv/bin/python build/verify_code.py manuscript-es samples/lumen-lending
```
+## Cover and interior artwork
+
+The English and Spanish manifests select separate front and back covers from
+`art/`. PDF covers occupy the complete 7.5 × 9.25 inch trim page, without running
+headers or page numbers. EPUB editions include localized cover and back-cover
+documents at the beginning and end of the reading order, while retaining the
+chapter navigation. The interior uses the approved jafly logo on title and part
+pages, compact numbered chapter banners, and the shared paper, charcoal, amber
+and gold palette. Chapter titles come from the localized manifests when the
+manuscript does not supply a heading. Technical prose, diagram geometry, and
+verbatim listings are preserved.
+
+The shared **Framework by Firefly** brand kit owns the cover design. Validate the
+checked-in outlined SVGs and 1500 × 1850 PNGs before rebuilding:
+
+```bash
+./build/.venv/bin/python build/gen_cover.py
+# To import an updated canonical set:
+./build/.venv/bin/python build/gen_cover.py --source /path/to/Framework-Brand-Kit/11-Books/java
+```
+
+On macOS, prefix these commands with
+`DYLD_FALLBACK_LIBRARY_PATH="$(brew --prefix)/lib"` if Cairo is not already on the
+loader path. The tool validates all four covers before copying them, and can
+rasterize missing PNGs from their outlined SVGs. It does not generate a new cover
+design. See [artwork provenance](art/BRAND-PROVENANCE.md) for ownership and print
+boundaries.
+
## Releases
-Built editions (EN + ES, EPUB + PDF) are attached to each [GitHub Release](../../releases) on a CalVer (`YY.MM.PATCH`) tag. The release workflow prepares a draft with all four files; publish the draft after reviewing the rendered editions.
+The redesigned English and Spanish editions are published together as
+[`books-2026.10.08`](https://github.com/fireflyframework/fireflyframework-java-book/releases/tag/books-2026.10.08):
+
+| Edition | PDF | EPUB |
+| --- | --- | --- |
+| English | [Download PDF](https://github.com/fireflyframework/fireflyframework-java-book/releases/download/books-2026.10.08/firefly-java-by-example.pdf) | [Download EPUB](https://github.com/fireflyframework/fireflyframework-java-book/releases/download/books-2026.10.08/firefly-java-by-example.epub) |
+| Español | [Descargar PDF](https://github.com/fireflyframework/fireflyframework-java-book/releases/download/books-2026.10.08/firefly-java-by-example-es.pdf) | [Descargar EPUB](https://github.com/fireflyframework/fireflyframework-java-book/releases/download/books-2026.10.08/firefly-java-by-example-es.epub) |
+
+Book-only editions use a `books-YYYY.MM.DD` tag. The existing CalVer
+(`vYY.MM.PATCH`) workflow remains available for subsequent technical editions.
## Scope
-This book is about the **Java** Firefly Framework only. The Python (PyFly), Rust, Go, and .NET ports, the Python agentic metaframework, the `agentic-bridge`, and the frontend frameworks are parallel projects and are **not** covered here.
+This book covers **jafly**, the Java family. The pyfly, larafly, rsfly and netfly
+frameworks, Firefly Agentic, the `agentic-bridge` and frontend projects have their
+own documentation and are outside this book's scope.
---
diff --git a/art/BRAND-PROVENANCE.md b/art/BRAND-PROVENANCE.md
new file mode 100644
index 0000000..5a16d7b
--- /dev/null
+++ b/art/BRAND-PROVENANCE.md
@@ -0,0 +1,43 @@
+# Book cover provenance
+
+Both language editions use the collection title **jafly by example**. Runtime
+package identifiers and the existing download filenames remain unchanged.
+
+The canonical source is the private Framework by Firefly digital brand kit,
+`Framework-Brand-Kit/11-Books/java/`. The assets use its product-first A2 identity
+with the official Firefly endorsement and outlined Manrope typography. The cover
+artwork requires no installed fonts. The interior uses the same paper, charcoal,
+amber and accessible gold palette, retaining the existing reading and code fonts.
+
+| Edition | Front | Back |
+| --- | --- | --- |
+| English | `cover.svg`, `cover.png` | `back-cover.svg`, `back-cover.png` |
+| Spanish | `cover-es.svg`, `cover-es.png` | `back-cover-es.svg`, `back-cover-es.png` |
+
+All canvases are 1500 × 1850 units/pixels, matching the book's 7.5 × 9.25 inch trim
+at 200 pixels per inch. These are individual digital cover faces, not a printer's
+wraparound jacket: no bleed, binding allowance, barcode, or production spine is
+implied. The brand kit's separate web spine is not part of the PDF or EPUB.
+
+Use `build/gen_cover.py --source /path/to/Framework-Brand-Kit/11-Books/java` to
+import an approved revision, then rebuild and visually review both editions.
+Running the tool without `--source` validates the checked-in art and rasterizes
+missing PNGs. It refuses missing SVGs, wrong dimensions, or live text.
+
+Firefly Software Foundation remains the book's author and publisher. The book's
+Apache-2.0 license and existing attribution remain in force; the brand identity
+does not change the technical content or imply additional trademark rights.
+
+## Interior identity
+
+`art/brand/` contains exact approved logo and banner exports from
+`Framework-Brand-Kit/12-Frameworks/java/`, including the gradient final y.
+The light logo appears on title and part-divider pages; the dark logo is embedded
+without redrawing it in the 24 compact numbered chapter openers. Regenerate those
+with `build/.venv/bin/python build/gen_openers.py` after importing approved logos.
+The same opener assets are used by both languages; chapter numbers are decorative
+and the localized chapter headings remain the accessible document titles.
+
+The existing code-anatomy figure and concept-preview generator receive palette
+changes only. Their geometry and technical text are preserved. Syntax colors and
+warning colors retain semantic distinctions.
diff --git a/art/back-cover-es.png b/art/back-cover-es.png
new file mode 100644
index 0000000..486319d
Binary files /dev/null and b/art/back-cover-es.png differ
diff --git a/art/back-cover-es.svg b/art/back-cover-es.svg
new file mode 100644
index 0000000..c3f8d9f
--- /dev/null
+++ b/art/back-cover-es.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/art/back-cover.png b/art/back-cover.png
new file mode 100644
index 0000000..aa524a0
Binary files /dev/null and b/art/back-cover.png differ
diff --git a/art/back-cover.svg b/art/back-cover.svg
new file mode 100644
index 0000000..30b2593
--- /dev/null
+++ b/art/back-cover.svg
@@ -0,0 +1 @@
+jafly - Back coverFramework by Firefly
\ No newline at end of file
diff --git a/art/brand/banner-dark.png b/art/brand/banner-dark.png
new file mode 100644
index 0000000..3f0f3c3
Binary files /dev/null and b/art/brand/banner-dark.png differ
diff --git a/art/brand/banner-dark.svg b/art/brand/banner-dark.svg
new file mode 100644
index 0000000..96fbc91
--- /dev/null
+++ b/art/brand/banner-dark.svg
@@ -0,0 +1 @@
+jafly for Javajafly - Java framework by Firefly
\ No newline at end of file
diff --git a/art/brand/banner-light.png b/art/brand/banner-light.png
new file mode 100644
index 0000000..9d5cc7b
Binary files /dev/null and b/art/brand/banner-light.png differ
diff --git a/art/brand/banner-light.svg b/art/brand/banner-light.svg
new file mode 100644
index 0000000..383fc61
--- /dev/null
+++ b/art/brand/banner-light.svg
@@ -0,0 +1 @@
+jafly for Javajafly - Java framework by Firefly
\ No newline at end of file
diff --git a/art/brand/logo-dark.png b/art/brand/logo-dark.png
new file mode 100644
index 0000000..ce0fe26
Binary files /dev/null and b/art/brand/logo-dark.png differ
diff --git a/art/brand/logo-dark.svg b/art/brand/logo-dark.svg
new file mode 100644
index 0000000..4348150
--- /dev/null
+++ b/art/brand/logo-dark.svg
@@ -0,0 +1 @@
+jafly - Java framework by Firefly
\ No newline at end of file
diff --git a/art/brand/logo-light.png b/art/brand/logo-light.png
new file mode 100644
index 0000000..fa4f512
Binary files /dev/null and b/art/brand/logo-light.png differ
diff --git a/art/brand/logo-light.svg b/art/brand/logo-light.svg
new file mode 100644
index 0000000..360f8d7
--- /dev/null
+++ b/art/brand/logo-light.svg
@@ -0,0 +1 @@
+jafly - Java framework by Firefly
\ No newline at end of file
diff --git a/art/cover-es.png b/art/cover-es.png
new file mode 100644
index 0000000..c63c2d0
Binary files /dev/null and b/art/cover-es.png differ
diff --git a/art/cover-es.svg b/art/cover-es.svg
new file mode 100644
index 0000000..dc0ced3
--- /dev/null
+++ b/art/cover-es.svg
@@ -0,0 +1 @@
+jafly by exampleFramework by Firefly
\ No newline at end of file
diff --git a/art/cover.png b/art/cover.png
index 3ebaac5..b46e547 100644
Binary files a/art/cover.png and b/art/cover.png differ
diff --git a/art/cover.svg b/art/cover.svg
index c694ec7..9208727 100644
--- a/art/cover.svg
+++ b/art/cover.svg
@@ -1,234 +1 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-CMD
-
-
-EVT
-
-
-SAGA
-
-
-Q
-
-
-HTTP
-
-
-DATA
-
-
-MSG
-
-
-
-
-EVENTS
-ASYNC
-REACTIVE
-CQRS
-
-
-FIREFLY SOFTWARE FOUNDATION
-Firefly for
-Java
-
-by Example
-Reactive Microservices with Spring Boot,
-WebFlux & the Firefly Framework
-
\ No newline at end of file
+jafly by exampleFramework by Firefly
\ No newline at end of file
diff --git a/art/figures/C1-anatomy.svg b/art/figures/C1-anatomy.svg
index dac5b38..c9c6342 100644
--- a/art/figures/C1-anatomy.svg
+++ b/art/figures/C1-anatomy.svg
@@ -2,18 +2,18 @@
-
+ Money.java
-
+ public record Money(long minorUnits) {public static Money of(long minor) {if (minor < 0) throw ...;
diff --git a/art/openers/ch01.svg b/art/openers/ch01.svg
index 94ae220..85cf1a3 100644
--- a/art/openers/ch01.svg
+++ b/art/openers/ch01.svg
@@ -1 +1 @@
-CHAPTER 1F
+01
diff --git a/art/openers/ch02.svg b/art/openers/ch02.svg
index c3ae0b3..4679140 100644
--- a/art/openers/ch02.svg
+++ b/art/openers/ch02.svg
@@ -1 +1 @@
-CHAPTER 2F
+02
diff --git a/art/openers/ch03.svg b/art/openers/ch03.svg
index 3fa0f29..f46902b 100644
--- a/art/openers/ch03.svg
+++ b/art/openers/ch03.svg
@@ -1 +1 @@
-CHAPTER 3F
+03
diff --git a/art/openers/ch04.svg b/art/openers/ch04.svg
index 3481d2a..2057a22 100644
--- a/art/openers/ch04.svg
+++ b/art/openers/ch04.svg
@@ -1 +1 @@
-CHAPTER 4F
+04
diff --git a/art/openers/ch05.svg b/art/openers/ch05.svg
index 147348b..e7ee1b3 100644
--- a/art/openers/ch05.svg
+++ b/art/openers/ch05.svg
@@ -1 +1 @@
-CHAPTER 5F
+05
diff --git a/art/openers/ch06.svg b/art/openers/ch06.svg
index f36545b..7a8cd9a 100644
--- a/art/openers/ch06.svg
+++ b/art/openers/ch06.svg
@@ -1 +1 @@
-CHAPTER 6F
+06
diff --git a/art/openers/ch07.svg b/art/openers/ch07.svg
index eda6b2d..3d99ffe 100644
--- a/art/openers/ch07.svg
+++ b/art/openers/ch07.svg
@@ -1 +1 @@
-CHAPTER 7F
+07
diff --git a/art/openers/ch08.svg b/art/openers/ch08.svg
index e810679..6222cfc 100644
--- a/art/openers/ch08.svg
+++ b/art/openers/ch08.svg
@@ -1 +1 @@
-CHAPTER 8F
+08
diff --git a/art/openers/ch09.svg b/art/openers/ch09.svg
index 7a4467b..f57afcd 100644
--- a/art/openers/ch09.svg
+++ b/art/openers/ch09.svg
@@ -1 +1 @@
-CHAPTER 9F
+09
diff --git a/art/openers/ch10.svg b/art/openers/ch10.svg
index 1fbcbaf..5bbc9b3 100644
--- a/art/openers/ch10.svg
+++ b/art/openers/ch10.svg
@@ -1 +1 @@
-CHAPTER 10F
+10
diff --git a/art/openers/ch11.svg b/art/openers/ch11.svg
index 2f0edbd..7b1668a 100644
--- a/art/openers/ch11.svg
+++ b/art/openers/ch11.svg
@@ -1 +1 @@
-CHAPTER 11F
+11
diff --git a/art/openers/ch12.svg b/art/openers/ch12.svg
index b25fda7..06d95e4 100644
--- a/art/openers/ch12.svg
+++ b/art/openers/ch12.svg
@@ -1 +1 @@
-CHAPTER 12F
+12
diff --git a/art/openers/ch13.svg b/art/openers/ch13.svg
index f1da3b4..ca8a6ff 100644
--- a/art/openers/ch13.svg
+++ b/art/openers/ch13.svg
@@ -1 +1 @@
-CHAPTER 13F
+13
diff --git a/art/openers/ch14.svg b/art/openers/ch14.svg
index 63ee294..a6930c4 100644
--- a/art/openers/ch14.svg
+++ b/art/openers/ch14.svg
@@ -1 +1 @@
-CHAPTER 14F
+14
diff --git a/art/openers/ch15.svg b/art/openers/ch15.svg
index ff61afe..14a3918 100644
--- a/art/openers/ch15.svg
+++ b/art/openers/ch15.svg
@@ -1 +1 @@
-CHAPTER 15F
+15
diff --git a/art/openers/ch16.svg b/art/openers/ch16.svg
index fea3c93..d7118d8 100644
--- a/art/openers/ch16.svg
+++ b/art/openers/ch16.svg
@@ -1 +1 @@
-CHAPTER 16F
+16
diff --git a/art/openers/ch17.svg b/art/openers/ch17.svg
index 8e596f5..8eebcd3 100644
--- a/art/openers/ch17.svg
+++ b/art/openers/ch17.svg
@@ -1 +1 @@
-CHAPTER 17F
+17
diff --git a/art/openers/ch18.svg b/art/openers/ch18.svg
index 325525a..03500be 100644
--- a/art/openers/ch18.svg
+++ b/art/openers/ch18.svg
@@ -1 +1 @@
-CHAPTER 18F
+18
diff --git a/art/openers/ch19.svg b/art/openers/ch19.svg
index 50ec662..6eef812 100644
--- a/art/openers/ch19.svg
+++ b/art/openers/ch19.svg
@@ -1 +1 @@
-CHAPTER 19F
+19
diff --git a/art/openers/ch20.svg b/art/openers/ch20.svg
index da28925..403647a 100644
--- a/art/openers/ch20.svg
+++ b/art/openers/ch20.svg
@@ -1 +1 @@
-CHAPTER 20F
+20
diff --git a/art/openers/ch21.svg b/art/openers/ch21.svg
index 9a99c8d..ed57203 100644
--- a/art/openers/ch21.svg
+++ b/art/openers/ch21.svg
@@ -1 +1 @@
-CHAPTER 21F
+21
diff --git a/art/openers/ch22.svg b/art/openers/ch22.svg
index 9071f8c..68933be 100644
--- a/art/openers/ch22.svg
+++ b/art/openers/ch22.svg
@@ -1 +1 @@
-CHAPTER 22F
+22
diff --git a/art/openers/ch23.svg b/art/openers/ch23.svg
index 8bb81ab..2c40ab7 100644
--- a/art/openers/ch23.svg
+++ b/art/openers/ch23.svg
@@ -1 +1 @@
-CHAPTER 23F
+23
diff --git a/art/openers/ch24.svg b/art/openers/ch24.svg
index 941d263..0845a92 100644
--- a/art/openers/ch24.svg
+++ b/art/openers/ch24.svg
@@ -1 +1 @@
-CHAPTER 24F
+24
diff --git a/book.es.yaml b/book.es.yaml
index d5df2f6..813f9b7 100644
--- a/book.es.yaml
+++ b/book.es.yaml
@@ -1,17 +1,24 @@
-title: "Firefly para Java con ejemplos"
-subtitle: "Microservicios reactivos con Spring Boot, WebFlux y el Firefly Framework"
+title: "jafly by example"
+subtitle: "De Spring Boot a una arquitectura empresarial conectada"
author: "Firefly Software Foundation"
publisher: "Firefly Software Foundation"
language: "es"
identifier: "urn:uuid:7b2f3d51-8c4f-4a2b-8d3e-1b2c3d4e5a6b"
rights: "Copyright (c) 2026 Firefly Software Foundation. Bajo licencia Apache-2.0."
-cover_svg: "art/cover.svg"
-cover_png: "art/cover.png"
+cover_svg: "art/cover-es.svg"
+cover_png: "art/cover-es.png"
+back_cover_svg: "art/back-cover-es.svg"
+back_cover_png: "art/back-cover-es.png"
+interior_logo: "art/brand/logo-light.png"
trim_width: "7.5in"
trim_height: "9.25in"
manuscript_dir: "manuscript-es"
output_basename: "firefly-java-by-example-es"
+cover_alt: "Portada: jafly by example — Framework by Firefly"
+back_cover_alt: "Contraportada: jafly by example — una guía para crear aplicaciones de negocio Java con Firefly"
labels:
+ cover: "Portada"
+ back_cover: "Contraportada"
contents: "Contenido"
front:
- {id: title, file: 00-front/00-title.md, nav: false}
diff --git a/book.yaml b/book.yaml
index 4584056..1f873b6 100644
--- a/book.yaml
+++ b/book.yaml
@@ -1,5 +1,5 @@
-title: "Firefly for Java by Example"
-subtitle: "Reactive Microservices with Spring Boot, WebFlux & the Firefly Framework"
+title: "jafly by example"
+subtitle: "From Spring Boot to a connected enterprise architecture"
author: "Firefly Software Foundation"
publisher: "Firefly Software Foundation"
language: "en"
@@ -7,11 +7,18 @@ identifier: "urn:uuid:6a1f2c40-7b3e-4f1a-9c2d-0a1b2c3d4e5f"
rights: "Copyright (c) 2026 Firefly Software Foundation. Licensed under Apache-2.0."
cover_svg: "art/cover.svg"
cover_png: "art/cover.png"
+back_cover_svg: "art/back-cover.svg"
+back_cover_png: "art/back-cover.png"
+interior_logo: "art/brand/logo-light.png"
trim_width: "7.5in"
trim_height: "9.25in"
manuscript_dir: "manuscript"
output_basename: "firefly-java-by-example"
+cover_alt: "Front cover: jafly by example — Framework by Firefly"
+back_cover_alt: "Back cover: jafly by example — a guide to building Java business applications with Firefly"
labels:
+ cover: "Cover"
+ back_cover: "Back cover"
contents: "Contents"
front:
- {id: title, file: 00-front/00-title.md, nav: false}
diff --git a/build/build-book.sh b/build/build-book.sh
index f3ea007..41f1e14 100755
--- a/build/build-book.sh
+++ b/build/build-book.sh
@@ -1,5 +1,5 @@
#!/usr/bin/env bash
-# Linux/CI build entry for *Firefly for Java by Example*. No DYLD shim (on Linux
+# Linux/CI build entry for *jafly by example*. No DYLD shim (on Linux
# the cairo/pango shared libs resolve via the normal loader). Resolves the repo
# root, prefers the project venv at build/.venv, and falls back to system
# python3. Pass-through args go to build.py (e.g. --config book.es.yaml).
diff --git a/build/build.py b/build/build.py
index 69008bd..562f091 100644
--- a/build/build.py
+++ b/build/build.py
@@ -1,4 +1,4 @@
-"""Build *Firefly for Java by Example* into EPUB + PDF from a book manifest.
+"""Build *jafly by example* into EPUB + PDF from a book manifest.
Defaults to ``book.yaml`` (English). Pass ``--config book.es.yaml`` to build the
Spanish edition; each manifest names its own ``manuscript_dir``, ``language``,
@@ -12,7 +12,7 @@
import re
import sys
from pathlib import Path
-from xml.sax.saxutils import escape
+from xml.sax.saxutils import escape, quoteattr
import yaml # PyYAML ships with the framework env; ensure installed in book/.venv
sys.path.insert(0, str(Path(__file__).resolve().parent))
@@ -87,6 +87,7 @@ def _items_from_manifest(cfg: dict, man: Path, *, contents_label: str) -> list[d
"num": ch["num"],
"path": str(man / ch["file"]),
"part": ptitle_full,
+ "opener": ch.get("opener"),
})
return items
@@ -118,14 +119,14 @@ def _toc_html(items: list[dict], *, href_fmt: str, label: str) -> str:
return f'
')
def main(argv: list[str] | None = None) -> int:
- ap = argparse.ArgumentParser(description="Build Firefly for Java by Example (EPUB + PDF).")
+ ap = argparse.ArgumentParser(description="Build jafly by example (EPUB + PDF).")
ap.add_argument("--config", default="book.yaml",
help="Manifest file at the repo root (default: book.yaml).")
ap.add_argument("--out", default=None,
@@ -141,44 +142,106 @@ def main(argv: list[str] | None = None) -> int:
(THEME / "pygments.css").read_text()]
items = _items_from_manifest(cfg, man, contents_label=contents_label)
+ covers = []
+ for kind, key, label_key, default_label in (
+ ("cover", "cover_png", "cover", "Cover"),
+ ("backcover", "back_cover_png", "back_cover", "Back cover"),
+ ):
+ if not cfg.get(key):
+ continue
+ path = BOOK / cfg[key]
+ if not path.is_file():
+ raise FileNotFoundError(f"Required {kind} artwork is missing: {path}")
+ label = cfg.get("labels", {}).get(label_key, default_label)
+ alt = cfg.get(label_key + "_alt", f"{label}: {cfg['title']}")
+ covers.append((kind, path, label, alt))
+
+ interior_assets = {}
+ if cfg.get("interior_logo"):
+ interior_assets["brand-logo"] = BOOK / cfg["interior_logo"]
+ for it in items:
+ if it.get("opener"):
+ interior_assets[f'opener-{it["id"]}'] = BOOK / it["opener"]
+ for path in interior_assets.values():
+ if not path.is_file():
+ raise FileNotFoundError(f"Required interior artwork is missing: {path}")
+
+ def interior_image(key: str, *, pdf: bool = False) -> str:
+ if key not in interior_assets:
+ return ""
+ path = interior_assets[key]
+ src = path.as_uri() if pdf else f"art/{key}{path.suffix}"
+ cls = "brand-logo" if key == "brand-logo" else "chapter-opener"
+ alt = "jafly by Firefly" if key == "brand-logo" else ""
+ return f''
+
# ---- EPUB ----
epub = EpubBuilder(title=cfg["title"], author=cfg["author"], language=cfg["language"],
- identifier=cfg["identifier"], css=css_text)
- cover_png = BOOK / cfg["cover_png"]
- if cover_png.exists():
- epub.add_file(cover_png, "art/cover.png", "cover-img", properties="cover-image")
+ identifier=cfg["identifier"], css=css_text,
+ publisher=cfg.get("publisher", ""), rights=cfg.get("rights", ""),
+ contents_title=contents_label)
+ for key, path in interior_assets.items():
+ epub.add_file(path, f"art/{key}{path.suffix}", key)
+ cover_docs = {}
+ for kind, path, label, alt in covers:
+ href = f"art/{kind}.png"
+ epub.add_file(path, href, f"{kind}-img",
+ properties="cover-image" if kind == "cover" else "")
+ cover_docs[kind] = Doc(id=kind, title=label, in_nav=False, kind=kind,
+ xhtml_body=f'')
+ if "cover" in cover_docs:
+ epub.add_doc(cover_docs["cover"])
for it in items:
if it["kind"] == "toc":
body = _toc_html(items, href_fmt="{cid}.xhtml", label=contents_label)
epub.add_doc(Doc(id=it["id"], title=it["title"], xhtml_body=body,
in_nav=True, kind="toc"))
elif it["kind"] == "divider":
- body = _divider_html(it["eyebrow"], it["ptitle"])
+ body = _divider_html(it["eyebrow"], it["ptitle"], interior_image("brand-logo"))
epub.add_doc(Doc(id=it["id"], title=it["part"], xhtml_body=body,
in_nav=False, kind="divider", part=it["part"]))
else: # front | chapter
body = render_markdown(Path(it["path"]).read_text(encoding="utf-8"), BOOK)
+ if (it["kind"] == "chapter" or it.get("in_nav")) and not re.search(r"
)", body):
+ body = f'
{escape(it["title"])}
' + body
+ body = interior_image("brand-logo" if it["id"] == "title" else f'opener-{it["id"]}') + body
epub.add_doc(Doc(id=it["id"], title=it["title"], xhtml_body=body,
in_nav=it.get("in_nav", True), kind=it["kind"],
part=it.get("part"), num=it.get("num")))
+ if "backcover" in cover_docs:
+ epub.add_doc(cover_docs["backcover"])
DIST.mkdir(exist_ok=True)
epub.build(DIST / f"{out_base}.epub")
# ---- PDF (single concatenated document) ----
parts_html: list[str] = []
- if cover_png.exists():
- parts_html.append(f'
')
+ def pdf_cover(kind: str) -> str:
+ for cover_kind, path, label, alt in covers:
+ if cover_kind == kind:
+ return (f'
')
+ return ""
+
+ parts_html.append(pdf_cover("cover"))
for it in items:
if it["kind"] == "toc":
body = _toc_html(items, href_fmt="#{cid}", label=contents_label)
parts_html.append(f'{body}')
elif it["kind"] == "divider":
- body = _divider_html(it["eyebrow"], it["ptitle"])
+ body = _divider_html(it["eyebrow"], it["ptitle"], interior_image("brand-logo", pdf=True))
parts_html.append(f'{body}')
else: # front | chapter
body = render_markdown(Path(it["path"]).read_text(encoding="utf-8"), BOOK)
+ if (it["kind"] == "chapter" or it.get("in_nav")) and not re.search(r"
)", body):
+ body = f'
{escape(it["title"])}
' + body
+ body = interior_image("brand-logo" if it["id"] == "title" else f'opener-{it["id"]}', pdf=True) + body
parts_html.append(f'{body}')
- full = (""
+ parts_html.append(pdf_cover("backcover"))
+ full = (f''
+ f'{escape(cfg["title"])}'
+ f''
+ f''
+ ''
+ "\n".join(parts_html) + "")
render_pdf(full, base_url=BOOK,
css_paths=[THEME / "tokens.css", THEME / "pygments.css",
diff --git a/build/epub.py b/build/epub.py
index 97934c0..9211855 100644
--- a/build/epub.py
+++ b/build/epub.py
@@ -37,6 +37,9 @@ class EpubBuilder:
docs: list[Doc] = field(default_factory=list)
assets: list[Asset] = field(default_factory=list)
cover_asset_id: str | None = None
+ publisher: str = ""
+ rights: str = ""
+ contents_title: str = "Contents"
def add_doc(self, d: Doc) -> None: self.docs.append(d)
def add_asset(self, a: Asset) -> None: self.assets.append(a)
@@ -51,12 +54,14 @@ def add_file(self, path: Path, href: str, aid: str, properties: str = "") -> Ass
def _xhtml(self, d: Doc) -> str:
links = "\n".join(f''
for i in range(len(self.css)))
- sec_class = {"toc": "toc", "divider": "part-divider"}.get(d.kind, "chapter")
+ sec_class = {"toc": "toc", "divider": "part-divider",
+ "cover": "cover-page", "backcover": "cover-page"}.get(d.kind, "chapter")
+ semantics = {"cover": ' epub:type="cover"', "backcover": ' epub:type="backmatter"'}.get(d.kind, "")
return (f'\n'
f'\n'
f'{escape(d.title)}\n{links}\n\n'
- f''
+ f''
f'{d.xhtml_body}\n\n')
def _nav(self) -> str:
@@ -90,8 +95,8 @@ def _nav(self) -> str:
return ('\n'
''
- 'Contents'
- f''
+ f'{escape(self.contents_title)}'
+ f''
'')
def _opf(self) -> str:
@@ -117,6 +122,8 @@ def _opf(self) -> str:
f'{escape(self.title)}'
f'{escape(self.author)}'
f'{self.language}'
+ f'{escape(self.publisher)}'
+ f'{escape(self.rights)}'
f'{modified}{cover_meta}'
f'{"".join(man)}'
f'{spine}')
diff --git a/build/gen_cover.py b/build/gen_cover.py
index 2fef766..f07266f 100644
--- a/build/gen_cover.py
+++ b/build/gen_cover.py
@@ -1,347 +1,54 @@
-"""Generate art/cover.svg and art/cover.png for *Firefly for Java by Example*.
+"""Validate or import canonical Framework by Firefly book artwork.
-A clean, self-contained typographic cover in the "Reactive Java at dusk" palette:
-a warm espresso base, amber-gold as the hero color (Java's warmth) and
-firefly-green as the spark (brand continuity) — a deliberate green+amber duotone,
-distinct from PyFly (light/green) and the Rust book (navy/amber). There is NO
-external logo dependency: the firefly mark is drawn inline as SVG, so the
-generator needs nothing under ~/Downloads or art/logo/. The canvas is
-1500 x 2100 px (7.5 x 9.25 in at 200 dpi) — the book's trim size.
-
-Run: build/.venv/bin/python build/gen_cover.py (writes art/cover.{svg,png})
+The shared digital brand kit owns the design. This tool cannot recreate the
+retired cover. Import all four localized covers with --source /path/to/11-Books/java,
+or run without arguments to validate the checked-in artwork. Missing PNGs are
+rasterized from the outlined SVG originals.
"""
from __future__ import annotations
-import math
+import argparse
from pathlib import Path
-from xml.sax.saxutils import escape as _xml_escape
+import shutil
+import xml.etree.ElementTree as ET
+
import cairosvg
+from PIL import Image
ART = Path(__file__).resolve().parents[1] / "art"
-W, H = 1500, 2100 # 7.5 x 9.25 in at 200 dpi
-
-# ---------------------------------------------------------------------------
-# Palette ("Reactive Java at dusk" — espresso base, amber hero, green spark)
-# ---------------------------------------------------------------------------
-INK = "#1B1610" # espresso base — background
-DEEP = "#2A2014" # deeper espresso layer — layering / accent nodes
-AMBER = "#E8A23A" # amber hero — Java's warmth
-AMBER_BRT = "#F4C24E" # bright amber — highlights / pulse glints
-AMBER_DEEP = "#C8801F" # deep amber — dim network lines / shadowed strokes
-GREEN = "#43B02A" # firefly green (spark) — brand continuity
-GREEN_DEEP = "#2C8A1C" # deep green
-GREEN_MID = "#2C8A1C" # mid green — node fills (kept name for callers)
-GREEN_DIM = "#2C8A1C" # dim green — subtle peer-ring lines
-CREAM = "#F3ECDD" # cream — body / title text on dark
-WHITE = "#F3ECDD" # title text (cream, kept name for callers)
-LIGHT = "#F3ECDD" # cream — "by Example" sits in green, motes in cream
-MUTED = "#C9B896" # warm muted — publisher / secondary text
-MUTED_LT = "#C9B896" # warm muted — subtitle
-
-FONT = "Avenir Next,Avenir,Helvetica Neue,Helvetica,Arial,sans-serif"
-
-
-# ---------------------------------------------------------------------------
-# Network motif helpers
-# ---------------------------------------------------------------------------
-def hex_points(cx: float, cy: float, r: float) -> str:
- """Return SVG polygon points string for a flat-top hexagon."""
- pts = []
- for i in range(6):
- angle = math.radians(30 + 60 * i) # flat-top orientation
- pts.append(f"{cx + r * math.cos(angle):.2f},{cy + r * math.sin(angle):.2f}")
- return " ".join(pts)
-
-
-def node(cx: float, cy: float, r: float, fill: str, stroke: str,
- stroke_w: float = 2.0, opacity: float = 1.0) -> str:
- pts = hex_points(cx, cy, r)
- op = f' opacity="{opacity}"' if opacity < 1.0 else ""
- return (
- f''
- )
-
-
-def quad_path(x1: float, y1: float, x2: float, y2: float) -> str:
- """Smooth quadratic bezier between two points, gently curved outward."""
- mx, my = (x1 + x2) / 2, (y1 + y2) / 2
- dx, dy = x2 - x1, y2 - y1
- cpx = mx - dy * 0.18
- cpy = my + dx * 0.18
- return f"M{x1:.1f},{y1:.1f} Q{cpx:.1f},{cpy:.1f} {x2:.1f},{y2:.1f}"
-
-
-def firefly_mark(cx: float, cy: float, scale: float = 1.0) -> str:
- """A clean, self-contained firefly/spark mark — the cover's brand emblem.
-
- A tilted firefly with a softly glowing abdomen and swept-back wings, sitting
- in a faint amber bloom. Drawn entirely inline (no external image), so the
- cover is reproducible from this script alone.
- """
- return (
- f''
- # ambient bloom rings
- f''
- f''
- f''
- # light trail curving in from the lower-left (two strokes = a taper)
- f''
- f''
- # the firefly, gently tilted for life
- ''
- f''
- f''
- # wings, swept back and translucent (warm cream)
- f''
- f''
- # glowing abdomen
- f''
- f''
- # dark thorax + head with a green rim (brand spark)
- f''
- f''
- # antennae
- f''
- f''
- ''
- # satellite motes — amber glints + one green spark
- f''
- f''
- f''
- ''
- )
-
-
-# ---------------------------------------------------------------------------
-# Build SVG
-# ---------------------------------------------------------------------------
-def build_svg() -> str:
- # -------------------------------------------------------------------------
- # EDA Network motif (illustration zone: y = 80 ... 1080)
- # Hub: center, slightly above midpoint of illustration zone
- # -------------------------------------------------------------------------
- HUB_X, HUB_Y, HUB_R = 750, 580, 72
-
- # Satellite nodes: (cx, cy, radius, label)
- SATS = [
- (260, 210, 44, "CMD"),
- (650, 185, 40, "EVT"),
- (1080, 240, 44, "SAGA"),
- (1220, 540, 38, "Q"),
- (1110, 870, 44, "HTTP"),
- (390, 930, 40, "DATA"),
- (150, 610, 38, "MSG"),
- ]
-
- # Small accent nodes (no labels, atmospheric depth)
- ACCENT_NODES = [
- (500, 100, 22),
- (930, 130, 18),
- (1350, 360, 20),
- (1370, 760, 17),
- (980, 1020, 19),
- (200, 990, 16),
- (80, 400, 18),
- ]
-
- # Peer-to-peer connections (satellite ring, one hop)
- CONNECTIONS_PEER = [(0, 1), (1, 2), (2, 3), (3, 4), (4, 5), (5, 6), (6, 0)]
-
- # SVG fragment list
- parts: list[str] = []
-
- # 1. Background fill
- parts.append(f'')
-
- # 2. Very faint amber hex-grid texture over whole canvas
- parts.append(f'')
- for row in range(15):
- for col in range(10):
- gx = col * 185 - 60
- gy = row * 165 + (80 if col % 2 else 0) - 30
- pts = hex_points(gx, gy, 78)
- parts.append(f'')
- parts.append('')
-
- # 3. Radial amber glow behind hub (defined inline; hoisted to )
- grad_id = "hubglow"
- parts.append(
- f''
- f''
- f''
- f''
- )
- parts.append(f'')
-
- # 4. Peer connection lines (dim green ring)
- parts.append(
- f''
- )
- for (a, b) in CONNECTIONS_PEER:
- sx, sy = SATS[a][0], SATS[a][1]
- ex, ey = SATS[b][0], SATS[b][1]
- parts.append(f'')
- parts.append('')
-
- # 5. Hub spokes — amber, brighter and thicker (the hero radiates)
- parts.append(
- f''
- )
- for (sx, sy, _, _) in SATS:
- parts.append(f'')
- parts.append('')
-
- # 6. Event-pulse dots along spokes at 1/3 and 2/3
- PULSE_COLORS = [AMBER, GREEN, AMBER, GREEN, AMBER, GREEN, AMBER]
- for i, (sx, sy, _, _) in enumerate(SATS):
- col = PULSE_COLORS[i % len(PULSE_COLORS)]
- for t, r_dot, op_dot in [(0.32, 6, 0.90), (0.65, 4, 0.55)]:
- px = HUB_X + (sx - HUB_X) * t
- py = HUB_Y + (sy - HUB_Y) * t
- parts.append(
- f''
- )
-
- # 7. Accent nodes (tiny, atmospheric — espresso fill, dim green rim)
- for (ax, ay, ar) in ACCENT_NODES:
- parts.append(node(ax, ay, ar, DEEP, GREEN_DIM, 1.4, opacity=0.55))
-
- # 8. Satellite nodes (green-mid) with green outer ring + cream label
- for (sx, sy, sr, lbl) in SATS:
- parts.append(node(sx, sy, sr + 9, "none", GREEN, 1.2, opacity=0.30))
- parts.append(node(sx, sy, sr, GREEN_MID, GREEN, 2.2))
- fs = 23 if len(lbl) <= 3 else 19
- parts.append(
- f'{lbl}'
- )
-
- # 9. Hub node — the firefly mark sits at the network's heart.
- # Green rims for brand continuity, an amber inner ring for the hero glow.
- parts.append(node(HUB_X, HUB_Y, HUB_R + 16, "none", GREEN, 1.8, opacity=0.42))
- parts.append(node(HUB_X, HUB_Y, HUB_R + 4, "none", GREEN, 2.8, opacity=0.78))
- parts.append(node(HUB_X, HUB_Y, HUB_R, GREEN_MID, AMBER, 3.8))
- # the drawn firefly emblem, centered on the hub
- parts.append(firefly_mark(HUB_X, HUB_Y, scale=0.62))
-
- # 10. Atmospheric micro-labels (subtle, spaced out)
- MICRO = [
- (500, 76, "EVENTS"),
- (1320, 315, "ASYNC"),
- (160, 375, "REACTIVE"),
- (960, 1050, "CQRS"),
- ]
- for (mx, my, mlbl) in MICRO:
- parts.append(
- f'{mlbl}'
- )
-
- # -------------------------------------------------------------------------
- # Amber + green divider rules
- # -------------------------------------------------------------------------
- RULE_Y = 1110
- parts.append(
- f''
- )
- parts.append(
- f''
- )
-
- # -------------------------------------------------------------------------
- # Publisher label — top edge, small caps, generous letter-spacing
- # -------------------------------------------------------------------------
- parts.append(
- f''
- f'FIREFLY SOFTWARE FOUNDATION'
- )
-
- # -------------------------------------------------------------------------
- # Title block
- # -------------------------------------------------------------------------
- TY = RULE_Y + 68 # baseline anchor of first title line
-
- # "Firefly for" — heavy, cream
- parts.append(
- f'Firefly for'
- )
-
- # "Java" — the BIG amber-gold hero (the standout word)
- parts.append(
- f'Java'
- )
-
- # Thin green spark separator under "Java" (brand continuity)
- RULE2_Y = TY + 326
- parts.append(
- f''
- )
-
- # "by Example" — firefly-green, demi-bold
- parts.append(
- f'by Example'
- )
-
- # Subtitle (two lines)
- SUB_Y = RULE2_Y + 188
- for i, line in enumerate([
- "Reactive Microservices with Spring Boot,",
- "WebFlux & the Firefly Framework",
- ]):
- parts.append(
- f'{_xml_escape(line)}'
- )
-
- # -------------------------------------------------------------------------
- # Assemble: hoist gradient elements into
- # -------------------------------------------------------------------------
- defs_tags = ("\n'
- f'{"".join(defs_parts)}\n'
- + "\n".join(body_parts)
- + "\n"
- )
- return svg
-
-
-def main() -> None:
+NAMES = ("cover", "cover-es", "back-cover", "back-cover-es")
+SIZE = (1500, 1850)
+
+
+def main(argv: list[str] | None = None) -> None:
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument("--source", type=Path, help="Canonical brand-kit 11-Books/java directory")
+ args = parser.parse_args(argv)
+ source = args.source or ART
+ for name in NAMES:
+ svg = source / f"{name}.svg"
+ root = ET.fromstring(svg.read_bytes())
+ if [float(n) for n in root.get("viewBox", "").split()] != [0, 0, *SIZE]:
+ raise ValueError(f"{svg}: expected 1500 × 1850 viewBox (7.5 × 9.25 inch trim)")
+ if any(node.tag.rsplit("}", 1)[-1] == "text" for node in root.iter()):
+ raise ValueError(f"{svg}: canonical cover typography must be outlined")
+ png = source / f"{name}.png"
+ if png.exists():
+ with Image.open(png) as image:
+ if image.size != SIZE:
+ raise ValueError(f"{png}: expected {SIZE} pixels")
ART.mkdir(parents=True, exist_ok=True)
- svg = build_svg()
- (ART / "cover.svg").write_text(svg, encoding="utf-8")
- cairosvg.svg2png(
- bytestring=svg.encode(),
- write_to=str(ART / "cover.png"),
- output_width=W,
- output_height=H,
- )
- print("wrote cover.svg and cover.png")
+ for name in NAMES:
+ svg, png = source / f"{name}.svg", source / f"{name}.png"
+ target_svg, target_png = ART / svg.name, ART / png.name
+ if svg.resolve() != target_svg.resolve():
+ shutil.copyfile(svg, target_svg)
+ if png.exists():
+ if png.resolve() != target_png.resolve():
+ shutil.copyfile(png, target_png)
+ else:
+ cairosvg.svg2png(bytestring=svg.read_bytes(), write_to=str(target_png),
+ output_width=SIZE[0], output_height=SIZE[1])
+ print("Validated EN/ES front and back covers: 1500 × 1850, outlined canonical artwork")
if __name__ == "__main__":
diff --git a/build/gen_diagrams.py b/build/gen_diagrams.py
index de6b32f..e5d8964 100644
--- a/build/gen_diagrams.py
+++ b/build/gen_diagrams.py
@@ -1,4 +1,4 @@
-"""Generate the on-brand inline concept diagrams for *Firefly for Rust by Example*.
+"""Generate the on-brand inline concept diagrams for *jafly by example*.
These are the in-text technical figures (NOT the chapter openers — see
``gen_openers.py`` for those). Each diagram is a self-contained ````
@@ -7,14 +7,14 @@
Design language (shared with the cover, the openers, and ``theme/tokens.css``):
- * Cards are rounded rects ``rx=9..10``, cream fill ``#fdf6ea`` (or ``#fffaf0``),
- stroke ``#e0cda8`` width ``1.5``. Accent/highlight cards use fill ``#fff6e6``
- stroke ``#e0b96a``.
- * Connectors are rust ``#d4793a`` ``stroke-width=3`` with arrowheads drawn as
+ * Cards are rounded rects ``rx=9..10``, cream fill ``#f3f1eb`` (or ``#ffffff``),
+ stroke ``#bfb8ab`` width ``1.5``. Accent/highlight cards use fill ``#fff0d9``
+ stroke ``#ffb34a``.
+ * Connectors are rust ``#855414`` ``stroke-width=3`` with arrowheads drawn as
explicit ```` triangles (never ````).
- * Titles ``#2a1d10`` / ``#3a2a1c`` in the sans stack; sub-labels ``#7a6450``
- smaller; code/type tokens in the mono stack. Gold accents ``#f6a821`` /
- ``#ffc24a`` used sparingly.
+ * Titles ``#10110f`` / ``#272820`` in the sans stack; sub-labels ``#62645b``
+ smaller; code/type tokens in the mono stack. Gold accents ``#ffb34a`` /
+ ``#ffb34a`` used sparingly.
WeasyPrint SVG constraints (violating these breaks PDF rendering):
@@ -29,7 +29,8 @@
computed from a few constants, not hand-tuned per figure.
Run: python build/gen_diagrams.py (writes art/diagrams/*.svg, bare SVG)
-The book EMBEDS the inline ``figure_*()`` strings; the bare files are for preview.
+These legacy concept previews are not included in the Java manifests.
+Do not insert them into the manuscript without a separate technical review.
"""
from __future__ import annotations
@@ -40,26 +41,26 @@
ART = Path(__file__).resolve().parents[1] / "art" / "diagrams"
# --- palette (mirrors tokens.css + the existing inline figures) --------------
-FIELD = "#fdf6ea" # cream card fill
-FIELD2 = "#fffaf0" # lighter cream card fill
-ACCENT = "#fff6e6" # highlight/accent card fill
-ACCENT_S = "#e0b96a" # highlight/accent card stroke
-CARD_S = "#e0cda8" # default card stroke
-RUST = "#d4793a" # connectors, accent stroke
-RUST_D = "#b5531f" # deeper rust (arrowheads/emphasis)
-AMBER = "#f6a821"
-AMBER_B = "#ffc24a"
+FIELD = "#f3f1eb" # cream card fill
+FIELD2 = "#ffffff" # lighter cream card fill
+ACCENT = "#fff0d9" # highlight/accent card fill
+ACCENT_S = "#ffb34a" # highlight/accent card stroke
+CARD_S = "#bfb8ab" # default card stroke
+RUST = "#855414" # connectors, accent stroke
+RUST_D = "#855414" # deeper rust (arrowheads/emphasis)
+AMBER = "#ffb34a"
+AMBER_B = "#ffb34a"
GREEN = "#1f8a4c" # query / success accent (matches admin "green" badges)
GREEN_BG = "#ecf9f0"
BLUE = "#2563c9" # command / note accent (matches admin "blue" badges)
BLUE_BG = "#eef4ff"
RED = "#b03a2e" # failure / compensation (matches existing saga figs)
RED_BG = "#fdecea"
-TITLE = "#2a1d10" # card titles
-TITLE2 = "#3a2a1c" # secondary titles
-SUB = "#7a6450" # sub-labels
-LANE_BG = "#f7ecd8" # swimlane background
-LANE_S = "#e6d4b0" # swimlane stroke
+TITLE = "#10110f" # card titles
+TITLE2 = "#272820" # secondary titles
+SUB = "#62645b" # sub-labels
+LANE_BG = "#f3f1eb" # swimlane background
+LANE_S = "#bfb8ab" # swimlane stroke
FONT = "Avenir Next,Avenir,Helvetica Neue,Helvetica,Arial,sans-serif"
MONO = "SF Mono,JetBrains Mono,Menlo,Consolas,monospace"
@@ -100,7 +101,7 @@ def card(x, y, w, h, title, *, sub=None, mono=False, accent=False,
out = [
# soft drop shadow plate, then the card
f'',
+ f'rx="9" fill="#bfb8ab" opacity="0.22"/>',
f'',
f'')
+ f'fill="#bfb8ab" opacity="0.22"/>')
b.append(f'')
b.append(f'')
diff --git a/build/gen_openers.py b/build/gen_openers.py
index a13fa9b..a0e6169 100644
--- a/build/gen_openers.py
+++ b/build/gen_openers.py
@@ -1,237 +1,34 @@
-"""Generate on-brand chapter-opener SVGs for *Firefly for Java by Example*.
+"""Generate compact chapter banners using the approved, unmodified jafly logo.
-Each opener is a 720x300 banner sharing one visual language with the cover:
-a warm espresso field with a faint event-mesh, an amber accent bar, and a darker
-espresso panel on the right holding a glowing **firefly** emblem (the same
-bioluminescent motif as the cover). The "Reactive Java at dusk" palette — espresso
-base, amber-gold hero, firefly-green spark — is constant so the set reads as a
-family; a per-chapter abstract "constellation" varies the left-hand field so no
-two openers look identical — no chapter-specific text is required.
-
-This writes EXACTLY the files the manifests reference: art/openers/ch01.svg
-through art/openers/ch24.svg (24 files). The prelude and appendices have no
-opener in book.yaml / book.es.yaml, so none are generated for them.
-
-Run: build/.venv/bin/python build/gen_openers.py (writes art/openers/chNN.svg)
+The embedded PNG comes directly from the shared brand kit; its outlined SVG
+master remains alongside it in art/brand. Numbers work in both book languages.
"""
from __future__ import annotations
-import math
-import random
+import base64
from pathlib import Path
-from xml.sax.saxutils import escape as _xml_escape
-
-ART = Path(__file__).resolve().parents[1] / "art" / "openers"
-# How many openers to emit — kept in lockstep with the manifests (ch01..chNN).
+ROOT = Path(__file__).resolve().parents[1]
+ART = ROOT / "art" / "openers"
N_CHAPTERS = 24
-def esc(s: str) -> str:
- """XML-escape SVG text content (these SVGs are inlined into XHTML, so raw
- & / < / > would make the chapter document malformed)."""
- return _xml_escape(str(s))
-
-
-# ---- palette ("Reactive Java at dusk"; shared with the cover) --------------
-FIELD1 = "#1B1610" # espresso base — field, top
-FIELD2 = "#2A2014" # deeper espresso — layering / node fills
-PANEL1 = "#211A12" # darker espresso panel, top
-PANEL2 = "#14100A" # darker espresso panel, bottom
-GREEN = "#43b02a" # firefly green — node rims / spark
-GREEN_B = "#5fd13a" # bright green accent
-GREEN_D = "#2c8a1c" # deep green — peer-ring mesh lines
-AMBER = "#e8a23a" # amber hero — spokes / firefly glow
-AMBER_B = "#f4c24e" # bright amber
-AMBER_D = "#c8801f" # deep amber
-CREAM = "#f3ecdd" # cream — text on the field
-LIGHT = "#f3ecdd" # cream — hub glyph (kept name for callers)
-MUTED = "#c9b896" # warm muted — secondary text
-
-W, H = 720, 300
-# the night-sky panel on the right
-PX, PY, PW, PH = 490, 14, 216, 272
-FONT = "Avenir Next,Avenir,Helvetica Neue,Helvetica,Arial,sans-serif"
-MONO = "SF Mono,JetBrains Mono,Menlo,Consolas,monospace"
-
-
-def header(label: str) -> str:
- return (
- f''
- )
-
-
-def defs() -> str:
- # NOTE: WeasyPrint's SVG engine drops gradients when a /
- # shares the , and resolves objectBoundingBox gradients unreliably.
- # So we use ONLY userSpaceOnUse gradients here and fake glows with stacked
- # translucent circles.
- return (
- ''
- f''
- f''
- f''
- # the darker panel gradient:
- f''
- f''
- f''
- # root-space vertical amber gradient for the accent bar (the hero spark):
- f''
- f''
- f''
- ''
- )
-
-
-def emblem() -> str:
- """The constant glowing-firefly emblem on the panel (right), matching the
- cover's bioluminescent mark."""
- cx, cy = 601, 150
- return (
- f''
- # ambient bloom on the panel
- f''
- f''
- # light trail curving in from lower-left (two strokes = a taper)
- f''
- f''
- # the firefly, gently tilted for life
- ''
- f''
- f''
- # wings, swept back and translucent (warm cream)
- f''
- f''
- # glowing abdomen
- f''
- f''
- # dark thorax + head with a green rim (brand)
- f''
- f''
- # antennae
- f''
- f''
- ''
- # satellite motes (one bioluminescent green, matching the cover)
- f''
- f''
- f''
- ''
- )
-
-
-def frame(num: str) -> str:
- """Forest-green field, green accent bar, darker panel + motes, kicker."""
- return (
- f''
- f''
- # darker panel on the right
- f''
- f''
- # a few distant motes inside the panel
- f''
- f''
- f''
- f''
- f''
- # chapter number — a TOP-left kicker, clear of the scene below it
- f'{esc(num)}'
- f''
- )
-
-
-def constellation(seed: int) -> str:
- """A per-chapter abstract event-mesh on the left-hand field: a small set of
- hexagonal nodes joined by gently curved spokes, with amber/green pulse dots
- travelling along them. Deterministic from ``seed`` so each chapter differs
- yet the whole set stays one coherent family. Stays within x < 470 so it
- never collides with the panel."""
- rng = random.Random(seed)
-
- def hexagon(cx, cy, r, fill, stroke, sw=2.0, op=1.0):
- pts = " ".join(
- f"{cx + r*math.cos(math.radians(30 + 60*i)):.1f},"
- f"{cy + r*math.sin(math.radians(30 + 60*i)):.1f}"
- for i in range(6)
- )
- o = f' opacity="{op}"' if op < 1.0 else ""
- return f''
-
- def quad(x1, y1, x2, y2):
- mx, my = (x1 + x2) / 2, (y1 + y2) / 2
- dx, dy = x2 - x1, y2 - y1
- return f"M{x1:.1f},{y1:.1f} Q{mx - dy*0.18:.1f},{my + dx*0.18:.1f} {x2:.1f},{y2:.1f}"
-
- # central hub, biased toward the middle-left of the field
- hub = (rng.uniform(190, 250), rng.uniform(140, 170))
- n_sat = rng.randint(4, 6)
- sats = []
- for k in range(n_sat):
- ang = (2 * math.pi * k / n_sat) + rng.uniform(-0.3, 0.3)
- dist = rng.uniform(95, 150)
- sx = max(60, min(440, hub[0] + dist * math.cos(ang)))
- sy = max(70, min(238, hub[1] + dist * math.sin(ang)))
- sats.append((sx, sy, rng.uniform(16, 24)))
-
- parts: list[str] = []
- # faint atmospheric accent nodes
- for _ in range(rng.randint(3, 5)):
- ax, ay = rng.uniform(60, 450), rng.uniform(60, 240)
- parts.append(hexagon(ax, ay, rng.uniform(7, 12), FIELD2, GREEN_D, 1.2, op=0.6))
- # spokes from hub to each satellite — amber (echoes the cover's hero spokes)
- parts.append(f'')
- for (sx, sy, _) in sats:
- parts.append(f'')
- parts.append('')
- # peer ring (satellite to next satellite), dim
- parts.append(f'')
- for i in range(len(sats)):
- a, b = sats[i], sats[(i + 1) % len(sats)]
- parts.append(f'')
- parts.append('')
- # pulse dots travelling along the spokes
- for j, (sx, sy, _) in enumerate(sats):
- col = AMBER if j % 2 == 0 else GREEN_B
- for t, r_dot, op in [(0.34, 4.5, 0.9), (0.66, 3.0, 0.55)]:
- px = hub[0] + (sx - hub[0]) * t
- py = hub[1] + (sy - hub[1]) * t
- parts.append(f'')
- # satellite nodes
- for (sx, sy, sr) in sats:
- parts.append(hexagon(sx, sy, sr + 6, "none", GREEN, 1.1, op=0.25))
- parts.append(hexagon(sx, sy, sr, FIELD2, GREEN, 2.0))
- # hub node — slightly larger, amber-rimmed (echoes the cover hub)
- parts.append(hexagon(hub[0], hub[1], 30, "none", GREEN, 1.6, op=0.4))
- parts.append(hexagon(hub[0], hub[1], 22, GREEN_D, AMBER, 2.6))
- parts.append(
- f'F'
- )
- return "".join(parts)
-
-
def build_one(num: int) -> str:
- """num is the 1-based chapter number (1..N_CHAPTERS)."""
- kicker = f"CHAPTER {num}"
- label = f"Firefly for Java by Example — opener for chapter {num}"
- return (
- header(label) + defs() + frame(kicker)
- + constellation(seed=1000 + num)
- + emblem() + "\n"
- )
+ logo = base64.b64encode((ROOT / "art/brand/logo-dark.png").read_bytes()).decode()
+ return (f''
+ ''
+ ''
+ f'{num:02d}'
+ f''
+ '\n')
def main() -> None:
ART.mkdir(parents=True, exist_ok=True)
for num in range(1, N_CHAPTERS + 1):
(ART / f"ch{num:02d}.svg").write_text(build_one(num), encoding="utf-8")
- print(f"wrote {N_CHAPTERS} chapter openers (ch01..ch{N_CHAPTERS:02d}) to {ART}")
+ print(f"Wrote {N_CHAPTERS} approved-brand chapter openers to {ART}")
if __name__ == "__main__":
diff --git a/build/md.py b/build/md.py
index c09e2c0..26e2fd9 100644
--- a/build/md.py
+++ b/build/md.py
@@ -1,4 +1,4 @@
-"""Markdown -> HTML for *Firefly for Java by Example*.
+"""Markdown -> HTML for *jafly by example*.
Custom block directives on top of python-markdown:
::: figure |