Tips

CSSのbackground-imageでurlで生のsvgコードを埋め込む際の罠

当サイトでは、ページロード時に画像がロードを完了するまでに表示されるくるくる回るローディング画像をhtml内に記載するCSSにbackground-imageでurl()に直接svgコードを埋め込んでいます。そのsvgコードはbase64にてエンコードしていたのですが、別にエンコードしなくても表示できるという情報を目にしました。なので、エンコードしないでやってみたら思わぬエラーが発生したので、その対処法をご説明します。

base64エンコードしなくていい?

よく画像が読み込まれるまでの間にくるくる回るローディング画像ありますよね。あれ自体が画像で、対象の画像が読み込まれるずっと前には読み込まれている必要があるので、外部ファイルにするのではなく、HTML内に直接埋め込むというのが一つの方法としてあります。

当サイトでも、以下のようにHTML内のstyleタグでCSSを記載し以下のように background-image の url() でbase64でエンコードしたsvgコードを直接書き込んでいました。以下の3行目の base64, と ') !important; に挟まれた部分が、元はXML形式だったsvgコードをbase64でエンコードした文字列です。ちなみに実際はもっとずっと長い文字列なのですが、記事で説明すると見づらくなってしまうので ... の部分は省略になります。エンコードすると、このように目視では何を意味しているか全くわからなくなってしまいます。

少し気になって調べてみると、ここではエンコードしないで実装している例を発見しました。

【CSS+SVG】インラインSVGを背景画像に指定

ただ、svgコードの改行を無くして1行にする必要があるようです。base64エンコードの場合は、エンコード前のsvgコードから改行を無くす必要はなく、まるっとエンコードしてくれました。もちろん、エンコード後の文字列は改行の無い長い文字列ですが。

とはいえ、エンコードしなければ可読性は上がります。また、エンコードにより元データよりバイト数が約33%多くなってしまうことがわかりました。

符号化によるサイズの増加

なのでHTMLの読み込み時間を少しでも短縮したいなら、もちろん元のXML形式の方が良いのです。

可読性と読み込み速度の向上のために、エンコードしない書き方に変えてみることにしました。

#が使えない?

すぐにXML内の改行を削除し、 data:image/svg+xml;base64base64 という部分を utf8 にしてCSSを書き換えてみました。ちなみに、 utf8 という部分、 charset=UTF-8 などのようにしている例もありますが、どちらでもいいようです。こちらも ... は省略です。

しかし、うまく表示されません。

Chromeのインスペクターから、対象のSVGのみ別タブで開けるのですが、見てみると以下のようなエラーが表示されていました。

This page contains the following errors:
error on line 1 at column 133: AttValue: ' expected
Below is a rendering of the page up to the first error.

参考にしたサイトの例と自分のXMLのコードを調整しながら、原因を調査しました。参考にしたサイトはシンプルなものでしたが、当方のsvgはアニメーションなどがあります。最初アニメーションは埋め込んだ場合は機能しないのかな、などと疑いました。でも、以下のサイト例ではきちんとアニメーションも機能しています。

Can you animate a svg "background-image"?

かなり時間がかかりましたが、コード内に # があるために起きているエラーということがわかりました。CSSの例ではないですが、以下の記事を読み気づきました。

file with hash (#) in its name does not open in web browser

# はURL内で使われると特殊な役割を持ちます。この文字はパーセントエンコードしないときちんと機能しないとのことです。

# がsvgのXML内でどう使われていたかというと、カラーコードです。当方のsvgの一部に以下のような部分があるのです。

このようにイメージの中にはどうしても色を決める部分があるのでこれは困りました。でも最初のサイトの例を見ると、カラーはrgb()を使っていました。

または、一部の色は以下のように指定することもできます

このような色の指定の仕方ならきちんと表示されました。とは言え、rgb()を使った指定法は文字列が少し長くなり、 # を使った方法の方が便利なので、 # をパーセントエンコードすることにしました。base64のエンコードでは、こんなことを考える必要は全くなく、まるッとエンコードすれば良いのである意味楽ではありましたね。

先ほどの記事に、 #はパーセントエンコードすると %23 になるとあったので、全て置換しました。するときちんと表示されました!

urlencodeかrawurlencodeか

まぁ、 #のみ置換すればいいかな、とも思ったのですが、もし今後別のSVGを変える際に、 別の特殊文字で同じようなことが起きてしまうことも考えられるので、なるべく普遍的な方法で対処しておきたいところです。

先ほどのページにphpでは urlencode() という関数でエンコードすれば良いとあったので、これでXML全体をエンコードすれば良いのだろうと考えました。

なので、このコードでエンコードした文字列をCSSに埋め込みました。でも、またエラーが発生しました。

This page contains the following errors:
error on line 1 at column 5: error parsing attribute name
Below is a rendering of the page up to the first error.

また時間をかけて原因を調査しました。urlencodeのPHP公式ページrawurlencode というコマンドがあるのを発見しました。これは urlencode とどう違うのだろうと思いましたが、一度こっちでやってみました。するとなんと正常にsvgが表示されました。

そもそも、CSSのurl()で指定できる文字列は以下によるとRFC3986に準拠する必要があります。

URLs and URIs

で、以下によると rawurlencodeはRFC3986に準拠しているが、 urlencode はしていないということです。

urlencode vs rawurlencode?

具体的にどう違うかというと、パーセントエンコードがメインなのは同じなのですが、スペースの扱い方が違います。スペースを rawurlencode%20 にエンコードするのですが、 urlencode+ にエンコードしてしまいます。これには歴史的な理由があるようなのですが、CSSのurl()は + をスペースと解釈しないので、上記のエラーが出るということです。

でも#だけ置換した方がバイト数少なくていいよね

ここまで原因を追求したので、自信を持って rawurlencodeを使おうかとも思いましたが、よく考えると、データ量を小さくするのが最初の目的でした。 rawurlencodeでXML全体をパーセントエンコードすると文字列が元より大きくなってしまいます。スペースが一文字が %20という3文字になることからも明らかです。

なので、最初に思いついた解決策、つまり、 #のみ %23に置換するという方法を採用することにしました。