faf3a2ca45
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
23 KiB
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
Related Diagrams
- Request Flow - HTTP request lifecycle
- Handler Organization - Handler structure
- Error Handling Flow - Error handling
- Template Rendering - Template system