ginokent Blog
RSS EN

svg-rs: ゼロ依存の SVG パーサーと SMIL アニメーション評価器を Rust で書いた

開発の動機

Rust で SVG を扱おうとするといくつか既存のクレートはあるが、どれも依存クレートが多くて気が進まない。依存は少なければ少ないほど良い。
SMIL アニメーション (<animate> とか) を評価できるものもない。パスのフラット化やストローク変換だけ使いたくても、レンダリングパイプラインに密結合していて切り出せない。


ほしい機能は明確だったので自分で書いた。

  • SVG のパース
  • ベジェ曲線のフラット化
  • ストロークのフィル変換
  • SMIL アニメーション評価

全部 std のみで、外部クレート依存ゼロ。


ginokent/svg-rs

概要

svg-rs は SVG をパースして PathSegment に変換し、フラット化・ストローク変換・SMIL 評価まで行えるライブラリ。各機能が独立した関数なので、必要なものだけ使える。

  • SVG パース<path>d 属性はもちろん、<rect>, <circle>, <ellipse>, <line>, <polyline>, <polygon> も内部的に PathSegment に正規化する
  • グラデーション・clipPath<linearGradient>, <radialGradient>, <clipPath>, <defs>/<use> に対応
  • パスフラット化 — 2 次・3 次ベジェ曲線や円弧を tolerance 指定で直線列に近似
  • ストロークのフィル変換 — 線幅・line cap・line join を考慮してフィルパスに変換
  • SMIL アニメーション評価<animate>, <set>, <animateTransform> を解釈して任意の時刻の属性値を計算

インストール

ライブラリとして:

cargo add --git https://github.com/ginokent/svg-rs.git

CLI ツールとして:

cargo install --git https://github.com/ginokent/svg-rs.git --features cli

使い方

SVG のパース

parse() に SVG データを渡すと SvgDocument が返ってくる。

use svg::parse;

fn main() {
    let svg_data = br#"
        <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
            <rect x="10" y="10" width="80" height="80" fill="blue" />
            <circle cx="50" cy="50" r="30" fill="red" />
        </svg>
    "#;

    let doc = parse(svg_data).expect("SVG のパースに失敗");

    for child in &doc.root.children {
        println!("{:?}", child);
    }
}

図形要素は全部 PathSegment のリストに正規化されるので、後段の処理を統一的に書ける。

パスのフラット化

ベジェ曲線や円弧を直線の列にする。ラスタライズやポリゴン化するときに要る。

use svg::{parse, flatten, SvgNode};

fn main() {
    let svg_data = br#"
        <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
            <path d="M10,80 C40,10 65,10 95,80 S150,150 180,80" fill="none" stroke="black" />
        </svg>
    "#;

    let doc = parse(svg_data).expect("SVG のパースに失敗");
    let segments = match &doc.root.children[0] {
        SvgNode::Path(path) => &path.segments,
        _ => panic!("パス要素が見つからない"),
    };

    // tolerance が小さいほど精度が上がるけど点が増える
    let polylines = flatten(segments, 0.5);

    for polyline in &polylines {
        println!("点の数: {}", polyline.len());
        for (x, y) in polyline {
            println!("  ({}, {})", x, y);
        }
    }
}

tolerance はピクセル単位の最大誤差。大体 0.5 で十分。

ストロークのフィル変換

ストローク (線) をフィルパスに変換する。レンダラーを書くときにストロークとフィルを統一的に扱えるので実装が楽になる。

use svg::{parse, stroke_to_fill, StrokeStyle, SvgNode, Paint, LineCap, LineJoin, Color};

