package main import ( "context" "flag" "fmt" "log" "os" "os/exec" "path/filepath" "smartbar/internal/config" "smartbar/internal/pdfassets" "github.com/chromedp/cdproto/emulation" "github.com/chromedp/cdproto/page" "github.com/chromedp/chromedp" ) func findChromeExec() (string, error) { // Honor CHROME_PATH if set if v := os.Getenv("CHROME_PATH"); v != "" { return v, nil } candidates := []string{ "google-chrome-stable", "google-chrome", "chromium-browser", "chromium", "chrome", } for _, name := range candidates { if p, err := exec.LookPath(name); err == nil { return p, nil } } return "", fmt.Errorf("no Chrome/Chromium executable found; set CHROME_PATH or install chromium/google-chrome") } func main() { var ( input string output string width float64 height float64 imageDPI int jpegQuality int ) flag.StringVar(&input, "in", "", "input HTML file path (required)") flag.StringVar(&output, "out", "", "output PDF path (required)") flag.Float64Var(&width, "w", config.PageWidthIn, "page width in inches") flag.Float64Var(&height, "h", config.PageHeightIn, "page height in inches") flag.IntVar(&imageDPI, "image-dpi", 144, "downsample image assets to this PDF pixel density; 0 disables") flag.IntVar(&jpegQuality, "jpeg-quality", 75, "JPEG quality for PDF image assets") flag.Parse() if input == "" || output == "" { log.Fatal("--in and --out are required") } absInput, _ := filepath.Abs(input) inputRoot := filepath.Dir(absInput) inputRel := filepath.Base(absInput) imageOpts := pdfassets.OptionsForPage(width, height, imageDPI, jpegQuality) prepared, err := pdfassets.PrepareTree(inputRoot, imageOpts) if err != nil { log.Fatal(err) } defer func() { if err := prepared.Cleanup(); err != nil { log.Printf("warning: cleanup optimized PDF assets: %v", err) } }() if prepared.Stats.ImageFiles > 0 { fmt.Printf( "Optimized PDF images: %d/%d files, %s -> %s (max %dx%d, JPEG quality %d)\n", prepared.Stats.OptimizedImages, prepared.Stats.ImageFiles, pdfassets.FormatBytes(prepared.Stats.OriginalBytes), pdfassets.FormatBytes(prepared.Stats.OutputBytes), imageOpts.MaxWidth, imageOpts.MaxHeight, imageOpts.JPEGQuality, ) } absInput = filepath.Join(prepared.Root, inputRel) url := "file://" + absInput execPath, err := findChromeExec() if err != nil { log.Fatal(err) } opts := append(chromedp.DefaultExecAllocatorOptions[:], chromedp.ExecPath(execPath), chromedp.Flag("headless", true), chromedp.Flag("disable-gpu", true), chromedp.Flag("hide-scrollbars", true)) allocCtx, cancel := chromedp.NewExecAllocator(context.Background(), opts...) defer cancel() ctx, cancel := chromedp.NewContext(allocCtx) defer cancel() if err := chromedp.Run(ctx, chromedp.Tasks{ chromedp.Navigate(url), chromedp.ActionFunc(func(ctx context.Context) error { // Ensure media is print for layout (API style for v0.9.x) if err := emulation.SetEmulatedMedia().WithMedia("print").Do(ctx); err != nil { return err } // Print to PDF with exact dimensions and zero margins wIn := width hIn := height params := page.PrintToPDF(). WithPaperWidth(wIn). WithPaperHeight(hIn). WithMarginTop(0).WithMarginBottom(0).WithMarginLeft(0).WithMarginRight(0). WithPrintBackground(true) buf, _, err := params.Do(ctx) if err != nil { return err } if err := os.WriteFile(output, buf, 0o644); err != nil { return err } fmt.Printf("Wrote %s\n", output) return nil }), }); err != nil { log.Fatal(err) } }