SVGはXMLで図形を記述するベクター画像形式で、どのサイズに拡大しても劣化しない、CSSで色を動的に変更できるなど、フロントエンド開発では扱いやすい形式です。それでも「SVGで作ったアイコンをOGPに設定したのに反映されない」「Slackに貼り付けてもプレビューが出ない」という経験をした人は多いはずです。

SVGを受け付けないプラットフォームは思いのほか多く、そういった場面ではPNGへの変換が必要になります。この記事では、どの場面でPNGが必要になるのか、そしてブラウザがどうやってSVGをPNGに変換しているのかを整理します。

SVGを受け付けないプラットフォーム

OGP(Open Graph Protocol)の og:image にSVGを指定しても、Facebook・Twitter(現X)・LINE・Discordのいずれもプレビューに使いません。OGP仕様自体は画像フォーマットを制限していませんが、各プラットフォームが独自にJPEGとPNGを対象として処理しているためです。

メールも同様です。GmailやOutlookのHTMLメールに <img src="*.svg"> を埋め込んでも、表示されないクライアントが多く存在します。特にOutlookはSVGレンダリングに対応しておらず、代替テキストまたは空白として扱われます。

iOSのホーム画面ショートカットアイコン(apple-touch-icon)はPNG必須で、Slackのカスタム絵文字・アイコンもPNG・GIF・JPEGのみ受け付けます。

これらに共通する理由は2つあります。第一に、プラットフォームが画像をサーバーサイドでリサイズ・キャッシュする際、SVGのラスタライザを持っていないことです。第二に、SVGはJavaScriptや外部リソースを埋め込める構造を持つため、ユーザー生成コンテンツとして扱う場面ではXSSのリスクとして弾かれます。

ベクターとラスターの根本的な違い

SVGは「この座標に半径20pxの円を描く」「このパスに沿って線を引く」という命令の集合です。数式で図形を記述するため、1000×1000で表示しても4000×4000で表示しても計算し直すだけで輪郭が劣化しません。

PNGはピクセルの集合です。256×256ピクセルのPNGは縦横それぞれ256個のピクセルの色情報を持つだけです。これを4倍に拡大すると1ピクセルを4×4に引き伸ばすことになり、輪郭がぼやけます。

プラットフォームがSVGを弾く最も単純な理由は、「受け取った画像をピクセル単位で操作したい」からです。サムネイルの生成にもキャッシュにも、最終的にはピクセルの集合が必要になります。

ブラウザがSVGをPNGに変換する仕組み

SVGからPNGへの変換はサーバーを必要とせず、ブラウザだけで完結します。Canvas APIを使った変換は4ステップで構成されます。

① SVG文字列をBlobに変換してURL化する

const svgBlob = new Blob([svgCode], { type: 'image/svg+xml;charset=utf-8' });
const url = URL.createObjectURL(svgBlob);

URL.createObjectURL() でBlobを指すローカルURL(blob:https://... 形式)を生成します。Blob URLはオリジンをまたがないローカルリソースとして扱われるため、Canvas のセキュリティ制限(tainted canvas)を回避できます。クロスオリジンのURLを <img> に読み込んでCanvasに描画するとCanvasがtainted状態になり、toDataURL() がセキュリティエラーを投げます。Blob URLを経由すれば同一オリジンのリソースとして扱われるため、その問題が起きません。

<img> 要素にBlobのURLを読み込む

const img = new Image();
img.onload = () => { /* 次のステップへ */ };
img.src = url;

SVGを直接Canvas APIに渡す方法はないため、一旦 <img> 要素を経由してブラウザにSVGをパース・レンダリングさせます。onload が発火した時点で、SVGはブラウザ内部でビットマップとして展開されています。

③ Canvasに drawImage() で描画する

const canvas = document.createElement('canvas');
canvas.width = 256;
canvas.height = 256;
const ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0);
URL.revokeObjectURL(url);

drawImage() でSVGの内容がCanvasのピクセルバッファに書き込まれます。Canvasのサイズが出力PNGの解像度を決定します。使い終わったBlob URLは revokeObjectURL() で解放します。

toDataURL() でPNGデータを取得する

const pngDataUrl = canvas.toDataURL('image/png');

Canvasのピクセルバッファが image/png に圧縮され、data:image/png;base64,... という形式で返されます。ダウンロードリンクの href に設定するか、fetch() でBlobに変換してからClipboard APIでコピーすることもできます。

Retina対応でなぜ2倍サイズが必要か

RetinaディスプレイなどHigh DPIディスプレイは、CSSの1ピクセルが物理的な2ピクセル(またはそれ以上)に対応します。デバイスピクセル比(DPR)が2のディスプレイで256×256のPNGを表示すると、実際には512×512の物理ピクセルに引き伸ばされ、輪郭がぼやけます。

これを防ぐには、Canvasを目標の2倍(512×512)で作成してPNGを出力し、表示時は width: 256px で半分のサイズに指定します。SVGであれば解像度を意識する必要はありませんが、PNG変換の時点でどの解像度を選ぶかは決めておく必要があります。OGP画像は各プラットフォームが推奨サイズを定めており(Facebookは1200×630px)、そのサイズで出力することで劣化なしに表示されます。

透明背景はPNGでも保持できる

SVGは背景を持たない(透明)のがデフォルトで、PNGもアルファチャンネルをサポートするため透明背景を保持できます。JPEGはアルファチャンネルを持てないため、透明が必要な用途では必ずPNGを選びます。

Canvas APIの変換で透明背景を保持するには、fillRect() による背景塗りつぶしを行わないだけで済みます。getContext('2d') で取得した直後のCanvasはすべてのピクセルのアルファ値が0(完全透明)で初期化されており、何もしなければ透明背景のPNGが出力されます。

使い分けは単純で、Slackの絵文字やロゴ素材として使う場合は透明背景で出力し、OGP画像やファビコンなど背景色が必要な場合は fillRect() で塗りつぶしてからSVGを描画します。

ブラウザ上でSVGコードのリアルタイムプレビューとPNG変換を試したい場合は、以下のツールでキャンバスサイズ・背景色・Retinaスケール・透明背景の設定を切り替えながら確認できます。