WebSurfer's Home

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

input type="file" を生成してアップロード

by WebSurfer 2023年4月2日 20:46

JavaScript を使って、ボタンクリックでファイル選択ダイアログを表示し、ユーザーがダイアログに表示されたファイルを選択したら即アップロードする方法を書きます。

ファイルアップロード

ファイルのアップロードでよくあるパターンは、<form> 要素の中に <input type="file"> 要素と <input type="submit"> 要素を静的に配置しておいて、(1) <input type="file"> のファイル選択ボタンをクリックしてファイル選択ダイアログを表示、(2) ユーザーがダイアログからファイルを選択、(3) <input type="submit"> をクリックして選択されたファイルを POST 送信する・・・というものだと思います。

その (1) から (3) のステップを短縮し、<input type="button"> ボタンをクリックしたらファイル選択ダイアログを表示し、ユーザーがファイルを選択したら File API を利用して File オブジェクトを取得し、それを fetch API を使って即 Web API にアップロードしてみます。

クライアント側のコードは以下の通りです。<input type="button"> ボタンだけを静的に配置しておき、それ以外は JavaScript で処置しています。説明はコード内のコメントに書きましたのでそれを見てください。

コードは ASP.NET Core MVC の View をものをそのままコピー&ペーストしましたが、その中の html と JavaScript のコードはどのような Web アプリにも使えるはずです。

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

<h1>UploadFile</h1>

<button type="button" class="btn btn-primary" id="btn">
    ファイル選択してアップロード
</button>

<div id="result"></div>

@section Scripts {
   <script type="text/javascript">
        window.addEventListener('DOMContentLoaded', () => {

            // Promise を返す非同期メソッド
            const showOpenFileDialog = async () => {
                return new Promise(resolve => {
                    // input type="file" 要素を動的に生成、
                    const input = document.createElement('input');
                    input.type = 'file';

                    // .jpg ファイルのみ選択できるよう設定してみた
                    input.accept = '.jpg';

                    // ユーザーがファイルを選択すると change イベント
                    // が発生するのでそれにリスナをアタッチし、File
                    // オブジェクトを取得。それを Promise の resolve
                    // コールバックに設定
                    input.addEventListener('change', 
                            e => resolve(e.target.files[0]));

                    // クリックしてファイル選択ダイアログを開く
                    input.click();
                });
            };

            // ボタンクリックで上のメソッドを呼び出し File オブジェ
            // クトを取得。それを fetch API を使って Web API に送信
            document.getElementById("btn")
                    .addEventListener('click', async () => {
                const file = await showOpenFileDialog();
                const formData = new FormData();
                formData.append("postedFile", file);
                const param = {
                    method: "POST",
                    body: formData
                }
                const response = await fetch("/api/Upload", param);
                const data = await response.text();
                document.getElementById("result").innerText = data;
            });
        });
   </script>
}

サーバー側で、アップロードされてきたファイルを受けて保存する 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;
        }
    }
}

Tags: , , ,

Upload Download

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

About this blog

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

Calendar

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

View posts in large calendar