2020年8月28日 字模接口的邏輯代碼支持到 32 * 32 了,更新的 commit 在此 https://github.com/sipeed/MaixPy/commit/a3a5d1a1975fa19265809d6b81d2ea02e91ce09f。
關於下文中存在的任何名詞和術語,如果不清楚不了解的自行到其他資料中學習得知,除了必要之外我都不會做出相關鏈接和解釋。
背景
此事要從一些奇怪芯片的國產化進程上說起,總得來說,一款有不少屏幕支持的芯片在實現了基礎功能后,中文字體的字庫功能是始終逃不掉的事實,那么我們該如何看待這樣的事實呢?
早在掌控版 esp32 的時期就已經在試圖推行這些漢字的操作了,不得不說,這確實是有利於國內用戶的需求。
尤其是在一些商業場合上,在中國做中國的生意,顯示中文是多么的基本鴨。
剛好在 MaixPy 的代碼里做完了這個功能,做得七七八八吧,不算完美,但也夠大部分場合使用了。
那么要如何在一款屏幕上顯示中文呢?
先說點歷史進程的事情,最早計算機由美國發展興起,所以當時設計出了 ASCII ((American Standard Code for Information Interchange): 美國信息交換標准代碼),而這個事情應該是在美蘇爭霸爭霸期間,當然我講的這點故事和編碼沒關系,主要是當時的美國在航空業上輸了蘇聯后就趁早開了新賽道走起了半導體路線,所以今天我們看到的很多計算機的定義和標准都是來源於美國國家標准學會,所以我們都知道 ASCII 編碼表指是下面這張表。
很快計算機興起了 ASCII 根本不夠其他國家表示字符來使用,就在原來的 ASCII 基礎上拓展出了 Unicode 萬國碼,所以說 Unicode 是兼容萬國碼的,它定義很簡單,就兩個字節的二進制數據繼續拓展編碼定義,所以在編碼的開頭里還是 ASCII 碼。
至此 Unicode 夠多了,2^16=65536 個編碼的可能性,足夠將大部分人類的文字涵蓋其中,可隨着 Web 的興起 Unicode 碼並不適合字符編碼的傳輸,例如兩個字節中,開頭就為 '\0' 這就會導致 socket 傳輸的字符串還沒開始傳就結束了,為了克服這個問題,就引出了 utf-8 的編碼設計,類似於上了一層 base64 編碼便於傳輸,也就是后來 unicode 互轉 utf-8 的原理,關於這個編碼的發展歷程可以多看一些知乎的故事。
其實講到這些,多少都會有些片面,因為事物發展是多種可能性的,我們知道了這些事實,就有利於我們理解為什么會演變成今天這樣,而非重新設計一套方案,開歷史的倒車。
就像我曾經升級 base64 到 base128 的編碼方式,然而我當時對自己的這個設計沾沾自喜,以為創造了新的編碼算法,后來才知道,這個叫 base128 編碼規則,所以咱們學習要全面,不要經驗主義。
但到這些也只是指文字的編碼,並不等於文字的顯示內容,從這里開始就引出了字模的定義,字模就像下面這款軟件產生的結果一樣。
我們可以看到它實際上就是對屏幕的像素點依次打印出來,打印的方式可以稱為掃描方式,如我所選的上下到左右,又或是其他,這個關於到字模的存儲格式和顯示方式,然后我們將這字體中一系列的文字導出來就變成了字庫。
所以字庫不一定具備兼容性的,相比 ttf 字體,這種字體的手段更為原始一些,因為它通過二進制數據控制打印的數據內容,將字模打印的邏輯也很簡單,只需要依次判斷所取的行或列的位數據是否存在為 1 即可,若是則打印該像素點。
關於字模的中文資料很多,我想也不是本文的重點了,這些基礎的理論知識就留給好奇的你自行去查閱資料吧。
那么如何實現呢?在實現前的准備工作
我們是要在 MaixPy 的環境下實現該功能,在 MaixPy 中存在兩類字模的 C 實現,一類為 lcd.draw_string ,另一類為 openmv 的 image.draw_string ,事實上 lcd 不應該參與繪圖函數的功能,反而應該放棄這部分實現,轉而到 image 的繪圖后統一顯示(lcd.display)到屏幕上。
但由於 openmv 的字體相當難看,我們若是想全部統一,則要提升 openmv 的字體效果。
關於 openmv 的 字模 定義 在這里。
那么它是如何實現的呢?
我們在 image 的 draw_string 處理過程中可以看到 const glyph_t *g = &font[ch - ' ']; 在和 ' '
做差,這表示它只能兼容到 ASCII 的可視化字符 ' '
部分,這樣就能最大程度的壓縮體積和保證基本功能。
但事實上這樣的做法都暴露了一個問題,字體的定義方式浪費了空間,沒有必要為此帶入 寬度 和 高度 的變量存儲,因為本身就沒有解耦接口的實現,就算帶入了也都是一堆重復的臟數據。
出現這樣的代碼都是說明 openmv 的開發者們在思考要不要做兼容,要不要預留字模的控制方法,但事實上這個存儲結構和存儲邏輯也有密切的關系,例如 8 位 和 16 位的字模在掃描方式上又要分離邏輯,並不能直接使用。
為復雜項目添加代碼,應當保證實現的代碼盡量都是最小化侵入架構,確保彼此功能獨立。
這時候我們就要思考了,現成的代碼很多很亂,如何讓它們在敏捷開發的思路下被迭代掉,這個思路得提前確定好,為此先梳理一遍整體的邏輯。
想要打印中文字體 < 能夠將畫布顯示到屏幕 < 能夠將字模打印到畫布上 < 能夠從字庫中獲取字模 < 能夠從存儲介質中獲取字庫 < 最終用戶能夠通過 API 實現打印中文字體。
接下來將順着這個流程依次實現各組件的功能,最后將其鏈接起來即可大功告成。
實現字模的打印
我們已經能夠在 MaixPy 的基礎上實現了將字模打印到畫布上,並顯示的功能,如下 Python 代碼。
import lcd
import image
lcd.init()
img = image.Image(size=(240, 240))
img.draw_rectangle((0,0,240,240), fill=True, color=(150,150,150))
img.draw_string(60, 100, "hello maixpy", scale=4)
lcd.display(img)
我們不難看到 draw_string 就已經實現了該功能,它是如何實現的呢?看如下代碼
STATIC mp_obj_t py_image_draw_string(size_t n_args, const mp_obj_t *args, mp_map_t *kw_args)
{
image_t *arg_img = py_helper_arg_to_image_mutable(args[0]);
const mp_obj_t *arg_vec;
uint offset = py_helper_consume_array(n_args, args, 1, 3, &arg_vec);
int arg_x_off = mp_obj_get_int(arg_vec[0]);
int arg_y_off = mp_obj_get_int(arg_vec[1]);
const char *arg_str = mp_obj_str_get_str(arg_vec[2]);
int arg_c =
py_helper_keyword_color(arg_img, n_args, args, offset + 0, kw_args, -1); // White.
float arg_scale =
py_helper_keyword_float(n_args, args, offset + 1, kw_args, MP_OBJ_NEW_QSTR(MP_QSTR_scale), 1.0);
PY_ASSERT_TRUE_MSG(0 < arg_scale, "Error: 0 < scale!");
int arg_x_spacing =
py_helper_keyword_int(n_args, args, offset + 2, kw_args, MP_OBJ_NEW_QSTR(MP_QSTR_x_spacing), 0);
int arg_y_spacing =
py_helper_keyword_int(n_args, args, offset + 3, kw_args, MP_OBJ_NEW_QSTR(MP_QSTR_y_spacing), 0);
bool arg_mono_space =
py_helper_keyword_int(n_args, args, offset + 4, kw_args, MP_OBJ_NEW_QSTR(MP_QSTR_mono_space), true);
imlib_draw_string(arg_img, arg_x_off, arg_y_off, arg_str,
arg_c, arg_scale, arg_x_spacing, arg_y_spacing,
arg_mono_space);
return args[0];
}
STATIC MP_DEFINE_CONST_FUN_OBJ_KW(py_image_draw_string_obj, 2, py_image_draw_string);
void imlib_draw_string(image_t *img, int x_off, int y_off, const char *str, int c, float scale, int x_spacing, int y_spacing, bool mono_space)
{
const int anchor = x_off;
for(char ch, last = '\0'; (ch = *str); str++, last = ch) {
if ((last == '\r') && (ch == '\n')) { // handle "\r\n" strings
continue;
}
if ((ch == '\n') || (ch == '\r')) { // handle '\n' or '\r' strings
x_off = anchor;
y_off += fast_roundf(font[0].h * scale) + y_spacing; // newline height == space height
continue;
}
if ((ch < ' ') || (ch > '~')) { // handle unknown characters
imlib_draw_rectangle(img,
x_off + (fast_roundf(scale * 3) / 2),
y_off + (fast_roundf(scale * 3) / 2),
fast_roundf(font[0].w * scale) - ((fast_roundf(scale * 3) / 2) * 2),
fast_roundf(font[0].h * scale) - ((fast_roundf(scale * 3) / 2) * 2),
c, fast_roundf(scale), false);
continue;
}
const glyph_t *g = &font[ch - ' '];
if (!mono_space) {
// Find the first pixel set and offset to that.
bool exit = false;
for (int x = 0, xx = g->w; x < xx; x++) {
for (int y = 0, yy = g->h; y < yy; y++) {
if (g->data[y] & (1 << (g->w - 1 - x))) {
x_off -= fast_roundf(x * scale);
exit = true;
break;
}
}
if (exit) break;
}
}
for (int y = 0, yy = fast_roundf(g->h * scale); y < yy; y++) {
for (int x = 0, xx = fast_roundf(g->w * scale); x < xx; x++) {
if (g->data[fast_roundf(y / scale)] & (1 << (g->w - 1 - fast_roundf(x / scale)))) {
imlib_set_pixel(img, (x_off + x), (y_off + y), c);
}
}
}
if (mono_space) {
x_off += fast_roundf(g->w * scale) + x_spacing;
} else {
// Find the last pixel set and offset to that.
bool exit = false;
for (int x = g->w - 1; x >= 0; x--) {
for (int y = g->h - 1; y >= 0; y--) {
if (g->data[y] & (1 << (g->w - 1 - x))) {
x_off += fast_roundf((x + 2) * scale) + x_spacing;
exit = true;
break;
}
}
if (exit) break;
}
if (!exit) x_off += fast_roundf(scale * 3); // space char
}
}
}
從邏輯上來說,它過度封裝了 string 到 font 的過程,將其邏輯融合在一起,事實上 string 的處理應當和 font 的處理分離成兩個函數,為此我將其解耦成一個新的 imlib_draw_font ,並公開接口。
void imlib_draw_font(image_t *img, int x_off, int y_off, uint8_t font_h, uint8_t font_w, const uint8_t *font, int c, float scale, int x_spacing, int y_spacing, bool mono_space)
{
const int anchor = x_off;
if (!mono_space) {
// Find the first pixel set and offset to that.
bool exit = false;
for (int x = 0, xx = font_w; x < xx; x++) {
for (int y = 0, yy = font_h; y < yy; y++) {
if (font[y] & (1 << (font_w - 1 - x))) {
x_off -= fast_roundf(x * scale);
exit = true;
break;
}
}
if (exit) break;
}
}
for (int y = 0, yy = fast_roundf(font_h * scale); y < yy; y++) {
uint8_t pos = fast_roundf(y / scale);
uint16_t tmp = font[pos];
if (8 < font_w && font_w <= 16) {
tmp <<= 8, tmp |= font[pos + font_h]; // font ↑ ↓ ← →
}
for (int x = 0, xx = fast_roundf(font_w * scale); x < xx; x++) {
if (tmp & (1 << (font_w - 1 - fast_roundf(x / scale)))) {
imlib_set_pixel(img, (x_off + x), (y_off + y), c);
}
}
}
if (mono_space) {
x_off += fast_roundf(font_w * scale) + x_spacing;
} else {
// Find the last pixel set and offset to that.
bool exit = false;
for (int x = font_w - 1; x >= 0; x--) {
for (int y = font_h - 1; y >= 0; y--) {
if (font[y] & (1 << (font_w - 1 - x))) {
x_off += fast_roundf((x + 2) * scale) + x_spacing;
exit = true;
break;
}
}
if (exit) break;
}
if (!exit) x_off += fast_roundf(scale * 3); // space char
}
}
STATIC mp_obj_t py_image_draw_font(size_t n_args, const mp_obj_t *args, mp_map_t *kw_args)
{
image_t *arg_img = py_helper_arg_to_image_mutable(args[0]);
const mp_obj_t *arg_vec;
uint offset = py_helper_consume_array(n_args, args, 1, 5, &arg_vec);
int arg_x_off = mp_obj_get_int(arg_vec[0]);
int arg_y_off = mp_obj_get_int(arg_vec[1]);
int arg_w_font = mp_obj_get_int(arg_vec[2]);
int arg_h_font = mp_obj_get_int(arg_vec[3]);
const uint8_t *arg_font = mp_obj_str_get_str(arg_vec[4]);
mp_int_t font_len = mp_obj_get_int(mp_obj_len(arg_vec[4]));
PY_ASSERT_TRUE_MSG(arg_w_font % 8 == 0 && arg_w_font <= 16, "Error: font arg_w_font %% 8 == 0 && arg_w_font <= 16!");
PY_ASSERT_TRUE_MSG((arg_w_font / 8) * arg_h_font == font_len, "Error: font (arg_w_font / 8) * arg_h_font == font_len!");
int arg_c =
py_helper_keyword_color(arg_img, n_args, args, offset + 0, kw_args, -1); // White.
float arg_scale =
py_helper_keyword_float(n_args, args, offset + 1, kw_args, MP_OBJ_NEW_QSTR(MP_QSTR_scale), 1.0);
PY_ASSERT_TRUE_MSG(0 < arg_scale, "Error: 0 < scale!");
int arg_x_spacing =
py_helper_keyword_int(n_args, args, offset + 2, kw_args, MP_OBJ_NEW_QSTR(MP_QSTR_x_spacing), 0);
int arg_y_spacing =
py_helper_keyword_int(n_args, args, offset + 3, kw_args, MP_OBJ_NEW_QSTR(MP_QSTR_y_spacing), 0);
bool arg_mono_space =
py_helper_keyword_int(n_args, args, offset + 4, kw_args, MP_OBJ_NEW_QSTR(MP_QSTR_mono_space), true);
imlib_draw_font(arg_img, arg_x_off, arg_y_off, arg_h_font, arg_w_font, arg_font,
arg_c, arg_scale, arg_x_spacing, arg_y_spacing,
arg_mono_space);
return args[0];
}
STATIC MP_DEFINE_CONST_FUN_OBJ_KW(py_image_draw_font_obj, 2, py_image_draw_font);
使用的 python 代碼如下:
# 8 * 8
tmp = b'\x20\xFC\xFC\x2C\xAC\x4C\x4D\xA3'
img.draw_font(10, 20, 8, 8, tmp, scale=1, color=(0,0,0))
img.draw_font(60, 15, 8, 8, tmp, scale=2, color=(255,0,0))
img.draw_font(110, 10, 8, 8, tmp, scale=3, color=(0,255,0))
img.draw_font(150, 10, 16, 8, b'\x00\x00\x00\x00\x7F\x01\x01\xFF\x00\x00\x00\x78\x80\x00\x04\xFE', scale=2, color=(0,0,255))
img.draw_font(200, 10, 8, 16, b'\x00\x00\x00\x00\x7F\x01\x01\xFF\x01\x01\x01\x01\x01\x01\x01\x01', scale=2, color=(0,0,255))
img.draw_font(200, 10, 8, 10, b'\x01\x02\x04\x08\x10\x20\x40\x80\x01\x02', scale=4, color=(0,0,255))
# 16 * 16
qian = b'\x00\x00\x00\x00\x7F\x01\x01\xFF\x01\x01\x01\x01\x01\x01\x01\x01\x00\x00\x00\x78\x80\x00\x04\xFE\x00\x00\x00\x00\x00\x00\x00\x00'
img.draw_font(10, 50, 16, 16, qian, scale=1, color=(0,0,0))
img.draw_font(10, 100, 16, 16, qian, scale=2, color=(0,0,0))
img.draw_font(10, 150, 16, 16, qian, scale=3, color=(0,0,0))
li = b'\x00\x00\x00\x1F\x11\x11\x1F\x11\x1F\x11\x01\x01\x3F\x01\x01\xFF\x00\x00\x08\xF8\x08\x08\xF8\x08\xF8\x08\x00\x08\xFC\x00\x00\xFE'
img.draw_font(60, 50, 16, 16, li, scale=1, color=(255,0,0))
img.draw_font(60, 100, 16, 16, li, scale=2, color=(255,0,0))
img.draw_font(60, 150, 16, 16, li, scale=3, color=(255,0,0))
zhi = b'\x00\x00\x02\x01\x01\x7F\x00\x00\x00\x00\x01\x06\x08\x30\x4C\x03\x00\x00\x00\x00\x08\xFC\x10\x20\x40\x80\x00\x00\x00\x00\x00\xFE'
img.draw_font(120, 50, 16, 16, zhi, scale=1, color=(0,255,0))
img.draw_font(120, 100, 16, 16, zhi, scale=2, color=(0,255,0))
img.draw_font(120, 150, 16, 16, zhi, scale=3, color=(0,255,0))
wai = b'\x00\x00\x04\x08\x08\x0F\x11\x11\x29\x26\x42\x04\x04\x08\x10\x20\x00\x00\x20\x20\x20\xA0\x20\x38\x24\x22\x20\x20\x20\x20\x20\x20'
img.draw_font(180, 50, 16, 16, wai, scale=1, color=(0,0,255))
img.draw_font(180, 100, 16, 16, wai, scale=2, color=(0,0,255))
img.draw_font(180, 150, 16, 16, wai, scale=3, color=(0,0,255))
lcd.display(img)
實現的效果圖如下:
這實際上就是開放了一個打印字模的接口,但很遺憾的是,為了照顧舊字模,我指定了掃描方向為 先上下 后左右 ,這表示它的工作方式就是下圖所示。
但這個區別只存在於 大於 8 位后的字模存儲方式,對比邏輯可以看到我后來設計的過程中,改變字模的到 uint16_t 的結構去方便判斷是否打印行像素,暫不支持大於 16 位寬度的字模的處理喔(我沒寫)。
這在舊代碼上是屬於兼容實現,不會破壞原有的功能,也更契合 openmv 的架構。
它帶來的好處就是方便調試字模的效果,但也舍棄了原本對 string 字符串的處理過程,關於這個我會在之后進行彌補,它涉及到其他編碼字符串的處理問題,draw_string 是無法適應的。
與傳統的燒寫字模數組來提取調用相比,用 Python 接口導入數據可以更快的確認效果,同時也方便調試功能,主體邏輯不變,剩下的只是調參,當然,參數多調幾次就出來了,也不礙事。
而這些功能,本來就可以在 C 層面實現,問題只在於,能否 調試接口 和 開發接口 共存,這也是 micropython 模塊設計的理念了。
實現字庫的索引
關於這個問題,在實現打印字模后就要考慮的問題,我們不可能全部都在 draw_font 接口下人工的輸入字模來打印字體吧。
所以用戶輸入的一定是可視化的中文字符,如下圖的內容。
通常來說呢,在 Python3 中,是在 str 中實現了 unicode 的轉換,也就是說到了 str 里,都是 unicode 編碼,而 micropython 說是支持 python3.5 ,實際上並沒有實現 Unicode 的轉換,這個做個簡單的代碼實現就知道了。
本來講道理應該是下圖這個理論的。
而實際上 micropython 的外部輸入什么,內部的內容就是什么,並不會做轉換,因為代碼里根本沒有實現編碼的轉換,不過可以開一個 utf-8 的 check 來幫助屏蔽不合法編碼。
那怎么辦呢?
沒有就創造出來唄。
通常用戶使用 IDE 向 micropython 傳遞的字符多為 utf-8 ,而為什么是 utf-8 前面已經提及了傳輸過程中可能在 unicode 頭部出現的 '\0' 終止寫入,事實上你用 base64 包裝一下也是可以的,但誰會沒事給自己的找麻煩呢?
所以我們上圖的測試可以得知編輯器輸入的中文就是 UTF-8 編碼,如果我們想要使用 Unicode 索引的字庫,則我們需要將 UTF-8 解碼回 Unicode 編碼,而這個由於是 Python 的標准實現,所以只能從 C 代碼里抄邏輯回來了(攤手)。
只是 cpython 實現的 utf-8 與 Unicode 太完整了,有點難抄啊.....(主要是 Python 類型牽扯太多變量,不想抄了)
所以隨便找了份 C 代碼來轉到 Python 實現了。
def encode_get_utf8_size(utf):
if utf < 0x80:
return 1
if utf >= 0x80 and utf < 0xC0:
return -1
if utf >= 0xC0 and utf < 0xE0:
return 2
if utf >= 0xE0 and utf < 0xF0:
return 3
if utf >= 0xF0 and utf < 0xF8:
return 4
if utf >= 0xF8 and utf < 0xFC:
return 5
if utf >= 0xFC:
return 6
def encode_utf8_to_unicode(utf8):
utfbytes = encode_get_utf8_size(utf8[0])
if utfbytes == 1:
unic = utf8[0]
if utfbytes == 2:
b1 = utf8[0]
b2 = utf8[1]
if ((b2 & 0xE0) != 0x80):
return -1
unic = ((((b1 << 6) + (b2 & 0x3F)) & 0xFF) << 8) | (((b1 >> 2) & 0x07) & 0xFF)
if utfbytes == 3:
b1 = utf8[0]
b2 = utf8[1]
b3 = utf8[2]
if (((b2 & 0xC0) != 0x80) or ((b3 & 0xC0) != 0x80)):
return -1
unic = ((((b1 << 4) + ((b2 >> 2) & 0x0F)) & 0xFF) << 8) | (((b2 << 6) + (b3 & 0x3F)) & 0xFF)
if utfbytes == 4:
b1 = utf8[0]
b2 = utf8[1]
b3 = utf8[2]
b4 = utf8[3]
if (((b2 & 0xC0) != 0x80) or ((b3 & 0xC0) != 0x80) or ((b4 & 0xC0) != 0x80)):
return -1
unic = ((((b3 << 6) + (b4 & 0x3F)) & 0xFF) << 16) | ((((b2 << 4) + ((b3 >> 2)
& 0x0F)) & 0xFF) << 8) | ((((b1 << 2) & 0x1C) + ((b2 >> 4) & 0x03)) & 0xFF)
if utfbytes == 5:
b1 = utf8[0]
b2 = utf8[1]
b3 = utf8[2]
b4 = utf8[3]
b5 = utf8[4]
if (((b2 & 0xC0) != 0x80) or ((b3 & 0xC0) != 0x80) or ((b4 & 0xC0) != 0x80) or ((b5 & 0xC0) != 0x80)):
return -1
unic = ((((b4 << 6) + (b5 & 0x3F)) & 0xFF) << 24) | (((b3 << 4) + ((b4 >> 2) & 0x0F) & 0xFF) << 16) | ((((b2 << 2) + ((b3 >> 4) & 0x03)) & 0xFF) << 8) | (((b1 << 6)) & 0xFF)
if utfbytes == 6:
b1 = utf8[0]
b2 = utf8[1]
b3 = utf8[2]
b4 = utf8[3]
b5 = utf8[4]
b6 = utf8[5]
if (((b2 & 0xC0) != 0x80) or ((b3 & 0xC0) != 0x80) or ((b4 & 0xC0) != 0x80) or ((b5 & 0xC0) != 0x80) or ((b6 & 0xC0) != 0x80)):
return -1
unic = ((((b5 << 6) + (b6 & 0x3F)) << 24) & 0xFF) | (((b5 << 4) + ((b6 >> 2) & 0x0F) << 16) & 0xFF) | ((((b3 << 2) + ((b4 >> 4) & 0x03)) << 8) & 0xFF) | ((((b1 << 6) & 0x40) + (b2 & 0x3F)) & 0xFF)
return unic
實際上只需要得到兩個接口,判斷當前的是不是 utf-8 編碼,如果是則給我它的長度,將 utf-8 的字符串轉換回 unicode 編碼,常見於 1 字節英文和 3 字節中文的 UTF-8 編碼還原回 2 字節的 unicode 編碼。
既然確認編碼的轉換代碼可行,字庫也可以通過軟件導出了,那么現在就是確認一下索引規則了,如下圖。
如果是 16*16
則它所占的字符數應為 int(high*(width/8))
,也就是 32 字節。
如果是其他編碼的字庫呢?
例如 GB2312 的話定位方式是這樣的,這些都可以在字庫軟件中得到,它們的存儲結構都是線性的,這也方便在制作字庫芯片的時候,將其直接導入 flash 上。
至此我們已經解決了用戶輸入中文,且中文能夠被轉碼到 unicode 后提取字庫中的字模了,也就進一步到打印字模這個接口上了。
實現字庫的加載
那么字庫可以如何加載呢?
-
直接全部加載到內存上(存放於某個存儲器中)?
-
通過文件指針 seek 提取字模?
顯然第一個是最符合邏輯的思考,但由於 K210 的執行結構把整個 bin 程序都加載到內存運行,所以將其數組編譯進 bin 和加載到內存中的效果是一樣的,並不會因此而減少內存的占用。
所以就會導致加載過多的內存了,那前者方案只能加載預置的小字體了,綜合考慮來說是這樣了,所以我在 Python 層面上實現了 seek 的邏輯。
最終整合所有功能
最終的效果如下:
這圖就是同時載入了不同規格的字庫,提取不同編碼的字模,如 8*8
、16*16
的共用 和 b'你好,世界'
的 b'hello world!'
字符串打印,順便貼 Python 代碼,對用戶來說,麻煩一些的地方可能就在傳入字庫這個地方上了吧。
import lcd, time
import image
lcd.init(freq=15000000)
img = image.Image(size=(240, 240))
img.draw_rectangle((0,0,240,240), fill=True, color=(150,150,150))
def draw_string(img, x, y, c, s, string, width, high, fonts, space=1):
i = 0
pos = 0
while i < len(string):
utfbytes = encode_get_utf8_size(string[i])
print(i, string[i], utfbytes, string[i:i+utfbytes])
tmp = encode_utf8_to_unicode(string[i:i+utfbytes])
i += utfbytes
pos += 1
fonts.seek(tmp * int(high*(width/8)))
img.draw_font(x + (pos * s * (width + space)), y, width, high, fonts.read(int(high*(width/8))), scale=s, color=c)
import os
unicode_dict = open('/sd/unicode_8_8_u_d_l_r.Dzk', 'rb')
draw_string(img, 0, 20, (0,0,0), 3, b'你好,世界', 8, 8, unicode_dict)
draw_string(img, 0, 60, (0,0,0), 2, b'hello world!', 8, 8, unicode_dict)
unicode_dict.close()
unicode_dict = open('/sd/unicode_16_16_u_d_l_r.Dzk', 'rb')
draw_string(img, 0, 100, (0,255,0), 2, b'你好,世界', 16, 16, unicode_dict)
draw_string(img, 0, 140, (0,255,0), 1, b'hello world!', 16, 16, unicode_dict)
unicode_dict.close()
lcd.display(img)
有興趣的可以自己琢磨 我提供的 draw_string 的 Python 實現,以及 draw_font 的 C 實現,這里面就是我主要寫的代碼內容了,理解設計后就是理解細節,有心想學的話,是可以拎出來理解的,畢竟理解別人寫的代碼本身就是一件痛苦的事情。
但這種事情,也只是最初痛苦,久了就成習慣了,也就不痛苦了,你說呢?
后記
關於一些設計上的問題待考慮,主要的問題是,從邏輯上來看,大體是不會有什么改變了,無論是加載完整字模還是小字庫都是同一套邏輯,性能方面可能可以從 Python 轉 C 編譯下手,這樣就可以減少 Python 解釋的開銷,尤其是循環上。
所以應該要把一些功能丟回 C 實現,但由於要外部提供字庫,對這個字庫的數據源從 C 層面操作的話,可能會給 API 的說明帶來一些困擾,例如要求 Python 提供一個字符串或者 file 的支持 StringIO 操作的類供內部使用,從而減少不必要的加載字模到內存的操作,重點在提供 seek 和 read 操作。
大概就這樣吧?
junhuanchen 2020年6月27日