AzureFunctionsとPowerAppsでぬり絵アプリを作ってみた

#概要
- Power Appsで自律神経を整えよう、ぬり絵アプリを作ってみました。
- Power Appsにはペン入力がありますが現状背景画像を設定できないので、下絵画像とペン入力画像を合成をAzure Functions(C#) でAPIを作って対応しました
- 応用すると、現場報告系のアプリで、撮影した写真に◯を書いて保存するようなアプリも作成可能です。

![PowerNurieApp2.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/293667/a02af594-b55e-a478-4b7d-4c54122887e1.gif)
※ペンタブで塗っています。

#アプリの仕様
- 塗り絵の下絵はギャラリーから選べる(SharePointリストに)
- 塗った絵に名前を付けて保存できる(AzureFunctions/Automate)
- あとで作品一覧を閲覧できる(ドキュメントライブラリ)

以前[Remove.bgというサービスを使って手軽にお絵かきアプリを実現しました](
https://qiita.com/Rambosan/items/8bc330d65d8dbe5b090a)が、無料プランだと回数制限があったり、性能の無駄遣い感があったため、自前でC#を使ってAzureFunctions上にAPIを作ってみました。
同じものを作るのも面白くないため、何か考えた結果ぬり絵アプリになりました。
PowerAppsにない機能は外部のAPI/自前のAPIを使って実現できるのが素晴らしいですね。

#作成方法
工夫が必要な部分だけ説明してみます。
APIの呼び出しは手軽にPower Automateを経由してHTTPでAzure Functionsを呼び出しました。
HTTPを利用するためコミュニティプランか有償プランが必要です。
for M365プラン内でやりたい場合、Dataverse for Teams環境からAPI Managementを経由して呼び出します。
覚え書き程度の糞記事ですが→https://qiita.com/Rambosan/items/bea833527d97c8ec6c4e


#構成
- SharePointリスト：ぬり絵の下地となる画像と画像サイズの情報を格納します。
- Power Apps：ぬり絵アプリ本体です。ぬり絵をリストから選ぶ→塗り塗り→Automate経由でAPI呼び出し合成→名前をつけてドキュメントライブラリに保存。
- Azure Function：Automateから受け取った2つの画像を合成します。
- Power Automate：Powe rAppsから背景画像(ぬり絵下地)、前景画像(ペン入力)を取得して画像合成APIを呼び出し、Power Appsに画像を返します。
- ドキュメントライブラリ：作品を保存するライブラリです。


#SharePointリスト
- 下絵を管理するリストです。
- タイトルの他、幅、高さが入力できるようにし、添付ファイルに下絵画像を添付します。
- 画像によって縦横のサイズが違うため、後でサイズ情報が必要になります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/293667/ba2395a9-6711-9a2c-f238-3b5a0874b028.png)

#Power Apps
ペン入力の画像を下絵とズレないようにするためには、下絵画像とペン入力を同じ縦横比になるよう調整します。
ペン入力から出力される画像サイズはコントロールのサイズとなりますので、これを利用します。

###塗り絵選択画面
SharePointリストから下絵を読み込みギャラリーで表示します
ギャラリーのOnClickでぬり絵に移動、画面引数で画像、縦、横を渡します。

```JavaScript:OnClick
Reset(PenInput_NurieFg);
Navigate(EditScreen_Nurie,ScreenTransition.Cover,{Width:ThisItem.Width,Height:ThisItem.Height,Image:Image_Thumbnail.Image})
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/293667/5b285508-f1a3-bdee-1c8e-20fae07fe04d.png)

###塗り絵画面
画像コントロールとペン入力コントロールを組み合わせて、塗り絵をしているように見せかけます。
画像の大きさは下絵によってバラバラなので、選んだ画像のサイズによって2つコントロールの大きさを変更してあげます。
ペン入力から出力される画像サイズはコントロールのサイズとなります。これを画像コントロールに合わせて調整してあげることで同じ縦横比で画像を作ることができます。


①画面のOnVisible。ぬり絵画面の最大サイズを設定してあげて、縦横比を維持したまま画像サイズを決定します。
Width、Heightは塗り絵選択画面から受け取った画面変数で、下絵画像のサイズが入っています。

```JavaScript:OnVisible
With(
    //最大幅、高さを設定
    {_maxWidth:1000,_maxHeight:600},
    With(
        {_minAspectRatio:Min(_maxWidth/Width,_maxHeight/Height)},
        UpdateContext({_ImageWidth:Width * _minAspectRatio});
        UpdateContext({_ImageHeight:Height * _minAspectRatio});
    );
)
```

②画像コントロール
Imageプロパティに画面引数で受け取ったImageをセットします。
Width、HeightはOnVisibleで計算した_ImageWidth、_ImageHeightを設定します。

③ペン入力
ペン入力ツールバーの✕ボタンは、塗り絵中に押してしまうと発狂するため、四角アイコンで隠しておきます。
※消しゴムアイコンでResetする
Widthは_ImageWidth、Heightは_ImageHeight+60を設定。
この60pxはツールバーの領域です。　ペン入力から出力される画像はツールバー部分も含まれますが、後のAPIで下60pxはクロップします。

④保存ボタン
ここで、この後のPowerAutomateを呼び出して画像を合成します。
その後保存画面に移動させます。

```JavaScript:
Set(
    MergedNurieImage,
    PowerApps塗り絵合成フロー.Run(
        Substitute(JSON(PenInput_NurieFg.Image,IncludeBinaryData),"""",""),
        Substitute(JSON(Image_NurieBg.Image,IncludeBinaryData),"""","")
    )
);
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/293667/c97c296e-9d94-f935-7160-850e935cd485.png)

###保存画面
APIから受け取った合成後の画像を表示し、名前を入力して保存できる画面です。

後になりますが、MergedNurieImage.response.dataには画像のDataUriが入るので、
画像コントロールのImageに設定します。

後は作品名の入力欄を用意し、保存ボタンクリックで画像をドキュメントライブラリに保存するフローを実行します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/293667/e29c852b-6223-af05-a3fc-3ce01d544535.png)

