package pdfrender import ( "bamort/config" "bamort/models" "fmt" "net/http" "os" "path/filepath" "time" "github.com/gin-gonic/gin" "github.com/pdfcpu/pdfcpu/pkg/api" ) // TemplateInfo represents information about an available export template type TemplateInfo struct { ID string `json:"id"` Name string `json:"name"` Description string `json:"description"` } // ListTemplates returns a list of available export templates func ListTemplates(c *gin.Context) { templatesDir := config.Cfg.TemplatesDir // Read template directories entries, err := os.ReadDir(templatesDir) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to read templates directory"}) return } var templates []TemplateInfo for _, entry := range entries { if entry.IsDir() { templates = append(templates, TemplateInfo{ ID: entry.Name(), Name: entry.Name(), Description: "PDF Export Template: " + entry.Name(), }) } } c.JSON(http.StatusOK, templates) } // ExportCharacterToPDF exports a character to PDF and saves it to xporttemp directory // Query params: // - template: template ID to use (default: "Default_A4_Quer") // - showUserName: whether to show user name (default: false) // // Returns JSON with filename: {"filename": "CharacterName_20231225_143045.pdf"} func ExportCharacterToPDF(c *gin.Context) { // Get character ID charID := c.Param("id") if charID == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "Character ID is required"}) return } // Load character char := &models.Char{} if err := char.FirstID(charID); err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "Character not found"}) return } // Get template parameter (default to Default_A4_Quer) templateID := c.DefaultQuery("template", "Default_A4_Quer") // Map character to view model viewModel, err := MapCharacterToViewModel(char) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to map character: " + err.Error()}) return } // Load templates templateDir := filepath.Join(config.Cfg.TemplatesDir, templateID) loader := NewTemplateLoader(templateDir) if err := loader.LoadTemplates(); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to load templates: " + err.Error()}) return } renderer := NewPDFRenderer() currentDate := time.Now().Format("02.01.2006") // Render all 4 pages with continuations var allPDFs [][]byte // Page 1: Stats page1PDFs, err := RenderPageWithContinuations(viewModel, "page1_stats.html", 1, currentDate, loader, renderer) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to render page 1: " + err.Error()}) return } allPDFs = append(allPDFs, page1PDFs...) // Page 2: Play page2PDFs, err := RenderPageWithContinuations(viewModel, "page2_play.html", 2, currentDate, loader, renderer) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to render page 2: " + err.Error()}) return } allPDFs = append(allPDFs, page2PDFs...) // Page 3: Spells page3PDFs, err := RenderPageWithContinuations(viewModel, "page3_spell.html", 3, currentDate, loader, renderer) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to render page 3: " + err.Error()}) return } allPDFs = append(allPDFs, page3PDFs...) // Page 4: Equipment page4PDFs, err := RenderPageWithContinuations(viewModel, "page4_equip.html", 4, currentDate, loader, renderer) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to render page 4: " + err.Error()}) return } allPDFs = append(allPDFs, page4PDFs...) // Merge PDFs if needed var finalPDF []byte if len(allPDFs) == 1 { finalPDF = allPDFs[0] } else { // Merge multiple PDFs tmpDir := "/tmp/bamort_pdf_export" if err := os.MkdirAll(tmpDir, 0755); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create temp directory"}) return } defer os.RemoveAll(tmpDir) // Save individual PDFs var filePaths []string for i, pdf := range allPDFs { filename := fmt.Sprintf("%s/page_%d.pdf", tmpDir, i) if err := os.WriteFile(filename, pdf, 0644); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to write temporary PDF"}) return } filePaths = append(filePaths, filename) } // Merge PDFs combinedPath := fmt.Sprintf("%s/combined.pdf", tmpDir) if err := api.MergeCreateFile(filePaths, combinedPath, false, nil); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to merge PDFs: " + err.Error()}) return } // Read combined PDF finalPDF, err = os.ReadFile(combinedPath) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to read combined PDF"}) return } } // Ensure export temp directory exists if err := EnsureExportTempDir(config.Cfg.ExportTempDir); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create export directory"}) return } // Generate filename filename := GenerateExportFilename(char.Name, time.Now()) filePath := filepath.Join(config.Cfg.ExportTempDir, filename) // Save PDF to file if err := os.WriteFile(filePath, finalPDF, 0644); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to save PDF file: " + err.Error()}) return } // Return filename c.JSON(http.StatusOK, gin.H{"filename": filename}) } // GetPDFFile serves a PDF file from the xporttemp directory func GetPDFFile(c *gin.Context) { filename := c.Param("filename") if filename == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "Filename is required"}) return } // Prevent path traversal attacks - only allow base filename if filepath.Base(filename) != filename { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid filename"}) return } // Only allow .pdf files if filepath.Ext(filename) != ".pdf" { c.JSON(http.StatusBadRequest, gin.H{"error": "Only PDF files are allowed"}) return } // Construct full path filePath := filepath.Join(config.Cfg.ExportTempDir, filename) // Check if file exists if _, err := os.Stat(filePath); os.IsNotExist(err) { c.JSON(http.StatusNotFound, gin.H{"error": "File not found"}) return } // Serve the file c.File(filePath) } // CleanupExportTemp removes PDF files older than 7 days from xporttemp directory func CleanupExportTemp(c *gin.Context) { // Clean up files older than 7 days maxAge := 7 * 24 * time.Hour count, err := CleanupOldFiles(config.Cfg.ExportTempDir, maxAge) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to cleanup files: " + err.Error()}) return } c.JSON(http.StatusOK, gin.H{ "deleted": count, "message": fmt.Sprintf("Deleted %d files older than 7 days", count), }) }