WebSurfer's Home

トップ > Blog 1   |   Login
Filter by APML

Canvas に複数の画像を順番通り描画

by WebSurfer 24. October 2019 12:30

Canvas に複数の画像を指定した順序で描画し、その後 Canvas に描画された画像を Data Url 形式で取得する方法を書きます。元の話は Teratail のスレッド「CanvasにdrawImageで描写した画像をtoDataURLで取得したい」です。

Canvas と img タグの画像

上の画像がその結果を Chrome で表示したもので、左が Canvas の画像、右が canvas.toDataURL メソッドを使って Data Url 形式で取得した画像データを img 要素の src 属性に設定して表示した結果です。

Canvas に表示する複数の画像の数だけ new Image() で img 要素を生成し、その src 属性に画像の url を設定すると img 要素が画像に読み込みを完了した時点で load イベントが発生するので、それにリスナをアタッチして Canvas への書き込みと Data Url 取得の処理を行うのが基本です。

ただし、問題は、Canvas に描く画像が複数ある場合、img 要素による画像の読み込みにかかる時間が画像のサイズなどによって異なるので、複数ある img 要素の load イベント発生の順番は不定となることです。

もう一つ、canvas.toDataURL メソッドを使って Canvas から Data Url 形式のデータを取得するタイミングも問題です。全ての画像の Canvas への描画が終わるのを待たなければなりません。

上の問題に対応するには、最後の load イベントの発生を待って、そのイベントリスナで複数ある全ての img 要素の画像 を希望する順番に context.drawImage メソッドで Canvas に書き込むのがよさそうです。

context.drawImage は同期メソッドなので、同じリスナ内で、全ての画像の context.drawImage が終わった後に canvas.toDataURL メソッドを記述すれば Data Url 形式のデータを取得できます。(リスナの外に canvas.toDataURL メソッドを置くのはダメです。img 要素の load イベントが発生する前に canvas.toDataURL に制御が飛ぶので)

検証に使ったコード、即ち上の画像を表示したコードを以下に書いておきます。(ASP.NET のページを利用していますが、そこは本題とは関係ないので気にしないでください) 元の画像は 125px x 75px の色が異なる .png 画像 5 枚です。それらを 20px づつ右下にずらしながら描画しています。

<%@ Page Language="C#" AutoEventWireup="true" 
    CodeFile="0076Canvas2.aspx.cs" Inherits="_0076Canvas2" %>

<!DOCTYPE html>

<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
<meta http-equiv="Content-Type" content="text/html; 
  charset=utf-8"/>
  <title>0076Canvas2.aspx</title>
  <script src="/Scripts/jquery.js"></script>
  <script type="text/javascript">
  //<![CDATA[
    window.onload = function () {
      // それぞれ 125px x 75px の単色 png 画像
      // 色は名前の通り赤, 青, 緑, 黄, 橙
      var srcs = [
          "/Images/red.png",
          "/Images/blue.png",
          "/Images/green.png",
          "/Images/yellow.png",
          "/Images/orange.png"
      ];

      var canvas = document.getElementById('mycanvas');
      var context = canvas.getContext('2d');

      // canvas に描かれる順番を保証するには以下のようにする。
      // そうすればリスナの中に canvas.toDataURL("image/png");
      // を記述でき Data Url が取得できる
      var images = [];
      var loadCount = 0;
      for (let i = 0; i < srcs.length; i++)
      {
        images[i] = new Image();                
        images[i].onload = function () {
          loadCount++;
          if (loadCount == images.length)
          {
            for (let j = 0; j < images.length; j++)
            {
              context.drawImage(images[j], j * 20, j * 20);
            }

            // image.onload のリスナの中でないと Data Url
            // は取得できないので注意
            var result = canvas.toDataURL("image/png");
            document.getElementById("image1").src = result;
          }
        };
        images[i].src = srcs[i];
      }
    }
    //]]>
  </script>
</head>
<body>
  <form id="form1" runat="server">
  <div style="float: right;">
    <canvas id="mycanvas" width="205" height="155">
    </canvas>
  </div>
  <div>
    <img id="image1" alt="" />
  </div>
  </form>
