はじめに
SVGファイルは点・線・形の情報で描画するため、解像度という概念がなく、拡大・縮小してもぼやけないと思っていました。
しかし、SVGファイルを使用しているのに、ぼやけて見えるファイルを発見!
原因を調査しました。
ぼやける原因
原因は 埋め込みPNGのSVGファイル でした。
PNGはサイズが拡大・縮小されると解像度が低くなるため、ぼやけて見えてしまいます。
埋め込みPNGのSVGファイルとは!?
純粋なSVGファイルは <path> などのベクター情報で描画されます。
一方、埋め込みPNGの場合は、SVG内に <image> タグとしてPNG画像が埋め込まれています。
どうして埋め込みPNGのSVGが存在する!?
通常の <image> タグでは、外部PNGファイルを参照するため、ファイルパスが変わるとリンク切れで表示されなくなるリスクがあります。
SVGに埋め込んでしまえば、そのリスクを回避できます。
例
純粋なSVG
ChatGPTに作成してもらった純粋なSVGファイルをエディタで開いた内容です。
描画するタグでできています。
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 800 450">
<defs>
<linearGradient id="bg" x1="0" y1="0" x2="1" y2="1">
<stop offset="0%" stop-opacity="1" stop-color="#a1c4fd"/>
<stop offset="100%" stop-opacity="1" stop-color="#c2e9fb"/>
</linearGradient>
<radialGradient id="orb" cx="50%" cy="50%" r="50%">
<stop offset="0%" stop-color="#ffffff" stop-opacity="0.9"/>
<stop offset="100%" stop-color="#6dd5ed" stop-opacity="0.9"/>
</radialGradient>
<filter id="softShadow" x="-20%" y="-20%" width="140%" height="140%">
<feDropShadow dx="0" dy="8" stdDeviation="12" flood-color="#000000" flood-opacity="0.25"/>
</filter>
<pattern id="dots" x="0" y="0" width="20" height="20" patternUnits="userSpaceOnUse">
<circle cx="2" cy="2" r="2" fill="#ffffff" opacity="0.35"/>
</pattern>
<clipPath id="roundedFrame">
<rect x="16" y="16" width="768" height="418" rx="24" ry="24"/>
</clipPath>
</defs>
<!-- Background -->
<rect width="100%" height="100%" fill="url(#bg)"/>
<!-- Framed area with subtle pattern -->
<g clip-path="url(#roundedFrame)">
<rect x="16" y="16" width="768" height="418" fill="url(#dots)" opacity="0.25"/>
<!-- Decorative waves -->
<path d="M0,320 C120,260 240,380 360,320 C480,260 600,380 800,320 L800,450 L0,450 Z"
fill="#ffffff" opacity="0.35"/>
<path d="M0,360 C150,300 300,420 450,360 C600,300 700,420 800,360 L800,450 L0,450 Z"
fill="#ffffff" opacity="0.5"/>
</g>
<!-- Orbs -->
<circle cx="650" cy="110" r="60" fill="url(#orb)" filter="url(#softShadow)"/>
<circle cx="170" cy="160" r="38" fill="url(#orb)" opacity="0.9"/>
<circle cx="270" cy="90" r="22" fill="#ffffff" opacity="0.8"/>
<!-- Geometric composition -->
<g transform="translate(120,220)">
<rect x="-50" y="-50" width="100" height="100" rx="16" fill="#ffffff" opacity="0.85" filter="url(#softShadow)"/>
<polygon points="120,-40 200,40 120,120 40,40" fill="#ffd166" opacity="0.9" filter="url(#softShadow)"/>
<circle cx="280" cy="40" r="48" fill="#06d6a0" opacity="0.9" filter="url(#softShadow)"/>
</g>
<!-- Title text -->
<g font-family="system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, sans-serif">
<text x="40" y="72" font-size="28" fill="#ffffff" opacity="0.95">Sample SVG</text>
<text x="40" y="104" font-size="16" fill="#ffffff" opacity="0.85">自由に編集して使ってね</text>
</g>
<!-- Animated accent line -->
<path id="line" d="M40,400 Q200,360 360,400 T680,400" fill="none" stroke="#ffffff" stroke-width="3" opacity="0.8">
<animate attributeName="stroke-dasharray" values="0,1000; 1000,0" dur="6s" repeatCount="indefinite"/>
</path>
</svg>
↓見た目(Qiitaにsvgファイルを貼ることができなかったため、スクショで代用しています)

埋め込みPNG
こちらもChatGPTに作成してもらった埋め込みPNGをエディタで開いた内容です。
imageタグが埋め込まれています。
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 800 450">
<image href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAyAAAAHCCAIAAACYATqfAAAIDElEQVR4nO3WQQ3AIADAQMC/Ktyggc9MNCFZ7hT02bnPHQAAdNbrAACAvzFYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEDNYAAAxgwUAEPsAVqQGKv/P6lQAAAAASUVORK5CYII=" width="800" height="450"/>
<text x="40" y="72" font-size="28" fill="#000000" opacity="0.8">Sample PNG Embedded</text>
</svg>
↓見た目(Qiitaにsvgファイルを貼ることができなかったため、スクショで代用しています)

おわりに
SVGファイルと言っても、中身や特徴が異なるものもあることを知りました。
画像に関して不可解な事象が起きたら、エディタで中身の確認をしてみようと思います。
おまけ:主なSVGベクタータグ一覧
主なSVGベクタータグについてChatGPTにまとめてもらいました。
参考: https://developer.mozilla.org/ja/docs/Web/SVG/Tutorials/SVG_from_scratch/Basic_shapes
| タグ | 用途 | 備考 |
|---|---|---|
<svg> |
SVGのルート要素 | 他のタグを内包 |
<g> |
グループ化 | 複数の要素をまとめて操作可能 |
<path> |
任意の線や曲線 | 曲線や複雑な形を描画 |
<rect> |
四角形 | x, y, width, height, rx, ry で制御 |
<circle> |
円 | cx, cy, r で制御 |
<ellipse> |
楕円 | cx, cy, rx, ry で制御 |
<line> |
線 | x1, y1, x2, y2 で制御 |
<polyline> |
折れ線 | 点を順番に結ぶ線 |
<polygon> |
多角形 | 点を順番に結び、最後は閉じる |
<text> |
テキスト | x, yで位置指定、フォントなど制御可能 |
装飾・定義用の補助タグ
| タグ | 用途 |
|---|---|
<defs> |
グラデーションやパターンなどの定義 |
<linearGradient> |
線形グラデーションの定義 |
<radialGradient> |
放射状グラデーションの定義 |
<pattern> |
繰り返しパターンの定義 |
<clipPath> |
クリッピング(切り抜き) |
<mask> |
マスク処理 |