🦁

Blazor WebAssembly: CanvasにC#のピクセルbyte配列を描画する

2022/01/05に公開

全然わからないに近い状態であり、とりあえず動いた気がする、程度の内容です。

C#側で画像のピクセルデータのbyte[]を持っているとして、それをHTML5 Canvasに転送して描画する方法です。

成果物

https://github.com/shimat/opencvsharp_blazor_sample

これは本記事の内容の少し発展形 (拙作OpenCvSharpのMatを表示するもの) ですがほとんど同じです。以下のようなものが出来上がります。

本当はきれいなグラデーションになりますが、Zennで圧縮されてしまうためか模様ができています。

完成図

C#側からJavaScriptを呼ぶ方法

https://docs.microsoft.com/ja-jp/aspnet/core/blazor/javascript-interoperability/call-javascript-from-dotnet?view=aspnetcore-6.0

「バイト配列のサポート」のところに説明されているように、C#のbyte[]は直接JavaScriptへ流しこめます。canvasのImageDataにそのまま入れられるということです。

コード

詳細を論じられるほど理解していないので、ひたすらコードスニペットを示して終わります。

wwwroot/js/canvas.js

HTMLで<canvas>要素を、加えてJavaScriptではC#のbyte[]データを受け取り、byte[]をCanvasにそのまま流し込む関数を定義しておきます。

wwwroot/js/canvas.js
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 から参照しておきます。

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#から呼び出すヘルパークラスです。

CanvasClient.cs
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バイト、アラインメントの考慮は要らないようです。
https://developer.mozilla.org/en-US/docs/Web/API/ImageData/data

OnAfterRenderAsyncについてはこちらが参考になります。
https://docs.microsoft.com/ja-jp/aspnet/core/blazor/components/lifecycle?view=aspnetcore-6.0

Pages/Canvas.razor
@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に登録しておくとよいでしょう。

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[] として得ることができます。

Pages/FileSample.razor
@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