™️

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);
}
脚注
  1. Glyph Bitmap Distribution Format - Wikipedia ↩︎

  2. Public Domain を公称している. ↩︎

Discussion