SVGなら表示できるが、SVGZが表示できない。クライアント側で解決できた。
成果物

SVGZをブラウザで表示する方法
SVGZはSVGをGzip圧縮しただけなので、それを展開してSVGで返却してやれば表示可能なはず。そのための機能をサーバかクライアントで実装すればいいはず。
- Webサーバを自作する(前回)
- クライアントで展開する
- Webサーバを自作する
1. Webサーバを自作する
1. Apatch
ググったら出てくる方法。
WebサーバアプリApatch上で.htaccessファイルを配置し、以下内容を追記する。
AddType image/svg+xml .svg .svgz AddEncoding gzip .svgz
ただ、私はApatchサーバを作ったことがないので、それ以前に何をどうすればいいかサッパリ判らない。いきなり上記のようなことを言われてもまるで判らない。
2. Python
前回、PythonでHTTPSローカルサーバを立てた。これをそのまま使えば表示できるかも? と思ったがダメだった。
おそらく拡張子svgzファイルをgzip展開して返却するような機能をサーバに実装すれば表示できるようになるだろうが、それをPythonコードでどう実装すればいいのかサッパリ判らない。
流石にPythonやそのライブラリから勉強し直すのは避けたい。私はただSVGZを表示して欲しいだけなのだから。一応、少しだけコードを書いて試行錯誤したがダメだった。
1. extensions_map
前回のサーバに以下を挿入した。
Handler.extensions_map['.svgz'] = 'image/svg+xml'
他にも以下のようなパターンを試してみた。
Handler.extensions_map['.svgz'] = 'image/svg+xml;Content-Encoding=gzip;'
Handler.extensions_map['.svgz'] = 'image/svg+xml;charset=UTF-8;Content-Encoding=gzip;'
が、ダメだった。SVGZファイルを参照する<img>タグで画像が表示されなかった。念の為、全体のコードを掲載しておく。
run_server.py
import ssl from http.server import HTTPServer, SimpleHTTPRequestHandler PORT = 443 CERTFILE = "./localhost.pem" Handler = SimpleHTTPRequestHandler Handler.extensions_map['.svgz'] = 'image/svg+xml;charset=UTF-8;Content-Encoding=gzip;' context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) context.load_cert_chain(CERTFILE) with HTTPServer(("", PORT), Handler) as httpd: print("serving at address", httpd.server_address, "using cert file", CERTFILE) httpd.socket = context.wrap_socket(httpd.socket, server_side=True) httpd.serve_forever()
2. SimpleHTTPRequestHandler
SimpleHTTPRequestHandlerはPythonでHTTPサーバを作る時のクラス。前回コードでもこれを使った。これを改造してSVGZのときはSVGに変換して返すように実装すればいいはず。
私はサーバの知識がまったくない。でも、適当にググってコードを書いてみた。
- How to use Content-Encoding: gzip with Python SimpleHTTPServer
- SimpleHTTPServerでmime typeを追加、文字コードを指定
- gzip化されたhttpレスポンスを解凍する
- PythonのHTTPServer/SimpleHTTPRequestHandlerを使って簡易なhttp severを立てる
- GzipSimpleHttpServer.py
import os.path import zlib import urllib from http.server import SimpleHTTPRequestHandler #https://github.com/ksmith97/GzipSimpleHTTPServer/blob/master/GzipSimpleHTTPServer.py#L85 class SvgzHTTPRequestHandler(SimpleHTTPRequestHandler): def do_GET(self): # 拡張子が.svgzならGzip展開する path = self.translate_path(self.path) if 'svgz' == os.path.splitext(path): with urllib.request.urlopen(path) as f: dec = gzip.GzipFile(fileobj=f) svg = dec.read().decode("utf-8") self.send_header("Content-length", str(len(str(svg)))) self.send_header("Content-Type", "image/svg+xml") self.end_headers() self.wfile.write(svg) self.wfile.flush() else: super.do_GET() def translate_path(self, path): path = path.split('?',1)[0] path = path.split('#',1)[0] path = posixpath.normpath(urllib.unquote(path)) words = path.split('/') words = filter(None, words) path = os.getcwd() for word in words: drive, word = os.path.splitdrive(word) head, word = os.path.split(word) if word in (os.curdir, os.pardir): continue path = os.path.join(path, word) return path
前回のスクリプトで起動し、ブラウザで警告を解除したが、サーバ起動に失敗したようで以下ログが端末に表示された。
----------------------------------------
Exception happened during processing of request from ('127.0.0.1', 44652)
Traceback (most recent call last):
File "/usr/lib/python3.7/socketserver.py", line 316, in _handle_request_noblock
self.process_request(request, client_address)
File "/usr/lib/python3.7/socketserver.py", line 347, in process_request
self.finish_request(request, client_address)
File "/usr/lib/python3.7/socketserver.py", line 360, in finish_request
self.RequestHandlerClass(request, client_address, self)
TypeError: 'module' object is not callable
----------------------------------------
ググったら書き込みの所で出るエラーらしい。self.wfile.write(svg)の所だと思う。でも、サーバの仕組みを理解していないので、これ以上は読み取れなかった。
私の技術力ではサーバ側からのアプローチはこれが限界。
これら失敗コードは以下リポジトリにアップした。
2. クライアントで展開する
Gzip展開するだけならクライアントでも可能なのでは? わざわざサーバで実装せずともクライアントで実装できるのでは? サーバの勉強せずに済むのでは?
というわけで試してみた。
1. CompressionStream API
JavaScriptの標準APIにCompressionStream APIがある。Chrome 80で実装された機能。
| クラス | 用途 |
|---|---|
| CompressionStream | 圧縮 |
| DecompressionStream | 展開 |
今回は展開だけ使う。以下のように使うらしい。
const ds = new DecompressionStream("gzip"); const decompressedStream = blob.stream().pipeThrough(ds);
1. 最小コード
全体の流れは以下のようになるはず。
- fetch APIでSVGZファイルのblobデータを取得する
- DecompressionStreamでGzipを展開する
- 2のバイナリデータをTextDecoderでUTF-8テキストに変換する
- 3をHTMLのbodyに挿入して表示確認する
これをJavaScriptで実装すると以下。
window.addEventListener('DOMContentLoaded', async(event) => { const res = await fetch('asset/image/icon/clipboard-8.svgz'); const blob = await res.blob(); const ds = new DecompressionStream('gzip'); const stream = blob.stream().pipeThrough(ds); const decompressed = await new Response(stream).arrayBuffer(); const svg = (new TextDecoder()).decode(decompressed); document.querySelector('main').innerHTML = svg; });
ローカルサーバを起動して表示確認すると成功した。異常にデカく表示されたけど。

