UnmarkPDF is a local, auditable PDF watermark cleanup tool. It produces a cleaned PDF, Markdown, page renders, and evidence for reviewing every run.
Download and open the interactive HTML diagram.
Requirements:
- Python 3.11–3.13
- Windows, macOS, or Linux
pip install .For development, use uv sync.
unmarkpdf input.pdfReplace input.pdf with the PDF you are authorized to process. Output is written to unmarkpdf-output/ by default.
Try the bundled synthetic sample:
unmarkpdf data/sample-watermarked.pdfIt contains two pages with CONFIDENTIAL and DRAFT text watermarks. A successful run removes two text spans while preserving the body text. Regenerate it at any time with:
python scripts/generate_sample.pyFor all options:
unmarkpdf --help- Discover: inspect text spans for light color, rotation, size, repetition, and stable position. Suggestions go to
watermark-candidates.jsonand are never applied automatically. - Clean: remove standard watermark annotations and artifacts; hide keyword-matched OCG layers; conservatively remove keyword-matched text and repeating watermark images.
- Audit: compare original and cleaned page renders, count keyword hits, and write
audit.json. - Convert: send
cleaned.pdfto MarkItDown, falling back to PyMuPDF text extraction if parsing fails.
All core processing stays inside the local-process boundary shown above. Optional VLM review is manual and does not run inside the CLI.
The defaults are:
机密, 水印, 内部资料, Confidential, Watermark, Draft
Keywords matter only for watermark forms that lack reliable PDF markers:
| Watermark form | Uses keywords |
|---|---|
| Watermark annotation | No |
/Subtype /Watermark artifact |
No |
| Repeating image | No |
| Named optional-content layer (OCG) | Yes |
| Untagged text | Yes |
Text is removed only when it also looks like a watermark: light or rotated, and not overlapping detected body text. This reduces false positives, but document-specific text such as a name, email, company, timestamp, or “仅供某某使用” may be missed.
--keywords replaces the defaults:
unmarkpdf input.pdf --keywords "ACME-7391,Internal Copy"--keywords-file appends UTF-8 lines to them:
# watermark-keywords.txt
ACME-7391
Internal Copy
unmarkpdf input.pdf --keywords-file watermark-keywords.txtEvery run inspects PDF text spans for light color, rotation, large size, central placement, repetition, and stable page position. Suggestions are written to watermark-candidates.json.
Candidates are advisory only and are never applied automatically. Review them, copy confirmed phrases into a keyword file, and run again with --keywords-file. This explicit step prevents a light title or repeated template text from being silently removed.
Discovery reads the PDF text layer. It cannot read watermark text flattened into a scanned image.
UnmarkPDF makes no network or model request. A local or remote VLM can help review difficult pages:
- Run UnmarkPDF once.
- Give selected
renders/page_XXXX_original.pngfiles to a local or remote vision model. - Ask for only the exact visible watermark phrases, one per line.
- Review the result and pass confirmed phrases with
--keywords-file.
unmarkpdf input.pdf --keywords-file vlm-reviewed-keywords.txtThis provider-neutral handoff keeps the core small and works with local models. The confirmed phrases can help only when the PDF still has a matching text layer or OCG name. They do not remove words flattened into image pixels; that requires localization and image restoration, not only a VLM description.
A remote VLM receives page contents, so do not upload confidential documents without authorization. A VLM may identify text, but it cannot reconstruct pixels destroyed by an opaque watermark.
Each run writes these files to the output directory:
| Path | Description |
|---|---|
original.pdf |
Copy of the input PDF |
cleaned.pdf |
Structurally cleaned PDF |
content.md |
Markdown converted from cleaned.pdf |
audit.json |
Processing stats, keyword counts, render diffs, warnings, and parser name |
watermark-candidates.json |
Advisory local heuristic suggestions |
renders/page_XXXX_original.png |
Rendered original page |
renders/page_XXXX_structural.png |
Rendered structurally cleaned page |
renders/page_XXXX_ocr_candidate.png |
Grayscale/thresholded OCR candidate render |
The OCR candidate render is for inspection only. It does not replace cleaned.pdf.
UnmarkPDF uses MarkItDown as the primary Markdown parser. If MarkItDown fails, it falls back to plain PyMuPDF text extraction and records a warning in audit.json.
The CLI uses platform-neutral Python paths and file operations. CI tests Python 3.11 and 3.13 on Linux, plus Python 3.13 on Windows and macOS.
- Opaque watermarks that destroyed the original pixels cannot be reliably restored.
- Single-page vector watermarks may be missed if they have no standard PDF watermark marker and do not match the configured keywords.
- Local keyword discovery cannot inspect text embedded only in pixels.
- Heuristic and VLM suggestions must be reviewed; neither is proof that text is a watermark.
- Fixed OCR-candidate thresholds do not work equally well on every colored background.
- Markdown quality for tables, columns, and reading order depends on the underlying parser.
UnmarkPDF runs locally and writes outputs to the selected output directory. Do not use it to remove ownership, provenance, access-control, or confidentiality marks unless you own the document or have explicit authorization.
UnmarkPDF is licensed under AGPL-3.0-only; see LICENSE.
PyMuPDF is also AGPL by default and offers a separate commercial licensing path. If you distribute or provide a modified service based on this project, review both this project's AGPL obligations and the licenses of its dependencies.
Keep changes small, auditable, and covered by a representative run when behavior changes.