###作品画面
ギャラリーコントロールでドキュメントライブラリを表示するだけです。


#Power Automate

①画像を合成するフロー
比較的単純です

全体
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/293667/d9b9da9b-5300-e3b1-9140-afb9787bd26b.png)

HTTP部分(URLにはAzureFunctionのURIを入れます。(Functionキー付きの
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/293667/912eba6b-973b-d0a9-51d7-c23dd6b6793d.png)

②画像の保存フロー
こちらのテンプレートを使って改造します。
PowerAppsからファイル名とファイルコンテンツを受け取ってドキュメントライブラリに保存するだけです。
https://japan.flow.microsoft.com/ja-jp/galleries/public/templates/fe971b57c1994482b565ccfce936900d/upload-a-photo-to-sharepoint-from-power-apps/

#Azure Functions
C#で作った動くだけのKUSOコードです。

https://github.com/Rambosan/PowerAppsFunction

バリデーションやらでコード全体は長くなりましたが、メイン関数はこれだけです。
ペン入力の画像は既に背景が透過なので、画像の合成はGraphicsを使った単純な合成処理で済みます。

```C#:MergeImages.cs

public static class MergeImages
{
    [FunctionName("MergePowerAppsImages")]
    public static async Task<IActionResult> Run(
        [HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = null)] HttpRequest req,
        ILogger log){

        log.LogInformation("C# HTTP trigger function processed a request.");

        //バリデーション
        HttpRequestBody<NurieImageEntity> requestBody;
        try {
            requestBody = await req.GetBodyAsync<NurieImageEntity>();
        }
        catch (Exception e) {
            return new BadRequestObjectResult(e.Message);
        }
        
        if (!requestBody.IsValid) {
            return new BadRequestObjectResult(requestBody.ValidationResults.First().ErrorMessage);
        }

        NurieImageEntity entity = requestBody.Value;

        //ビットマップへの変換
        Bitmap fgBitmap, bgBitmap;
        try {
            fgBitmap = entity.image_fg_datauri.ReadAsBitmap();
        }
        catch (Exception) {
            return new BadRequestObjectResult($"{nameof(entity.image_fg_datauri)}の変換に失敗しました。");
        }

        try {
            bgBitmap = entity.image_bg_datauri.ReadAsBitmap();
        }
        catch (Exception) {
            return new BadRequestObjectResult($"{nameof(entity.image_bg_datauri)}の変換に失敗しました。");
        }
        //ペン入力ツールバーのクロップ有無
        var cropFgToolbar = false;
        if (entity.image_fg_crop == "true") cropFgToolbar = true;


        //画像の合成
        var nurieBitmap = MergeTwoImages(fgBitmap, bgBitmap, cropFgToolbar);
        var dataUri = new ImageDataUri(ImageFormat.Png, nurieBitmap);

        object resultJson = new
        {
            data = dataUri.ToString()
        };

        return new JsonResult(resultJson);

    }

    /// <summary>
    /// 背景画像に前景画像を描画してビットマップを返します。
    /// </summary>
    /// <param name="fg">前景画像</param>
    /// <param name="bg">背景画像</param>
    /// <param name="cropToolbar">ツールバーの60pxを切り抜くかどうか</param>
    /// <returns></returns>
    public static Bitmap MergeTwoImages(Bitmap fg, Bitmap bg, bool cropToolbar = false) {

        var bitmapBase = new Bitmap(bg);

        //ツールバーの60pxを切り抜き
        Bitmap fgBitmap;
        if (cropToolbar) {
            fgBitmap = fg.Clone(new Rectangle(0, 0, fg.Width, fg.Height - 60), fg.PixelFormat);
        }
        else {
            fgBitmap = fg;
        }

        //ビットマップに前景画像を描画
        using (var g = Graphics.FromImage(bitmapBase)) {
            g.DrawImage(fgBitmap, g.VisibleClipBounds);
        }

        return bitmapBase;
    }

```

#参考資料など
HttpRequestのバリデーション
https://tsuyoshiushio.medium.com/how-to-validate-request-for-azure-functions-e6488c028a41
塗り絵素材はぬりえパークさんからいただきました。
https://nurie-park.com/
Azure Functions
https://docs.microsoft.com/ja-jp/azure/azure-functions/functions-create-your-first-function-visual-studio

#あとがき
Power Appsで不足する機能をAzure Functionsを使って強化しました。
撮影した画像に描画するといった要件は現場系だとあると思うので、ペン入力の背景画像を設定できるようになってほしいですね。