とにかくSVGZは単にテキストをGzip圧縮しただけのファイルなので、それを展開してSVGテキストにしてインライン挿入すれば表示できることは確認できた。
2. SVGスプライトをGzip圧縮
もう少し改善してみる。
SVGスプライトは複数のSVG画像を<symbol>定義して<use>で参照するもの。これにより複数ヶ所で参照する場合のコードが短縮でき、HTML文書サイズ軽減につながる。
なら、SVGスプライトをGzip圧縮すればファイルサイズを節約できる。それをGzip展開して使用すれば、HTML文書サイズも節約できる。方法は次のようになる。
- SVG画像をSVGスプライト形式にする(
<symbol>) - 1のファイルをGzip圧縮して拡張子をSVGZにする
- CSSで1文字分のサイズにする
- 2のファイルをGzip展開してSVGに戻す
- 4を参照するコードを書く(
<use>)
sprite.svg
ここでは二つのアイコン画像を定義した。それぞれのid属性値をclipboard, fileとした。HTML側でアイコン参照するときに使う。
<symbol>の中にはアイコン画像時<svg>内にあった<path>要素を含める。<svg>要素丸ごと含めても問題ないし、アウトラインデータだけなら<symbol>のd属性でもいい。今回は以下のようにした。
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="0" height="0"> <symbol id="clipboard" viewBox="0 0 256 256"><path style="opacity:1;fill-opacity:0;stroke:currentColor;stroke-width:16;stroke-linecap:square;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" d="M80.08 8.04h95.84v56.658H80.08ZM45.04 36.488V247.96h165.92V36.488h-35.04v28.21H80.08v-28.21Z"/></symbol> <symbol id="file" viewBox="0 0 256 256" ><path style="opacity:1;fill-opacity:0;stroke:currentColor;stroke-width:16;stroke-linecap:square;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" d="M224 72h-64V8ZM32 8v240h192V72h-64V8Z" fill="none"/></symbol> </svg>
上記ファイルを以下コマンドでGzip圧縮し、sprite.svgzファイルを生成した。上記JSコードではこのSVGZファイルをfetch APIで取得している。
gzip -c sprite.svg > sprite.svgz
CompressionStreamを使い、結果をファイルダウンロードするJSコードを書けば、Gzip圧縮ファイル作成できるはず。でも実装が面倒だったのでLinuxコマンドで済ませた。
main.js
window.addEventListener('DOMContentLoaded', async(event) => { const res = await fetch('asset/image/icon/sprite.svgz'); const blob = await res.blob(); const ds = new DecompressionStream('gzip'); const stream = blob.stream().pipeThrough(ds); const decompressed = await new Response(stream).arrayBuffer(); const sprite = (new TextDecoder()).decode(decompressed); const css = `<style>.icon{width:1em;height:1em;}</style>` const use1 = `<svg class="icon"><use href="#clipboard"></use></svg>` const use2 = `<svg class="icon"><use href="#file"></use></svg>` document.querySelector('main').innerHTML = sprite + css + use1 + use2; });
ローカルサーバを起動して表示確認すると以下。成功!

