package services import ( "context" "encoding/base64" "fmt" "os" "time" "github.com/chromedp/cdproto/page" "github.com/chromedp/chromedp" ) // RenderHTMLToPDF convierte un documento HTML (ya renderizado, con JS/CSS inline si aplica) // a PDF usando Chrome headless. Es el motor de "impresión" que comparten cotizaciones, // contratos, actas de proyecto y cuentas de cobro: la plantilla se renderiza a HTML con // text/template y este servicio la convierte al PDF final. // // Requiere Chrome/Chromium instalado en el host (ver Dockerfile). La ruta del binario se // puede forzar con la variable de entorno CHROME_EXEC_PATH; si no está definida, chromedp // intenta ubicar un Chrome instalado en el sistema. func RenderHTMLToPDF(html string) ([]byte, error) { opts := append(chromedp.DefaultExecAllocatorOptions[:], chromedp.Flag("headless", true), chromedp.Flag("no-sandbox", true), chromedp.Flag("disable-gpu", true), chromedp.Flag("disable-dev-shm-usage", true), ) if execPath := os.Getenv("CHROME_EXEC_PATH"); execPath != "" { opts = append(opts, chromedp.ExecPath(execPath)) } allocCtx, cancelAlloc := chromedp.NewExecAllocator(context.Background(), opts...) defer cancelAlloc() ctx, cancel := chromedp.NewContext(allocCtx) defer cancel() ctx, cancelTimeout := context.WithTimeout(ctx, 30*time.Second) defer cancelTimeout() dataURI := "data:text/html;base64," + base64.StdEncoding.EncodeToString([]byte(html)) var pdfBuf []byte err := chromedp.Run(ctx, chromedp.Navigate(dataURI), chromedp.WaitReady("body", chromedp.ByQuery), chromedp.ActionFunc(func(ctx context.Context) error { buf, _, err := page.PrintToPDF(). WithPrintBackground(true). WithPreferCSSPageSize(true). WithMarginTop(0.4). WithMarginBottom(0.4). WithMarginLeft(0.4). WithMarginRight(0.4). Do(ctx) if err != nil { return err } pdfBuf = buf return nil }), ) if err != nil { return nil, fmt.Errorf("error generando PDF: %w", err) } return pdfBuf, nil } // SaveGeneratedFile persiste bytes generados (ej. un PDF) en disco bajo uploads/documentos/{subdir} // y retorna la ruta relativa guardada en el modelo (DocumentoGenerado.Archivo, ProyectoDocumento.Archivo, etc). func SaveGeneratedFile(subdir, filename string, data []byte) (string, error) { dir := fmt.Sprintf("uploads/documentos/%s", subdir) if err := os.MkdirAll(dir, 0755); err != nil { return "", fmt.Errorf("no se pudo crear el directorio %s: %w", dir, err) } path := dir + "/" + filename if err := os.WriteFile(path, data, 0644); err != nil { return "", fmt.Errorf("no se pudo escribir %s: %w", path, err) } return path, nil }