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

jQuery.ajax で JSONP

by WebSurfer 3. May 2017 14:30

jQuery.ajax を使うと、JSONP(JSON with Padding・・・script タグを使用してクロスドメインでデータを取得する仕組み)を利用してデータを取得するのがかなり簡単にできるという話を書きます。

JSONP の応答を alert に表示

何が簡単になるかというと以下のことを jQuery.ajax が自動的にやってくれることです。

  1. jQuery.ajax を呼び出した時点で script タグを生成し DOM ツリーへ追加。(本来は、src 属性に呼出先の URL を設定した script タグを生成して DOM ツリーに追加するためのスクリプトを自力で書かなければなりません)
  2. 呼出先の URL にコールバックの名前を指定するクエリ文字列 &callback=jQuery... を追加。
  3. 応答が返ってくるとコールバックが呼び出され、引数に設定された JSON 文字列が JavaScript オブジェクトにパースされ、success: function (data) の data に渡される。

以下に具体例を書きます。MVC を例にとっていますが、もちろん Web Forms でも同様なことは可能です。

まず、ブラウザからの要求を受けて応答を返す窓口を作ります。MVC アプリの場合は、以下のようにアクションメソッドを使うのが簡単そうです。

具体的にどういう操作をしているかはコメントに書きましたのでそちらを見てください。

using System;
using System.Collections.Generic;
using System.Data;
using System.Data.Entity;
using System.Linq;
using System.Net;
using System.Web;
using System.Web.Mvc;
using AdventureWorksLT;
using System.Web.Script.Serialization;

namespace Mvc5App.Controllers
{
  public class AddressesController : Controller
  {
    // サンプルデータベース AdventureWorksLT をベースに
    // Visual Studio のウィザードで作った EDM を利用。
    // AdventureWorksLTEntities は DbContext を継承
    private AdventureWorksLTEntities db = 
                            new AdventureWorksLTEntities();

    // JSONP が呼び出すアクションメソッド
    public ActionResult Jsonp(int? id, string callback)
    {
      if (id == null)
      {
          return new HttpStatusCodeResult(
                                 HttpStatusCode.BadRequest);
      }

      // Address テーブルから指定された AddressID(主キー)
      // でレコードを検索。
      // 当該レコードの AddressID, AddressLine1, City フィー
      // ルドのデータを匿名クラスのオブジェクトとして取得
      var address = (from a in db.Address
         where a.AddressID == id
         select new { a.AddressID, a.AddressLine1, a.City }).
         FirstOrDefault();

      if (address == null)
      {
          return HttpNotFound();
      }

      // 取得したオブジェクトを JSON 文字列にシリアライズし
      // 変数 callback に名前が指定されたコールバックメソッ
      // ドの引数に設定。
      // code は「コールバック(JSON 文字列)」という文字列に
      // なる
      JavaScriptSerializer ser = new JavaScriptSerializer();
      string code = string.Format("{0}({1})", 
                          callback, ser.Serialize(address));

      // 文字列「コールバック(JSON 文字列)」を返す。その際
      // ヘッダに Content-Type: application/x-javascript; が
      // 付与される
      return JavaScript(code);
    }
    // ・・・中略・・・
  }
}

上記のアクションメソッドを、例えば /Addresses/Jsonp?id=25&callback=name で呼び出すと、以下の応答が返ってきます。

name({"AddressID":25,"AddressLine1":"9178 Jumping St.","City":"Dallas"})

呼び出し側(View)のコードは以下のようになります。上にも書きましたが、script 要素を DOM に追加するコードを書く必要はありません。以下のコードだけで jQuery.ajax が自動的にやってくれます。

<input type="text" id="tb1" />
<input type="button" value="検索" onclick="btn_click()" />

@section Scripts {
  <script type="text/javascript">
  //<![CDATA[
    function btn_click() {
      var address = '/Addresses/Jsonp?id=' + $("#tb1").val();

      $.ajax({
          url: address,
          dataType: 'jsonp',
          success: function (data) {
              alert(data.AddressID + ', ' +
                    data.AddressLine1 + ', ' + 
                    data.City);
          }
      });
    }
  //]]>
  </script>    
}

上記には jQuery の外部スクリプトファイルの参照が書いてありませんが、Visual Studio 2015 のテンプレートで自動生成した _Layout.cshtml を利用していますので、デフォルトで取り込まれるようになっています。