</body>
</html>

他に jQuery.Deferred() を使う方法もあります。そのコードを以下に書いておきます。

// jQuery.Deferred を使用
var images = [];
var dfds = [];
for (let i = 0; i < srcs.length; i++)
{
    images[i] = new Image();
    let dfd = new $.Deferred();
    dfds[i] = dfd;
    images[i].onload = function () { dfd.resolve(); };
    images[i].src = srcs[i];
}

$.when.apply($, dfds).done(function () {
    for (let i = 0; i < images.length; i++)
    {
        context.drawImage(images[i], i * 20, i * 20);
    }
    var result = canvas.toDataURL("image/png");
    document.getElementById("image1").src = result;
});

全ての img 要素の load イベントが発生し終わるのを待って Canvas への描画と Data Url 形式のデータを取得するというところは同じですが、見かけは何となくカッコいいかも。(笑)

Tags: , , , ,

JavaScript

canvas の画像をアップロード

by WebSurfer 2. July 2015 16:27

クライアントの PC にある画像を HTML5 の File API を利用して取得し、それを指定のサイズに縮小して HTML5 の canvas に描画し、canvas に描画された画像を jQuery.Ajax を使って Web サーバーにアップするサンプルを書きます。

canvas の画像をアップロード

大まかな手順は以下の通りです。詳しくは下のサンプルコードとそれに書いたコメントを参照ください。

  1. クライアントによる画像ファイルの選択は ASP.NET の FileUpload コントロール(普通の HTML の <input type="file" ... /> でも可)を利用する。
  2. FileUpload コントロールで画像ファイルが選択されたタイミングで、HTML5 File API の FileReader オブジェクトに readAsDataURL メソッド を使って選択された画像ファイルを読み込む。
  3. FileReader の result プロパティ を使って、読み込んだ画像ファイルを Data url 形式("data:image/jpeg;base64, ..." という文字列)で取得し、それを image オブジェクトの src 属性に設定する。
  4. その image オブジェクトを HTML5 の canvas に drawImage メソッド を使って描画する。その際、描画する画像の最大サイズの制限を設け(今回のサンプルでは 500 x 500 とした)、それに入る場合はそのまま、入らない場合は幅・高さどちらか大きい方を 500px に縮小し他方をその縮小率と同じに縮小(要するに縦横比を保ったまま 500 x 500 に入るよう縮小)する。
  5. canvas 上の縮小後の画像データを取得して Web サーバーに非同期で送信するメソッドを作る。canvas からの画像データの取得は toDataURL メソッド を用いる。Data url 形式で取得できるので、それを jQuery.Ajax を用いて JSON 形式で送信する。(BASE64 でエンコードされているので、バイナリ形式よりサイズが約 1.3 倍大きくなってしまうが・・・)
  6. <input type="button" ... /> タイプのボタンを配置し、その onclick 属性に上記のメソッドを設定する。
  7. 非同期でクライアントから送信された BASE64 形式の画像データは、.aspx ページに静的メソッドを追加してそれで受け、デコードしてバイナリ形式に戻してファイルに保存する。
  8. オマケとして、画像ファイルのサイズを 500,000 bytes に、ファイルのタイプを image/jpeg に制限した。

サンプルコードは以下の通りです。自分の開発環境の IE9 では File API が使えないので試してませんが、Firefox 38.0.5, Chrome 43.0.2357.130 m, Opera 12.17 で期待通り動くことは確認しました。

同様なコードを 実験室 にアップしました。ファイルのアップロードと保存はできませんが(代わりにごく短い JSON 文字列を送信し、応答は返すようにしています)、よろしければ実際に動かして試してみてください。

