作成日時:2016年02月18日 22時51分33秒
更新日時:2016年08月29日 12時36分40秒
この記事は3年ほど前に投稿された記事です。
この記事の内容は古くなっている可能性があります。更新日時にご注意ください。
※こちらの記事は以下の記事に統合しましたのでこちらを見てください
この記事の目次
以下元記事の内容。
前回はRaspberry Piにて作っていたフォントリーダーを活用するため、SDカードから文字一覧画像ファイルを読み込んでArduinoで漢字パターンを読み込みましたが。今回はビットマップフォントファイルから読みだしてみたいと思います。
はじめに
今までこのサイトではグラフィック液晶などに漢字を表示する場合に、美咲フォントの画像版を使っていました。
理由としては最初にそれを見つけたからというのが主、画像なので回転したりが容易で扱いやすいという所が主な理由でした。
しかし、実際のところ回転処理はプログラムを組んでやればいい話ですし。Arduinoの場合は処理速度も遅くメモリも少ないので正直あまり頭のいい方法とはいえませんでした。(Raspberry Piを使ったとしても余り賢い方法ではないけど。)
そこで今回はFONTXフォーマットという形式のフォントファイルを使って文字を表示してみたいと思います。
※この記事を読む前に前回の記事の「はじめに」と「シリアル通信で漢字を扱う」を読んでおいたほうがいいと思います。
必要なもの
- SDカードシールド
- FONTX形式のフォントファイル
ネット上にフリーの既存フォントが多く存在していますし。エディタや変換ツールも存在します。
例:ELMソフトウェア FONTXエディタ (Windows用)
ホビーユースでは気にする必要はないでしょうが、他人の作ったフォントを流用したり、変換する場合はフォントファイルのライセンスにご注意ください。
FONTXとはなんぞや?
1991年にIBM DOS上の日本語環境改善策として、日本の有志により作成されたフォントドライバがこのFONTXというドライバです。このソフトウェア自体はDOS/V専用で今となっては無用のものとなりましたが。そのフォントドライバで扱われていたフォントファイルは、形式が単純で軽く、様々なフォントサイズのフォントが扱えるため。
当時のコンピュータやそれ以下の処理性能しか無いマイコンなどでフォントを扱う場合にもってこいのフォント形式なので最近人気が再燃しているようです。 (参考資料:FONTXの使いかた)
ファイルフォーマット
1バイト目から6バイトがファイルシグネチャ「FONTX2」が入っています。
6バイト目から14バイト目までの8バイトにフォント名が入っています。
この2つはマイコンにてフォントを扱う場合においては気にする必要はないでしょうし、扱うメモリも惜しいですので気にせずに14バイトシークしてしまいましょう。
14バイト以降は「フォント幅・フォント高さ・文字コードフラグ」の順で記録されていて各データのデータサイズは1バイトとなっております。
17バイト目はフォントの種類によって違い。半角文字用のフォント(コードフラグ0)の場合はここからフォントイメージが始まります。全角文字用のフォント(フォントフラグ1)の場合は17バイト目にはコードブロックの総数が格納れています。
- コードブロックとは・・・
日本語の文字コードは2バイト文字で65536文字格納することが可能ですが。実際に利用されるのは7000文字程度なので、正直いって殆どの領域が無駄領域となっています。(ただしこれは一般的なフォントファイルはJIS第2水準漢字まで採用していることが多いためであり。Shift_JISの規格上はもう少し文字数は多いです。)そのため、FONTXでは、そのフォントが文字コードのどこからどこまでを収録しているものなのかを格納しておく領域が存在します。これをコードブロックと呼んでおります。
半角フォントの場合はここからフォントデータが始まりますが。全角フォントの場合は先程説明したコードブロックのデータが続いております。2バイトコードですので開始コードと終了コード、計4バイトのデータがコードブロックの総数分記録されています。
つまりこのフォントファイルのヘッダは以下のとおりとなっています。
フォントイメージのについて
フォントデータはBMP等と同様に横8ドット分が1バイトに収まっていて、例えば12ドットや10ドットのような8の倍数でない場合には8ビット(1バイト)単位で穴埋めされています。
イメージまでのオフセット計算方法
それでは、以上のフォーマットで目的の文字パターンが何処に格納されているのかをどうやって知ればいいのかというと。オフセットを計算すればいいわけですが。半角フォントと全角フォントで計算方法が微妙に違います。
なお、フォントのデータサイズの計算方法は以下のとおりです。(16×16ドットの場合)
- (幅 + 7) ÷ 8 × 高さ
- (16 + 7) ÷ 8 × 16
- 23÷8×16
- 2※×16
- =32バイト
※ 端数切り捨て(int型で計算する)
半角フォントの場合の計算式
- ヘッダサイズ + 文字コード×フォントサイズ
8×16ドットのフォントファイルで、「S」の文字までのオフセットを計算する場合は、
1フォントあたり16バイト、Sの文字コードは0x53(83)ヘッダーサイズは17バイトなので。
- 17+83×16 = 1345バイト
となります。
全角フォントの場合の計算式
- ヘッダサイズ+4×Cb+該当コードまでの文字数×フォントサイズ
8×8ドットのフォントファイルで。「さ」の文字までのオフセットを計算する場合は、
1フォント8バイト、「さ」の文字コードは82B3となります。コードブロックの総数はフォントの収録内容によって違いますが。例えば何時も利用している美咲フォントのサイトにあるFONTX2形式のファイルの場合は92個のブロックに分かれています。
次に該当コードまでの文字数の計算方法ですが。
- フォントブロックの終了コード-フォントブロックの開始コード+1
で1ブロックあたりの文字数を計算して、該当文字コードがその範囲にない場合は次のブロックに進み、該当の文字コードが含まれるまで計算結果を累積していきます。
「さ」の文字は82B3なので、11ブロック目に存在し、10ブロック目までに209文字存在することになり、11ブロック目の開始位置は829Fなので手前に20文字存在することになり、足すと229文字目が「さ」が格納されている文字になります。
これを元に計算すると
- 18+4×92+229×8
- 18+368+1832 = 2218バイト
となります。
つまり上で説明したフォント形式の場合、半角の「S」なら1345バイト目から16バイト抜き出し、全角「さ」なら2218バイト目から8バイト抜き出せば目当ての文字が得られるということです。
コーディング
まずSDカードを使用するので標準ライブラリをインクルードし、defineでフォントファイルのファイル名を指定します。ファイルはSDカード直下に記録しています。
1 2 3 4 5 6 7 | // SDカード用ライブラリ #include <SPI.h> #include <SD.h> // フォントファイルのファイル名を指定 #define FONT_FILEJ "FONTXJ.FNT" // 全角文字用フォントを指定 #define FONT_FILEA "FONTXA.FNT" // 半角文字用フォントを指定 |
次に上の説明を実現し、パターンを取得する関数を作ります。
関数の詳しい説明はコメントに書いてあるのでそれを読んでください。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 | ////// FONTX形式のフォントファイルからフォントファイルを読み込み。 ////// // 対応フォントはFONTX形式 最小8ドット四方・最大255ドット四方まで対応。 // 最小フォントサイズは実用値、最大フォントサイズは論理値。 // // 受け渡し用の変数: // code : 文字コード(符号なしint型) // *pat : 戻ってきた文字パターンを格納するための配列変数(符号なしchar型) // 1フォントあたりのバイト数以上を確保すること。 // // 戻り値: 符号なしlong型。 unsigned long KanjiReadX(unsigned int code , unsigned char *pat) { // 参考資料:http://www.elm-chan.org/docs/dosv/fontx.html // オフセット計算用 // ccc = 現在の文字数(Character Count C…?) // ofset = 最終的なオフセット値(OFfSET) unsigned long ccc = 0 , ofset = 0; // フォントファイルを開く // 文字コードを判別して、文字コードが1バイトの範囲内なら半角コード // 2バイトの範囲内なら全角コード用のファイルを読み込む。 File fontx_file; if (code < 0x100) fontx_file = SD.open(FONT_FILEA, FILE_READ); if (code > 0x100) fontx_file = SD.open(FONT_FILEJ, FILE_READ); // フォントファイルのファイルタイプとファイル名は不要なので // その分ファイルをシークしておく fontx_file.seek(14); // フォントデータの読み込み。 // Fw = 文字サイズ(横)単位:ピクセル // Fh = 文字サイズ(縦)単位:ピクセル // Ft = 文字コード(00ならアスキー 01ならShift_JIS) 最後2つは2バイト文字のみの変数 // Fb = コードブロック数(Fbwはループ用) uint8_t Fw = fontx_file.read() , Fh = fontx_file.read(), Ft = fontx_file.read() , Fb, Fbw; // 1文字あたりのデータサイズ(バイト)を計算 uint16_t Fsize = ((Fw + 7) / 8 ) * Fh ; // コードフラグが0なら英文フォント用のオフセットを書き込む。 if (Ft == 0x00) { // ヘッダサイズ + 文字コード×1文字あたりのサイズ if (code < 0x100) ofset = 17 + code * Fsize ; } else { // ブロック数のデータは2バイト用フォントにしか無いのでココで取得する。 Fb = Fbw = fontx_file.read(); // フォントオフセット取得用ループ while (Fbw --) { // コードブロックからコードの範囲を読み込む // Cbs = 開始コード(Code Block Start) // Cbe = 終了コード(Code Block End) uint16_t Cbs = fontx_file.read() | fontx_file.read() << 8 , Cbe = fontx_file.read() | fontx_file.read() << 8; // 指示された文字コードがコードブロック内に存在するかどうかを確認 if (code >= Cbs && code <= Cbe) { // コードブロック内に文字が存在したのなら // そのコードブロック内で何文字目に存在するのかを取得 ccc += code - Cbs; // 全体のオフセット値を計算する // ヘッダサイズ+4バイト×コードブロック数+直前の文字コードまでの文字数×1文字あたりのサイズ ofset = 18 + 4 * Fb + ccc * Fsize; // 抜ける break; } // 取得したブロック内に該当文字コードが存在しない場合。 // 存在しなかったブロック内の文字数を計算して次へ。 ccc += Cbe - Cbs + 1 ; } } // ループ用にフォントサイズを退避 uint16_t i = Fsize; // 指示された文字コードが見つかった場合の処理 if (ofset) { fontx_file.seek(ofset); // パターン格納用の変数に漢字パターンを取得し、格納 while (i--) *(pat++) = fontx_file.read(); // 終わったらファイルを閉じる fontx_file.close(); // フォントデータをリターンして終了 return ((unsigned long)Fw << 24) | ((unsigned long)Fh << 16) | Fsize; // 戻り値は4バイトデータで、1バイトごとに上から // 1バイト目 =フォントサイズ(幅) // 2バイト目 =フォントサイズ(高さ) // 3・4バイト目=フォントデータのバイト数(2バイト) // を返す。 } // 見つからなかった場合はファイルを閉じて0をリターンして終了 fontx_file.close(); return 0 ; } |
この関数の動作は
- 受け取った文字コードの数値により使用フォントを決定
256以下なら半角用フォント、それ以上なら全角用フォントを呼び出す - フォントファイルを14バイトシーク
- ヘッダである文字サイズ縦・横、文字コードを取り出す
- 取り出した文字サイズからデータサイズを計算する
- フォントタイプによって条件を分岐する
半角フォントならオフセットを計算。
全角フォントの場合は、ブロック総数のデータを取得し該当文字が格納されているブロックが見つかるまでブロック内の文字数を控えつつループ、見つかったらオフセットを計算。 - 計算されたオフセット分ファイルをシークする
- そこからフォントのデータサイズ分フォントデータを配列に読み出す
- フォントファイルを閉じる
- 戻り値を返して完了。
戻り値はフォントが見つからない場合は0を返す。
見つかった場合はunsigned longのデータに「フォント幅(1)」「フォント高さ(1)」「データサイズ(2)」を連結させて返す。( )内はバイト数
例えば24×18ドットのフォントの場合はデータサイズは54バイトで「0x18120036」が戻り値となり、これをビット演算で切り出すことで目的の値が取得できる。
という動作になっている。これを使うためには1つのunsigned int(short)型変数と1つのunsigned char型配列、戻り値を格納するunsigned long型が必要となります。
例えば
1 2 3 4 5 6 7 8 | // 文字コード格納用 unsigned int kanji; // 漢字パターン格納用 unsigned char pat[32]; // 16x16dot font. // 戻り値用 unsigned long ret; |
という変数を確保した場合
- ret = KanjiReadX( kanji , pat );
記述すれば使用することが出来ます。
なお、Arduinoのシリアル通信は1バイトずつ取得できますので、文字コードの受信は以下のとおりに行います。
1 2 3 4 5 6 7 | kanji = (unsigned int)Serial.read() << 8; if ((kanji >= 0x8000 && kanji <= 0xA000) || kanji >= 0xE000) { while (!Serial.available()); kanji |= Serial.read(); } else { kanji >>= 8; } |
最初に8bitシフトして1バイト目を取得します。
そのコードが2バイト文字を扱う範囲にあるかどうかを確認し、2バイト文字の上位バイトだった場合は、2バイト目を続けて取得します。もし1バイト文字と判断された場合は、最初にシフトした8bitを戻して終了します。
戻り値ですが、フォント幅を取得する場合は24bitシフトして0xFFでマスクをして抜き出します。
フォントの高さは16bitシフトしてマスクします。 データサイズを取得する場合はビットシフトは不要で0xFFFFのマスクをかけるだけで取得できます。
実行結果
例えば最後に掲載しているサンプルコードを実行して「桜」の文字を送信すると以下のようになるはずです。
(文字コードはShift_JISです。)
終わりに
今まで画像データから読み込んでいましたが。こっちのほうが簡単だしファイルシークも少ないので高速だと思います。じゃあ何故最初からこれをやらなかったのかというと…。まぁ単純にFONTXファイルの存在を知らなかったというのが一番です。
Arduinoで使う場合は漢字ROMを使う以外では最良の方法だと思います。
Raspberry Piに関しても、多分この方法のほうが早いです。
というわけで今回はフォントファイルから漢字パターンを取得する方法をご紹介しました。
これを使えば8×8ピクセル以外に12×12や16×16、もっと大きなフォントも使えるので例えばカラー液晶などに転送する場合はこっちの方がいいでしょうね。
なお、この文字コードは縦はドット分そのままですが、横が8ドット単位で格納されています。
14ドットなどの場合は2pxの空きとなるので逆に見やすいですが。10pxや12pxの場合はちょっと文字間が開きすぎなので、文字を転送し終えたあと例えば12ドットなら4ドット分戻って次の文字を転送すると穴埋め分をかき消すことができると思います。
それでは。
サンプルコード
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 | // SDカード用ライブラリ #include <SPI.h> #include <SD.h> // フォントファイルのファイル名を指定 #define FONT_FILEJ "FONTXJ.FNT" // 全角文字用フォントを指定 #define FONT_FILEA "FONTXA.FNT" // 半角文字用フォントを指定 void setup() { // put your setup code here, to run once: // シリアルポートの初期化 9600bps Serial.begin(9600); // シリアル通信が開かれるまで待つ while (!Serial); // SDカードの初期化 if (!SD.begin(4)) { Serial.println("SD card error !"); return; } // 準備ができたら「準備完了!」と送信。 // ArduinoIDEのシリアルモニタはShift_JISなのでエスケープ文字をつけて文字コードを送信してやる。 Serial.write("\x8F\x80\x94\xF5\x8A\xAE\x97\xB9\x81\x49\n"); } ////// FONTX形式のフォントファイルからフォントファイルを読み込み。 ////// // 対応フォントはFONTX形式 最小8ドット四方・最大255ドット四方まで対応。 // 最小フォントサイズは実用値、最大フォントサイズは論理値。 // // 受け渡し用の変数: // code : 文字コード(符号なしint型) // *pat : 戻ってきた文字パターンを格納するための配列変数(符号なしchar型) // 1フォントあたりのバイト数以上を確保すること。 // // 戻り値: 符号なしlong型。 unsigned long KanjiReadX(unsigned int code , unsigned char *pat) { // 参考資料:http://www.elm-chan.org/docs/dosv/fontx.html // オフセット計算用 // ccc = 現在の文字数(Character Count C…?) // ofset = 最終的なオフセット値(OFfSET) unsigned long ccc = 0 , ofset = 0; // フォントファイルを開く // 文字コードを判別して、文字コードが1バイトの範囲内なら半角コード // 2バイトの範囲内なら全角コード用のファイルを読み込む。 File fontx_file; if (code < 0x100) fontx_file = SD.open(FONT_FILEA, FILE_READ); if (code > 0x100) fontx_file = SD.open(FONT_FILEJ, FILE_READ); // フォントファイルのファイルタイプとファイル名は不要なので // その分ファイルをシークしておく fontx_file.seek(14); // フォントデータの読み込み。 // Fw = 文字サイズ(横)単位:ピクセル // Fh = 文字サイズ(縦)単位:ピクセル // Ft = 文字コード(00ならアスキー 01ならShift_JIS) 最後2つは2バイト文字のみの変数 // Fb = コードブロック数(Fbwはループ用) uint8_t Fw = fontx_file.read() , Fh = fontx_file.read(), Ft = fontx_file.read() , Fb, Fbw; // 1文字あたりのデータサイズ(バイト)を計算 uint16_t Fsize = ((Fw + 7) / 8 ) * Fh ; // コードフラグが0なら英文フォント用のオフセットを書き込む。 if (Ft == 0x00) { // ヘッダサイズ + 文字コード×1文字あたりのサイズ if (code < 0x100) ofset = 17 + code * Fsize ; } else { // ブロック数のデータは2バイト用フォントにしか無いのでココで取得する。 Fb = Fbw = fontx_file.read(); // フォントオフセット取得用ループ while (Fbw --) { // コードブロックからコードの範囲を読み込む // Cbs = 開始コード(Code Block Start) // Cbe = 終了コード(Code Block End) uint16_t Cbs = fontx_file.read() | fontx_file.read() << 8 , Cbe = fontx_file.read() | fontx_file.read() << 8; // 指示された文字コードがコードブロック内に存在するかどうかを確認 if (code >= Cbs && code <= Cbe) { // コードブロック内に文字が存在したのなら // そのコードブロック内で何文字目に存在するのかを取得 ccc += code - Cbs; // 全体のオフセット値を計算する // ヘッダサイズ+4バイト×コードブロック数+直前の文字コードまでの文字数×1文字あたりのサイズ ofset = 18 + 4 * Fb + ccc * Fsize; // 抜ける break; } // 取得したブロック内に該当文字コードが存在しない場合。 // 存在しなかったブロック内の文字数を計算して次へ。 ccc += Cbe - Cbs + 1 ; } } // ループ用にフォントサイズを退避 uint16_t i = Fsize; // 指示された文字コードが見つかった場合の処理 if (ofset) { fontx_file.seek(ofset); // パターン格納用の変数に漢字パターンを取得し、格納 while (i--) *(pat++) = fontx_file.read(); // 終わったらファイルを閉じる fontx_file.close(); // フォントデータをリターンして終了 return ((unsigned long)Fw << 24) | ((unsigned long)Fh << 16) | Fsize; // 戻り値は4バイトデータで、1バイトごとに上から // 1バイト目 =フォントサイズ(幅) // 2バイト目 =フォントサイズ(高さ) // 3・4バイト目=フォントデータのバイト数(2バイト) // を返す。 } // 見つからなかった場合はファイルを閉じて0をリターンして終了 fontx_file.close(); return 0 ; } void loop() { // put your main code here, to run repeatedly: // 文字コード格納用とパターン格納用 static unsigned int kanji; static unsigned char pat[2 * 16], i; // 受信データが届くまで待つ while (!Serial.available()); // 文字コードを検知して1バイト文字なら1バイトだけ抜き出し、 // 2バイト文字なら2バイトをくっつける kanji = (unsigned int)Serial.read() << 8; if ((kanji >= 0x8000 && kanji <= 0xA000) || kanji >= 0xE000) { while (!Serial.available()); kanji |= Serial.read(); } else { kanji >>= 8; } // パターン取得 unsigned long p = KanjiReadX(kanji, pat) ; // 1バイトに満たない場合は穴埋めされるので // それに合わせて穴埋めする。 int j = (((p >> 24) & 0xff) + 7) / 8; // 戻り値からフォントサイズのみ抜き出す。 p &= 0xffff; // 表示用ループ。 // 改行位置が違う以外は前回のものと同じ。 if (p) { int x = 0; while (x < p) { i = 8; while (i--) { if (((pat[x] >> i) & 1) == 0 ) Serial.print(" "); if (((pat[x] >> i) & 1) == 1 ) Serial.print("##"); } x++; if (!(x % j)) Serial.println(); } // 動いてないのか範囲外のコードなのかわからないので範囲外のコードはエラーを返すようにする。 }else{ char er[32]; sprintf(er,"[ Character code out of range. 0x%04X ]",kanji); Serial.println(er);} } |
前後に投稿された記事
前後に投稿された記事(カテゴリー『 Arduino 』内)
関連記事