先の記事「canvas の画像をアップロード」の機能を .NET 6.0 の ASP.NET Core MVC アプリに実装しました。以下に備忘録として書いておきます。
(2024/1/2 追記: 「JavaScript の非同期関数」に、この記事と全く同じことを Promise, async, await を使って非同期で行う例がありますので興味があればそちらも見てください)
基本的には先の記事と同じことを行っており、クライアントの 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 のコントローラーとしたことです。
概略の動きは以下の通りです。詳しくは下のサンプルコードとそれに書いたコメントを参照ください。
-
クライアントによる画像ファイルの選択は HTML の <input type="file" ... /> を利用する。
-
<input type="file" ... /> で画像ファイルが選択されたタイミングで、HTML5 File API の FileReader オブジェクトに readAsDataURL メソッド を使って選択された画像ファイルを読み込む。
-
FileReader の result プロパティ を使って、読み込んだ画像ファイルを Data url 形式("data:image/jpeg;base64, ..." という文字列)で取得し、それを image オブジェクトの src 属性に設定する。
-
その image オブジェクトを HTML5 の canvas に CanvasRenderingContext2D.drawImage() メソッド を使って描画する。その際、描画する画像の最大サイズの制限を設け(今回のサンプルでは 500 x 500 とした)、それに入る場合はそのまま、入らない場合は幅・高さどちらか大きい方を 500px に縮小し他方をその縮小率と同じに縮小(要するに縦横比を保ったまま 500 x 500 に入るよう縮小)する。
-
canvas 上の縮小後の画像データを取得して Web サーバーに fetch API を使って送信するメソッドを作る。canvas からの画像データの取得は HTMLCanvasElement.toDataURL() メソッド を用いる。Data url 形式で取得できるので、それを fetch API を用いて JSON 形式で送信する。(BASE64 でエンコードされているので、バイナリ形式よりサイズが約 1.3 倍大きくなってしまうが・・・)
-
<input type="button" ... /> タイプのボタンを配置し、その onclick 属性に上記のメソッドを設定する。
-
非同期でクライアントから送信された BASE64 形式の画像データは、ASP.NET Core Web API のコントローラーで受け、デコードしてバイナリ形式に戻してファイルに保存する。
-
先の記事と同様に、画像ファイルのサイズを 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 では期待通り動くことは確認しました。