fn main() {
    let svg_data = br#"
        <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
            <path d="M10,50 L90,50" stroke="black" stroke-width="10"
                  stroke-linecap="round" stroke-linejoin="round" />
        </svg>
    "#;

    let doc = parse(svg_data).expect("SVG のパースに失敗");
    let segments = match &doc.root.children[0] {
        SvgNode::Path(path) => &path.segments,
        _ => panic!("パス要素が見つからない"),
    };

    let style = StrokeStyle {
        paint: Paint::Color(Color { r: 0, g: 0, b: 0, a: 255 }),
        width: 10.0,
        cap: LineCap::Round,
        join: LineJoin::Round,
        dash: None,
        opacity: 1.0,
    };

    let fill_segments = stroke_to_fill(segments, &style);
    println!("変換後のセグメント数: {}", fill_segments.len());
}

SMIL アニメーションの評価

SVG の SMIL アニメーション要素を解釈して、指定した経過時間での属性値を返す。

use svg::{parse, evaluate};

fn main() {
    let svg_data = br#"
        <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
            <rect x="10" y="10" width="80" height="80" fill="blue">
                <animate attributeName="x" from="10" to="50" dur="2s" fill="freeze" />
            </rect>
        </svg>
    "#;

    let doc = parse(svg_data).expect("SVG のパースに失敗");
    let animation = &doc.animations[0];

    for t in [0.0, 1.0, 2.0] {
        if let Some(value) = evaluate(animation, t) {
            println!("t={:.1}s: {:?}", t, value);
        }
    }
}

対応している SMIL 要素:

要素対応する属性の例
<animate>x, y, width, height, opacity, fill など
<set>任意の属性 (離散値の切り替え)
<animateTransform>translate, rotate, scale, skewX, skewY
<animateMotion>認識するがフル対応ではない

CLI ツール (svg-cli)

svg-rs の機能を CLI から叩けるツールも付いてくる。


静止画 PNG の出力:

svg-cli -i input.svg -o output.png --width 512 --height 512

SMIL アニメーション入りの SVG から APNG 生成:

svg-cli -i input.svg -o output.apng --fps 30 --duration 3.0

特定の時刻での静止画出力:

svg-cli -i input.svg -o frame.png --time 1.5

主なオプション:

オプション説明
-i, --input入力 SVG (必須)
-o, --output出力ファイル (必須, .png or .apng)
--width, --height出力サイズ (viewBox から自動設定)
--fpsフレームレート (デフォルト: 30)
--durationアニメーションの長さ (秒, デフォルト: 3.0)
--time静止画の時刻オフセット (秒, デフォルト: 0.0)

プロジェクト構成

svg-rs/
├── src/
│   ├── lib.rs              # 公開 API
│   ├── types.rs            # 全公開型定義
│   ├── parse.rs            # SVG パース エントリポイント
│   ├── xml/                # XML トークナイザー・ツリー構築
│   ├── svg/                # SVG 要素変換・スタイル・色
│   ├── path/               # フラット化・ストローク変換
│   ├── smil/               # SMIL パーサー・評価器
│   └── bin/cli/            # CLI (main, render, animation, apng)
├── tests/
├── Cargo.toml
└── README.md

モジュールごとに独立しているので、xml だけ、path だけ読んでも分かるようにしてある。

特徴

ビルドが速い。 外部クレートがないのでクリーンビルドが数秒で終わる。CI も速い。


Wasm で使える。 std のみなので wasm32-unknown-unknown へのコンパイルがすんなり通る。ブラウザ上で SVG を Wasm でレンダリングする、みたいな用途にそのまま使える。no_std 対応もそのうちやりたい。


サプライチェーンリスクがない。 当たり前だが、依存ゼロなので、サプライチェーン攻撃を気にしなくていい。

注意点

今のところ未対応のもの:

  • CSS <style> 要素によるスタイル指定 (インラインスタイルは対応済み)
  • <text> 要素
  • <filter> 要素
  • <mask> 要素
  • <animateMotion> のフル対応 (認識はするがパース未対応)

必要に応じて対応予定。

まとめ

ゼロ依存で SVG パース・フラット化・ストローク変換・SMIL 評価ができる Rust ライブラリを作った。グラデーションや clipPath にも対応している。軽量な SVG レンダラーを自作したいとか、SMIL アニメーションから APNG を生成したいとか、そういう用途に使える。


リポジトリ: ginokent/svg-rs