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

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

  • quickjs and v8 run 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.
  • merman and 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.

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, for backend="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).

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.10.1

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.10.1
File Interpreter ABI Platform
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.

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