MicroPython だけで UTF-8 の漢字を表示
はじめに
最近 ESP32 + MicroPython で遊んでいます.Cに比べて試行錯誤が楽で,日曜プログラミングのネタとしていいですね.こんな感じで OLED につなげて遊んでいます.
これを作るにあたって,以下の課題にチャレンジしてみました.
- MicroPython 公式配布イメージだけで漢字表示 (再構築や Cを使わない).
- UTF-8 文字列を素直に受け取る (UTF-8 → SHIFT_JIS の変換表等不要).
- コンパクトなフォント形式 の設計とフォント作成ツール実装 (車輪の再発明?)
できたコードを GitHub に置いてあります.
200KB 程度のフォントファイル 1個と MicroPython で 150行程度のファイル1個を追加すれば,漢字のグリフを扱えるので,日本語表示をちょっと足したい場合に便利だと思います.
以下,自分の備忘録も兼ねて,つらつらと解説を書いてみたいと思います.
使ってみよう
とりあえずどんな物なのか試してみる手順です.
-
GitHub から pinot を clone
git clone https://github.com/yoshinari-nomura/pinot
-
MicroPython で動かす前に手元の PC の Python でテストしてみます.
python3 -c 'from src.lib.pnfont import Font; f = Font("src/fonts/shnmk16u.pfn"); print(f.glyph("山").banner())'
........#....... ........#....... ........#....... ........#....... ........#....... ..#.....#.....#. ..#.....#.....#. ..#.....#.....#. ..#.....#.....#. ..#.....#.....#. ..#.....#.....#. ..#.....#.....#. ..#.....#.....#. ..#.....#.....#. ..#############. ................
グリフの展開は,pnfont.py だけで完結していて,150行程度とコンパクトです.
-
MicroPython の動作するマイコンボードを PC の USB に接続して,フォントファイルといくつかのコードを PC からボードに転送します.
export AMPY_PORT=/dev/ttyUSB0 ampy put src/fonts/shnmk12u.pfn /fonts/shnmk12u.pfn ampy put src/lib/display.py /lib/display.py ampy put src/lib/pnfont.py /lib/pnfont.py ampy put src/lib/ssd1306.py /lib/ssd1306.py
shnmk12u.pfn は 130KB 程度あるので,少し転送時間がかかりますが,他は一瞬です.
-
以下を display-test.py という名前でPC 側に置いて,ampy で走らせてみましょう.Pin と addr あたりは配線に合わせて調整が必要かもしれません.
from machine import Pin, SoftI2C from ssd1306 import SSD1306_I2C from pnfont import Font from display import PinotDisplay i2c = SoftI2C(scl = Pin(22), sda = Pin(21)) disp = PinotDisplay(panel = SSD1306_I2C(128, 32, i2c, addr = 0x3c), font = Font('/fonts/shnmk12u.pfn')) disp.text('本日は晴天なり\nってほどでもない')
ampy run display-test.py
冒頭のこんなのが出ます.
ちなみに,ハードウェアは,このあたりです.Amazon あたりでも買えます.
pinot のリポジトリ には,BDF フォントから今回使用した独自形式のフォント (PFN 形式と命名) ファイルを作るスクリプトも同梱しています.好きな BDF から PFN を作ったり,選択的に必要なグリフだけ (漢字+絵文字とか) 追加したりするのも面白そうです.
Pinot Font (PFN) フォーマット
フォントのファイル構造に関するメモです.
PFN の特徴
Pinot Font (PFN) は,マイコン等での利用を想定した Fixed フォントのファイル形式で,以下の特徴があります.
-
ファイルサイズが比較的小さい
ASCII + JISX0201 + JISX0208 の東雲フォントを PFN に変換すると以下のサイズでした.- 12ドット: 139,258バイト
- 14ドット: 188,043バイト
- 16ドット: 236,670バイト
-
1ファイルで完結している
UTF-8 (UCS4) ベースなので UTF-8 → SHIFT_JIS のようなコード変換テーブルが不要です.また,ASCII用/JISX0208用等の区別もありません. -
ファイルの seek だけでグリフの検索が可能
これは,検索コードの実装依存の話なので特徴ではないですが,全体をメモリに置くことなくグリフを取得できるように意識はしています.
PFN の全体構造
Pinot font (font-file) の形式を BNF っぽく書くとこんな感じです:
font-file = font-header font-block{1,}
font-block = block-header glyph-entry{num_glyphs}
glyph-entry = codepoint bitmap
- font-file は,先頭の font-header とそれに続く1つ以上の font-block からなります.
- 各 font-block は,先頭の block-header とそれに続く num_glyphs 個の glyph-entry からなります (num_glyphs は block-header 内で与えられている).
- 各 glyph-entry は,UCS4 (UTF-32) のコードポイントとビットマップの連接です.
各エレメントの詳細
以下,font-header, block-header, glyph-entry の構造についての詳細です.なお,断りがなければ,数値は全て符号なしでリトルエンディアンです.
-
font-header (16 bytes):
サイズ フィールド名 例 説明 7 signature "PINOTFN" 固定の文字列 1 version 0x01 バージョン番号 8 fontname "shnmk14u" 8文字のフォント名 -
block-header (6 bytes):
サイズ フィールド名 説明 1 width 続く glyph-entry 内の bitmap 幅 1 height 続く glyph-entry 内の bitmap 高さ 1 codepoint_size 続く glyph-entry 内の codepoint サイズ (1/2/4 いずれか) 1 attribute 将来(?) 拡張用 2 num_glyphs 続く glyph-entry の個数 block-header の各フィールドから font-block 全体のサイズを計算できます:
sizeof(font-block) = ((width * height + 7) / 8 + codepoint_size) * num_glyphs
これによって,次の font-block の先頭まで一気にシークできます.
-
glyph-entry (codepoint_size + (width * height + 7 ) / 8 bytes):
サイズ フィールド名 例 説明 codepoint_size codepoint 0x41 ('A') UTF-32LE だが必要な桁数 (width * height + 7) / 8 bitmap 0b00110000…. MSB→LSB 順に水平方向から
特記事項
- 各 glyph-entry は,font-block を越えて codepoint 順に並んでいなければなりません.
- codepoint_size は,UTF-32 だと本来 4 ですが,ASCII は 1バイト,日本語を扱う場合でもほぼ 2バイトに収まるので,block-header で最適な長さを設定可能にしています.
- bitmap のサイズについて,例えば,7x14 のグリフは,7*14 = 98 ビット必要ですが,バイト境界に合わせるために末尾に 6ビットパディングして 104ビット (13バイト) とします.
- Python だと
(width * height + 7) // 8
でしょうか - BDF フォントのエンコードと異なることに気をつけて下さい.BDF は,各水平ライン毎に 0 パディングする方式です.
- Python だと
- font-block の個数は任意ですが,構造上,グリフの width, height, codepoint_size のいずれかが変化した時点で新たな font-block が必要でしょう.
従って,codepoint でソートした場合にこれらが頻繁に変化するような glyph-entry の集合を扱うのに PFN フォーマットは向いていません.Fixed フォント向けです.
グリフ検索の例
フォントファイルからグリフを検索するコード例は, pinot リポジトリの pnfont.py Font.glyph あたりにあります.各文字について,以下のような処理をしています.
- UTF-8 → UTF-32 codepoint に変換
- 当該グリフのある font-block まで seek
- font-block 内を codepoint をターゲットにして 2分探索
おわりに
-
実験のために 東雲フォント を使いました.作者の皆様ありがとうございます.東雲フォントの 14 ドットって,K14 由来なのですね.昔からお世話になっていました.
-
今回,MicroPython (というか Python) をはじめて書いたのですが,あまりに慣れてなさ過ぎて,大半 Ruby で書いて Python に手で翻訳するという意味不明なことをしていました (おかげで PFN フォントを作るコードは,ボードで動かす訳ではないので,Ruby のままです).
しかし,この方法は,ロジックを考える部分と記法を調べる部分が独立になるので,新しい言語を学ぶのにかえって効率いいのでは,と思ったりもしました.
-
手元で開発したコードを ampy で PC からボードにチマチマとコピーをするのが面倒になってきたので,ampy を裏で使って rsync のようなことをするスクリプトを書きました.
これも pinot に収めたので,よかったら使ってみてください.機会があれば,また別の記事で触れたいです.
Discussion