DRCSのSixel拡張について

# はじめに
　[Sixel Graphicsを活用したアプリケーションの御紹介](https://qiita.com/arakiken/items/3e4bc9a6e43af0198e46)を書いてからもう3年も経ってしまいましたが，この間，端末エミュレータで画像を表示するということ自体は，普通のこととして定着してきたのではないかと思います。
　ただ，コンソールアプリケーションにおいて，Sixel Graphicsの利用が進んでいるかというと，まだハードルは高いかもしれません。
　Sixel Graphicsは，コンソールで扱いやすいテキスト形式のフォーマットではあるものの，基本的には，JPEGやPNGと同様のピクセル単位の画像フォーマットであることには変わりなく，キャラクターベースの処理を前提とするコンソールアプリケーションにとって，必ずしも扱いやすいものではありません。
　こうした問題に対する一つの解として，[@kefir](http://twitter.com/kefir_)さんの[Vimで画像を見る(DRCSのSIXEL拡張の話)](https://qiita.com/kefir_/items/78e64c545a70a0ebea8a)に書かれているように，[RLogin](http://nanno.dip.jp/softlib/man/rlogin/)が実装したDRCSのSixel拡張という機能を活用できないかというアイデアが以前からありました。
　今回，DRCSのSixel拡張について，仕様を一部変更した上で，[mlterm](https://github.com/arakiken/mlterm)も，次期リリース(3.8.5)で対応予定としているところであり，この機能を使った画像表示の方法などを概説してみたいと思います。

# そもそもDRCSって？
　DRCSとは，Dynamically Redefinable Character Setの略であり，指定した文字のグリフを登録する機能ですが，簡単にいうと，いわゆる外字登録のようなものと思えばいいと思います。
　これには，次のような形式の[DECDLD](https://www.vt100.net/docs/vt510-rm/DECDLD.html)というシーケンスを使います。

DCS Pfn ; Pcn; Pe; Pcmw; Pss; Pt; Pcmh; Pcss { Dscs ... ST
(通常，DCSはESC P，STはESC \です。)

　各パラメータはざっくり以下のような意味になります(詳しくは，https://www.vt100.net/docs/vt510-rm/DECDLD.html を参照)。なお，Pfn，Pss及びPtは，RLogin・mltermは使用しません。

|パラメータ|説明|
|:----:|:---------------------------------------------------------------|
|Pfn   |0,1又は2のいずれかのフォント番号を指定|
|Pcn   |Dscsで指定する文字集合の中で，グリフを登録する文字を0x20+Pcnとします。<br>Pcn=0は0x20(US-ASCII文字集合の場合Space(**以下SPと表記**))を意味します。|
|Pe    |0=指定したDscsの全グリフをクリア<br>1=指定したDscsの文字のグリフのみ置換<br>2=全てクリア|
|Pcmw  |文字幅をピクセルで指定|
|Pss   |画面の行数・列数を指定|
|Pt    |0,1=Text font，2=Full cell font|
|Pcmh  |文字高をピクセルで指定|
|Pcss  |0=94文字集合，1=96文字集合|
|Dscs  |"I I F"の形式(Iは0,1又は2個)で文字集合を指定(Iはintermediate character(0x20-0x2f)，Fはfinal character(0x30-0x7e))。<br>グリフを登録する文字集合としては，I=0x20 F=0x40 (SP@)の使用が推奨されています。<br>※mltermでは，I=0x20 F=0x30～0x7eのみサポート|

　例えば，
DCS 1;1;1;0;0;2;0;0{SP@???owYn||~ywo??/?IRJaVNn^NVbJRI ST
というシーケンスは，SP@(Dscs:I=0x20 F=0x40)という文字集合の文字!(Pcn)のグリフを，???owYn||~ywo??/?IRJaVNn^NVbJRIと登録します。
　この文字列は，Sixel Graphicsと同じく，ASCII文字に割り当てられた縦6ドットのパターン![Sixel characters](https://www.vt100.net/dec/vt320/sixel-characters.png)により，![ビットマップ](https://www.vt100.net/dec/vt320/bell2.png)なビットマップに変換されます。
　この後，ESC(SP@によりSP@文字集合をG0に呼び出し，!を入力すると，このビットマップが表示されるという仕組みです。
　なお，http://euc.jp/i18n/charcode.ja.html#chap4 の辺りでISO 2022の考え方をざっと知っておくと理解しやすいかと思います。

　こうしたDRCSの仕組みに基づき，DECDLDの中にグリフとしてSixel Graphicsのシーケンスを埋め込めるようにすれば，Sixel Graphicsを文字のグリフとして扱えるのではないかというのが，DRCSのSixel拡張の基本的なアイデアです。

# DRCSにSixel Graphicsを融合
　では，具体的に，DRCSとSixel Graphicsをどのように組み合わせるのでしょうか。
　RLoginは，DECDLDのPtが3の場合に，Dscsに続く"..."をSixel Graphicsのシーケンスとして解釈するという方法を採用しています。
　埋め込まれたSixel Graphicsを解釈してできた画像は，Pcmw及びPcmhの大きさのセルに分解され，分解された画像一つ一つが，Dscsで指定した文字集合の，Pcnで指定した文字から，順次，当該文字のグリフとして登録されます。
　この結果，Sixel Graphicsの画像を文字として扱えるようになるという仕組みです。

　例えば，Dscs=SP@，Pcn=1を指定して，DECDLDにSixel Graphicsを埋め込んだ場合，Sixel Graphicsの画像は，次のように分割して，文字に割り付けられます。
![drcs-sixel](http://mlterm.sf.net/drcs-sixel.png)
※ RLogin-2.23.0以前では，画像サイズをPcmw又はPcmhで割った端数を切り捨てるため，注意が必要です。

　この方法には，次のメリットがあります。
1. 表示したい画像を一度DRCS-Sixelとして端末エミュレータに流し込んでおけば，あとは画像を文字として扱うことができるため，
　(1)既存のコンソールアプリケーションにおいて，キャラクターベースの画面管理をほとんど修正せずに画像表示に対応できる。
　(2)画像を表示するたびにSixel Graphicsのシーケンスを端末エミュレータに送る必要がなくなる。
2. PcmwとPcmhで指定したセルサイズと，実際の端末エミュレータのセルサイズが違う場合に，画像の拡縮は端末エミュレータ側で行うため，コンソールアプリケーション側で実際のセルサイズを知らなくてもよい。

　しかし，まだ問題があります。
　DRCSは，ISO 2022に基づく文字集合について，グリフを登録する仕組みになっているため，現在一般的に使われているUnicodeになじみません。
　このため，DRCSのSixel拡張をより扱いやすくするには，Unicodeとうまく共存させる方法が必要です。

# Unicode→DRCSマッピング
　[drcsterm](http://github.com/saitoha/drcsterm)は，UnicodeをDRCSにマップする機能を提案にしており，mltermやRLoginなどがサポートしています。
　これは，次のようにUnicodeのPrivate User Areaの文字(0x100000～0x10FFFF)について，DECDLDで登録した文字を呼び出すISO 2022のシーケンスとみなすものであり，DRCSMMv1(ESC[?8800h)によって有効になります。

|Unicode|ISO 2022|
|:----:|:---------------------------------------------------------------:|
|U+10XXYY (0x40 <= 0xXX <= 0x7E, 0x20 <= 0xYY <= 0x7F)|ESC(SP<\xXX><\xYY>ESC(B|

　なお，mlterm-3.8.5及びRLogin-2.23.1では，96文字集合の利用を可能とするとともに，XXを30～3fまで広げることとし，次のとおり拡張しています。

|Unicode|ISO 2022|
|:----:|:---------------------------------------------------------------:|
|U+10XXYY (0x30 <= 0xXX <= 0x7E, 0x20 <= 0xYY <= 0x7F)|ESC(SP<\xXX><\xYY>ESC(B|
|U+10XXZZ (0x30 <= 0xXX <= 0x7E, 0xA0 <= 0xZZ <= 0xFF)|ESC-SP<\xXX><\xZZ>

　これにより，DECDLDで登録した文字の呼び出しを，Unicodeの文字として表現することが可能となるため，内部コードをUnicodeにしている多くのアプリケーションにおいて，DRCSを利用しやすくなります。

　最終的に，この機能とDRCSのSixel拡張を組み合わせることで，DECDLDで登録した画像片をUnicodeの文字として扱うことができるようになるわけです。

# DRCSのSixel拡張を使って画像を表示してみる。
　では，実際に，DRCSのSixel拡張を使って画像を表示してみましょう。
　DRCSのSixel拡張を使ってSixel GraphicsをUnicodeの文字に変換する処理については，drcs_sixel_from_file()という関数を用意してみましたので，中身の詳しい説明はせず，これを使って画像を表示してみます。
https://raw.github.com/arakiken/mlterm/master/drcssixel/drcssixel.c
　(RLogin 2.23.0以前では動作しないので注意してください。)

```c:drcssixel.c
/*
 * file: Sixel Graphicsのファイルパスを指定します
 * charset: 0x30-0x7eの文字集合を指定します
 * is_96cs: 96文字集合を指定する場合は1を指定します
 * num_cols: 画像の幅のカラム数を返します
 * num_rows: 画像の高さの行数を返します
 * 戻り値: 画像を表示するためのUTF-32の文字列を返します
 * (charset及びis_96cs引数は次の画像用にインクリメントされますので，
 * 次にdrcs_sixel_from_file()を呼ぶ場合，インクリメントされた
 * charset及びis_96csをそのまま使ってください。)
 */
unsigned int *drcs_sixel_from_file(const char *file, char *charset, int *is_96cs, int *num_cols, int *num_rows);
```

　さて，まず，[libsixel](http://github.com/saitoha/libsixel)をインストールし，img2sixelコマンドにより，表示したい画像をSixel Graphicsに変換しておきます。
　変換処理をCで書きたい場合は，libsixelの提供するsixel.hをインクルードし，sixel_output_new()，sixel_dither_{new|get}()，sixel_dither_set_pixelformat()及びsixel_encode()辺りを使えば，簡単に書けると思います。
　(参考: https://github.com/saitoha/libsixel/blob/master/examples/drawing/main.c)

　このSixel Graphicsファイルのパスをdrcs_sixel_from_file()関数に渡します。
　drcs_sixel_from_file()は，Sixel Graphicsを読み込んでDRCSに埋め込み，端末エミュレータに流し込むとともに，画像をセル単位に分割し，Unicodeの文字として返します。
　あとは，これをUTF8に変換し，表示したい位置で標準出力に出力すればいいだけです。

　引数で渡されたSixel Graphicsのファイルを画面に表示する単純なコマンドは，以下のように実装します。

```c:drcssixel-test.c
#include "drcssixel.c"

static void pua_to_utf8(unsigned char *dst, unsigned int *src, unsigned int len) {
  unsigned int i;

  for(i = 0; i < len; i++) {
    unsigned int code = src[i];
    *(dst++) = ((code >> 18) & 0x07) | 0xf0;
    *(dst++) = ((code >> 12) & 0x3f) | 0x80;
    *(dst++) = ((code >> 6) & 0x3f) | 0x80;
    *(dst++) = (code & 0x3f) | 0x80;
  }
}

int main(int argc, char **argv) {
  unsigned int *buf, *buf_p;
  char charset = '0';
  int is_96cs = 0;
  int count;
  int num_cols, num_rows, col, row;

  if (argc < 2) {
    printf("Usage: drcssixel-test [Sixel Graphics Files]\n");
    return 1;
  }

  for (count = 1; count < argc; count++) {
    buf = drcs_sixel_from_file(argv[count], &charset, &is_96cs, &num_cols, &num_rows);
    if (buf == NULL) {
      return 1;
    }

    pua_to_utf8(buf, buf, num_cols * num_rows); /* UTF32 -> UTF8 */
    buf_p = buf;
    for(row = 0; row < num_rows; row++) {
      for (col = 0; col < num_cols; col++) {
        write(STDOUT_FILENO, buf_p++, 4);
      }
      write(STDOUT_FILENO, "\n", 1);
    }
    free(buf);
  }

  return 0;
}
```

　また，活用例の一つとして，DRCSのSixel拡張を利用し，neovimの:terminalをSixel Graphicsに対応させ，:terminal上で画像を表示できるようにしてみました。DRCSのSixel拡張によって画像を文字として処理することにより，neovim本体には一切手を加えずに実現しています(neovimのterminalが依存するlibvtermというライブラリを修正しています。)。
　https://twitter.com/arakiken/status/948335136545779712
※このツイートに記載の bitbucket のリポジトリは、2020年6月以降削除されるため、代わりに https://github.com/arakiken/libvterm/tree/sixel を参照してください。<br>
  (sayaka on neovim :terminal on RLogin-2.23.1)![neovim :terminal on RLogin](http://mlterm.sourceforge.net/nvim-terminal-rlogin2.png)

# おわりに
　DRCSとSixel Graphicsを融合し拡張するという素晴らしいアイデアは，RLoginで初めて提案・実装されたものです。
　また，mltermをDRCSのSixel拡張に対応するに当たっては，より画像表示に使いやすいものにしたいと考え，RLoginに仕様変更をお願いしたところ，迅速に御対応いただきました。
　https://github.com/kmiya-culti/RLogin/issues/12
　この場をお借りし感謝いたします。

　今回はDRCSのSixel拡張という機能を紹介しましたが，コンソール環境において画像や動画を利用する方法には，様々なアプローチが考えられます。今後も，新しいアイデアや活用事例がさらに増えることを期待したいと思います。

[参考]
[RLogin-2.23.1及びmlterm-3.8.5におけるDRCS-Sixelの仕様](https://github.com/arakiken/mlterm/blob/master/drcssixel/DRCS-SIXEL-v2)
[RLoginのDECDLD解説](http://nanno.dip.jp/softlib/man/rlogin/ctrlcode.html#DCS)
[Soft Character Set(DRCS)](https://togetter.com/li/385813)