PerfectVector
著者: Irene Kim

SVG.js:アートワークを読み込み、パスを編集してSVGを書き出す

信頼できるSVGマークアップをSVG.jsに読み込み、名前付きパスを色変更。ルート全体と子要素だけの書き出しを比較し、元の形状と保存ファイルを確認します。

4 分で読める
目次

アートワークをSVG.jsへ読み込み、部分ごとに編集するには、信頼できるSVGマークアップを.svg()に渡し、元データで付けたIDから目的の要素を探して、その塗りを変えます。独立したファイルには、アートワークのSVGルート全体を書き出してください。子要素だけの書き出しは断片であり、ドキュメントとして渡す前に、意図した寸法とviewBoxを持つルートが必要です。

このガイドは、独自の中空ひし形と別の三角形を使います。三角形は黄土色からコーラル色に変わり、ひし形はティール色の塗りと開口部を保ちます。以下の完全な例を、公式の3.2.6リリースに固定したSVG.js 3.2.6でテストしました。これは自作の二つの単純なパスについての観察であり、別のSVGに含まれるすべての機能を対象にしていません。

生きた図形と画像のどちらが必要かを決める

アセットURLを生成するJavaScriptのimport文は、読み込みの問題に答えます。それだけでは、ファイルのパスがエディターの生きたSVGツリーに入るわけではありません。SVG.jsの現在のインポートとエクスポートの文書は、マークアップの読み込みと、外側のSVG要素または子要素の取得を説明しています。

表現この例で対象にできるもの納品の確認
生きたSVGルートに読み込んだ信頼できるマークアップaccentという名前のパス編集したルートを確認し、書き出しを開き直す
画像要素を通して読み込んだSVG画像要素全体画像の内容を別に調べる
子要素だけのマークアップ元のルートのない二つのパス要素意図したルート、寸法、viewBoxを用意する

画像内のSVGもSVGの内容です。ただし、画像ラッパーの対照例では、外側のSVGツリーに一つの画像要素があり、直接のパスはありません。そのツリーでaccentを検索しても見つからず、埋め込み画像のドキュメントまでは探しません。MDNは別のSVGの画像コンテキストと制約を説明しています。鮮明に見える描画から、編集のモデルを推測しないでください。

より広い埋め込み方法の判断には、インラインSVGと画像の比較を参照してください。Fabricのcanvasオブジェクトモデルが必要なら、Fabric.jsのインポート解説で別のパーサーと編集APIを扱っています。

名前付きパスを一つ編集し、各表現を保存する

以下の完全なコードを、空のフォルダーにstarter.htmlとして保存します。たとえばpython3 -m http.server 8000でHTTP配信し、http://localhost:8000/starter.htmlを開きます。スクリプトは固定バージョンのパッケージをCDNから読み込むため、ネットワーク接続が必要です。

ページは元のアートワーク、生きた編集済みコピー、画像ラッパーの対照例を表示します。リンクから、元のマスター、編集前後のルート全体、子要素だけのテキスト断片、新しいルートで囲んだコピー、画像ラッパーをダウンロードできます。報告はキャンバスに描いたSVG画像からピクセルを読み取り、PNGは書き出しません。

