simpdf
simpdf is a small Python library for rendering Markdown into PDF with fpdf2, while keeping font handling explicit so Cyrillic and other non-Latin text work reliably with external TTF fonts.
The library is centered on a MarkdownPdfRenderer class. You give it a directory with TTF files, describe the font face to register, and then render Markdown into PDF bytes or directly into a file.
Features
- Uses
fpdf2as the PDF backend - Supports external TTF fonts and Cyrillic-friendly fonts such as DejaVu Sans
- Provides a helper to download DejaVu Sans fonts into a target directory
- Supports markdown images from filesystem paths,
data:URLs, remote URLs, and custom callbacks - Class-first API with optional convenience helpers
- Minimal CLI for rendering Markdown and downloading DejaVu fonts
- Supports these Markdown elements in v1:
- headings
- paragraphs
- bold and italic text
- inline code
- ordered and unordered lists
- tables
- blockquotes
- fenced code blocks
- thematic breaks
- clickable links
- block images
Installation
pip install simpdf
For development and tests:
pip install -e .[dev]
Quick Start
from simpdf import FontFace, MarkdownPdfRenderer
renderer = MarkdownPdfRenderer(
font_directory="fonts",
font_face=FontFace.dejavu_sans(),
)
markdown_text = """
# Example
Привет, мир.
- one
- two
"""
pdf_bytes = renderer.render_to_bytes(markdown_text)
with open("output.pdf", "wb") as handle:
handle.write(pdf_bytes)
Downloading DejaVu Fonts
The library does not bundle fonts inside the wheel. Use the download helper to populate your own font directory.
from simpdf import download_dejavu_fonts
downloaded = download_dejavu_fonts("fonts")
print([path.name for path in downloaded])
The helper downloads the DejaVu font files from public GitHub repository.
Using Custom Fonts
You can use any TTF family as long as you provide at least a regular face. Bold, italic, and bold-italic are optional; if omitted, the regular face is reused.
from simpdf import FontFace, MarkdownPdfRenderer
renderer = MarkdownPdfRenderer(
font_directory="my-fonts",
font_face=FontFace(
family="NotoSansCustom",
regular="NotoSans-Regular.ttf",
bold="NotoSans-Bold.ttf",
italic="NotoSans-Italic.ttf",
bold_italic="NotoSans-BoldItalic.ttf",
),
)
Font file values may be file names relative to font_directory or absolute paths.
Formatting Options
Formatting is configured with a plain nested dictionary. Any omitted value falls back to the library defaults.
Example:
from simpdf import FontFace, MarkdownPdfRenderer
renderer = MarkdownPdfRenderer(
font_directory="fonts",
font_face=FontFace.dejavu_sans(),
formatting_options={
"text": {"font_size": 11},
"headings": {
"sizes": {1: 26, 2: 20, 3: 16},
},
"lists": {"indent": 9},
"table": {"heading_font_size": 13},
},
)
Supported option groups:
page: page size, orientation, and marginstext: base font size, line height multiplier, text colorheadings: per-level sizes and spacingparagraph: paragraph spacinglists: indent, bullet symbol, list spacingblockquote: indent, bar styling, text colortable: font sizes, padding, minimum column width, spacingcode_block: font size, padding, colors, spacinginline_code: inline code text colorimages: block image sizing, spacing, alignment, table-cell best-effort heightlinks: link color and underline togglethematic_break: rule color, width, spacing
Image Support
Markdown images use the normal syntax:

Supported image sources in 0.1.1:
- custom callback
- filesystem paths
data:URLs with embedded image data- remote
http/httpsURLs
Filesystem Images
If you do nothing, filesystem images are resolved relative to the current working directory. If you want a different base directory, pass image_base_dir.
from simpdf import FontFace, MarkdownPdfRenderer
renderer = MarkdownPdfRenderer(
font_directory="fonts",
font_face=FontFace.dejavu_sans(),
image_base_dir="docs-assets",
)
markdown_text = ""
pdf_bytes = renderer.render_to_bytes(markdown_text)
Custom Callback
The callback receives (alt_text, source) and may return raw bytes, a binary stream, or a PIL.Image.Image when Pillow is installed. Returning None falls through to the built-in resolvers.
from io import BytesIO
from simpdf import FontFace, MarkdownPdfRenderer
def image_callback(alt_text: str, source: str):
if source == "custom://badge":
return BytesIO(open("badge.png", "rb").read())
return None
renderer = MarkdownPdfRenderer(
font_directory="fonts",
font_face=FontFace.dejavu_sans(),
image_resolver=image_callback,
)
Layout Behavior
- Images render as standalone block elements.
- If a paragraph mixes text and images, the renderer outputs the text and image blocks in order.
- Images inside table cells are best-effort in
0.1.1.- image-only cells are rendered as images
- mixed text+image cells fall back to text rendering using the image alt text
Convenience Helpers
If you prefer a functional call site, simpdf also exports:
render_markdown_to_pdf_bytes(...)render_markdown_to_pdf_file(...)
These helpers internally construct MarkdownPdfRenderer.
CLI Usage
Render Markdown into PDF:
simpdf render input.md output.pdf \
--fonts-dir ./fonts \
--family-name DejaVuSans \
--font-regular DejaVuSans.ttf \
--font-bold DejaVuSans-Bold.ttf \
--font-italic DejaVuSans-Oblique.ttf \
--font-bold-italic DejaVuSans-BoldOblique.ttf \
--image-base-dir ./images
Download DejaVu fonts:
simpdf download-dejavu ./fonts
If you want custom formatting from the CLI, pass a JSON file with --options-file.
Examples
See examples/basic_render.py, examples/custom_font_and_style.py, examples/rich_markdown.py, examples/images_from_filesystem.py, and examples/images_custom_callback.py.
Tests
The repo now contains a pytest suite that covers:
- font config validation
- DejaVu download helper behavior
- markdown token flattening and table extraction
- image resolvers and image rendering
- PDF rendering with Cyrillic content
- formatting overrides
- CLI render and download flows
Run tests with:
pytest
Compatibility Note
For older code that imported simpdf.pdfgen, a small compatibility wrapper is still present. The recommended API is the class-based renderer from simpdf.
Metadata
Release files for simpdf 0.1.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| simpdf-0.1.1.tar.gz | 22.7 kB | Without provenance | Details for simpdf-0.1.1.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| simpdf-0.1.1-py3-none-any.whl | Python 3 | none | any | Details for simpdf-0.1.1-py3-none-any.whl |
Total release size: 41.1 kB
Release files / simpdf-0.1.1.tar.gz
| Download URL | simpdf-0.1.1.tar.gz |
|---|---|
| Size | 22.7 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
1b520f7d80fcf3a63767674cb653e54ecf25f764e621fce9c99893e1cffae499
|
|
BLAKE2b-256 checksum How to use checksums |
3c1ea4ada1db913d88a22ca15fe12d72e5b3495203f826bdc641f2001d1db54f
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.12.0
|
Release files / simpdf-0.1.1-py3-none-any.whl
| Download URL | simpdf-0.1.1-py3-none-any.whl |
|---|---|
| Size | 18.5 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
e2276e69293647b04db11db7277879e92b39a9b7e09028f2793fdf6d0f22d8c1
|
|
BLAKE2b-256 checksum How to use checksums |
459e6dbb641ba55a0ede4543d36d95b4a97645fde1a1b979201cac289371193d
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.12.0
|
Security for simpdf 0.1.1
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.