Skip to main content
mermaidx logo

mermaidx

Mermaid diagrams → SVG, PNG, JPG, PDF and ASCII, from plain Python.
No browser. No Node.js. No system packages. Just pip install mermaidx.

PyPI Python License: MIT Tests

mermaid.js pele.js Backends GitHub stars

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 12 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 12 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. mermaid.js (default), pele, or native Rust (optional), on QuickJS-ng (default) or V8 (optional). pele is far faster than mermaid.js but draws its own layout: see the benchmarks before you choose.

Installation

pip install mermaidx

Extras are opt-in:

Command Adds
pip install mermaidx Base install. Default backend="js:mermaid" on engine="quickjs", zero system dependencies.
pip install mermaidx[v8engine] Real V8 (mini-racer) as engine="v8". Faster thanks to JIT, runs in an isolated child process.
pip install mermaidx[rust] Native-Rust backends via mmdr: rust:merman and rust:mermaid-rs-renderer.
pip install numpy Enables .numpy() export.
pip install mermaidx[all] Every extra above (v8engine + rust + numpy).

Quick start

Open in Colab

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 / pele.js<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. A backend is named <kind>:<name>, and the js: backends also take an engine. List what is installed at runtime:

import mermaidx

print(mermaidx.backends())
# ['js:mermaid', 'js:pele']                                                 base install
# ['js:mermaid', 'js:pele', 'rust:merman', 'rust:mermaid-rs-renderer']      with mermaidx[rust]

print(mermaidx.available_engines())
# ['quickjs']                                                               base install
# ['quickjs', 'v8']                                                         with mermaidx[v8engine]
Backend What runs Notes
js:mermaid (default) The original mermaid.js Same library, same diagram types, same options (theme, config, css).
js:pele pele, a small Mermaid renderer written in JavaScript Its own layout, not mermaid.js. Themed with CSS variables (css=), so theme must stay unset.
rust:merman A Rust reimplementation (mmdr) No JavaScript engine involved. Not mermaid.js.
rust:mermaid-rs-renderer A different Rust reimplementation (mmdr) Same idea as rust:merman, with its own layout engine.

The js: backends run inside a JS engine, chosen with engine=. It is None unless you set it, which means quickjs:

Engine What runs Notes
quickjs (default) QuickJS-ng Lightweight and always available, runs in-process. Safe for every diagram type, including mindmap.
v8 V8 (mini-racer) Faster thanks to the JIT. Runs in an isolated child process so memory is fully reclaimed.

A rust: backend has no JS engine, so passing engine= with one raises ValueError.

flowchart TB
    CALL["mermaidx.render(source, backend=..., engine=...)"] --> PICK{"backend"}

    subgraph JSB ["Diagram: a js: backend inside a JS engine"]
        MJ["js:mermaid (default)<br/>real mermaid.js v12 + DOM/SVG shims"]
        PE["js:pele<br/>pele, synchronous, no DOM"]
        ENG["engine=None means quickjs<br/>quickjs: QuickJS-ng, in-process<br/>v8: V8 via mini-racer, child process"]
        MJ --> ENG
        PE --> ENG
    end

    subgraph RSB ["DiagramRust: optional mmdr package"]
        MM["rust:merman<br/>native Rust"]
        MR["rust:mermaid-rs-renderer<br/>native Rust"]
    end

    PICK -->|"mermaidx"| MJ
    PICK -->|"mermaidx"| PE
    PICK -->|"mermaidx[rust]"| MM
    PICK -->|"mermaidx[rust]"| MR

    ENG --> 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:

  • js:mermaid is the original mermaid.js. Only the JavaScript engine differs between quickjs and v8: V8 is faster, QuickJS installs anywhere. The engine never changes the result. Text is measured with the bundled DejaVu Sans, not with the fonts of a browser, so a diagram can differ from your own mmdc run by a fraction of a pixel in text width (see Known issues in the docs).
  • js:pele is a different implementation, in JavaScript, and much faster: in our timings it renders a small diagram in a few milliseconds where js:mermaid takes about a second, over 100 times faster. The price is that its layout and styling differ from mermaid.js, so the output will not match it. The Benchmarks page shows the measured times and every sample rendered by every backend side by side; have a look before you pick one. It takes config and css but no theme: restyle it with its CSS variables, for example css="svg{--pele-bg:#111;--pele-fg:#eee}".
  • rust:merman and rust:mermaid-rs-renderer are 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_ratio instead of config and css).

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. (pele paints with nested CSS variables, which novasvg resolves from 0.11.4 on.)

