文字の描画
次に文字を描いてみましょう。こちらもいろいろな方法がありますが、今回はビットマップフォントを利用することにします。ビットマップフォントとは、文字を点(ドット)の集合体として表現したフォントです。
ビットマップフォントには、その名のとおり、ビットマップ画像となっているものや、BDF(Bitmap Distribution Format)形式となっているものがあります。BDF形式は、文字のビット情報がテキストになっています。テキストなので扱いやすいのですが、それを実際のビットデータに変換するにはやや手間がかかります。
ビットマップフォント
今回は、フリーソフトウェアの8×12ドット日本語フォントを利用します。このフォントは、半角、全角ごとに、文字すべてが1つのPNG形式のビットマップ画像でも提供されています。ビットマップ画像は、JISコードの順番に文字を並べた画像となっています。そのため、JISコードがわかれば、文字のビット情報が参照できます。
このフォントは、半角、全角と2つの画像に分かれていて扱いが面倒なため、今回は、画像を1つに結合することにします。画像の結合をRaspberry Pi上で行うには、ImageMagickというツールを使います。ImageMagicをインストールすると、convertコマンドが使えるようになります。
$ sudo apt-get install imagemagick $ convert -append k8x12_jisx0201.png k8x12_jisx0208.png font.png
半角(k8x12_jisx0201.png)と全角(k8x12_jisx0208.png)の画像を縦に結合して、font.pngという画像ファイルを出力します。
ビットマップフォント操作クラス
では、この8×12ドット日本語フォントを操作するクラス(Bitmapfontクラス)を作りましょう。
ビットマップ画像を操作するには、.NET FrameworkのSystem.Drawing.Bitmapクラスを利用します。ただ、.NET Core 3.0には、標準では、BitmapクラスのあるSystem.Drawing.Commonパッケージが含まれていません。あらかじめ、インストールしておく必要があります。
インストール方法にはいくつかありますが、今回は、Visula Studioのパッケージマネージャーコンソールから、Install-Packageコマンドを用います。 Visual Studioのツールメニューから、[NuGet パッケージ マネージャー]ー[パッケージ マネージャー コンソール]を開き、次のように入力します。
PM> Install-Package System.Drawing.Common -Version 4.6.0
Bitmapfontクラスのプロパティとコンストラクタは、次のように定義しました。コンストラクタで、あらかじめ結合したビットマップフォントの画像ファイルのパスを指定します。
// ビットマップフォント操作クラス public class Bitmapfont { public readonly int[] Width = { 4, 8 }; // フォントの幅(1) public int Height { get; } = 12; // フォントの高さ private Bitmap Bmap { get; } // フォントのビットマップオブジェクト public Bitmapfont(string path) => Bmap = new Bitmap(path); }
フォントの幅を配列にしているのは、半角、全角の幅の2つを参照するためです(1)
文字のビットデータを取得する
次に、フォント画像から、指定のJISコードに該当する文字のビットデータを、BitArrayとして読み出すメソッドです。このメソッドで全角文字を指定するときは、JISコード2バイトを、下位と上位1バイトに分けて引数に指定します。半角文字のときは、下位4ビット、上位4ビットを、それぞれ1バイトに変換して指定します。
// フォント画像から、指定のJISコードの文字のビットデータを取得する public BitArray GetFontBitArray(int cx, int cy) { var zh = (cy < 0x21) ? 0 : 1; // 半角:0,全角:1 (1) // フォント画像の文字の開始位置のオフセット var ofs_x = (zh == 0) ? cx : (cx - 0x21); var ofs_y = (zh == 0) ? cy : (cy - 0x21 + 16); // (2) var fdata = new BitArray(Width[zh] * Height); // 1文字のビット情報 for (int y = 0; y < Height; y++) { for (int x = 0; x < Width[zh]; x++) { Color p = Bmap.GetPixel(ofs_x * Width[zh] + x, ofs_y * Height + y); if (p.R == 0) // 赤成分を調べる (3) { fdata.Set(x + y * Width[zh], true); } } } return fdata; }
JISコードの全角文字は、0x2121という文字コードから始まっています。それを利用して、最初に、第1引数の値が0x21より大きいか小さいかで、全角半角を判断しています(1)。
また、ビットマップの位置を指定する際には、全角の場合、引数の文字コードから、この0x2121をマイナスして、相対的な位置に変換しています。さらにビットマップ画像の最初の部分は、半角文字になっていますので、縦に16文字分下にずらしています(2)。半角文字の場合は、ビットマップ画像のいちばん左上の位置から読み出します。
フォントのビットマップ画像は、白色背景に黒の文字のモノクロ画像となっています。つまり、該当の画像のピクセルが黒なら、ビットを1、それ以外は0とすれば、文字のビット情報が得られることになります。
BitmapクラスのGetPixelメソッドは、指定のピクセルの色のColor構造体を返します。ここでは指定のピクセルが黒かどうかは、Color構造体の赤成分が0かどうかで判断しています(3)。フォントのビットマップ画像は白と黒なので、Color構造体のRGB(赤、緑、青)は同じ値となるためです。
たとえば、「愛」のJISコードは、0x3026なので、愛の文字データは、X座標、(0x26 - 0x21 )* 8 = 40、Y座標、(0x30 - 0x21 + 16 )* 12 = 372の位置から読み出します。
文字コードの変換と画像バッファへの書き込み
文字のビットデータが参照できるようになりましたので、次は、OledSsd1306クラスに、指定の文字列を画像バッファに書き込むSetTextメソッドを追加します。
SetTextメソッドでは、文字コードの変換に、System.Text.Encodingクラスを利用しています。なお.NET Core 3.0では、JISコードなどを扱うには、不足のファイルがあるので、System.Text.Encoding.CodePagesパッケージをインストールしておきます。インストールは、パッケージ マネージャー コンソールで、次のように入力します。
PM> Install-Package System.Text.Encoding.CodePages -Version 4.6.0
なお、JISコードに変換するメソッドを呼び出す前に、JISコード(iso-2022-jp)を扱えるように、Encoding.RegisterProviderメソッドを一度実行しておく必要があります。
// エンコーディング情報をランタイムに登録する Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
SetTextメソッドでは、次のような処理になっています。メソッドのコメントの番号は、この処理の番号を示しています。
- 文字列から1文字を取り出す
- 1文字をJISコードに変換する
- 文字コードを2バイトに揃える
- フォントのビットデータを取得する
- 画面バッファにコピーする
// 画面バッファの指定位置(x0,y0)に文字列を書き込む public void SetText(Bitmapfont bmap, int x0, int y0, string str) { // 文字列から1文字を取り出す(1) foreach (char c in str) { // JISコードに変換(2) byte[] b = Encoding.GetEncoding("iso-2022-jp").GetBytes(c.ToString()); // 全角、半角とも、2バイトのコードに変換する(3) var zh = (b.Length < 5) ? 0 : 1; var code_x = (zh == 0) ? b[0] & 0x0f : b[4]; var code_y = (zh == 0) ? (b[0] & 0xf0) >> 4 : b[3]; // 指定文字のJISコードからフォントビットデータを取得する(4) var ba = bmap.GetFontBitArray(code_x, code_y); for (int y = 0; y < bmap.Height; y++) { for (int x = 0; x < bmap.Width[zh]; x++) { // 画面バッファにコピー(5) SetPixel(x0 + x, y0 + y, ba.Get(x + y * bmap.Width[zh])); } } x0 += bmap.Width[zh]; if (Width <= x0) // 画面の横幅を超えた場合は改行 { y0 += bmap.Height; x0 = 0; } if (Height <= y0 + bmap.Height) // 画面の高さを超えたら終了 { break; } } }
(1)では、指定された文字列から1文字を取り出して、1文字ずつ処理します。(2)では、1文字の文字コードをJISコードに変換し、それをバイト配列として参照しています。
JISコードでは、「エスケープ・シーケンス」と呼ばれる3バイトの制御コードが、文字コードが変化する位置に追加されます。そのため、全角1文字でもJISコードに変換すると、制御コード(3バイト)+文字コード2バイト+制御コード(3バイト)の計8バイトになります(英数などの半角文字なら1バイトです)。そのため、(2)で求めたバイト配列は、全角文字の場合は、最初の3文字は制御コード、次の2バイト(バイト配列の3つめ、4つめ)が文字コードになります。半角の場合は、1バイトのコードになるので、上位下位の4ビットをそれぞれ1バイトに変換しています(3)。
(4)では、先ほど作成したGetFontBitArrayメソッドを呼び出して、JISコードに応じたビットデータを取得しています。(5)では、指定の文字コードのビットデータを、画面バッファにコピーしています。
SetTextメソッドの引数x0、y0は、描画したい文字列の位置を指定します。また、このメソッドでは、文字を書き込む位置のX座標が画面の横幅を超えた場合は改行処理、 Y座標が画面の高さを超えたら終了としています。
文字表示のサンプル
次のソースコードは、NHKニュースのRSSを参照して、主要ニュースを15秒おきに順番に表示します。なお、このソースの実行には、System.ServiceModel.Syndicationパッケージのインストールが必要です。
int fd1 = WiringPi.I2CSetup(0x3C); // ディスプレイ用I2C通信の初期化 var oled = new OledSsd1306(fd1); // ディスプレイの初期化 oled.SendBuffer(); // 画面クリア // ビットマップフォントの指定 var bf = new Bitmapfont("/home/pi/k8x12_png_2017-02-20/font.png"); while (true) { var url = "https://www.nhk.or.jp/rss/news/cat0.xml"; // NHKニュースのRSS using (XmlReader rdr = XmlReader.Create(url)) { SyndicationFeed feed = SyndicationFeed.Load(rdr); foreach (SyndicationItem item in feed.Items) { oled.SetAll(); // 画面データクリア oled.SetText(bf, 0, 0, item.Summary.Text); // ニューステキストの描画 oled.SendBuffer(); // 画面データ送信 Thread.Sleep(15000); } } }
最後に
今回は、ディスプレイモジュールに直線や文字を表示するコードを解説しました。次回は、温湿度センサーを使って、OLEDディスプレイに測定値を表示してみます。