WebSurfer's Home

トップ > Blog 1   |   ログイン
APMLフィルター

img 要素の画像データを取得してアップロード

by WebSurfer 2023年2月1日 14:30

下の画像のように html の img 要素の src 属性に url が指定されてブラウザ上に表示されている状態で、その画像データを JavaScript で取得し、さらにサーバーにアップロードする方法を書きます。

img 要素の画像をアップロード

img 要素から直接画像データを JavaScript で取得する方法は無さそうです。少なくとも自分は見つけられませんでした。なので、img 要素の画像を一旦 canvas に描画し、canvas から画像データを取得するようにしました。

なお、この記事の方法は src 属性の url が同一オリジンになっている場合に限りますので注意してください。

クロスドメインになっている場合は、canvas から画像データを取得する toDataURL メソッドまたは toBlob メソッドで DOMException がスローされます。エラーメッセージは、例えば Chrome で toDataURL メソッドを使った場合、以下のようになります。

DOMException: Failed to execute 'toDataURL' on 'HTMLCanvasElement': Tainted canvases may not be exported.

以下にコード例を載せます。ASP.NET Core MVC の View のものですが、html とJavaScript のコードはそのまま使えるはずです。

コードのコメントに書いた「その1」は toDataURL メソッドを使って画像データを DataURL 形式の文字列として取得し JSON 文字列として送信するものです。データは BASE64 でエンコードされるのでバイナリ形式よりサイズが約 1.3 倍大きくなることに注意してください。

「その2」は toBlob メソッドを使って画像データをバイナリ形式で取得し multipart/form-data 形式で送信するものです。toBlob メソッドは非同期で動き、バイナリデータは引数のコールバックに渡されることに注意してください。

@{
    ViewData["Title"] = "UploadImage";
}

<h1>UploadImage</h1>

<input id="button1" type="button" value="Upload DataUrl" />
<input id="button2" type="button" value="Upload Blob" />
<br />
<img id="image1" src="/images/911GT2_2.jpg" alt="" />
<div id="result"></div>

@section Scripts {
    <script type="text/javascript">
        //<![CDATA[
        let myImage, uploadDataUrl, uploadBlob, myCanvas, resultDiv;

        // DOMContentLoaded イベントのリスナ設定
        window.addEventListener('DOMContentLoaded', () => {
            uploadDataUrl = document.getElementById("button1");
            uploadBlob = document.getElementById("button2");
            myImage = document.getElementById("image1");
            resultDiv = document.getElementById("result");
            myCanvas = document.createElement("canvas");

            uploadDataUrl.addEventListener('click', uploadDataUrlImage);
            uploadBlob.addEventListener('click', uploadBlobImage);
        });

        // その1
        // 上の img 要素の画像を canvas に描画、canvas の画像データ
        // を DataURL 形式の文字列として取得し JSON 文字列を組み立
        // て、それを fetch API を使ってサーバーに送信。
        const uploadDataUrlImage = async () => {
            const context = myCanvas.getContext('2d');
            myCanvas.setAttribute('width', myImage.width);
            myCanvas.setAttribute('height', myImage.height);
            context.drawImage(myImage, 0, 0);
            const dataUrl = myCanvas.toDataURL("image/jpeg");
            const params = {
                method: "POST",
                body: '{"imgBase64":"' + dataUrl + '"}',
                headers: { 'Content-Type': 'application/json' }
            }
            const response = await fetch("/api/Canvas", params);
            if (response.ok) {
                const message = await response.text();
                resultDiv.innerText = message;
            } else {
                resultDiv.innerText = "アップロード失敗";
            }
        }

        // その2
        // img 要素の画像を canvas に描画、canvas の画像データを 
        // Blob 形式で取得し、それを fetch API を使ってサーバー
        // に送信。
        const uploadBlobImage = async () => {
            const context = myCanvas.getContext('2d');
            myCanvas.setAttribute('width', myImage.width);
            myCanvas.setAttribute('height', myImage.height);
            context.drawImage(myImage, 0, 0);

            const data = await getBlobFromCanvas();

            const formData = new FormData();
            formData.append("postedFile", data);
            const param = {
                method: "POST",
                body: formData
            }
            const response = await fetch("/api/Upload", param);
            if (response.ok) {
                const message = await response.text();
                resultDiv.innerText = message;
            } else {
                resultDiv.innerText = "アップロード失敗";
            }
        }

        // toBlob メソッドは非同期で動き、結果の Blob データは引数
        // のコールバックに渡されるので、結果は以下のように Promise
        // にラップして返す
        const getBlobFromCanvas = () => {
            return new Promise((resolve) => {
                myCanvas.toBlob((blob) => resolve(blob), "image/jpeg");
            });
        };

        //]]>
    </script>
}