import mermaidx

source = "flowchart LR; A[Parse] --> B[Layout] --> C[Render]"

# Original mermaid.js: same output, different JS engine
mermaidx.render(source).png()                          # js:mermaid on quickjs, the default
mermaidx.render(source, backend="js:mermaid").png()    # the same, spelled out
mermaidx.render(source, engine="v8").png()             # JIT, faster on big batches

# pele: small, its own layout, any engine
mermaidx.render(source, backend="js:pele").png()
mermaidx.render(source, backend="js:pele", engine="v8").png()

# Rust reimplementations: no JS engine, output may differ
mermaidx.render(source, backend="rust:merman").png()
mermaidx.render(source, backend="rust: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.replace(':', '-')}.png")

Options depend on the backend:

# js:mermaid: theme, config and css
mermaidx.render(source, theme="dark", config={"flowchart": {"curve": "basis"}})

# js:pele: config and css (no theme)
mermaidx.render(source, backend="js:pele", css="svg{--pele-surface:#202020}")

# Rust backends: spacing and aspect ratio
mermaidx.render(source, backend="rust: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")   # any Mermaid theme: "default", "forest", "dark", "neutral", "neo", "redux", ...
mermaidx.render(source)                 # no theme: Mermaid 12 picks one per diagram type, like mmdc
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 js:pele
mermaidx -i diagram.mermaid -o diagram.svg --engine v8        # needs mermaidx[v8engine]
mermaidx -i diagram.mermaid -o diagram.svg --backend rust: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 12 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, for engine="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, mmdc on phasma (2025-12-26 to 2026-06-28): the original version, named after the mmdc command of mermaid-cli. It rendered through PhantomJS driven by phasma (a 25.0 MB wheel at phasma 0.7.1, measured with pip-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 Diagram class and render() appeared. 0.6.2 added backend=: js (renamed quickjs in 0.9.0), plus merman and mermaid-rs-renderer through the optional mmdr package.
  • 0.7.0 and 0.8.0 (2026-07-10), renamed to mermaidx: 0.7.0 is the last release under the mmdc name, with a deprecation notice; 0.8.0 is the first mermaidx. The old name was easy to confuse with the tool this project replaces. If you have mmdc pinned anywhere (requirements.txt, a Dockerfile, CI config), switch it to mermaidx.
  • 0.9.0 (2026-07-18): an optional real-V8 engine (backend="v8", installed today with mermaidx[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 raises ValueError).
  • 0.11.0 (2026-10-30), breaking: backends are named <kind>:<name> (js:mermaid, rust:merman), and the JS engine is a separate engine= parameter (quickjs by default, or v8). The pele renderer was added as js:pele. See the changelog for how to migrate.

The longer version, in my own words: The story of mermaidx.

Acknowledgments

  • Mermaid: the diagramming library this project renders. mermaidx just 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

  1. Fork and create a feature branch.
  2. Add tests for new functionality.
  3. Run pytest tests/. Everything must pass.
  4. 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.11.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Built distribution (wheel)

Table of built distributions (wheels) for mermaidx 0.11.0
File Interpreter ABI Platform Actions
mermaidx-0.11.0-py3-none-any.whl Python 3 none any Details for mermaidx-0.11.0-py3-none-any.whl

Release files / mermaidx-0.11.0-py3-none-any.whl

Download URL mermaidx-0.11.0-py3-none-any.whl
Size 2.7 MB
Tags Python 3
SHA-256 checksum
How to use checksums
61c00efdcb0cf965fa91beab2ae4de5176aa44b3b137781103513ad901b85834
BLAKE2b-256 checksum
How to use checksums
70a11b582d5252ffa79f09527af8a620c61a7716c078b1049aad511647a32160
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.11.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.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page