​ ​

SVGとCanvasで画像の縮小を試してみる

こんにちは、帯広の夏は日差しがヤバイですが、湿度があまり高くないので、なんとか生きていけてるファームノートクラウド開発グループの方川です。

今日はいろいろ既出な感じですがSVGとCanvasを使ってJavascript上で画像の縮小を試してみようと思います。

最近の主流の4GやLTEの回線だとあまり困らないかもしれませんが、それでも電波の悪い環境下だと画像のアップロードは結構厳しいかと思います。 とくにスマートフォンだとアップロードに使う画像は無加工でサイズが大きい状態のままアップロードされる事が多いのではないでしょうか。 ユーザー側が意識せずにファイルのアップロードをするタイミングで、ある程度サイズを調整してアップロード出来ると良さそうです。

環境構築

簡易のサーバーをローカルに立てます。使うのはnode.jsexpressとexpress-generator

mkdir node_modules 
sudo npm install express
sudo npm install express-generator
./node_modules/express-generator/bin/express sample
cd sample
DEBUG=sample:* npm start

これでサーバーが起動するのでhttp://localhost:3000/にアクセスして進めます。 使う場所は、public/javascripts以下に検証用のJSを用紙して、 views/layout.jadeに必要なjavascriptのリンクをはります。

├── public
│   ├── images
│   ├── javascripts
│   │   └── sample.js (追加)
│   └── stylesheets
│         └── style.css
├── routes
│   ├── index.js
│   └── users.js
└── views
    ├── error.jade
    ├── index.jade
    └── layout.jade

view/layout.jade

doctype html
html
  head
    title= title
    link(rel='stylesheet', href='/stylesheets/style.css')
    script(type='text/javascript' src='https://ajax.googleapis.com/ajax/libs/jquery/2.2.4/jquery.min.js' charset='UTF-8')
    script(type='text/javascript' src='http://d3js.org/d3.v3.min.js' charset='UTF-8')
    script(type='text/javascript' src='/javascripts/sample.js' charset='UTF-8')
  body
    block content

こんな感じです。

express.png

今回利用したJavascriptライブラリ

  • jquery version2
  • D3.js

D3.jsはsvgの操作で使いました。

試したコード

とりあえず比較としてsvgとcanvasの両方で試せるコードを作成してみました。

(function () {

    /**
     * @param size 変更後の画像サイズを指定する
     * @param useSvg svgを使う
     * @constructor
     */
    function ImageResizer(size, useSvg) {
        _this = this;
        this.distSize = size || 60;
        this.useSvg = useSvg;
    }

    /**
     * 指定された画像のサイズを変更する
     * @param file
     */
    ImageResizer.prototype.resize = function (file) {
        _this = this;
        return this.readFile(file)
            .then(this.toImage.bind(this))
            .then(this.useSvg
                ? this.resizeBySvg.bind(this)
                : this.resizeByCanvas.bind(this));
    };

    /**
     * エラー処理
     */
    ImageResizer.prototype.error = function () {
        alert('エラーが発生した');
    };

    /**
     * FileReaderAPIを使ってファイルをdataURLに変換
     *
     * @param file
     * @returns {*}
     */
    ImageResizer.prototype.readFile = function (file) {
        var def = $.Deferred();
        if (!file) return def.reject();

        var reader = new FileReader();
        reader.readAsDataURL(file);
        reader.onload = function () {
            def.resolve(reader.result);
        };
        reader.onerror = function () {
            def.reject();
        };
        return def.promise();
    };

    /**
     * 画像サイズを知るためにdataURLをimageに設定
     *
     * @param dataURL
     * @returns {*}
     */
    ImageResizer.prototype.toImage = function (dataURL) {
        var def = $.Deferred(),
            image = new Image();

        image.onload = function () {
            def.resolve(image);
        };
        image.onerror = function () {
            def.reject();
        };
        image.src = dataURL;
        return def.promise();
    };

    /**
     * canvasを使って画像サイズを変更
     * @param targetImage
     * @returns {*}
     */
    ImageResizer.prototype.resizeByCanvas = function (targetImage) {
        var def = $.Deferred(),
            size = this.distSize,
            width = targetImage.width,
            height = targetImage.height,
            scale = size / Math.max(width, height),
            swidth = scale * width,
            sheight = scale * height,
            adjustx = size != Math.ceil(swidth) ? Math.ceil((size - swidth) / 2) : 0,
            adjusty = size != Math.ceil(sheight) ? Math.ceil((size - sheight) / 2) : 0;
        var canvas = document.createElement("canvas"),
            ctx = canvas.getContext("2d");
        canvas.width = size;
        canvas.height = size;
        ctx.drawImage(targetImage, adjustx , adjusty, swidth, sheight);
        return def.resolve(canvas.toDataURL("image/png"));
    };

     /**
     * svgを使って画像サイズを変更
     * @param targetImage
     * @returns {*}
     */
    ImageResizer.prototype.resizeBySvg = function (targetImage) {
        var def = $.Deferred(),
            size = this.distSize,
            width = targetImage.width,
            height = targetImage.height,
            scale = size / Math.max(width, height),
            swidth = scale * width,
            sheight = scale * height,
            adjustx = size != Math.ceil(swidth) ? Math.ceil((size - swidth) / 2) : 0,
            adjusty = size != Math.ceil(sheight) ? Math.ceil((size - sheight) / 2) : 0;

        // 変換
        var svg = $('body')
            .append('<svg></svg>')
            .find('svg')
            .get(0);

        // DOM操作はd3を利用
        d3.select('svg')
            .attr('x', 0)
            .attr('y', 0)
            .attr('width', size)
            .attr('height', size)
            .append('image')
            .attr('x', 0)
            .attr('y', 0)
            .attr('width', width)
            .attr('height', height)
            .attr('xlink:href', targetImage.src)
            .attr('transform', 'translate('+adjustx+','+adjusty+'),scale('+scale+','+scale+')');
        // svgをXML化
        var xml = new XMLSerializer().serializeToString(svg),
            img = new Image();
        $('svg').remove();
        img.onload = function () {
            var canvas = document.createElement("canvas"),
                ctx = canvas.getContext("2d");
            canvas.width = size;
            canvas.height = size;
            ctx.drawImage(img, 0, 0);
            def.resolve(canvas.toDataURL("image/png"));
        };
        img.onerror = function () {
            def.reject();
        };
        // dataURLに指定できるsvgの形式に合わせる
        img.src = "data:image/svg+xml;base64," + window.btoa(unescape(encodeURIComponent(xml)));
        return def.promise();
    };
    $(document).ready(function () {
        var useSvg = true,
            noSvg = false,
            distSize = 200,
            svgResizer = new ImageResizer(distSize, useSvg),
            canvasResizer = new ImageResizer(distSize, noSvg);
        $('body')
            .append($('<div style="display: inline-block;width: '+distSize+'px;"></div>')
                .append('<h3>SVG</h3>')
                .append('<div id="preview1"></div>')
                .append('<div id="time1"></div>')
            )
            .append($('<div style="display: inline-block;width: '+distSize+'px;"></div>')
                .append('<h3>Canvas</h3>')
                .append('<div id="preview2"></div>')
                .append('<div id="time2"></div>')
            )
            .append('<br / ><input id="upload" type="file" />');

        $('#upload').on('change', function (e) {
            var time = Date.now();
            $(e.currentTarget).prop('disabled', true);
                $.when(
                    svgResizer.resize(e.target.files[0])
                        .then(function (dataURL) {
                            var image = new Image;
                            image.src = dataURL;
                            $('#time1').text((Date.now() - time) + "ms");
                            $('#preview1').empty().append(image);
                        })
                    ,canvasResizer.resize(e.target.files[0])
                        .then(function (dataURL) {
                            var image = new Image;
                            image.src = dataURL;
                            $('#time2').text((Date.now() - time) + "ms");
                            $('#preview2').empty().append(image);
                        }))
                    .always(function () {
                        $(e.currentTarget).prop('disabled', false);
                    });
            });
    });
})();

