PerfectVector
著者: Irene Kim3 分で読める

GraphvizのSVG画像:ノードの図形を持ち運べる形にする

GraphvizのノードにSVG図形を追加し、画像参照を確認します。素材一式、図だけ移動した状態、自己完結した納品コピーを比較して検証します。

目次

Graphvizの図にはベクターのノードや矢印があっても、カスタムのノード図形は別の画像ファイルを参照している場合があります。SVGを移動した後に図形が消えたら、描画を変える前に生成された画像参照を確認してください。足りないのは納品用のファイル一式かもしれません。

以下で検証したSVG出力では、Graphvizは motif.svg をリンク画像として保持します。一式が揃っていれば描画されますが、図だけを移動すると失われます。明示的にパッケージ化したコピーは図形のバイト列を含み、同じブラウザーテストで描画されます。それでもSVGのimage要素は残り、モチーフがグラフのネイティブなパスになるわけではありません。

ノード名と関係を保持するため、DOTの元データを残してください。図形のマスターは別に保管し、実際の出力を受け取り側のビューアーや公開手順でテストします。

ノード、図形、ファイル参照を区別する

Graphvizのimage属性は、ノード内に表示する図形を指定します。グラフ内でのノードの識別や関係を置き換えるものではありません。ドキュメントは元画像の寸法を必要としています。SVG画像には、適切な単位の明示的な width と height を指定してください。

今回のオリジナルモチーフは96 × 64の座標空間を持つ2パスの図形です。外寸は 96pt × 64pt です。周囲のノードと矢印はGraphviz側の要素です。

部分管理する場所起こり得る問題
ノード名、ラベル、エッジDOTの元データグラフの関係や文言の誤り
カスタムモチーフ図形のSVGマスター寸法の欠落、不適切な細部、縦横比の誤り
図からモチーフへのリンク書き出したSVGの画像参照解決されたパスに図形が存在しない
最終的な配置とサイズレンダラーの出力と受け取り側のビューアー切れ、引き伸ばし、非対応の依存関係

image要素はSVG図形もピクセル画像も参照できます。ラスター元データを調べるには、別のSVG埋め込み画像の診断ガイドを使ってください。ファイル名が .svg で終わるだけでは、どちらのファイルの内容も確定できません。

レンダラーを明示して小さな例を実行する

ここでは @viz-js/viz 3.31.0を使いました。Graphviz 16.1.0と表示されるWebAssemblyビルドで、dot レイアウトエンジンと組み込みSVG出力を使用しています。これはそのビルドとレンダラーのテストであり、すべてのGraphviz環境で画像の読み込みが同じという主張ではありません。

新しいNode.jsプロジェクトのフォルダーに、固定したバージョンのパッケージをインストールします。

npm install @viz-js/viz@3.31.0

元の図形を motif.svg として保存します。

<svg xmlns="http://www.w3.org/2000/svg"
     width="96pt" height="64pt" viewBox="0 0 96 64">
  <path fill="#173b40" fill-rule="evenodd"
    d="M48 4C70 4 88 17 88 32S70 60 48 60S8 47 8 32S26 4 48 4Z
       M48 14C31 14 19 22 19 32S31 50 48 50S77 42 77 32S65 14 48 14Z"/>
  <path fill="#28bfa3"
    d="M43 20H53V27H60V37H53V44H43V37H36V27H43Z"/>
</svg>

グラフを diagram.dot として保存します。

digraph G {
  graph [rankdir=LR, bgcolor="white", margin=0.1];
  node [shape=box, fontname="Arial", fontsize=16,
        color="#173b40", penwidth=1.5,
        fixedsize=true, width=1.8, height=1.1];
  artwork [label="", image="motif.svg", imagescale=true];
  review [label="Review"];
  artwork -> review [color="#173b40", penwidth=1.5];
}

次を render.mjs として保存し、そのフォルダーから node render.mjs を実行します。

import { instance } from '@viz-js/viz';
import fs from 'node:fs';
 
const viz = await instance();
const dot = fs.readFileSync('diagram.dot', 'utf8');
const svg = viz.renderString(dot, {
  engine: 'dot',
  format: 'svg',
  images: [{ name: 'motif.svg', width: '96pt', height: '64pt' }]
});
 
fs.mkdirSync('bundle', { recursive: true });
fs.copyFileSync('motif.svg', 'bundle/motif.svg');
fs.writeFileSync('bundle/diagram.svg', svg);
process.stdout.write(`Graphviz ${viz.graphvizVersion}\n`);

