[JavaScript] 動的SVGからpngを作りたいときはcanvgを使う。Image.src経由だとIEのSecurity Errorが出ちゃう

2018/03/21

こんにちは。きんくまです。

今回は、「動的SVGからpngを作りたいときはcanvgを使う。Image.src経由だとIEのSecurity Errorが出ちゃう」です。(長い)
SVGを動的にJavaScriptでいろいろいじったものを画像化したかったのですが、最初にやってた方法だとIE11でエラーが出てしまいました。

2018年3月の現時点だと、SVGから直接png化するメソッドはなさそうです。
なので、SVGをcanvasに描画して、そこからtoDataURLでbase64化するという流れになります。

でハマった方を書く前に忙しい方のために、先にcanvgを使った方法を書いておきます。

canvgでsvgを画像化する

>> GitHub – canvg/canvg: Javascript SVG parser and renderer on Canvas

canvgはSVGをパースして、Canvasの中に再現するライブラリです。

サンプルhtml(中身てきとうです)
JSで最終的にこんな感じのSVGができたとします。

<input type="button" value="save" id="saveButton" /><br>

<svg id="mysvg"
  version="1.1" xmlns="http://www.w3.org/2000/svg"
  width="400" height="400" >
    <rect width="100%" height="100%" fill="red"></rect>
    <circle cx="150" cy="100" r="80" fill="green"></circle>
    <text x="150" y="125" font-size="32" text-anchor="middle" fill="white">えすぶいじー</text>
    <circle cx="250" cy="300" r="80" fill="blue"></circle>
</svg>

参考)キャプチャ

JSのcanvgを使う部分。
動的にcanvasを生成して、canvgを使ってcanvasにSVGを再現。toDataURLを使って、base64化されたpngイメージのデータ(文字列)を取得します。


var OFF_SCREEN_CANVAS_ID = "svgOffScreeenRenderCanvas";
var OFF_SCREEN_CANVAS_CLASS = "svg-off-screen-render-canvas";

function saveToPngByCanvg(svgId, callback){
    var svg = document.getElementById(svgId);
    var svgStr = new XMLSerializer().serializeToString(svg);

    var canvas = document.getElementById(OFF_SCREEN_CANVAS_ID);
    if(!canvas){
        var svgW = svg.getAttribute('width');
        var svgH = svg.getAttribute('height');
        canvas = createOffScreenCanvas(svgW, svgH);
    }

    canvg(OFF_SCREEN_CANVAS_ID, svgStr, {
        renderCallback:function(data){
            if(callback){
                var data = canvas.toDataURL('image/png');
                callback(data);
            }
        }
    });
}

function createOffScreenCanvas(width, height){
    var newCanvas = document.createElement('canvas');
    newCanvas.setAttribute('id', OFF_SCREEN_CANVAS_ID);
    newCanvas.setAttribute('width', width);
    newCanvas.setAttribute('height', height);

    //styleの設定
    var style = newCanvas.style;
    style.position = 'absolute';
    style.left = '-9999px';
    style.top = '0px';

    newCanvas.classList.add(OFF_SCREEN_CANVAS_CLASS);
    document.querySelector('body').appendChild(newCanvas);
    return newCanvas;
}

使い方

window.onload = function(){
    document.getElementById('saveButton').addEventListener('click', function(){
        saveToPngByCanvg('mysvg', function(data){
            console.log('callback----');
            console.log(data);
            //ここからajaxでもfetchでも使ってサーバーにデータを送る
        });
    })
};

画像データを保存したいサンプル

canvasでbase64のテキストを取得したらサーバーに保存します。
jQueryで通信してますけど、気になる人はfetchでも良いです。

JavaScript

function postImageData(dataURL){
    $.ajax({
        type:'post',
        url: './save_image.php',
        data: {
            imgBase64: dataURL
            //ここに連携するパラメータを追加
            ,id: '12345abc'
        }
    }).done(function(msg){
        console.log(msg);
    })
}

サーバー側

save_image.php

<?php

// requires php5
// 保存するフォルダ名 writeの所有者権限を追加する必要があります
define('UPLOAD_DIR', 'images/');

$id = $_POST['id'];

//base64 / urlencoded された画像データ(文字列)
$img = $_POST['imgBase64'];
$img = str_replace('data:image/png;base64,', '', $img);
$img = str_replace(' ', '+', $img);
$data = base64_decode($img);

$file = UPLOAD_DIR . $id .'.png';
//$file = UPLOAD_DIR . uniqid() . '_'. $id .'.png';

$success = file_put_contents($file, $data);

//クライアントにメッセージを返す
print $success ? '画像を保存しました'.$file : '画像が保存中にエラーが発生しました';

気をつけるポイント。SVGを設定するときはclassではなくインラインでスタイルを設定する

SVGの中のrectやgなど各エレメントはcssのclassでスタイルを設定することが可能です。(外部cssファイルとかに書ける)
ただし動的に画像化するときは、classで設定することはできません。

手順を考えてみるとわかるのですが、SVGをシリアライズした際にそこにはclassから計算されたスタイルは読み込まれません。
なので、インラインで全て書いてあげる必要があります。(ていうかここでもハマった)

Image経由でcanvasにレンダリングするとIEでセキュリティエラーが出る

というわけで本編です。

手順

1. SVGからDOMをシリアライズ
2. 新規のImageのsrcに1を入れる
3. Imageのonloadを待つ
4. Canvasに2をdrawImage
5. CanvasのtoDataURLでbase64化する

IEはどこでエラーを出すかというと5です。

どうやら調べたところ、IEは外部のドメインのImageが入っているCanvasのtoDataURLを呼んだときにセキュリティエラーを出す仕様のようです。

>> IE throws Security Error when calling toDataUrl on canvas

書いたJS

function svgToPngByNative(svgId, callback){
    var svg = document.getElementById(svgId);
    var svgStr = new XMLSerializer().serializeToString(svg);
    svgStr = encodeURIComponent(svgStr);
    var newImage = new Image();
    newImage.crossOrigin = "Anonymous";
    newImage.onload = function(){
        var canvas = document.getElementById(OFF_SCREEN_CANVAS_ID);
        if(!canvas){
            const svgW = svg.getAttribute('width');
            const svgH = svg.getAttribute('height');
            canvas = createOffScreenCanvas(svgW, svgH);
        }
        var ctx = canvas.getContext("2d");
        ctx.drawImage(newImage, 0, 0);
        if(callback){
            //ここでIEのセキュリティエラーが起きた
            const data = canvas.toDataURL('image/png');
            callback(data);
        }
    };
    //document.querySelector('body').appendChild(newImage);
    newImage.src = "data:image/svg+xml," + svgStr;
}

じゃあなんでcanvgはセキュリティエラーが出ないかというと、canvasで1から動的にちゃんとデータを作っているからだと思います。
data:image の Imageを drawImageしているわけじゃないから。
以前やったことある案件でCanvasを1から動的に生成したあと、toDataURLしたときもエラーが出てなかったです。

将来的にSVGから直接pngに書き出すことができると良いですね。

LINEで送る
Pocket

自作iPhoneアプリ 好評発売中!
フォルメモ - シンプルなフォルダつきメモ帳
ジッピー電卓 - 消費税や割引もサクサク計算!

LINEスタンプ作りました!
毎日使える。とぼけたウサギ。LINEスタンプ販売中! 毎日使える。とぼけたウサギ

ページトップへ戻る