查看原文
其他

如何让UEFI BIOS支持汉字显示:汉字编码与显示实践

Wolf UEFI社区 2022-05-01
点击上方“公众号” 可以订阅哦!

我们通常看到的UEFI BIOS都是大片英文字符,对于中国用户而言,提供中文提示可以帮助我们建立更好的用户体验。那么如何在BIOS中显示中文呢,其实比较简单,我们分成两个部分介绍,第一部分介绍字符编码理论,第二部分介绍UEFI实践。如果你对字符编码理论很了解,可以直接从第二部分看起。

UEFI在订立之初就充分考虑了多国语言的管理、编码和显示问题。多语言的管理不在本文的范围内,大家可以参考UEFI spec。UEFI中的字符串统一采用Unicode的UCS-2编码。说起Unicode,它很容易和ASCII、UTF-8、UTF-16、UTF-32和GB2312编码等等概念混淆混淆。我们先来从历史沿革来看看他们都是什么,各自的优缺点都是什么。

Unicode、UTF-16与UCS-2

如果你是一个生活在2003年的程序员,却不了解字符、字符集、编码和Unicode这些基础知识。那你可要小心了,要是被我抓到你,我会让你在潜水艇里剥六个月洋葱来惩罚你。

这个邪恶的恫吓是Joel Spolsky在1993首次发出的,时至今日,unicode已经广泛使用在了我们周围,2003年到了现在又过去十几年了,如果还不知道unicode,被他抓住就不只是剥洋葱这么简单了。。。

为了避免被他抓住的可悲下场。我们赶紧来复习下字符编码的历史和unicode的产生。

1

历史

话说很久以前,计算机制造商有自己的表示字符的方式。他们并不需要担心如何和其它计算机交流,并提出了各自的方式来将字形渲染到屏幕上。随着计算机越来越流行,厂商之间的竞争更加激烈,在不同的计算机体系间转换数据变得十分头疼,人们厌烦了这种自定义造成的混乱。

最终,计算机制造商一起制定了一个标准的方法来描述字符。他们定义使用一个字节的低7位来表示字符,例如,字母A是65,c是99,~是126等等, ASCII码就这样诞生了。原始的ASCII标准定义了从0到127 的字符,这样正好能用七个比特表示。不过好景不长,它国家的人趁这个机会开始使用128到255范围内的编码来表达自己语言中的字符。例如,144在阿拉伯人的ASCII码中是گ,而在俄罗斯的ASCII码中是ђ。即使在美国,对于未使用区域也有各种各样的利用。IBM PC就出现了“OEM 字体”或”扩展ASCII码”,为用户提供漂亮的图形文字来绘制文本框并支持一些欧洲字符,例如英镑(£)符号。

勤劳的中国人民也行动起来,我们不客气地把那些127号之后的奇异符号们直接取消掉, 规定:一个小于127的字符的意义与原来相同,但两个大于127的字符连在一起时,就表示一个汉字,前面的一个字节(他称之为高字节)从0xA1用到0xF7,后面一个字节(低字节)从0xA1到0xFE,这样我们就可以组合出大约7000多个简体汉字了。在这些编码里,我们还把数学符号、罗马希腊的字母、日文的假名们都编进去了,连在 ASCII 里本来就有的数字、标点、字母都统统重新编了两个字节长的编码,这就是常说的"全角"字符,而原来在127号以下的那些就叫"半角"字符了。

中国人民看到这样很不错,于是就把这种汉字方案叫做 "GB2312"。GB2312 是对 ASCII 的中文扩展。但是中国的汉字太多了,我们很快就就发现有许多人的人名没有办法在这里打出来,于是我们不得不继续把 GB2312 没有用到的码位找出来老实不客气地用上。

后来还是不够用,于是干脆不再要求低字节一定是127号之后的内码,只要第一个字节是大于127就固定表示这是一个汉字的开始,不管后面跟的是不是扩展字符集里的内容。结果扩展之后的编码方案被称为 GBK 标准,GBK 包括了 GB2312 的所有内容,同时又增加了近20000个新的汉字(包括繁体字)和符号。

