# 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}} {{else}} {{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