WasabiOS 上に任意のビットマップフォント表示を実装する
私も流行りに乗って(?) hikalium さん著の「[作って学ぶ]OSのしくみⅠ──メモリ管理、マルチタスク、ハードウェア制御」を進めているところです.めちゃくちゃ楽しすぎて研究その他に悪影響を及ぼしていて大変に困っています.誰か助けてください.
というわけで題の通りです.この本においては,文字を描画するために,川合秀実さん著「30日でできる! OS自作入門」に付属していたビットマップフォントもどきのテキストファイルを使用しているのですが,せっかくなので任意のビットマップフォントを表示できるように実装しました.本稿はそのまとめです.
ビットマップフォント
文字をドット絵のような形で保存するフォントファイル形式のことを指します.この形式にはいくつかの形式があります.次にその例を示します.
- バイナリ
- BDF(Glyph Bitmap Distribution Format)
- Windows BMP
- TrueType Embedded
今回は簡単のために,機械判読性と人間可読性の両方を兼ね備えた BDF 形式を選択しました.BDF は Adobe が開発したテキスト形式のフォーマットで,主として UNIX の X Window 環境で使用されてきたようです[1].
BDF フォーマット
BDF フォーマットは,ヘッダに該当するグローバルセクションと各グリフの情報エントリから成ります.例として東雲ビットマップフォントファミリの内容を示します[2].
STARTFONT 2.1
COMMENT
COMMENT Shinonome 16dot font for JISX 0201, 1976
COMMENT
COMMENT The original is designed by
COMMENT Yasuyuki Furukawa <Furukawa.Yasuyuki@fujixerox.co.jp>, 2000.
COMMENT (Public Domain)
COMMENT
COMMENT Modified and Maintained by /efont/.
COMMENT
COMMENT (c) /efont/ -- Efont Open Laboratory 2001
COMMENT http://openlab.ring.gr.jp/efont/
COMMENT
FONT -Shinonome-Gothic-Medium-R-Normal--16-150-75-75-C-80-JISX0201.1976-0
SIZE 16 75 75
FONTBOUNDINGBOX 8 16 0 -2
STARTPROPERTIES 20
FONTNAME_REGISTRY ""
FOUNDRY "Shinonome"
FAMILY_NAME "Gothic"
WEIGHT_NAME "Medium"
SLANT "R"
SETWIDTH_NAME "Normal"
ADD_STYLE_NAME ""
PIXEL_SIZE 16
POINT_SIZE 150
RESOLUTION_X 75
RESOLUTION_Y 75
SPACING "C"
AVERAGE_WIDTH 80
CHARSET_REGISTRY "JISX0201.1976"
CHARSET_ENCODING "0"
DEFAULT_CHAR 32
FONT_DESCENT 2
FONT_ASCENT 14
COPYRIGHT "Public Domain"
_XMBDFED_INFO "Edited with xmbdfed 4.4."
ENDPROPERTIES
CHARS 221
STARTCHAR 01
ENCODING 1
SWIDTH 480 0
DWIDTH 8 0
BBX 8 16 0 -2
BITMAP
10
10
38
38
7c
7c
fe
fe
7c
7c
38
38
10
10
00
00
ENDCHAR
少し見にくいですが,行指向のデータであることがわかります.STARTFONT 2.1
から CHARS 221
がグローバルセクション,STARTCHAR
から ENDCHAR
がグリフ情報のエントリになります.エントリは CHARS
に続く数だけ続きます.
実装
型定義
さて,ファイルフォーマットはわかったので早速実装に入っていきたいところですが,現時点ではメモリ管理すら OS に未実装なので,ヒープを使うような高尚な Vec
などは使えません.従って,BDF ファイルのパース後の結果データのサイズは静的に決まっていなければなりません.今回は,これを加味して結果データの型を次のように定めました.
/// フォントの幅(px)
const FONT_WIDTH: usize = 8;
/// フォントの高さ(px)
const FONT_HEIGHT: usize = 16;
/// グリフ情報
type Font = [[bool; FONT_WIDTH]; FONT_HEIGHT];
/// 全てのグリフデータ
type Fonts = [Font; 256];
フォントの幅と高さはグローバルセクションの FONTBOUNDINGBOX
プロパティの値から定めることができます.グリフ情報は白黒のビットマップデータなので,bool
型の FONT_WIDTH
x FONT_HEIGHT
な配列にします.全てのグリフデータは Font
型の要素数 256 の配列です.今回は CHARS
プロパティに続く値を見て余裕を持つ形にしています.
最終的には,Fonts
型の n 番目の要素が n: char
なグリフデータを保持できるようにしていきます.
簡易 BDF パーサ
BDF 形式のパーサを fn init_bdf_font() -> Option<Fonts>
関数として書いていきます.次のコードは完成形です.
方針としては,BDF のテキストデータを一行ずつ処理していき,STARTCHAR
~ ENDCHAR
セクションの中のみを繰り返し処理していきます.STARTCHAR
に到達したらオペランドである文字コードを記録し,BITMAP
セクションに到達したら ENDCHAR
に達するまでグリフ情報をひたすら読んでいくといった具合です.
ポイントは if is_bitmap
ブロックの中の for
の中で fonts
に値を設定しているところでしょう.BITMAP
セクション内部の各行の数字はビットマップ一行ぶんのデータを示していて,16 進数です.実際には 1 ピクセルごとの塗りの有無が 16 進数で BDF ファイル上に記録されているので,一文字ずつ読み取って u8
に変換しては適当なマスクをかけて塗り情報を読み取ってあげます.アンチエイリアスがついているような形式では塗りの有無ではなく濃さを表す数値となるので,マスクではなくそのまま数値を読み取ってあげるだけで良いでしょう.
fn init_bdf_font() -> Option<Fonts> {
// BDF のテキストデータ
const FONT_SOURCE: &str = include_str!("../third_party/font/shnm8x16r.bdf");
// 結果セット
let mut fonts = [[[false; FONT_WIDTH]; FONT_HEIGHT]; 256];
// BDF の各行
let mut lines = FONT_SOURCE.split('\n');
// 現在 BITMAP セクションにいるかどうか
let mut is_bitmap = false;
// 現在パース中の文字コード
let mut char_code: usize = 0;
// ビットマップの行番号
let mut row = 0;
while let Some(line) = lines.next() {
if line.starts_with("ENDCHAR") {
is_bitmap = false;
row = 0;
continue;
}
if is_bitmap {
let mut column = 0;
for c in line.chars() {
// char -> &str
let bytes = [c as u8];
let s: &str = unsafe { core::str::from_utf8_unchecked(&bytes) };
let hex = u8::from_str_radix(s, 16).unwrap();
fonts[char_code][row][column * 4 + 0] = hex & 0b1000 != 0;
fonts[char_code][row][column * 4 + 1] = hex & 0b0100 != 0;
fonts[char_code][row][column * 4 + 2] = hex & 0b0010 != 0;
fonts[char_code][row][column * 4 + 3] = hex & 0b0001 != 0;
column += 1;
}
row += 1;
continue;
}
if line.starts_with("STARTCHAR") {
char_code = usize::from_str_radix(line.split(' ').last().unwrap(), 16).unwrap();
continue;
}
if line.starts_with("BITMAP") {
is_bitmap = true;
continue;
}
}
Some(fonts)
}
描画する
あとは描画するだけです.efi_main
関数の中で上記 init_bdf_font
関数を呼び出して BDF ファイルをパースします.その後,適当なタイミングで draw_str
関数を呼び出してあげます.
fn lookup_font(fonts: &Fonts, c: char) -> Option<Font> {
Some(fonts[c as usize])
}
fn draw_char<T: Bitmap>(buf: &mut T, color: u32, fonts: &Fonts, c: char, x: i64, y: i64) -> Result<()> {
let font = lookup_font(fonts, c).unwrap();
for fy in 0..FONT_HEIGHT {
for fx in 0..FONT_WIDTH {
if font[fy][fx] {
let _ = draw_point(buf, color, x + fx as i64, y + fy as i64);
}
}
}
Ok(())
}
fn draw_str<T: Bitmap>(buf: &mut T, color: u32, fonts: &Fonts, str: &str, x: i64, y: i64) -> Result<()> {
for (i, c) in str.chars().enumerate() {
let _ = draw_char(buf, color, fonts, c, x + (i * FONT_WIDTH) as i64, y);
}
Ok(())
}
結果
こんな感じで表示できます!嬉しい!
fn efi_main() {
// 省略
let fonts = init_bdf_font().unwrap();
let _ = draw_str(&mut vram, 0xffffff, &fonts, "Hello, World!", 500, 500);
}
-
Public Domain を公称している. ↩︎
Discussion