Viz.jsのAPIドキュメントでは、images オプションを画像のサイズ情報として説明しています。それを指定すると、名前を付けた図形の寸法がレイアウトに伝わりますが、ファイルのバイト列が結果に入るわけではありません。単位を明示することで、単位なしの数値をCSSピクセルと誤解せずに済みます。APIでは単位のない寸法をポイントとして扱います。

ネイティブのGraphviz環境については、公式のimageの説明がローカル画像リソースと元ファイルから読み取る寸法を説明しています。WebAssemblyのメタデータ設定をネイティブコマンドにそのまま写し、同じ読み込み方式だと思い込まないでください。

レンダラーが書き出した内容を確認する

生成されたSVGには次の要素があります。

<image xlink:href="motif.svg"
       width="118.8px" height="79.2px"
       preserveAspectRatio="xMinYMin meet"
       x="5.4" y="-79.2"/>

寸法と位置は、この例で観察した値です。納品を左右するのは xlink:href="motif.svg" の部分です。結果は引き続き、その相対パスにあるリソースを必要とします。

diagram.svg だけを別のフォルダーにコピーし、図形は元の場所に残してください。一式が揃った場合と比較します。私たちは両フォルダーをローカルHTTPで配信し、各SVGをHTMLの object 要素から文書として表示しました。

3つの実際のGraphviz SVG図。素材一式では図形が表示され、図だけの移動後は消え、SVG画像のバイト列を埋め込んだコピーでは表示されています
中央のファイルは左と同じグラフ形状と図形参照を持ちますが、解決されたパスにモチーフがありません。右のコピーは図形をSVGのdata URLとして明示的に含めています。

リンクされたモチーフがなくても、生成されたノードの枠と矢印は残ります。グラフのレイアウトを再生成しても、不足したファイルは補われません。想定された素材の場所を復元するか、一式を納品するか、利用先に適したパッケージ化の工程を選んでください。

SVGを文書として開くことと、HTMLの img で表示することは別のテストです。MDNのSVGを画像として使う説明では、画像としての文脈では外部リソースが制限される場合があり、data URLなら内部に含められると説明しています。そこに記載された画像としての制限は、SVG文書を直接開く場合や object、iframe で文書として埋め込む場合には適用されません。実際に使う納品先の文脈を確認してください。

この単純なモチーフを明示的にパッケージ化する

この自己完結したオリジナルモチーフでは、次のスクリプトで既知の参照1つだけをファイルのSVGバイト列に置き換えます。package.mjs として保存し、描画後に実行してください。

import fs from 'node:fs';
 
const svg = fs.readFileSync('bundle/diagram.svg', 'utf8');
const needle = 'xlink:href="motif.svg"';
if (svg.split(needle).length !== 2) {
  throw new Error('Expected exactly one motif reference');
}
 
const data = 'data:image/svg+xml;base64,' +
  fs.readFileSync('bundle/motif.svg').toString('base64');
const packaged = svg.replace(needle, `xlink:href="${data}"`);
fs.writeFileSync('self-contained.svg', packaged);

これは対象を限定したパッケージ化の例で、汎用的なSVGインライン化ツールではありません。この出力にある既知の参照と、外部フォント、画像、スタイルシートなどの依存関係がないモチーフだけを扱います。より複雑な図形には個別の依存関係の確認が必要です。

パッケージ化したコピーは、ブラウザーテストでモチーフを表示しました。画像参照の値を隠して2つの出力文字列を比較すると、グラフ形状は同じでした。ファイルには引き続きimage要素が1つあり、その参照は data:image/svg+xml;base64, で始まります。

SVG画像のバイト列を埋め込めば、ベクターの元図形を維持しながら、このファイルパスへの依存を取り除けます。ただし、図形をGraphvizのネイティブなノード形状にしたり、その内部に接続点を加えたり、別の編集ソフトでパスを直接編集できることを保証したりはしません。受け取り側の読み込み動作をテストしてください。

また、svg_inline と画像の埋め込みを混同しないでください。GraphvizのSVG出力ドキュメントでは、HTMLに含めるためのヘッダーなし出力として説明しています。同じグラフで svg_inline を選んでも、motif.svg の参照は生成されました。出力モードは図形をパッケージ化しませんでした。

