Add KDP 6×9 grayscale print pipeline and ship print PDF.
Parallel to the A4 eBook build: print assets, pdf/print templates, print QA mode, /pdf-print skill, and rebuilt interior PDF.
This commit is contained in:
1 parent
0624756b2d
commit
bd721b3df1
21 files changed
+1498
-49
No files matched your search
@@ -0,0 +1,17 @@
|
||||
---
|
||||
description: Baut die KDP-Print-Interior-PDF (6×9 Grayscale) und führt Print-QA aus
|
||||
argument-hint: "[full|assets-only|qa-only]"
|
||||
---
|
||||
# Print-PDF (KDP Interior) bauen und prüfen
|
||||
|
||||
Führe den PDF-Print-Skill aus. Lade `.grok/skills/pdf-print/SKILL.md`.
|
||||
|
||||
Argument:
|
||||
|
||||
| Wert | Aktion |
|
||||
|------|--------|
|
||||
| `full` (default) | `just pdf-print` + `just qa-print` |
|
||||
| `assets-only` | nur `just print-assets` |
|
||||
| `qa-only` | nur `just qa-print` |
|
||||
|
||||
Die eBook-PDF (`just pdf`) bleibt unberührt, sofern nicht `just clean` verlangt wird.
|
||||
+69
-12
@@ -9,14 +9,19 @@ Build-Regeln für die PDF-Pipeline dieses Buchs.
|
||||
| Kommando | Wirkung | Wann |
|
||||
|----------|---------|------|
|
||||
| `just assemble` | `00–18-*.md` → `build/book.md` | Debug Assembler / Callouts |
|
||||
| `just pdf` | assemble + pandoc/lualatex → `dist/grok-build-praxisleitfaden.pdf` | nach inhaltlichen Änderungen (wenn Build verlangt) |
|
||||
| `just pdf` | assemble + pandoc/lualatex → `dist/grok-build-praxisleitfaden.pdf` | eBook A4 Farbe (wenn Build verlangt) |
|
||||
| `just pdf-draft` | Draft-PDF mit Wasserzeichen | Review-Runden |
|
||||
| `just pdf-plain` | ohne gestyltes Template | schneller Layout-Vergleich |
|
||||
| `just qa` | `scripts/check_pdf.py` | nach `just pdf` |
|
||||
| `just all` | pdf + qa | Release-nahe Prüfung |
|
||||
| `just pdf-print` | assemble + Grayscale-Assets + KDP 6×9 → `dist/…-print.pdf` | Paperback-Interior |
|
||||
| `just print-assets` | nur `scripts/prepare_print_assets.py` | Debug Bild-Prep |
|
||||
| `just pdf-print-optimize` | optional GS Gray/prepress auf Print-PDF | vor KDP-Upload |
|
||||
| `just qa` | `scripts/check_pdf.py` (eBook) | nach `just pdf` |
|
||||
| `just qa-print` | `scripts/check_pdf.py --mode print` | nach `just pdf-print` |
|
||||
| `just all` | pdf + qa | Release-nahe Prüfung eBook |
|
||||
| `just all-print` | pdf-print + qa-print | Release-nahe Prüfung Print |
|
||||
| `just clean` | `build/` + dist-PDF-Artefakte | sauberer Neu-Build |
|
||||
|
||||
PDF nur bauen, wenn der Task es verlangt.
|
||||
PDF nur bauen, wenn der Task es verlangt. eBook und Print sind **parallele** Outputs.
|
||||
|
||||
## Pipeline
|
||||
|
||||
@@ -31,7 +36,7 @@ PDF nur bauen, wenn der Task es verlangt.
|
||||
-H pdf/styles/guide-colors.tex
|
||||
--metadata-file=pdf/metadata.yaml
|
||||
--lua-filter=pdf/filters/tables.lua
|
||||
--resource-path=.:beispiele:vorlagen:assets:assets/illustrations:assets/cover
|
||||
--resource-path=.:beispiele:vorlagen:assets:…
|
||||
│
|
||||
▼
|
||||
dist/grok-build-praxisleitfaden.pdf
|
||||
@@ -40,6 +45,40 @@ PDF nur bauen, wenn der Task es verlangt.
|
||||
QA: Existenz, Seitenzahl (Soft 40–280), Keywords, Fonts
|
||||
```
|
||||
|
||||
### Print / KDP (Paperback Interior)
|
||||
|
||||
```text
|
||||
build/book.md (gleicher Assembler)
|
||||
│
|
||||
▼ scripts/prepare_print_assets.py
|
||||
build/print-assets/ (Grayscale-JPGs, MD-Pfad-Spiegel assets/…)
|
||||
│
|
||||
▼ pandoc --pdf-engine=lualatex
|
||||
--template=pdf/templates/guide.tex (print-interior: true)
|
||||
-H pdf/print/print-colors.tex
|
||||
--metadata-file=pdf/print/metadata.yaml
|
||||
--lua-filter=pdf/filters/tables.lua (-V printmode=true)
|
||||
--syntax-highlighting=monochrome (Code ohne RGB)
|
||||
resource-path: build/print-assets:… zuerst
|
||||
│
|
||||
▼
|
||||
dist/grok-build-praxisleitfaden-print.pdf
|
||||
│
|
||||
▼ scripts/check_pdf.py --mode print
|
||||
QA: 6×9, Fonts, Seiten, Keywords + Gray-Stage + manuelle Hinweise
|
||||
```
|
||||
|
||||
| Print-Datei | Rolle |
|
||||
|-------------|--------|
|
||||
| `pdf/print/metadata.yaml` | 6×9 geometry, twoside, 10 pt, linestretch 1,22 |
|
||||
| `pdf/print/print-colors.tex` | Grayscale-Palette (gleiche Farbnamen) |
|
||||
| `pdf/print/print-preamble.tex` | Sans-Body, Kopf/Fuß außen, Callouts B/W, Cover-Titelseite |
|
||||
| `scripts/prepare_print_assets.py` | RGB → Grayscale + Kontrast |
|
||||
| `pdf/print/QA-NOTES.md` | Scorecard + manuelle Prüfpunkte |
|
||||
|
||||
**KDP-Spec (Interior):** Trim 6×9 in · White Paper · kein Bleed · inner **0,65"** · outer **0,60"** · top **0,70"** · bottom **0,65"** · Titelseite Cover-Safe-Frame **0,30"** · Figuren Gin **0,87** (1280 px ≈ 300 DPI) · openany · Body **10 pt** (TeX Gyre Heros Scale=1.0) · linestretch **1,22** · Box/Code 9 pt · Schriften embedded. Optional: `just pdf-print-optimize` (Ghostscript Gray/prepress).
|
||||
**Cover-PDF** (Bleed/Spine) ist **nicht** Teil dieser Pipeline. Interior-Titelseite nutzt `assets/cover/cover-front-print.jpg` (Fallback: `COVER-01B-full.jpg` / `cover-front.jpg`).
|
||||
|
||||
## Callout-Transform (Assembler)
|
||||
|
||||
`scripts/assemble_book.py` mappt:
|
||||
@@ -56,29 +95,47 @@ Diese Marker im Manuskript nicht „verschönern“ oder in andere Markdown-Kons
|
||||
|------|-------|-------------|
|
||||
| `00–18-*.md` | Quellkapitel | ✅ ja |
|
||||
| `scripts/assemble_book.py` | Concat + Callouts | ⚠️ nur mit Auftrag |
|
||||
| `scripts/check_pdf.py` | QA | ⚠️ nur mit Auftrag |
|
||||
| `pdf/templates/*`, `pdf/filters/*`, `pdf/styles/*` | Layout | ⚠️ nur mit Auftrag |
|
||||
| `pdf/metadata.yaml`, `pdf/front/*` | Metadaten / Impressum | ✅ bei Meta-Tasks |
|
||||
| `assets/**` | Bilder | ✅ bei Illustrations-Tasks |
|
||||
| `scripts/check_pdf.py` | QA eBook + Print (`--mode`) | ⚠️ nur mit Auftrag |
|
||||
| `scripts/prepare_print_assets.py` | Grayscale-Assets für Print | ⚠️ nur mit Auftrag |
|
||||
| `pdf/templates/*`, `pdf/filters/*`, `pdf/styles/*` | Layout eBook (+ Template-Schalter Print) | ⚠️ nur mit Auftrag |
|
||||
| `pdf/print/*` | Layout KDP Interior | ⚠️ nur mit Auftrag |
|
||||
| `pdf/metadata.yaml` | Metadaten eBook | ✅ bei Meta-Tasks |
|
||||
| `pdf/print/metadata.yaml` | Metadaten Print | ✅ bei Print-Meta-Tasks |
|
||||
| `pdf/front/*` | Impressum | ✅ bei Meta-Tasks |
|
||||
| `assets/**` | Cover/Bilder | ✅ bei Illustrations-Tasks |
|
||||
| `build/`, `dist/*` | generiert | ❌ neu bauen, nicht handpatchen |
|
||||
|
||||
## QA (`just qa`)
|
||||
## QA (`just qa` / `just qa-print`)
|
||||
|
||||
**eBook (`just qa`):**
|
||||
|
||||
- PDF existiert und ist nicht leer
|
||||
- Soft-Seitenzahl: ca. 40–280 (Warnung außerhalb; wächst mit Kapitel-Füllung)
|
||||
- Soft-Seitenzahl: ca. 40–280
|
||||
- Keywords u. a.: Grok Build, AGENTS.md, config.toml, MCP, Plan Mode
|
||||
- Font-Embedding, falls `pdffonts` verfügbar
|
||||
|
||||
**Print (`just qa-print`):**
|
||||
|
||||
- wie eBook, plus **Page size ≈ 6×9 in** (432×648 pt)
|
||||
- Soft-Seitenzahl: ca. 100–320
|
||||
- Gray-Stage / optional `pdfimages` RGB-Fail
|
||||
- Ausgabe manueller Prüfpunkte (Diagramme, Tabellen, DPI)
|
||||
|
||||
## Fehlerbehandlung
|
||||
|
||||
| Symptom | Prüfen |
|
||||
|---------|--------|
|
||||
| `pandoc: command not found` | PATH / Installation |
|
||||
| `lualatex: command not found` | TeX Live auf dem Host |
|
||||
| Font not found (TeX Gyre Heros) | TeX Live Fonts / TeX Gyre |
|
||||
| Assembler-Fehler | Kapiteldateien in `CHAPTER_ORDER` vorhanden? |
|
||||
| fehlende Bilder im PDF | Pfade unter `assets/`, `resource-path` im justfile |
|
||||
| Print-Bilder farbig | Stage muss MD-Pfade spiegeln (`build/print-assets/assets/…`); resource-path `build/print-assets` zuerst; QA: `pdfimages -list` → color=gray |
|
||||
| Print-Code farbig | `--syntax-highlighting=monochrome` in `just pdf-print` (eBook-Default bleibt farbig) |
|
||||
| Print nicht 6×9 | `pdf/print/metadata.yaml` geometry; Template `print-interior` |
|
||||
| Callout fehlt im PDF | Label-Schreibweise im Markdown |
|
||||
| PIL/Pillow missing | `pip install pillow` für `print-assets` |
|
||||
|
||||
## Voraussetzungen
|
||||
|
||||
`pandoc`, `just`, TeX Live (LuaLaTeX, KOMA-Script `scrbook`), Fonts Charter + DejaVu.
|
||||
`pandoc`, `just`, TeX Live (LuaLaTeX, KOMA-Script `scrbook`), Fonts Charter + DejaVu (eBook), TeX Gyre Heros (Print), optional Pillow + Ghostscript + poppler (`pdffonts`/`pdfinfo`/`pdfimages`).
|
||||
@@ -21,11 +21,13 @@ Pipeline: Assembler → Pandoc → LuaLaTeX → QA. Details: `.grok/rules/bau-pi
|
||||
|
||||
| Art | Kommando | Output |
|
||||
|-----|----------|--------|
|
||||
| full (default) | `just pdf` | `dist/grok-build-praxisleitfaden.pdf` |
|
||||
| full (default) | `just pdf` | `dist/grok-build-praxisleitfaden.pdf` (A4 eBook) |
|
||||
| draft | `just pdf-draft` | `dist/grok-build-praxisleitfaden-draft.pdf` |
|
||||
| plain | `just pdf-plain` | `dist/grok-build-praxisleitfaden-plain.pdf` |
|
||||
| nur assemblieren | `just assemble` | `build/book.md` |
|
||||
|
||||
**KDP Print (6×9 Grayscale):** separater Skill `pdf-print` / `just pdf-print` — nicht dieser Skill.
|
||||
|
||||
## Ablauf
|
||||
|
||||
### 1. Klären und vorbereiten
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
---
|
||||
name: pdf-print
|
||||
description: >
|
||||
Baut die druckfertige KDP-Paperback-Interior-PDF (6×9 in, Grayscale) für
|
||||
„Grok Build – Der umfassende Praxisleitfaden“ und führt Print-QA aus.
|
||||
Nutzen bei: „Print-PDF“, „KDP Interior“, „Paperback bauen“, „6x9“,
|
||||
„pdf-print“, /pdf-print.
|
||||
---
|
||||
|
||||
# PDF-Print-Skill — KDP Interior (6×9 Grayscale)
|
||||
|
||||
Parallele Pipeline zur eBook-PDF. eBook bleibt `just pdf` / Skill `pdf-bauen`.
|
||||
|
||||
Details: `.grok/rules/bau-pipeline.md` → Abschnitt „Print / KDP“.
|
||||
|
||||
## Prinzipien
|
||||
|
||||
1. Denken vor dem Schreiben — Fehler analysieren, bevor blind neu gebaut wird.
|
||||
2. Einfachheit — nur die Build-Art, die nötig ist.
|
||||
3. Chirurgisch — während des Bauens keine Manuskript-Edits „nebenbei“.
|
||||
4. Zielgetrieben — fertig erst, wenn Print-PDF und Print-QA stehen.
|
||||
|
||||
## Spec (KDP Interior)
|
||||
|
||||
| Parameter | Wert |
|
||||
|-----------|------|
|
||||
| Trim | 6 × 9 in |
|
||||
| Farbe | Grayscale (White Paper); Code: `--syntax-highlighting=monochrome` |
|
||||
| Bleed | nein |
|
||||
| Ränder | inner **0,65"** · outer **0,60"** · top **0,70"** · bottom **1,00"** (Footer-Ink ≈ **0,65"**) |
|
||||
| Typo | Body **10 pt** (Scale=1.0) · Leading ~14,6 pt · Box/Code **9 pt** |
|
||||
| Titelseite | Cover im **0,30"**-Safe-Frame (KDP-Außen ≥ 0,25") |
|
||||
| Figuren | Gin `0,87\linewidth` → 1280 px ≈ 300 DPI |
|
||||
| Output | `dist/grok-build-praxisleitfaden-print.pdf` |
|
||||
| PDF | 1.4–1.7, Schriften embedded; optional GS Gray (`just pdf-print-optimize`) |
|
||||
|
||||
## Build-Arten
|
||||
|
||||
| Art | Kommando | Output |
|
||||
|-----|----------|--------|
|
||||
| full (default) | `just pdf-print` | assemble + grayscale assets + Print-PDF |
|
||||
| nur Assets | `just print-assets` | `build/print-assets/` |
|
||||
| GS-Optimize | `just pdf-print-optimize` | Gray/prepress-Nachlauf auf Print-PDF |
|
||||
| nur QA | `just qa-print` | Checks auf existierendes Print-PDF |
|
||||
| full + QA | `just all-print` | pdf-print + qa-print |
|
||||
|
||||
**eBook (unabhängig):** `just pdf` → `dist/grok-build-praxisleitfaden.pdf`
|
||||
|
||||
## Ablauf
|
||||
|
||||
### 1. Klären und vorbereiten
|
||||
|
||||
- Default: full Print-Build + QA.
|
||||
- Optional: `just clean` nur wenn sauberer Neu-Build gewünscht (löscht auch eBook-PDF in `dist/`).
|
||||
|
||||
### 2. Bauen
|
||||
|
||||
```bash
|
||||
just pdf-print
|
||||
```
|
||||
|
||||
Pipeline intern:
|
||||
|
||||
1. `scripts/assemble_book.py` → `build/book.md`
|
||||
2. `scripts/prepare_print_assets.py` → Grayscale-JPGs unter `build/print-assets/assets/…` (MD-Pfad-Spiegel)
|
||||
3. pandoc + LuaLaTeX mit `pdf/print/*` (6×9, twoside, hidelinks, monochrome Syntax)
|
||||
|
||||
**Typische Fehler:**
|
||||
|
||||
| Fehler | Ursache | nächster Schritt |
|
||||
|--------|---------|------------------|
|
||||
| `pandoc` / `lualatex` missing | Tool fehlt im PATH | Installation / Host-TeX |
|
||||
| Font not found (TeX Gyre Heros) | TeX Live unvollständig | `texlive-fonts-recommended` / TeX Gyre |
|
||||
| LaTeX-Error | print-preamble, geometry | Log in Terminal lesen |
|
||||
| PIL/Pillow missing | Asset-Prep | `pip install pillow` |
|
||||
| Overfull hbox | schmale Tabellen/Code | `tables.lua` / Code umbrechen |
|
||||
|
||||
→ Verifikation: `dist/grok-build-praxisleitfaden-print.pdf` existiert und > 0 Bytes.
|
||||
|
||||
### 3. QA
|
||||
|
||||
```bash
|
||||
just qa-print
|
||||
```
|
||||
|
||||
Prüft u. a.: Existenz, Seitenzahl (Soft 100–320), Font-Embedding, Keywords, **Page size ≈ 6×9 in**, Gray-Stage.
|
||||
|
||||
### 4. Ergebnis melden
|
||||
|
||||
```text
|
||||
## Build-Ergebnis (Print / KDP)
|
||||
**Datei:** dist/grok-build-praxisleitfaden-print.pdf
|
||||
**Format:** 6×9 in · Grayscale · twoside
|
||||
**Seiten:** …
|
||||
**QA:** bestanden / Warnungen / Fehler
|
||||
**eBook unverändert:** dist/grok-build-praxisleitfaden.pdf (falls nicht mit-clean)
|
||||
|
||||
### Änderungs-Checkliste (Pipeline)
|
||||
- [ ] Trim 6×9, Ränder inner 0,65", outer 0,60", top 0,70", bottom 1,00" (Footer-Ink ≈ 0,65")
|
||||
- [ ] Titelseite Cover-Safe-Frame ≥ 0,30"
|
||||
- [ ] Grayscale-Palette + grayscale Assets + monochrome Code-Highlight
|
||||
- [ ] Figuren Gin 0,87 → ~300 DPI bei 1280 px
|
||||
- [ ] Sans-Body TeX Gyre Heros **10 pt Scale=1.0**, Mono/Box/Code ~9 pt
|
||||
- [ ] Seitenzahlen außen; Kolumnentitel dezent
|
||||
- [ ] Schriften embedded; kein Media-Box-Overflow
|
||||
- [ ] Keine unnötigen Kapitel-Leerseiten (openany)
|
||||
|
||||
### Manuelle Nacharbeit (bei Bedarf)
|
||||
- Diagramme mit reinem Farbkanal (K-01, K-05, M-*, S-*)
|
||||
- Cover: natives ≥1800×2700 (cover-front-print.jpg)
|
||||
- Sehr breite Tabellen im schmalen Satzspiegel
|
||||
- Optional: just pdf-print-optimize vor KDP-Upload
|
||||
```
|
||||
|
||||
## Aufräumen
|
||||
|
||||
```bash
|
||||
just clean
|
||||
```
|
||||
|
||||
Löscht `build/` (inkl. print-assets) und `dist/*.pdf`.
|
||||
|
||||
## Abgrenzung
|
||||
|
||||
| Skill / Command | Rolle |
|
||||
|-----------------|--------|
|
||||
| `pdf-bauen` / `/pdf` | A4 eBook (Farbe) |
|
||||
| **`pdf-print` / `/pdf-print`** | KDP Interior 6×9 B/W |
|
||||
| KDP-**Cover** | separat (Bleed + Spine) — nicht dieser Skill |
|
||||
+22
-2
@@ -6,6 +6,7 @@ Dieses Dokument hält fest, was aus den Schwesterbüchern übernommen und was f
|
||||
|--------|----------------|
|
||||
| `../ki-claude` | Der Claude-Code-Harness |
|
||||
| `../ki-codex` | Der Codex-Harness |
|
||||
| `../ki-eit` | Agents mit Angular, Spring Boot & Quarkus |
|
||||
|
||||
Stand der Adaption: **August 2026**.
|
||||
|
||||
@@ -129,8 +130,27 @@ Der Auftrag nannte u. a. `book/` und `examples/`. Zugunsten der Schwesterproje
|
||||
|
||||
| | Claude | Codex | Grok |
|
||||
|-|--------|-------|------|
|
||||
| Dateiname | `dist/claude-code-harness.pdf` | `dist/codex-harness.pdf` | `dist/grok-build-praxisleitfaden.pdf` |
|
||||
| Soft-Seitenzahl QA | 30–120 | ähnlich schlank | 40–280 (19 Kapitel) |
|
||||
| Dateiname eBook | `dist/claude-code-harness.pdf` | `dist/codex-harness.pdf` | `dist/grok-build-praxisleitfaden.pdf` |
|
||||
| Dateiname Print | — | — | `dist/grok-build-praxisleitfaden-print.pdf` (KDP 6×9) |
|
||||
| Soft-Seitenzahl QA | 30–120 | ähnlich schlank | eBook 40–280 · Print 100–320 |
|
||||
|
||||
---
|
||||
|
||||
## 3b. Übernommen aus `ki-eit` (Print / KDP Interior, August 2026)
|
||||
|
||||
Parallele Paperback-Interior-Pipeline (nicht eBook-ersetzend).
|
||||
|
||||
| Element | Übernahme / Anpassung |
|
||||
|---------|------------------------|
|
||||
| `just pdf-print`, `print-assets`, `qa-print`, `all-print`, `pdf-print-optimize` | ja — Output-Slug Grok |
|
||||
| `pdf/print/*` (metadata, colors, preamble, QA-NOTES) | ja — Titel/Cover-Pfade Grok |
|
||||
| `scripts/prepare_print_assets.py` | ja — Cover-Picker: `cover-front-print.jpg` → `COVER-01B-full.jpg` → `cover-front.jpg` |
|
||||
| `check_pdf.py --mode print` | ja — Keywords/Soft-Seiten Grok |
|
||||
| `guide.tex` `print-interior` + geometry-Zweig | ja |
|
||||
| `tables.lua` `printmode` | ja |
|
||||
| Skill `pdf-print` + `/pdf-print` | ja — Buchtitel/Slug angepasst |
|
||||
| TeX Gyre Heros 10 pt Body (Print) | ja (eBook bleibt Charter) |
|
||||
| Manuskript-Tabellen-Splits à la EIT Anhang C | **nicht** — nur Pipeline; Overflows separat |
|
||||
|
||||
### Was (noch) nicht mitgenommen wurde
|
||||
|
||||
|
||||
@@ -45,7 +45,7 @@ Projekt-Regeln für Coding-Agents. Karpathy-Prinzipien, adaptiert auf ein deutsc
|
||||
|
||||
**Autoren-Harness vs. Leser-Vorlagen:** Root-`AGENTS.md` / `CLAUDE.md` / `.grok/` steuern **Agenten, die am Buch arbeiten**. `vorlagen/` und `beispiele/` sind **Content für Leser** – nicht mit dem Autoren-Harness vermischen.
|
||||
|
||||
**Autoren-Skills (`.grok/skills/`):** `pdf-bauen`, `lektorat`, `kapitel-review`, `humanizer`, `update-pics`. Slash-Commands: `.grok/commands/` (`/pdf`, `/lektorat`, `/review`, `/status`, `/update-pics`). Schreib- und Build-Details: `.grok/rules/`.
|
||||
**Autoren-Skills (`.grok/skills/`):** `pdf-bauen`, `pdf-print`, `lektorat`, `kapitel-review`, `humanizer`, `update-pics`. Slash-Commands: `.grok/commands/` (`/pdf`, `/pdf-print`, `/lektorat`, `/review`, `/status`, `/update-pics`). Schreib- und Build-Details: `.grok/rules/`.
|
||||
|
||||
**Intern (nicht Manuskript):** `prompts/` ist Autoren-Material.
|
||||
|
||||
@@ -70,15 +70,18 @@ Operativer Kurzkompass (Build, Do/Don’t): Root-`CLAUDE.md`. Details: `.grok/ru
|
||||
|
||||
```bash
|
||||
just assemble # 00–18 → build/book.md
|
||||
just pdf # assemble + pandoc/lualatex → dist/grok-build-praxisleitfaden.pdf
|
||||
just pdf # assemble + pandoc/lualatex → dist/grok-build-praxisleitfaden.pdf (A4 eBook)
|
||||
just pdf-draft # Draft-PDF mit Wasserzeichen
|
||||
just pdf-plain # ohne gestyltes Template
|
||||
just qa # scripts/check_pdf.py
|
||||
just pdf-print # KDP Interior 6×9 Grayscale → dist/…-print.pdf
|
||||
just qa # scripts/check_pdf.py (eBook)
|
||||
just qa-print # scripts/check_pdf.py --mode print
|
||||
just all # pdf + qa
|
||||
just all-print # pdf-print + qa-print
|
||||
just clean # build/ und dist-PDF-Artefakte
|
||||
```
|
||||
|
||||
PDF nur bauen, wenn der Task es verlangt (nach inhaltlichen Änderungen oder explizit). Nach `just pdf` bei Bedarf `just qa`.
|
||||
PDF nur bauen, wenn der Task es verlangt (nach inhaltlichen Änderungen oder explizit). Nach `just pdf` / `just pdf-print` bei Bedarf `just qa` bzw. `just qa-print`. eBook und Print sind parallele Outputs.
|
||||
|
||||
**Voraussetzungen:** `pandoc`, `just`, TeX Live (LuaLaTeX, KOMA `scrbook`), Fonts Charter + DejaVu.
|
||||
|
||||
@@ -131,7 +134,7 @@ PDF nur bauen, wenn der Task es verlangt (nach inhaltlichen Änderungen oder exp
|
||||
|
||||
### Nicht ohne Auftrag anfassen
|
||||
|
||||
- `pdf/templates/*`, `pdf/filters/*`, `pdf/styles/*` (Layout-Pipeline)
|
||||
- `pdf/templates/*`, `pdf/filters/*`, `pdf/styles/*`, `pdf/print/*` (Layout-Pipeline)
|
||||
- Generierte `build/`-Inhalte (kommen aus dem Assembler)
|
||||
- Große Binaries unter `dist/`, `assets/` ohne konkreten Bild-/Build-Task
|
||||
- `beispiele/*` nur ändern, wenn das Beispiel selbst Thema ist
|
||||
|
||||
@@ -8,17 +8,20 @@ Dies ist ein **deutschsprachiges Praxisbuch** über Grok Build – kein App-Stac
|
||||
|
||||
```bash
|
||||
just assemble # Kapitel → build/book.md
|
||||
just pdf # → dist/grok-build-praxisleitfaden.pdf
|
||||
just pdf # → dist/grok-build-praxisleitfaden.pdf (A4 eBook)
|
||||
just pdf-draft # Draft mit Wasserzeichen
|
||||
just pdf-plain # ohne gestyltes Template
|
||||
just qa # PDF-Checks (Seiten, Keywords, Fonts)
|
||||
just pdf-print # KDP Interior 6×9 Grayscale → dist/…-print.pdf
|
||||
just qa # PDF-Checks eBook (Seiten, Keywords, Fonts)
|
||||
just qa-print # PDF-Checks Print (6×9, Gray-Stage)
|
||||
just all # pdf + qa
|
||||
just all-print # pdf-print + qa-print
|
||||
just clean
|
||||
```
|
||||
|
||||
Voraussetzungen: `pandoc`, `just`, TeX Live (LuaLaTeX, KOMA `scrbook`), Fonts Charter + DejaVu.
|
||||
Voraussetzungen: `pandoc`, `just`, TeX Live (LuaLaTeX, KOMA `scrbook`), Fonts Charter + DejaVu (eBook), TeX Gyre Heros (Print), optional Pillow.
|
||||
|
||||
PDF nur bauen, wenn der Task es verlangt. Nach dem Build bei Bedarf `just qa`.
|
||||
PDF nur bauen, wenn der Task es verlangt. Nach dem Build bei Bedarf `just qa` bzw. `just qa-print`.
|
||||
|
||||
## Layout (Source of Truth)
|
||||
|
||||
@@ -51,4 +54,4 @@ PDF nur bauen, wenn der Task es verlangt. Nach dem Build bei Bedarf `just qa`.
|
||||
- Keine erfundenen npm/poetry/React-Stacks für dieses Repo.
|
||||
- `pdf/templates/*`, `pdf/filters/*`, `pdf/styles/*` nur bei explizitem Auftrag.
|
||||
- Plan vor größeren Mehrdatei-Änderungen; Erfolgskriterien prüfbar formulieren.
|
||||
- Details: `.grok/rules/`; Skills: `pdf-bauen`, `lektorat`, `kapitel-review`, `humanizer`, `update-pics`.
|
||||
- Details: `.grok/rules/`; Skills: `pdf-bauen`, `pdf-print`, `lektorat`, `kapitel-review`, `humanizer`, `update-pics`.
|
||||
+153
@@ -0,0 +1,153 @@
|
||||
# Handoff — Grok Build Praxisleitfaden (`ki-grok`)
|
||||
|
||||
Stand: **2026-08-08** · Branch: `main` · **kein Commit** der Print-Arbeit (Working Tree dirty)
|
||||
|
||||
Zweck: nächste Session ohne Kontextverlust fortsetzen. Autoren-Harness; kein Manuskript-Inhalt.
|
||||
|
||||
---
|
||||
|
||||
## Projekt-Kompass
|
||||
|
||||
| | |
|
||||
|--|--|
|
||||
| **Titel** | *Grok Build – Der umfassende Praxisleitfaden* |
|
||||
| **Repo** | `/Volumes/SSD2TB/books/ki-grok` |
|
||||
| **Kapitel** | `00-vorwort.md` … `18-anhang-schnellreferenz.md` (kanonisch) |
|
||||
| **eBook PDF** | `dist/grok-build-praxisleitfaden.pdf` (A4, Farbe) |
|
||||
| **Print PDF** | `dist/grok-build-praxisleitfaden-print.pdf` (6×9, Grayscale, KDP Interior) |
|
||||
| **Schwestern** | `../ki-claude`, `../ki-codex`, `../ki-eit` — siehe `ADAPTIONEN.md` |
|
||||
| **Fakten-Stand Buch** | Juli–Aug 2026, CLI 0.2.x |
|
||||
|
||||
---
|
||||
|
||||
## Erledigt in dieser Arbeitsspur
|
||||
|
||||
### 1. Print-Pipeline aus `ki-eit` portiert
|
||||
|
||||
Parallele KDP-Interior-Pipeline neben dem bestehenden A4-eBook.
|
||||
|
||||
| Artefakt | Pfad / Befehl |
|
||||
|----------|----------------|
|
||||
| Just-Targets | `just pdf-print`, `print-assets`, `qa-print`, `all-print`, `pdf-print-optimize` |
|
||||
| Metadata / Farben / Preamble | `pdf/print/{metadata.yaml,print-colors.tex,print-preamble.tex,QA-NOTES.md}` |
|
||||
| Asset-Prep | `scripts/prepare_print_assets.py` → `build/print-assets/` |
|
||||
| Template | `pdf/templates/guide.tex` — Branch `print-interior` (twoside, openany) |
|
||||
| Tabellen-Filter | `pdf/filters/tables.lua` — `printmode` |
|
||||
| QA | `scripts/check_pdf.py --mode print` (Soft-Seiten Print **100–320**) |
|
||||
| Skill / Command | `.grok/skills/pdf-print/`, `.grok/commands/pdf-print.md` |
|
||||
| Doku | `AGENTS.md`, `CLAUDE.md`, `README.md`, `ADAPTIONEN.md` §3b, `.grok/rules/bau-pipeline.md`, `pdf-bauen` Skill |
|
||||
|
||||
**Anpassungen vs. eit (nicht 1:1):** Output-Slug Grok, Keywords Grok, Cover-Picker `cover-front-print.jpg` → `COVER-01B-full.jpg` → `cover-front.jpg`, Soft-Seiten höher, resource-path `beispiele:vorlagen`.
|
||||
|
||||
### 2. Unteren Rand / Seitenzahl KDP-sicher
|
||||
|
||||
Messung **vor** Fix: unterster Tintenabstand **27 pt ≈ 0,375″** (zu knapp).
|
||||
|
||||
| Parameter | Alt | Neu (wie `ki-eit`) |
|
||||
|-----------|-----|---------------------|
|
||||
| `bottom` | 0,65″ | **1,00″** |
|
||||
| `footskip` | 0,28″ | **0,30″** |
|
||||
| Footer-Ink (gemessen) | ~27 pt | **~51 pt ≈ 0,708″** |
|
||||
|
||||
Dateien: `pdf/print/metadata.yaml`, Kommentare in `print-preamble.tex`, `QA-NOTES.md`, Skill, `check_pdf.py`-Hinweis.
|
||||
|
||||
**Verify (2026-08-08):**
|
||||
|
||||
```text
|
||||
just pdf-print && just qa-print # exit 0
|
||||
Seiten: 194 · 432×648 pt · ~3.0 MB
|
||||
Stichproben S. 11/51/101/150/180/190: bottom_ink = 51 pt (PASS ≥ 43)
|
||||
```
|
||||
|
||||
Unverändert gelassen: Trim 6×9, Body 10 pt / leading, inner/outer/top, Boxen/Gin, Manuskript, eBook.
|
||||
|
||||
---
|
||||
|
||||
## Aktueller Build-Stand
|
||||
|
||||
| Output | Status |
|
||||
|--------|--------|
|
||||
| eBook A4 | vorhanden (~143 S., Build ~16:55) — Smoke nach Port OK |
|
||||
| Print 6×9 | **194 S.**, Grayscale stage OK, QA exit 0 |
|
||||
| Soft-WARN Print | 13/24 Figuren **< 1280 px** (DPI Soft, kein Blocker) |
|
||||
| Cover stage | `COVER-01B-full` / staged 1680×2520 (QA Cover DPI OK; natives `cover-front-print.jpg` optional) |
|
||||
|
||||
### Wichtige geometry (Print) — nicht zurückdrehen
|
||||
|
||||
```yaml
|
||||
# pdf/print/metadata.yaml
|
||||
inner=0.65in · outer=0.60in · top=0.70in
|
||||
bottom=1.00in · footskip=0.30in · headsep=0.28in
|
||||
fontsize=10pt · linestretch 1.22 · toc-depth 1
|
||||
```
|
||||
|
||||
`geometry`-Semantik: Footer-Baseline ≈ `bottom − footskip` → Ink ≈ 0,65–0,71″.
|
||||
**Nicht** nur `bottom=0.65in` setzen — das drückt die Seitenzahl wieder unter 0,4″.
|
||||
|
||||
---
|
||||
|
||||
## Working Tree (uncommitted)
|
||||
|
||||
**Modified (u. a.):** `justfile`, `guide.tex`, `tables.lua`, `check_pdf.py`, `AGENTS.md`, `CLAUDE.md`, `README.md`, `ADAPTIONEN.md`, `.grok/rules/bau-pipeline.md`, `.grok/skills/pdf-bauen/SKILL.md`, eBook-`dist/…pdf`, evtl. `guide-preamble.tex`
|
||||
|
||||
**Untracked:** `pdf/print/`, `scripts/prepare_print_assets.py`, `.grok/skills/pdf-print/`, `.grok/commands/pdf-print.md`, `dist/grok-build-praxisleitfaden-print.pdf`, **diese** `HANDOFF.md`
|
||||
|
||||
Kein Auto-Commit vereinbart.
|
||||
|
||||
---
|
||||
|
||||
## Offene / optionale nächste Schritte
|
||||
|
||||
Priorisiert nach Nutzen:
|
||||
|
||||
1. **Commit** der Print-Pipeline + Margin-Fix (ein logischer Commit oder zwei: Pipeline / Margin)
|
||||
2. **Natives Print-Cover** `assets/cover/cover-front-print.jpg` (1800×2700 gray) — vermeidet Upscale-Hinweis
|
||||
3. Figuren **≥ 1280 px** Breite für 300-DPI-Ziel (Soft-WARN)
|
||||
4. Optional `just pdf-print-optimize` (Ghostscript Gray/prepress)
|
||||
5. Manuskript: breite Tabellen im 6×9-Spiegel (Soft; kein Umbau erzwungen)
|
||||
6. eBook nur bei Inhaltsänderungen neu bauen; Print-Layout berührt eBook nicht
|
||||
|
||||
**Nicht anfassen ohne Auftrag:** Manuskript-Kapitel umstrukturieren, eBook-Layout, KDP-Außenumschlag (Bleed/Spine) — Interior ≠ Cover-PDF.
|
||||
|
||||
---
|
||||
|
||||
## Befehle (Cheat Sheet)
|
||||
|
||||
```bash
|
||||
just assemble # Kapitel → build/book.md
|
||||
just pdf # eBook A4
|
||||
just pdf-print # Print 6×9 + prepare_print_assets
|
||||
just qa / just qa-print
|
||||
just all / just all-print
|
||||
just pdf-print-optimize
|
||||
```
|
||||
|
||||
Messung unterer Tintenabstand (Beispiel):
|
||||
|
||||
```bash
|
||||
# pdftoppm -gray -r 72 -f N -l N dist/…-print.pdf /tmp/p
|
||||
# unterste Zeile mit Pixel < 240 → margin_pt = 648 - y
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Einstiegs-Prompt (neue Session)
|
||||
|
||||
```text
|
||||
Repo: /Volumes/SSD2TB/books/ki-grok, Branch main.
|
||||
Lies HANDOFF.md. Print-Pipeline (ki-eit-Port) und KDP-Bottom-Fix
|
||||
(bottom=1.00in, footskip=0.30in, Ink≈51pt) sind fertig, QA grün, uncommitted.
|
||||
Nächste Aufgabe: [Commit | Cover print | Figuren DPI | …].
|
||||
eBook-Pipeline und Manuskript nicht anfassen, außer die Aufgabe verlangt es.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Do / Don’t (Session)
|
||||
|
||||
| Do | Don’t |
|
||||
|----|--------|
|
||||
| Print nur über `just pdf-print` | `build/` handpatchen |
|
||||
| Bei Margin-Fragen geometry in `pdf/print/metadata.yaml` | Footer in scrlayer „hochschieben“ ohne bottom zu erhöhen |
|
||||
| Soft-WARNs als optional tracken | Soft-DPI als Hard-Fail behandeln |
|
||||
| Schwester `ki-eit` als Layout-Referenz | Produktlogik/Curriculum aus eit in Grok-Kapitel mischen |
|
||||
@@ -117,12 +117,15 @@ Installationswege, Config-Keys und Slash-Commands können sich mit Releases änd
|
||||
|
||||
Pipeline analog zu [`../ki-claude`](../ki-claude): `just` + `pandoc` + `lualatex` + KOMA-Template.
|
||||
|
||||
**Voraussetzungen:** `pandoc`, `just`, TeX Live (LuaLaTeX, KOMA-Script `scrbook`), Fonts Charter + DejaVu.
|
||||
**Voraussetzungen:** `pandoc`, `just`, TeX Live (LuaLaTeX, KOMA-Script `scrbook`), Fonts Charter + DejaVu (eBook), TeX Gyre Heros (Print), optional Pillow.
|
||||
|
||||
```bash
|
||||
just pdf # → dist/grok-build-praxisleitfaden.pdf
|
||||
just qa # Seiten-/Font-/Keyword-Checks
|
||||
just all # pdf + qa
|
||||
just pdf # → dist/grok-build-praxisleitfaden.pdf (A4 eBook)
|
||||
just pdf-print # → dist/grok-build-praxisleitfaden-print.pdf (KDP 6×9 Grayscale)
|
||||
just qa # eBook-Checks
|
||||
just qa-print # Print-Checks (6×9, Gray-Stage)
|
||||
just all # pdf + qa
|
||||
just all-print # pdf-print + qa-print
|
||||
just clean
|
||||
```
|
||||
|
||||
@@ -131,7 +134,7 @@ Weitere Targets: `just pdf-draft`, `just pdf-plain`, `just assemble`.
|
||||
```text
|
||||
00–18-*.md → scripts/assemble_book.py → build/book.md
|
||||
↓ pandoc + template
|
||||
dist/*.pdf
|
||||
dist/*.pdf (eBook und/oder Print)
|
||||
```
|
||||
|
||||
## Weiterarbeiten am Projekt (Autoren-Harness)
|
||||
@@ -141,8 +144,8 @@ Weitere Targets: `just pdf-draft`, `just pdf-plain`, `just assemble`.
|
||||
| [`AGENTS.md`](AGENTS.md) | Prinzipien, Dateirollen, Qualitätsregeln |
|
||||
| [`CLAUDE.md`](CLAUDE.md) | Build/QA, Layout, Agent notes |
|
||||
| [`ADAPTIONEN.md`](ADAPTIONEN.md) | Schwesterprojekt-Mapping |
|
||||
| [`.grok/`](.grok/) | rules, skills (`pdf-bauen`, `lektorat`, `kapitel-review`), commands |
|
||||
| [`.grok/`](.grok/) | rules, skills (`pdf-bauen`, `pdf-print`, `lektorat`, …), commands |
|
||||
|
||||
**Autoren-Harness** (Root) steuert Agents, die am Buch arbeiten. **`vorlagen/`** und **`beispiele/`** sind Content für Leser des Buchs.
|
||||
|
||||
Typischer Mitautor-Workflow: Kapitel editieren → bei Bedarf `just pdf && just qa` → Lektorat/Review über Skills bzw. Grok-Commands (`/pdf`, `/lektorat`, `/review`, `/status`).
|
||||
Typischer Mitautor-Workflow: Kapitel editieren → bei Bedarf `just pdf && just qa` (bzw. Print) → Lektorat/Review über Skills bzw. Grok-Commands (`/pdf`, `/pdf-print`, `/lektorat`, `/review`, `/status`).
|
||||
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
@@ -1,13 +1,21 @@
|
||||
# Justfile — PDF-Pipeline für „Grok Build – Der umfassende Praxisleitfaden“
|
||||
#
|
||||
# Analog zu ../ki-claude (pandoc + lualatex + KOMA-Template + Farbpalette).
|
||||
# pandoc + lualatex + KOMA-Template.
|
||||
# TeX Live (KOMA-Script / scrbook) kommt vom Host.
|
||||
#
|
||||
# Outputs:
|
||||
# just pdf → dist/grok-build-praxisleitfaden.pdf (A4 eBook)
|
||||
# just pdf-print → dist/grok-build-praxisleitfaden-print.pdf (KDP 6×9 B/W)
|
||||
|
||||
default: pdf
|
||||
|
||||
# resource-path: illustrations + cover (+ content roots)
|
||||
_resource_path := ".:beispiele:vorlagen:assets:assets/illustrations:assets/cover"
|
||||
|
||||
# Print: grayscale stage mirrors MD paths (assets/illustrations/… under
|
||||
# build/print-assets/). First path wins → gray before RGB originals.
|
||||
_print_resource_path := "build/print-assets:.:beispiele:vorlagen:assets:assets/illustrations:assets/cover"
|
||||
|
||||
assemble:
|
||||
@python3 scripts/assemble_book.py
|
||||
|
||||
@@ -57,11 +65,52 @@ pdf-plain: assemble
|
||||
--toc \
|
||||
-o dist/grok-build-praxisleitfaden-plain.pdf
|
||||
|
||||
# --- KDP Paperback Interior (6×9, grayscale) --------------------------------
|
||||
|
||||
print-assets:
|
||||
@python3 scripts/prepare_print_assets.py
|
||||
|
||||
pdf-print: assemble print-assets
|
||||
@mkdir -p dist
|
||||
pandoc build/book.md \
|
||||
--pdf-engine=lualatex \
|
||||
--template=pdf/templates/guide.tex \
|
||||
-H pdf/print/print-colors.tex \
|
||||
--metadata-file=pdf/print/metadata.yaml \
|
||||
--lua-filter=pdf/filters/tables.lua \
|
||||
--resource-path={{_print_resource_path}} \
|
||||
--syntax-highlighting=monochrome \
|
||||
-V lang=de-DE \
|
||||
-V printmode=true \
|
||||
--toc \
|
||||
-o dist/grok-build-praxisleitfaden-print.pdf
|
||||
|
||||
# Optional: Ghostscript grayscale/prepress pass (KDP-freundlicher, best-effort)
|
||||
pdf-print-optimize:
|
||||
@test -f dist/grok-build-praxisleitfaden-print.pdf || (echo "Run just pdf-print first" && exit 1)
|
||||
@command -v gs >/dev/null || (echo "Ghostscript (gs) not found" && exit 1)
|
||||
gs -dBATCH -dNOPAUSE -dSAFER -sDEVICE=pdfwrite \
|
||||
-dCompatibilityLevel=1.5 \
|
||||
-dPDFSETTINGS=/prepress \
|
||||
-dColorConversionStrategy=/Gray \
|
||||
-dProcessColorModel=/DeviceGray \
|
||||
-dEmbedAllFonts=true \
|
||||
-dSubsetFonts=true \
|
||||
-sOutputFile=dist/grok-build-praxisleitfaden-print-opt.pdf \
|
||||
dist/grok-build-praxisleitfaden-print.pdf
|
||||
@mv dist/grok-build-praxisleitfaden-print-opt.pdf dist/grok-build-praxisleitfaden-print.pdf
|
||||
@echo "optimized → dist/grok-build-praxisleitfaden-print.pdf"
|
||||
|
||||
qa:
|
||||
@python3 scripts/check_pdf.py
|
||||
|
||||
qa-print:
|
||||
@python3 scripts/check_pdf.py --pdf dist/grok-build-praxisleitfaden-print.pdf --mode print
|
||||
|
||||
all: pdf qa
|
||||
|
||||
all-print: pdf-print qa-print
|
||||
|
||||
clean:
|
||||
@rm -rf build
|
||||
@rm -f dist/*.pdf dist/*.aux dist/*.log dist/*.out dist/*.toc
|
||||
|
||||
+49
-4
@@ -246,8 +246,43 @@ local function max_plain_len(plain)
|
||||
return m
|
||||
end
|
||||
|
||||
local function wrap_style(ncols, max_cell)
|
||||
local function is_print_mode()
|
||||
-- Set via pandoc -V printmode=true (just pdf-print)
|
||||
if PANDOC_WRITER_OPTIONS and PANDOC_WRITER_OPTIONS.variables then
|
||||
local v = PANDOC_WRITER_OPTIONS.variables["printmode"]
|
||||
if v == true or v == "true" or v == "yes" or v == "1" then
|
||||
return true
|
||||
end
|
||||
end
|
||||
local env = os.getenv("PRINT_MODE")
|
||||
return env == "1" or env == "true"
|
||||
end
|
||||
|
||||
local function wrap_style(ncols, max_cell, print_mode)
|
||||
-- Returns (font_cmd or nil, tabcolsep or nil)
|
||||
-- Print (6×9): narrower text block → slightly more aggressive scaling
|
||||
if print_mode then
|
||||
if ncols >= 6 then
|
||||
return "\\scriptsize", "1.6pt"
|
||||
end
|
||||
if ncols >= 5 then
|
||||
return "\\scriptsize", "1.8pt"
|
||||
end
|
||||
if ncols >= 4 then
|
||||
return "\\footnotesize", "2.2pt"
|
||||
end
|
||||
if ncols >= 3 then
|
||||
if max_cell >= 70 then
|
||||
return "\\footnotesize", "2.6pt"
|
||||
end
|
||||
return "\\small", "3.0pt"
|
||||
end
|
||||
if ncols == 2 and max_cell >= 100 then
|
||||
return "\\small", "3.2pt"
|
||||
end
|
||||
return nil, nil
|
||||
end
|
||||
|
||||
if ncols >= 6 then
|
||||
return "\\footnotesize", "2.5pt"
|
||||
end
|
||||
@@ -273,6 +308,7 @@ function Table(tbl)
|
||||
return nil
|
||||
end
|
||||
|
||||
local print_mode = is_print_mode()
|
||||
local plain, code = collect_column_stats(tbl, ncols)
|
||||
local widths = rebalance_widths(ncols, plain, code)
|
||||
apply_widths(tbl, widths)
|
||||
@@ -282,7 +318,7 @@ function Table(tbl)
|
||||
walk_cell_contents(cell, make_code_breakable)
|
||||
end)
|
||||
|
||||
local font_cmd, sep = wrap_style(ncols, max_plain_len(plain))
|
||||
local font_cmd, sep = wrap_style(ncols, max_plain_len(plain), print_mode)
|
||||
-- Always wrap multi-column tables: emergencystretch reduces overfull
|
||||
-- hboxes in narrow p-columns (German compounds, long headers).
|
||||
if ncols < 2 and not font_cmd and not sep then
|
||||
@@ -296,8 +332,17 @@ function Table(tbl)
|
||||
if sep then
|
||||
open = open .. "\\setlength{\\tabcolsep}{" .. sep .. "}"
|
||||
end
|
||||
if ncols >= 3 then
|
||||
open = open .. "\\sloppy\\setlength{\\emergencystretch}{2.5em}"
|
||||
local stretch = print_mode and "3.2em" or "2.5em"
|
||||
if ncols >= 2 then
|
||||
open = open .. "\\sloppy\\setlength{\\emergencystretch}{" .. stretch .. "}"
|
||||
end
|
||||
-- Print: ragged-right p-columns avoid word-spacing collapse that looks like
|
||||
-- overlapping headers when long German compounds / paths refuse to break.
|
||||
if print_mode and ncols >= 3 then
|
||||
open = open
|
||||
.. "\\raggedright"
|
||||
.. "\\hyphenpenalty=50\\exhyphenpenalty=50"
|
||||
.. "\\setlength{\\LTleft}{0pt}\\setlength{\\LTright}{0pt}"
|
||||
end
|
||||
|
||||
return {
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
# Print-Interior QA-Notes (KDP 6×9)
|
||||
|
||||
Grok Build – Der umfassende Praxisleitfaden. Stand: Pipeline-Übernahme aus `ki-eit`.
|
||||
|
||||
## Build
|
||||
|
||||
| Item | Wert |
|
||||
|------|------|
|
||||
| Output | `dist/grok-build-praxisleitfaden-print.pdf` |
|
||||
| Trim | 6 × 9 in (432 × 648 pt) |
|
||||
| Soft-Seiten | 100–320 (aktuell ~194; eBook A4 separat ~143) |
|
||||
| Farbe | Grayscale (Palette + Assets); Code `--syntax-highlighting=monochrome` |
|
||||
| Schriften | TeX Gyre Heros **Scale=1.0** + DejaVu Sans Mono, embedded |
|
||||
| PDF | LuaTeX; optional `just pdf-print-optimize` (GS Gray/prepress) |
|
||||
| Ränder | inner **0,65"** · outer **0,60"** · top **0,70"** · bottom **1,00"** (Textkörper) |
|
||||
| Kopf/Fuß | headsep 0,28" · footskip 0,30" (Seitenzahl-Ink ≈ **0,65"** vom Trim) |
|
||||
| Titelseite | Cover im **0,30"**-Safe-Frame (KDP-Außen ≥ 0,25") |
|
||||
| Figuren | Gin `width=0,87\linewidth` → 1280 px ≈ **300 DPI** (Textbreite 4,75") |
|
||||
| Typo | Body **10 pt** · linestretch **1,22** (~14,6 pt) · Box/Code/Tabelle **9 pt** |
|
||||
|
||||
## KDP-Scorecard (Interior)
|
||||
|
||||
| Kriterium | KDP | Status |
|
||||
|-----------|-----|--------|
|
||||
| Trim 6×9, kein Bleed | ja | ✅ Pipeline |
|
||||
| Gutter ≥ 0,375" (24–150 S.) | 0,65" | ✅ |
|
||||
| Außen/oben/unten ≥ 0,25" | 0,60 / 0,70 / Text 1,00 · Footer-Ink ≈ 0,65 | ✅ |
|
||||
| Titelseite ≥ 0,25" | 0,30" | ✅ |
|
||||
| Fonts embedded | ja | QA |
|
||||
| Min. Schrift ≥ 7 pt | Body 10 / Box·Code 9 | ✅ |
|
||||
| Bilder ≥ 300 DPI | Gin 0,87 + Cover ≥ 1620 px | Ziel; Soft-WARN |
|
||||
| Cover-PDF (Spine/Bleed) | separat | ❌ out of scope |
|
||||
|
||||
## Änderungs-Checkliste (Pipeline)
|
||||
|
||||
- [x] Parallele Print-Pipeline (`just pdf-print` / `qa-print`)
|
||||
- [x] Skill `pdf-print` + Command `/pdf-print`
|
||||
- [x] eBook unverändert (A4, Farbe)
|
||||
- [x] twoside, openany, Seitenzahlen außen unten
|
||||
- [x] Cover-Titelseite mit KDP-Safe-Frame (0,30")
|
||||
- [x] Grayscale-Assets via `prepare_print_assets.py`
|
||||
- [x] Code monochrome (`--syntax-highlighting=monochrome`)
|
||||
- [x] Tabellen-Filter Print-Modus (`-V printmode=true`)
|
||||
- [x] Cover-Picker: `cover-front-print.jpg` → `COVER-01B-full.jpg` → `cover-front.jpg`
|
||||
- [x] Optional GS-Optimize (`just pdf-print-optimize`)
|
||||
|
||||
## Manuelle Nacharbeit empfohlen
|
||||
|
||||
### Abbildungen (B/W-Lesbarkeit)
|
||||
|
||||
Display-DPI ist per Gin abgesichert. Bei scharfer Print-Kritik:
|
||||
|
||||
| Priorität | IDs | Warum |
|
||||
|-----------|-----|--------|
|
||||
| Hoch | K-01, K-05, M-01–M-08 | Farbe oft Unterscheidungskanal |
|
||||
| Mittel | S-01–S-07, N-01–N-03 | dichte Labels / Cyan-Akzente |
|
||||
| OK | Abbildungen ≥ 1280 px Breite | Gin 0,87 ≈ 300 DPI |
|
||||
|
||||
Maßnahmen: neu rendern (Schraffur statt reine Farbe) oder Imagine ≥ 1800 px Breite, dann `just print-assets` + `just pdf-print`.
|
||||
|
||||
### Cover-Quelle
|
||||
|
||||
- Aktuell oft nur `COVER-01B-full.jpg` (kann unter 1620×2520 liegen → LANCZOS-Upscale + WARN).
|
||||
- Besser: natives `assets/cover/cover-front-print.jpg` ≥ 1800×2700 ohne Upscale.
|
||||
|
||||
### Sonstiges
|
||||
|
||||
- **Seite 2** nach Titelseite: leeres Verso — üblich im Paperback.
|
||||
- **KDP-Außenumschlag** (Bleed + Spine) ist **nicht** dieses Interior.
|
||||
- PDF/X-1a: nicht erzwungen; `pdf-print-optimize` ist best-effort Grayscale.
|
||||
- Breite Tabellen im 6×9-Spiegel: `tables.lua` skaliert Print ab 3–4 Spalten aggressiver.
|
||||
|
||||
## Kommandos
|
||||
|
||||
```bash
|
||||
just pdf-print # Interior bauen
|
||||
just pdf-print-optimize # optional GS Gray/prepress
|
||||
just qa-print # Checks
|
||||
just all-print # pdf-print + qa-print
|
||||
just pdf # eBook (unabhängig)
|
||||
```
|
||||
@@ -0,0 +1,31 @@
|
||||
title: "Grok Build – Der umfassende Praxisleitfaden"
|
||||
subtitle: "Der Coding-Agent von xAI richtig einsetzen"
|
||||
author: "Martin Richardt"
|
||||
date: "Juli 2026"
|
||||
lang: de-DE
|
||||
documentclass: scrbook
|
||||
# KDP Paperback Interior: 6×9 in, twoside, openany (keine erzwungenen Leerseiten)
|
||||
print-interior: true
|
||||
# KOMA key-value (bare "10.5pt" is ignored by scrbook)
|
||||
classoption:
|
||||
- fontsize=10pt
|
||||
# 10 pt body · nominal baselineskip ~12 pt × 1.22 ≈ 14.6 pt leading
|
||||
linestretch: "1.22"
|
||||
geometry:
|
||||
- paperwidth=6in
|
||||
- paperheight=9in
|
||||
# Professionelle KDP-Ränder (Typeblock ≥0.65″ vom Trim; Gutter > Außen)
|
||||
- inner=0.65in
|
||||
- outer=0.60in
|
||||
- top=0.70in
|
||||
# bottom trägt Textkörper + Footer: Footer-Baseline ≈ bottom − footskip
|
||||
# Glyph-Unterkante Seitenzahl ≥ 0,65″ vom Trim → bottom 1.00, footskip 0.30
|
||||
# (Baseline ≈ 0,70″; 9-pt-Ziffern ~3 pt unter Baseline → ≈ 0,65″ Ink)
|
||||
- bottom=1.00in
|
||||
- headheight=14pt
|
||||
- headsep=0.28in
|
||||
- footskip=0.30in
|
||||
has-frontmatter: true
|
||||
toc-title: "Inhaltsverzeichnis"
|
||||
# LaTeX tocdepth: 0=nur Kapitel, 1=Kapitel+## (schmaler Spiegel)
|
||||
toc-depth: 1
|
||||
@@ -0,0 +1,18 @@
|
||||
% print-colors.tex — Grayscale-Palette für KDP-Interior (White Paper)
|
||||
% Gleiche Farbnamen wie guide-colors.tex → Callouts/Templates bleiben kompatibel.
|
||||
% Geladen via Pandoc `-H`. Vorausgesetzt: xcolor (Template lädt es).
|
||||
|
||||
\IfFileExists{xcolor.sty}{\usepackage{xcolor}}{}
|
||||
|
||||
% --- Grayscale-Palette (druckbar, hoher Kontrast) ---------------------------
|
||||
\definecolor{nexttokenblue}{gray}{0.12} % fast schwarz (H1, Kopf)
|
||||
\definecolor{nexttokengold}{gray}{0.38} % mittleres Grau (Linien, Akzent)
|
||||
\definecolor{nexttokenburgundy}{gray}{0.22} % dunkles Grau (H2)
|
||||
\definecolor{nexttokentext}{gray}{0.08} % Fließtext
|
||||
\definecolor{nexttokenslate}{gray}{0.32} % Zitate, Hinweise
|
||||
\definecolor{nexttokenparchment}{gray}{0.96} % Box-Fond
|
||||
\definecolor{nexttokenlink}{gray}{0.12}
|
||||
|
||||
\colorlet{nexttokenblue!soft}{nexttokenblue!18!white}
|
||||
\colorlet{nexttokengold!soft}{nexttokengold!25!white}
|
||||
\colorlet{nexttokenburgundy!soft}{nexttokenburgundy!12!white}
|
||||
@@ -0,0 +1,317 @@
|
||||
% print-preamble.tex — KDP Paperback Interior (6×9, grayscale)
|
||||
% Ersetzt guide-preamble.tex im Print-Build (Template: print-interior: true).
|
||||
%
|
||||
% Layout-Ziel (Professionell / KDP-tauglich):
|
||||
% Body TeX Gyre Heros 10 pt, Leading ~14.5–15 pt
|
||||
% Ränder: inner 0.65" · outer 0.60" · top 0.70" · bottom 1.00"
|
||||
% footskip 0.30" → Seitenzahl-Ink ≈ 0.65" vom Trim (KDP-sicher)
|
||||
% Boxen/Tabellen/Code 9 pt; etwas mehr Weißraum um Floats & Callouts
|
||||
|
||||
% ===========================================================================
|
||||
% 0) SPRACHE / SILBENTRENNUNG
|
||||
% ===========================================================================
|
||||
\usepackage[shorthands=off,main=ngerman]{babel}
|
||||
|
||||
% ===========================================================================
|
||||
% 1) FONTS — Sans-Body (echte 10 pt), Mono für Code/Pfade
|
||||
% Scale=1.0: MatchLowercase skaliert Heros auf LM-x-Höhe (~8 pt optisch) — vermeiden
|
||||
% ===========================================================================
|
||||
\directlua{
|
||||
luaotfload.add_fallback("printfb", {"DejaVu Sans:mode=node;"})
|
||||
}
|
||||
\setmainfont{TeX Gyre Heros}[
|
||||
Ligatures=TeX,
|
||||
Scale=1.0,
|
||||
RawFeature={fallback=printfb}
|
||||
]
|
||||
\setsansfont{TeX Gyre Heros}[
|
||||
Ligatures=TeX,
|
||||
Scale=1.0,
|
||||
RawFeature={fallback=printfb}
|
||||
]
|
||||
% DejaVu Sans Mono: gute Unicode-Abdeckung für Dateipfade / CLI
|
||||
\setmonofont{DejaVu Sans Mono}[
|
||||
Scale=0.95,
|
||||
RawFeature={fallback=printfb}
|
||||
]
|
||||
% Body 10 pt + linestretch 1.22 (metadata) → Leading ≈ 14.6 pt
|
||||
% (kein zusätzliches \fontsize{10}{14.5}: würde mit setstretch multiplizieren)
|
||||
|
||||
% Lange URLs umbrechen (Impressum / Links) — vor hyperref im Template geladen
|
||||
\usepackage{xurl}
|
||||
|
||||
% Schmaler 6×9-Spiegel: weniger Overfull-\hbox (lange Pfade/Komposita)
|
||||
\sloppy
|
||||
\setlength{\emergencystretch}{3em}
|
||||
\tolerance=2000
|
||||
|
||||
% ===========================================================================
|
||||
% 2) KOMA-Script Überschriften (Grayscale-Farben aus print-colors.tex)
|
||||
% Chapter ~16.5 pt · Section ~12.5 pt · Subsection ~11 pt
|
||||
% ===========================================================================
|
||||
\addtokomafont{chapter}{\color{nexttokenblue}\sffamily\bfseries\fontsize{16.5}{20}\selectfont}
|
||||
\addtokomafont{section}{\color{nexttokenblue}\sffamily\bfseries\fontsize{12.5}{15}\selectfont}
|
||||
\addtokomafont{subsection}{\color{nexttokenburgundy}\sffamily\bfseries\fontsize{11}{13.5}\selectfont}
|
||||
\addtokomafont{disposition}{\normalcolor}
|
||||
|
||||
\RedeclareSectionCommand[beforeskip=1.4\baselineskip,afterskip=.6\baselineskip]{chapter}
|
||||
\RedeclareSectionCommand[beforeskip=1.1\baselineskip,afterskip=.4\baselineskip]{section}
|
||||
\RedeclareSectionCommand[beforeskip=0.85\baselineskip,afterskip=.28\baselineskip]{subsection}
|
||||
|
||||
% ===========================================================================
|
||||
% 3) Kopf-/Fußzeile — twoside: außen; Seitenzahl nicht im Gutter
|
||||
% ===========================================================================
|
||||
\usepackage{scrlayer-scrpage}
|
||||
\clearpairofpagestyles
|
||||
\ohead{\headmark}
|
||||
\ihead{}
|
||||
\chead{}
|
||||
\cfoot{}
|
||||
\ofoot*{\pagemark}
|
||||
\ifoot{}
|
||||
\setkomafont{pagehead}{\color{nexttokenslate}\sffamily\fontsize{8.5}{10}\selectfont}
|
||||
\setkomafont{pagenumber}{\color{nexttokenblue}\sffamily\bfseries\fontsize{9}{11}\selectfont}
|
||||
\KOMAoptions{headsepline=.4pt}
|
||||
\addtokomafont{headsepline}{\color{nexttokengold}}
|
||||
% Leerseiten (Verso nach Titel o.ä.) ohne Kolumnentitel/Seitenzahl
|
||||
\KOMAoptions{cleardoublepage=empty}
|
||||
|
||||
% ===========================================================================
|
||||
% 4) TOC
|
||||
% ===========================================================================
|
||||
\setcounter{tocdepth}{1}
|
||||
\AtBeginDocument{\hypersetup{bookmarksdepth=2}}
|
||||
\setcounter{secnumdepth}{-\maxdimen}
|
||||
\setkomafont{chapterentry}{\color{nexttokenblue}\sffamily}
|
||||
\KOMAoptions{toc=chapterentrywithdots}
|
||||
\DeclareTOCStyleEntry[
|
||||
beforeskip=.08em plus .1pt,
|
||||
entryformat=\small,
|
||||
pagenumberformat=\small
|
||||
]{tocline}{section}
|
||||
\DeclareTOCStyleEntry[beforeskip=.4em plus .1pt]{tocline}{chapter}
|
||||
|
||||
% ===========================================================================
|
||||
% 5) Code / Shaded — 9 pt, Zeilenumbruch im Satzspiegel
|
||||
% ===========================================================================
|
||||
\usepackage{framed}
|
||||
\usepackage{color}
|
||||
\definecolor{shadecolor}{gray}{0.96}
|
||||
|
||||
\usepackage{fvextra}
|
||||
\DefineVerbatimEnvironment{Highlighting}{Verbatim}{
|
||||
breaklines=true,
|
||||
breakanywhere=true,
|
||||
commandchars=\\\{\},
|
||||
fontsize=\fontsize{9}{11}\selectfont
|
||||
}
|
||||
|
||||
% Inline-Code ~9 pt (Breaks in Tabellen: tables.lua; Body: emergencystretch)
|
||||
\let\oldtexttt\texttt
|
||||
\renewcommand{\texttt}[1]{{\fontsize{9}{11}\selectfont\oldtexttt{#1}}}
|
||||
|
||||
% ===========================================================================
|
||||
% 6) Zitat- / Hinweis-Boxen (Callouts: Kern / Übung / Achtung) — B/W
|
||||
% ===========================================================================
|
||||
\usepackage{mdframed}
|
||||
\usepackage{tikz}
|
||||
\usepackage{tcolorbox}
|
||||
\tcbuselibrary{breakable,skins}
|
||||
|
||||
\newmdenv[
|
||||
leftline=true,
|
||||
rightline=false,
|
||||
topline=false,
|
||||
bottomline=false,
|
||||
linewidth=2.5pt,
|
||||
linecolor=nexttokengold,
|
||||
backgroundcolor=nexttokenparchment,
|
||||
leftmargin=0pt,
|
||||
rightmargin=0pt,
|
||||
innerleftmargin=10pt,
|
||||
innerrightmargin=6pt,
|
||||
innertopmargin=5pt,
|
||||
innerbottommargin=5pt,
|
||||
skipabove=10pt,
|
||||
skipbelow=10pt
|
||||
]{guidequotebox}
|
||||
|
||||
\newenvironment{guidequote}
|
||||
{\begin{guidequotebox}\itshape\color{nexttokenslate}}
|
||||
{\end{guidequotebox}}
|
||||
|
||||
\renewenvironment{quote}{%
|
||||
\begin{guidequotebox}\itshape\color{nexttokenslate}%
|
||||
}{%
|
||||
\end{guidequotebox}%
|
||||
}
|
||||
|
||||
\newcommand{\guideIconKern}{%
|
||||
\tikz[baseline=-0.28ex]{%
|
||||
\draw[line width=0.65pt, color=nexttokenblue]
|
||||
(0,0) circle (0.30em);%
|
||||
\fill[nexttokenblue] (0,0) circle (0.07em);%
|
||||
\draw[line width=0.65pt, color=nexttokenblue]
|
||||
(0.10em,0.10em) -- (0.22em,0.22em);%
|
||||
}%
|
||||
}
|
||||
\newcommand{\guideIconUebung}{%
|
||||
\tikz[baseline=-0.28ex]{%
|
||||
\draw[line width=0.65pt, color=nexttokengold, rounded corners=0.4pt]
|
||||
(-0.02em,0) rectangle (0.50em,0.50em);%
|
||||
\draw[line width=0.75pt, color=nexttokengold, line cap=round, line join=round]
|
||||
(0.08em,0.24em) -- (0.20em,0.10em) -- (0.42em,0.40em);%
|
||||
}%
|
||||
}
|
||||
\newcommand{\guideIconAchtung}{%
|
||||
\tikz[baseline=-0.32ex]{%
|
||||
\draw[line width=0.65pt, color=nexttokenburgundy, line join=round]
|
||||
(0.28em,0.56em) -- (0.54em,0.06em) -- (0.02em,0.06em) -- cycle;%
|
||||
\draw[line width=0.7pt, color=nexttokenburgundy, line cap=round]
|
||||
(0.28em,0.40em) -- (0.28em,0.20em);%
|
||||
\fill[nexttokenburgundy] (0.28em,0.12em) circle (0.035em);%
|
||||
}%
|
||||
}
|
||||
|
||||
\newcommand{\guideCalloutLabel}[3]{%
|
||||
\noindent{\sffamily\bfseries\fontsize{9}{11}\selectfont\color{#1}%
|
||||
#3\enspace\MakeUppercase{#2}}\par\vspace{0.35em}%
|
||||
}
|
||||
|
||||
\tcbset{
|
||||
guidecallout/.style={
|
||||
enhanced,
|
||||
frame hidden,
|
||||
boxrule=0pt,
|
||||
leftrule=0pt,
|
||||
rightrule=0pt,
|
||||
toprule=0pt,
|
||||
bottomrule=0pt,
|
||||
left=9pt,
|
||||
right=7pt,
|
||||
top=6pt,
|
||||
bottom=6pt,
|
||||
arc=0pt,
|
||||
outer arc=0pt,
|
||||
boxsep=1pt,
|
||||
before skip=10pt,
|
||||
after skip=10pt,
|
||||
breakable,
|
||||
fontupper=\fontsize{9}{12}\selectfont\color{nexttokentext}
|
||||
}
|
||||
}
|
||||
|
||||
\newtcolorbox{guidekern}{
|
||||
guidecallout,
|
||||
colback=nexttokenparchment,
|
||||
borderline west={2.5pt}{0pt}{nexttokenblue}
|
||||
}
|
||||
|
||||
\newtcolorbox{guideuebung}{
|
||||
guidecallout,
|
||||
colback=nexttokenparchment,
|
||||
borderline west={2.5pt}{0pt}{nexttokengold}
|
||||
}
|
||||
|
||||
\newtcolorbox{guideachtung}{
|
||||
guidecallout,
|
||||
colback=gray!8!white,
|
||||
borderline west={2.5pt}{0pt}{nexttokenburgundy}
|
||||
}
|
||||
|
||||
% ===========================================================================
|
||||
% 7) Tabellen — 9 pt, etwas luftiger
|
||||
% ===========================================================================
|
||||
\usepackage{array}
|
||||
\setlength{\tabcolsep}{4.2pt}
|
||||
\renewcommand{\arraystretch}{1.18}
|
||||
\setlength{\LTleft}{0pt}
|
||||
\setlength{\LTright}{0pt}
|
||||
|
||||
% ===========================================================================
|
||||
% 7b) Figures / Floats — mehr Weißraum
|
||||
% ===========================================================================
|
||||
\usepackage{float}
|
||||
\usepackage{placeins}
|
||||
\usepackage{etoolbox}
|
||||
\floatplacement{figure}{H}
|
||||
\BeforeBeginEnvironment{Shaded}{\FloatBarrier}
|
||||
\BeforeBeginEnvironment{verbatim}{\FloatBarrier}
|
||||
\BeforeBeginEnvironment{longtable}{\fontsize{9}{11}\selectfont}
|
||||
\BeforeBeginEnvironment{tabular}{\fontsize{9}{11}\selectfont}
|
||||
\setlength{\textfloatsep}{12pt plus 3pt minus 2pt}
|
||||
\setlength{\floatsep}{10pt plus 2pt minus 2pt}
|
||||
\setlength{\intextsep}{10pt plus 2pt minus 2pt}
|
||||
% Abbildungen: Textbreite ≈ 4,75 in · 0,87 · 300 ≈ 1239 px → 1280-Quellen ok
|
||||
\setkeys{Gin}{width=0.87\linewidth,keepaspectratio}
|
||||
|
||||
% ===========================================================================
|
||||
% 8) Titelseite — Cover-Abbildung (Grayscale), Fallback typografisch
|
||||
% KDP-Außenumschlag bleibt separat; Interior-Titelseite = Seite 1 mit Cover.
|
||||
% Ohne Bleed: Cover im Safe-Frame; KDP-Außenrand ≥ 0,25" → hier 0,30".
|
||||
% ===========================================================================
|
||||
\newcommand{\GuideTitlePageTypography}{%
|
||||
\centering
|
||||
\vspace*{2.2cm}%
|
||||
{\sffamily\color{nexttokengold}\large\bfseries\MakeUppercase{Praxisbuch}\par}%
|
||||
\vspace{0.9cm}%
|
||||
\rule{0.55\textwidth}{1.0pt}\par
|
||||
\vspace{1.0cm}%
|
||||
{\sffamily\color{nexttokenblue}\Huge\bfseries Grok Build\par}%
|
||||
\vspace{0.35cm}%
|
||||
{\sffamily\color{nexttokenblue}\LARGE\bfseries Der umfassende\par
|
||||
\vspace{0.15cm} Praxisleitfaden\par}%
|
||||
\vspace{0.8cm}%
|
||||
\rule{0.55\textwidth}{1.0pt}\par
|
||||
\vspace{1.1cm}%
|
||||
{\sffamily\color{nexttokenslate}\large
|
||||
Der Coding-Agent von xAI\par
|
||||
\vspace{0.3cm}
|
||||
richtig einsetzen\par}%
|
||||
\vspace{1.8cm}%
|
||||
{\sffamily\color{nexttokentext}\large Martin Richardt\par}%
|
||||
\vspace{0.45cm}%
|
||||
{\sffamily\color{nexttokenslate}Juli 2026 · Stand Grok Build\par}%
|
||||
\vfill
|
||||
{\sffamily\color{nexttokenslate}\footnotesize
|
||||
Paperback-Interior · 6$\times$9 in · Grayscale\par}%
|
||||
}
|
||||
|
||||
\newcommand{\GuideTitlePageCover}[1]{%
|
||||
\null\vfill
|
||||
\centering
|
||||
\includegraphics[width=\textwidth,height=\textheight,keepaspectratio]{#1}\par
|
||||
\vfill\null
|
||||
}
|
||||
|
||||
\newcommand{\GuideTitlePage}{%
|
||||
% Ohne titlepage-Env (twoside erzeugt sonst Leer-Verso mit Header-Artefakten).
|
||||
% Safe-Frame 0,30" via newgeometry; danach restore + normaler Satzspiegel.
|
||||
\clearpage
|
||||
\thispagestyle{empty}%
|
||||
\pagestyle{empty}%
|
||||
\newgeometry{margin=0.30in}%
|
||||
\thispagestyle{empty}%
|
||||
% 1) Print-Assets (Grayscale) 2) eBook-Cover-Namen 3) Typografie-Fallback
|
||||
\IfFileExists{build/print-assets/assets/cover/cover-front.jpg}{%
|
||||
\GuideTitlePageCover{build/print-assets/assets/cover/cover-front.jpg}%
|
||||
}{%
|
||||
\IfFileExists{assets/cover/COVER-01B-full.jpg}{%
|
||||
\GuideTitlePageCover{assets/cover/COVER-01B-full.jpg}%
|
||||
}{%
|
||||
\IfFileExists{assets/cover/cover-front.jpg}{%
|
||||
\GuideTitlePageCover{assets/cover/cover-front.jpg}%
|
||||
}{%
|
||||
\GuideTitlePageTypography
|
||||
}%
|
||||
}%
|
||||
}%
|
||||
\thispagestyle{empty}%
|
||||
\clearpage
|
||||
\restoregeometry
|
||||
\thispagestyle{empty}%
|
||||
\pagestyle{scrheadings}%
|
||||
}
|
||||
|
||||
% Fließtext-Farbe
|
||||
\AtBeginDocument{\color{nexttokentext}}
|
||||
@@ -1,6 +1,15 @@
|
||||
% guide-preamble.tex — Präambel für Grok-Build-Buch
|
||||
% Visuell kompatibel zu ki-hinterm-vorhang / ki-copilot (nexttoken-*).
|
||||
|
||||
% ===========================================================================
|
||||
% 0) SPRACHE / SILBENTRENNUNG
|
||||
% ===========================================================================
|
||||
% Custom Pandoc-Template lädt babel nicht automatisch (trotz lang=de-DE).
|
||||
% Ohne ngerman greifen englische Patterns → Fehltrennungen wie
|
||||
% „Planungss-chleifen“, „zwis-chen“.
|
||||
% shorthands=off: kein Konflikt mit csquotes \MakeOuterQuote{"} im Template
|
||||
\usepackage[shorthands=off,main=ngerman]{babel}
|
||||
|
||||
% ===========================================================================
|
||||
% 1) FONTS
|
||||
% ===========================================================================
|
||||
|
||||
+17
-2
@@ -4,15 +4,22 @@ $if(fontsize)$
|
||||
$else$
|
||||
11pt,
|
||||
$endif$
|
||||
$if(papersize)$
|
||||
$if(geometry)$
|
||||
$elseif(papersize)$
|
||||
$papersize$paper,
|
||||
$else$
|
||||
a4paper,
|
||||
$endif$
|
||||
$if(print-interior)$
|
||||
twoside,
|
||||
openany,
|
||||
BCOR=0mm,
|
||||
$else$
|
||||
oneside,
|
||||
openany,
|
||||
DIV=13,
|
||||
BCOR=0mm,
|
||||
$endif$
|
||||
$for(classoption)$
|
||||
$classoption$$sep$,
|
||||
$endfor$
|
||||
@@ -66,11 +73,19 @@ $endfor$
|
||||
|
||||
$highlighting-macros$
|
||||
|
||||
$if(print-interior)$
|
||||
\IfFileExists{pdf/print/print-preamble.tex}{%
|
||||
\input{pdf/print/print-preamble.tex}%
|
||||
}{%
|
||||
\InputIfFileExists{print-preamble.tex}{}{}%
|
||||
}
|
||||
$else$
|
||||
\IfFileExists{pdf/templates/guide-preamble.tex}{%
|
||||
\input{pdf/templates/guide-preamble.tex}%
|
||||
}{%
|
||||
\InputIfFileExists{guide-preamble.tex}{}{}%
|
||||
}
|
||||
$endif$
|
||||
|
||||
\usepackage{hyperref}
|
||||
\hypersetup{
|
||||
@@ -131,7 +146,7 @@ $endif$
|
||||
$if(colorlinks)$
|
||||
\hypersetup{linkcolor=$if(toccolor)$$toccolor$$else$nexttokenlink$endif$}
|
||||
$endif$
|
||||
\setcounter{tocdepth}{$if(toc-depth)$$toc-depth$$else$2$endif$}
|
||||
\setcounter{tocdepth}{$if(toc-depth)$$toc-depth$$else$1$endif$}
|
||||
\tableofcontents
|
||||
}
|
||||
$endif$
|
||||
|
||||
+265
-10
@@ -2,7 +2,7 @@
|
||||
"""Post-build quality checks for the Grok Build book PDF.
|
||||
|
||||
CLI:
|
||||
python3 scripts/check_pdf.py [--pdf PATH]
|
||||
python3 scripts/check_pdf.py [--pdf PATH] [--mode ebook|print]
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -16,10 +16,18 @@ import zlib
|
||||
|
||||
REPO_ROOT = pathlib.Path(__file__).resolve().parent.parent
|
||||
DEFAULT_PDF = REPO_ROOT / "dist" / "grok-build-praxisleitfaden.pdf"
|
||||
DEFAULT_PRINT_PDF = REPO_ROOT / "dist" / "grok-build-praxisleitfaden-print.pdf"
|
||||
|
||||
# Soft target for a multi-chapter practice book (19 chapters → larger span)
|
||||
SOFT_MIN_PAGES = 40
|
||||
SOFT_MAX_PAGES = 280
|
||||
PRINT_SOFT_MIN_PAGES = 100
|
||||
PRINT_SOFT_MAX_PAGES = 320
|
||||
|
||||
# 6×9 in in PDF points (1 in = 72 pt)
|
||||
PRINT_W_PT = 6 * 72 # 432
|
||||
PRINT_H_PT = 9 * 72 # 648
|
||||
PRINT_SIZE_TOL_PT = 1.5
|
||||
|
||||
KEYWORDS = [
|
||||
"Grok Build",
|
||||
@@ -57,7 +65,7 @@ def check_exists(pdf: pathlib.Path) -> bool:
|
||||
return True
|
||||
|
||||
|
||||
def check_pages(data: bytes) -> int:
|
||||
def check_pages(data: bytes, mode: str) -> int:
|
||||
text = data.decode("latin-1", "ignore")
|
||||
counts = [int(m) for m in re.findall(r"/Count\s+(\d+)", text)]
|
||||
if counts:
|
||||
@@ -67,25 +75,28 @@ def check_pages(data: bytes) -> int:
|
||||
for chunk in _decompress_streams(data):
|
||||
pages += len(re.findall(r"/Type\s*/Page[^s]", chunk))
|
||||
|
||||
soft_min = PRINT_SOFT_MIN_PAGES if mode == "print" else SOFT_MIN_PAGES
|
||||
soft_max = PRINT_SOFT_MAX_PAGES if mode == "print" else SOFT_MAX_PAGES
|
||||
|
||||
if pages == 0:
|
||||
_print("WARN", "Page count", "konnte nicht geschätzt werden")
|
||||
elif pages < SOFT_MIN_PAGES:
|
||||
elif pages < soft_min:
|
||||
_print(
|
||||
"WARN",
|
||||
"Page count",
|
||||
f"~{pages} Seiten (unter Ziel {SOFT_MIN_PAGES}–{SOFT_MAX_PAGES})",
|
||||
f"~{pages} Seiten (unter Ziel {soft_min}–{soft_max})",
|
||||
)
|
||||
elif pages > SOFT_MAX_PAGES:
|
||||
elif pages > soft_max:
|
||||
_print(
|
||||
"WARN",
|
||||
"Page count",
|
||||
f"~{pages} Seiten (über Ziel {SOFT_MIN_PAGES}–{SOFT_MAX_PAGES})",
|
||||
f"~{pages} Seiten (über Ziel {soft_min}–{soft_max})",
|
||||
)
|
||||
else:
|
||||
_print(
|
||||
"OK",
|
||||
"Page count",
|
||||
f"~{pages} Seiten (Ziel {SOFT_MIN_PAGES}–{SOFT_MAX_PAGES})",
|
||||
f"~{pages} Seiten (Ziel {soft_min}–{soft_max})",
|
||||
)
|
||||
return pages
|
||||
|
||||
@@ -148,21 +159,265 @@ def check_keywords(pdf: pathlib.Path) -> None:
|
||||
_print("OK", "Keywords", f"{len(KEYWORDS)} Schlüsselbegriffe gefunden")
|
||||
|
||||
|
||||
def _pdfinfo_fields(pdf: pathlib.Path) -> dict[str, str]:
|
||||
pdfinfo = shutil.which("pdfinfo")
|
||||
if not pdfinfo:
|
||||
return {}
|
||||
try:
|
||||
proc = subprocess.run(
|
||||
[pdfinfo, str(pdf)],
|
||||
check=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
except subprocess.CalledProcessError:
|
||||
return {}
|
||||
fields: dict[str, str] = {}
|
||||
for line in proc.stdout.splitlines():
|
||||
if ":" not in line:
|
||||
continue
|
||||
key, val = line.split(":", 1)
|
||||
fields[key.strip()] = val.strip()
|
||||
return fields
|
||||
|
||||
|
||||
def check_page_size_print(pdf: pathlib.Path) -> None:
|
||||
"""Expect ~6×9 in (432×648 pt)."""
|
||||
fields = _pdfinfo_fields(pdf)
|
||||
if not fields:
|
||||
_print("SKIP", "Page size (print)", "pdfinfo nicht verfügbar")
|
||||
return
|
||||
|
||||
size = fields.get("Page size", "")
|
||||
# e.g. "432 x 648 pts" or "432 x 648 pts (letter)"
|
||||
m = re.search(r"([\d.]+)\s*x\s*([\d.]+)\s*pts", size, re.I)
|
||||
if not m:
|
||||
_print("WARN", "Page size (print)", f"unparseable: {size!r}")
|
||||
return
|
||||
w, h = float(m.group(1)), float(m.group(2))
|
||||
ok_w = abs(w - PRINT_W_PT) <= PRINT_SIZE_TOL_PT
|
||||
ok_h = abs(h - PRINT_H_PT) <= PRINT_SIZE_TOL_PT
|
||||
if ok_w and ok_h:
|
||||
_print("OK", "Page size (print)", f"{w:.1f} × {h:.1f} pts (6×9 in)")
|
||||
else:
|
||||
_print(
|
||||
"FAIL",
|
||||
"Page size (print)",
|
||||
f"{w:.1f} × {h:.1f} pts — erwartet ~{PRINT_W_PT} × {PRINT_H_PT} (6×9 in)",
|
||||
)
|
||||
|
||||
|
||||
def check_pdf_version(pdf: pathlib.Path) -> None:
|
||||
fields = _pdfinfo_fields(pdf)
|
||||
if not fields:
|
||||
return
|
||||
ver = fields.get("PDF version", "?")
|
||||
pages = fields.get("Pages", "?")
|
||||
_print("OK", "PDF meta", f"version={ver}, pages={pages}")
|
||||
|
||||
|
||||
def check_print_assets() -> None:
|
||||
"""Soft checks on staged/source print assets (DPI readiness + gray stage)."""
|
||||
try:
|
||||
from PIL import Image
|
||||
except ImportError:
|
||||
_print("SKIP", "Print assets DPI", "Pillow nicht verfügbar")
|
||||
return
|
||||
|
||||
# Cover: prefer print-specific source, then staged gray, else eBook names
|
||||
cover_candidates = [
|
||||
REPO_ROOT / "assets" / "cover" / "cover-front-print.jpg",
|
||||
REPO_ROOT / "build" / "print-assets" / "assets" / "cover" / "cover-front.jpg",
|
||||
REPO_ROOT / "assets" / "cover" / "COVER-01B-full.jpg",
|
||||
REPO_ROOT / "assets" / "cover" / "cover-front.jpg",
|
||||
]
|
||||
cover = next((p for p in cover_candidates if p.exists()), None)
|
||||
# Safe frame 5.4×8.4 in @ 300 DPI
|
||||
min_cover_w, min_cover_h = 1620, 2520
|
||||
if cover is None:
|
||||
_print("WARN", "Cover asset", "kein Cover (cover-front-print / COVER-01B) gefunden")
|
||||
else:
|
||||
with Image.open(cover) as im:
|
||||
w, h = im.size
|
||||
if w >= min_cover_w and h >= min_cover_h:
|
||||
_print(
|
||||
"OK",
|
||||
"Cover asset DPI",
|
||||
f"{cover.relative_to(REPO_ROOT)} {w}×{h} (≥ {min_cover_w}×{min_cover_h})",
|
||||
)
|
||||
else:
|
||||
_print(
|
||||
"WARN",
|
||||
"Cover asset DPI",
|
||||
f"{cover.relative_to(REPO_ROOT)} {w}×{h} — Ziel ≥ {min_cover_w}×{min_cover_h} "
|
||||
f"(300 DPI im 0,30\"-Safe-Frame)",
|
||||
)
|
||||
|
||||
# Figures: 1280 px @ Gin 0.87 ≈ 300 DPI; warn if many below threshold
|
||||
illus = REPO_ROOT / "assets" / "illustrations"
|
||||
min_fig_w = 1280 # matches prepare_print_assets MIN at Gin 0.87
|
||||
low: list[str] = []
|
||||
total = 0
|
||||
if illus.is_dir():
|
||||
for p in sorted(illus.glob("*.jpg")) + sorted(illus.glob("*.png")):
|
||||
if p.name.startswith("."):
|
||||
continue
|
||||
total += 1
|
||||
with Image.open(p) as im:
|
||||
w, _h = im.size
|
||||
if w < min_fig_w:
|
||||
low.append(f"{p.name} ({w}px)")
|
||||
if total == 0:
|
||||
_print("SKIP", "Figure assets", "keine Illustrations gefunden")
|
||||
elif not low:
|
||||
_print("OK", "Figure assets DPI", f"{total} Datei(en) ≥ {min_fig_w}px Breite")
|
||||
else:
|
||||
_print(
|
||||
"WARN",
|
||||
"Figure assets DPI",
|
||||
f"{len(low)}/{total} unter {min_fig_w}px — bei Gin 0,87 < ~300 DPI: "
|
||||
+ ", ".join(low[:6])
|
||||
+ ("…" if len(low) > 6 else ""),
|
||||
)
|
||||
|
||||
# Staged grayscale must mirror MD paths and be single-channel
|
||||
staged = REPO_ROOT / "build" / "print-assets" / "assets" / "illustrations"
|
||||
if not staged.is_dir():
|
||||
_print(
|
||||
"WARN",
|
||||
"Print gray stage",
|
||||
"build/print-assets/assets/illustrations fehlt — just print-assets?",
|
||||
)
|
||||
return
|
||||
not_gray: list[str] = []
|
||||
staged_n = 0
|
||||
for p in sorted(staged.glob("*.jpg")):
|
||||
staged_n += 1
|
||||
with Image.open(p) as im:
|
||||
if im.mode not in ("L", "1", "LA"):
|
||||
not_gray.append(f"{p.name}({im.mode})")
|
||||
if staged_n == 0:
|
||||
_print("WARN", "Print gray stage", "keine staged JPEGs")
|
||||
elif not_gray:
|
||||
_print(
|
||||
"FAIL",
|
||||
"Print gray stage",
|
||||
f"{len(not_gray)} nicht grayscale: " + ", ".join(not_gray[:8]),
|
||||
)
|
||||
else:
|
||||
_print("OK", "Print gray stage", f"{staged_n} staged JPEG(s), mode L")
|
||||
|
||||
|
||||
def check_print_pdf_images_gray(pdf: pathlib.Path) -> bool:
|
||||
"""Fail if embedded images are DeviceRGB (figures must be gray in print PDF).
|
||||
|
||||
Returns True when check fails (for exit code).
|
||||
"""
|
||||
if not shutil.which("pdfimages"):
|
||||
_print("SKIP", "PDF images gray", "pdfimages nicht verfügbar")
|
||||
return False
|
||||
try:
|
||||
out = subprocess.check_output(
|
||||
["pdfimages", "-list", str(pdf)],
|
||||
text=True,
|
||||
stderr=subprocess.DEVNULL,
|
||||
)
|
||||
except (subprocess.CalledProcessError, OSError) as e:
|
||||
_print("SKIP", "PDF images gray", f"pdfimages fehlgeschlagen: {e}")
|
||||
return False
|
||||
|
||||
rgb = 0
|
||||
gray = 0
|
||||
other = 0
|
||||
for line in out.splitlines():
|
||||
# skip header lines; data rows start with page number
|
||||
parts = line.split()
|
||||
if len(parts) < 6 or not parts[0].isdigit():
|
||||
continue
|
||||
# columns: page num type width height color comp bpc ...
|
||||
color = parts[5].lower()
|
||||
if color in ("gray", "grey", "image-mask", "stencil"):
|
||||
gray += 1
|
||||
elif color in ("rgb", "cmyk", "icc"):
|
||||
rgb += 1
|
||||
else:
|
||||
other += 1
|
||||
|
||||
total = rgb + gray + other
|
||||
if total == 0:
|
||||
_print("WARN", "PDF images gray", "keine eingebetteten Bilder gefunden")
|
||||
return False
|
||||
if rgb > 0:
|
||||
_print(
|
||||
"FAIL",
|
||||
"PDF images gray",
|
||||
f"{rgb}/{total} Bild(er) farbig (rgb/cmyk) — resource-path muss "
|
||||
f"build/print-assets (assets/…-Spiegel) vor den RGB-Originalen auflösen",
|
||||
)
|
||||
return True
|
||||
_print("OK", "PDF images gray", f"{gray}/{total} gray, 0 rgb")
|
||||
return False
|
||||
|
||||
|
||||
def print_manual_notes() -> None:
|
||||
print()
|
||||
print("── Print QA notes (manuell prüfen) ──────────────────────────────")
|
||||
print("• Titelseite: Cover im ≥0,30\"-Safe-Frame (KDP-Außen ≥0,25\").")
|
||||
print("• Body-Ränder: inner 0,65\", outer 0,60\", top 0,70\", bottom 1,00\" (Footer-Ink ≈ 0,65\").")
|
||||
print("• Body 10 pt / Leading ~14,6 pt; Boxen/Code/Tabellen 9 pt.")
|
||||
print("• Figuren: Gin width=0,87\\linewidth → 1280px ≈ 300 DPI.")
|
||||
print("• Diagramme mit ehemals farbigem Kanal: K-01, K-05, M-*, S-*")
|
||||
print(" (Kontrast und Linienunterscheidung im Grayscale-PDF).")
|
||||
print("• Breite Tabellen (Vergleichskapitel, Schnellreferenz): Satzspiegel.")
|
||||
print("• Lange Codezeilen und Dateipfade: innerhalb der Ränder?")
|
||||
print("• Seitenzahlen außen unten; Kolumnentitel nicht im Gutter.")
|
||||
print("• KDP-Außenumschlag (Bleed/Spine) ist separat — dieses PDF = Interior.")
|
||||
print("• Optional: just pdf-print-optimize (Ghostscript Gray/prepress).")
|
||||
print("────────────────────────────────────────────────────────────────")
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description="QA checks for book PDF")
|
||||
parser.add_argument("--pdf", type=pathlib.Path, default=DEFAULT_PDF)
|
||||
parser.add_argument("--pdf", type=pathlib.Path, default=None)
|
||||
parser.add_argument(
|
||||
"--mode",
|
||||
choices=("ebook", "print"),
|
||||
default="ebook",
|
||||
help="ebook (default A4) or print (KDP 6×9)",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
mode = args.mode
|
||||
pdf = args.pdf
|
||||
if pdf is None:
|
||||
pdf = DEFAULT_PRINT_PDF if mode == "print" else DEFAULT_PDF
|
||||
|
||||
ok = check_exists(pdf)
|
||||
if not ok:
|
||||
return 1
|
||||
|
||||
data = pdf.read_bytes()
|
||||
check_pages(data)
|
||||
check_pages(data, mode)
|
||||
check_fonts(pdf)
|
||||
check_keywords(pdf)
|
||||
return 0
|
||||
check_pdf_version(pdf)
|
||||
|
||||
failed = False
|
||||
if mode == "print":
|
||||
check_page_size_print(pdf)
|
||||
check_print_assets()
|
||||
if check_print_pdf_images_gray(pdf):
|
||||
failed = True
|
||||
# Re-run logic for exit code
|
||||
fields = _pdfinfo_fields(pdf)
|
||||
size = fields.get("Page size", "")
|
||||
m = re.search(r"([\d.]+)\s*x\s*([\d.]+)\s*pts", size, re.I)
|
||||
if m:
|
||||
w, h = float(m.group(1)), float(m.group(2))
|
||||
if abs(w - PRINT_W_PT) > PRINT_SIZE_TOL_PT or abs(h - PRINT_H_PT) > PRINT_SIZE_TOL_PT:
|
||||
failed = True
|
||||
print_manual_notes()
|
||||
|
||||
return 1 if failed else 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -0,0 +1,242 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Prepare grayscale, high-contrast print assets for KDP interior PDF.
|
||||
|
||||
Writes gray JPEGs under build/print-assets/ mirroring the MD-relative paths
|
||||
(assets/illustrations/…, assets/cover/…). Pandoc --resource-path puts
|
||||
build/print-assets first, so ``assets/illustrations/foo.jpg`` resolves to the
|
||||
gray stage instead of the RGB original under repo root.
|
||||
|
||||
CLI:
|
||||
python3 scripts/prepare_print_assets.py [--force]
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import pathlib
|
||||
import shutil
|
||||
import sys
|
||||
|
||||
from PIL import Image, ImageEnhance, ImageFilter, ImageOps
|
||||
|
||||
REPO_ROOT = pathlib.Path(__file__).resolve().parent.parent
|
||||
SRC_ILLUSTRATIONS = REPO_ROOT / "assets" / "illustrations"
|
||||
SRC_COVER = REPO_ROOT / "assets" / "cover"
|
||||
OUT_ROOT = REPO_ROOT / "build" / "print-assets"
|
||||
# Mirror MD paths: assets/illustrations/… and assets/cover/…
|
||||
OUT_ILLUSTRATIONS = OUT_ROOT / "assets" / "illustrations"
|
||||
OUT_COVER = OUT_ROOT / "assets" / "cover"
|
||||
# Legacy layout (pre path-mirror) — removed so stale color lookups cannot win
|
||||
_LEGACY_ILLUSTRATIONS = OUT_ROOT / "illustrations"
|
||||
_LEGACY_COVER = OUT_ROOT / "cover"
|
||||
|
||||
# Geometry must stay in sync with pdf/print/metadata.yaml + print-preamble Gin
|
||||
# text width = 6 − inner − outer = 6 − 0.65 − 0.60 = 4.75 in
|
||||
TEXT_WIDTH_IN = 4.75
|
||||
GIN_FRACTION = 0.87 # \setkeys{Gin}{width=0.87\linewidth}
|
||||
FIG_DISPLAY_IN = TEXT_WIDTH_IN * GIN_FRACTION # ≈ 4.1325 in
|
||||
|
||||
# Cover interior: 6×9 with 0.30" safety → safe frame ≈ 5.4 × 8.4 in
|
||||
COVER_SAFE_W_IN = 5.40
|
||||
COVER_SAFE_H_IN = 8.40
|
||||
COVER_MIN_W_PX = int(COVER_SAFE_W_IN * 300) # 1620
|
||||
COVER_MIN_H_PX = int(COVER_SAFE_H_IN * 300) # 2520
|
||||
|
||||
TARGET_DPI = 300
|
||||
MIN_WIDTH_300DPI = int(FIG_DISPLAY_IN * TARGET_DPI) # ≈ 1279
|
||||
|
||||
|
||||
def _to_print_gray(im: Image.Image) -> Image.Image:
|
||||
"""RGB/RGBA → print grayscale with contrast and mild sharpening."""
|
||||
if im.mode in ("RGBA", "LA") or (im.mode == "P" and "transparency" in im.info):
|
||||
rgba = im.convert("RGBA")
|
||||
bg = Image.new("RGBA", rgba.size, (255, 255, 255, 255))
|
||||
im = Image.alpha_composite(bg, rgba).convert("RGB")
|
||||
else:
|
||||
im = im.convert("RGB")
|
||||
|
||||
gray = ImageOps.grayscale(im)
|
||||
gray = ImageOps.autocontrast(gray, cutoff=1)
|
||||
gray = ImageEnhance.Contrast(gray).enhance(1.12)
|
||||
gray = gray.filter(ImageFilter.UnsharpMask(radius=0.8, percent=80, threshold=2))
|
||||
return gray
|
||||
|
||||
|
||||
def _process_figure(src: pathlib.Path, dest: pathlib.Path) -> dict:
|
||||
dest.parent.mkdir(parents=True, exist_ok=True)
|
||||
with Image.open(src) as im:
|
||||
im.load()
|
||||
mode = im.mode
|
||||
w, h = im.size
|
||||
gray = _to_print_gray(im)
|
||||
gray.save(
|
||||
dest,
|
||||
format="JPEG",
|
||||
quality=92,
|
||||
optimize=True,
|
||||
dpi=(TARGET_DPI, TARGET_DPI),
|
||||
)
|
||||
|
||||
dpi_at_display = w / FIG_DISPLAY_IN if w else 0
|
||||
return {
|
||||
"src": src.name,
|
||||
"dest": str(dest.relative_to(REPO_ROOT)),
|
||||
"size": f"{w}x{h}",
|
||||
"mode_in": mode,
|
||||
"kind": "figure",
|
||||
"dpi_at_display": round(dpi_at_display),
|
||||
"low_res": w < MIN_WIDTH_300DPI,
|
||||
}
|
||||
|
||||
|
||||
def _process_cover(src: pathlib.Path, dest: pathlib.Path) -> dict:
|
||||
"""Grayscale cover; upscale to ≥300 DPI in interior safe-frame if needed."""
|
||||
dest.parent.mkdir(parents=True, exist_ok=True)
|
||||
with Image.open(src) as im:
|
||||
im.load()
|
||||
mode = im.mode
|
||||
w0, h0 = im.size
|
||||
gray = _to_print_gray(im)
|
||||
w, h = gray.size
|
||||
upscaled = False
|
||||
|
||||
# Ensure enough pixels for 300 DPI inside the 0.30" safety frame
|
||||
need_w, need_h = COVER_MIN_W_PX, COVER_MIN_H_PX
|
||||
scale = max(need_w / w, need_h / h, 1.0)
|
||||
if scale > 1.0:
|
||||
nw, nh = int(round(w * scale)), int(round(h * scale))
|
||||
gray = gray.resize((nw, nh), Image.Resampling.LANCZOS)
|
||||
gray = gray.filter(
|
||||
ImageFilter.UnsharpMask(radius=1.0, percent=90, threshold=2)
|
||||
)
|
||||
w, h = gray.size
|
||||
upscaled = True
|
||||
|
||||
gray.save(
|
||||
dest,
|
||||
format="JPEG",
|
||||
quality=94,
|
||||
optimize=True,
|
||||
dpi=(TARGET_DPI, TARGET_DPI),
|
||||
)
|
||||
|
||||
dpi_safe = min(w / COVER_SAFE_W_IN, h / COVER_SAFE_H_IN) if w and h else 0
|
||||
return {
|
||||
"src": src.name,
|
||||
"dest": str(dest.relative_to(REPO_ROOT)),
|
||||
"size": f"{w}x{h}",
|
||||
"size_src": f"{w0}x{h0}",
|
||||
"mode_in": mode,
|
||||
"kind": "cover",
|
||||
"dpi_at_safe_frame": round(dpi_safe),
|
||||
"upscaled": upscaled,
|
||||
"low_res": dpi_safe < TARGET_DPI - 5,
|
||||
}
|
||||
|
||||
|
||||
def _pick_cover_source() -> pathlib.Path | None:
|
||||
"""Prefer dedicated print cover; fall back to eBook front covers."""
|
||||
for name in (
|
||||
"cover-front-print.jpg",
|
||||
"COVER-01B-full.jpg",
|
||||
"cover-front.jpg",
|
||||
):
|
||||
p = SRC_COVER / name
|
||||
if p.exists():
|
||||
return p
|
||||
return None
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description="Prepare KDP print assets (grayscale)")
|
||||
parser.add_argument(
|
||||
"--force",
|
||||
action="store_true",
|
||||
help="Re-process even if output is newer than source",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
if not SRC_ILLUSTRATIONS.is_dir():
|
||||
print(f"[FAIL] missing {SRC_ILLUSTRATIONS}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
# Drop pre-mirror staging dirs so resource-path cannot hit stale trees.
|
||||
for legacy in (_LEGACY_ILLUSTRATIONS, _LEGACY_COVER):
|
||||
if legacy.is_dir():
|
||||
shutil.rmtree(legacy)
|
||||
|
||||
OUT_ILLUSTRATIONS.mkdir(parents=True, exist_ok=True)
|
||||
OUT_COVER.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
reports: list[dict] = []
|
||||
sources = sorted(SRC_ILLUSTRATIONS.glob("*.jpg")) + sorted(
|
||||
SRC_ILLUSTRATIONS.glob("*.png")
|
||||
)
|
||||
for src in sources:
|
||||
if src.name.startswith("."):
|
||||
continue
|
||||
# Keep extension when already JPEG; convert PNG → .jpg for print stage
|
||||
dest = OUT_ILLUSTRATIONS / (src.stem + ".jpg")
|
||||
if (
|
||||
not args.force
|
||||
and dest.exists()
|
||||
and dest.stat().st_mtime >= src.stat().st_mtime
|
||||
):
|
||||
reports.append({"src": src.name, "skipped": True})
|
||||
continue
|
||||
reports.append(_process_figure(src, dest))
|
||||
|
||||
cover_src = _pick_cover_source()
|
||||
if cover_src is not None:
|
||||
# MD/eBook cover name; print source may be COVER-01B-full.jpg
|
||||
cover_dest = OUT_COVER / "cover-front.jpg"
|
||||
if (
|
||||
args.force
|
||||
or not cover_dest.exists()
|
||||
or cover_dest.stat().st_mtime < cover_src.stat().st_mtime
|
||||
):
|
||||
reports.append(_process_cover(cover_src, cover_dest))
|
||||
else:
|
||||
reports.append({"src": cover_src.name, "skipped": True})
|
||||
|
||||
processed = [r for r in reports if not r.get("skipped")]
|
||||
skipped = [r for r in reports if r.get("skipped")]
|
||||
low_figs = [
|
||||
r for r in processed if r.get("kind") == "figure" and r.get("low_res")
|
||||
]
|
||||
covers = [r for r in processed if r.get("kind") == "cover"]
|
||||
|
||||
print(f"[OK] print assets → {OUT_ROOT.relative_to(REPO_ROOT)}")
|
||||
print(f" processed={len(processed)} skipped={len(skipped)}")
|
||||
print(
|
||||
f" figure display ≈ {FIG_DISPLAY_IN:.2f} in "
|
||||
f"({GIN_FRACTION:.0%} · {TEXT_WIDTH_IN:.2f} in text); "
|
||||
f"300 DPI needs ≥ {MIN_WIDTH_300DPI} px width"
|
||||
)
|
||||
if low_figs:
|
||||
print(
|
||||
f"[WARN] {len(low_figs)} figure(s) below ~300 DPI at print display width "
|
||||
f"(not upscaled; reduce Gin or re-render):"
|
||||
)
|
||||
for r in low_figs:
|
||||
print(
|
||||
f" - {r['src']}: {r['size']} "
|
||||
f"(~{r['dpi_at_display']} dpi @ {FIG_DISPLAY_IN:.2f} in)"
|
||||
)
|
||||
for r in covers:
|
||||
flag = "upscaled" if r.get("upscaled") else "native"
|
||||
print(
|
||||
f"[OK] cover {r['src']} → {r['size']} "
|
||||
f"(src {r.get('size_src', '?')}, {flag}, "
|
||||
f"~{r.get('dpi_at_safe_frame')} dpi @ safe frame)"
|
||||
)
|
||||
if r.get("upscaled"):
|
||||
print(
|
||||
"[WARN] cover was LANCZOS-upscaled for 300 DPI interim — "
|
||||
"prefer assets/cover/cover-front-print.jpg ≥ 1620×2520 native"
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in new issue
Block a user