package main import ( "fmt" "os" "path/filepath" "strings" ) const usage = `pdfstamp - terminal PDF stamp picker usage: pdfstamp pick INPUT.pdf [-o PLAN.json] place stamps; write plan (stdout if no -o) pdfstamp apply PLAN.json OUTPUT.pdf burn a plan into an output PDF pdfstamp INPUT.pdf OUTPUT.pdf pick then apply in one step PLAN.json may be - to read the plan from stdin, so commands compose: pdfstamp pick INPUT.pdf | pdfstamp apply - OUTPUT.pdf ` func main() { args := os.Args[1:] if len(args) == 0 { fmt.Fprint(os.Stderr, usage) os.Exit(2) } switch args[0] { case "pick": cmdPick(args[1:]) case "apply": cmdApply(args[1:]) case "-h", "--help", "help": fmt.Print(usage) default: if len(args) == 2 { cmdOneShot(args[0], args[1]) return } usageErr("unknown command %q", args[0]) } } func cmdPick(args []string) { input, planPath, err := parsePickArgs(args) if err != nil { usageErr("%v", err) } if planPath != "" { if err := refuseInPlace(input, planPath); err != nil { die("%v", err) } } d, err := openDoc(input) if err != nil { die("open %s: %v", input, err) } defer d.close() p := newPicker(d, input) p.savePath = planPath if err := p.run(); err != nil { die("%v", err) } } // parsePickArgs parses "pick INPUT.pdf [-o PLAN.json]" strictly: the plan path // is set only by -o, and unknown options or extra positionals are rejected. func parsePickArgs(args []string) (input, planPath string, err error) { for i := 0; i < len(args); i++ { arg := args[i] switch { case arg == "-o": i++ if i >= len(args) { return "", "", fmt.Errorf("pick: -o requires a file argument") } planPath = args[i] case strings.HasPrefix(arg, "-") && arg != "-": return "", "", fmt.Errorf("pick: unknown option %q", arg) case input == "": input = arg default: return "", "", fmt.Errorf("pick: unexpected argument %q (plan path goes after -o)", arg) } } if input == "" { return "", "", fmt.Errorf("pick: missing INPUT.pdf") } return input, planPath, nil } func cmdApply(args []string) { if len(args) != 2 { usageErr("apply: usage is apply PLAN.json OUTPUT.pdf") } var p *plan var err error if args[0] == "-" { // stdin plans carry absolute paths (pick emits them) or paths relative // to the current directory; use them as given. p, err = readPlan(os.Stdin, "") } else { p, err = loadPlan(args[0]) if err == nil { p.resolveRelativeTo(filepath.Dir(args[0])) } } if err != nil { die("%v", err) } if err := apply(p, args[1]); err != nil { die("%v", err) } fmt.Printf("wrote %s\n", terminalText(args[1])) } func cmdOneShot(input, output string) { // Fail before the picker starts, not at commit time after placement work. if err := refuseInPlace(input, output); err != nil { die("%v", err) } d, err := openDoc(input) if err != nil { die("open %s: %v", input, err) } defer d.close() p := newPicker(d, input) p.applyPath = output if err := p.run(); err != nil { die("%v", err) } // Printed after run() so the terminal is back on the main screen, and only // on a real apply: quitting with q writes nothing. if p.wrote { fmt.Printf("wrote %s\n", terminalText(output)) } } func die(format string, a ...any) { fmt.Fprintf(os.Stderr, "pdfstamp: %s\n", terminalText(fmt.Sprintf(format, a...))) os.Exit(1) } func usageErr(format string, a ...any) { fmt.Fprintf(os.Stderr, "pdfstamp: %s\n", terminalText(fmt.Sprintf(format, a...))) fmt.Fprint(os.Stderr, usage) os.Exit(2) } // terminalText neutralizes control and escape characters in text that comes // from file names, plan contents, or library errors, none of which are // trusted to be safe to write to a terminal. func terminalText(s string) string { return strings.Map(func(r rune) rune { if r < 0x20 || r == 0x7f || (r >= 0x80 && r <= 0x9f) { return '?' } return r }, s) }