1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
|
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, "<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)
}
|