Power Appsのキャンバス アプリでリッチ テキスト エディターに貼り付けた画像を保存する方法

# はじめに
皆様はキャンバスアプリの[リッチ テキスト エディター](https://learn.microsoft.com/ja-jp/power-apps/maker/canvas-apps/controls/control-richtexteditor)を使うことがありますか？

https://learn.microsoft.com/ja-jp/power-apps/maker/canvas-apps/controls/control-richtexteditor

`OneNote`やメールでもおなじみ、文字装飾ができる非常に便利な機能です。
このような機能ですと、`スクリーンショット`をはじめとした*画像の貼り付け*、ということも当然やりたくなります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2734332/0fd39f72-1928-48f4-a4c3-b2e568d82f9f.png)

いざ使ってみると[リッチ テキスト エディター](https://learn.microsoft.com/ja-jp/power-apps/maker/canvas-apps/controls/control-richtexteditor)には **画像の貼り付け自体はできます。**

一方でデータは登録されるでしょうか？下記のリストで試してみましょう。

**■ TestHTML 列**
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2734332/4d8a97f1-6f68-4567-ae02-64ebc3118120.png)

**■ 登録するSharePoint Lists**
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2734332/9c62f88f-a92a-4e51-9d49-b97e11a56273.png)

```javascript:OnSelect
Patch(
    Test_RichText, // SharePoint Lists
    Defaults(Test_RichText), // 新規登録
    {TestHTML: rteImageWriter.HtmlText} // リッチ テキスト エディター
);
```


この設定の列を作成し、リッチテキストを保存してみます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2734332/1a96bde2-4dd1-454f-9c5f-1c9d41f79352.png)

画像が無視されて情報が登録されました😲

この挙動は[Microsoft MVP for Business Applicationsのヨウセイさんのブログ](https://youseibubu.com/apps/richtext_imagepaste1/)が非常に参考になります。

https://youseibubu.com/apps/richtext_imagepaste1/

`Dataverse`であればいい感じにできますが、SharePoint Listsの場合は上手くできません。なんってこった。


# SharePoint Listsの列の種類 - 複数行テキスト
[公式サイト](https://support.microsoft.com/ja-jp/office/%E3%83%AA%E3%83%83%E3%83%81-%E3%83%86%E3%82%AD%E3%82%B9%E3%83%88-%E3%83%AA%E3%82%B9%E3%83%88%E5%88%97%E3%82%92%E7%B7%A8%E9%9B%86%E3%81%99%E3%82%8B-6ba62e7e-ee63-4716-9f95-f626770c3fff)を見てみましょう。


https://support.microsoft.com/ja-jp/office/%E3%83%AA%E3%83%83%E3%83%81-%E3%83%86%E3%82%AD%E3%82%B9%E3%83%88-%E3%83%AA%E3%82%B9%E3%83%88%E5%88%97%E3%82%92%E7%B7%A8%E9%9B%86%E3%81%99%E3%82%8B-6ba62e7e-ee63-4716-9f95-f626770c3fff

> リストの列内ではテキストの既定の設定は、プレーン テキストです。 ただし、[拡張リッチ テキスト] 列の設定を選択すれば、テキストのフォントの色、サイズ、またはスタイルまたはマークアップをカスタマイズできます。 [拡張リッチ テキスト] 列を作成するには、[編集] アクセス許可レベルまたはそれ以上のレベルが必要です。
>> [リッチ テキスト リスト列を編集する](https://support.microsoft.com/ja-jp/office/%E3%83%AA%E3%83%83%E3%83%81-%E3%83%86%E3%82%AD%E3%82%B9%E3%83%88-%E3%83%AA%E3%82%B9%E3%83%88%E5%88%97%E3%82%92%E7%B7%A8%E9%9B%86%E3%81%99%E3%82%8B-6ba62e7e-ee63-4716-9f95-f626770c3fff)

この列の種類であれば画像もサポートされているのだな、という想像がわきます。
実際のリストの設定では `拡張リッチ テキストを使用 (画像、表、ハイパーリンクを含むリッチ テキスト)`をオンにすることで、画像を挿入することが可能です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2734332/98a26b41-01c7-43dd-bb67-66050a4f906a.png)

編集のメニューが複数存在します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2734332/4d4f6273-f94b-4d5e-8e4f-44334352eb6d.png)

画像はソースを指定することで挿入できるようですね。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2734332/32d5389d-1b04-4a1e-943d-8c1b6dbfd348.png)

