210 lines
7.8 KiB
Markdown
210 lines
7.8 KiB
Markdown
|
|
# PDF Export Implementation Summary
|
||
|
|
|
||
|
|
## Overview
|
||
|
|
Successfully implemented a complete PDF export system for character sheets following TDD and KISS principles.
|
||
|
|
|
||
|
|
## Components Implemented
|
||
|
|
|
||
|
|
### 1. View Model (viewmodel.go)
|
||
|
|
- `CharacterSheetViewModel`: Main data structure for character sheet rendering
|
||
|
|
- `CharacterInfo`: Basic character information (name, player, type, grade, etc.)
|
||
|
|
- `AttributeValues`: Character attributes (St, Gs, Gw, Ko, In, Zt, Au, PA, Wk, B)
|
||
|
|
- `DerivedValueSet`: Calculated values (LP, AP, bonuses, resistances)
|
||
|
|
- `SkillViewModel`, `WeaponViewModel`, `SpellViewModel`: Skill representations
|
||
|
|
- `EquipmentViewModel`: Equipment and container data
|
||
|
|
- `PageMeta`, `PageData`: Page metadata for rendering
|
||
|
|
|
||
|
|
### 2. Mapper (mapper.go)
|
||
|
|
- `MapCharacterToViewModel()`: Main conversion function
|
||
|
|
- Converts `models.Char` to `CharacterSheetViewModel`
|
||
|
|
- Maps attributes, derived values, skills, weapons, spells, equipment
|
||
|
|
- **All 8 mapper tests passing**
|
||
|
|
|
||
|
|
### 3. Template Metadata (template_metadata.go, template_parser.go)
|
||
|
|
- `BlockMetadata`: Defines list capacities (MAX) and filters (FILTER)
|
||
|
|
- `ParseTemplateMetadata()`: Extracts metadata from HTML comments
|
||
|
|
- Format: `<!-- BLOCK: name, TYPE: type, MAX: 12, FILTER: learned -->`
|
||
|
|
- Self-documenting templates store their own capacity constraints
|
||
|
|
- **All 4 parser tests passing**
|
||
|
|
|
||
|
|
### 4. Template Loader (templates.go)
|
||
|
|
- `TemplateLoader`: Manages HTML template loading and rendering
|
||
|
|
- `LoadTemplates()`: Loads all .html files from template directory
|
||
|
|
- `RenderTemplate()`: Renders templates with view model data
|
||
|
|
- Custom template functions: `iterate` for fixed-size loops
|
||
|
|
- **All 5 template tests passing**
|
||
|
|
|
||
|
|
### 5. PDF Renderer (chromedp.go)
|
||
|
|
- `PDFRenderer`: Converts HTML to PDF using chromedp
|
||
|
|
- `RenderHTMLToPDF()`: Browser-based HTML to PDF conversion
|
||
|
|
- A4 landscape format (11.69" x 8.27")
|
||
|
|
- Includes background colors and images
|
||
|
|
- `ImageToBase64DataURI()`: Helper for image embedding
|
||
|
|
- **All 5 chromedp tests passing**
|
||
|
|
|
||
|
|
### 6. Pagination System (pagination.go)
|
||
|
|
- `Paginator`: Core pagination engine with template awareness
|
||
|
|
- `PageDistribution`: Represents data distribution for a single page
|
||
|
|
- `PaginateSkills()`: Splits skills across columns and pages (64 per page)
|
||
|
|
- `PaginateSpells()`: Handles spell pagination (24 per page)
|
||
|
|
- `PaginateWeapons()`: Distributes weapons (30 per page)
|
||
|
|
- `PaginateEquipment()`: Manages equipment pagination
|
||
|
|
- `CalculatePagesNeeded()`: Pre-calculates required pages
|
||
|
|
- **All 13 pagination tests passing**
|
||
|
|
|
||
|
|
### 7. Integration Tests (integration_test.go)
|
||
|
|
- `TestIntegration_FullPDFGeneration`: End-to-end workflow test
|
||
|
|
- Character → ViewModel → Template → HTML → PDF
|
||
|
|
- Successfully generates ~31KB PDF
|
||
|
|
- `TestIntegration_TemplateMetadata`: Verifies all templates have metadata
|
||
|
|
- `TestIntegration_PaginationWithPDF`: Tests 100 skills across 2 pages with PDF generation
|
||
|
|
- Page 1: 64 skills, ~45KB PDF
|
||
|
|
- Page 2: 36 skills
|
||
|
|
- `TestIntegration_MultiPageSpellList`: Tests 30 spells across 2 pages
|
||
|
|
- Page 1: 24 spells (12+12 columns)
|
||
|
|
- Page 2: 6 spells
|
||
|
|
- **All 4 integration tests passing**
|
||
|
|
|
||
|
|
## Templates Converted
|
||
|
|
|
||
|
|
All 4 HTML templates converted to Go template syntax:
|
||
|
|
|
||
|
|
1. **page1_stats.html**: Character stats, attributes, skills, history
|
||
|
|
- Metadata: `skills_column1 MAX:32`, `skills_column2 MAX:32`
|
||
|
|
|
||
|
|
2. **page2_play.html**: Adventure sheet, combat stats, weapons
|
||
|
|
- Metadata: `skills_learned MAX:24 FILTER:learned`, `skills_unlearned MAX:15 FILTER:unlearned`, `weapons_main MAX:30`
|
||
|
|
|
||
|
|
3. **page3_spell.html**: Spell lists and magic items
|
||
|
|
- Metadata: `spells_left MAX:12`, `spells_right MAX:10`, `magic_items MAX:5`
|
||
|
|
- Different capacities for left vs right columns
|
||
|
|
|
||
|
|
4. **page4_equip.html**: Equipment, containers, currency
|
||
|
|
- Metadata: `equipment_worn MAX:10 FILTER:worn`
|
||
|
|
|
||
|
|
## Test Results
|
||
|
|
|
||
|
|
**Total: 39/39 tests passing** ✓
|
||
|
|
|
||
|
|
- Mapper: 8/8 ✓
|
||
|
|
- Parser: 4/4 ✓
|
||
|
|
- Templates: 5/5 ✓
|
||
|
|
- Chromedp: 5/5 ✓
|
||
|
|
- Pagination: 13/13 ✓
|
||
|
|
- Integration: 4/4 ✓
|
||
|
|
|
||
|
|
## Dependencies Added
|
||
|
|
|
||
|
|
- `github.com/chromedp/chromedp v0.14.2`
|
||
|
|
- `github.com/chromedp/cdproto v0.0.0-20250803210736-d308e07a266d`
|
||
|
|
|
||
|
|
## Next Steps (Not Yet Implemented)
|
||
|
|
|
||
|
|
1. **API Endpoint**: Create HTTP endpoint to trigger PDF generation
|
||
|
|
2. **Image Loading**: Load character icons from filesystem/database
|
||
|
|
3. **PDF Merging**: Combine multiple pages into single PDF document
|
||
|
|
4. **Error Handling**: Add comprehensive error handling and logging
|
||
|
|
5. **Caching**: Consider template caching for performance
|
||
|
|
6. **Frontend Integration**: Connect to Vue.js frontend
|
||
|
|
7. **Download Handler**: Implement PDF download endpoint with proper headers
|
||
|
|
|
||
|
|
## Usage Example
|
||
|
|
|
||
|
|
### Basic Single Page
|
||
|
|
|
||
|
|
```go
|
||
|
|
// 1. Map character to view model
|
||
|
|
viewModel, err := pdfrender.MapCharacterToViewModel(char)
|
||
|
|
|
||
|
|
// 2. Load templates
|
||
|
|
loader := pdfrender.NewTemplateLoader("templates/Default_A4_Quer")
|
||
|
|
loader.LoadTemplates()
|
||
|
|
|
||
|
|
// 3. Render template to HTML
|
||
|
|
pageData := &pdfrender.PageData{
|
||
|
|
Character: viewModel.Character,
|
||
|
|
Skills: viewModel.Skills,
|
||
|
|
// ... other data
|
||
|
|
}
|
||
|
|
html, err := loader.RenderTemplate("page1_stats.html", pageData)
|
||
|
|
|
||
|
|
// 4. Convert to PDF
|
||
|
|
renderer := pdfrender.NewPDFRenderer()
|
||
|
|
pdfBytes, err := renderer.RenderHTMLToPDF(html)
|
||
|
|
```
|
||
|
|
|
||
|
|
### With Pagination (Multiple Pages)
|
||
|
|
|
||
|
|
```go
|
||
|
|
// 1. Map character to view model
|
||
|
|
viewModel, err := pdfrender.MapCharacterToViewModel(char)
|
||
|
|
|
||
|
|
// 2. Paginate skills (100 skills -> 2 pages)
|
||
|
|
templateSet := pdfrender.DefaultA4QuerTemplateSet()
|
||
|
|
paginator := pdfrender.NewPaginator(templateSet)
|
||
|
|
pages, err := paginator.PaginateSkills(viewModel.Skills, "page1_stats.html", "")
|
||
|
|
|
||
|
|
// 3. Load templates and renderer
|
||
|
|
loader := pdfrender.NewTemplateLoader("templates/Default_A4_Quer")
|
||
|
|
loader.LoadTemplates()
|
||
|
|
renderer := pdfrender.NewPDFRenderer()
|
||
|
|
|
||
|
|
// 4. Generate PDF for each page
|
||
|
|
var pdfFiles [][]byte
|
||
|
|
for _, page := range pages {
|
||
|
|
// Extract data for this page
|
||
|
|
col1 := page.Data["skills_column1"].([]pdfrender.SkillViewModel)
|
||
|
|
col2 := page.Data["skills_column2"].([]pdfrender.SkillViewModel)
|
||
|
|
|
||
|
|
pageData := &pdfrender.PageData{
|
||
|
|
Character: viewModel.Character,
|
||
|
|
Attributes: viewModel.Attributes,
|
||
|
|
DerivedValues: viewModel.DerivedValues,
|
||
|
|
Skills: append(col1, col2...),
|
||
|
|
Meta: pdfrender.PageMeta{
|
||
|
|
Date: time.Now().Format("02.01.2006"),
|
||
|
|
PageNumber: page.PageNumber,
|
||
|
|
},
|
||
|
|
}
|
||
|
|
|
||
|
|
// Render and convert
|
||
|
|
html, _ := loader.RenderTemplate(page.TemplateName, pageData)
|
||
|
|
pdfBytes, _ := renderer.RenderHTMLToPDF(html)
|
||
|
|
pdfFiles = append(pdfFiles, pdfBytes)
|
||
|
|
}
|
||
|
|
|
||
|
|
// 5. Save or merge PDFs
|
||
|
|
for i, pdf := range pdfFiles {
|
||
|
|
os.WriteFile(fmt.Sprintf("character_page%d.pdf", i+1), pdf, 0644)
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
## Architecture Decisions
|
||
|
|
|
||
|
|
1. **TDD Approach**: All components developed test-first
|
||
|
|
2. **KISS Principle**: Simple slices instead of complex generic wrappers
|
||
|
|
3. **Self-Documenting**: Templates contain their own capacity metadata
|
||
|
|
4. **Separation of Concerns**: Clear boundaries between mapper, template, PDF rendering
|
||
|
|
5. **Type Safety**: Strong typing throughout with Go structs
|
||
|
|
6. **Browser-Based Rendering**: chromedp ensures accurate HTML/CSS rendering
|
||
|
|
|
||
|
|
## Files Created/Modified
|
||
|
|
|
||
|
|
- `backend/pdfrender/viewmodel.go` (new)
|
||
|
|
- `backend/pdfrender/mapper.go` (new)
|
||
|
|
- `backend/pdfrender/mapper_test.go` (new)
|
||
|
|
- `backend/pdfrender/pagination.go` (new)
|
||
|
|
- `backend/pdfrender/template_metadata.go` (new)
|
||
|
|
- `backend/pdfrender/template_parser.go` (new)
|
||
|
|
- `backend/pdfrender/template_parser_test.go` (new)
|
||
|
|
- `backend/pdfrender/templates.go` (new)
|
||
|
|
- `backend/pdfrender/templates_test.go` (new)
|
||
|
|
- `backend/pdfrender/chromedp.go` (new)
|
||
|
|
- `backend/pdfrender/chromedp_test.go` (new)
|
||
|
|
- `backend/pdfrender/integration_test.go` (new)
|
||
|
|
- `backend/templates/Default_A4_Quer/page1_stats.html` (modified)
|
||
|
|
- `backend/templates/Default_A4_Quer/page2_play.html` (modified)
|
||
|
|
- `backend/templates/Default_A4_Quer/page3_spell.html` (modified)
|
||
|
|
- `backend/templates/Default_A4_Quer/page4_equip.html` (modified)
|
||
|
|
- `backend/go.mod` (modified - added chromedp)
|