155 lines
4.9 KiB
Go
155 lines
4.9 KiB
Go
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))
|
|
}
|
|
|
|
|