画像縮小までのフローを簡単にまとめると

  1. FileReaderAPIを使って画像をdataURL形式で受け取る
  2. dataURLをImgeオブジェクトに設定して画像のサイズなどを調べる
  3. 画像サイズを元にスケール値を求める
  4. 画像変換
  5. canvasの内容をdataURLに変換する

という流れです。

画像変換について

Canvasを使った処理だと、drawImageを使って縮小ができます。 transleteやscaleをつかっても縮小できますがchrome上で試した限りでは縮小後の結果が変わらないようでしたので drowImageを使ったほうが良さそうです。

SVGを使った方法では画像への出力に対応していないようなので一度canvas側を利用する必要がありました。 SVGの内容をcanvasへ書き出す方法は、dataURLにSVGが指定出来ることを利用します。 SVGの内容をxml化しdataURL形式に置き換える事でimageのソースに指定することができるようになります。 あとはcanvas側でやったのと同様にdrawImageを使って書き出すだけです。

気になった点

iPhone5sで確認した限りでは、svg側の処理のdrawImageで書き出す処理とcanvasをdataURL化している間に500msぐらい時間差を空けないと画像の出力に失敗するようでした。

実行結果

サンプル画像のサイズは1952 × 619です。

firefox

svg_canvas_firefox.png

chrome

svg_canvas_chrome.png

SVGでchromeとfirefoxでは速度が違うのはchrome側のSVGでは補正がかかる為だと思います。 変換後の画像を見ると、canvasの粗い変換とちがって滑らかな補完がされています。

まとめ

chromeの結果を見るとsvgを使うと良い感じにしてくるのかと思ってしまいますが、 SVG+firefoxでは効果がなかったのでブラウザ毎に品質が変わってしまう事は間違いなさそうです。 速度的なものは十分ですが、実際にアップロードして使う画像としては十分ではありません。 品質を保った形で縮小などを必要とする場合はpicaのようなライブラリを使うか、 自前で縮小処理を書く必要がありそうです。

https://github.com/nodeca/pica

ちなみにpicaで縮小すると検証で使った画像で493msあたりでした。

おわりに

ファームノートではお客様により使いやすい機能を提供する為にWEBアプリケーションで開発された機能をAndroid / iOS の ネイティブUI に移植するスマートフォン/ネイティブアプリケーション開発エンジニアを募集しています。

このエントリーをはてなブックマークに追加