上のコードで送信された画像データを受け取るサーバー側のコードは、「その1」の Data Url 形式のデータを JSON 文字列で受け取る場合は先の記事「canvas の画像をアップロード (その 2)」の下の方に例がありますので見てください。

「その2」のコードでは以下の Fiddler でのキャプチャ画像のようにバイナリデータが multipart/form-data 形式で送信されます。

Blob を maultipart/form-data 形式で送信

サーバー側のコードはそれを受け取って処理できるように書く必要があります。ASP.NET Core Web API でのコード例を下に載せておきます。

using Microsoft.AspNetCore.Mvc;

namespace MvcCore6App3.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class UploadController : ControllerBase
    {
        // 物理パスの取得用
        private readonly IWebHostEnvironment _hostingEnvironment;

        public UploadController(IWebHostEnvironment hostingEnvironment)
        {
            this._hostingEnvironment = hostingEnvironment;
        }

        // .NET 6.0 で作ったプロジェクトはnull許容参照型がデフォルトで有効に
        // 設定してあるので、引数を NULL 許容にしておかないと、クライアントでファ
        // イルを選択しないで送信した場合、HTTP 400 Bad Request エラーになって
        //  "The postedFile field is required."というエラーメッセージが返ってくる
        [HttpPost]
        public async Task<string> Post([FromForm] IFormFile? postedFile)
        {
            string result = "";
            if (postedFile != null && postedFile.Length > 0)
            {
                // アップロードされたファイル名を取得
                string filename = System.IO.Path.GetFileName(postedFile.FileName);

                // アプリケーションルートの物理パスを取得
                string contentRootPath = _hostingEnvironment.ContentRootPath;

                string filePath = $"{contentRootPath}\\UploadedFiles\\" +
                    $"{filename}{DateTime.Now.ToString("yyyyMMddHHmmss")}.jpg";

                // フォルダ UploadedFile に画像ファイルを保存
                using (var stream = new FileStream(filePath, FileMode.Create))
                {
                    await postedFile.CopyToAsync(stream);
                }

                result = $"{filename} ({postedFile.ContentType}) - " +
                    $"{postedFile.Length} bytes アップロード完了";
            }
            else
            {
                result = "ファイルアップロードに失敗しました";
            }

            return result;
        }
    }
}

JavaScript はブラウザ依存なところがありますが、Edge 109.0.1518.70, Chrome 109.0.5414.120, Firefox 109.0, Opera 94.0.4606.76 で期待通り動くことは確認しました。

Tags: , , , ,

Upload Download

canvas の画像をアップロード (その 2)

by WebSurfer 2022年11月20日 11:46

先の記事「canvas の画像をアップロード」の機能を .NET 6.0 の ASP.NET Core MVC アプリに実装しました。以下に備忘録として書いておきます。

2024/1/2 追記: 「JavaScript の非同期関数」に、この記事と全く同じことを Promise, async, await を使って非同期で行う例がありますので興味があればそちらも見てください)

canvas の画像をアップロード

基本的には先の記事と同じことを行っており、クライアントの PC にある画像を HTML5 の File API を利用して取得し、それを指定のサイズに縮小して HTML5 の canvas に描画し、canvas に描画された画像をサーバーにアップロードして保存するというものです。

先の記事との違いは、(1) アプリが先の記事では ASP.NET Web Forms であったものがこの記事では ASP.NET Core MVC であること、(2) jQuery は使わないようにしたこと(なので、jQuery ajax に代えて fetch API を使ってます)、(3) アップロードされてきたファイルを受けるのを ASP.NET Core Web API のコントローラーとしたことです。

