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
|
pdfstamp
========
pdfstamp is a terminal-only PDF stamp-region picker. It renders a coarse,
content-aware page preview directly in the terminal and lets you place text and
image stamps with the keyboard, save the layout as a JSON plan, and burn the
stamps into an output PDF.
It does coarse placement, not pixel-perfect editing. Zoom in for finer control
over stamps, initials, signatures, dates, and labels.
Build
-----
pdfstamp uses MuPDF (via go-fitz) for rendering, which requires cgo. go-fitz
bundles musl-built MuPDF static libraries (no system PDF library is needed),
and the build links fully static: the one binary has no libc dependency and
runs on both glibc and musl machines.
On Alpine:
apk add build-base go
make build
On Debian, install the musl toolchain and select it explicitly:
apt install build-essential musl-tools
make build CC=musl-gcc
There is no native Windows build; on Windows, run the same static binary
under WSL.
Usage
-----
pdfstamp pick INPUT.pdf [-o PLAN.json] place stamps; write the plan
pdfstamp apply PLAN.json OUTPUT.pdf burn a plan into an output PDF
pdfstamp INPUT.pdf OUTPUT.pdf pick then apply in one step
With no -o, pick writes the plan to stdout. apply reads the plan from stdin when
the path is -, so the two steps compose as a pipe:
pdfstamp pick INPUT.pdf | pdfstamp apply - OUTPUT.pdf
The picker draws to /dev/tty, so stdout carries only the plan. apply reads the
source PDF path from the plan's "input" field. A sample document ships with the
repo; try:
pdfstamp pick example.pdf -o plan.json
pdfstamp apply plan.json stamped.pdf
Controls
--------
The picker is modal on selection: with no stamp selected, h/j/k/l move the
cursor; with a stamp selected (tab), they move the stamp.
n / p next / previous page
g go to page
h j k l move cursor (or selected stamp)
arrows move (same as h j k l)
H J K L resize selected stamp
space start / finish a rectangle, then add a stamp in it
a add a stamp at the cursor (or edit the selected one)
d delete the selected stamp
P set the selected stamp's pages (e.g. 1,3-5 or all)
o set the selected stamp's opacity (0-1)
tab cycle stamp selection (and back to cursor mode)
+ / - zoom in / out
enter save the plan (pick), or apply and quit (one-step)
? toggle help
q quit
A truecolor terminal (COLORTERM=truecolor) gets a colored half-block preview;
otherwise a grayscale ASCII preview is used. tmux and screen usually strip
COLORTERM; set it inside the multiplexer to keep the colored preview.
Plan format
-----------
The plan is plain JSON. Coordinates are PDF points with a bottom-left origin.
Pages are 1-based. Text uses Helvetica, sized to fit the rectangle (no per-stamp
font/size/color). Only printable ASCII is supported, as no font is embedded;
plans with other characters are rejected.
Images (PNG or JPEG) are scaled to fit the rectangle while preserving aspect
ratio, anchored at the bottom-left corner.
pick writes absolute paths so a saved plan applies from any directory. In a
hand-written plan, relative "input" and "src" paths are resolved relative to the
plan file's directory (or the working directory when the plan is read from
stdin).
{
"version": 2,
"input": "input.pdf",
"stamps": [
{ "id": "stamp-1", "type": "text", "content": "APPROVED",
"pages": [1], "rect": { "x": 420, "y": 610, "w": 120, "h": 42 },
"opacity": 0.8 },
{ "id": "stamp-2", "type": "image", "src": "sig.png",
"pages": [1, 3], "rect": { "x": 100, "y": 90, "w": 80, "h": 40 },
"opacity": 1.0 }
]
}
How it works
------------
pick rasterizes each page with MuPDF and draws it as Unicode half-blocks, one
character per two vertical pixels, mapping terminal cells to PDF points.
apply builds an in-memory overlay PDF with one page per source page, drawing
each stamp at its exact point coordinates (text via a base-14 Helvetica content
stream, images as RGB XObjects with a soft-mask for transparency). pdfcpu then
overlays that PDF onto the source 1:1 (multi-stamp, centered, scale 1, no
rotation) and validates the result. Because the overlay pages share the source
MediaBoxes, the overlay is an identity transform. On pages with /Rotate, the
overlay uses the viewed page size and pdfcpu counter-rotates it, so stamps land
where the picker showed them.
Debugging
---------
The overlay content streams are uncompressed and human-readable; dump the
overlay or output with a viewer or `strings` to inspect the drawing operators
(`cm`, `Tj`, `Do`). Run the integration test, which applies a known plan and
asserts that stamps land in the expected pixel regions:
make test
On Debian, use the same musl compiler override as the build:
make test CC=musl-gcc
If stamps appear rotated or offset, check the watermark fields set in apply.go:
pdfcpu defaults to a diagonal placement, so Rotation 0 and NoDiagonal must both
be set to get a straight 1:1 overlay.
Exit status
-----------
0 on success, 1 on runtime errors, 2 on usage errors. The picker exits 130 on
SIGINT and 143 on SIGTERM, after restoring the terminal.
Limitations
-----------
apply refuses, rather than silently misplacing stamps, on pages it cannot map
1:1: pages whose MediaBox/CropBox origin is not at (0,0). Text stamps are
printable ASCII only (no embedded font). Each stamp image is capped at 50
megapixels, with a 100-megapixel total across distinct images.
License
-------
pdfstamp links MuPDF (AGPL-3.0). Distributing the binary therefore puts it under
the AGPL-3.0 unless you hold a commercial MuPDF license. pdfcpu is Apache-2.0.
|