this_file: README.md
vexy-pdf3md
Convert one PDF or an entire folder tree to Markdown. Choose a converter, let the package select one, or run several strategies side by side. Failed conversions try another eligible tool; results record every attempt and the actual winner.
Install
uv sync
uv run vexy-pdf3md --help
# Install this checkout as a CLI available outside the repository:
uv tool install .
Python 3.12+ is required. PyMuPDF provides a working text-layer backend immediately. Optional tools are discovered on PATH:
- markit: install from upstream instructions. Preferred for fast text-layer conversion and multi-column layouts.
- pdf22md: build/install pdf22md on macOS for PDFKit extraction and Apple Vision OCR.
- Tesseract:
brew install tesseracton macOS; install the corresponding package on other systems. Extra languages:brew install tesseract-lang. - Docling:
uv sync --extra docling, then useuv run --extra docling .... The adapter uses Tesseract for OCR and may download layout/table models on first use. For an installed CLI, useuv tool install '.[docling]'from this checkout.
Convert
# book.pdf -> book.md, beside the PDF
uv run vexy-pdf3md --input book.pdf
# Explicit output file, with images in ./results/renamed/
uv run vexy-pdf3md --input book.pdf --output results/renamed.md --media
# Recursively map PDFs to Markdown, preserving relative subfolders
uv run vexy-pdf3md --input ./pdfs --output ./markdown --workers=4
# Omit --output to write Markdown alongside PDFs throughout the input tree
uv run vexy-pdf3md --input ./pdfs
# Run independent strategies concurrently
uv run vexy-pdf3md --input book.pdf --strategies=markit,pymupdf --media
# OCR, selected pages, chosen language, and full-page images (including vectors)
uv run vexy-pdf3md --input scan.pdf --strategies=tesseract \
--pages=1-3,5 --language=eng+pol --media=pages --dpi=200
# Use Docling for layout/table reconstruction
uv run --extra docling vexy-pdf3md --input book.pdf --strategies=docling
pdf3md and python -m vexy_pdf3md expose the same Fire CLI. Single-file input
accepts an explicit .md filename or an existing output directory. Directory input
always treats --output as a directory, even if its name ends in .md.
Output contract
pdfs/part/book.PDF
--output markdown
-> markdown/part/book.md
--output markdown --strategies=markit,pymupdf --media
-> markdown/part/book.markit.md
markdown/part/book.markit/page-0001-001.png
-> markdown/part/book.pymupdf.md
markdown/part/book.pymupdf/page-0001-001.png
Suffixes are added only when multiple strategies are requested. They identify the
requested strategy, even if a fallback produces that output; JSON reports identify
the actual backend. Use --fallback=False for strict backend comparisons.
--media / --media=images exports displayed raster-image regions as PNGs, appended
under a Media heading in page order. --media=pages renders each complete selected
page, including vector artwork. Media links are relative and URL-escaped. Original
embedded image formats, editable vectors, and exact inline placement are not preserved.
The media directory uses the complete Markdown stem and is created even if no images
are found; .vexy-pdf3md marks it as generated.
Existing Markdown is skipped by default. --overwrite=True replaces it only after
conversion passes quality checks. Existing media directories are replaceable only if
they carry this package's ownership marker. Keep curated files outside generated media
directories. Case-insensitive destination collisions are rejected before processing.
Directory symlinks are not followed and PDF symlinks within trees are skipped; an
explicit single-file symlink is resolved to its target.
Selection and fallback
- Text-layer PDFs: markit → pymupdf → pdf22md → tesseract → docling.
- Suspected scanned/mixed PDFs: pdf22md → tesseract → docling.
- Only installed candidates participate in automatic selection. An explicitly named missing backend produces a failed attempt before fallback.
- Native crashes, timeout, nonzero exit, empty output, replacement-character corruption, and substantial text loss trigger fallback. The default retention threshold is 50% of the PDF's alphanumeric text layer, before media is appended.
- All selected pages are inspected. A document is routed to OCR only when at least
20% of the selected pages look scanned; a lone screenshot page in a long text document
produces a warning instead. Detection and length checks are heuristics; they cannot
prove complete or correct OCR. Use
--ocr=alwayswhen automatic routing misses scanned content.--ocr=neverdeliberately permits text-layer-only extraction. - With several strategies requested, a failed strategy never falls back to one of its siblings: those already get their own output, so the file would be a duplicate under a misleading suffix. A warning names the actual backend whenever it differs.
PyMuPDF's basic fallback preserves text blocks and infers larger-font headings. It does not reconstruct sophisticated tables. For difficult layouts, compare outputs or request Docling. Image-only documents still require usable OCR text: exporting page images alone does not count as successful Markdown conversion.
Engine comparison
Measured on 123 Microsoft localization style guides (text-layer PDFs, 30–100 pages, 59 in non-Latin scripts), each converted with every strategy. Only English OCR models were installed, so OCR ratings cover text-layer input, not scans.
| Strategy | Rating | Observed |
|---|---|---|
| markit | ★★★★☆ default | Tables, reflowed paragraphs, running headers/footers dropped. Loses 4–22% of Greek and Indic letters (conjuncts, pre-base vowels), joins hyphenated words, shows 312x90pt image placeholders, may emit U+FFFD for fonts without Unicode maps. |
| pymupdf | ★★★☆☆ most complete text | Keeps every mapped glyph, font-size headings, page footers, and hard line breaks. No tables. Missed inline Cyrillic runs in one Serbian guide that markit recovered. |
| pdf22md | ★★☆☆☆ | Text equals PyMuPDF's layer, but one line per text run, no headings or tables, footers kept. Vision OCR turned Cyrillic into Latin lookalikes when routing forced OCR. |
| tesseract | ★☆☆☆☆ on text PDFs | About 5% of words lost and 8% garbage on Latin scripts; non-Latin scripts unreadable without language packs. Only for real scans. |
| docling | not rated | Not installed during the run: all 123 .docling.md files were byte-identical markit fallbacks, which prompted the sibling-fallback rule above. |
Best choice per guide: markit for 110 documents, PyMuPDF for the Greek and ten Indic guides where markit dropped script letters, and for two Bengali/Konkani guides whose fonts lack Unicode maps. One Kyrgyz guide was misrouted to OCR by a single screenshot page; the page-share threshold above fixes that.
Options
| Option | Default | Meaning |
|---|---|---|
--strategies |
auto |
One tool or comma-separated independent variants |
--workers |
2 |
Maximum simultaneous isolated jobs, 1–64 |
--timeout |
600 |
Seconds per inspection, backend attempt, or media export |
--fallback |
True |
Try alternative eligible converters |
--media |
False |
True/images, pages, or False/none |
--overwrite |
False |
Replace Markdown and owned generated media |
--pages |
all | One-based ranges, e.g. 1-3,5, sorted and deduplicated |
--password |
empty | Password for encrypted PDFs; omitted from reports |
--ocr |
auto |
auto, always, or never |
--language |
eng |
Tesseract/Docling codes joined with + |
--vision-languages |
en |
pdf22md Vision codes joined with commas |
--dpi |
150 |
OCR and media resolution, 72–600 |
--min-text-ratio |
0.5 |
Minimum retained text-layer fraction, 0–1 |
--verbose |
False |
Backend diagnostics on stderr |
Timeouts apply per operation, so fallback can extend total time. POSIX timeout and Ctrl-C terminate the worker process group, including converter subprocesses. On Windows only the direct worker is terminated. Separate invocations should not overwrite the same destinations concurrently.
Python API and exit codes
from vexy_pdf3md import convert
batch = convert("pdfs", "markdown", strategies="auto", media=True, workers=4)
for item in batch.results:
print(item.input, item.output, item.status, item.strategy)
for attempt in item.attempts:
print(attempt.strategy, attempt.status, attempt.error)
assert batch.ok
The CLI prints a JSON batch report to stdout. Exit status 0 means every output
succeeded or was skipped, 1 means at least one conversion failed, and 2 means
invalid configuration/input arguments. A bad PDF does not stop other jobs. API argument
and path errors raise; individual document failures are returned in BatchResult.
No cloud OCR or AI service is invoked by this package.
Develop
./test.sh # lint, types, 80% coverage gate, smoke
uv run --extra docling pytest -m optional # real Docling + model integration
uvx hatch test # isolated package test environment
uv build # Git-derived version, wheel and sdist
Publish
./publish.sh # commit changes, tag, push, build, uv publish
PUBLISH_SKIP_UPLOAD=1 ./publish.sh # same Git release, skip only the PyPI upload
hatch-vcs derives versions from Git tags and generates the gitignored
src/vexy_pdf3md/__version__.py during builds/installs. Rebuild or reinstall after
changing tags to refresh editable-install versions; never commit the generated file.
publish.sh validates before and after uvx gitnextver, which stages and commits
all nonignored changes, creates the next vX.Y.Z tag, and pushes configured remotes.
Generated version files are cleaned; source changes are committed, not discarded.
The script checks the clean tree, exact tag, and remote push, then builds fresh artifacts
and compares wheel/sdist/generated versions to the tag before calling uv publish.
Only the newly built wheel and sdist are uploaded; copies remain in dist/.
Configure uv publishing credentials, e.g. UV_PUBLISH_TOKEN, through your environment.
Arguments to publish.sh are forwarded to uv publish. A clean tagged checkout can
retry a failed upload. A clean untagged checkout is rejected because gitnextver
requires changes to create a tag. Validation installs all extras but does not run the
optional model-download integration by default. A failed push, test, build, or version
check stops the upload.
See ANALYSIS.md for both source-project investigations and routing tradeoffs, DEPENDENCIES.md for dependency roles/licenses, and WORK.md for verification evidence. The package's own code is Apache-2.0; dependencies have their own licenses, including PyMuPDF's AGPL/commercial terms.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file vexy_pdf3md-1.0.2.tar.gz.
File metadata
- Download URL: vexy_pdf3md-1.0.2.tar.gz
- Upload date:
- Size: 199.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via:
uv/0.12.3 {"installer":{"name":"uv","version":"0.12.3","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
cceb43da006171e8f96bca19f24f69b437b4ab1264474ccbc797165ca6906cf7
|
|
| MD5 |
da77b2926ca716fa5e3a9a7ab179b7b4
|
|
| BLAKE2b-256 |
d53c4bc9b61c46e40c75b980f5e449f33739ebcf5dc154d6bad3419344ea76a3
|
File details
Details for the file vexy_pdf3md-1.0.2-py3-none-any.whl.
File metadata
- Download URL: vexy_pdf3md-1.0.2-py3-none-any.whl
- Upload date:
- Size: 25.8 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via:
uv/0.12.3 {"installer":{"name":"uv","version":"0.12.3","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
775ae6d4bb0e52039f6d119f44fc104a82c326035c7e1333192093977c2eca52
|
|
| MD5 |
cee327035c08683aff3c42270134e835
|
|
| BLAKE2b-256 |
eb288c19c8cdc97b6e9d26f8a95074bbff9ebfb34b3b28299a220dbcfea8fb5b
|