_Layout.cshtml を利用していますので、インラインのスクリプトは、上記のように View に @section Scripts { ... } を追加して、その中に書くのが適当です。header タグ内に書くと jQuery の外部スクリプトファイルが取り込まれる前になってしまいますので注意してください。

コールバックのメソッド名は、上にも書きましたが、jQuery.ajax がクエリ文字列として自動的に自動的にデフォルトの名前を追加しますので、それを利用しています。(デフォルトの名前では不都合がある場合は指定することも可能です)

data には、コールバックの引数の JSON 文字列が JavaScript オブジェクトに変換されて渡されます。なので、上記のようにプロパティを使ってデータを取得することができます。

Tags: ,

AJAX

クロスドメインの WCF サービス

by WebSurfer 27. December 2016 21:58

先の記事「WCF と jQuery AJAX」で書いた ASP.NET がホストする WCF サービスを、クロスドメインで利用する方法について書きます。

プリフライトリクエスト

Tsuyoshi Matsuzaki さん Blog の記事「JavaScript のクロス ドメイン (Cross-Domain) 問題の回避と諸注意」では以下の 3 つの方法が紹介されています。それぞれの概要が説明されていて分かりやすいので一読されるといいと思います。

  1. Cross Document Messaging (XDM)
  2. Cross-Origin Resource Sharing (CORS)
  3. JSONP

ここでは 2 番目の Cross-Origin Resource Sharing (CORS) という方法を使います。上の画像は CORS でのプリフライトリクエストとそれに対する応答を Fiddler で見たものです。

(最初は jQuery.ajax を利用した JSONP が一番簡単かと思ったのですが、jQuery.ajax では応答のスクリプトにコールバック jQuery1830...26( ... ) で囲んだ JSON 文字列を期待しているのに、WCF サービスは JSON 文字列しか返さないので使えませんでした)

CORS は HTML 5 の仕様なので依然として使えないブラウザがありますが、対応ブラウザが増えるにつれ今後の本命になるだろうということです。(ちなみに、IE9 は対応してませんでした。上に紹介した MSDN Blog の記事によると独自の XDomainRequest オブジェクトを使用するという回避方法はあるそうですが)

まず、この記事を書くに当たって自分が参考にさせていただいた記事へのリンクを張っておきます。私の説明では分からない場合はそちらを読まれるといいと思います。手抜きでスミマセン。(汗)

  1. オリジン間リソース共有 (CORS)
  2. CORS(Cross-Origin Resource Sharing)について整理してみた

ブラウザが CORS をサポートしていれば、クライアント側の処理はブラウザが自動的に行いますので、開発者はクライアント側の対応は何もする必要がないです。

サーバー側すなわち ASP.NET Web アプリで CORS に必要な応答ヘッダ返せるようにするだけで対応できます。基本的には Global.asax ファイルに以下のコードを記述してやれば可能になります。(厳格なセキュリティを要求せず無条件で応答するというような場合はもっと簡単になります)

protected void Application_BeginRequest(object sender, 
                                                 EventArgs e)
{
  CrossOriginResourceSharingSetting();
}

private void CrossOriginResourceSharingSetting()
{
  HttpRequest request = HttpContext.Current.Request;
  HttpResponse response = HttpContext.Current.Response;

  // プリフライトリクエストの場合
  if (request.HttpMethod == "OPTIONS")
  {
    // 要求ヘッダの origin フィールドを取得
    string origin = request.Headers["origin"];

    // ここで origin に指定されたドメインのチェックを行った
    // 方がいいかもしれないが省略。要求ヘッダの origin フィ
    // ールドで送信されてきたものをそのまま返す

    // 要求ヘッダに origin フィールドがなければ何もしない
    if (!string.IsNullOrEmpty(origin))
    {
      string method = 
       request.Headers["Access-Control-Request-Method"];
      string header = 
        request.Headers["Access-Control-Request-Headers"];

      // 要求ヘッダに Access-Control-Request-Method および
      // Access-Control-Request-Headers がなければ何もしない
      if (!string.IsNullOrEmpty(method) && 
          !string.IsNullOrEmpty(header))
      {
        // method が GET または POST 以外は対応しない
        // header もチェックした方がいいかもしれない・・・
        if (method.Contains("GET") || method.Contains("POST"))
        {                            
          response.
            AddHeader("Access-Control-Allow-Origin", origin);                            
          response.
            AddHeader("Access-Control-Allow-Methods", method);                            
          response.
            AddHeader("Access-Control-Allow-Headers", header);

          // Access-Control-Max-Age ヘッダも任意で追加できる。
          // 指定した時間(秒)プリフライトの結果をキャッシュで
          // きるので、実際に運用に移す際は追加する方がよさそう
          //response.AddHeader("Access-Control-Max-Age", "600");

          response.End();
        }
      }
    }                
  }
  else
  {
    // 要求ヘッダの origin フィールドを取得
    string origin = request.Headers["origin"];

    // ここで origin に指定されたドメインのチェックを行った方
    // がいいかもしれないが省略。要求ヘッダの origin フィール
    // ドで送信されてきたものをそのまま返す

    // 要求ヘッダに origin フィールドがなければ何もしない
    if (!string.IsNullOrEmpty(origin))
    {                    
      response.AddHeader("Access-Control-Allow-Origin", origin);
    }
  }            
}

