530 lines
23 KiB
Markdown
530 lines
23 KiB
Markdown
|
|
# PDF Generation Diagram
|
||
|
|
|
||
|
|
## PDF Export Architecture
|
||
|
|
|
||
|
|
```
|
||
|
|
┌──────────────────────────────────────────────────────────────┐
|
||
|
|
│ PDF Export Architecture │
|
||
|
|
└──────────────────────────────────────────────────────────────┘
|
||
|
|
|
||
|
|
Client (Browser)
|
||
|
|
│
|
||
|
|
├─→ User clicks "Export PDF"
|
||
|
|
│
|
||
|
|
▼
|
||
|
|
┌─────────────────────────┐
|
||
|
|
│ Modal with options │
|
||
|
|
│ ├─ Language (en/es) │
|
||
|
|
│ ├─ Length (short/long) │
|
||
|
|
│ ├─ Icons (show/hide) │
|
||
|
|
│ └─ Version (with/clean)│
|
||
|
|
└─────────────────────────┘
|
||
|
|
│
|
||
|
|
▼
|
||
|
|
POST /export/pdf
|
||
|
|
│
|
||
|
|
▼
|
||
|
|
┌─────────────────────────┐
|
||
|
|
│ Route Middleware │
|
||
|
|
│ ├─ OriginChecker │
|
||
|
|
│ └─ RateLimiter │
|
||
|
|
└─────────────────────────┘
|
||
|
|
│
|
||
|
|
▼
|
||
|
|
┌─────────────────────────┐
|
||
|
|
│ CVHandler.ExportPDF() │
|
||
|
|
│ (cv_pdf.go) │
|
||
|
|
└─────────────────────────┘
|
||
|
|
│
|
||
|
|
▼
|
||
|
|
┌─────────────────────────┐
|
||
|
|
│ PDF Generator │
|
||
|
|
│ (internal/pdf/) │
|
||
|
|
└─────────────────────────┘
|
||
|
|
│
|
||
|
|
▼
|
||
|
|
┌─────────────────────────┐
|
||
|
|
│ Chromedp │
|
||
|
|
│ (Headless Chrome) │
|
||
|
|
└─────────────────────────┘
|
||
|
|
│
|
||
|
|
▼
|
||
|
|
PDF Response
|
||
|
|
```
|
||
|
|
|
||
|
|
## PDF Generation Flow
|
||
|
|
|
||
|
|
```
|
||
|
|
┌────────────────────────────────────────────────────────────┐
|
||
|
|
│ PDF Generation Flow │
|
||
|
|
└────────────────────────────────────────────────────────────┘
|
||
|
|
|
||
|
|
1. REQUEST VALIDATION
|
||
|
|
┌─────────────────────────────────────────────────────────┐
|
||
|
|
│ Handler.ExportPDF(w, r) │
|
||
|
|
│ (internal/handlers/cv_pdf.go) │
|
||
|
|
│ │
|
||
|
|
│ // Parse JSON request │
|
||
|
|
│ var req PDFExportRequest │
|
||
|
|
│ err := json.NewDecoder(r.Body).Decode(&req) │
|
||
|
|
│ │
|
||
|
|
│ // Validate fields │
|
||
|
|
│ if req.Lang != "en" && req.Lang != "es" { │
|
||
|
|
│ return InvalidLanguageError(req.Lang) │
|
||
|
|
│ } │
|
||
|
|
│ if req.Length != "short" && req.Length != "long" { │
|
||
|
|
│ return InvalidLengthError(req.Length) │
|
||
|
|
│ } │
|
||
|
|
└─────────────────────────────────────────────────────────┘
|
||
|
|
|
||
|
|
2. HTML GENERATION
|
||
|
|
┌─────────────────────────────────────────────────────────┐
|
||
|
|
│ // Build template data │
|
||
|
|
│ data := map[string]interface{}{ │
|
||
|
|
│ "CV": cv, │
|
||
|
|
│ "UI": ui, │
|
||
|
|
│ "Preferences": &middleware.Preferences{ │
|
||
|
|
│ CVLength: req.Length, │
|
||
|
|
│ CVIcons: req.Icons, │
|
||
|
|
│ CVLanguage: req.Lang, │
|
||
|
|
│ }, │
|
||
|
|
│ "SkillsColumns": skillColumns, │
|
||
|
|
│ "IsPDF": true, // PDF-specific flag │
|
||
|
|
│ } │
|
||
|
|
│ │
|
||
|
|
│ // Render to buffer │
|
||
|
|
│ var buf bytes.Buffer │
|
||
|
|
│ err := h.tmpl.Render(&buf, "index.html", data) │
|
||
|
|
│ htmlContent := buf.String() │
|
||
|
|
└─────────────────────────────────────────────────────────┘
|
||
|
|
|
||
|
|
3. PDF OPTIONS
|
||
|
|
┌─────────────────────────────────────────────────────────┐
|
||
|
|
│ opts := pdf.Options{ │
|
||
|
|
│ PaperSize: pdf.A4, │
|
||
|
|
│ Orientation: pdf.Portrait, │
|
||
|
|
│ MarginTop: "1cm", │
|
||
|
|
│ MarginRight: "1cm", │
|
||
|
|
│ MarginBottom: "1cm", │
|
||
|
|
│ MarginLeft: "1cm", │
|
||
|
|
│ PrintBackground: true, // Include colors │
|
||
|
|
│ Scale: 1.0, │
|
||
|
|
│ Landscape: false, │
|
||
|
|
│ } │
|
||
|
|
└─────────────────────────────────────────────────────────┘
|
||
|
|
|
||
|
|
4. PDF GENERATION
|
||
|
|
┌─────────────────────────────────────────────────────────┐
|
||
|
|
│ pdfBytes, err := pdf.GeneratePDF(htmlContent, opts) │
|
||
|
|
│ if err != nil { │
|
||
|
|
│ return PDFGenerationError(err) │
|
||
|
|
│ } │
|
||
|
|
└─────────────────────────────────────────────────────────┘
|
||
|
|
|
||
|
|
5. RESPONSE
|
||
|
|
┌─────────────────────────────────────────────────────────┐
|
||
|
|
│ // Build filename │
|
||
|
|
│ filename := fmt.Sprintf("CV-%s-%s.pdf", │
|
||
|
|
│ cv.Personal.Name, req.Lang) │
|
||
|
|
│ filename = strings.ReplaceAll(filename, " ", "-") │
|
||
|
|
│ │
|
||
|
|
│ // Set headers │
|
||
|
|
│ w.Header().Set("Content-Type", "application/pdf") │
|
||
|
|
│ w.Header().Set("Content-Disposition", │
|
||
|
|
│ fmt.Sprintf("attachment; filename=%s", filename)) │
|
||
|
|
│ w.Header().Set("Content-Length", │
|
||
|
|
│ fmt.Sprintf("%d", len(pdfBytes))) │
|
||
|
|
│ │
|
||
|
|
│ // Send PDF │
|
||
|
|
│ w.WriteHeader(http.StatusOK) │
|
||
|
|
│ w.Write(pdfBytes) │
|
||
|
|
└─────────────────────────────────────────────────────────┘
|
||
|
|
```
|
||
|
|
|
||
|
|
## Chromedp PDF Generation
|
||
|
|
|
||
|
|
```
|
||
|
|
┌────────────────────────────────────────────────────────────┐
|
||
|
|
│ Chromedp PDF Generation (internal/pdf/generator.go) │
|
||
|
|
└────────────────────────────────────────────────────────────┘
|
||
|
|
|
||
|
|
func GeneratePDF(htmlContent string, opts Options) ([]byte, error) {
|
||
|
|
|
||
|
|
1. CREATE CONTEXT
|
||
|
|
┌──────────────────────────────────────────────────────┐
|
||
|
|
│ // Allocate context │
|
||
|
|
│ ctx, cancel := chromedp.NewContext( │
|
||
|
|
│ context.Background(), │
|
||
|
|
│ chromedp.WithLogf(log.Printf), │
|
||
|
|
│ ) │
|
||
|
|
│ defer cancel() │
|
||
|
|
│ │
|
||
|
|
│ // Set timeout │
|
||
|
|
│ ctx, cancel = context.WithTimeout(ctx, 30*time.Second) │
|
||
|
|
│ defer cancel() │
|
||
|
|
└──────────────────────────────────────────────────────┘
|
||
|
|
|
||
|
|
2. PREPARE HTML
|
||
|
|
┌──────────────────────────────────────────────────────┐
|
||
|
|
│ // Wrap HTML in data URL │
|
||
|
|
│ dataURL := fmt.Sprintf( │
|
||
|
|
│ "data:text/html;base64,%s", │
|
||
|
|
│ base64.StdEncoding.EncodeToString( │
|
||
|
|
│ []byte(htmlContent), │
|
||
|
|
│ ), │
|
||
|
|
│ ) │
|
||
|
|
└──────────────────────────────────────────────────────┘
|
||
|
|
|
||
|
|
3. LAUNCH CHROME
|
||
|
|
┌──────────────────────────────────────────────────────┐
|
||
|
|
│ // Run Chrome tasks │
|
||
|
|
│ var pdfBytes []byte │
|
||
|
|
│ err := chromedp.Run(ctx, │
|
||
|
|
│ // Navigate to data URL │
|
||
|
|
│ chromedp.Navigate(dataURL), │
|
||
|
|
│ │
|
||
|
|
│ // Wait for body to be ready │
|
||
|
|
│ chromedp.WaitReady("body", chromedp.ByQuery), │
|
||
|
|
│ │
|
||
|
|
│ // Wait for fonts and images │
|
||
|
|
│ chromedp.Sleep(500 * time.Millisecond), │
|
||
|
|
│ │
|
||
|
|
│ // Generate PDF │
|
||
|
|
│ chromedp.ActionFunc(func(ctx context.Context) error { │
|
||
|
|
│ buf, _, err := page.PrintToPDF(). │
|
||
|
|
│ WithPrintBackground(opts.PrintBackground). │
|
||
|
|
│ WithPaperWidth(opts.PaperWidth). │
|
||
|
|
│ WithPaperHeight(opts.PaperHeight). │
|
||
|
|
│ WithMarginTop(opts.MarginTop). │
|
||
|
|
│ WithMarginRight(opts.MarginRight). │
|
||
|
|
│ WithMarginBottom(opts.MarginBottom). │
|
||
|
|
│ WithMarginLeft(opts.MarginLeft). │
|
||
|
|
│ WithScale(opts.Scale). │
|
||
|
|
│ Do(ctx) │
|
||
|
|
│ if err != nil { │
|
||
|
|
│ return err │
|
||
|
|
│ } │
|
||
|
|
│ pdfBytes = buf │
|
||
|
|
│ return nil │
|
||
|
|
│ }), │
|
||
|
|
│ ) │
|
||
|
|
│ │
|
||
|
|
│ if err != nil { │
|
||
|
|
│ return nil, fmt.Errorf("chromedp: %w", err) │
|
||
|
|
│ } │
|
||
|
|
└──────────────────────────────────────────────────────┘
|
||
|
|
|
||
|
|
4. RETURN PDF
|
||
|
|
┌──────────────────────────────────────────────────────┐
|
||
|
|
│ return pdfBytes, nil │
|
||
|
|
└──────────────────────────────────────────────────────┘
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
## PDF-Specific Template Adjustments
|
||
|
|
|
||
|
|
```
|
||
|
|
┌────────────────────────────────────────────────────────────┐
|
||
|
|
│ PDF-Specific Template Adjustments │
|
||
|
|
└────────────────────────────────────────────────────────────┘
|
||
|
|
|
||
|
|
In templates/index.html:
|
||
|
|
|
||
|
|
{{if .IsPDF}}
|
||
|
|
<!-- PDF-specific styles -->
|
||
|
|
<style>
|
||
|
|
/* Hide interactive elements */
|
||
|
|
.toggle-button, .interactive-controls {
|
||
|
|
display: none !important;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* Optimize for print */
|
||
|
|
body {
|
||
|
|
background: white !important;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* Better page breaks */
|
||
|
|
.experience-item {
|
||
|
|
page-break-inside: avoid;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* Consistent sizing */
|
||
|
|
.cv-section {
|
||
|
|
margin-bottom: 1.5cm;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* Font optimization */
|
||
|
|
body {
|
||
|
|
font-size: 10pt;
|
||
|
|
line-height: 1.4;
|
||
|
|
}
|
||
|
|
</style>
|
||
|
|
{{else}}
|
||
|
|
<!-- Web-specific styles -->
|
||
|
|
<style>
|
||
|
|
.interactive-controls {
|
||
|
|
display: block;
|
||
|
|
}
|
||
|
|
</style>
|
||
|
|
{{end}}
|
||
|
|
```
|
||
|
|
|
||
|
|
## PDF Request/Response Example
|
||
|
|
|
||
|
|
```
|
||
|
|
┌────────────────────────────────────────────────────────────┐
|
||
|
|
│ PDF Request/Response Example │
|
||
|
|
└────────────────────────────────────────────────────────────┘
|
||
|
|
|
||
|
|
REQUEST:
|
||
|
|
POST /export/pdf HTTP/1.1
|
||
|
|
Host: localhost:8080
|
||
|
|
Content-Type: application/json
|
||
|
|
Origin: http://localhost:8080
|
||
|
|
|
||
|
|
{
|
||
|
|
"lang": "es",
|
||
|
|
"length": "long",
|
||
|
|
"icons": "show",
|
||
|
|
"version": "with_skills"
|
||
|
|
}
|
||
|
|
|
||
|
|
RESPONSE (Success):
|
||
|
|
HTTP/1.1 200 OK
|
||
|
|
Content-Type: application/pdf
|
||
|
|
Content-Disposition: attachment; filename="CV-John-Doe-es.pdf"
|
||
|
|
Content-Length: 245678
|
||
|
|
|
||
|
|
[PDF binary data]
|
||
|
|
|
||
|
|
RESPONSE (Error - Invalid Language):
|
||
|
|
HTTP/1.1 400 Bad Request
|
||
|
|
Content-Type: application/json
|
||
|
|
|
||
|
|
{
|
||
|
|
"success": false,
|
||
|
|
"error": {
|
||
|
|
"code": "INVALID_LANGUAGE",
|
||
|
|
"message": "Unsupported language: xx (use 'en' or 'es')",
|
||
|
|
"field": "lang"
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
RESPONSE (Error - Rate Limited):
|
||
|
|
HTTP/1.1 429 Too Many Requests
|
||
|
|
Content-Type: application/json
|
||
|
|
|
||
|
|
{
|
||
|
|
"success": false,
|
||
|
|
"error": {
|
||
|
|
"code": "RATE_LIMIT_EXCEEDED",
|
||
|
|
"message": "Too many PDF exports. Please wait a minute."
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
RESPONSE (Error - PDF Generation Failed):
|
||
|
|
HTTP/1.1 500 Internal Server Error
|
||
|
|
Content-Type: application/json
|
||
|
|
|
||
|
|
{
|
||
|
|
"success": false,
|
||
|
|
"error": {
|
||
|
|
"code": "PDF_GENERATION",
|
||
|
|
"message": "Failed to generate PDF. Please try again."
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
## PDF Options Structure
|
||
|
|
|
||
|
|
```
|
||
|
|
┌────────────────────────────────────────────────────────────┐
|
||
|
|
│ PDF Options (internal/pdf/options.go) │
|
||
|
|
└────────────────────────────────────────────────────────────┘
|
||
|
|
|
||
|
|
type Options struct {
|
||
|
|
// Paper settings
|
||
|
|
PaperSize PaperSize // A4, Letter, Legal
|
||
|
|
Orientation Orientation // Portrait, Landscape
|
||
|
|
PaperWidth float64 // In inches
|
||
|
|
PaperHeight float64 // In inches
|
||
|
|
|
||
|
|
// Margins
|
||
|
|
MarginTop string // "1cm", "0.5in"
|
||
|
|
MarginRight string
|
||
|
|
MarginBottom string
|
||
|
|
MarginLeft string
|
||
|
|
|
||
|
|
// Rendering
|
||
|
|
PrintBackground bool // Include background colors
|
||
|
|
Scale float64 // 0.5 to 2.0
|
||
|
|
Landscape bool // True for landscape
|
||
|
|
|
||
|
|
// Quality
|
||
|
|
PreferCSSPageSize bool
|
||
|
|
DisplayHeaderFooter bool
|
||
|
|
HeaderTemplate string
|
||
|
|
FooterTemplate string
|
||
|
|
}
|
||
|
|
|
||
|
|
Default A4 Options:
|
||
|
|
Options{
|
||
|
|
PaperSize: A4, // 8.27 x 11.69 inches
|
||
|
|
Orientation: Portrait,
|
||
|
|
MarginTop: "1cm",
|
||
|
|
MarginRight: "1cm",
|
||
|
|
MarginBottom: "1cm",
|
||
|
|
MarginLeft: "1cm",
|
||
|
|
PrintBackground: true,
|
||
|
|
Scale: 1.0,
|
||
|
|
Landscape: false,
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
## Rate Limiting
|
||
|
|
|
||
|
|
```
|
||
|
|
┌────────────────────────────────────────────────────────────┐
|
||
|
|
│ Rate Limiting for PDF Export │
|
||
|
|
└────────────────────────────────────────────────────────────┘
|
||
|
|
|
||
|
|
RateLimiter Middleware:
|
||
|
|
├─ 3 requests per minute per IP
|
||
|
|
├─ Uses token bucket algorithm
|
||
|
|
└─ Applied only to /export/pdf endpoint
|
||
|
|
|
||
|
|
Implementation:
|
||
|
|
|
||
|
|
type RateLimiter struct {
|
||
|
|
requests map[string]*bucket
|
||
|
|
mu sync.RWMutex
|
||
|
|
}
|
||
|
|
|
||
|
|
type bucket struct {
|
||
|
|
tokens int
|
||
|
|
lastReset time.Time
|
||
|
|
}
|
||
|
|
|
||
|
|
func (rl *RateLimiter) Allow(ip string) bool {
|
||
|
|
rl.mu.Lock()
|
||
|
|
defer rl.mu.Unlock()
|
||
|
|
|
||
|
|
bucket := rl.requests[ip]
|
||
|
|
if bucket == nil {
|
||
|
|
bucket = &bucket{
|
||
|
|
tokens: 3,
|
||
|
|
lastReset: time.Now(),
|
||
|
|
}
|
||
|
|
rl.requests[ip] = bucket
|
||
|
|
}
|
||
|
|
|
||
|
|
// Reset bucket every minute
|
||
|
|
if time.Since(bucket.lastReset) > time.Minute {
|
||
|
|
bucket.tokens = 3
|
||
|
|
bucket.lastReset = time.Now()
|
||
|
|
}
|
||
|
|
|
||
|
|
// Check tokens
|
||
|
|
if bucket.tokens <= 0 {
|
||
|
|
return false // Rate limited
|
||
|
|
}
|
||
|
|
|
||
|
|
bucket.tokens--
|
||
|
|
return true
|
||
|
|
}
|
||
|
|
|
||
|
|
Response when rate limited:
|
||
|
|
{
|
||
|
|
"success": false,
|
||
|
|
"error": {
|
||
|
|
"code": "RATE_LIMIT_EXCEEDED",
|
||
|
|
"message": "Too many PDF exports. Please wait a minute."
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
## PDF Performance
|
||
|
|
|
||
|
|
```
|
||
|
|
┌────────────────────────────────────────────────────────────┐
|
||
|
|
│ PDF Performance │
|
||
|
|
└────────────────────────────────────────────────────────────┘
|
||
|
|
|
||
|
|
Timing Breakdown:
|
||
|
|
|
||
|
|
┌─────────────────────────────────────────────────────────┐
|
||
|
|
│ Operation Time % │
|
||
|
|
├─────────────────────────────────────────────────────────┤
|
||
|
|
│ Request validation ~1ms 0.1% │
|
||
|
|
│ HTML generation ~50ms 5% │
|
||
|
|
│ Chrome launch ~200ms 20% │
|
||
|
|
│ Page navigation ~100ms 10% │
|
||
|
|
│ Font loading ~50ms 5% │
|
||
|
|
│ PDF rendering ~550ms 55% │
|
||
|
|
│ Response transmission ~50ms 5% │
|
||
|
|
├─────────────────────────────────────────────────────────┤
|
||
|
|
│ TOTAL ~1000ms 100% │
|
||
|
|
└─────────────────────────────────────────────────────────┘
|
||
|
|
|
||
|
|
Optimization Strategies:
|
||
|
|
1. Keep Chrome instance warm
|
||
|
|
└─→ Pre-launch Chrome on startup
|
||
|
|
Reuse context for multiple PDFs
|
||
|
|
|
||
|
|
2. Optimize HTML
|
||
|
|
└─→ Inline critical CSS
|
||
|
|
Remove unused styles
|
||
|
|
|
||
|
|
3. Font optimization
|
||
|
|
└─→ Use web-safe fonts
|
||
|
|
Preload font files
|
||
|
|
|
||
|
|
4. Cache templates
|
||
|
|
└─→ Pre-compile templates
|
||
|
|
Reuse parsed templates
|
||
|
|
|
||
|
|
5. Parallel processing
|
||
|
|
└─→ Queue PDF jobs
|
||
|
|
Process multiple concurrently
|
||
|
|
```
|
||
|
|
|
||
|
|
## Error Scenarios
|
||
|
|
|
||
|
|
```
|
||
|
|
┌────────────────────────────────────────────────────────────┐
|
||
|
|
│ PDF Error Scenarios │
|
||
|
|
└────────────────────────────────────────────────────────────┘
|
||
|
|
|
||
|
|
1. Chrome Launch Failed
|
||
|
|
Error: chromedp: failed to allocate context
|
||
|
|
Cause: Chrome not installed or crashed
|
||
|
|
Recovery: Log error, return 500, suggest retry
|
||
|
|
|
||
|
|
2. Timeout
|
||
|
|
Error: context deadline exceeded
|
||
|
|
Cause: PDF generation took > 30 seconds
|
||
|
|
Recovery: Cancel operation, return timeout error
|
||
|
|
|
||
|
|
3. Memory Limit
|
||
|
|
Error: out of memory
|
||
|
|
Cause: Too many concurrent PDF generations
|
||
|
|
Recovery: Rate limiting, queue system
|
||
|
|
|
||
|
|
4. Template Error
|
||
|
|
Error: template execution failed
|
||
|
|
Cause: Missing data or invalid template
|
||
|
|
Recovery: Fix template, ensure all data present
|
||
|
|
|
||
|
|
5. Navigation Error
|
||
|
|
Error: navigation failed
|
||
|
|
Cause: Invalid HTML or data URL too large
|
||
|
|
Recovery: Check HTML validity, reduce size
|
||
|
|
```
|
||
|
|
|
||
|
|
## Related Diagrams
|
||
|
|
|
||
|
|
- [Request Flow](./02-request-flow.md) - HTTP request lifecycle
|
||
|
|
- [Handler Organization](./04-handler-organization.md) - Handler structure
|
||
|
|
- [Error Handling Flow](./06-error-handling-flow.md) - Error handling
|
||
|
|
- [Template Rendering](./07-template-rendering.md) - Template system
|