カスタムタグを使った Web 版 Pyxel の動作を理解する（２）

Web 版 Pyxel の仕組みについて理解する下記の記事の続きとなります。

https://qiita.com/munepi0713/items/8acf9ce353948fc1baf1

本記事では、 `pyxel-edit` について調べてみます。

## なぜこのような調査をしようと思ったのか

サムライアプスという団体名で、Pyxel を使った写経スタイルのプログラミング教育ウェブサービス「Code & Magic」（コードアンドマジック）を開発しています。

https://codeandmagic.samurai-apps.com/

本記事は、このサービスの開発に際して調査したことのまとめとなります。

## Pyxel Editor とは

まず、Pyxel Editor についておさらいします。

Pyxel には、スプライト画像や音楽、効果音のデータをまとめた**Pyxel リソースファイル**というデータ保存形式が用意されています。
このリソースファイルを編集するために用意されているアプリが「Pyxel Editor」です。

通常のターミナルで実行する場合はこんな感じになります。

```sh
pyxel edit resource.pyxres
```

![PyxelEditor.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/957245/15ab1a83-de95-0de0-1684-56601459b440.png)

面白いのは、この Pyxel Editor も Pyxel で作られたアプリケーションであるという点です。  
ですので、エディタだからといって特別なことはなく、ゲームなどを作る場合と同じメカニズムで動作していると考えればよいでしょう。

そういえば、昔ベーマガにスプライト編集アプリを投稿したことがあったことを思い出しました。（遠い目）

## Web 版の `pyxel-edit` 

さて、この Pyxel Editor も Web ブラウザ上で実行できます。

```html
<html>
<body>
    <script src="https://cdn.jsdelivr.net/gh/kitao/pyxel/wasm/pyxel.js"></script>
    <pyxel-edit name="resource.pyxres"></pyxel-edit>
</body>
</html>
```

立ち上げるには、前回記事同様サーバーからホストされる必要があります。

ローカルのターミナルなら、下記のようにして、

```sh
python3 -m http.server
```

そして、ブラウザで、下記の URL で開けば OK です。

```
http://localhost:8000/
```

このあたりの手順は `pyxel-run` や `pyxel-play` と同じです。 Pyxel Editor も Pyxel アプリケーションの１つであることを考えれば、同じメカニズムで動くということは自然に理解できます。

## Web 版のデータ保存の仕組み

さて、 Web 版 Pyxel Editor で気になるのは、データ保存についてです。  
Pyxel Editor はリソースの編集をするアプリですので、データ保存ができなければなりません。

:::note
一般的に、Web ブラウザで動作するアプリはユーザーのPCのファイルシステムにアクセスすることはできません。これは、悪意あるサイトがユーザーのファイルに自由にアクセスできてしまうことを防ぐためです。
:::

まずは挙動を調べてみましょう。

### デフォルト挙動はダウンロード

Pyxel Editor のセーブアイコンをクリック、または `Ctrl + S` を入力すると、編集結果が**ダウンロード**されます。そう、 Web サイトを見ているときのダウンロードと同じです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/957245/64f53f59-2bdd-2202-109b-9d8aaa53a69d.png)

どのように実現されているのか、深堀してみます。

