---
title: "SVGのラスタライズをsipsとresvgとChromeで比べる"
description: "エージェントに書かせたSVGをPNGにして目視させる確認ループで、sipsだけ太字が出ませんでした。sipsがfont-weightを無視しているのではなく、boldキーワードとfont-familyの先頭が解決できない場合の2つで落ちていた話と、resvg・ヘッドレスChromeと比べた結果をまとめます。"
category: "AI"
tags: ["SVG","resvg","sips"]
publishedAt: "2026-08-14"
lastmod: "2026-08-14"
---

エージェントに書かせたSVGをPNGへ変換し、そのPNGを本人に読ませて崩れを確認するループを、macOS同梱の `sips` で回していました。`<marker>` が落ちることは分かっていたので他の機能も測ったところ、`font-weight` が全滅していました。追試すると `sips` が `font-weight` を無視しているわけではなく、落ちる条件が2つあっただけでした。条件は回避できます。ただし当たったときに何も言わずに落ちるので、[resvg](https://github.com/linebender/resvg) への乗り換えを検討しています。確認ループ自体はまだ `sips` のままです。

::card[/posts/claude-code-svg-figure-sips-loop]

差し替えるとしても1行です。

```diff
-sips -s format png fig.svg --out check.png
+resvg fig.svg check.png
```

## 3枚のプローブで測ったら、font-weightが全滅した

計測は、レンダラーの挙動を1軸ずつ切り出した3枚のSVGに対して行いました。`<marker>` とグラデーションとぼかしを並べたもの、`font-weight` と `<pattern>` を並べたもの、`font-family` の宣言を10通り並べたものです。見栄えは意図的に捨てて、機能が生き残るかどうかだけを見ています。環境はmacOS 26.6のApple Silicon、Chrome 151.0.7922.108、resvg 0.48.0です。

![resvg・sips・Chromeのレンダリング結果を縦に並べた比較画像。resvgとChromeでは700とboldの行が400の行よりはっきり太いのに対し、sipsでは3行とも同じ太さで描画されている。italicを傾けているのはChromeだけで、patternの3つの見本はどのレンダラーでも同じに出ている](/images/posts/svg-rasterize-resvg-over-sips/renderer-font-weight.png)

`400`・`700`・`bold` の3行が同じ太さで出ました。標準エラーは0バイトで、`--debug` を付けてもフォントに関する出力はありません。この時点では「`sips` は `font-weight` を見ていない」と読み、SVG側に回避策もないと判断しています。

`font-style: italic` については、傾けるのはChromeだけでした。`resvg` も `sips` も無視します。`<pattern>`・`patternTransform`・`linearGradient`・`feGaussianBlur` は3つとも同じに描いています。

## 落ちていたのは2つの条件だった

この読みが間違っていました。指定の書き方を変えながら `sips` へ通し直すと、太字が出る組み合わせがあります。

![sipsでラスタライズしたPNG。-apple-system始まりのスタックに700を指定した行とboldキーワードを指定した行は通常の太さのまま、Hiragino Sans始まりのスタックに700・600を指定した行とsans-serifに700を指定した行は太字で描画されている](/images/posts/svg-rasterize-resvg-over-sips/sips-font-weight-conditions.png)

条件は2つありました。

1つ目は値の形式です。`700` や `600` のような数値は効きますが、`bold` と `bolder` のキーワードは無視されます。`Hiragino Sans`・`Helvetica`・`sans-serif` のどれで試しても、キーワードだけが落ちました。ブラウザーと `resvg` はどちらの形式も同じに扱うので、`bold` と書いてある図は `sips` を通したときだけ太字を失います。

2つ目は `font-family` の先頭です。先頭に書いた名前を `sips` が解決できたときだけ、`font-weight` が反映されます。`-apple-system` や `BlinkMacSystemFont`、インストールされていない `Noto Sans JP` を先頭に置くと、グリフ自体はフォールバックしたゴシック体で出るのに、太さの指定だけが落ちます。同じスタックでも `'Hiragino Sans'` を先頭へ動かすと効きます。

```xml
<!-- 太字にならない -->
<text font-family="-apple-system, 'Hiragino Sans', sans-serif" font-weight="700">
<text font-family="'Hiragino Sans', sans-serif" font-weight="bold">

<!-- 太字になる -->
<text font-family="'Hiragino Sans', -apple-system, sans-serif" font-weight="700">
<text font-family="sans-serif" font-weight="700">
```

数値指定は属性でもインラインの `style` でも `<style>` のクラス経由でも同じに効いたので、書き方の自由度は残ります。上の2つを避けさえすればよい、という話です。

最初のプローブは、その2つに両方当たっていました。ルートの `font-family` が `-apple-system` 始まりのスタックで、3行のうち1行は `bold` キーワードだったためです。図版のルールとして自分で決めたフォントスタックがそのまま条件に当たっていたので、全滅に見えていました。

## sipsのオプションでは変えられない

SVG側を直す前に、コマンドライン側で渡せるものがないかを確認しました。レンダリングに触れる指定はありませんでした。

`sips --formats` を見ると、`public.svg-image` には他の形式に付いている `Writable` がありません。SVGは読み込み専用の入力で、書き出し先として選べない形式です。`sips --helpProperties` が並べる画像プロパティも `dpiWidth`・`format`・`formatOptions`・`description`・`artist` といったラスター画像のメタデータばかりで、テキストやフォントに関わるキーはありません。

実際に叩いた結果も同じでした。

| 試したもの | 結果 |
|---|---|
| `-s formatOptions best` | 出力がバイト単位で同一 |
| `-s dpiWidth 300 -s dpiHeight 300` | 描画は変わらない |
| `-Z 1240` / `-z` で大きく描かせる | 同じ行が同じ太さのまま |
| `-s format pdf` を経由してからPNGへ | 同じ |
| `-j` のJavaScriptモード | 同じ |

`-j` のスクリプトから触れる画像オブジェクトは `getProperty`・`setProperty`・`sizeToFitLongestEdge`・`size`・`aspectRatio`・`properties` だけで、SVGを入力にしたときに読めるプロパティは `PixelHeight`・`PixelWidth`・`typeIdentifier` の3つでした。`-s` と同じキーの空間を触っているだけで、増えるものはありません。

太字を出したいなら、SVG側を直すしかありません。

## それでもresvgを考えている

回避策が分かった以上、`sips` のままでも図は作れます。実際、確認ループはまだ差し替えていません。それでも乗り換え先として考えているのは3つの理由からです。

`<marker>` が落ちる問題は残ります。矢頭を `<polygon>` の実体で描く運用はすでに入れているので致命傷ではありませんが、レンダラー側を替えれば要らなくなる制約です。

条件に当たったときに何も言わないのも大きいところです。矢頭が消えれば「矢印のはずのものが罫線になっている」と目で分かりますが、太字が効いていない図は、ただ強調のない図として成立してしまいます。並べて比べるまで気づけません。

そして、当たりやすい条件でした。図版のルールに書いたフォントスタックは `-apple-system` 始まりで、これは明朝体へ落ちるのを防ぐために決めたものです。その指定を守るかぎり、`sips` では太字が必ず消えます。噛み合っていないルールどうしを、レンダラーを替えるだけで一度に解けます。フォントスタックの規約を直す手もありますが、そちらは既存の図版を全部見直すことになります。

ヘッドレスChromeは、乗り換えたとしても確認先として残す想定です。フィルターを多用した図で挙動が疑わしいときに、本物のブラウザーで確かめる先が必要です。1枚1.5秒は毎回のループには重すぎますが、最後の1回だけなら許せます。

## OS同梱のsipsより別バイナリのresvgが速い

`bench.sh 3` で1枚あたりの実時間を測った結果です。

| | ms/枚 |
|---|---|
| resvg | 23 |
| sips | 51 |
| ヘッドレスChrome | 1576 |

`resvg` は3.3MBの別バイナリを持ち込むことになりますが、OSに最初から入っている `sips` の半分以下の時間で終わります。確認ループを回す用途では、追加インストールが必要な点を差し引いても速いほうが効きます。

## Chromeの1.5秒はレンダリングではなく起動

ヘッドレスChromeの1576msは、フラグの調整では動きませんでした。何も描かない `about:blank` を開かせても1551msかかります。つまり測っているのはほぼプロセスの起動時間で、レンダリング自体は誤差に埋もれています。

`--disable-extensions`・`--disable-background-networking`・`--disable-sync`・`--disable-component-update` といった軽量化のフラグを一式足しても1546msで、有意な差は出ませんでした。`--headless=old` もChrome 151でまだ動きますが、1582msで変わりません。削る対象がレンダリング側にない以上、当然の結果です。

罠が1つありました。プロファイルを分離するつもりで `--user-data-dir` に新しいディレクトリを渡すと、Chromeが**10分以上ハング**します。スクリーンショットは1枚も書かれず、残留プロセスもなく、ただ終わりません。

短縮する唯一の手は、1つのブラウザープロセスを複数の画像にまたがって生かしておくことです。これはCDPを喋るということで、つまりPuppeteerかPlaywrightが必要です。`puppeteer-core` なら既にインストール済みのChromeへ接続できるので、Chromiumの再ダウンロードだけは避けられます。図を1枚書き直すたびに回すループには、いずれにせよ重すぎます。

## ピクセル差分率は判断材料にならない

当初は「Chromeの出力にどれだけ近いか」を数値で決められると考えていました。3枚のプローブで測った差分率がこれです。

| 図版 | resvg vs sips | resvg vs Chrome | sips vs Chrome |
|---|---|---|---|
| marker + gradient + filter | 5.6% | 5.6% | 3.3% |
| font-weight + pattern | 11.0% | 11.1% | 8.6% |
| font-family解決 | 9.7% | 10.2% | 7.4% |

3枚とも、矢頭と太字を落としている `sips` のほうが `resvg` よりChromeに近いという結果になりました。数値が測っているのは文字のラスタライズ差です。`resvg` は独自エンジン、`sips` はCoreGraphics、ChromeはSkiaと、3つとも別のエンジンでヒンティングも字間もアンチエイリアスも違うので、そのノイズが機能の欠落より大きく出ます。

実際の記事に使っている図版40枚超でも同じ方向の結果でした。Chromeに対する平均差分は `resvg` が4.2%、`sips` が4.5%で、`resvg` のほうが近かったのは6割強にとどまります。0.3ポイントの差はレンダラーを選ぶ根拠になりません。

差分率は出力サイズが揃っているかの確認に使い、判断は縦に並べた画像を見て決める、という位置づけに変えました。今回の `font-weight` の件も、差分率では何も見えていません。

## resvgではfont-family忘れが見えなくなる

乗り換えれば素直に良くなる話ばかりでもありません。`font-family` の宣言を10通り並べたプローブの結果です。

![同じ3つのレンダリング結果を縦に並べた比較画像。resvgでは10行すべてが同じゴシック体で描画されている。sipsとChromeでは、指定なし・serif・Noto Sans JP・Meiryo・存在しない名前の行が明朝体に落ちている](/images/posts/svg-rasterize-resvg-over-sips/renderer-font-family.png)

`resvg` は10行すべてを同じゴシック体で描きます。指定なしの行も、`serif` を指定した行も、存在しないフォント名を書いた行も同じです。`resvg` の既定フォントは `Times New Roman` で、日本語のグリフを持たないため `Arial Unicode MS` へフォールバックし、結果として何を指定しても同じ顔になります。

前の記事では「ルートの `<svg>` にフォントスタックを書く。書き忘れると明朝に落ちる」というルールを作り、書き忘れた図が4枚見つかったのは、既存の図をまとめてラスタライズし直して目で見たときでした。`resvg` に替えると、この見落としは**目視では拾えません**。`sips` とChromeでは明朝に落ちて見えるものが、`resvg` では正しく見えてしまいます。

代わりに `resvg` はstderrへ警告を出します。

```text
Warning (in usvg::text:183): Fallback from Times New Roman to Arial Unicode MS.
```

目で気づくより、この警告を自動で拾うほうが確実なので、乗り換えるならここを見ることになります。

```bash
resvg fig.svg out.png 2>&1 >/dev/null | grep -q Fallback && echo "unresolved font"
```

なお、フォントの解決結果は入っているフォントに依存します。検証したマシンには `Hiragino Sans` はありましたが `Noto Sans JP` と `Meiryo` はなく、その2つを名指しした宣言は素通りしています。手元の状況は `resvg --list-fonts` で確認できます。

## テキスト実装を総入れ替えしても出力は変わっていなかった

`resvg` 0.48.0（2026-07-31）は、テキスト周りの両輪を入れ替えたリリースです。[CHANGELOG](https://github.com/linebender/resvg/blob/af7573d4431226cd9f8ed7d8b7e4df4e22dd8586/CHANGELOG.md)によれば、シェーピングが `rustybuzz` から `harfrust` へ、フォントのパースが `ttf-parser` から `skrifa`（fontations）へ移っています。グリフの送り幅の計算や、既定値のままの `font-weight` が `wght` 座標を設定してしまう問題の修正も入っています。

これだけテキストに触っているので、0.47.0（2026-02-05）との出力を比べました。

| 図版 | 差分ピクセル |
|---|---|
| marker + gradient + filter | 0.00%（変化した領域なし） |
| font-weight + pattern | 0.00%（なし） |
| font-family解決 | 0.00%（なし） |

バイト単位で同一でした。どちらのバージョンに固定しても図版の見た目は変わらないことになります。ただしこれはプローブが触っている範囲の話で、可変フォント・複雑な字形・双方向テキストは確かめていません。

速度のほうは動きました。`03-font-family-resolution.svg` を20回、2パス測って、0.47.0が27.3ms・26.6ms、0.48.0が23.3ms・22.4msです。おおよそ15%速く、2パスとも同じ方向に出ました。`skrifa` への移行が効いている可能性はありますが、そこまでは切り分けていません。

## プローブの書き方でレンダラーの評価が変わる

今回いちばん効いたのは、`sips` の挙動そのものより、それを測ったプローブの書き方でした。1枚のSVGにルートの `font-family` を1つだけ置いて全行に継承させる作りだったので、そのスタックが当たった条件が全行に乗り、レンダラーの側の性質として見えていました。

機能ごとに1軸を切り出したつもりでも、切り出せていない軸が背後で効いていることがあります。プローブに残っている共通の前提は、次から先に列挙してから測ります。

同じ疑いは `font-style: italic` にもかかります。あの行もルートのスタックを継承していたので、`font-weight` と同じ理由で落ちていた可能性がありました。こちらは測り直しています。先頭を `Helvetica` や `'Hiragino Sans'` にしても、`Helvetica-Oblique` を直接名指ししても、`sips` の出力は1行も傾きませんでした。`font-style` のほうは条件付きではなく、そのまま無視されています。