Files
cv-site/_go-learning/diagrams/08-pdf-generation.md
T
juanatsap faf3a2ca45 docs: Add comprehensive system architecture diagrams
Created detailed ASCII diagrams documenting the entire system architecture:

1. System Architecture (_go-learning/diagrams/01-system-architecture.md)
   - Overall system architecture with client/server/storage layers
   - Layered architecture (Presentation → Application → Business → Data)
   - Component interaction and HTTP request flow
   - Data flow from app start through per-request lifecycle
   - Package dependencies and file organization
2025-11-20 20:17:29 +00:00

23 KiB

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