package pdf import ( "context" "fmt" "io" "time" "github.com/chromedp/cdproto/page" "github.com/chromedp/chromedp" ) // Generator handles PDF generation using headless Chrome type Generator struct { timeout time.Duration } // NewGenerator creates a new PDF generator with the specified timeout func NewGenerator(timeout time.Duration) *Generator { if timeout == 0 { timeout = 30 * time.Second } return &Generator{ timeout: timeout, } } // GenerateFromURL generates a PDF from a given URL func (g *Generator) GenerateFromURL(ctx context.Context, url string) ([]byte, error) { // Create context with timeout ctx, cancel := context.WithTimeout(ctx, g.timeout) defer cancel() // Create chromedp context allocCtx, allocCancel := chromedp.NewContext(ctx) defer allocCancel() // Buffer to store PDF var pdfBuffer []byte // Run chromedp tasks err := chromedp.Run(allocCtx, // Navigate to URL chromedp.Navigate(url), // Wait for page to be ready chromedp.WaitReady("body"), // Small delay to ensure all content is loaded chromedp.Sleep(500*time.Millisecond), // Generate PDF with print-optimized settings chromedp.ActionFunc(func(ctx context.Context) error { var err error pdfBuffer, _, err = page.PrintToPDF(). WithPrintBackground(true). WithPreferCSSPageSize(true). WithMarginTop(0). WithMarginBottom(0). WithMarginLeft(0). WithMarginRight(0). WithPaperWidth(8.27). // A4 width in inches WithPaperHeight(11.69). // A4 height in inches Do(ctx) return err }), ) if err != nil { return nil, fmt.Errorf("chromedp execution failed: %w", err) } if len(pdfBuffer) == 0 { return nil, fmt.Errorf("generated PDF is empty") } return pdfBuffer, nil } // StreamFromURL generates a PDF and writes it to the provided writer func (g *Generator) StreamFromURL(ctx context.Context, url string, w io.Writer) error { pdfData, err := g.GenerateFromURL(ctx, url) if err != nil { return err } _, err = w.Write(pdfData) return err }