package handlers import ( "fmt" "log" "net/http" "strings" "time" cvmodel "github.com/juanatsap/cv-site/internal/models/cv" "github.com/juanatsap/cv-site/internal/pdf" ) // ============================================================================== // PDF EXPORT HANDLER // Handles PDF generation with customizable options (length, icons, version, theme) // ============================================================================== // ExportPDF handles PDF export requests using chromedp func (h *CVHandler) ExportPDF(w http.ResponseWriter, r *http.Request) { // Parse and validate request parameters req, err := ParsePDFExportRequest(r) if err != nil { HandleError(w, r, BadRequestError(err.Error())) return } log.Printf("PDF export requested: lang=%s, length=%s, icons=%s, version=%s", req.Lang, req.Length, req.Icons, req.Version) // Load CV data to get name for filename cv, err := cvmodel.LoadCV(req.Lang) if err != nil { HandleError(w, r, DataLoadError(err, "CV")) return } // Prepare cookies to set preferences cookies := map[string]string{ "cv-length": req.Length, "cv-icons": req.Icons, "cv-language": req.Lang, } // Set theme cookie based on version parameter if req.Version == "clean" { cookies["cv-theme"] = "clean" } else { cookies["cv-theme"] = "default" } // CRITICAL: ALWAYS force light mode for PDF generation (print-friendly) // This ensures PDFs are NEVER generated in dark mode, regardless of user's preference cookies["color-theme"] = "light" // Construct URL for PDF generation (navigate to home page) targetURL := fmt.Sprintf("http://%s/?lang=%s", h.serverAddr, req.Lang) // Determine render mode based on version parameter // Clean version: use @media print CSS (print-friendly, no sidebars) // Extended version: use @media screen CSS (full layout with sidebars) var renderMode pdf.RenderMode if req.Version == "clean" { renderMode = pdf.RenderModePrint } else { renderMode = pdf.RenderModeScreen } // Generate PDF with cookies and appropriate render mode ctx := r.Context() pdfData, err := h.pdfGenerator.GenerateFromURLWithOptions(ctx, targetURL, cookies, renderMode) if err != nil { log.Printf("PDF generation failed: %v", err) HandleError(w, r, InternalError(err)) return } // Generate filename based on parameters // Format: cv-{length}[-{version}]-{initials}-{year}-{lang}.pdf // Note: {version} is OMITTED when it's "clean" // Examples: // - cv-short-jamr-2025-es.pdf (clean version, no skills) // - cv-short-with_skills-jamr-2025-es.pdf (with skills sidebar) // - cv-long-jamr-2025-en.pdf (clean version, no skills) // - cv-long-with_skills-jamr-2025-en.pdf (with skills sidebar) // Generate initials from name nameParts := strings.Fields(cv.Personal.Name) initials := "" for _, part := range nameParts { if len(part) > 0 { // Take first letter of each name part initials += string([]rune(part)[0]) } } initials = strings.ToLower(initials) // Get current year currentYear := time.Now().Year() // Build filename: cv-{length}[-{version}]-{initials}-{year}-{lang}.pdf // Omit version if it's "clean" // Replace underscores with hyphens in version for filename (with_skills → with-skills) var filename string if req.Version == "clean" { filename = fmt.Sprintf("cv-%s-%s-%d-%s.pdf", req.Length, initials, currentYear, req.Lang) } else { versionForFilename := strings.ReplaceAll(req.Version, "_", "-") filename = fmt.Sprintf("cv-%s-%s-%s-%d-%s.pdf", req.Length, versionForFilename, initials, currentYear, req.Lang) } // Set response 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(pdfData))) // Write PDF data if _, err := w.Write(pdfData); err != nil { log.Printf("Error writing PDF response: %v", err) return } log.Printf("PDF generated successfully: %s (%d bytes)", filename, len(pdfData)) }