PDF-Compare
A powerful tool for comparing PDF files. Generates vector-based side-by-side comparison reports with content-aware highlighting.
Features
- Vector-Based Rendering: Preserves text quality and keeps file sizes small (no image conversion)
- Searchable Output: Generated PDFs maintain searchable, selectable text
- Visual Comparison: Side-by-side view of two PDFs with intelligent page alignment
- Content-Aware Highlighting: Detects text changes based on content, ignoring layout shifts
- Smart Page Alignment: Automatically detects inserted/deleted pages
- Color-Coded Differences:
- Red: Deleted text (on the original document)
- Green: Added text (on the modified document)
- Multiple Interfaces: CLI, GUI Desktop App, and Python API
- Cross-Platform: Works on Windows, macOS, and Linux
Installation
The package is published on PyPI as py-pdf-compare:
pip install py-pdf-compare
Or, to install it as a standalone command-line tool with uv (recommended — no virtual environment to manage):
uv tool install py-pdf-compare
To add it as a dependency of your own project instead:
uv add py-pdf-compare
Note on names: the distribution is
py-pdf-compare, but the import name ispdf_compareand the commands arepdf-compareandpdf-compare-gui.
Prerequisites
- Python 3.12+ is required
Windows: Download from python.org and check "Add Python to PATH" during installation.
macOS:
brew install python@3.12
Linux (Ubuntu/Debian):
sudo apt install python3.12 python3.12-venv
Note: No additional dependencies (like Poppler) are required. PyMuPDF handles all PDF operations natively.
Quick Start
CLI Usage
# Compare two PDFs
pdf-compare original.pdf modified.pdf -o diff.pdf
# Get a machine-readable summary instead of a PDF
pdf-compare original.pdf modified.pdf --json result.json
# Compare two folders: diff documents plus an HTML report
pdf-compare ./version-1 ./version-2 --out ./diffs
# Compare two folders, report only
pdf-compare ./version-1 ./version-2 --report changes.html
# Launch GUI application
pdf-compare-gui
# Show help
pdf-compare --help
Python API
from pdf_compare import PDFComparator
# Create comparator instance
comparator = PDFComparator('original.pdf', 'modified.pdf')
# Generate comparison report (None when both documents are identical)
pdf_bytes = comparator.compare_visuals()
if pdf_bytes is None:
print('No differences found')
else:
# Save to file
with open('report.pdf', 'wb') as f:
f.write(pdf_bytes)
API Reference
PDFComparator(file_a, file_b)
Main class for comparing PDF files.
Parameters:
file_a(str): Path to the first PDF (Original)file_b(str): Path to the second PDF (Modified)
Methods:
compare_visuals() -> bytes | None
Generate a vector-based visual comparison report.
Returns: PDF report as bytes, or None if no differences found. "No differences" means no page was added or removed and no word changed on any matched page.
Attributes:
missing_text_layer(bool): set after callingcompare_visuals().Truewhen either document has no extractable text (a scan, for example), which makes the comparison unreliable — the report is still returned in that case, since the absence of differences cannot be confirmed.
Example:
from pdf_compare import PDFComparator
comparator = PDFComparator('a.pdf', 'b.pdf')
result = comparator.compare_visuals()
if comparator.missing_text_layer:
print("Warning: no text layer, differences cannot be detected reliably")
if result is not None:
with open('diff.pdf', 'wb') as f:
f.write(result)
print("Report generated successfully")
else:
print("No differences found")
analyze() -> dict
Run the same comparison without composing the PDF, and return the result as plain data. Intended for automation: which files were compared and how much changed.
{
"files": {
"original": {"name": "a.pdf", "path": "/abs/a.pdf", "pages": 3},
"modified": {"name": "b.pdf", "path": "/abs/b.pdf", "pages": 4}
},
"identical": false,
"missing_text_layer": false,
"changes": {
"pages_added": 1, "pages_removed": 0, "pages_modified": 0,
"words_added": 78, "words_removed": 0
}
}
Counts are reported rather than a similarity percentage, deliberately: a percentage needs a denominator nobody agrees on (words of the original? of both? what is a whole added page worth?), while counts are facts the caller can turn into whatever ratio they need.
identical: true is only trustworthy when missing_text_layer is false. A scan has no extractable text, so it looks unchanged — always check both fields together.
The same comparison, literally: analyze() and compare_visuals() share the page alignment and the word-level diff, so they can never disagree. It is not meaningfully faster, though — composing the report references the source pages as vector objects rather than rendering them, so building the PDF costs almost nothing. The reason to use it is the format, and not writing a large file you do not need.
Comparing folders
Pass two directories instead of two files and every document is compared against its counterpart in the other folder:
pdf-compare ./version-1 ./version-2 --out ./diffs # diffs + report
pdf-compare ./version-1 ./version-2 --report r.html # report only
Pairing. Documents are matched by file name. Identical names (ignoring case) are matched first, so a perfect name can never be taken by a similar one; the rest are matched by name similarity, best score first and one-to-one, above a threshold. A file whose counterpart cannot be found is never compared silently — it is listed in the report, since a document with no counterpart usually means one was added or withdrawn.
Output. --out DIR writes the diff of every differing pair into DIR; identical pairs
produce no document. An HTML report is always written: to report.html inside --out,
or to the path given by --report. Without --out nothing but the report is produced.
The report is a single self-contained file — no external requests — listing every pair with its page and word changes, linking each diff document, and calling out the two cases that need a human: documents with no counterpart, and pairs with no text layer, whose result cannot be trusted.
How It Works
- Text Extraction: Extracts text and layout information from each page using PyMuPDF
- Similarity Scoring: Calculates similarity between pages using sequence matching
- Smart Alignment: Detects insertions, deletions, and shifts between documents
- Vector-Based Report: Creates a new PDF that preserves the original vector content
- Visual Highlighting: Adds vector-based highlights over text differences (no rasterization)
- Optimized Output: Maintains searchable text and small file sizes
Example: Inserted Page
If you insert a page in the middle of a document:
- The inserted page is shown with a blank page on the left, labeled "Added"
- Subsequent pages are correctly aligned and labeled as "Shifted"
Limitations and edge cases
- Identical documents: no report is produced. The CLI says so and exits with code
0; the API returnsNone. - Scanned documents (no text layer): differences are detected from extractable text, so a scan cannot be compared. The tool detects this and warns instead of reporting "no differences"; there is no OCR.
- GUI preview: only the first 20 pages are rendered in the window, to keep memory bounded. The generated report file always contains every page.
- Large page shifts: alignment looks at most 3 pages ahead, so a block of more than 3 consecutive inserted or deleted pages in the middle of a document may not be recovered, and the pages after it can be reported as changed.
Project Structure
Py-PDF-Compare/
├── pdf_compare/
│ ├── __init__.py # Package initialization
│ ├── comparator.py # Core comparison logic
│ ├── batch.py # Folder pairing and HTML report
│ ├── cli.py # Command-line interface
│ ├── gui.py # Desktop GUI application
│ └── config.py # Configuration
├── tests/ # Test suite (pytest)
├── .claude/skills/ # Claude Code skill shipped with the project
├── scripts/
│ ├── build_windows.py # Build Windows executable
│ ├── build_linux.py # Build Linux executable
│ └── build_macos.py # Build macOS application
├── sample-files/ # Test PDFs for development
│ ├── original.pdf
│ ├── modified.pdf
│ ├── modified_extra_page.pdf
│ └── modified_removed_page.pdf
└── pyproject.toml # Python package configuration
Development
From Source
git clone https://github.com/grananda/Py-PDF-Compare.git
cd Py-PDF-Compare
uv sync
uv sync installs the project into .venv in editable mode, but it does not put pdf-compare on your PATH. From source, run the commands through uv run — that way every change to the code takes effect immediately, with no reinstall step:
# Compare sample files
uv run pdf-compare sample-files/original.pdf sample-files/modified.pdf -o test-output.pdf
# Launch the GUI
uv run pdf-compare-gui
# From outside the repository
uv run --project /path/to/Py-PDF-Compare pdf-compare a.pdf b.pdf -o diff.pdf
Careful:
uv tool install py-pdf-compareinstalls the published version from PyPI, so it will never pick up your local changes. To get a globalpdf-comparecommand that tracks your working copy, install it from the repository instead:uv tool install --editable .
Sample files included for testing:
sample-files/original.pdf- Base documentsample-files/modified.pdf- Document with text changessample-files/modified_extra_page.pdf- Document with added pagesample-files/modified_removed_page.pdf- Document with removed page
GUI Application
# From source
uv run pdf-compare-gui
# Or after installing the package
pdf-compare-gui
The GUI needs the Tk system libraries, which are not part of the Python package. If it fails with ImportError: libtk8.6.so, install them:
sudo pacman -S tk # Arch / CachyOS
sudo apt install python3-tk # Debian / Ubuntu
brew install python-tk # macOS
They are already included in the standard Python installers for Windows and macOS.
Building Standalone Executables
Windows Executable:
uv run python scripts/build_windows.py
# Result: dist/PDF Compare.exe
Linux Binary:
uv run python scripts/build_linux.py
# Result: dist/pdf-compare
macOS Application:
uv run python scripts/build_macos.py
# Result: dist/PDF Compare.app
Using as Git Submodule
This package can be integrated into other projects as a Git submodule:
git submodule add https://github.com/grananda/Py-PDF-Compare.git
Then import in your Python code:
from pdf_compare import PDFComparator
Claude Code skill
The repository ships a Claude Code skill in
.claude/skills/pdf-compare/. It is picked up automatically when Claude Code runs in this
project — nothing to install — and turns a request like "compare these two PDFs" or
"compare these folders" into the right invocation. If you do not use Claude Code, the
directory is inert.
What it encodes is the operational knowledge that is otherwise learned the hard way:
- Never run a bare
pdf-compare. A copy installed withuv tool installis frozen at whatever version was current that day. The skill resolves the build explicitly — the working copy when there is one,uvx --from "py-pdf-compare@latest"otherwise — and states which one it ran, so a surprising result is never confused with a stale binary. - Argument order decides the colours: the first document is drawn on the left, with its differences in red.
- An identical pair writes no file at all, so there is no report to go looking for.
- A missing text layer makes the result untrustworthy — and in a batch of two hundred documents, easy to miss among the pairs that worked.
- Documents with no counterpart were never compared, which usually means one was added or withdrawn: often the most important finding in a batch.
- A wall of differences starting at one specific page points at the alignment lookahead window being exceeded, before it points at a genuinely rewritten document.
- Document contents never get pasted into the conversation. The PDFs compared in practice are contracts, policies and invoices.
Ask for it in plain language, or invoke it explicitly with /pdf-compare.
License
Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
Support
For issues, questions, or contributions, visit: https://github.com/grananda/Py-PDF-Compare
Metadata
Release files for py-pdf-compare 2026.8.24
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| py_pdf_compare-2026.8.24.tar.gz | 660.0 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| py_pdf_compare-2026.8.24-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 698.9 kB
Release files / py_pdf_compare-2026.8.24.tar.gz
| Download URL | py_pdf_compare-2026.8.24.tar.gz |
|---|---|
| Size | 660.0 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
697b344d15b150a2b167fafb63637d76637e007dc7e50faa386deda967ab8eef
|
|
BLAKE2b-256 checksum How to use checksums |
eaf38d568f38b22bd002a8d2312aa6ebcf56eebcf00505130e231776adf75e1c
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Aug 24, 2026.
Transparency logRelease files / py_pdf_compare-2026.8.24-py3-none-any.whl
| Download URL | py_pdf_compare-2026.8.24-py3-none-any.whl |
|---|---|
| Size | 38.9 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
83b38ad31c74e413456cca3df817d34f77f31173ed919c5567d3fb528de2a510
|
|
BLAKE2b-256 checksum How to use checksums |
e513e5a8f28c387098cecfbe8d90e1d30400121c9c4176843b2985e2f09897de
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Aug 24, 2026.
Transparency log