ScreenshotNeo

BlogHTML to image & PDF

How to Add Text Watermarks to PDFs in Go

Add Draft or Confidential watermarks to PDF pages in Go with pdfcpu, including styling, page selection, troubleshooting, and CLI usage.

By the ScreenshotNeo team1 October 20267 min read

Use pdfcpu to add text watermarks to PDFs in Go. Its documented api.AddTextWatermarksFile function reads an input PDF, applies a text watermark to selected pages, and writes a new PDF. Set onTop to false for content behind the existing page (a watermark) or true for content in front (a stamp).

The examples below show how to watermark every page, target odd or even pages, control font and opacity, use the command line, and avoid the common problem where a background watermark is hidden by a scanned page.

1. Install pdfcpu

Create a Go module and add pdfcpu:

mkdir pdf-watermark
cd pdf-watermark
go mod init example.com/pdf-watermark
go get github.com/pdfcpu/pdfcpu/pkg/api

Check the version and current API or CLI syntax before pinning a production build. The documentation and examples used here do not specify a verified library version.

2. Add a text watermark to every page

Passing nil for the selected-page expression applies the watermark to all pages. This complete program writes a new file and leaves the source PDF unchanged:

package main

import (
    "context"
    "log"

    "github.com/pdfcpu/pdfcpu/pkg/api"
)

func main() {
    ctx := context.Background()

    // false places the generated content behind existing page content.
    onTop := false

    // Descriptor options control the text appearance. Verify descriptors
    // against the pdfcpu version used by your build.
    descriptor := "font:Helvetica, points:48, color:.8 .8 .4, rot:45, scale:1, op:.6"

    err := api.AddTextWatermarksFile(
        ctx,
        "input.pdf",
        "watermarked.pdf",
        nil,          // nil means every page
        onTop,
        "DRAFT",
        descriptor,
        nil,          // use the default configuration
    )
    if err != nil {
        log.Fatal(err)
    }
}

Run it with:

go run .

The result is watermarked.pdf. Treat the descriptor as version-sensitive configuration and confirm accepted values with the installed pdfcpu documentation.

3. Choose watermark or stamp placement

pdfcpu uses two terms for fixed page content:

Setting Placement Use it when
onTop = false Behind existing page content You want a subtle background watermark and the page artwork has transparent or uncovered areas.
onTop = true In front of existing page content The label must remain visible over scans, images, or dense page artwork.

A full-page scan is a common edge case. The scanned bitmap can cover a background watermark completely. In that situation, use a foreground stamp and reduce opacity so the original content remains readable.

4. Control font, size, color, rotation, scale, and opacity

The descriptor string carries appearance settings. The documented examples demonstrate options such as:

Option Purpose Example
font Select the font family. font:Courier
points Set text size in points. points:48
color Set the color; the CLI examples use space-separated channel values. color:.8 .8 .4
rot Rotate the text. rot:45
scale Adjust the absolute scale. scale:1
op Set opacity. op:.6

For example, a foreground confidential stamp can use:

descriptor := "font:Courier, points:48, color:1 0 0, rot:45, scale:1, op:.6"
err := api.AddTextWatermarksFile(
    context.Background(),
    "input.pdf",
    "confidential.pdf",
    nil,
    true, // place over existing content
    "CONFIDENTIAL",
    descriptor,
    nil,
)

There is no universal best opacity or size. Compare the output against the actual page backgrounds and the purpose of the document.

5. Watermark selected pages

The page-selection argument accepts page expressions. The API example applies a foreground watermark to odd pages:

package main

import (
    "context"
    "log"

    "github.com/pdfcpu/pdfcpu/pkg/api"
)

func main() {
    descriptor := "font:Courier, points:48, color:1 0 0, rot:45, scale:1, op:.6"

    err := api.AddTextWatermarksFile(
        context.Background(),
        "input.pdf",
        "odd-pages.pdf",
        "odd",
        true,
        "CONFIDENTIAL",
        descriptor,
        nil,
    )
    if err != nil {
        log.Fatal(err)
    }
}

Use the expression supported by your pdfcpu version for the pages you need. If the API needs different text or styling on different pages, use the stream-oriented AddWatermarks function or an AddWatermarksMap variant to supply page-specific watermark configurations.

6. Use the pdfcpu command line

For deployments where an external executable is acceptable, pdfcpu documents this command:

pdfcpu watermark add 'Draft' 'points:48, scale:1, color:.8 .8 .4, op:.6' input.pdf output.pdf --mode text