<%@ Page Language="C#" %>
<%@ Import Namespace="System.Web.Services" %>
<%@ Import Namespace="System.IO" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
 "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<script runat="server">
  // jQuery.Ajax で POST された画像データを受けるためのメソッ
  // ド。public static にして WebMethodAttribute 属性を付与
  [WebMethod]
  public static string ReceiveImage(string imgBase64)
  {
    // 文字列先頭の "data:image/jpeg;base64," を除去。
    imgBase64 = 
      imgBase64.Replace("data:image/jpeg;base64,", "");

    // BASE64 エンコードされた画像データを元のバイト列に変換
    Byte[] imgByteArray = Convert.FromBase64String(imgBase64);

    // ファイル格納フォルダ FileUploadTest の物理パスを取得
    string savePath = 
      HttpContext.Current.Server.MapPath("~/FileUploadTest/");

    // ファイル名を設定
    string filename = 
      "img" + DateTime.Now.ToString("yyyyMMddHHmmss") + ".jpg";

    // ファイル格納フォルダ FileUploadTest に画像ファイルを保存
    File.WriteAllBytes(savePath + filename, imgByteArray);

    return "ファイル名 " + filename + " として保存しました。";
  }
</script>

<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
  <title>Canvas Image Upload</title>
  <script src="Scripts/jquery-1.8.3.js" type="text/javascript">
  </script>
  <script type="text/javascript">
  //<![CDATA[
    // 画像ファイルのサイズ制限を 500,000 bytes、タイプ制限を
    // image/jpeg とする
    var maxFileSize = 500000;
    var allowedContentType = "image/jpeg";

    // アップロードする画像のサイズを 500 x 500 以下とする
    var maxWidth = 500;
    var maxHeight = 500;

    // HTML5 File API の FileReader を利用して FileUpload で
    // 選択された画像ファイルを読み込み、その画像の Data url
    // 形式文字列を取得。それを image オブジェクトの src 属性
    // に設定する。その際 image.onload イベントが発生するので
    // そのイベントにコールバック関数(リスナ)をアタッチ。そ
    // のリスナの中で image オブジェクトが保持する画像をにサ
    // イズ制限以下に縮小して canvas 描画する。以下の 3 行は
    // そのための準備
    var fileReader;
    var image = new Image();
    image.onload = DrawImageOnCanvas;

    // document の読み込み完了後に { } 内の処置を行う
    $(function () {
      // ブラウザの HTML5 File API サポートを確認
      if (window.File && window.FileReader && window.FileList) {
        fileReader = new FileReader();

        // FileReader オブジェクトに画像を読み込むメソッド
        // readAsDataURL は非同期で動くので、読み込み完了の
        // イベントを待ってリスナで処置する必要がある。 
        // readAsDataURL で読み込みが完了すると onloadend 
        // イベントが発生するので、それにリスナをアタッチし、
        // そこで FileReader から Data url を取得し image
        // オブジェクトの src 属性に設定する。
        fileReader.onloadend = function () {
            image.src = fileReader.result;
        };

        // FileUpload1 でファイルの選択が完了すると change
        // イベントが発生するのでそれにリスナをアタッチし、
        // そこで以下の処置を行う。
        $("#<%=FileUpload1.ClientID%>").change(function () {
          var fileUpload =
            document.getElementById("<%=FileUpload1.ClientID%>");

          // ファイルが選択されているか、選択されたファイル
          // のタイプ/サイズは制限に入っているかを確認
          if (ClientValidate(fileUpload) == false) {
              $('#Button1').attr('style', 'display:none');
              return;
          }

          // fileReader オブジェクトに FileUpload1 で選択
          // された画像ファイルを読み込む
          fileReader.readAsDataURL(fileUpload.files[0]);
        });
      }
      else {
        $("#<%=FileUpload1.ClientID%>").
            attr('style', 'display:none');
        $('#mycanvas').attr('style', 'display:none');
        $('#result').text('File API がサポートされてません。');
      }
    });

    // ファイルの確認のためのヘルパ関数
    function ClientValidate(fileUpload) {
        if (fileUpload.files[0] == null) {
            alert("ファイルが未選択です。");
            return false;
        }

        if (fileUpload.files[0].type != allowedContentType) {
            alert("選択されたファイルのタイプが" + 
                allowedContentType + "ではありません。");
            return false;
        }

        if (fileUpload.files[0].size > maxFileSize) {
            alert("ファイルのサイズが制限の " + 
                maxFileSize + " バイトを超えています。");
            return false;
        }

        return true;
      }

      // 上で定義した image オブジェクトの src 属性に Data url
      // が設定されると発生する onload イベントのリスナ。ここ
      // で image 要素から canvas を描画する。
      function DrawImageOnCanvas( )
      {
        // オリジナル画像のサイズ
        var w = image.width;
        var h = image.height;

        var targetW, targetH;
        var context = 
          document.getElementById('mycanvas').getContext('2d');

        if (w <= maxWidth && h <= maxHeight) {
            // w, h ともに制限 maxWidth, maxHeight 以内 ⇒
            // そのままのサイズで canvas に描画
            $('#mycanvas').attr('width', w);
            $('#mycanvas').attr('height', h);
            context.drawImage(image, 0, 0);
        }
        else if (w < h) {
            // w, h どちらかが制限オーバーで h の方が大きい ⇒
            // 高さを maxHeight に縮小
            targetH = maxHeight;
            // 幅は高さの縮小比率で縮小
            targetW = Math.floor(w * targetH / h);
            $('#mycanvas').attr('width', targetW);
            $('#mycanvas').attr('height', targetH);
            context.drawImage(image, 0, 0, targetW, targetH);
        }
        else {
            // w, h どちらかが制限オーバーで w の方が大きい ⇒
            // 幅を maxWidth に縮小
            targetW = maxWidth;
            // 高さは幅の縮小比率で縮小
            targetH = Math.floor(h * targetW / w);
            $('#mycanvas').attr('width', targetW);
            $('#mycanvas').attr('height', targetH);
            context.drawImage(image, 0, 0, targetW, targetH);
        }

        $('#Button1').attr('style', 'display:inline-block');
      }   

      // canvas の画像データを取得して jQuery.Ajax で送信。
      // クライアントからサーバーへ送信できる JSON 文字列の
      // 長さは、デフォルトで 102,400 文字に制限されている。
      // 制限を越える場合 web.config の jsonSerialization 
      // 要素の設定によって変更が必要。
      function uploadImage() {
        var context = 
          document.getElementById('mycanvas').getContext('2d');
        var url = context.canvas.toDataURL("image/jpeg");
            
        $.ajax({
            type: "POST",
            url: "0118-CanvasImageUpload2.aspx/ReceiveImage",
            data: '{"imgBase64":"' + url + '"}',
            contentType: "application/json; charset=utf-8",

            success: function (data) {
                // .NET 3.5 で追加された d パラメータの処置
                if (data.hasOwnProperty('d')) {
                    data = data.d;
                }
                $('#result').text(data);
            },

            error: function (jqXHR, textStatus, errorThrown) {
                $('#result').text('textStatus: ' + textStatus +
                       ', errorThrown: ' + errorThrown);
            }
          });
      }
  //]]>
  </script>
