PowerAppsで撮影した写真にお絵描きする(外部API無し版)

# 概要

- 現場報告アプリなどで、撮影した画像の上にペン入力で絵を書いて保存したいときに使える方法です。
- 以前の投稿で外部APIや[Azure Functions](https://qiita.com/Rambosan/items/29ae971e1e7463559b4f)などを使って画像合成するアプリを作成しましたが、それらは使いません。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/293667/22456bdc-3de5-7265-644b-969e1065b317.png)



# 事前準備
2通りの方法があります。
ペン入力で絵を書くUIはそれぞれ共通であるため先に作っておきます。
上記サンプルは背景が「写真」ですが、今回は事前に用意した「jpeg画像」を背景とした「塗り絵アプリ」で解説します。
主に2つの画面を使います。

**①お絵描き画面**
背景となる画像コントロールとペン入力コントロールを重ねて配置し、画像の上に描けるようにする画面です。

ペン入力で印や文字を書いて、保存ボタンで2つの画像を1枚に合成して②の画面に移動します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/293667/745ff863-f20d-105d-d9af-31ef88a2440f.png)
各コントロールのサイズは以下のようにします。

画像コントロールの縦横
→Imageとなる画像や写真と縦横比と一致させます。

ペン入力コントロールの縦横
→画像コントロールと一致させますが、下側60pxがツールバーの領域なのでHeightは+60します。

**②作品保存画面**
ペン入力を確定させ作品名をつけて保存する画面です。

作品名を入力し保存を押すと、Power Automateを経由して画像をSharePointに保存します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/293667/4176ee34-61b3-1e3f-21b4-0327ae13284c.png)

その他細かい実装は以前の記事と同じです。
https://qiita.com/Rambosan/items/29ae971e1e7463559b4f



# 画像合成する方法2つ

# その1 SVGを使う方法
svgタグをつかって2枚の画像を合成(レイヤー化)した状態にして保存します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/293667/93744222-c0fa-66d1-14cb-344e38cf4f61.png)


## 必要なリソース

・保存用SharePointリスト
・保存用Power Automate(Appsトリガー)
・アプリ本体

## 解説

### Power Apps

**①お絵描き画面**

保存ボタンの関数は以下のように設定します。

```JavaScript:OnSelect
UpdateContext({_bgImage :Substitute(JSON(背景画像.Image,IncludeBinaryData),"""","")});
UpdateContext({_fgImage : Substitute(JSON(ペン入力.Image,IncludeBinaryData),"""","")});
Navigate(SvgScreen_SvsMerge,ScreenTransition.Cover,{_bgImage:_bgImage , _fgImage : _fgImage, _width:_ImageWidth, _height : _ImageHeight  } );
```
Navigate()の第三引数で移動先画面の変数を4つ設定しています。
_bgImage：背景画像のDataUri
_fgImage：ペン入力画像のDataUri
_width：画像の幅
_height ：画像の高さ

画像の幅高さは事前に定義しておく必要があります。
上記の塗り絵アプリでは事前に画像ごとの縦横サイズを管理しています。
背景をカメラコントロールの画像とする場合は、_width:640,_height:480を設定します(AndroidやWindowsデバイス)。


**②作品保存画面**

2枚の画像をSVGで合成します。
真ん中に配置した画像のImageプロパティは以下のようになります。
imageタグを2つ書くとそのまま重なります。
_height などは①のNavigateで定義した変数です。


```JavaScript:Image
"data:image/svg+xml,"& 
EncodeUrl(
"<svg height='" & _height & "' width='" & _width & "' viewBox='0 0 "& _width &" "& _height &"' xmlns='http://www.w3.org/2000/svg'> 
    <image href='"& _bgImage &"' height='" & _height  & "' width='" & _width & "'/>
    <image href='"& _fgImage &"' height='" & _height + 60  & "' width='" & _width & "'/>
</svg>"
)
```

