mermaidx
Mermaid diagrams → SVG, PNG, JPG, PDF and ASCII, from plain Python.
No browser. No Node.js. No system packages. Just pip install mermaidx.
Documentation · Quick start · How it works · Python API · CLI · Changelog
import mermaidx
d = mermaidx.render("graph TD; A[Install] --> B[Import] --> C[Convert] --> D[Done]")
d.save("diagram.svg")
d.save("diagram.png", scale=2.0)
mermaidx runs the real Mermaid v11 JavaScript library inside a small embedded JS engine instead of a headless Chrome. The result is the same SVG you would get from the official mermaid-cli, produced without spawning a browser, which makes it a good fit for CI pipelines, notebooks and serverless functions.
Highlights
- Browserless. No Chromium (~170 MB), no Puppeteer, no Node.js, no compiler. Prebuilt wheels only.
- The real Mermaid. Not a reimplementation and not a subset: every diagram type Mermaid v11 supports.
- Many outputs. SVG, PNG, JPG/JPEG, BMP, TGA, PDF, raw RGBA, NumPy arrays and terminal ASCII art.
- One process. Synchronous, in-process rendering, plus
render_many()for parallel batches. - Jupyter-ready. A diagram displays itself as the last expression of a cell.
- Pluggable backends. QuickJS-ng (default), V8 (optional) or native Rust (optional).
Installation
pip install mermaidx
Extras are opt-in:
| Command | Adds |
|---|---|
pip install mermaidx |
Base install. Default backend="quickjs", zero system dependencies. |
pip install mermaidx[v8engine] |
Real V8 (mini-racer) as backend="v8". Faster thanks to JIT, runs in an isolated child process. |
pip install mermaidx[rust] |
Native-Rust backends via mmdr: merman and mermaid-rs-renderer. |
pip install numpy |
Enables .numpy() export. |
pip install mermaidx[all] |
Every extra above (v8engine + rust + numpy). |
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())
The same from the command line:
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
In Jupyter there is nothing to call. A Diagram renders itself, like a DataFrame or a matplotlib figure:
mermaidx.render("flowchart LR; A-->B-->C") # just shows up
How it works
mermaidx has a deliberately narrow job: turn Mermaid source into an SVG that matches mermaid-cli. Everything after the SVG (PNG, JPG and friends) is novasvg's job.
flowchart TB
SRC["Mermaid source"] --> MX
SRC -.-> ASCII["ASCII art<br/>termaid, straight from the source"]
subgraph MX ["mermaidx: Mermaid to SVG"]
direction LR
JS["JS engine<br/>QuickJS-ng, or V8 (optional)"] --> MJS["mermaid.js v11<br/>bundled, unmodified"]
SHIM["DOM and SVG shims<br/>stand-ins for browser APIs"] -.-> MJS
end
MX --> SVG(["SVG<br/>same output as mermaid-cli"])
SVG --> RAS
subgraph NS ["novasvg: SVG to pixels"]
direction LR
RAS["Rasterizer<br/>paints foreignObject labels"] --> PNG["PNG"]
RAS --> OTHER["JPG, BMP, TGA, raw RGBA"]
end
RAS -.-> PDF["PDF<br/>tiny stdlib-only writer in mermaidx<br/>embeds the rendered pixels"]
| Stage | Who does it | Notes |
|---|---|---|
| Mermaid → SVG | mermaidx |
mermaid.js runs unmodified in QuickJS-ng (or V8) against a minimal fake DOM/SVG layer (the shims). Anything a real browser would answer, such as text size via getBBox, comes from novasvg's font stack and one bundled font. |
| SVG → PNG / JPG / BMP / TGA / raw | novasvg |
Same bundled font as the layout step, so what Mermaid measured is exactly what gets painted. JPEG has no alpha, so it defaults to a white background. |
| SVG → PDF | mermaidx |
A small hand-written PDF writer (stdlib zlib/struct) embeds the rasterized pixels. No Pillow, no Cairo, no reportlab. |
| Source → ASCII | termaid |
A separate lightweight path that parses the Mermaid source directly, without going through the SVG. |
Every backend is a small subclass of one shared DiagramBase and only overrides the private _svg() hook. Everything downstream (png(), pdf(), raw(), numpy(), ascii(), save()) is written once and behaves identically no matter which backend produced the SVG.
Why novasvg?
Mermaid draws its labels as HTML inside <foreignObject>. Many SVG rasterizers skip that element, so the text silently disappears or the layout breaks. mermaidx originally used resvg, which cannot paint <foreignObject>, and had to force plain SVG text as a workaround. With novasvg the default HTML labels are used again, and their box sizes are computed by novasvg itself, so layout and paint cannot disagree.
In novasvg's own benchmark (60 samples, 49 of them Mermaid diagrams, compared against Chromium as ground truth), it came out the most accurate renderer while staying among the fastest:
| Renderer | Samples that look right (of 60) | Mean RMSE vs Chromium (lower is better) | Avg render time |
|---|---|---|---|
| novasvg | 59 | 15.4 | 1.28 ms |
| resvg | 7 | 19.2 | 3.38 ms |
| lunasvg | 31 | 30.9 | 1.85 ms |
| cairosvg | 19 | 46.0 | 42.92 ms |
| nanosvg | 4 | 48.3 | 0.99 ms |
| thorvg | 6 | 76.4 | 0.64 ms |
thorvg and nanosvg are faster, but they get far more samples visibly wrong. See the benchmark page for the methodology and side-by-side renders.
Why not a browser?
The official @mermaid-js/mermaid-cli drives a real headless Chrome through Puppeteer. That works, but a browser is slow to boot and heavy to install, which shows up directly in wall-clock time for CI jobs that render many diagrams. mermaidx removes the browser and nothing else: same Mermaid library, no browser process, no page load, no DOM boot.
Every other Mermaid-to-image tool reachable from Python either drives a browser or shells out to Node.js. mermaidx is the one that renders current Mermaid with neither.
Backends
All backends share one API. List what is installed at runtime:
import mermaidx
print(mermaidx.backends())
# ['quickjs'] base install
# ['quickjs', 'v8'] with mermaidx[v8engine]
# ['quickjs', 'merman', 'mermaid-rs-renderer'] with mermaidx[rust]
| Backend | What runs | Notes |
|---|---|---|
quickjs (default) |
The original mermaid.js in QuickJS-ng | Lightweight and always available, runs in-process. Safe for every diagram type, including mindmap. |
v8 |
The original mermaid.js in V8 (mini-racer) |
Faster thanks to the JIT. Runs in an isolated child process so memory is fully reclaimed. |
merman |
A Rust reimplementation (mmdr) |
No JavaScript engine involved. Not mermaid.js. |
mermaid-rs-renderer |
A different Rust reimplementation (mmdr) |
Same idea as merman, with its own layout engine. |
flowchart TB
CALL["mermaidx.render(source, backend=...)"] --> PICK{"backend"}
subgraph JSB ["Diagram: real mermaid.js v11 + DOM/SVG shims"]
QJ["quickjs (default)<br/>QuickJS-ng, in-process<br/>text metrics via Python callback"]
V8["v8<br/>V8 via mini-racer, child process<br/>text metrics via JS-side tables"]
end
subgraph RSB ["DiagramRust: optional mmdr package"]
MM["merman<br/>native Rust"]
MR["mermaid-rs-renderer<br/>native Rust"]
end
PICK -->|"default"| QJ
PICK -->|"mermaidx[v8engine]"| V8
PICK -->|"mermaidx[rust]"| MM
PICK -->|"mermaidx[rust]"| MR
QJ --> SVG
V8 --> SVG
MM --> SVG
MR --> SVG
SVG(["svg()<br/>the only step that differs per backend"]) --> SHARED["DiagramBase, shared by all backends<br/>novasvg: PNG, JPG, BMP, TGA, raw, numpy<br/>PDF writer<br/>save(), caching"]
The API is the same for every backend: render() always returns a Diagram, and .svg(), .png(), .pdf() and .save() work identically whichever one you pick. What is not the same is how the SVG gets made, so speed and the rendered result can differ between backends:
quickjsandv8run the original mermaid.js. Same library, same diagram types, same options (theme,config,css). Only the JavaScript engine differs: V8 is faster, QuickJS installs anywhere.mermanandmermaid-rs-rendererare different implementations, written in Rust rather than JavaScript. Their layout can differ from mermaid.js, they may not support every diagram type, and they take different options (theme,node_spacing,rank_spacing,aspect_ratioinstead ofconfigandcss).
Whatever the backend, the SVG then goes through the same novasvg rasterizer, so the pixels are painted the same way. Only the SVG it receives differs.
import mermaidx
source = "flowchart LR; A[Parse] --> B[Layout] --> C[Render]"
# Original mermaid.js: same output, different JS engine
mermaidx.render(source).png() # quickjs, the default
mermaidx.render(source, backend="quickjs").png() # the same, spelled out
mermaidx.render(source, backend="v8").png() # JIT, faster on big batches
# Rust reimplementations: no JS engine, output may differ
mermaidx.render(source, backend="merman").png()
mermaidx.render(source, backend="mermaid-rs-renderer").png()
To see the differences yourself, render the same diagram with every backend you have installed:
for name in mermaidx.backends():
mermaidx.render(source, backend=name).save(f"diagram-{name}.png")
Options depend on the family:
# mermaid.js backends: config and css
mermaidx.render(source, theme="dark", config={"flowchart": {"curve": "basis"}})
# Rust backends: spacing and aspect ratio
mermaidx.render(source, backend="merman", theme="dark", node_spacing=60, rank_spacing=80)
Python API
render() only stores the source. Every Diagram method is lazy and cached: nothing is computed until you call it, and repeating a call returns the memoized result.
d = mermaidx.render("flowchart LR; A-->B-->C")
d.svg() is d.svg() # True, the second call is a cache hit
| Method | Returns | Notes |
|---|---|---|
.svg() |
str |
|
.png(width?, height?, scale?, background?) |
bytes |
Aspect ratio is always preserved. |
.jpg(quality?), .bmp(), .tga() |
bytes |
Same defaults as .save(). |
.pdf(pdf_format?, pdf_landscape?, pdf_margin?, ...) |
bytes |
pdf_format=None fits the page to the diagram. |
.ascii(**opts) |
str |
Renders from the source, no SVG needed. |
.raw(width?, height?, background?) |
(bytes, w, h) |
Raw RGBA8888, no imaging library. |
.numpy(width?, height?, background?) |
np.ndarray |
(H, W, 4) uint8. Requires numpy. |
.save(path, format=None, ...) |
None |
Format comes from the extension unless forced with format=. |
d.save("out.png") # format from the extension
d.save("out.whatever", format="png") # or forced explicitly
d.save("out.pdf", pdf_format="A4", pdf_margin="1cm")
Themes, config and 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 batches. Rendering is pure CPU work, so real concurrency means processes rather than async. Each worker starts its own engine once and reuses it:
diagrams = mermaidx.render_many(sources, workers=4, theme="dark")
for d, name in zip(diagrams, output_names):
d.save(name)
ASCII output works out of the box (termaid is a core dependency):
print(mermaidx.render_ascii("graph LR; A-->B-->C"))
┌───┐ ┌───┐ ┌───┐
│ A ├───►│ B ├───►│ C │
└───┘ └───┘ └───┘
Rasterize any SVG 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)
Full reference: Python API docs.
CLI
mermaidx -i diagram.mermaid # SVG to stdout
mermaidx -i diagram.mermaid -o diagram.png # format from the extension
mermaidx -i diagram.mermaid -o diagram.png -w 1200 # or --scale 2.0
mermaidx -i diagram.mermaid -o diagram.svg --theme dark
mermaidx -i diagram.mermaid -o diagram.pdf --pdf-format A4 --landscape --margin 1cm
mermaidx -i diagram.mermaid -o diagram.svg --config config.json --css style.css
mermaidx -i diagram.mermaid -o diagram.svg --backend merman # needs mermaidx[rust]
mermaidx --list-backends
mermaidx --info # bundled Mermaid version
mermaidx --version
Use -q to silence the "saved to…" message and --quality N for JPEG quality. More in the CLI docs.
Supported diagrams
Everything Mermaid v11 itself supports, because the real library is bundled: flowcharts, sequence, class, state, ER, Gantt, pie, git graphs, mindmaps, timelines, and more.
Requirements
- Python 3.9+
quickjs-ng,novasvg,termaid(installed automatically)mini-racer(optional, forbackend="v8")
Documentation
| Quick start | Python API |
| Command line | Backends |
| Known issues | Architecture |
Testing
pip install -e ".[test]"
pytest tests/ -v
History
How the project got here (mmdc → mermaidx)
- 0.1.0 to 0.5.0,
mmdcon phasma (2025-12-26 to 2026-06-28): the original version, named after themmdccommand of mermaid-cli. It rendered through PhantomJS driven by phasma (a 25.0 MB wheel at phasma 0.7.1, measured withpip-size) and was stuck on mermaid.js 7.1.2, because recent Mermaid needs ES6 and PhantomJS does not have it (#5). - 0.6.0 (2026-07-08), a full rewrite: PhantomJS was removed. mermaid.js now runs inside QuickJS-ng against a hand-written DOM/SVG shim, with resvg for rasterization, and the
Diagramclass andrender()appeared. 0.6.2 addedbackend=:js(renamedquickjsin 0.9.0), plusmermanandmermaid-rs-rendererthrough the optionalmmdrpackage. - 0.7.0 and 0.8.0 (2026-07-10), renamed to mermaidx: 0.7.0 is the last release under the
mmdcname, with a deprecation notice; 0.8.0 is the firstmermaidx. The old name was easy to confuse with the tool this project replaces. If you havemmdcpinned anywhere (requirements.txt, a Dockerfile, CI config), switch it tomermaidx. - 0.9.0 (2026-07-18): an optional real-V8 engine (
backend="v8", installed today withmermaidx[v8engine]) next to the default QuickJS-ng one. Through the 0.9.x series the SVG was patched after rendering so that resvg could draw HTML labels. - 0.10.0 (2026-10-17): novasvg became the only rasterizer and the only text and label measurer, so resvg, the post-render patching and font embedding were removed. Because novasvg paints
<foreignObject>directly, mermaid.js's default HTML labels are used again (htmlLabels: true). JPEG, BMP and TGA output was added, and output is checked structurally against reference SVGs. See the changelog for breaking changes (font embedding removed,raster="resvg"now raisesValueError).
The longer version, in my own words: The story of mermaidx.
Acknowledgments
- Mermaid: the diagramming library this project renders.
mermaidxjust runs the real thing somewhere a browser can't go. - novasvg: SVG rasterization, image encoding and text measurement.
- mmdr: a native-Rust Mermaid renderer by the same author, usable as an extra backend.
- termaid: powers ASCII/Unicode terminal output.
Contributing
- Fork and create a feature branch.
- Add tests for new functionality.
- Run
pytest tests/. Everything must pass. - Open a pull request.
License
MIT. See LICENSE.
If mermaidx saves you from booting headless Chrome a hundred times in CI, a ⭐ helps other people find it.
Metadata
Release files for mermaidx 0.10.1
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.1-py3-none-any.whl | Python 3 | none | any | Details |
Release files / mermaidx-0.10.1-py3-none-any.whl
| Download URL | mermaidx-0.10.1-py3-none-any.whl |
|---|---|
| Size | 1.8 MB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
aa2e44648a96629df21acab3cac0ee2d6caca044be72898819d09736ea2f160e
|
|
BLAKE2b-256 checksum How to use checksums |
d80d7ae2cf9f372dc4d6096a3f8f0a67e65163003b1250bfbce85c4a6c299dd9
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Security for mermaidx 0.10.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.