package converter import ( "context" "errors" "fmt" "os" "path/filepath" "runtime" "strings" "time" "gitea.tecamino.com/paadi/html2pdf/models" "github.com/chromedp/cdproto/page" "github.com/chromedp/chromedp" ) // html to pdf converter structure for type Converter struct { chromePath string ctx context.Context cancel context.CancelFunc progress func(progress int) } // NewConverter starts a new converter instance with a chrome headless shell executable func NewConverter(chromePath string) (*Converter, error) { var err error c := &Converter{chromePath: chromePath} chromePath, err = c.getChromePath() if err != nil { return nil, err } opts := append(chromedp.DefaultExecAllocatorOptions[:], chromedp.ExecPath(chromePath), chromedp.NoSandbox, chromedp.Headless, chromedp.DisableGPU, ) opts = append(opts, platformOptions()) var allocCtx context.Context allocCtx, c.cancel = chromedp.NewExecAllocator(context.Background(), opts...) c.ctx, c.cancel = chromedp.NewContext(allocCtx) return c, nil } func (c *Converter) SetProgressCallback(cb func(progress int)) { c.progress = cb } // Convert converts all given input files func (c *Converter) Convert(files ...models.File) error { for i, f := range files { if c.progress != nil { c.progress(i + 1) } if f.Input == "" || filepath.Ext(f.Input) != ".html" { return fmt.Errorf("no .html input file path provided: %s", f.Input) } else if f.Output == "" || filepath.Ext(f.Output) != ".pdf" { return fmt.Errorf("no .pdf output file path provided: %s", f.Output) } var htmlURL strings.Builder htmlURL.WriteString("file://") switch runtime.GOOS { case "windows": htmlURL.WriteString("/") } // Convert to absolute path absPath, err := filepath.Abs(f.Input) if err != nil { return err } htmlURL.WriteString(filepath.ToSlash(absPath)) c.ctx, c.cancel = context.WithTimeout(c.ctx, 60*time.Second) var pdfData []byte err = chromedp.Run(c.ctx, chromedp.Navigate(htmlURL.String()), chromedp.WaitReady("body", chromedp.ByQuery), chromedp.ActionFunc(func(ctx context.Context) error { buf, _, err := page.PrintToPDF(). WithPrintBackground(true). WithPaperWidth(8.27). WithPaperHeight(11.69). Do(ctx) if err != nil { return err } pdfData = buf return nil }), ) if err != nil { c.cancel() return err } // Save PDF to file if err := os.WriteFile(f.Output, pdfData, 0644); err != nil { c.cancel() return err } } c.cancel() return nil } // getChromePath checks for system Chrome, else falls back to bundled headless shell func (c *Converter) getChromePath() (string, error) { chromeExec := "chrome-headless-shell" if runtime.GOOS == "windows" { chromeExec += ".exe" } path := filepath.Join(c.chromePath, chromeExec) if _, err := os.Stat(path); err == nil { return path, nil } // Candidate paths for system Chrome candidates := []string{} // Fallback: switch runtime.GOOS { case "windows": candidates = []string{ `C:\Program Files\Google\Chrome\Application\chrome.exe`, `C:\Program Files (x86)\Google\Chrome\Application\chrome.exe`, `C:\Program Files\Chromium\Application\chrome.exe`, } case "darwin": candidates = []string{ "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome", "/Applications/Chromium.app/Contents/MacOS/Chromium", } default: // Linux candidates = []string{ "/usr/bin/google-chrome", "/usr/bin/chromium-browser", "/usr/bin/chromium", } } // Check system paths first for _, path := range candidates { if _, err := os.Stat(path); err == nil { return path, nil } } return "", errors.New("chrome path not found") }