Gradioでhtmlファイルを読み込み、iframe(srcdoc)とgradio.HTMLで表示する方法

ツイーティ・ザ・フォックスと申します。
Qiita初投稿です。新参者なのでマサカリ歓迎です。
Zennにもエラー対処法を中心に投稿を行っています。
https://zenn.dev/tweeteafox300?tab=scraps

# 記事概要と検証環境
PythonでWebアプリを作成できるライブラリのGradioで、ディレクトリに配置したhtmlファイルを、iframe(srcdoc)と`gradio.HTML`を使って表示させる方法を、サンプルによって説明します。

Hugging Faceのフォーラムで提案されていた別の方法：[★参考にした記事(本記事とは別の方法)](#参考にした記事本記事とは別の方法)に、難しい部分や原因不明のエラー発生があったので、それらよりもシンプルな方法を考えてみました。

&nbsp;
**検証環境**
・pythonは`3.11`、Gradioは`4.37.2`です。
・ローカル環境(Win10のPowershell)に起動したローカルサーバーで確認。
・本番環境(Hugging Face等)にデプロイした時の挙動は確認していません。
```toml:pyproject.toml
[tool.poetry.dependencies]
python = "^3.11"
gradio = "^4.37.2"
```

# [本題]：Gradioでhtmlファイルを読み込み、iframe(srcdoc)とgradio.HTMLで表示

## 0：ディレクトリ構成
Pythonのプログラムとhtmlファイルのみです。
```txt:ディレクトリ構成
.
├── app.py
└── html/
    └── test.html
    
```

## 1：表示させるhtmlファイルを準備
GradioのUI上に表示させたいhtmlファイルを`./html/test.html`に作成します。

<details><summary><font color="OrangeRed"><b>今回準備した簡単なhtmlファイル<br>&nbsp;&nbsp;&nbsp;--------------折りたたみ----------------</b></font></summary>

```html:test.html
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>

  <style>
    body{
      background-color:aquamarine;
    }
  </style>

</head>
<body>

  <h1>html埋め込みテスト</h1>
  <h2>テスト1</h2>
  <h3>テスト2</h3>
  <textarea id="testtextarea">あああああ</textarea>
  <button id="testbutton">JavaScriptテストボタン</button>

  <script>
    // ボタンを押すとテキストエリアの中身を変えるだけ
    const tarea = document.getElementById("testtextarea");
    document.getElementById("testbutton")
      .addEventListener("click", function () {
        tarea.value = "ボタンが押されるとこの文字列を表示\n\nこれが表示できている\n＝JavaScriptの動作に成功";
    });
  </script>

</body>
</html>
```

</details>


## 2：iframe(srcdoc)と`gradio.HTML`を使ってGradio上で表示！

1. ディレクトリに配置したhtmlを`open`で読み込み、文字列として取得します。
&nbsp;
2. Python標準モジュールの`HTML.escape`を使用して、html文字列内にあるhtml特殊文字をエスケープします。
https://docs.python.org/ja/3/library/html.html
&nbsp;
3. f文字列を使って「iframeタグのsrcdocプロパティに、エスケープ済みのhtml文字列を入れ込んだ文字列」を作成します。**srcdocプロパティにエスケープしていないhtml文字列(特に`"`)を入れ込んでしまうと、iframeタグが機能しなくなってしまうので注意です。**
https://developer.mozilla.org/ja/docs/Web/API/HTMLIFrameElement/srcdoc
&nbsp;
4. 作成したiframeタグ文字列を`gradio.HTML`のコンポーネントに入力すると、htmlを表示することができます！
https://www.gradio.app/docs/gradio/html
 &nbsp;
```python:app.py
import gradio as gr

# gradionのHTMLと重複してエラー出た？ので別名に
import html as html_lib

# 表示させるhtmlファイルのパス
html_file_pass = "./html/test.html"


def load_html():
    # htmlファイルを文字列として読み込む
    html_string = open(html_file_pass, "r", encoding="UTF-8").read()

    # python標準のhtmlモジュールでHTML特殊文字をエスケープする
    # iframeのsrcdoc内にいれる関係で、ダブルクォート(")含めエスケープが必要
    # https://docs.python.org/ja/3/library/html.html
    html_escaped = html_lib.escape(html_string, quote=True)

    # iframeのsrcdoc内に読み込んだhtmlの文字列を入れる
    # https://developer.mozilla.org/ja/docs/Web/API/HTMLIFrameElement/srcdoc
    iframe = (
        f"""<iframe srcdoc="{html_escaped}" width="100%" height="300px"></iframe>"""
    )
    return iframe


with gr.Blocks() as html_test:
    gr.Markdown("# エメラルド色の部分がiframeで埋め込んだhtml")
    view_btn = gr.Button("./html/test.htmlを読み込み表示", variant="primary")

    # iframeのタグをgr.HTMLに入れて表示させる
    view_html = gr.HTML(label="HTML preview", show_label=True)
    view_btn.click(fn=load_html, outputs=view_html)


html_test.launch()

```

## 3：実際に表示される画面
上記のプログラムを`gradio app.py`で実行、ローカルサーバーを起動してアクセスすると、このようになります。CSSでの色変更(エメラルド色)と、JavaScriptの動作(ボタンのイベントリスナー)が効いていることを確認できます。

![iframe.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3807953/d6f06a2f-2516-18a8-866b-087de17180f8.png)

# ★参考にした記事(本記事とは別の方法)

## Gradio上でhtmlファイルを生成し、それのファイルパスをiframeのsrcで`file={ファイルパス}`と指定して表示

Hugging Faceのフォーラム「**How to serve an HTML file?**」の8番目の投稿で共有されている方法
https://discuss.huggingface.co/t/how-to-serve-an-html-file/33921/8

- この方法を本記事のサンプルと同じようなやつで試した所、iframeの部分で`{“detail”:“File not allowed: /html/test.html.”}`というエラーが出て失敗してしまいました。hugging faceのディスカッション「**How to access tmp files of a space?**」で同様のエラーが報告されているのですが、2024年7月27日時点では、原因不明かつ解決策が出ていないようでした。
https://discuss.huggingface.co/t/how-to-access-tmp-files-of-a-space/59257

## FastAPIアプリにGradioアプリをマウント、Gradio上でhtmlを生成し、FastAPIで静的ページとして公開。Gradioでそれのパスをiframeのsrcに指定して表示

Hugging Faceのフォーラム「**How to serve an HTML file?**」の2番目の投稿で共有されている方法
https://discuss.huggingface.co/t/how-to-serve-an-html-file/33921/2

- Gradioに加えて、FastAPIも同時に使用する方法なので、ハードルがちょっと高いかもです。
https://fastapi.tiangolo.com/ja/

# ★参考にした記事(応用例)

## FastAPI＋Gradioで、クエリの結果をインタラクティブな表として閲覧できる、DuckDBのSQLエディタを作成した事例
一つ上にあるような「**Gradio上でhtmlを生成して、FastAPIで静的ページとして公開**」というアプローチを使うと、かなり複雑なことができるみたいです。

**Live DuckDB editor on Gradio**
https://medium.com/@liquidc/live-duckdb-editor-on-gradio-533addd0666c
**duckdb-fastapi-gradio**
https://huggingface.co/spaces/liquidcarbon/duckdb-fastapi-gradio