エフアンダーバー

個人開発の記録

SVGをCanvasに変換しようとして失敗した話

Electronでsvg要素を画像として保存したいと思い、Canvasに変換してからPNGとして保存しようと試みた結果、失敗したという話。

最終的に諦めたのでこの記事に解決策は書いてありません。 同じことをしたい人が時間を無駄にしないための記事です。

drawImageによる変換

svg要素はシリアライズしてからBlobURLに変換するとImageのソースとすることができ、 ImageはdrawImageメソッドによりCanvasに描画できます。

const serializer = new XMLSerializer();
const data = serializer.serializeToString(svg);
const blob = new Blob([data], { type: "image/svg+xml;charset=utf-8" });
const url = URL.createObjectURL(blob);

const image = new Image();
image.addEventListener('load', () => {
    const canvas = document.createElement('canvas');
    canvas.width = image.width;
    canvas.height = image.height;
    canvas.getContext('2d').drawImage(image, 0, 0);
    URL.revokeObjectURL(url);

    // ...
});
image.src = url;

元画像とこの方法による描画結果はそれぞれ次の通り。

f:id:fspace:20171023183142p:plain
元画像(スクリーンショットで保存)

f:id:fspace:20171023183152p:plain
drawImageによる描画結果

元画像と比べると描画結果の線は若干太く、ジャギーが目立ちます。

WebGLによる変換

CanvasのdrawImageでは何らかのフィルタ処理がかかってしまうのではないかと考え、 今度はWebGLでコピーしてみることにしました。

drawImageを次のblit関数に置き換えて描画します。

f:id:fspace:20171023183200p:plain
WebGLによる描画結果

今度は一部の線の色が周期的に変わってしまっています。 この手の模様が発生してしまうケースはどこかで見たような気がするんですが忘れてしまった・・・。

余談;ElectronでWebGLが利用できない場合の対処

実装時、Electronでcanvas.getContext('webgl')を呼んだところエラーが発生。

[11304:1020/195856.066:ERROR:gles2_cmd_decoder.cc(2510)] [GroupMarkerNotSet(crbug.com/242999)!:9896753476020000]GL ERROR :GL_INVALID_ENUM : BackFramebuffer::Create: <- error from previous GL command
[11304:1020/195856.066:ERROR:gles2_cmd_decoder_autogen.h(1612)] [.Offscreen-For-WebGL-000001A9DA2BA5E0]GL ERROR :GL_INVALID_ENUM : GetIntegerv: <- error from previous GL command

呼んでもいないAPIに対してエラーが出ているので、どうもgetContextの内部実装に問題があるようです。

調べたところ--disable-gpu--disable-d3d11といったオプションを指定すると直る場合があるとのこと。

--disable-gpuはGPUハードウェアアクセラレーションを無効にするためのオプションです。 これを指定してみたところエラー自体は消えましたが、同時にWebGLも無効となってしまうようで戻り値がnullになってしまいました。 この動作はバグじゃないのかということでissueが挙がっていたので今後修正されるかもしれません。

--disable-d3d11はDirect3D 11による描画を無効にするためのオプションです。 今回はこれを指定したところエラーが消え、WebGLによる描画もできるようになりました。

原因の推測

うまく描画されない原因についての推測。

drawImageに関してはSVGをソースとするImageを指定した場合のレンダリング方法が表示時と違うのだと思います。 ラスター画像の場合でも拡大縮小時にはフィルタの粗さに違いがあるらしく、ImageをきれいにCanvasに変換するのは難しいらしいです (ただし、最近の仕様ではいくらか調整可能な模様 cf. imageSmoothingEnabled, imageSmoothingQuality)。

WebGLの方はSVGをソースとするImageをTextureへと変換する処理あたりかなと思ってはいますが、 WebGLの機能や仕様自体それほど把握していないのでなんとも言えません。 今回初めてWebGLのコードを書いたので、単純にコードが間違っているだけの可能性もなきにしもあらず。

おわりに

ブラウザに表示されるのと同じように保存したいだけだったのですが、案外うまくいかないものですね。

使用した図から想像がつくかと思いますが、関数グラフの簡易描画ソフトを作ろうとしていました。 フリーのものを探したのですが、自由度が高く、描画がきれいで、広告のあるブログに貼れるとなると結構難しいようです。 とはいえ、これ以上手間をかけて実装する気もないので、どこかで妥協するしかないですね・・・。