執筆・検証: 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.0 | Mermaid 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つあります。
htmlLabels: falseにする:<text>は枠で切られないので、文字は最後まで描かれます。ただし代わりのフォントで描かれるため、文字が枠の端に寄ったり、はみ出したりすることがあります。- フォントをSVGに埋め込む:フォントファイルをdata URLにして、SVGの
<style>に@font-faceとして入れます。画像の中でも同じフォントが使えるので、ページ上と同じ見た目になります。
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 の失敗に備えたエラー処理を入れ、利用者が使うブラウザでも確認してください。
書き出し前のチェックリスト
- 書き出したSVGに
<foreignObject>が残っていない htmlLabels: falseを全体とflowchartの両方に指定した- 矢印のラベル(
-->|...|)がある図でも文字が出ることを確認した - SVGの
widthとheightをviewBoxから指定し直した - Webフォントを使う場合は、フォントを埋め込むか、閲覧環境にあるフォントを指定した
- PNGはブラウザ以外のアプリ(OSの画像ビューアなど)でも開いて確認した
MermaidをWord文書に入れたい場合は、Markdown Document ConverterのMD→DOCX変換が使えます。この記事の対策を入れており、Mermaid図はラベルを <text> で描いたSVG(PNGのフォールバック付き)としてdocxに埋め込みます。変換はブラウザ内で行われます。
関連記事: Mermaid図入りMarkdownをPDFにする方法 / JavaScriptでMarkdownをWord(docx)に変換すると崩れる原因と直し方 / MarkdownとMermaidでドキュメントを書くメリット