2つ目のimageはペン入力画像です。
ペン入力コントロールの出力画像は、「背景が透過(色指定しなければ)」、「縦横サイズはコントロールのサイズ」といった特徴があります。
画像の下側60pxはツールバーなのでheightに+60しています。
60pxはviewboxからはみ出しますが描画されません。

保存ボタンでは、Power Automateを実行して画像コントロールのImageと作品名を送信します。
```JavaScript:保存ボタン
SVGで画像合成フロー.Run(合成後の画像.Image  , TextInput_作品名_2.Text);
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/293667/b9896f72-c1d7-bb50-d56a-12ee5586e143.png)

### Power Automate(SharePoint保存用)
以下のようなフローでSharePointリストに保存します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/293667/e2b84d5e-5e4a-9dea-98d3-9d0dd723261a.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/293667/d1bb3be0-fcce-dcdc-211c-3de2a74f35a4.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/293667/dbd79e36-9789-867c-82f8-7638c18a9f25.png)

保存先はSharePointリストの添付ファイルです。
※ドキュメントライブラリはsvgのサムネイルに対応していないためPower Appsに表示できません。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/293667/0c252078-7df3-7873-356f-5fd869e24b45.png)

svgファイルの中身は以下のようになります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/293667/1427b184-609a-7032-b573-a29e16b07645.png)



### Power Appsで作品の表示
Power Appsから画像を表示するには、ギャラリーコントロールなどに画像コントロールを配置します、
Imageには、```First(ThisItem.添付ファイル).Value```を設定しファイルコンテンツを取得して表示します。

※ただし、モバイルアプリではsvgを表示できませんでした。
普通のsvg画像は表示できるため何か工夫が必要かもしれません。


# その2 カスタムコードを使う方法

カスタムコネクタを使った方法です。
**C#カスタムコード**の機能を使ってプログラムで画像を合成します。
外部のAPIは利用しませんｗ

カスタムコードを使ったコネクタの作り方は以下

https://qiita.com/Rambosan/items/7cda28bac81da794b935

### 構成
・保存用SharePointライブラリ
・保存用Power Automateフロー(Appsトリガー)
・カスタムコネクタ
・アプリ本体

## 解説

### カスタムコネクタの作成

まずはカスタムコネクタを作成します。
基本的な作成方法は[こちら](https://qiita.com/Rambosan/items/7cda28bac81da794b935)

設定は以下のようにします。


コネクタ名：cs_CustomCode

基本設定：
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/293667/5e67c378-f922-bcc6-8c6e-f6e9f660fb15.png)

認証：なし

定義：
操作ID「MergeImages」

要求：
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/293667/61bc2272-4f6d-70c7-b502-2cdc8b196da1.png)

```
{
    "image_fg_datauri":"data:image/png;base64,iVBORw0KGgoAAAmM～",
    "image_bg_datauri":"data:image/png;base64,iVBORw0KGgoAAAAN～"
}
```

応答
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/293667/0a6ea8f7-c5a2-b5cc-4135-fe7c27fc01c4.png)

```
{
    "data":"data:image/png;base64,iVBORw～"
}
```

### カスタムコードの登録

カスタムコードに以下を登録します。
制約が多いので手続き的なコードになってしまいますが・・

```CS:カスタムコネクタのコード
public class Script : ScriptBase {

        public override async Task<HttpResponseMessage> ExecuteAsync() {
            // Create a new response
            var response = new HttpResponseMessage();

            var body = await Context.Request.Content.ReadAsStringAsync().ConfigureAwait(false); 
            var contentAsJson = JObject.Parse(body);

            Bitmap bg = DataUriToBitmap((string)contentAsJson["image_bg_datauri"]);
            Bitmap fg = DataUriToBitmap((string)contentAsJson["image_fg_datauri"]);

            var nurieBitmap = MergeTwoImages(fg, bg, true);
            var dataUri = BitmapToDataUri(nurieBitmap,ImageFormat.Png);

            // Initialize a new JObject and call .ToString() to get the serialized JSON
            response.Content = CreateJsonContent(new JObject {
                ["data"] = dataUri
            }.ToString());

            return response;
        }

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

