diff --git a/cmd/pdfbook/main.go b/cmd/pdfbook/main.go new file mode 100644 index 0000000..f7d07e8 --- /dev/null +++ b/cmd/pdfbook/main.go @@ -0,0 +1,154 @@ +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)) +} + +