上記のコードが何をしているかと言うと、要求が「プリフライトリクエスト」になる場合と「シンプルなリクエスト」になる場合とに処置を分けて、CORS に必要な応答ヘッダを設定しているだけです。詳しくはコード中のコメントに書きましたのでそれを見てください。

CORS をサポートしているブラウザがクロスドメインでどのような処置を行うかは、要求が「シンプルなリクエスト」になるか否かによって異なってきます。要求が「シンプルなリクエスト」になる条件は、上に紹介した記事「HTTP アクセス制御 (CORS)」を読んでください。

例えば、先の記事「WCF と jQuery AJAX」で言うと「(3) WCF AJAX サービスへのアクセス」のコードで getAllCars メソッドを使う方が「シンプルなリクエスト」になります。

getCarsByDoors メソッドを使う方は要求ヘッダに contentType: "application/json; charset=utf-8" が含まれるので「シンプルなリクエスト」にはならず、「プリフライトリクエスト」が事前に行われます。

シンプルなリクエスト

要求が「シンプルなリクエスト」になる場合、CORS をサポートしているブラウザからは要求ヘッダに origin: http://<要求元のドメイン> を追加する以外には普通にサーバーに要求を出します。(ただし、IE9 では要求さえ出ませんので注意)

サーバー側では要求ヘッダに含まれる origin フィールドを見て要求元のドメインがクロスドメインアクセスを許可する対象であるかを判定し、受け入れの判断をすることができます。

ただし、上記コードは origin フィールドの内容の検証はしていません。origin フィールドの内容(要求元のドメイン)をそのまま応答ヘッダの Access-Control-Allow-Origin に設定して返しています。それだけでも、* を設定するよりはよさそうです。

ブラウザは応答ヘッダの Access-Control-Allow-Origin: http://<求元のドメイン> を見てクロスドメインアクセスに成功したと判断し、応答に含まれる JSON 文字列などのコンテンツを処置します。

プリフライトリクエスト

要求が「シンプルなリクエスト」にならない場合、CORS をサポートしているブラウザからは実際のリクエストを送信しても安全かを確かめるための「プリフライトリクエスト」が事前に行われます。

上の画像はその要求と応答を Fiddler で見たものです。

左下のウィンドウはブラウザがクロスドメインに対する要求でかつ「シンプルなリクエスト」にならないと判定して自動的にプリフライトリクエストを要求した要求ヘッダの内容です。

赤枠で囲った部分を見てください。HTTP メソッドの OPTIONS、要求ヘッダに含まれる origin, Access-Control-Request-Method, Access-Control-Request-Headers に注目です。

サーバーがクロスドメインからのリクエストを受け、それがプリフライトリクエストであるかどうかを判断するにはそれらの情報をチェックします。

そして、プリフライトリクエストであると判断した場合は上の画像の右下のウィンドウの赤枠で囲ったような応答ヘッダを返します。ここでは、Access-Control-Allow-Origin, Access-Control-Allow-Methods, Access-Control-Allow-Headers は要求ヘッダで受けたものと同じ内容に設定しています。

ブラウザはそれらの情報を見て実際のリクエストを送信しても安全かを確認し、サーバーに要求を出して(上の画像で #3 がそれ)応答を取得し、コンテンツに含まれる JSON 文字列などの情報を処置します。

Tags: , , ,

AJAX

About this blog

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

Calendar

<<  December 2019  >>
MoTuWeThFrSaSu
2526272829301
2345678
9101112131415
16171819202122
23242526272829
303112345

View posts in large calendar