ファイルサイズが小さくできた。複数アイコンがあっても、スプライトにまとめたおかげでGZip展開が一度で済む。そして参照コードも<use>のおかげで短くなりHTML文書の短縮になる。(ただし二回以上参照した場合に限る。今回のように同アイコンを一度しか参照していないと逆に冗長になる)
問題
- 環境
- HTTPSサーバ上でしか使えない(
file:プロトコル上では動作しない(htmlファイルをブラウザで開いただけでは動作しない)) - Chrome 80以降でないと使えない
- JavaScript動作環境でないと使えない(JS禁止環境だと動作しない)
- HTTPSサーバ上でしか使えない(
- 用法
- 対象画像の参照方法は上記コードにせねばならない(
<img>要素などによる外部参照が使えない) - サイズ調整CSSなどを実装せねばならない
- 対象画像の参照方法は上記コードにせねばならない(
パフォーマンスの低下が懸念される。
- SVGZを参照するたびに以下が発生する
そもそも他の画像ファイル同様、以下のようにシンプルに表示できるようにして欲しかった。
<img src="clipboard.svgz">
これに関してはカスタム要素を使えばいくらか改善できるかもしれない。
<img is="svg-z" src="clipboard.svgz">
たとえば上記のようなHTML要素を書けば、自動的に先述のJSコードを実行して、以下SVG要素に置換してくれるとか。
<svg>...</svg>
流石にそこまで実装するとなると大変そう。
そもそも、どのような出力をすべきなのか検討の余地がある。たとえばSVG画像そのままではなく、SVGスプライト形式で出力したほうがいいとか、SVGフォントをWOFF2形式に変換して文字コードとして参照したほうが、もっとHTMLファイルサイズを縮小できるとか。なら実装する必要性なくなるのでは? CSSどうするの? 色は? 他の属性は? ライト/ダークモードに対応できる? 等々。
アイコンを表示する際、果たして最終的にどのような形式で実装すべきか? どのような方法があって、その違いは何で、戦略的なメリットとデメリットは何か? どのような条件のとき、どのような方法が最善か? それらを調査・理解した上で、実装するか否かを判断したい。
たかがアイコン表示したいだけなのに、次々と仕事が増えていく……。