💄

data: URL 向けに文字列を%エンコーディングするRubyワンライナー

2023/06/08に公開

CSS に data: URL を使って、データを埋め込む場合、画像などは Base64 エンコーディングを利用しますが、埋め込みたいデータが SVG などのテキストをベースとするデータの場合、 Base64 エンコードではなく、URIの標準的なエスケープ方法(%エンコーディング) を使う方が可読性やサイズなどの点で有利になる場合があります。

この際に重要なのは、埋め込む際にどの文字をエスケープしなければならないかの情報です。
この記事では、 Fetch Standard の要件の簡単なまとめと、それに従ったエスケープを行うための Ruby ワンライナーを記載します。

エスケープが必要な文字

RFC2397 (Obsoleted)

"data" URL scheme を提案した RFC2397 で定義されているデータ本体(body)に用利できる文字は、RFC2396 で定義されているURIに利用できる文字(uric)に準拠しており、エンコードしなければならない文字の判定は大体以下の Ruby コードで表すことが出来ました:

RFC2397準拠
require "erb"
alphanum = %q{[a-zA-Z0-9]}
mark = %q{[-_.!~*'()]}
reserved = %q{[;/?:@&=+$,]}
char2escape = /[^#{reserved}#{alphanum}#{mark}]/
ans = ''
while gets
  print($_.chomp.gsub(char2escape){|c|ERB::Util.url_encode(c)})
end

要するに、この仕様では 文字クラス(正規表現における文字の集合) alphanummarkreserved のいずれにも含まれていない文字はエスケープ対象です。
※こちらの仕様では結構多くの文字がエスケープされます。

Fetch Standard

しかし、近年のブラウザ標準を定めている WHATWGFetch Standard ではこの仕様を大幅に緩和していて、簡単にいうと以下のような仕様になります。

  1. data: URL の body 部分は byte sequence で構成される。
  2. byte sequence は 0xXX 形式の byte をスペースで区切ったものとして表記される。
  3. ただし、byte の値の範囲が 0x200x7e に収まっている byte sequence は通常の文字列として表示しても良い。※body はこの規定に基づいて文字列で表現されている。
  4. body を解釈する前には%デコードを行う。すなわち、エスケープするべき文字は data: URL では%エンコードされる。
  5. # (0x23) は URI における Fragment の予約文字として解釈されるので、この値が body 中で出てくる場合はエスケープする

また、この data: URL を CSS や HTML に埋め込む場合、以下のような記法になります:

CSS
:root {
  --ver: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg'>...</svg>");
}
HTML
<img src="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg'>...</svg>"

この際、URLの中身はダブルクォーテーション " により囲んで記載する関係で、CSSに利用する場合はこの文字もエスケープしないとブラウザのパーサが理解することが出来なくなりますので、次の副次的な要件が発生します:

  1. " (0x22) もエスケープする

エスケープを行うワンライナー

上記を反映した、SVGなどのテキストデータを data: URL の body に埋め込むためにエスケープするための Ruby ワンライナーは以下になります:

Fetch Standard準拠
ruby -r cgi -npe '$_.chomp!.gsub!(/[^ !\x24-\x7e]/){|c|CGI.escape(c)}' < $INPUT

※改行コードを残す場合は chomp! を削除して $_.gsub! としてください。

補足: [\x20-\x21] は2文字だけなので下記のように実際の文字で表記しています:

正規表現 文字 補足
\x20 半角スペース
\x21 ! エクスクラメーションマーク

多くの場合動く代替案

Chrome などの実際のブラウザの多くでは body 部分の文字列が [\x20-\x7e] の範囲に収まっていなくても UTF-8 の範囲であればきちんと解釈してくれるように見えますので、標準に厳密に準拠する必要がないのであれば、以下の変換でもおそらく十分です。

省略版
ruby -r cgi -npe '$_.chomp!.gsub!(/[#"]/){|c|CGI.escape(c)}' < $INPUT

SVGやXMLなどを埋め込む際のコツ

XMLや(XMLに基づく)SVGでは、Attribute Value を囲うための記号として、ダブルクォーテーション " だけでなくシングルクォーテーション ' も使えるので、できるだけシングルクォーテーションを使うようにすると、エスケープの量を最小化することが出来ます。

例1.svg ※こちらだとエスケープが増える
<svg xmlns="http://www.w3.org/2000/svg" style="background:#AAA"></svg>
エスケープ後
<svg xmlns=%22http://www.w3.org/2000/svg%22 style=%22background:%23AAA%22></svg>

例1.svg と同じ内容をシングルクォーテーションで書いたものはこちら:

例2.svg ※こちらがベター
<svg xmlns='http://www.w3.org/2000/svg' style='background:#AAA'></svg>
エスケープ後
<svg xmlns='http://www.w3.org/2000/svg' style='background:%23AAA'></svg>

こちらだとエスケープは # 一箇所です。

Discussion