PDF comparison tool — compares text or visual appearance of two PDF files.
disparPDF is a Qt6 port of ConfrontaPDF by Luca Bellonda (2015), which is itself a fork of DiffPDF by Mark Summerfield (2008–2013).
This Qt6 port was created by Yuwsuke Kieda in 2026 with the assistance of AI tools (Claude by Anthropic).
- Compare two PDF files page by page (text or visual mode)
- Word or character comparison
- Page range specification, and a page offset for when one document has pages the other does not
- Margin exclusion
- Comparing only inside a page boundary — the MediaBox, CropBox, BleedBox,
TrimBox or ArtBox (
--box) — with the pages lined up on its corner - Font comparison (
--compareFonts, in batch mode) - Pages compared in parallel, one worker thread per core
- Batch/command line mode (
disparPDFc), which can also show the window with--interactive - Manual page (
man disparPDF) and bash/zsh completion
brew tap yuw/disparPDF
brew trust yuw/disparPDF
brew install yuw/disparPDF/disparPDFThe disparPDF and disparPDFc commands work right away. To also have the
app in /Applications (for Finder, Dock and Spotlight), copy it there yourself:
ditto /opt/homebrew/opt/disparpdf/disparPDF.app /Applications/disparPDF.appmacOS does not allow Homebrew to write into an existing app bundle in
/Applications (App Management protection), so repeat this command after
every brew upgrade — otherwise the Finder copy stays on the old version.
Use ditto, not cp -r: with an existing bundle cp -r nests the new copy
inside the old one instead of replacing it. ditto preserves the code
signature, so no re-signing is needed.
After installation:
| Location | Description |
|---|---|
/Applications/disparPDF.app |
GUI app (Finder) |
/opt/homebrew/opt/disparpdf/disparPDF.app |
Homebrew-managed copy |
/opt/homebrew/bin/disparPDF |
Opens the window; with --batch, --help or --version, runs in the terminal |
/opt/homebrew/bin/disparPDFc |
CLI batch mode |
brew update
brew upgrade yuw/disparPDF/poppler-qt6 yuw/disparPDF/disparPDFThe disparPDF and disparPDFc commands are current as soon as this finishes.
If only poppler-qt6 was upgraded, rebuild disparPDF against the new
bindings. Homebrew does not rebuild a formula when one of its dependencies is
updated, so the binary would keep running against the previous Poppler:
brew reinstall yuw/disparPDF/disparPDFIf you keep a copy in /Applications, refresh it after every upgrade.
macOS does not allow Homebrew to write into an existing app bundle there, so
this step cannot be automated:
ditto /opt/homebrew/opt/disparpdf/disparPDF.app /Applications/disparPDF.appTo check what is installed, and to reclaim the disk space held by the superseded versions:
brew list --versions disparPDF poppler-qt6
brew cleanupIf you have previously built and installed disparPDF manually, follow these steps:
# 1. Install via Homebrew tap
brew tap yuw/disparPDF
brew trust yuw/disparPDF
brew install yuw/disparPDF/disparPDF
# 2. Verify the tap installation
brew info yuw/disparPDF/disparPDF
ls /opt/homebrew/bin/disparPDF
ls /opt/homebrew/bin/disparPDFc
# 3. Remove the manual install
sudo rm -f /usr/local/bin/disparPDF
sudo rm -f /usr/local/bin/disparPDFc
sudo rm -rf /usr/local/disparPDF.app
sudo rm -rf /Applications/disparPDF.app
# 4. Copy to /Applications
ditto /opt/homebrew/opt/disparpdf/disparPDF.app /Applications/disparPDF.app
# 5. Verify
open /Applications/disparPDF.app
disparPDFc --versionHomebrew's poppler does not include Qt6 bindings.
Use the poppler-qt6.rb formula in packaging/homebrew/ to install via a local tap.
disparPDF also needs qpdf, which reads and rewrites the page boundaries.
brew install qt@6 qpdf
mkdir -p ~/homebrew-disparPDF/Formula
cp packaging/homebrew/poppler-qt6.rb ~/homebrew-disparPDF/Formula/
cd ~/homebrew-disparPDF
git init
git add Formula/poppler-qt6.rb
git commit -m "Add poppler-qt6 formula"
cd -
brew tap yuw/disparPDF ~/homebrew-disparPDF
brew install yuw/disparPDF/poppler-qt6On macOS, CMake locates Homebrew's keg-only qt@6 and poppler-qt6, and
qpdf, automatically, so no environment variables are needed. Elsewhere it
finds qpdf with pkg-config (libqpdf; e.g. the libqpdf-dev package on
Debian and Ubuntu):
cmake -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build --parallelTo use a Qt or Poppler installation elsewhere, pass it explicitly — an
explicit CMAKE_PREFIX_PATH takes precedence over the auto-detected paths:
cmake -B build -DCMAKE_BUILD_TYPE=Release \
-DCMAKE_PREFIX_PATH="/path/to/qt6;/path/to/poppler-qt6"The tests are unit tests of the page boundaries, batch-mode comparisons of small PDFs, tests of what is drawn and of the window (run on Qt's offscreen platform), and tests of the shell completions (skipped without bash-completion or zsh). None needs a display:
ctest --test-dir buildThey are described in test/CMakeLists.txt; configure with
-DBUILD_TESTING=OFF to leave them out.
# Install to /usr/local
sudo cmake --install build --prefix /usr/local
# Re-sign after install (required on macOS 26+)
codesign --force --sign - /usr/local/disparPDF.app/Contents/MacOS/disparPDF
# Optional: copy to /Applications (ditto replaces an existing bundle and
# keeps the signature; cp -r would nest the new copy inside the old one)
ditto /usr/local/disparPDF.app /Applications/disparPDF.app
# Optional: add symlink for CLI use (bin/disparPDFc is installed already)
sudo ln -sf /usr/local/disparPDF.app/Contents/MacOS/disparPDF /usr/local/bin/disparPDFcmake --install also installs disparPDFc, as a symbolic link (on
Windows, a copy), the manual page (man disparPDF, or man disparPDFc),
bash and zsh completion (set BASH_COMPLETION_DIR and
ZSH_COMPLETION_DIR to choose where), and the README and CHANGES; on
Linux and the BSDs, also a desktop file and icon for the application
menus. Packagers can use DESTDIR and the usual CMAKE_INSTALL_*
directories. The manual page is
generated with help2man from --help; without it, or when cross
compiling, the copy in doc_man/ is installed instead (refresh it with
cmake --build build --target update-manpage).
# Launch from Finder
open /Applications/disparPDF.app
# Launch with files from terminal
disparPDF a.pdf b.pdfKeyboard shortcuts, which work wherever the keyboard is in the window (on macOS, Ctrl is ⌘ and Alt is Option):
| Keys | Action |
|---|---|
| Ctrl+Up, Ctrl+Down | The previous and the next pair of differing pages |
| Ctrl++, Ctrl+- | Zoom in and out by 25% (Ctrl+= and Ctrl+; zoom in too, for keyboards where + is a shifted key) |
| Ctrl+0 | Zoom back to 100% |
| Alt+arrow keys | Move the page views a little |
| Alt+Page Up, Alt+Page Down | Move them up and down by what they show |
| Alt+Home, Alt+End | Go to the top and the bottom |
After a comparison the keyboard is in the View box, where Up and Down alone go from one pair to the next. A click on a page moves it to the page views, where the arrow keys scroll; a click on the Controls dock, anywhere but on one of its controls, gives it back to the View box.
In the Characters and Words modes, a character that Poppler cannot map to Unicode (a glyph whose name is in no glyph list comes back as a control character) is compared by how it looks: where such a text and the one in the same place on the other page differ, they count as the same if their renderings are the same. Otherwise these modes compare characters, not shapes, so a letter set in another typeface is found only by the Appearance mode.
# Returns 0 if identical, non-0 if differences found
disparPDFc -b a.pdf b.pdf
# With description
disparPDFc -b --outType=1 a.pdf b.pdf
# XML output
disparPDFc -b --xmlResult=result.xml a.pdf b.pdf# Only what is inside the trim box: crop marks, colour bars and bleed are
# ignored, and a page with bleed matches the same page without it
disparPDFc -b --box=trim a.pdf b.pdfdisparPDFc is not a separate program but a symbolic link to disparPDF
(on Windows, a copy of it): run by that name, it is in batch mode, so
-b is optional, and it needs no display. disparPDF -b does the
same, and disparPDFc --interactive shows the window, as disparPDF does.
A PDF page can have up to five boundaries (ISO 32000-2:2020, 14.11.2):
- the MediaBox, the physical medium;
- the CropBox, to which it is clipped when displayed or printed;
- the BleedBox, to which it is clipped in production;
- the TrimBox, the finished page after trimming;
- the ArtBox, the page's meaningful content.
--box=BOX (or the GUI's Page Box dock) compares only what is inside
one of them. none compares whole pages, as without the option.
-
A page without the box uses its default from the standard's Table 31: the CropBox for the BleedBox, TrimBox and ArtBox, and the MediaBox for the CropBox. A warning says so.
-
Clipping, chosen with
--box-clip=CLIP:CLIPwhat is compared media(the default)the box clipped to the MediaBox, as 14.11.2.1 requires of a processor cropthe box clipped to the MediaBox and the CropBox, i.e. only what a viewer shows nonethe box as written, even where it extends past the MediaBox or the CropBox -
Text is judged character by character. In a word that straddles the box's edge, the characters inside it count. A character on the edge, or overlapping it, counts as inside. The character's box is the one Poppler gives, not where the glyph actually puts ink. Appearance is compared on the pixels inside the box.
-
The pages are lined up on the box's top left corner, after
/Rotate, rather than the CropBox's. So a page with bleed matches the same page trimmed. -
Excluded margins are measured from the box's edges.
-
The views and saved differences show the whole page: the CropBox (within the MediaBox) and the box together. Everything outside the box is dimmed, the box is outlined in blue, and nothing outside it is highlighted.
-
Warnings are given once per file, or for the pair, with the pages they apply to, when:
- the box is absent (
absent); - with
crop, the box extends past the CropBox (beyond-crop); - with
none, it extends past the MediaBox (beyond-media); - nothing is left once it is clipped (
empty); - the two files' boxes differ in size or position on the page by more
than 0.01pt (
differ, for the pair); --box-clipis given without--box, and so ignored (clip-without-box).
disparPDFcwrites them to standard error and leaves the result code alone. With--xmlResultthey also go into the XML, as a<warnings>element holding one<warning>per kind and file:kindis the name in brackets above,fileis 1 or 2 (left out for the pair and forclip-without-box),pageslists the pages, e.g.1-3, 5(left out forclip-without-box; fordiffer, if any pair compared has different page numbers in the two files, every entry is a FILE1/FILE2 pair such as3/4), andmessageis the text. The GUI writes them to the log and counts them in the status line. - the box is absent (
-
In the GUI, changing the box or the clipping forgets the comparison, which was made inside another box, so click Compare again. The choice is kept for next time, but one given with
--boxor--box-clipwhen starting the program is used without being saved, unless it is changed in the window. -
Boxes, like margins, are in default user space units:
/UserUnit(PDF 1.6) is not applied. -
The boxes are read, and the pages rewritten to them, with qpdf. A file qpdf cannot read is reported as unreadable (
-3or-4) rather than compared whole.
The GUI keeps its settings per user, on macOS in
~/Library/Preferences/com.disparpdf.disparPDF.plist, and on Linux and
the BSDs in ~/.config/disparPDF/disparPDF.conf (an INI file).
disparPDFc ignores that file and reads an INI file given with
--settings=FILE, which is how a scripted comparison gets non-default
options:
disparPDFc -a --settings=mysettings.ini a.pdf b.pdfMost keys below also have a control in the GUI; they are listed so that an
INI file for disparPDFc can be written by hand. Those in the last table
have no GUI control at all. The GUI also keeps its window layout (keys
under MainWindow/ and HelpForm/), which batch mode ignores.
| Key | Default | GUI | Meaning |
|---|---|---|---|
InitialComparisonMode |
2 |
Compare box | Mode the GUI starts in: 0 appearance, 1 characters, 2 words. Batch mode ignores this and compares appearance unless -a, -c or -w is given |
Margins/Exclude |
false |
Exclude Margins | Ignore everything outside the margins below |
Margins/Top, /Bottom, /Left, /Right |
0 |
Margins dock | Margin sizes, in points; inside a page box, from its edges. In the GUI each can be up to half the width or height, less 10pt, of the first page of the file chosen last, or inside a page box, of the smaller of the two files' boxes |
Margins/Unit |
pt |
Margins dock ▸ Unit | The unit the GUI shows margins in and reads them in: pt or mm. The margins themselves are still held and saved in whole points, so a margin typed in millimetres goes to the nearest point (20 mm is 57 pt, shown as 20.1 mm). Batch mode ignores this |
PageBox/Box |
none |
Page Box dock ▸ Page boundary | Compare only inside this page box: none, media, crop, bleed, trim or art (see Comparing inside a page box). --box overrides it |
PageBox/Clip |
media |
Page Box dock ▸ Clipped to | What that box is clipped to: media, crop or none. --box-clip overrides it |
Zoning/Enable |
false |
Zoning | Group text into zones before comparing |
Columns |
1 |
Columns | How many columns the page has; improves zoning |
Tolerance/R |
8 |
Tolerance/R | Largest distance (4–144) between word rectangles for them to land in the same zone |
Tolerance/Y |
10 |
Tolerance/Y | Text y coordinates are rounded up to a multiple of this (0–32) when zoning |
RequirePdfExtension |
true |
Options ▸ Files to compare | Accept only files named *.pdf on the command line and show only those in the file dialogs (a name typed in the window, or a file dropped on it, is taken whatever it is called). Batch mode ignores this and always requires *.pdf unless --any-extension is given |
| Key | Default | GUI | Meaning |
|---|---|---|---|
Zoom |
100 |
Zoom | View magnification, per cent |
Outline, Fill |
— | Options ▸ Highlighting | Pen and brush for highlights. These are serialised Qt values, so set them through the dialog rather than by hand |
Opacity |
13 |
Options ▸ Fill Opacity | Fill opacity, per cent (1–100) |
RuleWidth |
1.5 |
Options ▸ Rule width | Width of the rule drawn at the left edge of the page beside each difference (0–10); 0 turns the rules off |
SquareSize |
10 |
Options ▸ Square Size | Size of the squares in which appearance differences are found and highlighted, in pixels (2–40) |
CombineTextHighlighting |
true |
Options ▸ Combine Highlighting in Text Modes | Merge adjacent highlights in the text modes |
ShowToolTips |
true |
Options ▸ Show Tooltips in the Main Window | Show tool tips in the main window |
CacheSizeMB |
25 |
Options ▸ Cache Size | Limit on the rendered-page cache, in MB (1–100) |
| Key | Default | Meaning |
|---|---|---|
Overlap |
5 |
With CombineTextHighlighting, how close (in pixels) text highlights must be to be merged. The GUI reads it from its own settings too |
CompareThreads |
0 |
Upper bound on comparison worker threads; 0 means one per core. Each worker opens its own copy of both documents, so lowering this lowers peak memory. On a 1536-page appearance comparison across 8 cores, peak RSS was 223 MB at 0, 152 MB at 4 and 89 MB at 1 — against 74 MB before the comparison was threaded — and it took 2.8 s at 1 against 1.4 s at 0. The GUI reads it from its own settings too |
compositionMode |
-1 |
QPainter::CompositionMode used to draw appearance differences; -1 draws the ordinary highlight. This is the batch-mode counterpart of the GUI's Show box, whose choice is not saved (the GUI does not read this key): 22 Difference, 23 Exclusion, 26 Src Xor Dest, 29 Not Src Xor Dest |
ShowHighlight |
-1 |
Read and copied into the --xmlResult parameters, but it has no effect |
GPL-2.0-or-later
Copyright © 2026 Yuwsuke Kieda
Based on ConfrontaPDF © 2015 Luca Bellonda
Based on DiffPDF © 2008–2013 Qtrac Ltd. (Mark Summerfield)