Split a PDF file at top-level bookmarks into separate PDF files, named after each bookmark.
Download the standalone executable for your platform from the latest release:
- Windows:
splitmarks.exe - macOS:
splitmarks - Linux:
splitmarks
On macOS/Linux, make it executable after downloading:
chmod +x splitmarksRequires Python 3.10+:
pip install git+https://github.com/jet52/splitmarks.gitOr clone and install in development mode:
git clone https://github.com/jet52/splitmarks.git
cd splitmarks
pip install -e .splitmarks input.pdf [-o OUTPUT_DIR] [-m MATCH] [-v|-vv] [--dry-run] [--no-clobber] [--check-text] [--version]
| Argument | Description |
|---|---|
input_pdf |
PDF file to split |
-o, --output-dir DIR |
Output directory (default: current directory) |
-m, --match TEXT |
Only extract bookmarks containing TEXT (case-insensitive) |
-v |
Show progress (page counts, bookmark counts) |
-vv |
Also show nested bookmark tree for each output file |
--dry-run |
Preview splits without creating files |
--no-clobber |
Avoid collisions: prepend case number from filename, or auto-increment from 00000000 |
--check-text |
After splitting, warn about output PDFs whose text layer is missing or corrupt (needs pdftotext) |
--version |
Show version number and exit |
Preview what files would be created:
splitmarks document.pdf --dry-runSplit a PDF into the current directory:
splitmarks document.pdfSplit into a specific directory with verbose output:
splitmarks document.pdf -o ./split_files -vExtract only bookmarks containing "Memo":
splitmarks document.pdf --match MemoExtract all briefs (case-insensitive matching):
splitmarks document.pdf -m brief -o ./briefsPreview with full bookmark tree:
splitmarks document.pdf --dry-run -vvSplit and flag any output that appears image-scanned (so you know what to OCR):
splitmarks packet.pdf -o ./split_output --check-textBatch extract memos from multiple PDFs, avoiding filename collisions:
for f in ./packets/*.pdf; do
splitmarks "$f" --match Memo --no-clobber -o ./memos
done- Opens the PDF and reads its bookmark outline
- Splits at top-level bookmarks (each becomes a separate file)
- Calculates page ranges for each section (from one bookmark to the next)
- Creates a separate PDF file for each section, named after the bookmark title
- Preserves nested bookmarks within each split file
- Removes unreferenced resources (images, fonts) so each file contains only what its pages need
textquality.py scores an extracted text layer and is the module --check-text
consults. It exists because character density alone answers the wrong question.
Density catches a pure image scan, but it is blind to a layer that is present
and garbage — the Acrobat "Paper Capture" and Google Books case, where a scan
yields plenty of characters of confident nonsense:
"the assessmellt thereof shall Le suberdmate to the gelleral plall"
That matters because ocrmypdf --skip-text is a silent no-op on such a file: it
skips every page that already carries text, leaves the corruption in place, and
reports success. So three states are reported rather than two, each with a
different remedy:
| State | Meaning | Remedy |
|---|---|---|
text-ok |
usable prose | use the text layer as-is |
no-text-layer |
image-only | ocrmypdf --skip-text |
text-layer-corrupt |
dense but wrong | ocrmypdf --force-ocr |
Thresholds are set against a measured corpus, not guessed; see the module docstring for the signals and the reference numbers.
Usable as a library or on its own:
textquality FILE.pdf [FILE.pdf ...] [--json] [--quiet]from textquality import score_pdf, STATE_CORRUPT
r = score_pdf("scan.pdf")
if r["state"] == STATE_CORRUPT:
subprocess.run(["ocrmypdf", *r["ocr_args"], src, dst])splitmarks.py imports it optionally: a standalone copy of the script with no
textquality.py beside it keeps the older density-only behaviour rather than
failing.
Bookmark titles are sanitized for use as filenames:
- Spaces and unsafe characters (
/\:*?"<>|) are replaced with hyphens - Unicode is normalized
- Long names are truncated at word boundaries (max 200 chars)
- Duplicate names get a counter:
Title.pdf,Title-1.pdf,Title-2.pdf - With
--no-clobber: case number prefix uses underscore:12345678_Bench-Memo.pdf
Standalone executables: No dependencies required.
Install from source: Python 3.10+ and pypdf >= 4.0.0
--check-text only: pdftotext (poppler) on PATH. If it is absent the check
degrades to "can't check, assume OK" rather than failing the run.
On a fresh clone, activate the local pre-push sensitive-content check:
git config --local core.hooksPath .githooksIt scans commits being pushed for likely ND court dockets, confidential-case
captions, and committed binaries. Bypass once with git push --no-verify.