后来少数民族也要用电脑了,于是我们再扩展,又加了几千个新的少数民族的字,GBK 扩成了 GB18030。从此之后,中华民族的文化就可以在计算机时代中传承了。

这带来了一点小麻烦:字符串长度和编码的长度不唯一对应,以为常常处理中英夹杂的问题。这也难不倒中国的程序猿们,一个有(di)趣(xiao)的函数就可以解决。另一个稍大点的麻烦是错误的传染性,在某处一个字节的缺失会使随后的其他文章成为乱码!

当各个国家都像中国这样搞出一套自己的编码标准,结果互相之间谁也不懂谁的编码,谁也不支持别人的编码,连大陆和台湾这样只相隔了150海里,使用着同一种语言的兄弟地区,也搓弄出来自己的编码:BIG5。天哪,从此黑暗笼罩在编码领域,工程师和文档管理大师们都念下面这个咒语:“换个编码,这些乱码一定就会好的,芝麻开门吧!”

ISO国际组织实在不能不管了,于是他们在1991年重新搞一个包括了地球上所有文化、所有字母和符号的编码!他们打算叫它"Universal Multiple-Octet Coded Character Set",简称 UCS, 俗称 "UNICODE"。他们雄心勃勃,创立了一个65535个码表的字符集,认为能Cover所有的字符,这个集合就是UCS-2。这里我只能说他们Too young, too simple, sometimes…咳咳。我天朝文字界表示不服,康熙字典就有4万多,再看看我们的词源和辞海,吓死你们!韩国人也表示不服:“汉字是我们韩国人发明的!”,更别提还有别的国家的各种蝌蚪文。幸亏ISO知错能改后面推出UCS-4方案,说简单了就是四个字节来表示一个字符,这样我们就可以组合出21亿个不同的字符出来(最高位有其他用途),这大概将来外星人入侵时可以将外星人的文字也包括进来!

Unicode背后的想法非常简单,然而却被普遍的误解了。Unicode就像一个电话本,标记着字符和数字之间的映射关系。Joel称之为「神奇数字」,因为它们可能是随机指定的,而且不会给出任何解释。官方术语是码位(Code Point),总是用U+开头。理论上每种语言中的每种字符都被Unicode协会指定了一个神奇数字。例如希伯来文中的第一个字母א,是U+2135,字母A是U+0061。Unicode并不涉及字符是怎么在字节中表示的,它仅仅指定了字符对应的数字,仅此而已。他的编码和传输的问题是UTF(Unicode Transformation Formats)定义的,我们通常会遇到的是UTF-8,UTF-16和UTF-32。

低字节序(Little Endian)和高字节序(Big Endian)


Endian读作End-ian或者Indian。这个术语的起源可以追溯到格列佛游记。(小说中,小人国为水煮蛋应该从大的一端(Big-End)剥开还是小的一端(Little-End)剥开而争论,争论的双方分别被称为“大端派”和“小端派”。)字节序方案只是一个微处理器架构设计者的偏好问题,例如,Intel使用低字节序,Motorola使用高字节序。UEFI中当然也是使用Little-endian.

2

UTF-8

UTF-8是一个非常惊艳的概念,它漂亮的实现了对ASCII码的向后兼容,以保证Unicode可以被大众接受。发明它的人至少应该得个诺贝尔和平奖。

在UTF-8中,0-127号的字符用1个字节来表示,使用和US-ASCII相同的编码。这意味着1980年代写的文档用UTF-8打开一点问题都没有。只有128号及以上的字符才用2个,3个或者4个字节来表示。因此,UTF-8被称作可变长度编码。

3

UTF-16

另一个流行的可变长度编码方案是UTF-16,它使用2个或者4个字节来存储字符。如果字符编码小于0x10000,则UTF-16直接用UCS-2的两个字节表示,否则用变换后的四字节表示。变换通过一个特殊保留的区间用来表示大于FFFF的值。这些值会通过4个字节来编码。这里不再详述。

