2025-08-10 17:17:01 -07:00
|
|
|
package main
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"context"
|
|
|
|
|
"flag"
|
|
|
|
|
"fmt"
|
|
|
|
|
"log"
|
|
|
|
|
"os"
|
|
|
|
|
"os/exec"
|
|
|
|
|
"path/filepath"
|
|
|
|
|
|
|
|
|
|
"smartbar/internal/config"
|
2026-07-04 17:46:27 -07:00
|
|
|
"smartbar/internal/pdfassets"
|
2025-08-10 17:17:01 -07:00
|
|
|
|
|
|
|
|
"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 (
|
2026-07-04 17:46:27 -07:00
|
|
|
input string
|
|
|
|
|
output string
|
|
|
|
|
width float64
|
|
|
|
|
height float64
|
|
|
|
|
imageDPI int
|
|
|
|
|
jpegQuality int
|
2025-08-10 17:17:01 -07:00
|
|
|
)
|
2025-08-10 17:36:57 -07:00
|
|
|
flag.StringVar(&input, "in", "", "input HTML file path (required)")
|
|
|
|
|
flag.StringVar(&output, "out", "", "output PDF path (required)")
|
2025-08-10 17:17:01 -07:00
|
|
|
flag.Float64Var(&width, "w", config.PageWidthIn, "page width in inches")
|
|
|
|
|
flag.Float64Var(&height, "h", config.PageHeightIn, "page height in inches")
|
2026-07-04 17:46:27 -07:00
|
|
|
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")
|
2025-08-10 17:17:01 -07:00
|
|
|
flag.Parse()
|
|
|
|
|
|
2025-08-10 17:36:57 -07:00
|
|
|
if input == "" || output == "" {
|
|
|
|
|
log.Fatal("--in and --out are required")
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-10 17:17:01 -07:00
|
|
|
absInput, _ := filepath.Abs(input)
|
2026-07-04 17:46:27 -07:00
|
|
|
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)
|
2025-08-10 17:17:01 -07:00
|
|
|
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)
|
|
|
|
|
}
|
|
|
|
|
}
|