概略の動きは以下の通りです。詳しくは下のサンプルコードとそれに書いたコメントを参照ください。

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

上記を実現するための JavaScript のコードを実装した View のサンプルコードは以下の通りです。

@{
    ViewData["Title"] = "Canvas";
}

<h1>Canvas</h1>

<input type="file" id="file1" />
<input id="button1" type="button" value="Upload" style="display:none;" />
<br />
<div id="msg"></div>
<canvas id="mycanvas"></canvas>
<div id="result"></div>

@section Scripts {
    <script type="text/javascript">
    //<![CDATA[
        // 以下の操作を行っている:

        // File API の FileReader を利用して、input type="file" で
        // 選択された画像ファイルを読み込み、その画像の Data url 
        // 形式の文字列を取得。

        // その Data url 形式文字列を image オブジェクトの src 属
        // 性に設定するとロード完了時に image.onload イベントが発
        // 生するので、そのイベントのリスナで image オブジェクトの
        // 画像を指定のサイズ以下に縮小して canvas に描画する。

        // [Upload] ボタンクリックで uploadImage メソッドを起動。
        // そのメソッド内で canvas の画像データを DataURL 形式で取
        // 得して JSON 文字列を組み立て、それを fetch API を使って
        // サーバーに送信する。

        // 上に述べた FileReader と image オブジェクトへの参照。
        // 初期化は下の DOMContentLoaded イベントのリスナで行う
        let fileReader;
        let image;

        // 画像のサイズとタイプの制限。ここではそれぞれ 500000 
        // バイト、image/jpeg に制限している
        const maxFileSize = 500000;
        const allowedContentType = "image/jpeg";
 
        // canvas に描く画像のサイズ制限。ここでは 500 x 500 以下
        // に制限している
        const maxWidth = 500;
        const maxHeight = 500;

        // 上に定義されている input type="file" などのオブジェクト
        // をいちいち getElementById で取得しなくて済むように以下
        // の変数に保持。代入は下の DOMContentLoaded イベントのリ
        // スナで行う
        let inputFile, uploadButton, msgDiv, myCanvas, resultDiv;         

        // DOMContentLoaded イベントのリスナ設定
        window.addEventListener('DOMContentLoaded', () => {
            inputFile = document.getElementById("file1");
            uploadButton = document.getElementById("button1");
            msgDiv = document.getElementById("msg");
            myCanvas = document.getElementById("mycanvas");
            resultDiv = document.getElementById("result");

            // ブラウザの HTML5 File API サポートを確認
            if (window.File && window.FileReader && window.FileList) {

                // fileReader と image オブジェクトの初期化
                fileReader = new FileReader();
                image = new Image();                

                // [Upload] ボタンクリックで uploadImage メソッド
                // を起動できるようリスナとして設定
                uploadButton.addEventListener('click', uploadImage);

                // input type="file" でファイルの選択が完了すると 
                // change イベントが発生するのでそれにリスナをアタ
                // ッチし、以下の処置を行う
                inputFile.addEventListener('change', () => {
                    resultDiv.innerText = "";

                    // 選択されたファイルのタイプ/サイズの検証
                    // ClientValidate ヘルパメソッドは下の定義参照
                    if (ClientValidate(inputFile) == false) {
                        uploadButton.style.display = "none";
                        myCanvas.style.display = "none";
                        return;
                    }

                    // fileReader オブジェクトに input type="file" 
                    // で選択された画像ファイルを読み込む
                    fileReader.readAsDataURL(inputFile.files[0]);
                });

                // 上の readAsDataURL メソッドは非同期で動くので、
                // 読み込み完了の onloadend イベントのリスナで 
                // FileReader から Data url を取得し image オブジ
                // ェクトの src 属性に設定する
                fileReader.onloadend = () => {
                    image.src = fileReader.result; 
                };
                
                // image オブジェクトで画像のロードが完了すると 
                // image.onload イベントが発生するので、そのイベン
                // トのリスナで image オブジェクトが保持する画像を
                // サイズ制限以下に縮小して canvas 描画する。
                // DrawImageOnCanvas メソッドは下の定義参照
                image.onload = DrawImageOnCanvas;
            }
            else {
                inputFile.style.display = "none";
                myCanvas.style.display = "none";
                msgDiv.innerText =
                    "File API がサポートされていません。";
            }
        });

        // 選択されたファイルの検証のためのヘルパ関数
        function ClientValidate(fileUpload) {
            msgDiv.innerText = "";

            if (fileUpload.files[0] == null) {
                msgDiv.innerText = "ファイルが未選択です。";
                return false;
            }

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

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

            return true;
        }

        // 上で定義した image オブジェクトの src 属性に Data url
        // が設定され画像のロードが完了すると発生する onload イ
        // ベントにアタッチするリスナ。これで image 要素の画像を
        // 指定されたサイズ以下に縮小して canvas に描画する
        function DrawImageOnCanvas() {
            // オリジナル画像のサイズ
            const w = image.width;
            const h = image.height;

            let targetW, targetH;
            const context = myCanvas.getContext('2d');

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

            uploadButton.style.display = "inline-block";
            myCanvas.style.display = "block";
        }

        // canvas の画像データを DataURL 形式で取得し、JSON 文
        // 字列を組み立てて fetch API で送信。
        async function uploadImage() {
            const context = myCanvas.getContext('2d');
            const dataUrl = context.canvas.toDataURL("image/jpeg");
            const params = {
                method: "POST",
                body: '{"imgBase64":"' + dataUrl + '"}',
                headers: { 'Content-Type': 'application/json' }
            }
            const response = await fetch("/api/Canvas", params);
            if (response.ok) { 
                const message = await response.text();
                resultDiv.innerText = message
            } else {
                resultDiv.innerText = "アップロード失敗";
            }
        }
    //]]>
    </script>
}

