コンソールグラフィック

作成:

前回ANSIエスケープコードの説明をしたが、 その続きで、同様にANSIエスケープコードを使って、コンソールをフルカラーで彩ることができる。 それを使って遊んでみようと思う。

実用性はおいておいて、「こういう仕組みがあるということは、こういうこともできるんじゃないか?」 という好奇心にかられて、勢いでプログラミングしてしまおうという試みである。 プログラミング技術の向上には、こういうお遊び要素が不可欠だと思うのだが、いかがだろうか?

タイトル的にフレームバッファの話かと期待されていた方がいたとしたら申し訳ないが、 今回はフレームバッファには一切触れない。 フレームバッファのような方法と違い、 この方法はグラフィカルな表示をする方法としては非常に効率が悪いし、 端末エミュレータが重くなるとかの弊害が出るかもしれない。 そこら辺も割り切った上で楽しんでいただければと思う。

コンソールでの色表現

フルカラーというとどれだけの色表現を指しているのかがまちまちだが、 (TeraTermはフルカラーに対応しているが、この場合のフルカラーは256色表現だったりする) ここでは24bitカラー(トゥルーカラー)を指している。 前回のSGRの一覧の中で、RGB値の指定ができるものがあったと思う。 今回はそれを利用する。

色指定の8番の引数として、次に2、続いてrgbの各色(0~255)を指定することで、24bitカラーの指定ができる。 文字色なら「ESC[38;2;r;g;bm」、 背景色なら「ESC[48;2;r;g;bm」である。 ただし、256色への対応も拡張だったが、 24bitカラーともなるとグラフィカル要素がそれほど必要ない端末エミュレータでは対応しているものは少ない。 TeraTermやxtermは256色までの対応だった。 一方、gnome-terminalはきちんと24bitカラーを表示してくれるようだ。

早速サンプルコードだが、24bitカラーを全部出す訳にはいかないので以下のようにしてみた。

for (i = 0; i < 16; i++) {
  for (j = 0; j < 32; j++) {
    printf("\033[38;2;%d;%d;255mX", i<<4, j<<3);
  }
  printf("\033[0m\n");
}

実行結果は以下のようになる。

gnome-terminal @Ubuntu

今度は背景色指定をしてみる、文字として空白を出力している。

for (i = 0; i < 16; i++) {
  for (j = 0; j < 32; j++) {
    printf("\033[48;2;%d;%d;255m ", i<<4, j<<3);
  }
  printf("\033[0m\n");
}

そして実行結果。

gnome-terminal @Ubuntu

一文字を1ドットしたグラフィックに使えそうな出力が得られた。 これをxtermで実行してみると。

xterm @Ubuntu

出力できる色数が少ないため、ちょっと残念な出力結果となった。

コンソールへの画像出力

一文字を1ドットとしたフルカラー出力が可能だとわかると、 コンソールに画像を表示してみたくならないだろうか? やりたくなったのでやってみた。

とりあえず画像ファイルはpngで用意して、libpngを使って読み込みを行うようにしている。 一文字が1ドットだと、1ドットが縦長すぎるので、2文字使って正方形に近づけている。

pngファイルの場合、インデックスカラー方式だったり、RGBだったり、アルファチャンネル付きだったりで、 画像形式が複数ありうる。 それぞれに対応するためにちょっと複雑になっているが、 やっていることは非常に単純なことなので分かってもらえると思う。 実はこれ以外にグレースケールの場合があるのだが、ここでは割愛している。 中途半端だが、あくまでお遊びということでご容赦願いたい。 また、エラーチェックなども最低限にしている。

#include <stdio.h>
#include <png.h>

int main(int argc, char**argv) {
  FILE *fp;
  int x, y;
  png_structp png_ptr = NULL;
  png_infop info_ptr = NULL;
  if (argc < 2) {
    fprintf(stderr, "invalid argument\n");
    return -1;
  }
  if ((fp = fopen(argv[1], "rb")) == NULL) {
    perror(argv[1]);
    return -1;
  }
  png_ptr = png_create_read_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL);
  info_ptr = png_create_info_struct(png_ptr);
  if (setjmp(png_jmpbuf(png_ptr))) {
    png_destroy_read_struct(&png_ptr, &info_ptr, NULL);
    fclose(fp);
    return -1;
  }
  png_init_io(png_ptr, fp);
  png_read_png(png_ptr, info_ptr, PNG_TRANSFORM_PACKING, NULL);
  png_bytepp row_pointers = png_get_rows(png_ptr, info_ptr);
  if (info_ptr->color_type == PNG_COLOR_TYPE_RGB) {
    for (y = 0; y < info_ptr->height; y++) {
      for (x = 0; x < info_ptr->width; x++) {
        const int r = row_pointers[y][x * 3 + 0];
        const int g = row_pointers[y][x * 3 + 1];
        const int b = row_pointers[y][x * 3 + 2];
        printf("\033[48;2;%d;%d;%dm  ", r, g, b);
      }
      printf("\033[0m\n");
    }
  } else if (info_ptr->color_type == PNG_COLOR_TYPE_RGB_ALPHA) {
    for (y = 0; y < info_ptr->height; y++) {
      for (x = 0; x < info_ptr->width; x++) {
        int r = row_pointers[y][x * 4 + 0];
        int g = row_pointers[y][x * 4 + 1];
        int b = row_pointers[y][x * 4 + 2];
        const int a = row_pointers[y][x * 4 + 3];
        if (a != 0xff) {
          r = r * a / 0xff + 0xff - a;
          g = g * a / 0xff + 0xff - a;
          b = b * a / 0xff + 0xff - a;
        }
        printf("\033[48;2;%d;%d;%dm  ", r, g, b);
      }
      printf("\033[0m\n");
    }
  } else if (info_ptr->color_type == PNG_COLOR_TYPE_PALETTE) {
    for (y = 0; y < info_ptr->height; y++) {
      for (x = 0; x < info_ptr->width; x++) {
        const int i = row_pointers[y][x];
        const int r = info_ptr->palette[i].red;
        const int g = info_ptr->palette[i].green;
        const int b = info_ptr->palette[i].blue;
        printf("\033[48;2;%d;%d;%dm  ", r, g, b);
      }
      printf("\033[0m\n");
    }
  }
  png_destroy_read_struct(&png_ptr, &info_ptr, NULL);
  fclose(fp);
  return 0;
}

libpngを使っているので、libpngがインストールされていなければインストールする必要がある。 libpngの最新バージョンは執筆段階で1.6.9になっているが、上記サンプルコードは1.2系を前提としている。 1.6系とAPI仕様が異なっているため注意してほしい。

Ubuntuの場合は以下のようにすればインストールできる。

$ sudo apt-get install libpng12-dev

手動でインストールする場合は、ソースをとってきてビルドする必要があるが、libpngはzlibに依存しているため、 zlibがインストールされていない場合はzlibを先にインストールする必要がある。

サンプルコードのコンパイル時にはlibpngとリンクするように-lpngオプションを忘れずに。

$ gcc png.c -lpng -o png

実行結果だが、2文字1ドットなので小さな画像しか表示させられない。 そこで、引数として渡す画像を、AndroidSDKで作られるドロイドくんのアイコン(48x48)を指定してみている。 それでも大きいのでフォントサイズを小さくしている。

gnome-terminal @Ubuntu
Bugdroid ©Google CC BY 3.0

見事にドロイド君がコンソールに現れた。

コンソールへの文字画像出力

つづいて、コンソールへの文字画像の出力をやってみようと思う。 「何言ってんだこいつ」というツッコミもごもっともだが、 実はエスケープコードの流れでやろうと思ったわけではなく、 FreeTypeの使い方を勉強していた時に、 結果を手っ取り早く確認する方法として思いついて作ったのだ。 要するに、フォントのレンダリング結果をコンソールに表示させるというもので、 基本的には画像を表示させようとしたのと変わりない。 ちょっとおもしろいかと思ったので紹介する。

M-Plusのフォント「mplus-1c-medium.ttf」で、「あ」をレンダリングさせている。 その結果をコンソールに表示する。というシンプルなFreetypeライブラリの使用例だ。

#include <stdio.h>
#include <ft2build.h>
#include FT_FREETYPE_H

int main(int argc, char**argv) {
  int x, y;
  FT_Face face;
  FT_Library library;
  FT_Bitmap *bm;
  FT_Init_FreeType(&library);
  FT_New_Face(library, "mplus-1c-medium.ttf", 0, &face);
  FT_Set_Pixel_Sizes(face, 40, 40);
  FT_Load_Char(face, 0x3042, FT_LOAD_RENDER);
  bm = &face->glyph->bitmap;
  for (y = 0; y < bm->rows; y++) {
    for (x = 0; x < bm->pitch; x++) {
      const int c = bm->buffer[bm->pitch * y + x];
      printf("\033[48;2;%d;%d;%dm  ", c, c, c);
    }
    printf("\033[0m\n");
  }
  FT_Done_Face(face);
  FT_Done_FreeType(library);
  return 0;
}

コンパイルするには、freetypeを使っているため、freetypeがインストールされている必要がある。 Ubuntuの場合は以下のようにすればインストールできる。

$ sudo apt-get install libfreetype6-dev

コンパイラへの指示では、 freetypeのヘッダファイルは/usr/include/freetype2以下にあり、 内部的にこのパスがベースになっているため、 インクルードディレクトリを指定する必要がある。 加えて、libfreetypeへのリンクを指定する必要がある。

$ gcc freetype.c -I/usr/include/freetype2 -lfreetype -o freetype

出力結果は以下のようになる。

gnome-terminal @Ubuntu

「あ」のレンダリング結果である。

以上、ほとんど実用性のないお遊びプログラミングを紹介した。 ちょっとは面白そうと思ってもらえただろうか? コンソールで色が使えるとなると、一歩進んだ表現が可能になる。 他にも何か面白いアイデアがあれば、いろいろと作ってみて欲しい。