            var bitmapBase = new Bitmap(bg);

            //ペン入力ツールバーの60pxを切り抜き
            Bitmap fgBitmap;
            if (cropToolbar) {
                if (fg.Height <= 60) {
                    throw new ArgumentException("Image height must be more than 60px");
                }
                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;
        }

        private Bitmap DataUriToBitmap(string dataUri) {
            var base64 = DatauriToBase64(dataUri);
            return Base64ToBitmap(base64);
        }

        private string DatauriToBase64(string dataUri) {
            var match = Regex.Match(dataUri, @"data:image/(?<type>\w{3,4});base64,(?<data>.+)");
            if (match.Success == false) {
                throw new ArgumentException("datauri is invalid format");
            }

            return match.Groups["data"].Value;
        }

        private Bitmap Base64ToBitmap(string base64) {

            byte[] binData = Convert.FromBase64String(base64);

            Bitmap bitmap;
            using (var stream = new MemoryStream(binData)) {
                bitmap = new Bitmap(stream);
            }

            return bitmap;
        }

        private string BitmapToDataUri(Bitmap bitmap, ImageFormat imageFormat) {

            string base64;
            using (var stream = new MemoryStream()) {
                bitmap.Save(stream, imageFormat);
                base64 = Convert.ToBase64String(stream.GetBuffer());
            }

            var format = imageFormat.ToString().ToLower();
            return $"data:image/{format};base64,{base64}";
        }

    }
```


### Power Apps

**①お絵描き画面**

画像の保存ボタンは上記で作成したカスタムコネクタを呼び出します。
成功すると、.dataに合成された画像、png形式のdatauriが格納されます。
pngなので保存先はドキュメントライブラリでもOKです。



```JavaScript:保存ボタン
Set(
    nurieImage,
    cs_CustomCode.MergeImages(
        {
            image_bg_datauri:Substitute(JSON(Image_NurieBg.Image,IncludeBinaryData),"""",""),
            image_fg_datauri:Substitute(JSON(PenInput_NurieFg.Image,IncludeBinaryData),"""","")
        }
    ).data
);

If(IsEmpty(nurieImage),
    Notify("画像の保存に失敗しました",NotificationType.Error),
    Navigate(作品保存画面,Cover)
)
```

**②作品保存画面**

Power Automateを実行してドキュメントライブラリに保存します。

```JavaScript:保存ボタン
If(!IsBlank(Text作品名.Text),
    合成画像をライブラリに保存.Run(Text作品名.Text & ".png",合成後の画像.Image)
    Navigate(塗り絵作品一覧,ScreenTransition.Cover);
)
```

### Power Automateのフロー
Power Appsからファイル名と画像のDataUriを受け取り、ファイルをドキュメントライブラリに保存するフローです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/293667/2bc2aeff-cf0d-b951-e55e-485073ec0817.png)

こちらの電源フローテンプレートを改造すると簡単です。
https://japan.flow.microsoft.com/ja-jp/galleries/public/templates/fe971b57c1994482b565ccfce936900d/power-apps-%E3%81%8B%E3%82%89-sharepoint-%E3%81%AB%E5%86%99%E7%9C%9F%E3%82%92%E3%82%A2%E3%83%83%E3%83%97%E3%83%AD%E3%83%BC%E3%83%89%E3%81%99%E3%82%8B/


### 作品一覧画面
保存先はドキュメントライブラリなのでThisItem.サムネイル.Largeで表示可能です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/293667/c0992ebd-9957-0c32-73ab-b88f26d58c7a.png)

# あとがき
Power Appsで画像にペンで絵を描くをやりたい人がいたので記事にしてみました。
標準機能でも対応してほしいですね。