Blazor WebAssembly: CanvasにC#のピクセルbyte配列を描画する
全然わからないに近い状態であり、とりあえず動いた気がする、程度の内容です。
C#側で画像のピクセルデータのbyte[]
を持っているとして、それをHTML5 Canvasに転送して描画する方法です。
成果物
これは本記事の内容の少し発展形 (拙作OpenCvSharpのMatを表示するもの) ですがほとんど同じです。以下のようなものが出来上がります。
本当はきれいなグラデーションになりますが、Zennで圧縮されてしまうためか模様ができています。
C#側からJavaScriptを呼ぶ方法
「バイト配列のサポート」のところに説明されているように、C#のbyte[]
は直接JavaScriptへ流しこめます。canvasのImageDataにそのまま入れられるということです。
コード
詳細を論じられるほど理解していないので、ひたすらコードスニペットを示して終わります。
wwwroot/js/canvas.js
HTMLで<canvas>
要素を、加えてJavaScriptではC#のbyte[]
データを受け取り、byte[]をCanvasにそのまま流し込む関数を定義しておきます。
var drawPixels = function (canvasElement, imageBytes) {
const canvasContext = canvasElement.getContext("2d");
const canvasImageData = canvasContext.createImageData(canvasElement.width, canvasElement.height);
canvasImageData.data.set(imageBytes);
canvasContext.putImageData(canvasImageData, 0, 0);
}
このjsを、wwwroot/index.html
から参照しておきます。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<title>BlazorApp2</title>
<base href="/" />
<link href="css/bootstrap/bootstrap.min.css" rel="stylesheet" />
<link href="css/app.css" rel="stylesheet" />
<link href="BlazorApp.styles.css" rel="stylesheet" />
</head>
<body>
<div id="app">Loading...</div>
<div id="blazor-error-ui">
An unhandled error has occurred.
<a href="" class="reload">Reload</a>
<a class="dismiss">🗙</a>
</div>
<script src="_framework/blazor.webassembly.js"></script>
<script src="/js/canvas.js"></script> <!-- ここ -->
</body>
</html>
CanvasClient.cs
上で用意したJavaScriptのdrawPixels
関数をC#から呼び出すヘルパークラスです。
using Microsoft.AspNetCore.Components;
using Microsoft.JSInterop;
namespace BlazorApp
{
public class CanvasClient
{
private readonly IJSRuntime jsRuntime;
private readonly ElementReference canvasElement;
public CanvasClient(
IJSRuntime jsRuntime,
ElementReference canvasElement)
{
this.jsRuntime = jsRuntime;
this.canvasElement = canvasElement;
}
public async Task DrawPixelsAsync(byte[] pixels)
{
await jsRuntime.InvokeVoidAsync("drawPixels", canvasElement, pixels);
}
}
}
Pages/Canvas.razor
ページの内容です。ここでは、適当にピクセルデータを作り出して、canvas要素へ送り込みます。
ピクセルデータの生成は以下を真似ました。このように基本的にはJavaScriptでの流儀と同じように作ればよいだけです。ピクセルはRGBAの順で、それぞれ1バイト、アラインメントの考慮は要らないようです。
OnAfterRenderAsync
についてはこちらが参考になります。
@page "/canvas"
@inject IJSRuntime jsRuntime;
<PageTitle>Canvas Sample</PageTitle>
<h1>Canvas Sample</h1>
<canvas @ref="canvasElement" width="128" height="128">
</canvas>
@code {
private ElementReference canvasElement;
protected override async Task OnAfterRenderAsync(bool firstRender)
{
await base.OnAfterRenderAsync(firstRender);
var imageBytes = new byte[128 * 128 * 4];
unsafe
{
fixed (byte* pImageBytes = imageBytes)
{
var p = pImageBytes;
for (int y = 0; y < 128; y++)
{
for (int x = 0; x < 128; x++)
{
// Percentage in the x direction, times 255
var xp = (byte)(x / 128.0 * 255);
// Percentage in the y direction, times 255
var yp = (byte)(y / 128.0 * 255);
*(p++) = xp; // R
*(p++) = yp; // G
*(p++) = (byte)(255 - xp); // B
*(p++) = 255; // A
}
}
}
}
var canvasClient = new CanvasClient(jsRuntime, canvasElement);
await canvasClient.DrawPixelsAsync(imageBytes);
}
}
作った.razorファイルは、Shared/NavMenu.razor
に登録しておくとよいでしょう。
<div class="top-row ps-3 navbar navbar-dark">
<div class="container-fluid">
<a class="navbar-brand" href="">BlazorApp</a>
<button title="Navigation menu" class="navbar-toggler" @onclick="ToggleNavMenu">
<span class="navbar-toggler-icon"></span>
</button>
</div>
</div>
<div class="@NavMenuCssClass" @onclick="ToggleNavMenu">
<nav class="flex-column">
<div class="nav-item px-3">
<NavLink class="nav-link" href="" Match="NavLinkMatch.All">
<span class="oi oi-home" aria-hidden="true"></span> Home
</NavLink>
</div>
<!-- これ -->
<div class="nav-item px-3">
<NavLink class="nav-link" href="canvas">
<span class="oi oi-map" aria-hidden="true"></span> Canvas Sample
</NavLink>
</div>
</nav>
</div>
@code {
private bool collapseNavMenu = true;
private string? NavMenuCssClass => collapseNavMenu ? "collapse" : null;
private void ToggleNavMenu()
{
collapseNavMenu = !collapseNavMenu;
}
}
画像ファイルを読み込みたい場合
wwwroot/
以下の適当な場所に画像ファイルを置きます。ここでは wwwroot/foo/bar.bmp
として置いたと仮定すると、以下のコードでそれを byte[]
として得ることができます。
@page "/file_sample"
@inject HttpClient httpClient;
<PageTitle>File Sample</PageTitle>
@code {
protected override async Task OnAfterRenderAsync(bool firstRender)
{
var imageBytes = await httpClient.GetByteArrayAsync("foo/bar.bmp");
}
}
ただし、前述のピクセルデータのsetへとつなげようと考える場合、BMPやJPEG等のbyte[]
を得たところでそれをデコードしなければなりません。BASE64文字列にしてImage.src
にセットしてからCanvasにdrawImageするとか、C#側でデコードするとか、色々と手は考えられます。
Discussion