Pyxel Editor アプリの保存ボタンをハンドリングしているのは [`App.__on_save_button_press()`](https://github.com/kitao/pyxel/blob/main/python/pyxel/editor/app.py#L162) です。

```python
    def __on_save_button_press(self):
        pyxel.save(self._resource_file)
```

`pyxel.save()` というメソッドを呼んでいるだけですね。  
このメソッドは公式にはドキュメントされていない、[上級者向け API](https://github.com/kitao/pyxel/blob/main/docs/README.ja.md#%E4%B8%8A%E7%B4%9A%E8%80%85%E5%90%91%E3%81%91-api)のひとつとなっています。ですので、ここからはソースコードを読み解いていきましょう。

### pyxel.save()メソッドの実体を探す

`pyxel.save()` の実体を探してみましょう。

Rust 言語で書かれた、[`pyxel/rust/pyxel-engine/src/resource.rs`](https://github.com/kitao/pyxel/blob/main/rust/pyxel-engine/src/resource.rs#L108) にありました。

```rust
    pub fn save(
        &mut self,
        filename: &str,
        exclude_images: Option<bool>,
        exclude_tilemaps: Option<bool>,
        exclude_sounds: Option<bool>,
        exclude_musics: Option<bool>,
        include_colors: Option<bool>,
        include_channels: Option<bool>,
        include_tones: Option<bool>,
    ) {
       ...
    }
```

:::note
実は Python のライブラリは、C/C++ や Rust で主要な処理を実装して、Python から呼び出すための**バインディング**と組み合わせて作ることができます。目的は、主に高速処理で、Python の標準ライブラリや多くの著名なライブラリもこういった方式で作られています。

Pyxel には、さらにゲームエンジンとしての高速動作だけでなく、グラフィックデバイス、サウンドデバイスといった、OSの機能にアクセスする目的もありそうです。
:::

このメソッドの引数リストから次のようなことが推測されます。
* 最初の引数 `filename` に、保存先ファイル名を指定する。省略不可。
* それ以降は、リソースファイル内の画像、タイルマップ、SE、BGMなどそれぞれを含むかどうかのスイッチで、省略できる。

さらに動作を深堀してみましょう。

### emscripten を通じて JavaScript を実行

引数リストの後ろはこんな感じです。

```rust
    pub fn save(
        ...
    ) {
        let toml_text = ResourceData2::from_runtime(self).to_toml(
            exclude_images.unwrap_or(false),
            exclude_tilemaps.unwrap_or(false),
            exclude_sounds.unwrap_or(false),
            exclude_musics.unwrap_or(false),
            include_colors.unwrap_or(false),
            include_channels.unwrap_or(false),
            include_tones.unwrap_or(false),
        );
        let path = std::path::Path::new(&filename);
        let file = std::fs::File::create(path)
            .unwrap_or_else(|_| panic!("Failed to open file '{filename}'"));
        let mut zip = ZipWriter::new(file);
        zip.start_file(RESOURCE_ARCHIVE_NAME, SimpleFileOptions::default())
            .unwrap();
        zip.write_all(toml_text.as_bytes()).unwrap();
        zip.finish().unwrap();
        #[cfg(target_os = "emscripten")]
        pyxel_platform::emscripten::save_file(filename);
    }
```

ほとんどは、リソースファイルのフォーマットである TOML の操作と、それを Zip する処理ですね。

注目は最後の行です。

```rust
        #[cfg(target_os = "emscripten")]
        pyxel_platform::emscripten::save_file(filename);
```

またどこかの `save_file()` というメソッドを呼んでいます。

[`/rust/pyxel-platform/src/emscripten.rs#L63`](https://github.com/kitao/pyxel/blob/main/rust/pyxel-platform/src/emscripten.rs#L63) にありました。  
`_savePyxelFile('{filename}');` という**文字列**を渡して、スクリプトを実行するのでしょうか。

```rust
pub fn save_file(filename: &str) {
    run_script(&format!("_savePyxelFile('{filename}');"));
}
```

下請けの `run_script()`も同じ[ファイル](https://github.com/kitao/pyxel/blob/main/rust/pyxel-platform/src/emscripten.rs#L41)にあります。  
さらに `emscripten_run_script()` というメソッドを呼び出しています。

```rust
pub fn run_script(script: &str) {
    let script = CString::new(script).unwrap();
    unsafe {
        emscripten_run_script(script.as_ptr());
    }
}
```

`emscripten_run_script` は、`emscripten` というツールが用意しているメソッドで、説明は下記にあります。

https://emscripten.org/docs/porting/connecting_cpp_and_javascript/Interacting-with-code.html#calling-javascript-from-c-c

:::note
[emscripten](https://emscripten.org/index.html) とは、 C/C++ のソースコードから Web Assembly (WASM) のバイナリを生成するツールです。 Pyodide は CPython がベースなので、 emscripten が使われているということでしょう。一方、 Pyxel は Rust を使っています。元の言語が違うのに WASM を通じて相互運用できるというのは、よくできていて、面白いですね。
:::

少々引用します。
>Emscripten provides two main approaches for calling JavaScript from C/C++: running the script using emscripten_run_script() or writing “inline JavaScript”.

（ChatGPT による翻訳）
> Emscripten は、C/C++ から JavaScript を呼び出すための主なアプローチとして、emscripten_run_script() を使用してスクリプトを実行する方法と、「インライン JavaScript」を記述する方法の2つを提供しています。

つまり、C/C++からJavaScriptを呼び出したいようです。

もう少し調べてみます。

`pyxel.js` に、 `_savePyxelFile()` というメソッドがありました。
このファイルは、HTMLで読み込んでいたアレですね！

```html
<html>
<body>
    <script src="https://cdn.jsdelivr.net/gh/kitao/pyxel/wasm/pyxel.js"></script>
    <pyxel-edit name="resource.pyxres"></pyxel-edit>
</body>
</html>
```

`_savePyxelFile()` の中身はこのようになっています。
[/wasm/pyxel.js#L227](https://github.com/kitao/pyxel/blob/main/wasm/pyxel.js#L227)

```js
  _savePyxelFile = (filename) => {
    let a = document.createElement("a");
    a.download = filename.split(/[\\/]/).pop();
    a.href = URL.createObjectURL(
      new Blob([fs.readFile(filename)], {
        type: "application/octet-stream",
      })
    );
    a.style.display = "none";
    document.body.appendChild(a);
    a.click();
    setTimeout(() => {
      document.body.removeChild(a);
      URL.revokeObjectURL(a.href);
    }, 2000);
  };
}
```

アンカータグを動的に作成するという、 Web サイトで自動的にファイルをダウンロードさせるときの典型的な実装方法が書かれています。

これで一連の挙動がつながりました。

## まとめ

まとめると、一連の挙動は次のようになっているようです。

1. Pyxel Editor で保存ボタン押下で、Pyxel ライブラリ内の `pyxel.save()` を実行
1. Pyxel ライブラリ内の `pyxel.save()` が、Emscripten の `emscripten_run_script()` に対して、JavaScriptの `_savePyxelFile()` 呼び出しを依頼
1. `pyxel.js` 内の `_savePyxelFile()` が実行されて、リソースファイルをダウンロード

ファイルの保存というのは、OS や実行環境によって具体的な処理が異なります。ですので、Pyxel ライブラリ内に直接実装するのではなく、プラットフォーム側である JavaScript に処理してもらう仕組みになっているのだと思います。

ここまでわかれば改造もできそうです。

たとえば、私の場合は前述のとおり Web サービスに利用したいので、 `pyxel.js` の `_savePyxelFile()` を改造すれば Web サービス側に送信することができそうです。

---

また改造にチャレンジしてみようと思っています。  
今回はここまで。

これにて御免！