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.* + + + jafly by Firefly + -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 @@ +jafly - ContraportadaFramework by Firefly \ 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'

{escape(label)}

{body}' -def _divider_html(eyebrow: str, ptitle: str) -> str: +def _divider_html(eyebrow: str, ptitle: str, logo: str = "") -> str: eb = f'{escape(eyebrow)}' if eyebrow else "" - return (f'
{eb}' + return (f'
{logo}{eb}' f'

{escape(ptitle)}

') 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'{quoteattr(alt)}/' + # ---- 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'{quoteattr(alt)}/') + 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 | (single line; inlines the SVG) @@ -86,21 +86,21 @@ def _listing(self, file_label: str, cap: str | None, code: str) -> str: # Professional inline SVG icons injected into callout titles (no emoji). _ADM_ICON = { "note": '', + '' + '' + '', "tip": '', + 'c.06-.52.33-.99.78-1.32A5.6 5.6 0 0 0 10 2.4z" fill="none" stroke="#855414" stroke-width="1.5"/>' + '', "warning": '', + '' + '' + '', "spring": '', } diff --git a/build/pdf.py b/build/pdf.py index 368478f..070857c 100644 --- a/build/pdf.py +++ b/build/pdf.py @@ -6,5 +6,5 @@ def render_pdf(full_html: str, base_url: Path, css_paths: list[Path], out: Path) -> Path: out = Path(out); out.parent.mkdir(parents=True, exist_ok=True) HTML(string=full_html, base_url=str(base_url)).write_pdf( - str(out), stylesheets=[CSS(filename=str(p)) for p in css_paths]) + str(out), stylesheets=[CSS(filename=str(p)) for p in css_paths], custom_metadata=True) return out diff --git a/build/requirements.txt b/build/requirements.txt index fd80996..8a11c5b 100644 --- a/build/requirements.txt +++ b/build/requirements.txt @@ -3,4 +3,5 @@ markdown==3.10.3 pygments==2.21.0 pyyaml==6.0.3 cairosvg==2.9.1 +pillow==12.3.0 pytest==9.1.1 diff --git a/build/verify_code.py b/build/verify_code.py index 30c618d..5aac7e9 100644 --- a/build/verify_code.py +++ b/build/verify_code.py @@ -1,7 +1,7 @@ """Verify that every ``::: listing