Mermaid図

執筆・検証: MD Converter編集部

Mermaid図をSVG・PNGに書き出すと文字が消える・切れる原因と対処法|foreignObjectとフォント

Mermaidの図をSVGやPNGに書き出すと、ラベルの文字が消える・途中で切れる・図が小さくなる。原因はラベルがHTML(foreignObject)で描かれることと、画像として描くとWebフォントが使えないこと。htmlLabelsの正しい設定、フォントの埋め込み、サイズ指定まで、実際に試した結果と実装例で解説します。

Mermaidの図をSVGやPNGに書き出したときに文字が消える・切れる原因は、主に2つです。1つは、Mermaidが既定でラベルをSVGの中のHTML(foreignObject)として描くことです。ブラウザ以外のアプリではこのHTMLが描かれず、文字だけが消えます。もう1つは、SVGを画像として描くとページで読み込んだWebフォントが使えず、幅が合わなくなった文字が枠で切れることです。

どちらも、ラベルをSVGの文字(<text>)で描かせる htmlLabels: false で解決できます。設定は全体と flowchart の両方に書くのが確実です。Webフォントの見た目を保ちたい場合は、フォントをSVGに埋め込みます。

この記事の検証は、Mermaid 11.4.0 と 11.16.1、Chrome、macOSの標準SVG変換(sips)とクイックルックで行いました。

症状別の原因早見表

症状主な原因対処
ブラウザでは見えるのに、ほかのアプリで開くと文字だけ消えるラベルが foreignObject(HTML)で描かれているhtmlLabels: false にする
PNGにすると文字の末尾が切れる(「Markdown docur」など)画像として描くとWebフォントが使えず、代わりのフォントで幅がはみ出すhtmlLabels: false、またはフォントをSVGに埋め込む
PNGが想定より小さい・ぼやけるSVGの width="100%" で、画像としての大きさが決まらないviewBoxから幅と高さを指定し、倍率を掛けて描く
矢印のラベル(`-->yes` の yes)だけ消える

原因1:ラベルがHTML(foreignObject)で描かれている

Mermaidのフローチャートは、既定でノードや矢印のラベルを <foreignObject> の中のHTML(<div> や <p>)として描きます。foreignObject はSVGの中にHTMLを埋め込む仕組みで、ブラウザはHTMLを描けるので問題なく表示されます。

<foreignObject width="72.19" height="24">
  <div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; ...">
    <span class="nodeLabel"><p>Markdown</p></span>
  </div>
</foreignObject>

ところが、ブラウザ以外のアプリはHTMLを描く仕組みを持たないことが多く、その場合は foreignObject の中身がまるごと描かれません。macOSの標準のSVG変換(sips -s format png diagram.svg --out diagram.png)で試すと、foreignObject のラベルは文字だけが消えて空の箱が並びました。同じ図をSVGの <text> で描いたものは、文字が正しく出ました。

書き出したSVGに foreignObject が含まれているかは、文字列で確認できます。

const { svg } = await mermaid.render("diagram-1", code);
console.log((svg.match(/<foreignObject/g) ?? []).length); // 0 ならラベルはすべて <text>

対処:htmlLabels を false にする(全体と flowchart の両方に)

htmlLabels: false にすると、Mermaidはラベルを <text> で描きます。SVGの基本機能なので、ブラウザ以外のアプリでも表示されます。

注意したいのは、設定を書く場所でバージョンごとに効き方が違うことです。矢印にラベルがある図(A -->|yes| B)で試した結果は次のとおりでした。

設定Mermaid 11.4.0Mermaid 11.16.1
全体の htmlLabels: false だけノードは <text>、矢印のラベルは foreignObject のまますべて <text>
flowchart.htmlLabels: false だけ効果なし矢印のラベルだけ <text>
両方すべて <text>すべて <text>

どちらのバージョンでも確実なのは、両方に書く方法です。

// サイト全体の初期化で指定する
mermaid.initialize({
  startOnLoad: false,
  htmlLabels: false,
  flowchart: { htmlLabels: false },
});

図ごとに指定したい場合は、図の先頭にディレクティブを書きます。

%%{init: {"htmlLabels": false, "flowchart": {"htmlLabels": false}}}%%
flowchart LR
  A[Start] -->|yes| B[End]

htmlLabels: false にすると、ラベル内でHTMLを使った装飾など、HTMLラベルに頼った表現は使えなくなる場合があります。文字の位置や折り返しも少し変わるので、書き出し後の見た目を一度確認してください。

原因2:画像として描くとWebフォントが使えない

SVGをPNGにするときは、SVGを Image として読み込み、canvasに描くのが一般的です。ところが、画像として読み込まれたSVGは、ページで読み込んだWebフォントを使えません。画像の中からは外部のファイルを読みに行かないためです。

Mermaidはラベルの枠の幅を、ページ上のフォントで文字の幅を測って決めます。ページでWebフォントを使っていると、枠はWebフォントの幅で作られますが、PNGにするときは代わりのフォントで描かれます。代わりのフォントのほうが幅が広いと、foreignObject の枠からはみ出した部分が切れます。

