Automate PDF watermarking with Ghostscript
Add a diagonal CONFIDENTIAL label to each page of a PDF, including pages with different sizes
and rotations. The script below writes a stamped copy and a separate compressed candidate into a
new directory. It preserves the input and refuses to replace previous results.
Use this for a sharing copy whose appearance you can inspect. Ghostscript rewrites the PDF: digital signatures become invalid, and forms, annotations, accessibility tags, and color can change. The label is opaque light gray, so check that it does not obscure important content. A visible watermark does not encrypt the document or prevent copying.
Install and configure Ghostscript
You need Bash, Ghostscript’s gs, Poppler’s pdfinfo and pdftoppm, and a PDF viewer. On Ubuntu
or Debian, install maintained packages:
sudo apt-get update && sudo apt-get install ghostscript poppler-utils
Check these separately installed tools before saving the example files:
bash --version && gs --version && pdfinfo -v && pdftoppm -v
The commands were tested on Ubuntu 24.04 with Bash 5.2, Ghostscript 10.02.1, and Poppler 24.02.0, and on Linux with Bash 5.3, Ghostscript 10.07.1 and 10.08.0, and Poppler 26.08.0. These are tested versions, not intrinsic minimums. Use maintained packages from your distribution or the Ghostscript release page, and inspect the results after an upgrade. This walkthrough does not cover native Windows or macOS execution.
Work in a directory containing your source as input.pdf. Save both files below in that same
directory; the script looks for watermark.ps in the directory where you run it.
Create a watermark with an EndPage procedure
Save this as watermark.ps. EndPage draws the label after the page’s existing content. It returns
false for device deactivation, so that event does not emit an extra page:
%!PS-Adobe-3.0
<< /EndPage {
2 eq {
pop false
} {
pop
gsave
initgraphics
12 dict begin
/size currentpagedevice /PageSize get def
/label (CONFIDENTIAL) def
/Helvetica-Bold findfont 36 scalefont setfont
newpath 0 0 moveto label true charpath pathbbox
/top exch def /right exch def /bottom exch def /left exch def
/extent right left sub top bottom sub add 0.70710678 mul def
/side size aload pop 2 copy gt { exch } if pop def
/factor side 0.8 mul extent div def
factor 1 gt { /factor 1 def } if
size 0 get 2 div size 1 get 2 div translate
45 rotate
factor dup scale
0.75 setgray
left right add -2 div bottom top add -2 div moveto
label show
end
grestore
true
} ifelse
} >> setpagedevice
initgraphics resets the page’s graphics state before positioning the label. The glyph bounds
center it on both axes; the scale calculation leaves room for the complete rotated label on narrow
pages. Keep this PostScript file trusted: it is executable code. Do not insert unescaped user input
into the label.
Save the PDF automation script
Save this as process.sh. It loads the watermark procedure before the source PDF, then passes
the actual stamped output to the compression stage:
#!/bin/bash
set -euo pipefail
if [ "$#" -ne 2 ]; then
echo "Usage: $0 <input-pdf> <new-output-directory>" >&2
exit 1
fi
INPUT_FILE=$1
OUTPUT_DIR=$2
WATERMARK_FILE=$PWD/watermark.ps
STAMPED_FILE=stamped.pdf
COMPRESSED_FILE=compressed.pdf
[[ $INPUT_FILE = /* ]] || INPUT_FILE=$PWD/$INPUT_FILE
[[ $OUTPUT_DIR = /* ]] || OUTPUT_DIR=$PWD/$OUTPUT_DIR
for tool in gs pdfinfo; do
if ! command -v "$tool" >/dev/null; then
echo "Error: Install $tool before running this script" >&2
exit 1
fi
done
if [ ! -f "$INPUT_FILE" ]; then
echo "Error: Input PDF not found" >&2
exit 1
fi
if [ ! -f "$WATERMARK_FILE" ]; then
echo "Error: watermark.ps not found in the working directory" >&2
exit 1
fi
if ! pdfinfo "$INPUT_FILE" >/dev/null; then
echo "Error: Input is not a readable PDF" >&2
exit 1
fi
if ! mkdir -m 700 -- "$OUTPUT_DIR"; then
echo "Error: Use a new output directory" >&2
exit 1
fi
cleanup() {
local status=$?
if [ "$status" -ne 0 ]; then
rm -f -- "$OUTPUT_DIR/$STAMPED_FILE" "$OUTPUT_DIR/$COMPRESSED_FILE"
rmdir -- "$OUTPUT_DIR"
fi
}
trap cleanup EXIT
cd -P -- "$OUTPUT_DIR"
OUTPUT_DIR=$PWD
process_pdf() {
local output=$1
local settings=$2
shift 2
if ! GS_OPTIONS= gs -dSAFER -dBATCH -dNOPAUSE -sDEVICE=pdfwrite \
-dPDFSETTINGS="$settings" -dCompatibilityLevel=1.7 \
-dAutoRotatePages=/None -dUseCropBox -dPDFSTOPONERROR \
-sOutputFile="$output" -f "$@"; then
echo "Error: PDF processing failed" >&2
exit 1
fi
}
echo "Adding watermark…"
process_pdf "$STAMPED_FILE" /prepress "$WATERMARK_FILE" "$INPUT_FILE"
echo "Creating compression candidate…"
process_pdf "$COMPRESSED_FILE" /ebook "$OUTPUT_DIR/$STAMPED_FILE"
echo "Processing complete: $OUTPUT_DIR/$COMPRESSED_FILE"
-dUseCropBox uses the PDF’s visible cropped region as the output page, rather than centering the
label on a larger, hidden canvas. This preserves the displayed size and orientation, while
normalizing page boxes and potentially rotation metadata. Use a different workflow if you need the
larger original page canvas. See Ghostscript’s
CropBox option.
-dAutoRotatePages=/None follows each preset to disable its text-based orientation heuristic.
In particular, /ebook otherwise selects /All, which can rotate pages when their dominant text
runs sideways. The preset table
and orientation controls
document that behavior. Clearing GS_OPTIONS for each invocation prevents inherited switches,
such as fixed paper size or page selection, from changing this example’s output.
Apply watermark securely
Run the saved script in Bash:
bash process.sh input.pdf new-results
A successful run creates new-results/stamped.pdf and new-results/compressed.pdf. The first is
the stamped copy; the second is a compression candidate containing the same label. The output
directory must be new. To repeat the operation, choose another directory name and keep earlier
results for comparison.
If a conversion command fails, the script removes its two output files and its newly created directory. It leaves the source and any preexisting result directory untouched. Fix the reported problem, then rerun the same command. Missing tools, missing files, and an unreadable PDF are rejected before creating the directory.
pdfinfo checks that Poppler can open the input, and -dPDFSTOPONERROR stops Ghostscript on PDF
errors instead of its usual recovery. Neither is a complete PDF validator; successful conversion
does not prove that a damaged source was reconstructed correctly. Read warnings and inspect the
output. Keep Ghostscript patched and -dSAFER enabled. Its
file access controls
do not replace an operating-system sandbox for untrusted documents.
Optimize PDF compression
Use stamped.pdf if you do not need the compression candidate. /ebook sets a 150 dpi color and
grayscale image downsampling target, subject to a resolution threshold; it also changes other
settings. /prepress, used for the first stage, is not a guarantee of unchanged quality. Neither
preset guarantees a smaller file.
Compare actual byte counts:
wc -c input.pdf new-results/stamped.pdf new-results/compressed.pdf
Render the source and both outputs with Poppler, using their visible CropBox regions. Run the whole block, including the parentheses, so a failed command stops this block without changing your shell:
(
set -eu
mkdir new-results/previews
pdftoppm -cropbox -r 150 -png input.pdf new-results/previews/original
pdftoppm -cropbox -r 150 -png new-results/stamped.pdf new-results/previews/stamped
pdftoppm -cropbox -r 150 -png new-results/compressed.pdf new-results/previews/compressed
)
Open corresponding PNGs side by side. Check every page’s orientation, dimensions, source text and
images, and one complete centered diagonal label. Also open the PDFs in your viewer at the intended
zoom or print size. The preview block refuses an existing previews directory; keep it or rename
it before making another set.
Choose the compressed candidate only if its size and appearance suit your document. For more on comparing presets and checking PDF features, see PDF optimization with Ghostscript CLI.
