Markdown-to-PDF rendering with FPDF2 and configurable TTF fonts.
Project description
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.
Project details
Release history Release notifications | RSS feed
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 simpdf-0.1.1.tar.gz.
File metadata
- Download URL: simpdf-0.1.1.tar.gz
- Upload date:
- Size: 22.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.0
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
1b520f7d80fcf3a63767674cb653e54ecf25f764e621fce9c99893e1cffae499
|
|
| MD5 |
c011354616751c7cdec67b2b5ac9c0ca
|
|
| BLAKE2b-256 |
3c1ea4ada1db913d88a22ca15fe12d72e5b3495203f826bdc641f2001d1db54f
|
File details
Details for the file simpdf-0.1.1-py3-none-any.whl.
File metadata
- Download URL: simpdf-0.1.1-py3-none-any.whl
- Upload date:
- Size: 18.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.0
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
e2276e69293647b04db11db7277879e92b39a9b7e09028f2793fdf6d0f22d8c1
|
|
| MD5 |
288ca321b423fa428b52bb0b62749c7a
|
|
| BLAKE2b-256 |
459e6dbb641ba55a0ede4543d36d95b4a97645fde1a1b979201cac289371193d
|