Planen und geplant zu werden 2
plan with html rendering created
This commit is contained in:
@@ -0,0 +1,14 @@
|
|||||||
|
# general instructions
|
||||||
|
- You are GitHub Copilot, an AI pair programmer.
|
||||||
|
- You are designed to help write code faster and with less effort.
|
||||||
|
- You can generate code snippets, complete code, and suggest improvements.
|
||||||
|
- You can understand and write code in multiple programming languages.
|
||||||
|
- You can help with debugging and fixing errors in code.
|
||||||
|
- You can assist with writing tests and documentation.
|
||||||
|
- You can learn from the context of the code you are working on.
|
||||||
|
- You will NOT write example or demonstration code.
|
||||||
|
- The GitHub repository is github.com:Bardioc26/bamort.git.
|
||||||
|
- The main package is cmd/backend/main.go.
|
||||||
|
- The main function is in cmd/backend/main.go.
|
||||||
|
- You write tests ONLY in _test.go files.
|
||||||
|
- You will NEVER create files for testing with a main() function!
|
||||||
@@ -1,238 +0,0 @@
|
|||||||
# Aufteilung der Informationen auf dem Characterbogen
|
|
||||||
|
|
||||||
Der Characterbogen besteht aus mehreren Seiten.
|
|
||||||
- Seite 1 ist die Statistikseite in der alle Werte in ihrem Grundwert eingetraqgen werden also ohne irgendwelche Boni.
|
|
||||||
- Seite 2 ist der Spielbogen. sie enthält alle werte so wie sie für das Spiel benötigt werden Boni und Mali jedweder Art sind hier in die Werte der Fertigkeiten eingerechnet. Das Gilt auch für magische Boni für Waffen, Rüstung oder ähnliches.
|
|
||||||
- Seite 3 enhält die Liste aller gelernten Zauber und aller Zauber die dem Charakter durch Artefakte zur Verfügung stehen
|
|
||||||
- Seite 4 enthält die Ausrüstung des Abenteurers. Hier ist Platz für "Am Körper getragen", "Auf dem Wagen", "Im Behältnis XYZ"
|
|
||||||
|
|
||||||
## Allgemeine Layoutwerte
|
|
||||||
|
|
||||||
- Seitengröße A4,quer: 297×210 mm
|
|
||||||
- Ränder
|
|
||||||
- oben: 6 mm
|
|
||||||
- unten: 6 mm
|
|
||||||
- links: 6 mm
|
|
||||||
- rechts: 6 mm
|
|
||||||
- Spaltenraster
|
|
||||||
- Anzahl: 4
|
|
||||||
- Breite: alle gleich
|
|
||||||
- Gutter: 4 mm
|
|
||||||
- Font
|
|
||||||
- Name: DejaVu Sans
|
|
||||||
- size
|
|
||||||
- default: 16
|
|
||||||
- small: 14
|
|
||||||
- large: 18,
|
|
||||||
- lineHeightFactor
|
|
||||||
- default: 1.2
|
|
||||||
- small: 1.1
|
|
||||||
- large: 1.3
|
|
||||||
- spacing
|
|
||||||
- vertical: 0.5 mm
|
|
||||||
- horizontal: 0.5 mm
|
|
||||||
- Boxtypen
|
|
||||||
- id: 01
|
|
||||||
- name: header
|
|
||||||
- f-Size: default
|
|
||||||
- type: Label: Wert
|
|
||||||
- id: 02
|
|
||||||
- name: stats
|
|
||||||
- f-Size: large
|
|
||||||
- type: Label: Wert
|
|
||||||
- style: boxed
|
|
||||||
- id: 03
|
|
||||||
- name: table
|
|
||||||
- f-Size: default
|
|
||||||
- type: Label: Wert
|
|
||||||
- style: nobr, shorten
|
|
||||||
|
|
||||||
Wenn anchor für Blöcke nicht angegeben ist wird als default "top" verwendet.
|
|
||||||
Wenn heightMode für Blöcke nicht angegeben ist wird als default "fixed" verwendet.
|
|
||||||
|
|
||||||
- In jedem Spaltenbereich:
|
|
||||||
- zuerst alle anchor=bottom-Blöcke von unten platzieren,
|
|
||||||
- dann alle anchor=top-Blöcke von oben,
|
|
||||||
- für heightMode=rows-Tabellen:
|
|
||||||
- maxRows = floor(availableHeight / rowHeight).
|
|
||||||
|
|
||||||
Rowheight wird wie folgt berechnet:
|
|
||||||
rowHeight_mm = fontSize_pt * lineHeightFactor * ptToMm + spacing.vertical
|
|
||||||
|
|
||||||
|
|
||||||
## Strukturierung der Seiten A4 Quer
|
|
||||||
|
|
||||||
Jede seite ist prinzipiell 4 spaltig angelegt. Die Informationen sind in Blöcken organisiert die bei Bedarf mehrere Spalten zusammen fassen können. Wenn nich anders festgelegt laufen die Informationen in den Blöcken von links oben nach rechts unten. „anders festgelegt“ bedeutet:
|
|
||||||
- anchor=bottom ist eine Ausnahme,
|
|
||||||
- ansonsten ist die Reihenfolge in der Seitendefinition die Flow-Reihenfolge.
|
|
||||||
|
|
||||||
## Seite 1 (Statistikseite)
|
|
||||||
|
|
||||||
- Block 1, Spalte 1,2,3,4, Darstellung Label: Wert
|
|
||||||
- Name des Charakters
|
|
||||||
- Name des Spielers
|
|
||||||
- Block 2, Spalte 1, Darstellung Label: Wert
|
|
||||||
- Typ, Grad
|
|
||||||
- Spezialisierung
|
|
||||||
- Block 3, Spalte 1, Darstellung Box Label: Wert
|
|
||||||
- St, Gs, Gw
|
|
||||||
- Ko, In, Zt
|
|
||||||
- Au, pA, Wk
|
|
||||||
- B, Raufen
|
|
||||||
- Block 4, Spalte 1, Darstellung Label: Wert
|
|
||||||
- "persönliche Boni für:"
|
|
||||||
- Ausdauer, Schaden, Angriff
|
|
||||||
- Abweht, Resistenz, Zaubern
|
|
||||||
- Block 5, Spalte 1,2, Darstellung Box Label: Wert
|
|
||||||
- LP-Max, AP-Max, GG, SG
|
|
||||||
- Block 6, Spalte 1,2, Darstellung Tabelle
|
|
||||||
- Tabelle Spielergebnisse
|
|
||||||
- Block 7, Spalte 2, Darstellung Label: Wert
|
|
||||||
- Geburtsdatum
|
|
||||||
- Alter Händigkeit
|
|
||||||
- Größe
|
|
||||||
- Gestalt, Gewicht
|
|
||||||
- Stand
|
|
||||||
- Heimat
|
|
||||||
- Glaube
|
|
||||||
- Besondere Merkmale
|
|
||||||
- Block 8, Spalte 3,4
|
|
||||||
- "Liste der gelernten und angeborenen Fertigkeiten"
|
|
||||||
- Block 9, Spalte 3, Darstellung Tabelle, heightMode: rows
|
|
||||||
- Tabelle Fertigkeiten
|
|
||||||
- Block 10, Spalte 3,4, Darstellung Label: Wert, anchor: bottom
|
|
||||||
- Sehen, Nachtsicht, Hören, Riechen/Schmecken, Sechster Sinn
|
|
||||||
- Block 11, Spalte 4, Darstellung Tabelle, heightMode: rows
|
|
||||||
- Fortsetzung Tabelle Fertigkeiten
|
|
||||||
|
|
||||||
|
|
||||||
## Seite 2 (Spielbogen).
|
|
||||||
|
|
||||||
- Block 1, Spalte 1,2,3,4, Darstellung Label: Wert
|
|
||||||
- Name des Charakters, Grad
|
|
||||||
- Typ, GG, SG
|
|
||||||
- Block 2, Spalte 1,2, Darstellung Box Label: Wert
|
|
||||||
- St, Gs, Gw, Ko, In,
|
|
||||||
- Zt, Au, pA, Wk, B
|
|
||||||
- Block 3, Spalte 1,2, Darstellung Label: Wert
|
|
||||||
- Abwehr, Abwehr mit Verteidigungswaffe, Resistenz, Zaubern
|
|
||||||
- Block 4, Spalte 1, Darstellung Tabelle, heightMode: rows
|
|
||||||
- Tabelle Fertigkeiten
|
|
||||||
- Block 5, Spalte 2, Darstellung Tabelle
|
|
||||||
- Tabelle ungelernte Fertigkeiten
|
|
||||||
- Block 6, Spalte 2, Darstellung Tabelle, heightMode: rows
|
|
||||||
- Fortsetzung Tabelle Fertigkeiten
|
|
||||||
- Block 7, Spalte 3,4, Darstellung Tabelle
|
|
||||||
- Tabelle LP/AP
|
|
||||||
- Block 8, Spalte 3, Darstellung Label: Wert
|
|
||||||
- "Bonus für"
|
|
||||||
- "Schaden", "Angriff", "Abwehr"
|
|
||||||
- Schaden, Angriff, Abwehr
|
|
||||||
- "Mit Rüstung", Angriff, Abwehr
|
|
||||||
- Block 9 ,Spalte 3, Darstellung Tabelle, heightMode: rows
|
|
||||||
- Tabelle Waffen
|
|
||||||
- Block 10, Spalte 3,4, Darstellung Label: Wert, anchor: bottom
|
|
||||||
- Sehen, Nachtsicht, Hören, Riechen/Schmecken, Sechster Sinn
|
|
||||||
- Block 11, Spalte 4, Darstellung Label: Wert
|
|
||||||
- "RK", rk-lp-bonus
|
|
||||||
- , mit Rüstung", B, Gw
|
|
||||||
- Block 12 ,Spalte 4, Darstellung Tabelle, heightMode: rows
|
|
||||||
- Fortsetzung Tabelle Waffen
|
|
||||||
- Block 13, Spalte 4, Darstellung Tabelle
|
|
||||||
- Tabelle PP Zauber
|
|
||||||
|
|
||||||
## Seite 3 (Zauber )
|
|
||||||
|
|
||||||
- Block 1, Spalte 1,2,3,4, Darstellung Label: Wert
|
|
||||||
- Name des Charakters, Grad
|
|
||||||
- Typ, Zaubern
|
|
||||||
- Block 2. Spalte 1,2, Darstellung Tabelle, heightMode: rows
|
|
||||||
- Tabelle Zauber
|
|
||||||
- Block 3. Spalte 3,4, Darstellung Tabelle, heightMode: rows
|
|
||||||
- Fortsetzung Tabelle Zauber
|
|
||||||
- Block 4, Spalte 3,4, Darstellung Tabelle, heightMode: rows
|
|
||||||
- Tabelle magische Gegenstände /Ausrüstung
|
|
||||||
|
|
||||||
## Seite 4 (Ausrüstung)
|
|
||||||
- Block 1, Spalte 1,2,3,4, Darstellung Label: Wert
|
|
||||||
- Name des Charakters, Grad
|
|
||||||
- Typ
|
|
||||||
- Block 2. Spalte 1,2, Darstellung Tabelle, heightMode: rows
|
|
||||||
- Tabelle Ausrüstung (Am Körper getragen)
|
|
||||||
- Block 3. Spalte 1,2, Darstellung Tabelle, heightMode: rows
|
|
||||||
- Tabelle Behälter 1 (Im Wagen)
|
|
||||||
- Block 4. Spalte 3,4, Darstellung Tabelle, heightMode: rows
|
|
||||||
- Tabelle Behälter 2
|
|
||||||
- Block 5. Spalte 3,4, Darstellung Tabelle, heightMode: rows
|
|
||||||
- Tabelle Behälter 2
|
|
||||||
|
|
||||||
|
|
||||||
## Seite 2.1 (Fortsetzung Spielbogen).
|
|
||||||
|
|
||||||
Wenn die Anzahl der Fertigkeiten oder die Anzahl der Waffen den Platz in der Liste überschreitet wird eine oder mehrere Seiten angefügt. Diese enthält:
|
|
||||||
|
|
||||||
- Block 1, Spalte 1,2,3,4, Darstellung Label: Wert
|
|
||||||
- Name des Charakters, Grad
|
|
||||||
- Typ, GG, SG
|
|
||||||
- Block 2, Spalte 1, Darstellung Tabelle, heightMode: rows
|
|
||||||
- Fortsetzung Tabelle Fertigkeiten
|
|
||||||
- Block 3, Spalte 2, Darstellung Tabelle, heightMode: rows
|
|
||||||
- Fortsetzung Tabelle Fertigkeiten
|
|
||||||
- Block 4 ,Spalte 3, Darstellung Tabelle, heightMode: rows
|
|
||||||
- Fortsetzung Tabelle Fertigkeiten
|
|
||||||
- Block 5 ,Spalte 4, Darstellung Tabelle, heightMode: rows
|
|
||||||
- Fortsetzung Tabelle Waffen
|
|
||||||
|
|
||||||
## Seite 3.1 (Forsetung Zauber)
|
|
||||||
|
|
||||||
Wenn die Anzahl der Zauber den Platz in der Liste überschreitet wird eine oder mehrere identische Seiten angefügt, Block 4 wird dann auf die von Seite 3 auf Seite 3.1 verschoben.
|
|
||||||
|
|
||||||
- Block 1, Spalte 1,2,3,4, Darstellung Label: Wert
|
|
||||||
- Name des Charakters, Grad
|
|
||||||
- Typ, Zaubern
|
|
||||||
- Block 2. Spalte 1,2, Darstellung Tabelle, heightMode: rows
|
|
||||||
- Fortsetzung Tabelle Zauber
|
|
||||||
- Block 3. Spalte 3,4, Darstellung Tabelle, heightMode: rows
|
|
||||||
- Fortsetzung Tabelle Zauber
|
|
||||||
- Block 4, Spalte 3,4, Darstellung Tabelle, heightMode: rows
|
|
||||||
- Tabelle magische Gegenstände /Ausrüstung
|
|
||||||
|
|
||||||
|
|
||||||
## Definition der Tabellen
|
|
||||||
|
|
||||||
### Tabelle Fertigkeiten
|
|
||||||
| Fertigkeit | EW | PP |
|
|
||||||
| ---------- | -- | -- |
|
|
||||||
| Fertigkeit | EW | PP |
|
|
||||||
|
|
||||||
### Tabelle Waffen
|
|
||||||
|Waffe | EW | Schaden | Nah |
|
|
||||||
| ---- | -- | -- | -- |
|
|
||||||
|Waffe | EW | Schaden | Nah |
|
|
||||||
|
|
||||||
### Tabelle PP Zauber
|
|
||||||
| Zauber klasse | PP |
|
|
||||||
| ---------- | -- |
|
|
||||||
| Fertigkeit | PP |
|
|
||||||
|
|
||||||
### Tabelle Zauber
|
|
||||||
| AP/Prozess | Zauber | Zd/Rw | Wb/Wd | Wirkung | WirkZiel/Art |
|
|
||||||
| ---- | -------- | ---- | ---- | -------- | ---- |
|
|
||||||
| AP/Prozess | Zauber | Zd/Rw | Wb/Wd | Wirkung | WirkZiel/Art |
|
|
||||||
|
|
||||||
### Tabelle magische Gegenstände
|
|
||||||
| Gegenstande | Wirkung, Inhalt, und andere Erlä#uterungen |
|
|
||||||
| -------- | -------------------- |
|
|
||||||
| Gegenstande | Wirkung, Inhalt, und andere Erlä#uterungen |
|
|
||||||
|
|
||||||
### Tabelle Spielergebnisse
|
|
||||||
|
|
||||||
|Datum | | | | | | | | | | |
|
|
||||||
|ES | | | | | | | | | | |
|
|
||||||
|EP | | | | | | | | | | |
|
|
||||||
|Geld | | | | | | | | | | |
|
|
||||||
|
|
||||||
### Tabelle LP/AP
|
|
||||||
|
|
||||||
| LP | | | | | | |
|
|
||||||
| AP | | | | | | |
|
|
||||||
@@ -1,194 +1,410 @@
|
|||||||
## Plan: PDF-Export für Charakterbögen
|
## Plan: PDF-Export für Charakterbögen
|
||||||
|
|
||||||
**Ziel:** Charakterbögen als mehrseitige PDF-Datei exportieren mit flexiblen Templates (A4 Hochformat, A4 Querformat) und automatischem Seitenumbruch für Listen variabler Länge.
|
**Ziel:** Charakterbögen als mehrseitige PDF-Datei exportieren mit HTML-Templates und automatischem Seitenumbruch für Listen variabler Länge.
|
||||||
|
|
||||||
**Tech-Stack:** Go Backend (Gin), Vue 3 Frontend, GORM Datenbank
|
**Tech-Stack:** Go Backend (Gin), Vue 3 Frontend, GORM Datenbank, chromedp, Go html/template
|
||||||
|
|
||||||
|
**Architektur-Entscheidung:** Statt low-level PDF-Generierung nutzen wir HTML-Templates + chromedp für Browser-basiertes Rendering. Dies ermöglicht:
|
||||||
|
- Moderne CSS-Layouts (Flexbox, Grid, @page media queries)
|
||||||
|
- Einfachere Template-Wartung (HTML/CSS statt Koordinaten-basiert)
|
||||||
|
- Perfekte Schrift-/Unicode-Unterstützung
|
||||||
|
- WYSIWYG-Entwicklung (Templates im Browser testbar)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### Step 1: PDF-Bibliothek integrieren
|
### Step 1: HTML-Templates vorbereiten & integrieren
|
||||||
|
|
||||||
**Details:**
|
**Template-Struktur:**
|
||||||
|
```
|
||||||
|
backend/
|
||||||
|
templates/
|
||||||
|
Default_A4_Quer/
|
||||||
|
page1_stats.html # Seite 1: Statistikseite
|
||||||
|
page2_play.html # Seite 2: Spielbogen
|
||||||
|
page3_spell.html # Seite 3: Zauberseite
|
||||||
|
page4_equip.html # Seite 4: Ausrüstung
|
||||||
|
shared/
|
||||||
|
export_format_a4_quer.css # Gemeinsames Stylesheet
|
||||||
|
headerimg.png # Statische Dekorationsgrafik
|
||||||
|
```
|
||||||
|
|
||||||
- **Anforderungen aus dem Layout-/Template-Design:**
|
**Template-Konvertierung (HTML → Go Templates):**
|
||||||
- Absolut positionierbare Inhalte (Koordinaten in mm/pt).
|
- Bestehende HTML-Dateien (`char_p1_stats_a4_quer.html` etc.) dienen als Basis
|
||||||
- Unterstützung für mehrere Seiten (Seitenumbrüche, Seitengröße, Ränder).
|
- Beispieldaten werden durch Go template-Syntax ersetzt:
|
||||||
- UTF‑8/Unicode für deutsche Texte inkl. Umlaute (DejaVu Sans als eingebettete Schrift).
|
- `Bjarnfinnur Haberdson` → `{{.Character.Name}}`
|
||||||
- Zeichen primitiver Formen (Linien, Rechtecke) für Tabellenraster/Boxen.
|
- `18` (Grad) → `{{.Character.Grade}}`
|
||||||
- Möglichkeit, Text an beliebigen Positionen mit einheitlicher Schrift und Größe auszugeben.
|
- `79` (St) → `{{.Character.Attributes.St}}`
|
||||||
|
- Tabellen-Loops: `{{range .Character.Skills}}...{{end}}`
|
||||||
|
|
||||||
- **Technische Auswahl (geplant):**
|
**CSS-Handling:**
|
||||||
- Einsatz einer reinen Go-PDF-Bibliothek (z.B. `gofpdf` o.ä.), die:
|
- Externes Stylesheet `export_format_a4_quer.css` bleibt erhalten
|
||||||
- Seitengröße in mm/pt setzen kann,
|
- Templates linken auf Stylesheet via `<link rel="stylesheet" href="export_format_a4_quer.css">`
|
||||||
- Schriften laden/registrieren kann (TrueType, DejaVu Sans),
|
- Beim Rendering wird CSS-Datei aus `templates/Default_A4_Quer/shared/` geladen
|
||||||
- Textausgabe an absoluten Koordinaten unterstützt.
|
- Chromedp wartet auf vollständiges Laden aller Stylesheets vor PDF-Generierung
|
||||||
|
|
||||||
- **Integrationskonzept:**
|
**Bild-Handling:**
|
||||||
- Neues Package `backend/pdf`:
|
- **Charakter-Icons:**
|
||||||
- Kapselt die PDF-Bibliothek.
|
- Aus Datenbank geladen (Character.IconData als Blob)
|
||||||
- Stellt einfache Operationen bereit:
|
- Als base64-kodiertes Data-URI in Template eingefügt:
|
||||||
- `NewDocument(pageSizeMm, marginsMm)`.
|
- `<img src="data:image/png;base64,{{.Character.IconBase64}}">`
|
||||||
- `AddPage()`.
|
- **Statische Bilder (headerimg.png):**
|
||||||
- `SetFont(name, sizePt, bold/normal)`.
|
- Aus Filesystem geladen (`templates/Default_A4_Quer/shared/headerimg.png`)
|
||||||
- `DrawText(xMm, yMm, text)`.
|
- Als base64-kodierte Data-URIs in Template eingebettet oder als relativer Pfad
|
||||||
- `DrawLine(x1Mm, y1Mm, x2Mm, y2Mm)`.
|
|
||||||
- Hilfsfunktionen für Tabellenzellen (Text mit optional mehreren Zeilen).
|
**Print-Media CSS:**
|
||||||
- Versteckt Einheitenkonvertierung (`ptToMm`) und Bibliotheksdetails.
|
- Bestehende `@page { size: A4 landscape; margin: 1cm; }` bleibt erhalten
|
||||||
- Fonts:
|
- Chromedp nutzt print media emulation für korrektes Seiten-Layout
|
||||||
- DejaVu Sans (normal, ggf. bold) als eingebettete TTF-Fonts.
|
|
||||||
- Registrierung beim Start im `pdf`-Package.
|
|
||||||
- Minimaler Prototyp:
|
|
||||||
- Funktion, die ein einfaches Test-PDF erzeugt (eine Seite, etwas Text), um Encoding/Fonts zu validieren.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### Step 2: Template-System aufbauen
|
### Step 2: Chromedp-Integration aufbauen
|
||||||
|
|
||||||
**Details:**
|
**Package-Struktur:**
|
||||||
- Layout- und Blockstruktur wird in `backend/doc/Characterbogen_Aufteilung.md` beschrieben und dient als Kanon.
|
```
|
||||||
- Ein Template (z.B. `backend/doc/template_a4_quer.yaml`) besteht aus:
|
backend/
|
||||||
- **Globaler Seitendefinition**:
|
pdfrender/
|
||||||
- Seitengröße (z.B. A4 quer 297×210 mm)
|
chromedp.go # Chromedp-Wrapper
|
||||||
- Seitenränder (oben/unten/links/rechts)
|
templates.go # Template-Loader & Renderer
|
||||||
- Spaltenraster (Anzahl, Gutter; Breite wird abgeleitet)
|
pagination.go # Listen-Pagination-Logik
|
||||||
- Font-Konfiguration (Name, Größen, `lineHeightFactor`, Spacing)
|
viewmodel.go # Domain → Template Mapping
|
||||||
- **Seitentypen**:
|
```
|
||||||
- Seite 1: Statistikseite
|
|
||||||
- Seite 2: Spielbogen
|
|
||||||
- Seite 3: Zauberseite
|
|
||||||
- Seite 4: Ausrüstung
|
|
||||||
- Seite 2.1: Fortsetzung Spielbogen (Fertigkeiten/Waffen)
|
|
||||||
- Seite 3.1: Fortsetzung Zauber (Zauber/Magische Gegenstände)
|
|
||||||
- **Blöcken pro Seite**:
|
|
||||||
- Jeder Block hat:
|
|
||||||
- `blockId` (z.B. 1–13 wie in `Characterbogen_Aufteilung.md`)
|
|
||||||
- `pageType` (z.B. `page1_stats`, `page2_play`, `page2_play_cont`, `page3_spells`, …)
|
|
||||||
- Spaltenstart/-spanne (`colStart`, `colSpan`), bezogen auf 4 gleich breite Spalten mit Gutter
|
|
||||||
- `blockType`:
|
|
||||||
- `labelValue` (Label: Wert)
|
|
||||||
- `boxLabelValue` (Label: Wert, mit Rahmen)
|
|
||||||
- `table`
|
|
||||||
- `boxTypeId` (01=header, 02=stats, 03=table) → liefert Fontgröße, Style (`boxed`, `nobr`, `shorten`)
|
|
||||||
- `anchor`: `"top"` (default) oder `"bottom"`
|
|
||||||
- `heightMode`:
|
|
||||||
- `"fixed"` (default): Höhe ergibt sich aus bekannter Zeilenanzahl (z.B. Spielergebnisse, LP/AP)
|
|
||||||
- `"rows"`: Anzahl Zeilen wird dynamisch aus verfügbarem Platz berechnet (Fertigkeiten, Waffen, Zauber, Ausrüstung)
|
|
||||||
- später: `binding` (welche Daten ins Feld/Tabelle kommen)
|
|
||||||
- **Boxtypen**:
|
|
||||||
- global definiert (Fontgröße, Style, Verhalten in Tabellen):
|
|
||||||
- `header` (id: 01): Label:Wert, `f-Size: default`
|
|
||||||
- `stats` (id: 02): Label:Wert, `f-Size: large`, `style: boxed`
|
|
||||||
- `table` (id: 03): Tabellenzellen, `f-Size: default`, `style: nobr, shorten` (kein Umbruch, Text wird ggf. gekürzt)
|
|
||||||
|
|
||||||
- Alle dynamischen Listen (Fertigkeiten, Waffen, Zauber, Ausrüstung) werden ausschließlich in Blöcken mit `heightMode: rows` gerendert; alle anderen Tabellen haben eine feste, aus dem Layout bekannte Zeilenanzahl.
|
**Chromedp-Wrapper (`chromedp.go`):**
|
||||||
|
- Funktion `RenderHTMLToPDF(htmlPages []string) ([]byte, error)`
|
||||||
|
- Input: Array von gerenderten HTML-Strings (eine pro Seite)
|
||||||
|
- Output: PDF als byte array
|
||||||
|
- Chromedp-Konfiguration:
|
||||||
|
- Headless-Modus
|
||||||
|
- Disable-GPU (für Server-Umgebung)
|
||||||
|
- Print-to-PDF mit Optionen:
|
||||||
|
- `Landscape: true` für A4 quer
|
||||||
|
- `PrintBackground: true` für CSS-Hintergründe
|
||||||
|
- `PreferCSSPageSize: true` für `@page`-Regeln
|
||||||
|
- Timeout-Handling (z.B. 30s max)
|
||||||
|
|
||||||
**Section-Renderer:**
|
**Template-Loader (`templates.go`):**
|
||||||
- Aufgabe des Section-Renderers:
|
- Funktion `LoadTemplates() (*template.Template, error)`
|
||||||
- Aus Template + Characterdaten → konkrete Seiten mit absoluten Koordinaten (mm/pt) erzeugen.
|
- Lädt alle `.html`-Dateien aus `templates/Default_A4_Quer/`
|
||||||
- Pro Seite:
|
- Parse mit Go's `html/template`
|
||||||
- Spaltenkoordinaten berechnen
|
- Validierung: Alle erwarteten Templates vorhanden
|
||||||
- Blöcke positionieren (Top-/Bottom-Anker)
|
- Caching: Templates einmalig beim Start laden
|
||||||
- Für `heightMode: rows` die `maxRows` aus verfügbaren Höhen bestimmen.
|
- Funktion `RenderTemplate(name string, data interface{}) (string, error)`
|
||||||
|
- Rendert einzelnes Template mit Daten
|
||||||
|
- Fügt base64-kodierte statische Bilder ein
|
||||||
|
- Error-Handling für fehlende Template-Variablen
|
||||||
|
|
||||||
- Globale Layoutwerte (aus `Characterbogen_Aufteilung.md`):
|
**Helper-Funktionen:**
|
||||||
- Seitengröße A4 quer: 297×210 mm
|
- `ImageToBase64DataURI(imageData []byte, mimeType string) string`
|
||||||
- Ränder: oben/unten/links/rechts je 6 mm
|
- Konvertiert Bild-Bytes zu Data-URI
|
||||||
- Spaltenraster:
|
- `LoadStaticImage(path string) (string, error)`
|
||||||
- 4 Spalten, alle gleich breit
|
- Lädt statisches Bild und konvertiert zu base64
|
||||||
- Gutter: 4 mm
|
|
||||||
- Font:
|
|
||||||
- Name: DejaVu Sans
|
|
||||||
- `size`:
|
|
||||||
- `default`: 16 pt
|
|
||||||
- `small`: 14 pt
|
|
||||||
- `large`: 18 pt
|
|
||||||
- `lineHeightFactor`:
|
|
||||||
- `default`: 1.2
|
|
||||||
- `small`: 1.1
|
|
||||||
- `large`: 1.3
|
|
||||||
- `spacing`:
|
|
||||||
- `vertical`: 0.5 mm
|
|
||||||
- `horizontal`: 0.5 mm
|
|
||||||
|
|
||||||
- Zeilenhöhe-Berechnung:
|
|
||||||
- Konstante: `ptToMm` (z.B. 1 pt ≈ 0.3528 mm)
|
|
||||||
- Für jede Font-Kategorie wird eine Zeilenhöhe berechnet:
|
|
||||||
- `rowHeight_mm = fontSize_pt * lineHeightFactor * ptToMm + spacing.vertical`
|
|
||||||
- Tabellen mit `style: nobr, shorten` werden so gerendert, dass jede Tabellenzeile **immer einzeilig** ist:
|
|
||||||
- Kein automatischer Zeilenumbruch
|
|
||||||
- Text ggf. hart gekürzt (`shorten`), um die konstante Zeilenhöhe zu garantieren.
|
|
||||||
|
|
||||||
- Mehrzeilige Tabellenzellen (Zauber-Tabelle, Spalten „AP/Prozess“ und „Zd/Rw“):
|
|
||||||
- Einige Tabellenspalten bestehen logisch aus **zwei Werten**, die in derselben Zelle untereinander stehen.
|
|
||||||
- Beispiel Zauber:
|
|
||||||
- Spalte „AP/Prozess“: erste Zeile = `ap`, zweite Zeile = `prozess`
|
|
||||||
- Spalte „Zd/Rw“: erste Zeile = `zd`, zweite Zeile = `rw`
|
|
||||||
- Im Template kann eine Spalte optional als „multiline“ markiert werden:
|
|
||||||
- Schema (pro Spalte):
|
|
||||||
- `multiline.linesPerCell`: Anzahl der Textzeilen in dieser Zelle (hier: `2`)
|
|
||||||
- `multiline.lines[]`: Liste von Bindings innerhalb einer Zelle, z.B.:
|
|
||||||
- `lines[0].binding = "ap"`
|
|
||||||
- `lines[1].binding = "prozess"`
|
|
||||||
- Für Tabellen, die solche Spalten verwenden (Zauber-Tabellen), gilt:
|
|
||||||
- Die effektive Zeilenhöhe dieser Tabelle wird so berechnet:
|
|
||||||
- `rowHeight_mm_multiline = multiline.linesPerCell * (fontSize_pt * lineHeightFactor * ptToMm + spacing.vertical)`
|
|
||||||
- Der Section-Renderer verwendet für diese Tabelle `rowHeight_mm_multiline` zur Berechnung von `maxRows`.
|
|
||||||
- In der Implementierung:
|
|
||||||
- Beim Rendern einer Zelle mit `multiline`:
|
|
||||||
- Text nach den in `multiline.lines[]` angegebenen Bindings aufteilen,
|
|
||||||
- jede Zeile übereinander zeichnen (gleiche X-Position, Y-Versatz pro Zeile),
|
|
||||||
- keine automatischen Zeilenumbrüche jenseits der zwei konfigurierten Zeilen.
|
|
||||||
|
|
||||||
- Block-Flow und Anker-Regeln:
|
|
||||||
- Wenn `anchor` für einen Block nicht angegeben ist → default `"top"`.
|
|
||||||
- Wenn `heightMode` nicht angegeben ist → default `"fixed"`.
|
|
||||||
- „Flow“:
|
|
||||||
- Wenn nicht anders festgelegt, laufen die Blöcke „von links oben nach rechts unten“:
|
|
||||||
- Reihenfolge in der Seitendefinition = Flow-Reihenfolge.
|
|
||||||
- „Anders festgelegt“ bedeutet:
|
|
||||||
- `anchor = "bottom"` → Block ist von unten verankert.
|
|
||||||
- In jedem Spaltenbereich pro Seite:
|
|
||||||
1. **Spaltenbereich berechnen**:
|
|
||||||
- `colTopY = pageTopMargin` (bzw. Ende der Kopfblöcke, wenn Seite das so vorsieht)
|
|
||||||
- `colBottomY = pageHeight - bottomMargin`
|
|
||||||
2. **Bottom-Blöcke platzieren**:
|
|
||||||
- Alle Blöcke mit `anchor="bottom"` in dieser Spaltenkombination ermitteln.
|
|
||||||
- Für jeden:
|
|
||||||
- Höhe bestimmen:
|
|
||||||
- `height = fixedHeight` (für Label/Box-Blöcke mit bekannter Zeilenanzahl)
|
|
||||||
- Position:
|
|
||||||
- `blockY = currentBottomY - height`
|
|
||||||
- `currentBottomY = blockY - verticalGap`
|
|
||||||
- Ergebnis: reduzierter nutzbarer Bereich:
|
|
||||||
- `maxContentBottomY = currentBottomY`
|
|
||||||
3. **Top-Blöcke platzieren**:
|
|
||||||
- Start: `cursorY = colTopY`
|
|
||||||
- Alle `anchor="top"`-Blöcke in definierter Flow-Reihenfolge:
|
|
||||||
- Für Blöcke mit `heightMode="fixed"`:
|
|
||||||
- Höhe aus bekannter Struktur (Anzahl Label:Wert-Zeilen oder Tabellenzeilen).
|
|
||||||
- `blockY = cursorY`
|
|
||||||
- `cursorY += height + verticalGap`
|
|
||||||
- Für Tabellen mit `heightMode="rows"`:
|
|
||||||
- Verfügbarer vertikaler Platz:
|
|
||||||
- `availableHeight = maxContentBottomY - cursorY`
|
|
||||||
- `rowHeight` für diesen Block aus Font/Boxtyp
|
|
||||||
- `maxRows = floor(availableHeight / rowHeight)`
|
|
||||||
- `blockY = cursorY`
|
|
||||||
- `cursorY += maxRows * rowHeight + verticalGap`
|
|
||||||
- `maxRows` wird später in der Pagination-Logik verwendet, um „wie viele Elemente dieser Liste passen in diesen Block?“ zu beantworten.
|
|
||||||
|
|
||||||
- Spezieller Fall „Block unten, Liste oben füllt Rest“:
|
|
||||||
- Beispiel Seite 1 (Statistikseite), Spalten 3–4:
|
|
||||||
- Block 10 (Sinne) hat `anchor="bottom"`, feste Zeilenanzahl → feste Höhe.
|
|
||||||
- Blöcke 9/11 (Fertigkeiten-Tabellen) haben `heightMode="rows"`, `anchor="top"`.
|
|
||||||
- Renderer:
|
|
||||||
- Platziert Block 10 von unten.
|
|
||||||
- Berechnet aus dem verbleibenden vertikalen Bereich die `maxRows` für 9 (Spalte 3) und 11 (Spalte 4).
|
|
||||||
- Diese `maxRows`-Werte liefern die Kapazität der ersten Statistikseite für Fertigkeiten. Alles darüber geht in spätere Seiten (Seite 2/2.1).
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### Step 3: API-Endpoint implementieren
|
### Step 3: Pagination-Logik implementieren
|
||||||
|
|
||||||
|
**Problem:** Listen (Skills, Weapons, Spells, Equipment) können Template-Kapazität überschreiten.
|
||||||
|
|
||||||
|
**Lösung:** Blockweise Pagination mit Fortsetzungsseiten
|
||||||
|
|
||||||
|
**Konzept:**
|
||||||
|
- Jede Liste hat eine `maxRows` pro Block/Seite
|
||||||
|
- Beispiel Skills Seite 1:
|
||||||
|
- Spalte 3: max ~32 Zeilen
|
||||||
|
- Spalte 4: max ~32 Zeilen
|
||||||
|
- Total Seite 1: ~64 Skills
|
||||||
|
- Bei Überschreitung: Fortsetzungsseite mit gleicher Struktur
|
||||||
|
|
||||||
|
**Implementation (`pagination.go`):**
|
||||||
|
|
||||||
|
```go
|
||||||
|
type PageData struct {
|
||||||
|
Character CharacterData
|
||||||
|
Skills []Skill // nur die Skills für diese Seite
|
||||||
|
Weapons []Weapon // nur die Weapons für diese Seite
|
||||||
|
// ... weitere Listen
|
||||||
|
}
|
||||||
|
|
||||||
|
type PaginationConfig struct {
|
||||||
|
Page1SkillsMax int // 64 (2x32)
|
||||||
|
Page2SkillsMax int // je nach Layout
|
||||||
|
Page2WeaponsMax int
|
||||||
|
Page3SpellsMax int
|
||||||
|
// ... für alle Listen
|
||||||
|
}
|
||||||
|
|
||||||
|
func PaginateCharacterData(char Character, config PaginationConfig) []PageData
|
||||||
|
```
|
||||||
|
|
||||||
|
**Pagination-Regeln (basierend auf Templates):**
|
||||||
|
- **Seite 1 (Stats):**
|
||||||
|
- Skills: 2 Spalten à ~32 Zeilen = 64 Skills max
|
||||||
|
- Overflow geht zu Seite 2
|
||||||
|
|
||||||
|
- **Seite 2 (Play):**
|
||||||
|
- Skills (gelernte): Linke Spalte, ~24 Zeilen
|
||||||
|
- Skills (ungelernte): Kleine Tabelle, ~15 Zeilen
|
||||||
|
- Skills (Sprachen/etc.): Weitere Tabelle, ~11 Zeilen
|
||||||
|
- Weapons: Große Tabelle rechts, ~30 Zeilen
|
||||||
|
- Bei Overflow: Seite 2.1 mit gleicher Struktur
|
||||||
|
|
||||||
|
- **Seite 3 (Spells):**
|
||||||
|
- Zauber: 2 Spalten à ~12 Zeilen = 24 Zauber
|
||||||
|
- Magische Gegenstände: ~5 Zeilen
|
||||||
|
- Bei Overflow: Seite 3.1
|
||||||
|
|
||||||
|
- **Seite 4 (Equipment):**
|
||||||
|
- Ausrüstungs-Sektionen: Variable Zeilenanzahl pro Sektion
|
||||||
|
- Bei Overflow: Seite 4.1
|
||||||
|
|
||||||
|
**Fortsetzungsseiten:**
|
||||||
|
- Gleiche Template-Struktur wie Hauptseite
|
||||||
|
- Header zeigt "Fortsetzung" an (optional)
|
||||||
|
- Datum bleibt gleich
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Step 4: View-Model Mapping
|
||||||
|
|
||||||
|
**Domain → Template Transformation (`viewmodel.go`):**
|
||||||
|
|
||||||
|
```go
|
||||||
|
type CharacterSheetViewModel struct {
|
||||||
|
// Basis-Daten
|
||||||
|
Character struct {
|
||||||
|
Name string
|
||||||
|
Player string
|
||||||
|
Type string
|
||||||
|
Grade int
|
||||||
|
Birthdate string
|
||||||
|
Age int
|
||||||
|
Height int
|
||||||
|
Weight int
|
||||||
|
IconBase64 string // base64-kodiertes Bild
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attribute
|
||||||
|
Attributes struct {
|
||||||
|
St, Gs, Gw, Ko, In, Zt, Au, PA, Wk, B int
|
||||||
|
}
|
||||||
|
|
||||||
|
// Abgeleitete Werte
|
||||||
|
DerivedValues struct {
|
||||||
|
LP, AP, GG, SG int
|
||||||
|
Abwehr, Resistenz string
|
||||||
|
Zaubern string
|
||||||
|
// Boni
|
||||||
|
AusdauerBonus int
|
||||||
|
SchadenBonus int
|
||||||
|
AngriffBonus int
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
|
||||||
|
// Listen (bereits paginiert für diese Seite)
|
||||||
|
Skills []SkillViewModel
|
||||||
|
Weapons []WeaponViewModel
|
||||||
|
Spells []SpellViewModel
|
||||||
|
MagicItems []MagicItemViewModel
|
||||||
|
Equipment []EquipmentViewModel
|
||||||
|
GameResults []GameResultViewModel
|
||||||
|
|
||||||
|
// Meta
|
||||||
|
Date string
|
||||||
|
PageNumber int
|
||||||
|
IsContinuation bool
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Mapping-Funktion:**
|
||||||
|
```go
|
||||||
|
func MapCharacterToViewModel(
|
||||||
|
char *models.Character,
|
||||||
|
pageType string,
|
||||||
|
pageNumber int,
|
||||||
|
) (*CharacterSheetViewModel, error)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Spezial-Handling:**
|
||||||
|
- **Fertigkeiten gruppieren:**
|
||||||
|
- Seite 1: Alle Skills mit Grundwerten (EW aus Grundfertigkeit)
|
||||||
|
- Seite 2: Nur gelernte Skills mit Spielwerten (PP, aktuelle EW)
|
||||||
|
- Ungelernte Skills mit Modifikatoren
|
||||||
|
|
||||||
|
- **Zauber formatieren:**
|
||||||
|
- Mehrzeilige Zellen: AP/Prozess als zwei Zeilen in einer Zelle
|
||||||
|
- `<hr>` für visuelle Trennung in HTML
|
||||||
|
|
||||||
|
- **Ausrüstung nach Container:**
|
||||||
|
- "Am Körper getragen"
|
||||||
|
- "Becher, Holz" (Container 1)
|
||||||
|
- "Wasserschlauch" (Container 2)
|
||||||
|
- etc.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Step 5: API-Endpoint implementieren
|
||||||
|
|
||||||
|
**Endpoint:**
|
||||||
|
```
|
||||||
|
GET /api/characters/{id}/pdf?format=a4_quer
|
||||||
|
```
|
||||||
|
|
||||||
|
**Handler-Flow:**
|
||||||
|
1. **Authentifizierung:** User-ID aus JWT/Session
|
||||||
|
2. **Autorisierung:** Prüfen ob User Zugriff auf Character hat
|
||||||
|
3. **Daten laden:**
|
||||||
|
```go
|
||||||
|
char := LoadCharacterWithRelations(id) // Preload Skills, Spells, Equipment, etc.
|
||||||
|
```
|
||||||
|
4. **Icon vorbereiten:**
|
||||||
|
```go
|
||||||
|
if char.IconData != nil {
|
||||||
|
iconBase64 := base64.StdEncoding.EncodeToString(char.IconData)
|
||||||
|
char.IconBase64 = "data:image/png;base64," + iconBase64
|
||||||
|
}
|
||||||
|
```
|
||||||
|
5. **Pagination:**
|
||||||
|
```go
|
||||||
|
pages := PaginateCharacterData(char, GetPaginationConfig())
|
||||||
|
```
|
||||||
|
6. **Template-Rendering:**
|
||||||
|
```go
|
||||||
|
htmlPages := make([]string, 0, 4)
|
||||||
|
for i, pageData := range pages {
|
||||||
|
var templateName string
|
||||||
|
switch pageData.Type {
|
||||||
|
case "stats": templateName = "page1_stats.html"
|
||||||
|
case "play": templateName = "page2_play.html"
|
||||||
|
case "spell": templateName = "page3_spell.html"
|
||||||
|
case "equip": templateName = "page4_equip.html"
|
||||||
|
}
|
||||||
|
html := RenderTemplate(templateName, pageData)
|
||||||
|
htmlPages = append(htmlPages, html)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
7. **PDF-Generierung:**
|
||||||
|
```go
|
||||||
|
pdfBytes, err := RenderHTMLToPDF(htmlPages)
|
||||||
|
```
|
||||||
|
8. **Response:**
|
||||||
|
```go
|
||||||
|
w.Header().Set("Content-Type", "application/pdf")
|
||||||
|
w.Header().Set("Content-Disposition",
|
||||||
|
fmt.Sprintf("attachment; filename=\"%s_charakterbogen.pdf\"",
|
||||||
|
sanitizeFilename(char.Name)))
|
||||||
|
w.Write(pdfBytes)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Error-Handling:**
|
||||||
|
- Character nicht gefunden: 404
|
||||||
|
- Keine Berechtigung: 403
|
||||||
|
- Template-Fehler: 500 mit Logging
|
||||||
|
- Chromedp-Fehler: 500 mit Logging
|
||||||
|
- Timeout: 504
|
||||||
|
|
||||||
|
**Route registrieren:**
|
||||||
|
- In `backend/character/routes.go`:
|
||||||
|
```go
|
||||||
|
charGroup.GET("/:id/pdf", CharacterPDFHandler)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Step 6: Frontend-Integration
|
||||||
|
|
||||||
|
**Vue Component: Character Detail View**
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<template>
|
||||||
|
<div class="character-actions">
|
||||||
|
<button @click="downloadPDF" :disabled="downloading">
|
||||||
|
<i class="icon-pdf"></i>
|
||||||
|
{{ downloading ? 'Generiere PDF...' : 'Charakterbogen (PDF)' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
const downloading = ref(false)
|
||||||
|
|
||||||
|
async function downloadPDF() {
|
||||||
|
downloading.value = true
|
||||||
|
try {
|
||||||
|
const response = await fetch(
|
||||||
|
`/api/characters/${characterId}/pdf?format=a4_quer`,
|
||||||
|
{ headers: { 'Authorization': `Bearer ${token}` } }
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!response.ok) throw new Error('PDF generation failed')
|
||||||
|
|
||||||
|
const blob = await response.blob()
|
||||||
|
const url = window.URL.createObjectURL(blob)
|
||||||
|
const a = document.createElement('a')
|
||||||
|
a.href = url
|
||||||
|
a.download = `${characterName}_charakterbogen.pdf`
|
||||||
|
a.click()
|
||||||
|
window.URL.revokeObjectURL(url)
|
||||||
|
} catch (error) {
|
||||||
|
// Error-Toast anzeigen
|
||||||
|
} finally {
|
||||||
|
downloading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Format-Auswahl (optional, später):**
|
||||||
|
```vue
|
||||||
|
<select v-model="selectedFormat">
|
||||||
|
<option value="a4_quer">A4 Querformat</option>
|
||||||
|
<option value="a4_hoch">A4 Hochformat</option>
|
||||||
|
</select>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Step 7: Deployment & Dependencies
|
||||||
|
|
||||||
|
**Go Dependencies:**
|
||||||
|
```bash
|
||||||
|
go get github.com/chromedp/chromedp
|
||||||
|
```
|
||||||
|
|
||||||
|
**System Requirements:**
|
||||||
|
- Chrome/Chromium installiert auf Server
|
||||||
|
- Ubuntu: `apt-get install chromium-browser`
|
||||||
|
- Alpine (Docker): `apk add chromium`
|
||||||
|
- Ausreichend Speicher für Chrome-Prozesse (~100-200MB pro Instanz)
|
||||||
|
|
||||||
|
**Docker Integration:**
|
||||||
|
```dockerfile
|
||||||
|
FROM golang:1.21-alpine
|
||||||
|
|
||||||
|
# Chrome installieren
|
||||||
|
RUN apk add --no-cache chromium
|
||||||
|
|
||||||
|
# Fonts für PDF (optional, verbessert Rendering)
|
||||||
|
RUN apk add --no-cache font-dejavu
|
||||||
|
|
||||||
|
# ... Rest des Dockerfiles
|
||||||
|
```
|
||||||
|
|
||||||
|
**Environment Variables:**
|
||||||
|
```bash
|
||||||
|
CHROME_BIN=/usr/bin/chromium-browser
|
||||||
|
PDF_TIMEOUT=30s
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### LEGACY REFERENCE (old approach - for reference only):
|
||||||
|
|
||||||
|
The following sections describe the original plan using YAML templates and coordinate-based rendering. This approach has been replaced by the HTML template + chromedp approach described above.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### OLD Step 3: API-Endpoint implementieren
|
||||||
|
|
||||||
**Details:**
|
**Details:**
|
||||||
|
|
||||||
@@ -250,48 +466,7 @@
|
|||||||
- `gameResults`.
|
- `gameResults`.
|
||||||
- Diese Listen sind unabhängig von Seiten und Blöcken.
|
- Diese Listen sind unabhängig von Seiten und Blöcken.
|
||||||
|
|
||||||
- **Template-Auswertung:**
|
|
||||||
- Template definiert:
|
|
||||||
- Seiten (`pages[]` mit `pageType`),
|
|
||||||
- Blöcke (`blocks[]` pro Seite),
|
|
||||||
- für `heightMode="rows"`: Tabellen, deren Zeilenanzahl limitiert ist.
|
|
||||||
- Der Section-Renderer berechnet:
|
|
||||||
- Für jeden `heightMode="rows"`-Block:
|
|
||||||
- `rowHeight` (inkl. `multiline.linesPerCell` bei Zauber),
|
|
||||||
- Nutzhöhe des Blocks (unter Berücksichtigung von Top-/Bottom-Ankern),
|
|
||||||
- `maxRows` = maximale Anzahl Datensätze für diesen Block.
|
|
||||||
|
|
||||||
- **Pagination-Engine:**
|
|
||||||
- Pro Liste (Fertigkeiten, Waffen, Zauber, Ausrüstung) wird eine Blockfüllreihenfolge definiert, basierend auf `Characterbogen_Aufteilung.md`:
|
|
||||||
- Fertigkeiten:
|
|
||||||
- Grundwerte: Seite 1, Block 9 → Block 11.
|
|
||||||
- Spielwerte: Seite 2, Block 4 → Block 6 → Seite 2.1 (Block 2/3/4) → weitere 2.x-Seiten.
|
|
||||||
- Waffen:
|
|
||||||
- Seite 2, Block 9 → Block 12 → Seite 2.1 (Block 5) → weitere 2.x-Seiten.
|
|
||||||
- Zauber:
|
|
||||||
- Seite 3, Block 2 → Block 3 → 3.x (Block 2/3) → weitere 3.x-Seiten.
|
|
||||||
- Magische Gegenstände:
|
|
||||||
- Seite 3, Block 4 → 3.x (Block 4) → weitere 3.x-Seiten.
|
|
||||||
- Ausrüstung:
|
|
||||||
- Seite 4, Blöcke 2–5 → ggf. 4.x mit gleichen Blöcken.
|
|
||||||
- Algorithmus:
|
|
||||||
1. Für jede Liste die zugehörigen Blöcke (Slots) in Reihenfolge sammeln (mit `pageType`, `blockId`, `maxRows`).
|
|
||||||
2. Über die Einträge der Liste iterieren und in diese Slots schreiben, bis `maxRows` erreicht ist.
|
|
||||||
3. Wenn alle Slots eines Seitentyps voll sind und noch Einträge existieren:
|
|
||||||
- Neue Instanz des Fortsetzungs-Seitentyps erzeugen (z.B. `page2_play_cont`).
|
|
||||||
- Die Slots dieser neuen Seite an die Füllreihenfolge anhängen.
|
|
||||||
- Ergebnis:
|
|
||||||
- Eine Liste konkreter Seiteninstanzen, bei denen jeder `heightMode="rows"`-Block bereits die passenden Datenzeilen (Schnitt der Liste) enthält.
|
|
||||||
|
|
||||||
- **Schnittstelle Renderer/Pagination:**
|
|
||||||
- Renderer erwartet:
|
|
||||||
- Seiten in Reihenfolge mit:
|
|
||||||
- `pageType`
|
|
||||||
- Liste von Blöcken mit:
|
|
||||||
- Layout-Infos (Koordinaten, Breite, Typ),
|
|
||||||
- bereits paginierte Daten (Zeilen für Tabellen).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Step 5: Frontend-Integration
|
### Step 5: Frontend-Integration
|
||||||
|
|
||||||
@@ -321,75 +496,89 @@
|
|||||||
|
|
||||||
### Step 6: Template-Konfiguration
|
### Step 6: Template-Konfiguration
|
||||||
|
|
||||||
**Details:**
|
|
||||||
|
|
||||||
- **Ablage & Format:**
|
|
||||||
- Templates als YAML-Dateien im Repo:
|
|
||||||
- z.B. `backend/doc/template_a4_quer.yaml` als Referenztemplate für A4 quer.
|
|
||||||
- Struktur orientiert sich an:
|
|
||||||
- `template.Id`, `name`,
|
|
||||||
- `layout` (pageSizeMm, marginsMm, columns, font, ptToMm),
|
|
||||||
- `boxTypes`,
|
|
||||||
- `pages[]` mit `blocks[]`.
|
|
||||||
|
|
||||||
- **Laden und Validieren:**
|
|
||||||
- Beim Start des Backends:
|
|
||||||
- Template-Dateien aus einem definierten Verzeichnis laden.
|
|
||||||
- Validierung:
|
|
||||||
- Pflichtfelder vorhanden (Id, layout, pages, blocks).
|
|
||||||
- `boxTypeId` verweist auf existierenden `boxTypes`-Eintrag.
|
|
||||||
- Spaltenbereiche sind konsistent (keine negativen `colStart/colSpan`, max 4 Spalten).
|
|
||||||
- Gültige Templates werden in einer Registry gehalten:
|
|
||||||
- Lookup per `templateId`.
|
|
||||||
|
|
||||||
- **MVP-Einschränkungen:**
|
|
||||||
- Templates nur als Files im Repo pflegen (kein UI zum Hochladen/Ändern).
|
|
||||||
- Änderungen am Layout erfolgen durch Anpassen der YAML-Dateien und Deployment.
|
|
||||||
|
|
||||||
**In-Code-Konstanten (KISS-Prinzip):**
|
|
||||||
|
|
||||||
- `defaultTemplateId = "a4_quer_char_sheet_v1"`.
|
|
||||||
- Pfad zum Template-Verzeichnis (z.B. `./backend/doc` oder eigener Ordner).
|
|
||||||
- Fallback-Logik:
|
|
||||||
- Wenn angefragtes Template nicht gefunden wird:
|
|
||||||
- Auf `defaultTemplateId` zurückfallen (sofern gültig).
|
|
||||||
|
|
||||||
**Font-Konfiguration:**
|
**Font-Konfiguration:**
|
||||||
|
|
||||||
- Zentral im `pdf`-Package:
|
- Zentral im `pdf`-Package:
|
||||||
- Registrierung der verwendeten Fonts (DejaVu Sans normal/bold).
|
- Registrierung der verwendeten Fonts (liberation sans als Arial Replacement).
|
||||||
- Mappings:
|
|
||||||
- BoxType → Fontgröße (`default/small/large`).
|
|
||||||
- Sicherstellen, dass:
|
- Sicherstellen, dass:
|
||||||
- Umlaute (ä, ö, ü, ß) und Sonderzeichen korrekt dargestellt werden,
|
- Umlaute (ä, ö, ü, ß) und Sonderzeichen korrekt dargestellt werden,
|
||||||
- gleiche Fontkonfiguration wie in Template/Plan (`Characterbogen_Aufteilung.md`).
|
- gleiche Fontkonfiguration für alle Template seitendamit die Zeilenhöhe konstant bleibt
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Implementierungs-Reihenfolge
|
## Implementierungs-Reihenfolge (New chromedp Approach)
|
||||||
|
|
||||||
1. **PDF-Bibliothek integrieren (Step 1):**
|
1. **HTML Templates vorbereiten (Step 1):**
|
||||||
- `pdf`-Package bauen, Test-PDF generieren.
|
- HTML-Templates nach `backend/templates/Default_A4_Quer/` kopieren
|
||||||
2. **Template-Lader und -Validator (Step 2 & 6):**
|
- CSS nach `backend/templates/Default_A4_Quer/shared/` kopieren
|
||||||
- YAML-Struktur für Templates definieren.
|
- Statische Bilder nach `backend/templates/Default_A4_Quer/shared/images/` kopieren
|
||||||
- `template_a4_quer.yaml` laden/prüfen.
|
- Beispieldaten durch Go-Template-Syntax ersetzen (z.B. `{{.Character.Name}}`)
|
||||||
3. **Section-Renderer (Layout-Math, Step 2):**
|
- Base64-Konvertierung für Bilder testen
|
||||||
- Spalten-/Koordinatenberechnung, Top-/Bottom-Anker, `rowHeight`-Berechnung.
|
|
||||||
4. **Pagination-Engine (Step 4):**
|
2. **Chromedp Integration (Step 2):**
|
||||||
- Blockfüllreihenfolgen gemäß `Characterbogen_Aufteilung.md` implementieren.
|
- `backend/pdfrender/chromedp.go` implementieren:
|
||||||
- Unit-Tests mit künstlichen Listen (viele Skills, viele Zauber, etc.).
|
- `RenderHTMLToPDF()` mit Context-Management
|
||||||
5. **View-Model-Mapping:**
|
- `PrintToPDF()` mit A4-Landscape-Settings
|
||||||
- Domänenstrukturen → Bindings im Template (Attribute, Fertigkeiten, Waffen, Zauber …).
|
- `backend/pdfrender/templates.go` implementieren:
|
||||||
6. **PDF-Render-Pipeline verbinden:**
|
- `LoadTemplates()` mit Parse-Fehlerbehandlung
|
||||||
- ViewModel → Pagination → Section-Renderer → `pdf`-Package.
|
- `RenderTemplate()` mit ViewModel-Binding
|
||||||
7. **API-Endpoint & Routing (Step 3):**
|
- CSS/Bild-Einbettung testen
|
||||||
- Handler implementieren, PDF streamen.
|
- Unit-Test: Einfaches HTML → PDF generieren
|
||||||
8. **Frontend-Integration (Step 5):**
|
|
||||||
- Download-Button, optional Template-Auswahl.
|
3. **Pagination-Logik (Step 3):**
|
||||||
9. **Feintuning & Tests:**
|
- `backend/pdfrender/pagination.go` implementieren:
|
||||||
- Layout gegen Originalbogen prüfen.
|
- `PaginationConfig` struct mit max rows
|
||||||
- `maxRows` ggf. feinjustieren.
|
- `PaginateCharacterData()` mit Liste-Splitting
|
||||||
- Tests aus der Testing-Checkliste durchgehen.
|
- Regeln implementieren (basierend auf HTML-Template-Kapazitäten):
|
||||||
|
- Seite 1: Skills max 64
|
||||||
|
- Seite 2: Weapons max 30, Skills various
|
||||||
|
- Seite 3: Spells max 24, Magic Items max 5
|
||||||
|
- Seite 4: Equipment nach Container
|
||||||
|
- Unit-Tests mit Overflow-Szenarien
|
||||||
|
|
||||||
|
4. **View-Model-Mapping (Step 4):**
|
||||||
|
- `backend/pdfrender/viewmodel.go` implementieren:
|
||||||
|
- `CharacterSheetViewModel` struct
|
||||||
|
- `MapCharacterToViewModel()` mit Domain → Template Transformation
|
||||||
|
- Spezialbehandlung:
|
||||||
|
- Skills gruppieren (Grundwerte/Spielwerte/ungelernt)
|
||||||
|
- Zauber formatieren (AP/Prozess mehrzeilig)
|
||||||
|
- Ausrüstung nach Container gruppieren
|
||||||
|
- Unit-Tests mit Mock-Character-Daten
|
||||||
|
|
||||||
|
5. **API-Endpoint (Step 5):**
|
||||||
|
- Handler `CharacterPDFHandler` in `backend/character/` implementieren:
|
||||||
|
- Auth/Authz
|
||||||
|
- Character + Relations laden
|
||||||
|
- Icon als base64 vorbereiten
|
||||||
|
- Pagination aufrufen
|
||||||
|
- Templates rendern
|
||||||
|
- Chromedp PDF-Generierung
|
||||||
|
- Stream als PDF
|
||||||
|
- Route registrieren in `backend/character/routes.go`
|
||||||
|
- Error-Handling (404/403/500/504)
|
||||||
|
|
||||||
|
6. **Frontend-Integration (Step 6):**
|
||||||
|
- Vue-Component anpassen:
|
||||||
|
- Download-Button mit Loading-State
|
||||||
|
- Fetch-Call zu `/api/characters/:id/pdf`
|
||||||
|
- Blob-Download-Handling
|
||||||
|
- Error-Toast bei Fehlern
|
||||||
|
- Optional: Format-Auswahl (A4-Quer/-Hoch)
|
||||||
|
|
||||||
|
7. **Deployment (Step 7):**
|
||||||
|
- Dockerfile anpassen:
|
||||||
|
- Chromium installieren
|
||||||
|
- Fonts installieren (dejavu)
|
||||||
|
- Environment-Variables konfigurieren:
|
||||||
|
- `CHROME_BIN`, `PDF_TIMEOUT`
|
||||||
|
- Docker-Compose testen
|
||||||
|
|
||||||
|
8. **Feintuning & Tests:**
|
||||||
|
- Layout in verschiedenen PDF-Readern testen
|
||||||
|
- `maxRows` justieren bei Bedarf
|
||||||
|
- Performance-Tests (Generierungs-Dauer messen)
|
||||||
|
- Testing-Checkliste durchgehen
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -422,28 +611,132 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Testing-Checkliste
|
## Testing-Checkliste (Chromedp Approach)
|
||||||
|
|
||||||
- [ ] Charakter mit wenigen Skills (1 Seite)
|
### Unit-Tests
|
||||||
- [ ] Charakter mit vielen Skills (Seitenumbruch)
|
- [ ] **Template Loading:**
|
||||||
- [ ] Charakter mit allen Listen gefüllt (Multi-Page)
|
- [ ] Alle 4 Templates laden ohne Fehler
|
||||||
- [ ] A4-Quer-Template (`template_a4_quer.yaml`)
|
- [ ] CSS-Datei korrekt eingebettet
|
||||||
- [ ] (später) A4-Hoch-Template
|
- [ ] Statische Bilder als base64 geladen
|
||||||
- [ ] Ungültiges Template (Fallback zu Default)
|
- [ ] Fehlerbehandlung bei fehlendem Template
|
||||||
- [ ] Charakter nicht gefunden (404)
|
|
||||||
- [ ] Nicht authentifiziert (401)
|
- [ ] **Pagination:**
|
||||||
- [ ] Download-Filename korrekt
|
- [ ] Skills: < 64 → 1 Seite, > 64 → 2 Seiten
|
||||||
- [ ] PDF öffnet korrekt in verschiedenen PDF-Readern
|
- [ ] Weapons: < 30 → 1 Block, > 30 → Continuation Page
|
||||||
- [ ] Umlaute (ä, ö, ü, ß) korrekt dargestellt
|
- [ ] Spells: < 24 → 1 Seite, > 24 → 2 Seiten
|
||||||
- ...
|
- [ ] Equipment: Multiple Container richtig gruppiert
|
||||||
|
|
||||||
|
- [ ] **View-Model Mapping:**
|
||||||
|
- [ ] Character-Basics korrekt gemappt
|
||||||
|
- [ ] Attributes alle vorhanden
|
||||||
|
- [ ] Derived Values berechnet
|
||||||
|
- [ ] Skills nach Kategorie gruppiert
|
||||||
|
- [ ] Zauber mit mehrzeiligen Feldern (AP/Prozess)
|
||||||
|
- [ ] Equipment nach Container sortiert
|
||||||
|
- [ ] Character-Icon als base64 Data-URI
|
||||||
|
|
||||||
|
- [ ] **Chromedp Integration:**
|
||||||
|
- [ ] Einfaches HTML → PDF erfolgreich
|
||||||
|
- [ ] A4-Landscape korrekt (297mm x 210mm)
|
||||||
|
- [ ] Print-CSS-Regeln angewendet (@page)
|
||||||
|
- [ ] Timeout-Handling funktioniert
|
||||||
|
|
||||||
|
### Integration-Tests
|
||||||
|
- [ ] **End-to-End PDF-Generierung:**
|
||||||
|
- [ ] Charakter mit minimalen Daten (leere Listen)
|
||||||
|
- [ ] Charakter mit wenigen Skills (< 64) → 1 Seite
|
||||||
|
- [ ] Charakter mit vielen Skills (> 64) → Multi-Page mit Continuation
|
||||||
|
- [ ] Charakter mit allen Listen gefüllt → 4+ Seiten
|
||||||
|
- [ ] Character mit DB-Icon → Icon korrekt im PDF
|
||||||
|
- [ ] Character ohne Icon → Platzhalter oder leer
|
||||||
|
|
||||||
|
### API-Tests
|
||||||
|
- [ ] **Endpoint-Funktionalität:**
|
||||||
|
- [ ] `GET /api/characters/:id/pdf` → 200 + PDF
|
||||||
|
- [ ] Character nicht gefunden → 404
|
||||||
|
- [ ] Nicht authentifiziert → 401
|
||||||
|
- [ ] Keine Berechtigung (fremder Character) → 403
|
||||||
|
- [ ] Timeout/Chromedp-Fehler → 500 mit Log
|
||||||
|
- [ ] Content-Type `application/pdf` korrekt
|
||||||
|
- [ ] Content-Disposition mit Filename korrekt
|
||||||
|
- [ ] Filename sanitized (keine Sonderzeichen-Probleme)
|
||||||
|
|
||||||
|
### Frontend-Tests
|
||||||
|
- [ ] **Download-Funktion:**
|
||||||
|
- [ ] Button klickbar
|
||||||
|
- [ ] Loading-State während Generierung
|
||||||
|
- [ ] Download startet automatisch
|
||||||
|
- [ ] Filename korrekt (z.B. `Gandalf_charakterbogen.pdf`)
|
||||||
|
- [ ] Error-Toast bei 404/500
|
||||||
|
|
||||||
|
### PDF-Qualitäts-Tests
|
||||||
|
- [ ] **Rendering-Qualität:**
|
||||||
|
- [ ] PDF öffnet in Adobe Reader
|
||||||
|
- [ ] PDF öffnet in Chrome/Firefox PDF-Viewer
|
||||||
|
- [ ] PDF öffnet in Evince (Linux)
|
||||||
|
- [ ] Layout identisch zu HTML-Preview in Browser
|
||||||
|
- [ ] Umlaute korrekt (ä, ö, ü, ß)
|
||||||
|
- [ ] Tabellen-Borders korrekt
|
||||||
|
- [ ] Schriftgrößen lesbar
|
||||||
|
- [ ] Keine abgeschnittenen Inhalte
|
||||||
|
- [ ] Page-Breaks an richtigen Stellen
|
||||||
|
|
||||||
|
### Performance-Tests
|
||||||
|
- [ ] **Generierungs-Dauer:**
|
||||||
|
- [ ] 1-seitiger Character: < 2 Sekunden
|
||||||
|
- [ ] 4-seitiger Character: < 5 Sekunden
|
||||||
|
- [ ] 10+ Seiten: < 10 Sekunden
|
||||||
|
- [ ] Memory-Leak-Test (100 PDFs generieren)
|
||||||
|
|
||||||
|
### Deployment-Tests
|
||||||
|
- [ ] **Docker-Integration:**
|
||||||
|
- [ ] Chromium in Container verfügbar
|
||||||
|
- [ ] Fonts installiert (dejavu)
|
||||||
|
- [ ] Environment-Variables gesetzt
|
||||||
|
- [ ] PDF-Generierung in Container funktioniert
|
||||||
|
- [ ] Keine Permission-Probleme mit Chrome-Binary
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Dateien zum Erstellen/Ändern
|
## Dateien zum Erstellen/Ändern (Chromedp Approach)
|
||||||
|
|
||||||
- `backend/doc/Characterbogen_Aufteilung.md` (Layout-Referenz)
|
### Zu erstellende Dateien:
|
||||||
- `backend/doc/template_a4_quer.yaml` (konkretes Template)
|
|
||||||
- `backend/pdf/*` (PDF-Wrapper)
|
**Backend:**
|
||||||
- `backend/pdfrender/*` oder ähnliches (Section-Renderer + Pagination)
|
- `backend/pdfrender/chromedp.go` (Chromedp-Wrapper, PDF-Generierung)
|
||||||
- API-Handler im Backend (z.B. `backend/api/character_pdf.go`)
|
- `backend/pdfrender/templates.go` (Template-Loader, Renderer)
|
||||||
- Frontend-Komponenten für Download/Template-Auswahl
|
- `backend/pdfrender/pagination.go` (Pagination-Logik, Config)
|
||||||
|
- `backend/pdfrender/viewmodel.go` (ViewModel-Structs, Mapping-Funktionen)
|
||||||
|
- `backend/character/pdf_handler.go` (API-Endpoint-Handler)
|
||||||
|
|
||||||
|
**Templates:**
|
||||||
|
- `backend/templates/Default_A4_Quer/page1_stats.html` (Seite 1: Stats)
|
||||||
|
- `backend/templates/Default_A4_Quer/page2_play.html` (Seite 2: Play)
|
||||||
|
- `backend/templates/Default_A4_Quer/page3_spell.html` (Seite 3: Spells)
|
||||||
|
- `backend/templates/Default_A4_Quer/page4_equip.html` (Seite 4: Equipment)
|
||||||
|
- `backend/templates/Default_A4_Quer/shared/export_format_a4_quer.css` (Shared CSS)
|
||||||
|
- `backend/templates/Default_A4_Quer/shared/images/` (Statische Bilder)
|
||||||
|
|
||||||
|
**Tests:**
|
||||||
|
- `backend/pdfrender/chromedp_test.go`
|
||||||
|
- `backend/pdfrender/pagination_test.go`
|
||||||
|
- `backend/pdfrender/viewmodel_test.go`
|
||||||
|
- `backend/character/pdf_handler_test.go`
|
||||||
|
|
||||||
|
### Zu ändernde Dateien:
|
||||||
|
|
||||||
|
**Backend:**
|
||||||
|
- `backend/character/routes.go` (Route für PDF-Endpoint hinzufügen)
|
||||||
|
- `backend/go.mod` (chromedp-Dependency)
|
||||||
|
|
||||||
|
**Frontend:**
|
||||||
|
- `frontend/src/views/CharacterDetail.vue` (oder ähnlich - Download-Button)
|
||||||
|
- `frontend/src/services/characterService.js` (API-Call für PDF)
|
||||||
|
|
||||||
|
**Docker:**
|
||||||
|
- `docker/Dockerfile.backend` (Chromium + Fonts installieren)
|
||||||
|
- `docker/docker-compose.yml` (Environment-Variables)
|
||||||
|
|
||||||
|
**Dokumentation:**
|
||||||
|
- `backend/doc/plan-pdfExportCharacterSheets.md` (dieses Dokument)
|
||||||
|
- `README.md` (PDF-Export-Feature dokumentieren)
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 374 KiB After Width: | Height: | Size: 374 KiB |
|
Before Width: | Height: | Size: 143 KiB After Width: | Height: | Size: 143 KiB |
Reference in New Issue
Block a user