package main import ( "context" "flag" "fmt" "log" "os" "os/exec" "path/filepath" "strings" "smartbar/internal/config" "github.com/chromedp/cdproto/emulation" "github.com/chromedp/cdproto/page" "github.com/chromedp/chromedp" pdfapi "github.com/pdfcpu/pdfcpu/pkg/api" ) func must(err error) { if err != nil { log.Fatal(err) } } func parseYAMLListOfStrings(src string) ([]string, error) { var out []string for _, line := range strings.Split(src, "\n") { line = strings.TrimSpace(line) if line == "" || strings.HasPrefix(line, "#") { continue } if strings.HasPrefix(line, "-") { item := strings.TrimSpace(strings.TrimPrefix(line, "-")) if item != "" { out = append(out, item) } } else { return nil, fmt.Errorf("invalid yaml line (expect '- item'): %s", line) } } return out, nil } func findChromeExec() (string, error) { 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 } } // macOS common app bundle paths macPaths := []string{ "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome", "/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary", "/Applications/Chromium.app/Contents/MacOS/Chromium", } for _, p := range macPaths { if _, err := os.Stat(p); err == nil { return p, nil } } return "", fmt.Errorf("no Chrome/Chromium executable found; set CHROME_PATH or install chromium/google-chrome") } func main() { var ( pagesDir string orderPath string outPDF string width float64 height float64 ) flag.StringVar(&pagesDir, "pages", "_dist", "path to compiled pages directory (required)") flag.StringVar(&orderPath, "order", "pages.yaml", "path to YAML file listing page order (required)") flag.StringVar(&outPDF, "out", "_dist/output.pdf", "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.Parse() if pagesDir == "" || orderPath == "" || outPDF == "" { log.Fatal("--pages, --order and --out are required") } // Load order data, err := os.ReadFile(orderPath) must(err) ordered, err := parseYAMLListOfStrings(string(data)) must(err) // Resolve Chrome chromeExec, err := findChromeExec() must(err) opts := append(chromedp.DefaultExecAllocatorOptions[:], chromedp.ExecPath(chromeExec), 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() // Temp dir for individual PDFs tmpDir, err := os.MkdirTemp("", "smartbar_pdfs_*") must(err) defer os.RemoveAll(tmpDir) var partPDFs []string for _, rel := range ordered { abs := filepath.Join(pagesDir, rel) if _, err := os.Stat(abs); err != nil { log.Printf("skip missing page: %s", rel) continue } absHTML, _ := filepath.Abs(abs) url := "file://" + absHTML outPath := filepath.Join(tmpDir, strings.TrimSuffix(filepath.Base(rel), filepath.Ext(rel)) + ".pdf") // Render one page PDF via printToPDF err := chromedp.Run(ctx, chromedp.Tasks{ chromedp.Navigate(url), chromedp.ActionFunc(func(ctx context.Context) error { if err := emulation.SetEmulatedMedia().WithMedia("print").Do(ctx); err != nil { return err } params := page.PrintToPDF(). WithPaperWidth(width). WithPaperHeight(height). WithMarginTop(0).WithMarginBottom(0).WithMarginLeft(0).WithMarginRight(0). WithPrintBackground(true) buf, _, err := params.Do(ctx) if err != nil { return err } return os.WriteFile(outPath, buf, 0o644) }), }) must(err) partPDFs = append(partPDFs, outPath) } if len(partPDFs) == 0 { log.Fatal("no pages produced; check order file") } // Merge PDFs into single output must(os.MkdirAll(filepath.Dir(outPDF), 0o755)) // pdfcpu expects []string in order and writes outPDF must(pdfapi.MergeCreateFile(partPDFs, outPDF, false, nil)) fmt.Printf("Wrote %s (%d pages)\n", outPDF, len(partPDFs)) }