mdtree2pdf.py recursively walks a directory tree and creates a sibling PDF
with the same base name for every lower-case *.md file. Conversion is
strictly local:
- Markdown, tables, and PDF layout:
pymd2pdf - Inline and block math: local Matplotlib Mathtext
- Mermaid: optional local
mmdc - PlantUML: optional local
plantuml
If a diagram renderer is unavailable or reports an error, the PDF contains a
visible source placeholder. Markdown and diagram contents are never sent to
Kroki or another service. Diagrams containing http:// or https://
references are also rendered as placeholders so optional local renderers
cannot download external content.
- Python 3.11 or newer
- uv
All Python dependencies are declared directly in the script as PEP 723
metadata. A separate virtual environment or pip install step is not
required. The checked-in mdtree2pdf.py.lock records the resolved dependency
versions and is used automatically by uv when running the script. Keep it
alongside the script when copying MarkTree to another directory.
# Process the current directory recursively
./mdtree2pdf.py
# Process another directory tree or a single file
./mdtree2pdf.py /path/to/documents
./mdtree2pdf.py report.md
# Preview planned actions
./mdtree2pdf.py --dry-run
# Rebuild every PDF
./mdtree2pdf.py --force
# Choose a page orientation, rebuilding existing PDFs
./mdtree2pdf.py --orientation portrait report.md
./mdtree2pdf.py --orientation landscape report.md
./mdtree2pdf.py --orientation auto /path/to/documents
# Exclude additional files or directories
./mdtree2pdf.py --exclude 'drafts/**' --exclude 'private.md'Full command-line help:
./mdtree2pdf.py --helpThe scanner excludes .git, .venv, venv, node_modules, and
__pycache__ by default. Directory symlinks are not followed.
An existing PDF is normally replaced only when its Markdown source or the
explicitly supplied configuration file is newer. Specifying --orientation
or --force rebuilds existing PDFs. Use --force after changing embedded
images, Markdown includes, or switching to an older configuration file.
PDFs are written atomically, so an existing target remains intact if rendering fails.
Use --orientation portrait, --orientation landscape, or --orientation auto.
The CLI option overrides orientation in the explicitly supplied --config
file. If neither specifies an orientation, the default is auto. Existing
configuration files that specify portrait or landscape keep that behavior.
Automatic selection chooses one orientation for the entire PDF, independently for each Markdown file. It measures parsed tables after preprocessing, using the renderer's column widths, cell padding, fonts, and text wrapping. Page size, theme settings, and explicit column width hints participate in the measurement. Table-like text inside code fences is ignored.
Portrait is preferred. Landscape is selected when it lets a tall row fit, reduces table width overflow, or reduces the width deficit for unbroken words by at least 25%, with at least one font-size unit of deficit in portrait. For tables with at least three columns and a row at least six text lines high, a reduction in total table height of at least 25% also selects landscape. Every table's tallest row and its repeated header must still fit within the shorter landscape page.
This is a table-readability heuristic, not a guarantee of optimal pagination. It does not select orientation based on images, diagrams, or code blocks, and does not mix portrait and landscape pages in one PDF. Very wide tables or oversized rows may still need a larger page size, different column widths, or source restructuring. Manual orientation always takes precedence.
Each conversion prints a LAYOUT line with the chosen orientation and reason.
Preview the decisions for all files, including existing PDFs, without writing PDFs:
./mdtree2pdf.py --orientation auto --dry-run /path/to/documentsUse --orientation auto once to apply automatic layout to PDFs created by
older MarkTree versions; otherwise up-to-date PDFs are skipped as usual.
pymd2pdf splits long tables across pages, repeats the header row, and
respects Markdown column alignment:
| Name | Count | Description |
|:-----|------:|:------------|
| Alpha | 12 | Longer text wraps cleanly inside the table cell. |Column widths can be specified in the header:
| Name {width=25%} | Description {width=75%} |
|:-----------------|:------------------------|
| Alpha | Detailed description |An md2pdf.toml file can also control the page size and orientation:
[md2pdf]
page_size = "A4"
orientation = "landscape"
toc = falseSet orientation = "auto" to use MarkTree's automatic selection with the
configured page size and theme.
Pass the configuration explicitly:
./mdtree2pdf.py --config md2pdf.toml /path/to/documentsFor safety and predictable output placement, the script always overrides the
input_file, output_file, offline, and emoji settings.
Common Markdown and LaTeX forms are supported:
Inline: $E = mc^2$
$$
\int_0^1 x^2\,dx = \frac{1}{3}
$$Matplotlib supports common mathematical expressions but is not a complete TeX distribution. Unsupported constructs are shown as labeled source text.
Mermaid fences are rendered automatically when mmdc is available in
PATH:
npm install --global @mermaid-js/mermaid-cliPlantUML fences are rendered automatically when plantuml is available in
PATH. On macOS, Homebrew installs PlantUML and its runtime dependencies:
brew install plantumlExamples:
```mermaid
flowchart LR
A[Start] --> B[End]
```
```plantuml
@startuml
Alice -> Bob: Hello
@enduml
```Successfully rendered diagrams are cached by content under
~/.cache/pymd2pdf/local-diagrams.
uv run \
--with 'pymd2pdf[matplotlib]>=0.6.2,<0.7' \
--with pytest \
--with pypdf \
pytest -qThe tests cover recursive discovery, exclusions, freshness checks, atomic writes, local diagram fallbacks, multi-page tables, offline operation, and manual/automatic orientation including page geometry, themes, width hints, includes, configuration precedence, and dry-run decisions.
Refresh the script's dependencies within the declared compatibility bounds:
uv lock --script mdtree2pdf.py --upgradeCommit the updated lockfile together with any dependency declaration changes.
See LICENSE.