幅の狭いWebフォント(Roboto Condensed)をページで読み込んで試すと、ページ上では正しく見える図が、PNGでは「Markdown document」が「Markdown docur」に、「Rendered preview」が「Rendered pre」に切れました。

対処は2つあります。

async function embedFont(svgRoot, family, fontUrl) {
  const bytes = new Uint8Array(await (await fetch(fontUrl)).arrayBuffer());
  let binary = "";
  for (const b of bytes) binary += String.fromCharCode(b);
  const style = svgRoot.ownerDocument.createElementNS("http://www.w3.org/2000/svg", "style");
  style.textContent =
    `@font-face{font-family:"${family}";src:url(data:font/woff2;base64,${btoa(binary)}) format("woff2");}`;
  svgRoot.insertBefore(style, svgRoot.firstChild);
}

埋め込むときの落とし穴は、フォントの文字セットです。Google Fontsなどは、1つのフォントを文字セット(キリル文字、ラテン文字など)ごとに別ファイルで配信しています。最初に見つかったファイルを埋め込んだところ、それはラテン文字を含まないファイルで、文字は切れたままでした。CSSの unicode-range を見て、図で使う文字(英数字なら U+0000-00FF を含むもの)のファイルを選んでください。日本語のフォントはファイルが大きいので、埋め込みよりも、閲覧する環境にあるフォントを fontFamily に指定するほうが現実的です。

落とし穴:width="100%" で画像の大きさが決まらない

MermaidのSVGは、ルート要素が width="100%" で、style に max-width が入った形で出力されます。ページに埋め込むには便利ですが、Image として読み込むと大きさが決まらず、Chromeでは幅300pxの既定サイズとして扱われました。img.naturalWidth を使ってcanvasを作ると、図が小さく、ぼやけたPNGになります。

viewBox に図の本来の大きさが入っているので、そこから width と height を指定し直してから読み込みます。高解像度の画面や印刷向けには、canvasを2倍などの大きさにして描きます。

実装例:文字が消えないPNG書き出し

ここまでの対策をまとめた関数です。ブラウザで動かす前提です。

import mermaid from "mermaid";

mermaid.initialize({
  startOnLoad: false,
  htmlLabels: false,
  flowchart: { htmlLabels: false },
});

let seq = 0;

export async function mermaidToPng(code, scale = 2) {
  const { svg } = await mermaid.render(`mermaid-export-${seq++}`, code);

  // viewBox から本来の大きさを指定し直す(width="100%" のままだと大きさが決まらない)
  const doc = new DOMParser().parseFromString(svg, "image/svg+xml");
  const root = doc.documentElement;
  const { width, height } = root.viewBox.baseVal;
  root.setAttribute("width", String(width));
  root.setAttribute("height", String(height));
  root.removeAttribute("style");
  const source = new XMLSerializer().serializeToString(root);

  const img = new Image();
  img.src = "data:image/svg+xml;charset=utf-8," + encodeURIComponent(source);
  await img.decode();

  const canvas = document.createElement("canvas");
  canvas.width = Math.ceil(width * scale);
  canvas.height = Math.ceil(height * scale);
  const ctx = canvas.getContext("2d");
  ctx.fillStyle = "#ffffff"; // 透過のままだと、暗い背景のアプリで黒い文字が読めない
  ctx.fillRect(0, 0, canvas.width, canvas.height);
  ctx.drawImage(img, 0, 0, canvas.width, canvas.height);

  return new Promise((resolve, reject) => {
    try {
      canvas.toBlob((blob) => (blob ? resolve(blob) : reject(new Error("PNG encode failed"))), "image/png");
    } catch (error) {
      reject(error); // 環境によってはセキュリティ上の理由で書き出しが拒否される
    }
  });
}

SVGのまま渡す場合も、同じように htmlLabels: false で描き、width と height を指定し直したものを使うと、ほかのアプリで開いたときに崩れにくくなります。WordやPowerPointにSVGを入れる場合は、SVGに対応していない環境のためにPNGも用意しておくと安全です。

Chromeでは、foreignObject を含むSVGをcanvasに描いてもPNGを書き出せました。ただ、ブラウザによってはSVGの描き方やcanvasの制限が異なる可能性があるため、toBlob の失敗に備えたエラー処理を入れ、利用者が使うブラウザでも確認してください。

書き出し前のチェックリスト


MermaidをWord文書に入れたい場合は、Markdown Document ConverterのMD→DOCX変換が使えます。この記事の対策を入れており、Mermaid図はラベルを <text> で描いたSVG(PNGのフォールバック付き)としてdocxに埋め込みます。変換はブラウザ内で行われます。

MarkdownをWord(DOCX)に変換する

関連記事: Mermaid図入りMarkdownをPDFにする方法 / JavaScriptでMarkdownをWord(docx)に変換すると崩れる原因と直し方 / MarkdownとMermaidでドキュメントを書くメリット