4

UTF-32

定长编码,所有字符都是四个字节。即UCS-4直接搬过来。

5

各种编码方式优缺点以及UEFI的选择

英语国家通常都和字母打交道,使用UTF-32会是一种巨大的浪费,想象一下你的.c程序忽然扩大四倍的样子!用UTF-16也会有些浪费。似乎UTF-8很优秀了?其实还是那个问题,字符串长度和编码长度不一致,导致要做些运算才能得到字符串长度。而UTF-32就方便了,直接字符串长度除以4即可!

UEFI采取了折中的方式,没有采用他们中任何一个,而是采用标准的UCS-2编码,即每个字符占两个字节,字符串长度=编码长度 / 2;(强迫症发作,不得不加个分号) ,对于UTF-16的扩展部分,答案很简单,不支持

字符显示原理

了解完了字符编码,我们接着看看一个字符如何显示出来。UEFI提供GOP或UGA protocol,我们可以用它来显示一幅位图。同样,如果我们有了点阵字库,我们也可以用它来显示字符。幸运的是,UEFI已经替我们考虑好了怎么显示字符,它定义了SimpleFont格式。SimpleFont是一种点阵字体,有两种格式,一种是窄体字,一种是宽体字。窄体字是一种8×19的点阵字库,宽体字是16×19的点阵字库,分成两半,前一半表示左边部分,后一半表示右边部分。点阵字库中每一位(bit)代表一个像素。我们以英文E做例子:

它的编码是:

EFI_NARROW_GLYPH GlyphData[]  = {…

     {0x0045, 0x00,{0x00, 0x00, 0x00, 0xFE, 0x66, 0x62,0x60, 0x68, 0x78, 0x68,0x60, 0x60,    0x62, 0x66, 0xFE, 0x00, 0x00, 0x00, 0x00}},

     …};

我们可以通过HiiAddPackages的形式注册字体点阵到HII的数据仓库中。在EDKII的GraphicConsoleDxe驱动中有现成的例子。窄体字点阵在MdeModulePkg\Universal\Console\GraphicsConsoleDxe\ LaffStd.c

中,注册函数在同一个驱动的GraphicsConsole.c的RegisterFontPackage函数里:

PackageLength   = sizeof (EFI_HII_SIMPLE_FONT_PACKAGE_HDR) + mNarrowFontSize + 4;

  Package = AllocateZeroPool (PackageLength);

  ASSERT (Package != NULL);

 

  WriteUnaligned32((UINT32 *) Package,PackageLength);

  SimplifiedFont = (EFI_HII_SIMPLE_FONT_PACKAGE_HDR *) (Package + 4);

  SimplifiedFont->Header.Length        = (UINT32) (PackageLength - 4);

  SimplifiedFont->Header.Type          = EFI_HII_PACKAGE_SIMPLE_FONTS;

  SimplifiedFont->NumberOfNarrowGlyphs = (UINT16) (mNarrowFontSize / sizeof (EFI_NARROW_GLYPH));

 

  Location = (UINT8 *) (&SimplifiedFont->NumberOfWideGlyphs + 1);

  CopyMem (Location, gUsStdNarrowGlyphData, mNarrowFontSize);


  mHiiHandle = HiiAddPackages (

                 &mFontPackageListGuid,

                 NULL,

                 Package,

                 NULL

                 );

注意Simple Font同时支持宽体和窄体两种。那么汉字应该是什么样呢,我们以“永”字来举个例子:

按照宽体字的编码标准应该编做:

EFI_WIDE_GLYPH gUsStdWideGlyphData[] = {

  { 0x6c38, 0x00, {0x00,0x02,0x01,0x00,0x1F,0x01,0x01,0x7D,0x05,0x05,0x09,0x09,0x11,0x21,0x41,0x05,0x02,0x00,0x00},                  {0x00,0x00,0x00,0x00,0x00,0x08,0x18,0xA0,0xC0,0x40,0x20,0x20,0x10,0x0E,0x04,0x00,0x00,0x00,0x00},  

 {0x00,0x00,0x00}},

     …};

