開発の動機
Rust で SVG を扱おうとするといくつか既存のクレートはあるが、どれも依存クレートが多くて気が進まない。依存は少なければ少ないほど良い。
SMIL アニメーション (<animate> とか) を評価できるものもない。パスのフラット化やストローク変換だけ使いたくても、レンダリングパイプラインに密結合していて切り出せない。
ほしい機能は明確だったので自分で書いた。
- SVG のパース
- ベジェ曲線のフラット化
- ストロークのフィル変換
- SMIL アニメーション評価
全部 std のみで、外部クレート依存ゼロ。
概要
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