クライアントから JSON 形式で送信されてきた BASE64 形式の画像データは、以下のコードの ASP.NET Core Web API のコントローラーで受け、デコードしてバイナリ形式に戻してファイルに保存しています。

using Microsoft.AspNetCore.Mvc;

namespace MvcCore6App3.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class CanvasController : ControllerBase
    {
        // 物理パスの取得用
        private readonly IWebHostEnvironment _hostingEnvironment;

        public CanvasController(IWebHostEnvironment hostingEnvironment)
        {
            this._hostingEnvironment = hostingEnvironment;
        }

        [HttpPost]        
        public async Task<string> Post([FromBody] JsonData data)
        {
            // 文字列先頭の "data:image/jpeg;base64," を除去。
            string imgBase64 = 
                data.ImgBase64.Replace("data:image/jpeg;base64,", "");

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

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

            // アプリケーションルートの物理パスを取得
            string contentRootPath = 
                _hostingEnvironment.ContentRootPath;

            string filePath = 
                $"{contentRootPath}\\UploadedFiles\\{filename}";

            // フォルダ UploadedFile に画像ファイルを保存
            await System.IO.File
                 .WriteAllBytesAsync(filePath, imgByteArray);

            return $"ファイル名 {filename} として保存しました。";
        }
    }

    // 先の記事の ASP.NET Web Forms アプリの Web メソッドでは
    // ReceiveImage(string imgBase64) という形で引数の imgBase64
    // に直接  {"imgBase64":"value"} の value を受け取ることがで
    // きたが、Web API ではそのようなことはできず、以下のような
    // クラスを定義して、それで受け取る必要がある
    public class JsonData
    {
        public string ImgBase64 { get; set; } = null!;
    }
}

JavaScript はブラウザ依存なところがありますが、Edge 107.0.1418.52, Chrome 107.0.5304.107, Firefox 107.0, Opera 93.0.4585.11 では期待通り動くことは確認しました。

Tags: , , , , ,

Upload Download

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

by WebSurfer 2019年10月24日 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

About this blog

2010年5月にこのブログを立ち上げました。主に ASP.NET Web アプリ関係の記事です。

Calendar

<<  2024年4月  >>
31123456
78910111213
14151617181920
21222324252627
2829301234
567891011

View posts in large calendar