MermaidX — Mermaid Diagram Converter for Python
Convert Mermaid diagrams to SVG, PNG, PDF, or ASCII art — fully offline and fast, just pip install mermaidx.
Completely browserless. No Node.js, no npm, no Chrome, no system packages, nothing to compile. And because there's no browser to boot, mermaidx renders noticeably faster than the official mermaid-cli, which drives a real headless Chrome through Puppeteer for every single diagram — mermaidx uses a fast, embedded JS engine instead.
Installation
mermaidx is published on PyPI as prebuilt wheels. It is entirely contained within the Python ecosystem — zero system dependencies, no system Node/Mermaid binaries, no browser, and no C/C++ compilers required.
Base Install
SVG, PNG, PDF, and ASCII output work fully out of the box with zero extra configuration:
pip install mermaidx
Optional Extras
Everything beyond the base installation is opt-in via standard PyPI extras:
| Command | Adds |
|---|---|
pip install mermaidx |
Base install — default backend="quickjs", zero system dependencies |
pip install mermaidx[v8engine] |
Real V8 engine (mini-racer) as a selectable backend="v8" (faster JIT) |
pip install mermaidx[rust] |
Native-Rust backends via mmdr — backend="merman", backend="mermaid-rs-renderer" |
pip install numpy |
Adds .numpy() export support (standard package dependency) |
pip install mermaidx[all] |
Every extra above (v8engine + rust + numpy) in one go |
- V8 Speedup & Behavior: V8 renders faster than QuickJS by leveraging JIT compilation. It runs in an isolated child process to ensure 100% memory reclamation.
Available Backends
mermaidx normalizes every backend under a single unified API (DiagramBase). Check currently available backends programmatically at runtime:
import mermaidx
print(mermaidx.backends())
# ['quickjs'] # Base install
# ['quickjs', 'v8'] # with mermaidx[v8engine]
# ['quickjs', 'merman', 'mermaid-rs-renderer'] # with mermaidx[rust]
Backend Comparison Table
| Backend | Engine Type | Features & Performance |
|---|---|---|
quickjs (default) |
Embedded JS (QuickJS-ng) | Lightweight, zero-config, always available. Runs real mermaid.js in-process. Safe for all diagram types (including mindmap). |
v8 |
Real V8 JIT (mini-racer) |
Faster JIT execution. |
merman |
Native Rust (mmdr) |
Pure Rust implementation — no JavaScript engine involved.² |
mermaid-rs-renderer |
Native Rust (mmdr) |
Alternative pure-Rust renderer from mmdr.² |
² Every backend shares the same mermaidx DiagramBase — only svg() differs per backend, everything downstream of it (PNG/PDF/raw/numpy) is the same novasvg + PDF-writer pipeline for all of them, so PDF/raw/numpy come along for free here too. |
# Unified interface regardless of backend:
d = mermaidx.render(source, backend="merman")
d.pdf() # Works flawlessly, bridging mmdr's SVG into mermaidx's PDF engine
There isn't really an equivalent to this in the Python ecosystem. Every other Mermaid-to-image tool reachable from Python — including the official mermaid-cli itself — works by driving an actual browser (Puppeteer/Chrome) or shelling out to a separate Node.js process. mermaidx is the only one that renders real, current Mermaid JS with no browser and no subprocess at all.
Looking for
mmdc? That was this project's old name, renamed tomermaidx— see History for why.mmdcis no longer published or maintained under that name; installmermaidxinstead.
Why mermaidx?
The official Mermaid CLI (@mermaid-js/mermaid-cli) works by spinning up a real headless Chrome via Puppeteer for every render. That works, but a full browser is slow to start and heavy to install (~170MB+ of Chromium), which shows up directly in wall-clock time — especially in CI pipelines rendering many diagrams, or anything short-lived like a serverless function.
mermaidx renders the actual, current Mermaid v11 JS library — not a reimplementation, not a subset — but runs it inside a small embedded JavaScript engine instead of a browser. No browser process to spawn, no page to load, no DOM to boot — just the JS engine running Mermaid's own layout code directly. That's the whole speed difference in one sentence: browser vs. no browser.
Quick Start
import mermaidx
d = mermaidx.render("""
graph TD
A[Install] --> B[Import]
B --> C[Convert]
C --> D[Done]
""")
d.save("diagram.svg")
d.save("diagram.png", scale=2.0)
d.save("diagram.jpg", quality=90) # also .jpeg / .bmp / .tga
d.save("diagram.pdf", pdf_format="A4")
print(d.ascii())
mermaidx -i diagram.mermaid -o diagram.svg
mermaidx -i diagram.mermaid -o diagram.png --scale 2.0
cat diagram.mermaid | mermaidx -i - -o diagram.pdf
Jupyter
A Diagram displays automatically as the last expression in a cell — no extra code needed, the same way a DataFrame or a matplotlib figure does:
import mermaidx
mermaidx.render("flowchart LR; A-->B-->C") # just shows up
.show() displays it explicitly (e.g. inside a loop) — it always shows exactly what .svg() returns, so what you see is this package's own render output, not a separate re-render through some other engine.
See examples/jupyter_demo.ipynb for a full walkthrough: display, PNG/PDF/ASCII export, themes, and batch rendering.
How It Works
flowchart LR
A[Mermaid source] --> B["QuickJS-ng (or V8, optional)"]
B -->|"mermaid.js v11 (bundled)"| C[SVG]
C --> D[novasvg]
D --> E[PNG]
C --> F["hand-written PDF writer<br/>(stdlib only)"]
F --> G[PDF]
H[bundled DejaVu Sans] -.font metrics.-> B
H -.same font, forced.-> D
Everything happens in one process, no subprocess, no I/O — with one deliberate exception, backend="v8", noted below.
- SVG — mermaid.js runs inside QuickJS-ng (default, in-process) or, optionally, real V8 (
backend="v8", in its own child process) against a minimal fake DOM/SVG implementation. The one thing a fake DOM can't fabricate — real text metrics (getBBox/getComputedTextLength) — is bridged back into Python (QuickJS) or reproduced from a precomputed per-glyph advance-width table plus a sparse ASCII kerning table (V8) — both backed by novasvg's own font stack and the same bundled font. - PNG — the SVG is rasterized by novasvg, forced to use that same bundled font, so what mermaid measured during layout is exactly what gets painted. Unlike the resvg this project used before (retired in this version), novasvg paints
<foreignObject>HTML labels directly, so mermaid.js's own default labels reach the pixels unmodified. - JPEG / BMP / TGA — the same rasterized bitmap, encoded by novasvg's own encoders (
Bitmap.to_jpg()/.to_bmp()/.to_tga()) — no Pillow, no temp file. JPEG has no alpha channel, so.jpg()/.save("x.jpg")default to an opaque white background instead of novasvg's usual transparent (which would otherwise come out black). - PDF — a small hand-written PDF writer (stdlib
zlib/structonly) embeds the rendered pixels directly. No Pillow, no Cairo, no reportlab — every mainstream "put an image in a PDF" library pulls in Pillow as a transitive dependency; this avoids that entirely. - ASCII — a completely separate, lightweight path via termaid (pure Python, ~700KB, zero dependencies), which parses the Mermaid source itself rather than going through the SVG.
Rendering is CPU-bound, synchronous — there's no browser to wait on, so there's nothing for async to usefully overlap. See mermaidx.render_many() below for real parallelism instead.
Every backend is a small subclass of one shared DiagramBase — Diagram for 'quickjs'/'v8', DiagramRust for anything from the optional mmdr package. Subclasses only override the private _svg() hook; the public, cached svg()/png()/pdf()/raw()/numpy()/ascii()/save() are all written once in the base class and work identically regardless of which backend produced the SVG.
Python API
render(source, backend=None, **opts) -> Diagram
import mermaidx
d = mermaidx.render("flowchart LR; A-->B-->C")
render() itself does nothing but store the source — every Diagram method below is lazy and cached: nothing is computed until you call it, and calling it again with the same arguments returns the memoized result instead of recomputing.
| Method | Returns | Notes |
|---|---|---|
.svg() |
str |
Computed on first call, cached after |
.png(width?, height?, scale?, background?) |
bytes |
Aspect ratio always preserved |
.pdf(pdf_format?, pdf_landscape?, pdf_margin?, width?, height?, scale?, background?) |
bytes |
pdf_format=None (default) fits the page to the diagram |
.ascii(**opts) |
str |
Renders straight from the Mermaid source, doesn't need .svg() first |
.raw(width?, height?, background?) |
(bytes, w, h) |
Raw RGBA8888, no imaging library involved |
.numpy(width?, height?, background?) |
np.ndarray |
(H, W, 4) uint8; requires numpy |
.save(path, format=None, ...) |
None |
Format from format=, or inferred from the extension otherwise |
._repr_svg_() |
str |
Automatic inline rendering in Jupyter/IPython |
d.svg() is d.svg() # True -- second call is a cache hit, not a re-render
d.png(width=1200, background="#ffffff")
d.raw() # (bytes, width, height) -- RGBA8888
d.numpy() # np.ndarray, no Pillow needed
d.ascii() # ASCII/Unicode box-drawing art
d.save("out.pdf", pdf_format="A4", pdf_margin="1cm")
save(): format from the extension, or forced explicitly
d.save("out.svg") # -> svg
d.save("out.png") # -> png
d.save("out.jpg") # -> jpg (also .jpeg; opaque white background by default)
d.save("out.bmp") # -> bmp
d.save("out.tga") # -> tga
d.save("out.pdf") # -> pdf
d.save("out.txt") # -> ascii
d.save("out.whatever", format="png") # force a format regardless of extension
d.jpg(quality=90) # bytes, same defaults as .save(".jpg")
d.bmp(); d.tga() # bytes
Themes, config, CSS
mermaidx.render(source, theme="dark") # "default" | "forest" | "dark" | "neutral"
mermaidx.render(source, config={"flowchart": {"curve": "basis"}})
mermaidx.render(source, css=".node rect { rx: 8; ry: 8; }")
Parallel batch rendering
Rendering is pure CPU work — no I/O to overlap, so real concurrency means real processes, not async:
diagrams = mermaidx.render_many(sources, workers=4, theme="dark")
for d, name in zip(diagrams, output_names):
d.save(name)
Each worker process starts its own persistent engine once and reuses it for every diagram routed to it.
ASCII / terminal output
Works out of the box — termaid (pure Python, ~700KB, zero dependencies of its own) is a core dependency, not an optional extra:
print(mermaidx.render_ascii("graph LR; A-->B-->C"))
# or, equivalently: mermaidx.render("graph LR; A-->B-->C").ascii()
┌───┐ ┌───┐ ┌───┐
│ A ├───►│ B ├───►│ C │
└───┘ └───┘ └───┘
Low-level utilities
Rasterize any SVG string directly, without going through render():
from mermaidx import svg_to_png, svg_to_raw
svg = open("diagram.svg").read()
png = svg_to_png(svg, width=1200, background="#ffffff")
raw, w, h = svg_to_raw(svg)
CLI
# SVG to stdout (no -o needed)
mermaidx -i diagram.mermaid
cat diagram.mermaid | mermaidx -i -
# save to file (format from extension)
mermaidx -i diagram.mermaid -o diagram.svg
mermaidx -i diagram.mermaid -o diagram.png
mermaidx -i diagram.mermaid -o diagram.pdf
# size
mermaidx -i diagram.mermaid -o diagram.png -w 1200
mermaidx -i diagram.mermaid -o diagram.png --scale 2.0
# theme & background
mermaidx -i diagram.mermaid -o diagram.svg --theme dark
mermaidx -i diagram.mermaid -o diagram.png --background "#f5f5f5"
# PDF options
mermaidx -i diagram.mermaid -o diagram.pdf --pdf-format A4 --landscape --margin 1cm
# config & CSS
mermaidx -i diagram.mermaid -o diagram.svg --config config.json --css style.css
# suppress the "saved to..." message
mermaidx -i diagram.mermaid -o diagram.svg -q
# info — Mermaid library version
mermaidx --info
# list available backends
mermaidx --list-backends
# pick a backend explicitly (quickjs/v8 always/optionally available; anything
# else requires mermaidx[rust])
mermaidx -i diagram.mermaid -o diagram.svg --backend merman
# version
mermaidx --version # or -v
Supported Diagram Types
Everything Mermaid v11 itself supports (this bundles the real library, not a subset): flowcharts, sequence diagrams, class diagrams, state diagrams, ER diagrams, Gantt charts, pie charts, git graphs, and more.
Requirements
- Python 3.9+
quickjs-ng,novasvg,termaid(installed automatically)mini-racer(optional,pip install mermaidx[v8engine], for the V8 engine)- No system packages, no Node.js, no npm, no browser
Testing
pip install -e ".[test]"
pytest tests/ -v
History
- **mmdc, powered by phasma** — the original version of this project. It bundled a real (if small — around 20MB) headless browser, PhantomJS, and exposed an
asyncPython API to match: rendering meant talking to a subprocess, soasync/awaitgenuinely mattered for concurrency. - 0.6.x — a full rewrite: PhantomJS's engine couldn't parse modern Mermaid (v11's ES2022+ syntax) at all, so the whole browser was replaced with mermaid.js running inside QuickJS-ng against a hand-written DOM/SVG shim, with resvg for rasterization (replaced by novasvg in a later release). No subprocess left to wait on, so the API became synchronous. Three backends appeared:
js(this engine), plusmermanandmermaid-rs-renderervia the optionalmmdrpackage. - 0.7.x, renamed to mermaidx — same engine, new name. The old name,
mmdc, was identical to the official Mermaid CLI's own binary name (@mermaid-js/mermaid-cliinstalls a command calledmmdc) — a real collision, not just a branding concern. Renamed early, while it still could be. If you havemmdcpinned anywhere (requirements.txt, a Dockerfile, CI config), switch it tomermaidx— the old name isn't maintained or published anymore. - Later 0.7.x — the embedded-JS-engine backend split into
mermaidx/engines/(quickjs_engine.py/v8_engine.py), and gained an optional real-V8 path (pip install mermaidx[v8engine],backend="v8") alongside the original QuickJS-ng one (backend="quickjs", still the default) — same mermaid.js, same output, just a choice of engine. - novasvg release — resvg retired; novasvg is now the sole rasterizer and the text-measurement engine (
font_metrics.pyno longer hand-parses TrueType tables). Because novasvg paints<foreignObject>directly, mermaid.js's default HTML labels are used again (htmlLabels: true), with their box sizes computed by novasvg itself (novasvg.fonts.measure_foreign_object) so layout and paint can't disagree.
Acknowledgments
- Mermaid — the actual diagramming library this project renders.
mermaidxwouldn't exist without it; all it does is run the real thing somewhere a browser can't go. - mmdr — a native-Rust Mermaid renderer by the same author, usable as an additional backend here.
- termaid — powers ASCII/Unicode terminal output.
Contributing
- Fork and create a feature branch
- Add tests for new functionality
- Run
pytest tests/— all must pass - Open a pull request
License
MIT — see LICENSE for details.
If mermaidx saves you from booting a headless Chrome instance a hundred times in CI, consider leaving a ⭐ on the repo — it genuinely helps others find the project.
Metadata
Release files for mermaidx 0.10.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| mermaidx-0.10.0-py3-none-any.whl | Python 3 | none | any | Details |
Release files / mermaidx-0.10.0-py3-none-any.whl
| Download URL | mermaidx-0.10.0-py3-none-any.whl |
|---|---|
| Size | 1.8 MB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
4139a75300f76045358d0472548d9bc8ca0ea16eb113048ae7c3b4516966e8af
|
|
BLAKE2b-256 checksum How to use checksums |
921f838dc87a39a04ea379cb4d1b614b53e2e589a1d0424ef6deb2816c65ad84
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Security for mermaidx 0.10.0
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.