To update or remove an existing watermark:

pdfcpu watermark update 'Draft' 'points:48, scale:1, color:.8 .8 .4, op:.6' input.pdf output.pdf --mode text
pdfcpu watermark remove input.pdf output.pdf

To target even pages, the documented form is:

pdfcpu watermark add 'Draft' 'points:48, scale:1, color:.8 .8 .4, op:.6' input.pdf output.pdf --mode text --pages even

Run pdfcpu watermark add -h and the help for your installed version before automating these commands because command and descriptor details can change.

7. Process readers and writers instead of file paths

AddTextWatermarksFile is convenient for file-to-file jobs. When a service already owns an io.Reader and io.Writer, use pdfcpu’s stream-oriented AddWatermarks API. The map variants are useful when each page needs a different watermark. Keep the request context connected to the job so cancellation can stop work that the API supports cancelling.

8. Verify the output in an automated pipeline

Use a separate output path or temporary file. That prevents a failed operation from destroying the original. A practical pipeline is:

  1. Check that the input exists and is readable.
  2. Write to a temporary destination in the same filesystem.
  3. Return the error without replacing the source if processing fails.
  4. Replace or upload the final file only after the call succeeds.
  5. Open the output with a PDF validator or your normal downstream reader.

For large batches, bound concurrency, pass cancellation through the context, and monitor output size and processing time. The supplied documentation does not provide performance benchmarks, so measure with your own PDFs.

9. Troubleshooting

Symptom Likely cause Fix
The watermark is invisible. It was added behind a full-page scan or opaque artwork. Set onTop to true and lower op if needed.
Only some pages contain text. A page expression selected a subset. Pass nil for all pages or verify the expression and page numbering.
The command fails with an unknown option. The installed CLI version differs from the example. Run the command’s -h help and adjust the descriptor or flags.
The source file is missing or unreadable. Wrong path or insufficient permissions. Check paths, permissions, and the process working directory before calling the API.
The result is hard to read. Font size, rotation, color, or opacity is too strong. Reduce point size or opacity, choose a contrasting color, and test on representative pages.
The program stops during a long job. The context was cancelled or the process exited. Use a request-scoped context, handle the returned error, and write results atomically.
Different pages need different labels. A single global watermark configuration is too limited. Use AddWatermarksMap or another page-specific API variant.

10. Reliability, performance, and cost considerations

  • Reliability: Preserve the original, use a temporary output, and only publish a successful result. Context cancellation lets callers stop work where supported by the API.
  • Performance: PDF size, page count, embedded images, fonts, and whether the document is scanned all affect processing time. Benchmark with production-like files; no benchmark is established by the cited documentation.
  • Memory: Prefer stream APIs for services that should avoid loading unnecessary data into application-managed buffers, and limit concurrent jobs.
  • Cost: pdfcpu is software you run yourself. Your cost comes from compute, storage, and operations rather than a per-watermark API charge. The sources do not provide a universal cost estimate.
  • Reproducibility: Pin the pdfcpu module or executable version and keep descriptor strings under source control.

11. Or skip the browser setup

If the job is to capture a web page or rendered document as an image or PDF rather than modify an existing PDF, ScreenshotNeo provides a single HTTP request. Its cleanup step accepts cookie banners and removes more than 60 known consent platforms, newsletter popups, and chat widgets before capture. Bot checks, blank pages, failed loads, timeouts, and cache hits are not billed, and the response identifies the result with X-Page-Verdict and X-Billed headers. An MCP server lets Claude, Cursor, and other MCP clients call screenshot and PDF tools.

See the ScreenshotNeo API documentation for the available options.

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);

Every feature is available on every plan. The free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000. Create a free ScreenshotNeo account.

12. FAQ

Does pdfcpu create an annotation that users can move?

No. The documented watermark and stamp operations add fixed page content. They are not described as movable stamp-comment annotations.

Can I watermark only odd pages in Go?

Yes. Pass the documented odd-page expression to AddTextWatermarksFile, and confirm the exact expression supported by your version.

Why does a watermark disappear on a scanned PDF?

A scan can be an opaque image covering the entire page. Place the text on top with onTop = true and choose an opacity that preserves readability.

Should I use the API or CLI?

Use the Go API when the operation belongs inside a Go service, needs context cancellation, streams, or page-specific logic. Use the CLI when an external executable fits your deployment and shell workflow.

Can I remove a watermark later?

The pdfcpu CLI documents watermark remove. Keep the original PDF when you may need to produce an unwatermarked copy later.