</head>
<body>
  <form id="form1" runat="server">
  <div>
    <asp:FileUpload ID="FileUpload1" runat="server" />
    <input id="Button1" type="button" value="Upload" 
        style="display:none;"
        onclick="javascript:uploadImage();" />
    <br />
    <canvas id="mycanvas" width="500" height="500"></canvas>
    <div id="result"></div>
  </div>
  </form>
</body>
</html>

上のコメントにも書きましたが、クライアントからサーバーへ送信できる JSON 文字列の長さは、デフォルトで 102,400 文字に制限されています。制限を越える場合 web.config の jsonSerialization 要素の設定によって変更が必要となりますので注意してください。以下のように設定します。(数字 300000 は実際にあわせて変更してください)

<configuration>
  <system.web.extensions>
    <scripting>
      <webServices>
        <jsonSerialization maxJsonLength="300000"/>
      </webServices>
    </scripting>
  </system.web.extensions>
</configuration>

Tags: ,

Upload Download

About this blog

2010年5月にこのブログを立ち上げました。その後 ブログ2 を追加し、ここは ASP.NET 関係のトピックス、ブログ2はそれ以外のトピックスに分けました。

Calendar

<<  November 2019  >>
MoTuWeThFrSaSu
28293031123
45678910
11121314151617
18192021222324
2526272829301
2345678

View posts in large calendar