👇わたしのプロフィール画像のリンクを設定した例です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2734332/370045e3-7ca4-4435-a599-bf5e2543566c.png)

それでは`Power Apps キャンバスアプリ`で`リッチ テキスト エディター`に貼り付けた画像を、この`SharePoint Lists`の列に反映するにはどのようにすればいいでしょうか。

# リッチ テキスト エディターの値
実際にキャンバス アプリを作成し、`リッチ テキスト エディター`に画像と文字を入力してみます。
入力した値は`{コントロール名}.HtmlText`で参照することができます。

HTMLを見ていくと`<img src="data:image/png;base64,..." />`というようになっています。
`base64文字列`で格納されているということですね。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2734332/55ea25d7-e23e-4048-b2dd-1b28c9d823e8.png)

これをそのまま`SharePoint Lists`に[Patch 関数](https://learn.microsoft.com/ja-jp/power-platform/power-fx/reference/function-patch)や[Collect 関数](https://learn.microsoft.com/ja-jp/power-platform/power-fx/reference/function-clear-collect-clearcollect)で保存しようとしても保存されません。

https://learn.microsoft.com/ja-jp/power-platform/power-fx/reference/function-patch

https://learn.microsoft.com/ja-jp/power-platform/power-fx/reference/function-clear-collect-clearcollect

そもそも画像の存在が無視されてしまいます。

`SharePoint Lists`の設定を変えて、そのまま`base64文字列`として、`HTML文字列全体`を複数行テキストで保存することも出来はします。
しかしながら、このサンプルと文字の組み合わせだけでも`18,741文字`という大きなデータになってしまい、**SharePoint サイト全体に負荷をかけてしまいます。**

まったく望ましくない状態です。

しかし **「え～、スクショの挿入できないんですか😒」** といったユーザーの声は聞いたことありませんか？私はたびたびあります。

**何とかして実現してみたいと考えたときに天啓がきたので、その内容をご紹介します。**

# 画像付き文字列をSharePoint Listsに保存する
まず`base64文字列`の情報は`拡張リッチ テキスト`の保存形式に対してアンマッチになることから、**画像の表示をbase64文字列からURL参照形式に変える方法を考えてみましょう。**

1. **Power Appsのリッチ テキスト エディターに入力された画像** を`SharePoint`・`OneDrive`に保存
1. 保存した画像のパスを取得する
1. リッチ テキスト エディターに入力された文字列を置き換え

上記の流れになります。この中でも`(1)`が曲者です。

1. **Power Appsのリッチ テキスト エディターに入力された画像** を`SharePoint`・`OneDrive`に保存
    1. 文字列の中から、`<img>base64文字列</img>`に該当する部分を抽出
    1. 1-1で抽出した部分から`dataUri`に該当する部分を取得
    1. `Power Automate`でファイルとして保存
    1. 保存した`ファイルのパスを取得`

文字列を解析して中身を抽出する必要があるということですね。
`正規表現`と[Power Fx](https://learn.microsoft.com/ja-jp/power-platform/power-fx/overview)を駆使する必要があります。

https://learn.microsoft.com/ja-jp/power-platform/power-fx/overview

まず登場する関数は[MatchAll 関数](https://learn.microsoft.com/ja-jp/power-platform/power-fx/reference/function-ismatch)です。

https://learn.microsoft.com/ja-jp/power-platform/power-fx/reference/function-ismatch#match-and-matchall-examples

::: note info
ここからは私の知識の範囲で設定した正規表現によるアプローチになります。
より素晴らしいマッチのさせ方も当然ありますので、ご了承ください。
:::

## 1. HTML文字列から画像を抽出する
まずはリッチ テキスト エディター`rteImageWriter`に入力されているHTMLテキストから`imgタグ全体`を抽出します。

```javascript: imgタグをすべて抽出
MatchAll(
    rteImageWriter.HtmlText,
    "<img[^>]*>"
)
```

こちらの正規表現`<img[^>]*>`は`<img`で始まり、`>` で終わるimgタグ全体にマッチします。
`<img>`タグ全体を抽出します。

しかしこれでは、`base64文字列`で設定された画像かどうかに構わずマッチしてしまいます。
正規表現を工夫し、内容を絞る方法が考えられますが、`Power Fx`の強力な力を借り、もっと簡単にやってみます。

```javascript: imgタグのなかでdata:image/を含むものだけ抽出
Filter(
    MatchAll(
        rteImageWriter.HtmlText,
        "<img[^>]*>"
    ),
    IsMatch(
        FullMatch,
        "data:image/",
        MatchOptions.Contains
    )
)
```
抽出した`<img>`タグのうち、`data:image/`という文字を含むものだけ抽出します。

このアプローチの利点は、`リッチ テキスト エディター`に直接貼り付けた画像の対して、`data:image/`は必ず含まれることから、シンプルな抽出が実現できることです。

そして`dataUri`の部分を抽出します。

```
With(
    {
        StartPos: Find(
            "data:image/",
            FullMatch
        ),
        EndPos: Find(
            Char(34),
            FullMatch,
            Find(
                "data:image/",
                FullMatch
            )
        )
    },
    Mid(
        FullMatch,
        StartPos,
        If(
            EndPos > 0,
            EndPos - StartPos,
            Len(FullMatch) - StartPos
        )
    )
)
```

- `StartPos`は 「`data:image/`」の開始位置
- `EndPos`は `dataUri`の終了位置`引用符"`を検索しています

Excelおなじみのアプローチで部分文字列を抽出する流れです。

この方法で`dataUri`を抽出することができたため、**リッチ テキスト エディターに直接貼り付けた画像をPower Automateでファイルとして作成する準備が整います。**


## 2. 画像をSharePointに保存する
`dataUri`から`SharePoint`に画像を保存する方法は単純です。

`Power Automate`側は`dataUri`を文字列として受け取り、`ファイルの作成`アクションで[dataUriToBinary](https://learn.microsoft.com/ja-jp/azure/logic-apps/workflow-definition-language-functions-reference#datauritobinary)関数を使います。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2734332/0eb6073e-7ddf-4ad0-a04a-c31cdcdafaed.png)

- ファイル名 `@{guid()}.png`
- ファイル コンテンツ `@{dataUriToBinary(outputs('作成'))}`

ファイル名が重複するのを避けるために[guid 関数](https://learn.microsoft.com/ja-jp/azure/logic-apps/workflow-definition-language-functions-reference#guid)で特定のライブラリにキャプチャは保存していきましょう。

https://learn.microsoft.com/ja-jp/azure/logic-apps/workflow-definition-language-functions-reference#datauritobinary

https://learn.microsoft.com/ja-jp/azure/logic-apps/workflow-definition-language-functions-reference#guid

::: note info
今回はスクリーンショットの保存をまずは想定しているため、PNG形式で拡張子は決め打ちしています。ご容赦ください
:::

ファイルが保存できたら[actions 関数](https://learn.microsoft.com/ja-jp/azure/logic-apps/workflow-definition-language-functions-reference#actions)と`ファイルの作成`の結果を組み合わせて、参照先を返します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2734332/2921024f-4e77-4935-ae5a-3f3e4e6e0853.png)

- Path `@{actions('ファイルの作成')?['inputs']?['parameters']?['dataset']}@{outputs('ファイルの作成')?['body/Path']}`

https://learn.microsoft.com/ja-jp/azure/logic-apps/workflow-definition-language-functions-reference#actions

`Power Automate`は単純な4ステップです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2734332/b22f5778-b5db-4681-97cd-e37539952c25.png)

## 3. リッチテキストの値をbase64文字列からパス参照に置き換える
さて仕上げの領域になります。
手法はわかったため、今度は`<img>`タグの中身を置き換えて保存します。

まずは`Power Apps`の関数の全体像です。

最初にbase64形式の`<img>`タグとインデックスのペア`colBase64Data`、Power Automateのもととなる`dataUri`とインデックスのペア`colDataURI`のコレクションを用意します。

```javascript : コレクションの準備部分
// 1. 初期化 - base64形式とURI別にコレクションを保存
With(
    {
        MatchData: Filter(
            MatchAll(
                rteImageWriter.HtmlText,
                "<img[^>]*>"
            ),
            IsMatch(
                FullMatch,
                "data:image/",
                MatchOptions.Contains
            )
        )
    },
    Concurrent(
        Clear(colBase64Data);
        // 置き換え前文字列のコレクション
        ForAll(
            MatchData,
            Collect(
                colBase64Data,
                {
                    Index: (CountRows(colBase64Data) + 1),
                    FullMatch: FullMatch
                }
            )
        );
        ,
        Clear(colDataURI);
        // Power Automateで渡すDataURI
        ForAll(
            AddColumns(
                MatchData,
                URI,
                With(
                    {
                        StartPos: Find(
                            "data:image/",
                            FullMatch
                        ),
                        EndPos: Find(
                            Char(34),
                            FullMatch,
                            Find(
                                "data:image/",
                                FullMatch
                            )
                        )
                    },
                    Mid(
                        FullMatch,
                        StartPos,
                        If(
                            EndPos > 0,
                            EndPos - StartPos,
                            Len(FullMatch) - StartPos
                        )
                    )
                )
            ),
            Collect(
                colDataURI,
                {
                    Index: (CountRows(colDataURI) + 1),
                    URI: URI
                }
            )
        )
    )
);
```

上記👆のうち、`colDataURI`を使ってPower Automateを実施します。

[ForAll 関数](https://learn.microsoft.com/ja-jp/power-platform/power-fx/reference/function-forall)を使って繰り返し、文字列の置き換え処理を実施するため、単一の文字列を`colReplace`に格納します。

[UpdateContext 関数](https://learn.microsoft.com/ja-jp/power-platform/power-fx/reference/function-updatecontext)や[Set 関数](https://learn.microsoft.com/ja-jp/power-platform/power-fx/reference/function-set)では実現できませんが、コレクションであれば繰り返し更新処理を実施することができます。

更新は[Update 関数](https://learn.microsoft.com/ja-jp/power-platform/power-fx/reference/function-update-updateif)で実施します。

https://learn.microsoft.com/ja-jp/power-platform/power-fx/reference/function-updatecontext

https://learn.microsoft.com/ja-jp/power-platform/power-fx/reference/function-set

https://learn.microsoft.com/ja-jp/power-platform/power-fx/reference/function-update-updateif


```javascript: Power Automateで画像を保存し、imgの参照を書き換える
// 1. 上書き保存するための一時的なコレクション
/*
 * colReplace - リッチ テキスト エディタに記載されている現在のHTML文字列
 * colResult - Power Automateの結果を保存
 */
Concurrent(
    ClearCollect(
        colReplace,
        {TempText: rteImageWriter.HtmlText}
    );
    ,
    Clear(colResult);
    
);
// 2. Power Automateをつかってスクリーンショットを`SharePoint`に保存、ファイルのフルパスを取得して置き換え文字列を取得
ForAll(
    colDataURI As A,
    Collect(
        colResult,
        {
            Index: A.Index,
            FullPath: dataURItoImageSave.Run(
                Substitute(
                    A.URI,
                    """",
                    ""
                )
            ).path
        }
    );
    Update(
        colReplace,
        First(colReplace),
        {
            TempText: Substitute(
                First(colReplace).TempText,
                LookUp(
                    colBase64Data,
                    Index = A.Index
                ).FullMatch,
                $"<img src='{LookUp(
                    colResult,
                    Index = A.Index
                ).FullPath}' />"
            )
        }
    );
    
);

// 3. SharePoint Listsのデータソースに保存
Patch(
    TestRich,
    First(TestRich),
    {HTMLTest: First(colReplace).TempText}
);

// 4. Clean
Concurrent(
    Reset(rteImageWriter),
    Clear(colBase64Data),
    Clear(colDataURI),
    Clear(colReplace),
    Clear(colResult),
    Notify(
        "Update Complete",
        NotificationType.Information,
        3000
    )
);
```

::: note info
[Concurrent 関数](https://learn.microsoft.com/ja-jp/power-platform/power-fx/reference/function-concurrent)は処理の並列実行を実現します。
好きな海外のMVPが多用しているため真似しています。
:::

改めてみるとコレクションも統合できそうなので、状況に合わせてアレンジしてみてください。

# 実際の挙動

実際に画面で挙動を見てみましょう。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2734332/8761a4d9-32f8-45db-b348-3b196984ada7.png)

スクショで貼り付けた画像が、`SharePoint`のドキュメント ライブラリに保存され、`SharePoint Lists`の`拡張リッチテキスト`の列に保存されました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2734332/988fc931-602d-45cb-8a43-91ca7e2c9132.png)


■ ドキュメント ライブラリ

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2734332/870262ab-9b63-4cd1-8ee5-75653e40700c.png)

意外といけるものだなと関心しています。
しかしながら`Dataverse`使える環境であればこのような手間もないため、ツールに頼れる場合はツールに頼りましょう。