寸法と拡大縮小を持ち運びやすさと区別する

Graphvizのimagescaleリファレンスでは、画像をノード内に収める処理と元画像の自然なサイズを区別しています。この例の imagescale=true は、比率を維持して収まるように拡大縮小します。both は幅と高さを個別に変えるので、縦横比が変わることがあります。

モチーフが表示されても引き伸ばされているなら、拡大縮小の設定を確認します。一部だけ表示されるなら、モチーフ自身の境界とノードの利用可能な空間を確認します。移動後に消えるなら、まずリソースのパスを確認してください。SVGのサイズガイドでは、宣言されたサイズと描画座標の違いを説明しています。

症状最初の確認試す修正
図を移動すると図形が消える生成された画像参照と相対的な素材の位置一式を戻すか、検証した元データを明示的にパッケージ化する
グラフのレイアウトは動くが図形がない元画像の寸法とビルド固有の画像設定そのビルドが想定するファイルと画像メタデータを確認する
モチーフが引き伸ばされるimagescaleの設定と意図した比率変形が不要なら、比率を維持して収める
SVGを直接開くと動くがWebページでは失敗するページが画像として使うか文書として使うか対応する埋め込み方法とリソースポリシーを検証する
レンダラー変更後に出力が異なる選んだレンダラーとSVG構造書き出したXMLと最終的な見た目を再確認する

公式のSVG出力ページでは、Cairo出力は組み込みSVG出力と比べ、XMLの読みやすさや変換のしやすさが異なる場合があるとしています。ここではCairoをテストしていません。再現可能なプロジェクトではレンダラーを明示し、変更したときはパッケージ化の確認を繰り返してください。

元データが失われた図形は上流で復元する

適切な説明用モチーフがPNGやスキャンでしか残っていない場合は、PerfectVectorを利用できます。科学図版の素材復元の手順は、使用前に確認するベクター候補を提供します。モチーフだけを切り抜いてシルエットと空白を確認し、グラフに加える前にダウンロードしたSVGの境界と寸法を検証してください。

その復元で、ノードの関係、矢印の意味、DOTのラベルは再構築されません。それらはグラフの元データに保持します。元のSVGがあれば使い、保守しやすい場合は単純な記号を描き直し、写真はラスターとして維持してください。画像ベクター化の概要では復元の限界を説明しています。

他の図作成ツールでは、カスタムの図形のパッケージ化方法が異なります。Mermaidのアイコンガイドは登録したアイコンデータを扱い、draw.ioのカスタムSVG図形は読み込んだ画像とネイティブのステンシルを区別しています。利用先の要件を図形の準備に反映してください。

よくある質問

GraphvizのSVG出力はノード画像を自動で含めますか? 検証した組み込みSVG出力では、図には別ファイルmotif.svgへの画像参照が含まれます。自分の出力を確認し、依存ファイルも納品するか、検証済みのパッケージ化工程を使ってください。

svg_inlineはSVG画像のパスをグラフに統合しますか? いいえ。HTMLに含めるためのヘッダーなし出力モードです。今回のテストでは外部のmotif.svg参照が残りました。

自己完結したSVGはネイティブの編集可能なノード形状と同じですか? いいえ。パッケージ化した例にはSVGバイト列を含むimage要素が残ります。グラフの構造はDOTに保持され、図形をどのように扱えるかは受け取り側の編集ソフトが決めます。

参考資料

  1. Graphviz — image属性 — ローカルのノード画像リソースと必要なSVG寸法を説明しています。
  2. Graphviz — imagescale属性 — 比率を維持した収め方と、幅・高さを個別に変える拡大縮小を説明しています。
  3. Graphviz — SVG出力 — 組み込み出力、Cairo出力、svg_inlineの意味を説明しています。
  4. Viz.js — API — 画像サイズのメタデータ、明示的な単位、SVG描画呼び出しを説明しています。
  5. MDN — SVGを画像として使う — 画像としての外部リソース制限と文書埋め込みとの違いを説明しています。

図形ライブラリー全体を準備する前に、1つのノードを納品手順に通してテストしてください。ラスターのモチーフしか残っていない場合は、SVG図形の候補を作成して隙間と比率を確認し、DOTグラフに加えます。その後、移動した出力またはパッケージ化した出力を、受け取り側のビューアーで検証してください。

ブログのその他の記事

編集しやすい
よりきれいなSVGから始めましょう