<!doctype html>
<html lang="en">
<meta charset="utf-8">
<title>Original SVG.js artwork import and export</title>
<style>
  body { margin: 24px; font: 16px system-ui; background: #f6f2e8; }
  main { display: grid; grid-template-columns: repeat(3, 260px); gap: 20px; }
  figure { margin: 0; }
  svg, img { display: block; background: white; border: 1px solid #bbb; }
  pre { white-space: pre-wrap; font-size: 12px; }
</style>
<h1>Trusted artwork, one fill edit and two export forms</h1>
<main>
  <figure><img id="original" alt="Original artwork"><figcaption>Original SVG image</figcaption></figure>
  <figure><div id="live"></div><figcaption>Imported live SVG, accent edited</figcaption></figure>
  <figure><div id="wrapper"></div><figcaption>Image wrapper, original artwork</figcaption></figure>
</main>
<p id="downloads"></p>
<pre id="report"></pre>
<script src="https://cdn.jsdelivr.net/npm/@svgdotjs/svg.js@3.2.6/dist/svg.min.js"></script>
<script>
  const markup = '<svg xmlns="http://www.w3.org/2000/svg" id="original-artwork" width="240" height="160" viewBox="0 0 240 160">'
    + '<path id="body" d="M80 20 L140 80 L80 140 L20 80 Z M80 56 L104 80 L80 104 L56 80 Z" fill="#176b5b" fill-rule="evenodd"/>'
    + '<path id="accent" d="M172 48 L220 80 L172 112 Z" fill="#d4a53a"/></svg>';
  const url = text => 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(text);
  async function pixels(text) {
    const image = new Image();
    image.src = url(text);
    await image.decode();
    const canvas = document.createElement('canvas');
    canvas.width = 240;
    canvas.height = 160;
    const ctx = canvas.getContext('2d');
    ctx.drawImage(image, 0, 0);
    const rgba = (x, y) => [...ctx.getImageData(x, y, 1, 1).data];
    return { naturalWidth: image.naturalWidth, naturalHeight: image.naturalHeight,
      opening: rgba(80, 80), body: rgba(80, 40), accent: rgba(188, 80), corner: rgba(0, 0) };
  }
  function inventory(text) {
    const node = new DOMParser().parseFromString(text, 'image/svg+xml').documentElement;
    return { root: node.localName, viewBox: node.getAttribute('viewBox'),
      paths: [...node.querySelectorAll('path')].map(p => ({ id: p.id, d: p.getAttribute('d'), fill: p.getAttribute('fill') })),
      imageCount: node.querySelectorAll('image').length };
  }
  async function run() {
    document.getElementById('original').src = url(markup);
    const host = SVG().addTo('#live').size(240, 160).viewbox(0, 0, 240, 160);
    host.svg(markup);
    const artwork = host.findOne('#original-artwork');
    const before = artwork.svg();
    const accent = artwork.findOne('#accent');
    const beforeD = accent.attr('d');
    accent.fill('#b95434');
    const full = artwork.svg();
    const children = artwork.svg(false);
    const rewrapped = SVG().size(240, 160).viewbox(0, 0, 240, 160);
    rewrapped.svg(children);
    const wrappedFull = rewrapped.svg();
    const imageWrapper = SVG().addTo('#wrapper').size(240, 160).viewbox(0, 0, 240, 160);
    imageWrapper.image(url(markup)).size(240, 160);
    const wrapperFull = imageWrapper.svg();
    const files = { 'master.svg': markup, 'before.svg': before, 'edited.svg': full,
      'children-only.txt': children, 'rewrapped.svg': wrappedFull, 'image-wrapper.svg': wrapperFull };
    for (const [name, text] of Object.entries(files)) {
      const a = document.createElement('a');
      a.href = name.endsWith('.txt') ? 'data:text/plain;charset=utf-8,' + encodeURIComponent(text) : url(text);
      a.download = name;
      a.textContent = name;
      a.style.marginRight = '12px';
      document.getElementById('downloads').append(a);
    }
    const results = {};
    for (const [name, text] of Object.entries(files)) {
      if (name.endsWith('.svg')) results[name] = { inventory: inventory(text), pixels: await pixels(text) };
    }
    document.getElementById('report').textContent = JSON.stringify({
      requestedPackageVersion: '3.2.6',
      scriptURL: document.querySelector('script[src]').src,
      selectedAccentFound: Boolean(accent), selectedAccentDUnchanged: beforeD === accent.attr('d'),
      allPathDUnchanged: JSON.stringify(inventory(before).paths.map(p => p.d)) === JSON.stringify(inventory(full).paths.map(p => p.d)),
      childrenHasOuterSVG: /<svg[\s>]/i.test(children),
      wrapperDirectPathCount: imageWrapper.find('path').length,
      wrapperAccentFound: Boolean(imageWrapper.findOne('#accent')), results
    }, null, 2);
  }
  run();
</script>
</html>
 

読み込むマークアップには、original-artworkという名前の入れ子のルートがあります。例は書き出し前にそのルートを選ぶため、納品コピーは無関係な親コンテナーに頼らず、元の240 × 160のビューポートを持ちます。その後、そのルート内のaccentを選択します。SVG.jsはセレクターによる参照と要素メソッドを文書化しています。部分の名前を明示すれば、パス順に依存せずに済みます。

コードは、自身の固定マークアップだけに意図的に限定しています。SVGアップロードのサニタイザーではありません。任意の文字列を解析、挿入しても、安全だと確認できるわけではありません。MDNのDOMParserリファレンスは、解析と、信頼できない内容の挿入リスクを区別しています。アップロード向けにこの処理を応用する前に、別の検証と信頼の方針を設計してください。

生きたプレビューだけでなく保存した結果を確認する

実際のブラウザー実行で、次の結果が得られました。

保存した表現ドキュメントツリー内のパス画像三角形の塗り
元のマスター20#d4a53a
編集前のルート全体20#d4a53a
編集後のルート全体20#b95434
新しい完全なルートに置いた子要素20#b95434
画像ラッパー01画像の内容内に元の黄土色がある

二つの元のパスのd文字列は、編集前後の書き出しで同一のままでした。三角形の内部サンプルはRGBA (212, 165, 58, 255)から(185, 84, 52, 255)に変わりました。ひし形の内部サンプルは(23, 107, 91, 255)のままで、開口部と外側の角はアルファ0を維持しました。五つの独立したSVGファイルはすべて、自然寸法240 × 160の画像として開き直せました。

名前付きの部分を一つ変える
同一の中空ひし形アートワークで、別の三角形だけが金色からコーラル色へ変わる様子を比較する概念図
部分単位の色変更の概念図です。装飾的な枠と矢印は図の目印であり、実際の根拠は、実行したひな形、保存した元要素の一覧、ブラウザーのピクセル確認によるものです。

この結果は、このテストファイルで目的の編集ができたことを確認します。XMLがバイト単位で同じに保存されること、すべてのフィルターやCSSへの対応、製造工程への適性を示すものではありません。ライブラリーのシリアライズは、この特定のパス文字列を保ちながら、ドキュメントのマークアップを変える場合があります。納品コピーと一緒に元のマスターを保管してください。

自分のアートワークでは、実際に使う構造を確認します。名前とグループ、クリッピング、グラデーション、変形、ストローク、テキストです。利用先のアプリケーションで書き出しファイルを開き直し、目的の編集を繰り返してください。SVG編集ガイドは、インポートコードを変える前に、元の構造を診断するのに役立ちます。

子要素だけの書き出しを意図的な断片として扱う

この実行では、artwork.svg(false)は、外側のsvgタグなしで二つのパスを返しました。アートワークのルートのビューポートは含まれません。別のSVGルートrewrappedは、子要素を受け取る前に240 × 160の寸法と同じviewBoxを用意します。その書き出しファイルは、確認済みのコーラル色三角形を描画します。

利用先に合わせて、完全なドキュメントと断片を選びます。独立したダウンロードには、目的のルート全体を残してください。既存の図面へ挿入する場合は、子要素が使う親座標系、継承スタイル、定義を決めます。この単純な例には外部リソースや共有定義がないため、参照を含む断片が必要な依存関係をすべて運ぶかはテストしていません。

画像ラッパーの書き出しは、デコードしたバイトが元のマスターと一致する、エンコード済みSVGを保持しました。この対照例は表現の違いを示すもので、ラスター変換ではありません。元データがすでに使えるベクター形状なら、再トレースする前に、要素選択または書き出し範囲を直してください。

元データに編集するパスがない場合

フラットなPNGやJPGのイラストに、別々に色変更できる領域が必要なら、PerfectVectorでSVG候補を準備します。輪郭と開口部を確認し、その後でアプリケーションに必要な部分名を付けてください。ベクター化は、ある領域をaccentと呼ぶべきだとは知らず、読み込むドキュメントがエディターのデータモデルに合うことも保証しません。

その図形がすでにある場合は、適切なSVGマスターを保管します。ベクター化の概要を使い、コードの前段にラスターからベクターへの準備が本当に必要かを判断してください。

よくある質問

SVG.jsはSVG文字列を編集用に読み込めますか? できます。このひな形は、自身の信頼できるマークアップを生きたSVGコンテナーへ読み込み、作成済みのパスを選んで塗りを変えます。任意のアップロードには別の信頼と検証の方針が必要で、マークアップAPIはサニタイザーではありません。

アートワークが見えるのにaccentパスが見つからないのはなぜですか? 生きたマークアップを読み込んだか、画像を置いたかを確認してください。画像ラッパーの対照例はSVGアートワークを表示しますが、ラッパーのツリーに直接のaccentパスはありません。選択した親と、元データの実際のIDも確認してください。

子要素だけの書き出しは完全なSVGファイルですか? この例では違います。外側のSVGビューポートがなく、二つのパスだけです。独立したファイルには目的のルート全体を使い、断片を挿入する場合は利用先のルートと座標系を明示的に用意してください。

編集の成功は、書き出しがすべてのSVG機能を保つ証拠になりますか? なりません。確認したのは二つの単純なパス、塗りの値、正確なパスデータ、開き直した五つのファイルです。自分のアートワークに必要な機能と利用先アプリケーションをテストし、元のマスターを保管してください。

出典

  1. SVG.js — インポートとエクスポート、バージョン3.2 — マークアップの読み込みと、ルート全体または子要素だけのシリアライズ。
  2. SVG.js — 要素の参照と作成、バージョン3.2 — 名前付き要素の検索とプロパティ変更。
  3. SVG.js — リリース3.2.6 — 実行したひな形で固定した公式リリース。
  4. MDN — 画像としてのSVG — 画像埋め込みのコンテキストと制約。
  5. MDN — DOMParser.parseFromString — マークアップ解析と、信頼できない入力の別処理。

イラストがラスターピクセルでしか存在せず、部分単位の編集が必要なら、自分のアートワークからベクター候補を準備してください。輪郭と開口部を確認してマスターを保管し、利用先の手順で名前付きの編集と保存したSVGを検証します。

ブログのその他の記事

PerfectVector

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

クレジットカード不要