Test tools
tclpdf itself needs nothing but Tcl. This page is about the tools the test suite and the acceptance run use to check what the package produces — and about the one rule that governs all of them: a missing tool is a skip, never a pass. A run that says Skipped 14 has checked less than one that says Skipped 0, and both say so out loud.
Nothing here is required to use tclpdf, and nothing here is required to run make test either: without any of it the suite still runs, it just checks less.
What make test uses
The suite is 4462 tests in 85 files. The table says what each tool costs you if it is absent — measured, not estimated: every number is the count of tests that stop running when that constraint is forced to 0, read off the suite's own Number of tests skipped for each constraint block (2026-08-27, tclsh8.6). Re-measure them after a release rather than trusting them: the suite grows.
| Tool | What it checks | Tests | macOS (Homebrew) | Debian/Ubuntu (apt) |
|---|---|---|---|---|
| tdom ≥ 0.9.0 | the XMP packet: everything that declares PDF/A, PDF/UA or ZUGFeRD | 383 | brew install tdom |
apt install tdom (0.9.5.1) |
| qpdf | that a written file is structurally sound | 50 | brew install qpdf |
apt install qpdf |
| veraPDF | PDF/A and PDF/UA conformance | 7 + (20) | brew install verapdf |
no package — see below |
poppler (pdftotext) |
that text comes back out as it went in | 22 | brew install poppler |
apt install poppler-utils |
poppler (pdfsig) |
that a signature reads back and its digest still covers the file | 6 | brew install poppler |
apt install poppler-utils |
poppler (pdftoppm) |
the colour of a rendered pixel — the only check that looks at the page rather than the file | 17 | brew install poppler |
apt install poppler-utils |
poppler (pdfinfo, pdffonts) |
what a reader reports about a finished file — page boxes, the font table | 4 | brew install poppler |
apt install poppler-utils |
| openssl | makes the test CA and the CMS object the signature tests sign with | 14 | brew install openssl |
apt install openssl |
tcllib (aes, sha256) |
the pure-Tcl crypto against a second implementation | 3 | brew install tcllib |
apt install tcllib |
HarfBuzz (hb-shape) |
that Arabic shaping and the contextual GSUB lookups produce the glyphs HarfBuzz produces | 11 | brew install harfbuzz |
apt install libharfbuzz-bin |
| fribidi | the bidirectional number handling against the reference implementation | 1 | brew install fribidi |
apt install libfribidi-bin |
libjpeg-turbo (cjpeg, djpeg) |
a JPEG built at test time rather than shipped | 1 | brew install jpeg-turbo |
apt install libjpeg-turbo-progs |
| tzint ≥ 1.3 | barcodes: the encoder's SVG drawn as vectors | 8 | no formula — build it | no package — build it |
| fontTools | nothing automatic — ttx and pyftsubset are the reference a font question is settled against by hand |
— | brew install fonttools |
apt install fonttools |
tdom is the one worth having. It is not optional for the tests the way the others are: 383 tests need it, and five test files step aside entirely without it. That is not an accident of the suite — it mirrors the package. xmp, pdfa, zugferd and ua are the four modules that build or read the XMP packet, and they are the only ones that need tdom; a document that claims no conformance never loads them and runs on Tcl alone.
tzint is the Tcl binding for libzint and has no package anywhere. The C library it binds does: brew install zint (2.16.0) or apt install libzint-dev (2.15.0); the Tcl binding is built against it. tclpdf carries no barcode encoder and does not need one — tzint encodes into SVG and svg -data draws it — so this is the one entry on the page that is about a feature rather than about a check.
fontTools is a reference, not a gate. make check never calls it and no test carries a constraint for it, which is why its row has no number: it is the second opinion a font question is settled against by hand. ttx dumps a face's tables as XML — the one way to see what a COLR, GSUB or cmap table really says — and pyftsubset cuts a face down the way this package's own subsetter does, so the two outputs can be held against each other. Measured here: 4.63.0, brew install fonttools. The Homebrew formula installs the command line tools into a virtual environment of their own, so import fontTools from an outside Python does not work and is not meant to.
openssl is not one program. macOS ships LibreSSL as /usr/bin/openssl, and it signs — but it has neither cms -cades nor -no_signing_time, so the PAdES tests skip on it. brew install openssl puts OpenSSL 3 beside it; measured here, 3.6.3 does both. 000-tools.test says which one it found and adds a line when that openssl cannot sign.
Two more prerequisites are neither tools nor shipped files. Tk carries 4 tests (haveTk): example 11.01-tk-canvas draws a Tk canvas as vectors, and on a machine without Tk or a display both the example and its tests step aside. Noto Color Emoji carries 6 (haveNotoColorEmoji): the 25 MB face behind example 02.18 lives in the working tree but is neither versioned nor in the source archive — the line the example prints when it steps aside says where to get it and where to put it. Both are the reason make examples counts 87 to 90 documents depending on the machine.
The hyphenation patterns are files, not tools. 62 tests load hyph_de_DE.dic and hyph_en_US.dic from examples/assets/languages, and that directory does not exist in a fresh checkout and is excluded from the source archive, on purpose: every published pattern set carries a licence of its own and this package is MIT, so the files are neither shipped nor versioned. The wiki page hyphenation says where they come from and where to put them; without them the tests skip and the example says on its page that it is setting unhyphenated text.
veraPDF has no Debian package. Take the installer from https://verapdf.org/software/; it is a Java application and brings its own launcher.
If a tool is installed and still not found, the suite's first test file prints the answer: 000-tools.test reports every tool and package with the version and the path it was found at, and — for the Tcl packages — the auto_path list it searched, in order. Two cases from real machines, both diagnosed by exactly that report:
A package in a private directory (say ~/lib, reachable through TCLLIBPATH): make test builds the interpreter's search path from the build directory plus your TCLLIBPATH — the build directory first, so the package under test wins. It used to replace the variable instead, and a tdom that every interactive tclsh found counted as missing under make test, with nothing saying that ~/lib was never searched. If the report lists tdom as MISSING, look at its auto_path block: the directory the package lives in has to be in that list.
A package that is registered but broken: Homebrew's tdom stood in the package index while its shared library was gone, so package require tdom threw a dlopen error. The suite asks for the four XMP modules inside a catch, so that case skips 383 tests instead of ending the run — and this line shows the breakage directly:
echo 'puts [package require tdom]' | tclsh8.6
What make check uses on top
make check is the full acceptance: the suite under every interpreter present, every example, and then every produced document through the validators.
| Tool | What it does there | macOS (Homebrew) | Debian/Ubuntu (apt) |
|---|---|---|---|
| veraPDF | every document against the profile it claims for itself — 3b, 3u, 3a, ua1, ua2, wt1r, wt1a |
brew install verapdf |
see above |
| qpdf | every document, plus a control: a truncated file must be rejected; and the field table of every document with an interactive form, read with --json --json-key=acroform |
brew install qpdf |
apt install qpdf |
poppler (pdfinfo) |
a reader rather than a validator — what a consumer actually gets | brew install poppler |
apt install poppler-utils |
poppler (pdfsig) |
every signed document, read back as a reader sees it | brew install poppler |
apt install poppler-utils |
| openssl | signs the example documents that carry a signature | brew install openssl |
apt install openssl |
| Java ≥ 11 | runs Mustangproject over every hybrid invoice, and PDFBox — out of the same jar — over every document that carries an interactive form; the jar is not part of tclpdf, see below | brew install openjdk |
apt install default-jre-headless |
| pandoc | builds doc/tclpdf.n and doc/tclpdf.html; the run checks they are no older than doc/tclpdf.md |
brew install pandoc |
apt install pandoc |
Three of the eleven sections are not in the table above, because what they check is not a tool but a promise of this tree; only the last of the three needs no outside program at all. The interactive forms are read twice: qpdf --json --json-key=acroform for the field table — every form has to declare needappearances: false and give every field a type — and PDFBox out of the Mustang jar, which uses a field through tools/formcheck.java, writing a value and building the appearance from /DA against /DR the way a viewer would; a document without a form is put through the same check as a control and has to be refused. The reference code of the tclpdf-tcl skill (doc/claude/skills/tclpdf-tcl/check.tcl) extracts every Tcl block of the skill's reference pages, runs each under every interpreter present and puts qpdf and veraPDF over what comes out — code that is copied has to keep running. And the error code table of the manual is compared against the source it claims to be generated from, tclsh tools/mkerrorcodes.tcl --check, which prints the rows that differ and the command that repairs them.
Mustangproject is not part of tclpdf and is in neither the repository nor the source archive — it is a separate project (Apache 2.0) and a 59 MB jar. make check looks for tools/Mustang-CLI-*.jar and skips that section when there is none, so this check is opt-in: download the jar from https://www.mustangproject.org/commandline/ — the source is at https://github.com/ZUGFeRD/mustangproject — and drop it in tools/. It is worth the download for anyone working on invoices, because it validates the XML and the PDF/A wrapper of a ZUGFeRD, Factur-X or Order-X document together, which neither qpdf nor veraPDF does.
No Ghostscript. It is not used anywhere and is not needed.
Reading a run
The suite tells you what it did not check. This is the last block of make test, measured on a machine that lacked five of the tools — the totals are from an earlier release and the shape of the block is what matters, not its numbers:
all.tcl: Total 2598 Passed 2516 Skipped 82 Failed 0
Number of tests skipped for each constraint:
1 haveCjpeg
1 haveFribidi
7 haveHbShape
6 pdfsig
13 signing
1 signingPades
34 haveGermanPatterns
13 haveEnglishPatterns
6 haveBlockHyphenate
That block is what a machine without openssl, HarfBuzz, fribidi, libjpeg and the hyphenation patterns reports: every number is the count of tests carrying that constraint, and they add up to the skip line. A machine with everything installed reports Skipped 0 — measured here on 2026-08-27, Total 4462 Passed 4462 Skipped 0.
Every have… line maps to a row of the first table. The Total can shrink as well as the skip count grow: without tdom the five test files that load the XMP modules step aside entirely (each says so: skipped: xmp.test needs tdom), and their tests never enter the count, so a tdom-less run reports a smaller Total rather than a larger skip count for them — of the 383 tests that need tdom, 306 sit in those five files and 77 ask for it singly. Missing never means failing; the reverse case is different: a tool that is present but broken — a qpdf on the PATH that errors when called — fails its tests, deliberately, because a broken tool should be visible rather than pass as "not installed". (The one such case on the package side, a registered tdom whose library is gone, is treated as missing: the load error is caught and the tests skip.) haveVariable and the other font constraints are different: they are not tools but files — the fonts and profiles the source archive carries under examples/assets. If those are skipped, the archive is incomplete rather than the machine.
veraPDF is the exception in that table, and its number is two numbers for that reason. Seven tests carry the verapdf constraint and skip loudly without the tool. The other twenty call the same helper without a constraint: they run either way, and without veraPDF the helper returns ok, so they check that the document was written and stop there. Nothing tells you that the conformance was not checked, which is why that half of the number is in brackets. make check does not work that way; there a missing veraPDF is a loud skip.
make check ends the same way and is stricter about it:
===
passed 81, failed 0, skipped 0
That total is one run's, the same way the suite total is: it counts the checks a full acceptance made on the day it ran, and it grows with the tree.
A run that could not do everything says so on a line of its own:
===
passed 22, failed 0, skipped 15
NOTE: 15 check(s) did not run - skipped is not passed
The exit status follows the failures alone: a run with skips still exits 0, so automation does not break on a lean machine — the NOTE line is what says the run proved less.
Everything at once
macOS:
brew install tdom qpdf verapdf poppler harfbuzz fribidi jpeg-turbo openssl tcllib openjdk pandoc fonttools
Debian and Ubuntu:
apt install tdom qpdf poppler-utils libharfbuzz-bin libfribidi-bin \
libjpeg-turbo-progs openssl tcllib default-jre-headless pandoc fonttools
# veraPDF: installer from https://verapdf.org/software/
Then make test should report Skipped 8 — the barcode tests, because tzint is built rather than installed — and make check should report skipped 2 — the Mustang section and the PDFBox half of the form section, both waiting on the same jar — until it is put in tools/.