pypdf · pdfium · pikepdf · reportlab · pdfplumber — nothing AGPL or GPL
pdf-tooling
Every common PDF chore. One safe CLI. No copyleft underneath.
One command-line tool for merge, split, extract, rotate, PDF↔images, text, tables, compress, encrypt, metadata, watermark, OCR and Office→PDF — orchestrating a permissively licensed engine stack behind one safe, scriptable interface.
Every common PDF chore, without the footguns
Six principles, machine-checked rather than promised.
Safe by default
--dry-run shows exactly what would be written and touches nothing. Outputs never clobber without --force. Every write goes to a temp file on the target filesystem, is fsync'd, and is moved into place with os.replace. Inputs are never modified unless you ask for --in-place, which writes a .bak sidecar first.
License-clean stack
Apache-2.0 top to bottom. pypdf, pdfium, pikepdf, reportlab and pdfplumber are BSD / MIT / Apache / MPL. CI fails the build on any AGPL, GPL or LGPL package, and a unit test walks the source for forbidden imports and shell-outs — because the realistic leak in a PDF tool is a convenience subprocess call, not a dependency.
Machine-first output
Every read path emits table, json or ndjson with a declared schema_version. json is automatic when stdout is not a terminal. Designed to be a link in a shell pipeline.
Honest engine reporting
pdftooling doctor states, per port, which adapter resolved, its version, and what is missing — with an OS-aware install hint. A verb whose engine is absent exits 3 with that hint; never a stack trace, never a silently degraded result.
One page-range grammar
1-3,5,9-, even, odd, first, last, -1, !5 — parsed by one module and shared by every page-addressing verb. Learn it once.
Stable exit codes
Seven codes, uniform across every verb, asserted by a contract test. A script's recovery for "wrong password" differs from "engine missing" and from "refused", so they are different codes.
A six-layer spine, one licence guarantee
Adapters are the only modules that import an engine library or spawn a process — that is what makes the licence guarantee auditable by reading eight files.
CLI (Typer)
Parses flags, builds one frozen OperationPlan, picks a renderer. The ONLY layer that may import Typer.
Ops
Framework-free verbs, pure functions over ports. No I/O except through Safety and Ports.
Safety
The single write chokepoint — atomic write-to-temp-then-rename, no-clobber, the --dry-run gate.
Ports
typing.Protocol definitions only: StructureEngine · RasterEngine · ComposeEngine · TextEngine · OcrEngine · OfficeConverter
Adapters
The ONLY modules that import an engine library or spawn a process — which is what makes the licence guarantee auditable.
Engines
pypdf · pypdfium2 · pikepdf · reportlab · pdfplumber · Tesseract · LibreOffice.
26 shipped · 0 planned
This list is checked against pdftooling --help at every deploy.
diagnostics
Report each port's resolved adapter, version, and OS-aware install hint for what is missing.
Page count/sizes, encryption + permission bits, metadata, producer, font list, signature presence, per-page rotation.
Report the tool, runtime and engine versions.
structure
Concatenate PDFs; per-input page selection via path:range; outline entry per source file.
One PDF → many, by fixed chunk size, explicit ranges, per page, or at top-level bookmarks.
Write the selected pages to a new PDF, in the order given.
Write everything except the selected pages.
Rotate the selected pages by a multiple of 90°, absolute or relative.
Rewrite page order from an explicit sequence; duplicates allowed, order preserved.
Structural recovery of a damaged/malformed PDF via libqpdf's recovery parser.
Rewrite for byte-serving ("fast web view").
Report the permission bits and encryption algorithm of an encrypted PDF.
Read the document information dictionary + XMP.
Write/clear document information fields and XMP.
raster
PDF → PNG/JPEG/TIFF/WEBP at a chosen DPI or pixel width.
compose
Images → PDF. JPEG inputs embed as DCTDecode streams (byte-preserving, no re-encode).
Text (v1) → PDF. Markdown/HTML behind the [html] extra (Phase 2).
text
Extract text: fast path (pdfium) or layout-aware (pdfplumber) with per-block geometry.
Detect and extract tables to CSV/JSON.
optimize
Shrink: libqpdf object streams plus stream recompression (lossless), optional Pillow image downsample/recompress (lossy).
crypto
Apply AES-256 (or RC4-128 with --legacy) with user/owner passwords and a permission set.
Remove encryption given the correct password.
overlay
Overlay/underlay generated text across the selected pages.
Overlay/underlay an existing PDF page onto the selected pages.
Add a Tesseract text layer over untouched pixels — a text-only PDF generated per page and merged.
external
Office → PDF via headless LibreOffice.
config and completion round out the surface — run pdftooling --help for the whole tree. Phase 2 (HTML/Markdown create, redaction, forms, outline editing, attachments/annotations, run recipes) is parked, additive on the same spine.
Quick Start
Install pdf-tooling; the command is pdftooling.
# Install (published on PyPI)
uv tool install pdf-tooling
# or: pip install pdf-toolingpdftooling doctor # which engines resolved, and how
pdftooling merge a.pdf b.pdf -O merged.pdf # concatenate PDFs into one file
pdftooling rotate report.pdf --pages 2-4 --angle 90 -O rotated.pdf # rotate pages 2-4 by 90°
pdftooling compress report.pdf -O small.pdf # lossless structural shrinkPublished on PyPI as pdf-tooling — no Homebrew formula, no system package, no container image yet. Every verb listed below is shipped —pdftooling --help is the authoritative list of what actually exists.
A stable exit-code contract
Seven codes, uniform across every verb — the same contract pdftooling --version and the source both agree on.
0
OK
Success — including an empty-but-valid report. A --dry-run mirrors the code the real run would return, so it is not always 0.
1
FAILURE
The operation ran and failed — corrupt input, engine error, unwritable destination.
2
USAGE
Bad invocation — unknown flag, mutually exclusive flags, malformed page range, unknown subcommand.
3
ENGINE_MISSING
A required engine or binary is unavailable. The message always carries an install hint.
4
NO_INPUT
Valid invocation, nothing to act on.
5
REFUSED
A safety gate declined.
6
AUTH
Password required, incorrect, or of the wrong kind.
Licensing
Apache-2.0, and every engine underneath it is BSD, MIT, Apache or MPL — nothing AGPL or GPL on the call graph.
The guarantee is a CI job and a unit test, not a promise in a README: the license gate fails the build on any AGPL, GPL or LGPL package in the distributed closure, and a source-level test walks every file under src/ for forbidden imports and subprocess shell-outs.
pikepdf bundles libqpdf under MPL-2.0 — file-level copyleft, used unmodified as a dependency, and Apache-compatible on that basis. It is documented in NOTICE and the full THIRD_PARTY_LICENSES inventory.
The default install closure is 21 packages, with no GPL-licensed package at all. pyphen (GNU General Public License v2 or later (GPLv2+); GNU Lesser General Public License v2 or later (LGPLv2+); Mozilla Public License 1.1 (MPL 1.1)) enters only via the optional [html] extra (10 additional packages) — Phase 2, and not shipped in v1.
| Package | Version | License | Tier |
|---|---|---|---|
| Pygments | 2.21.0 | BSD-2-Clause | default install |
| annotated-doc | 0.0.5 | MIT | default install |
| brotli | 1.2.0 | MIT | optional · [html] extra |
| cffi | 2.1.1 | MIT-0 | default install |
| charset-normalizer | 3.5.1 | MIT | default install |
| cryptography | 50.0.1 | Apache-2.0 OR BSD-3-Clause | default install |
| cssselect2 | 0.9.0 | BSD License | optional · [html] extra |
| fonttools | 4.63.0 | MIT | optional · [html] extra |
| lxml | 6.1.2 | BSD-3-Clause | default install |
| markdown-it-py | 4.2.0 | MIT License | default install |
| mdurl | 0.1.2 | MIT License | default install |
| packaging | 26.3 | Apache-2.0 OR BSD-2-Clause | default install |
| pdfminer.six | 20260107 | MIT | default install |
| pdfplumber | 0.11.10 | MIT License | default install |
| pikepdf | 10.12.0 | MPL-2.0 | default install |
| pillow | 12.3.0 | MIT-CMU | default install |
| pycparser | 3.0 | BSD-3-Clause | default install |
| pydyf | 0.12.1 | BSD License | optional · [html] extra |
| pypdf | 6.16.2 | BSD-3-Clause | default install |
| pypdfium2 | 5.13.0 | BSD-3-Clause, Apache-2.0, dependency licenses | default install |
| pyphen | 0.18.1 | GNU General Public License v2 or later (GPLv2+); GNU Lesser General Public License v2 or later (LGPLv2+); Mozilla Public License 1.1 (MPL 1.1) | optional · [html] extra |
| pytesseract | 0.3.13 | Apache Software License | default install |
| reportlab | 5.0.1 | BSD License | default install |
| rich | 15.0.0 | MIT License | default install |
| shellingham | 1.5.4 | ISC License (ISCL) | default install |
| tinycss2 | 1.5.1 | BSD License | optional · [html] extra |
| tinyhtml5 | 2.1.0 | MIT License | optional · [html] extra |
| typer | 0.27.2 | MIT | default install |
| weasyprint | 69.0 | BSD License | optional · [html] extra |
| webencodings | 0.6.1 | BSD License | optional · [html] extra |
| zopfli | 0.4.3 | Apache Software License | optional · [html] extra |
Full inventory: THIRD_PARTY_LICENSES.
Technology Stack
Every version below is read straight from src/data/licenses.json, generated by make licenses and diffed in CI.
| Package | Version | License | Role |
|---|---|---|---|
| annotated-doc | 0.0.5 | MIT | |
| brotli | 1.2.0 | MIT | |
| cffi | 2.1.1 | MIT-0 | |
| charset-normalizer | 3.5.1 | MIT | |
| cryptography | 50.0.1 | Apache-2.0 OR BSD-3-Clause | AES-256 encryption backing pypdf[crypto] |
| cssselect2 | 0.9.0 | BSD License | |
| fonttools | 4.63.0 | MIT | |
| lxml | 6.1.2 | BSD-3-Clause | |
| markdown-it-py | 4.2.0 | MIT License | |
| mdurl | 0.1.2 | MIT License | |
| packaging | 26.3 | Apache-2.0 OR BSD-2-Clause | |
| pdfminer.six | 20260107 | MIT | layout analysis engine behind pdfplumber |
| pdfplumber | 0.11.10 | MIT License | layout-aware text, tables (renders via pypdfium2, not poppler) |
| pikepdf | 10.12.0 | MPL-2.0 | repair, linearize, object-stream compression, robust encryption |
| pillow | 12.3.0 | MIT-CMU | image plumbing, downsampling for compression |
| pycparser | 3.0 | BSD-3-Clause | |
| pydyf | 0.12.1 | BSD License | |
| Pygments | 2.21.0 | BSD-2-Clause | |
| pypdf | 6.16.2 | BSD-3-Clause | merge, split, reorder, rotate, metadata, bookmarks, watermark overlay, encrypt/decrypt |
| pypdfium2 | 5.13.0 | BSD-3-Clause, Apache-2.0, dependency licenses | PDF → images, page rendering, fast plain-text extraction |
| pyphen | 0.18.1 | GNU General Public License v2 or later (GPLv2+); GNU Lesser General Public License v2 or later (LGPLv2+); Mozilla Public License 1.1 (MPL 1.1) | |
| pytesseract | 0.3.13 | Apache Software License | OCR driver for the tesseract binary |
| reportlab | 5.0.1 | BSD License | create PDFs from scratch, images → PDF (lossless DCTDecode embed) |
| rich | 15.0.0 | MIT License | |
| shellingham | 1.5.4 | ISC License (ISCL) | |
| tinycss2 | 1.5.1 | BSD License | |
| tinyhtml5 | 2.1.0 | MIT License | |
| typer | 0.27.2 | MIT | CLI surface |
| weasyprint | 69.0 | BSD License | optional HTML/Markdown → PDF ([html] extra, Phase 2) |
| webencodings | 0.6.1 | BSD License | |
| zopfli | 0.4.3 | Apache Software License |
Two system binaries are detected at runtime, never bundled:tesseract (Apache-2.0) and soffice / LibreOffice (MPL-2.0).