很简单是不是?

实践

1

环境搭建

有了前面的理论我们来验证一下, NT32提供了很好的实验环境。我们先从Github上下载最新的EDKII的源程序。接下来配置编译环境:

Edksetup –nt32

环境设置好后我们编译一下:

Build

一切顺利的话,我们可以运行看看:

Build run

2

编程

到这里,实验环境搭建完成。我们开始进行我们最喜欢的活动吧!打开LaffStd.c,加入“永”的点阵字模:

EFI_WIDE_GLYPH gUsStdWideGlyphData[] = {

  { 0x6c38, 0x00, {0x00,0x02,0x01,0x00,0x1F,0x01,0x01,0x7D,0x05,0x05,0x09,0x09,0x11,0x21,0x41,0x05,0x02,0x00,0x00},              {0x00,0x00,0x00,0x00,0x00,0x08,0x18,0xA0,0xC0,0x40,0x20,0x20,0x10,0x0E,0x04,0x00,0x00,0x00,0x00},  {0x00,0x00,0x00}},

  { 0x0000, 0x00, {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00},                  {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00},  {0x00,0x00,0x00}}            //EOL

};

UINT32 mWideFontSize =  sizeof (gUsStdWideGlyphData);

接下来在GraphicConsoleDxe.c里将其和窄体字一起注册到HII(红色是新加的):

PackageLength   = sizeof (EFI_HII_SIMPLE_FONT_PACKAGE_HDR) + mNarrowFontSize + mWideFontSize + 4;

  Package = AllocateZeroPool (PackageLength);

  ASSERT (Package != NULL);

  WriteUnaligned32((UINT32 *) Package,PackageLength);

  SimplifiedFont = (EFI_HII_SIMPLE_FONT_PACKAGE_HDR *) (Package + 4);

  SimplifiedFont->Header.Length        = (UINT32) (PackageLength - 4);

  SimplifiedFont->Header.Type          = EFI_HII_PACKAGE_SIMPLE_FONTS;

  SimplifiedFont->NumberOfNarrowGlyphs = (UINT16) (mNarrowFontSize / sizeof (EFI_NARROW_GLYPH));

 

  Location = (UINT8 *) (&SimplifiedFont->NumberOfWideGlyphs + 1);

  CopyMem (Location, gUsStdNarrowGlyphData, mNarrowFontSize);

  SimplifiedFont->NumberOfWideGlyphs = (UINT16) (mWideFontSize / sizeof (EFI_WIDE_GLYPH));

  Location += mNarrowFontSize;

  CopyMem (Location, gUsStdWideGlyphData, mWideFontSize);

大功告成,开香槟!Wait a minute,好像需要验证一下。我们从MdeModulePackage的HelloWorld Shell应用下手,加入下面两行到main函数:

  Print (L"I like it!\n");

  Print (L"永\n");

试试看吧。运行NT32环境,进入shell,键入HelloWorld,得到如下:

一切正常!顺便考验一下HII对宽体和窄体字混排会不会出问题,代码改成如下:

  Print (L"I like it!\n");

  Print (L"永\n");

  Print (L"I永like it!\n");

  Print (L"Il永ike it!\n");

再次编译测试,结果如下:

哒哒,HII顺利过关!

后记

这里只举了一个字的字模,其他的字怎么办?有一种办法是在Windows上用程序抓取True Type的字库,将其转换成点阵字库。即写个简单的python程序,将true type的字投影到16×19的方格上,投影的每个格子有色即是1,无色即是0。简单方便,瞬间就可以完成。这里有两个小问题:

1.      True Type字库是矢量字库,放大不会变形。放到我们的小格子里面,有些边边角角需要在修饰一下。可以借助网上免费的小工具,数量也不会太多。

2.      字体是有版权的!不能乱用。我们可以找些Open的字库,将其copy到windows的font目录下即可。

最后要提醒的是,如果用在产品中最好另外写个模块单独注册,不要全部加到GraphicConsoleDxe中去了。


您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存