1.文字渲染的简单理解
渲染图像,可以理解为用cpu/gpu构造出原本不存在的图像。比如输入计算机的英文字符都是ASCII码,而我们在屏幕上看到显示的字符对应的应该是RGB/YUV的像素。计算机把ASCII字符转化成像素的过程就是文字渲染。又比如我们GPU用多个2D图片渲染出3D图片,图片亮度根据时间自动调整阳光的强度,阴影等。原图像中不存在的信息,这个是GPU的渲染。
渲染图像和sonser直接采样进来的RGB/YUV图像帧显示是不一样的。
cpu/gpu要显示字符需要以下的信息:
- 从键盘采样ASCII码
- 应用程序(word,terminal等)设定的字体属性 包括 字体,颜色,大小,背景
- 字符显示的位置----来自输入时候的光标指定的位置
cpu/gpu根据上面的信息,渲染出一个RGB帧,然后把这帧图像送给DISPALY做叠加显示
2.计算机系统是如何显示一个字符的
经过上面的简单描述,我们接下来详细描述文字渲染的过程。以下内容摘自知乎大神们的回答。
两位答主侧重点不同,乌鸦侧重于在Linux+Xorg系统下整个处理流程;孙笑凡侧重于字体与字符的编码
原问题链接为:
计算机系统是如何显示一个字符的? - 知乎
2.1知乎“乌鸦大神的回答”
作者:乌鸦
链接:https://www.zhihu.com/question/24340504/answer/28902204
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
我来就 Linux + Xorg 系统回答一下吧。我不知道,也没办法知道 Windows 上的情况是怎么样的。我的回答在细节方面可能不会太详细,一方面是因为细节并不影响理解,另一方面也是因为我对一些细节的了解也不是那么深。
从按键到字符在屏幕上显示,其实是一个很复杂的过程……我粗略估计了一下,这个过程中执行的指令,对应到源代码,可能要超过10万行。
那,就从按下键开始讲起吧。当你按下键后,一个电信号从键盘通过 USB 接口或者 PS/2 接口送到了总线,然后引起了一个中断。如果是在老机子上,管理中断的可能是8259A 芯片,新的机子上则可能是 APIC 。CPU 收到中断之后,会暂停当前的程序的执行,调用事先设定好的中断处理过程。Linux系统的中断处理一般分为两个部分,Top half 和 bottom half。其中直接响应中断的是 top half,因为系统不能在中断处理过程中停太久,所以 top half 只是记录中断的发生,真正中断处理过程是在 bottom half 中完成的。
bottom half 在 top half 之后执行,具体执行取决于进程调度。在 bottom half 中内核会找来负责的驱动,比如说 USB HID 驱动,处理键盘输入。这个键盘输入会反映在 /dev 下面某一个字符设备中,比如说 /dev/input/event0 。Xorg 为了获取键盘输入,会对这个设备进行 select() 。select 这个系统调用会将进程 block ,直到有可用输入。在内核中的反应就是,内核会将进程的状态设为 TASK_INTERRUPTIBLE ,然后调用 schedule() 将运行权让给别的进程。当键盘输入到来的时候,内核会查看是否有进程正在 select /dev/input/event0 这个设备,如果有,内核会将对应的进程从select 中唤醒,以便处理这个输入。
于是Xorg就被唤醒了,我们也终于从 kernel space 里逃了出来,来到了 user space 。Xorg 会从 /dev/input/event0 读出一组数据。这组数据的定义如下:
struct input_event {
struct timeval time;
unsigned short type;
unsigned short code;
unsigned int value;
};
time 是事件发生的时间,type 是事件类型,对应键盘就是 EV_KEY ,code 在这种情况下是一个 key code,value 则代表是按下键还是放开键。
Xorg 在读到事件之后,还需要将来自 kernel 的 key code 翻译成 Xorg 自己的 key code 。完成这个的是 libxkb ,键盘布局也是在这个阶段起作用的。然后,Xorg 会找出当前焦点所在的窗口,然后将事件发给它——这是简单的情况。如果你运行着一个窗口管理器的话,这个事件很可能还需要在窗口管理器中过一遍,并且由窗口管理器决定是否要发给窗口。如果接受按键的程序支持输入法,这个按键可能还要在输入法里走一圈。这个过程中的事件发送/接受都是通过 socket ,使用的协议是 Xorg 的协议,使用的库(归根结底)一般是 xlib 或者 libxcb 。
(补充一段关于编码的)
应用程序在接到 Xorg 发来的事件之后,就会对事件作出反应。比如说一个文本编辑器,在接到按下‘X’的事件之后,需要在文件中插入一个‘X’。那么这个‘X’是怎样在文件中表示的呢?你应该知道,内存也好硬盘也好,是用二进制来保存数据的。要表示一个‘X’,我们必须先给它分配一个对应的数字。幸好,早在近60年前,就有人帮我们把这件事办好了。这就是所谓的 ASCII 码。‘X’在其中对应的数字是120。
但是,如果我通过我的输入法,输入了一个中文字的话怎么办?当然,我们还是用一样的办法,给每个中文字一个编号。这个编号,在中国的国标码中被称作区位码,在 unicode 中被称作一个code point。比如说,国标码给“啊”字编的号就是1601。
虽然现在我们给中文字也编上了号,我们还不知道应该怎么把它存到文件中去。你可能会觉得这很简单,直接把编号存进去不就是了?比如1601就存两个字节0x06和0x41。但是这会造成一个问题。假如你的文本编辑器同时支持ASCII码和我们设计的这个中文编码,它要怎样才能知道某个文件是按什么编码的?0x16和0x41在 ASCII 中都是合法编码,这样就会造成一个文件存在歧义。不过幸好,ASCII 只使用了0~127来编码字符,所以只要我们只用128~255,就可以和ASCII区分开来。比如说,我们可以将编号按7位二进制分段,然后在第一位添上一个1:
1601 -> 11001000001 -> 0001100/1000001 -> 10001100 11000001 -> 0x8c 0xc1
在国标码里,这个编码后的结果就是这个字符的内码,注意内码和区位码的区别。而 unicode 和 UTF8,UTF16 也是类似。unicode 是一种给字符编号的方法,UTF8、UTF16则是把这个编号记录到文件里的方法。
到这里,我们的旅程才完成了一半。而且是比较简单的一半。
现在你的输入已经跑完前半程,终于到达应用程序了,不过等等——这个程序,是什么程序?是一个终端虚拟器,文本编辑器,还是chrome里头的某个文本框?取决于对这个问题的回答,后半程的路线会非常不一样。
假如说这个程序是 xterm ,它会通过 X core font API ,将这个字符直接送给 Xorg ,由 Xorg 来完成字体渲染(当然现在 xterm 也支持 libXft 了,不过先不管这个)。Xorg 拿到应用程序送来的请求之后,会在字体中检索每个字对应的字形,然后渲染出来。这个检索过程中用到的索引,就是我们之前给字符编上的号。如果要指定字体,需要向Xorg提供一个长得像这样:“-adobe-times-medium-r-normal--12-120-75-75-p-64-iso8859-1” 的字符串来指定字体。当然,这样渲染出来的字体不会很好看,像这样:
(我不是很清楚 X core font 是否有反锯齿一类的能力,应该是没有,欢迎纠正。)
如果这个应用程序是 gedit,那么它就会选择自己渲染字体,然后将渲染结果直接发给 Xorg,由 Xorg 呈现。在 Linux 下,你想用这种方式渲染字体,有两个库你是逃不过的:fontconfig 和 FreeType2 。大致上来说,fontconfig 让你能通过一个字体名找到对应的字体,FreeType2 则进行具体的渲染。
那么字体是怎么被渲染的呢?不管你用的字体是TrueType还是OpenType还是什么格式,从根本上来说,(除掉点阵字体)都是一些矢量图的集合。渲染字体,只是把矢量图画出来而已——说的轻松!
渲染文字是个复杂的工作,我也不能说我对此了解很深,只能大致讲讲。
字体如果是显示在屏幕上,因为屏幕的dpi和纸张没法比,所以必须反锯齿,否则就会这样:
这只是通过灰度进行反锯齿,我们还可以做的更好。如果你是在使用LCD显示器的话,你屏幕上的像素,长的可能是这个样子的(图片来源,wikipedia):
灰度反锯齿只是在整个像素尺度上进行的,但是既然像素中还可以分为更小的成分,那么我们为什么不通过这更小的单位进行反锯齿?这种方法就叫做 Subpixel Rendering,或者一个更被人熟知的叫法:ClearType。下面是两种反锯齿的比较,图还是来自 Wiki :
可以看到 subpixel 渲染的结果会让边缘带一点色彩。一般来说 subpixel 渲染可以字体看起来更清晰一点,你可在自己的系统上开启/关闭 subpixel redenering 比较一下。
人们为了让字体在屏幕上看起来能清晰一点,真是用尽了心思。因为反锯齿会让字体的边缘变得模糊,让人觉得字体看起来不是那么“锐利”,人们又想出来要给字体做Hinting(不知道如何翻译)。Hinting将字体微调,让横和竖都对齐像素,来尽量减少反锯齿带来的模糊。下面是个(应该一下就能看明白的)例子(图还是来自 Wiki):
Hinting 的具体实现是很复杂的。因为不同字体需要不同的 Hinting 对策,所以字体中会存一些 Hinting 数据来指导 Hinting。而这些数据,每一个都是一个用特定汇编语言写成的小程序!FreeType2中则包含了一个虚拟机,来运行这些程序,来将字体对齐到像素。
当然也有些字体会不带 Hinting 数据,但是 FreeType2 还是可以进行 Hinting ,也就是 Auto-hinting。具体的算法我就不清楚了。
值得一提的是,因为 Apple 拿着很多有关 Hinting 的专利,FreeType2 默认是关闭 Hinting 的,而只使用 Auto-hinting 。直到2011年这些专利全都过期之后,FreeType2才默认支持 Hinting 。
好了,不管你是让Xorg进行字体渲染,还是让应用程序自己进行渲染,最后的结果都是一小块比特图。我们要怎么把这块图显示在屏幕上呢?
Xorg 之中,有一个部件叫做 EXA,是用来提供硬件加速的2D渲染的。EXA的历史很长,最早可以追溯到1996年 Harm Hanemaayer 给 Xorg 实现的 XAA。XAA 后来被 EXA 所接替。现在 EXA 又开始要被 UXA 取代。这些部件虽然都有同样的目的,但是实现都有所不同。具体的区别在这里就不展开了。总的来说,他们的功能是,在显卡驱动的帮助下,将二维图形显示在屏幕上。
到这里,好像就结束了。但其实还没有。2011年的光棍节,Glamor诞生了。Glamor试图将2D的渲染也用 OpenGL 来完成。一张二维图,大概会被转变成 OpenGL 的贴图,然后渲染在屏幕上。目前这个项目还在开发中,性能(在大部分情况下)还不如EXA。
如果所有的渲染都由 OpenGL 也就罢了。如果你还在用 EXA,但是你的某个程序(比如说Chrome)非想用 OpenGL 来显示文字怎么办?显然的办法可以是让程序把所有的 OpenGL 请求都发给 Xorg,让 Xorg 代理。这种方法就叫做间接渲染(Indirect rendering),Xorg 的一个扩展:AIGLX,就是用来完成这个工作的。
有一个额外的中介大概是会影响性能。虽然我没有具体的数据来证明这一点,不过既然后来 DRI 的出现大概是一个佐证。DRI 是 Direct Rendering Infrastructure 的缩写,分为两个部分,分别存在于内核与 Xorg 中。内核的部分叫做 DRM,用来协调多个程序对 GPU 的访问;Xorg 的部分则是一个驱动,也叫 DRI 。程序想要使用 OpenGL ,先要通过 DRI 向 Xorg 申请一块缓冲区,然后通过 DRM 访问 GPU 向这个缓冲区进行渲染。当然这些事不会直接让程序去做,都是通过 Mesa 这个 GL 库完成的。
到这里,还是没有结束。
万一你不幸有个带 NVIDIA® Optimus™ 的本子怎么办?使用 Optimus 技术的独显不会直接和显示器连接,所以想要直接让独显渲染是不行的。现有的解决方案是 BumbleBee,就是之前那个删了用户 /usr 目录,出了点小名的那个。BumbleBee 的做法是再启动一个独立的、跑在独显上的 Xorg,然后用它进行渲染。但是独显没法输出到屏幕,怎么办呢——只好把渲染结果在两个 Xorg 之间传来传去了。BumbleBee 用了 VirtualGL 来完成这个任务。
另外一个还在开发中的解决方案叫做 Prime(开发者就是喜欢和变形金刚过不去)。Prime 利用的是一个还在开发中的内核功能:DMA-BUF,让两个硬件共享一块内存区域。我不清楚具体的原理,不过大概是让一块显卡渲染完之后,DMA 写到内存里,再让连接显示器的显卡 DMA 读出,输出到屏幕吧。
用来 Optimus 以后,感觉本来的长跑,直接就变成了马拉松……
以上差不多就是全部了。不知道你们是不是还有兴趣,要是把场景换成 Wayland ,还有很多可以讲。
2.2知乎大神“孙笑凡”的回答
作者:孙笑凡
链接:https://www.zhihu.com/question/24340504/answer/29927340
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
正巧,今天下午我也遇到了难缠的编码问题,既然前面乌鸦朋友已经介绍的非常详细的计算机底层原理,我就从另一个侧面谈谈计算机编码及乱码问题产生的根源。今天写了这样一段文章,就拿出来和大家分享。
从编码问题的产生说起
我们知道,计算机是美国人发明的,人家的英语体系总从来就只有26个英文字母和一些数字、特殊字符等,为了储存文字信息,于是使用了最早的ascii码进行字符编码。而后来由于计算机的普及,多国语言文字变得重要起来,于是多语言的特性成为了计算机的必备,各国进行各国的国家标准编码,中国的便是GB2312(1980年),而后1995年又颁布了《汉字编码扩展规范》(GBK),GBK与GB2312相兼容,但又增加了一些兼容汉字,方便了和Big5码等进行转换。这套GBK编码,逐渐成为了中国计算机的主流编码。
Unicode字符集和UTF-8编码
随着计算机的发展,往往一款软件不但要兼容一个国家的语言,还要兼容许多国家的语言,尤其是亚洲国家中日韩三国,光中文常用字符就有7000,所以要解决这么多文字的编码问题,就需要用更多规模的表示,Unicode是一个巨大的字符集,囊括了世界各地的语言,这样编码就更为统一和方便了。但Unicode并没有规定这些字符具体应该怎么样在计算机中存储,虽然字符集是全的,但计算机中要兼顾效率和方便等问题,UCS-2是其中的一种常用方案,采用两个字节来编码一个字符,这和ascii码不兼容,而且还有一个大问题,UCS-2并不能表示全部的汉字,汉字的简繁体加起来总共有六七万,UCS-2只有65536个编码,根本存不下,所以只收入了大部分常用字。也有表示所有汉字的方案UCS-4,不过4个字节用来存储汉字,效率就较为低下。
UTF是“UCS Transformation Format”的缩写,是Unicode字符集的一类高效实现方式。
UTF-8是我个人比较喜欢的一种编码方式,它是一种针对Unicode的可变长度字符编码,又称为万国码。它使用1-6个字节进行编码,最前面128个编码内容和ascii码兼容,所以我们用UTF-8编写纯英文文本是和ascii码几乎一样的。(注意我说的是几乎)。
如果UNICODE字符由2个字节表示,则编码成UTF-8很可能需要3个字节。而如果UNICODE字符由4个字节表示,则编码成UTF-8可能需要6个字节。用4个或6个字节去编码一个UNICODE字符可能太多了,但这样的UNICODE字符往往是生僻字,极为少见。
这种编码的思想和哈夫曼编码是类似的,将高频字符缩短,低频字符变长,使得整体的编码效率更优。
我以前有段时间,就经常将Unicode和UTF-8搞混了,Unicode是字符集,网上不严格的情况也指Unicode字符集的常用编码UCS-2,也就是用两个字节编码的Unicode码。
一些常见系统的编码:
VC 6.0 Ascii码
VS2008 - 2013 UCS-2(开启Unicode后)
Windows中文版 GBK
Linux(andorid) UTF-8
Mac OX UTF-8
JAVA UCS-2
Python2 ascii (byte)
Python3 UCS-2
注:操作系统所述都是默认编码,这三种主流操作系统的系统编码都是可以更改的。
MBCS(Multi-Byte Character Set)和内码表(codepage)
再介绍两个字符集中较为深入的概念,MBCS和CodePage。
MBCS(Multi-Byte Chactacter System,即多字节字符系统),是所有多字节编码方案的总称,MBCS 编程主要用于为国际市场编写的应用程序。由于往往是针对一国市场只有一种文字,那么为了节约资源,往往将这些文字用双字节或尽量少的字节的方式进行保存。
因为这些双字节文字和ANSI是混和在一起的,为了加以区别,Windows将这些字符的最高位置为1(即这些双字节文字的每个字节都>=127),所以这种表示法可以表示 127x127 约一万多种非ANSI文字 ,其本上可以表示任何一种语言的常用文字了。于是,Windows为每一个区域版本,都制定了分别独立的文字编码,这就是MBCS(多字节码)。
而这些分页后的编码方式,都被保存成了不同的CodePage(内码表,这里内码的意思是机器内部编码,相对于外码,外部输入文字用的编码,例如拼音、五笔、郑码等),例如中文就是大家熟知的CP936。要注意,这种编码方式是早期windows独有的,由于使用较早,应用也十分广泛,而CP936和GB2312-80在编码上则是几乎一样的(此处见Wiki百科——汉字内码扩展规范),后来扩展GBK后,CP936也进行了同样的扩展。
此技术的使用最早追溯到MS-DOS3.3(1987年4月发行)向IBMPC用户引进了内码表的概念,Windows也使用此概念。
这是微软官网上给的10081页的CodePage
内码表成为了系统进行多语言编码及转换的重要工具。当然,Unicode码也被收入CodePage中。
说白了,MBCS和CodePage就是计算机解决多字节编码问题的通用手段,各种区域编码可以在其中找到对应编码的映射的,Unicode和UTF-8也是可以在其中找到对应的映射的。
常见的CodePage
932 —日文
936 —简体中文(GBK)
949 —韩文
950 —繁体中文(大五码)
1200 —UCS-2LE Unicode 小端序
1201 —UCS-2BE Unicode 大端序
1252 —西欧拉丁字母ISO-8859-1.
65000 — UTF-7 Unicode
65001 — UTF-8 Unicode
计算机是如何显示文字的呢?
计算机要对文字进行存储后就需要显示出来,而我们的液晶屏都是一个个的像素点组成的,这就必须要对文字进行渲染绘制,发送到显卡中进行栅格化和显示等操作。
Dos下最简单,利用主板BIOS就能对ascii码进行点阵化输出。简单的我们就不再多谈,windows下是如何对文字进行绘制显示的呢?
字库
我们都知道,显示字体除了要有文字的编码,还需要有显示用的字的样式,这就是 字库,windows下的fonts文件夹大家应该都十分熟悉,更改或删除字库就是就是文件的移动而已。而字库实际上也有不同的种类的,构建原理也不都相同。
最早的字库是点阵字库,这种字库看起来和黑白图片貌似没有什么大区别,就是记录像素是黑还是白,我们用windows自带的字库编辑程序,就可以处理这种字库。
这种字库的缺点也是显而易见的,首先是缩放不易,在小字体和大字体显示时都容易失真走样,而且占用空间较大。
矢量轮廓字库,这种字库是用的矢量图原理进行存储的,将外部边界抽象成数学上的矢量线段,可以方便的缩放旋转等操作。缺点则是连续性不好,放大后就变出行了折痕,效果不够理想。
曲线轮廓字库,这是通过直线和曲线段共同构造文字的方法,往往使用Bezier曲线对轮廓进行拟合,效果非常好,字体平滑美工,但惟独就是计算较为费时间。
TrueType字形技术
TrueType采用几何学中二次B样条曲线及直线来描述字体的外形轮廓。由一些数学算法进行对应大小字体的生成,无论放大或缩小,字符总是光滑的。
TrueType字体与PostScript字体、OpenType字体是主要的三种计算机矢量字体(又称轮廓字体、描边字体),后缀.ttf。
虽然计算较快,但相比于PostScript字体,质量要差一些,特别是文字太小时,不够清晰。
所以字库一般会对小字体和常用打印字号制作对应的点阵字库,保证其精度,其他情况则使用TrueType字体。
WOFF–WebOpen Font Format (.woff)
WOFF(Web开发字体格式)是一种专门为了Web而设计的字体格式标准,实际上是对于TrueType/OpenType等字体格式的封装,每个字体文件中含有字体以及针对字体的元数据(Metadata),字体文件被压缩,以便于网络传输。
其实大家就会发现了,网络上很火的图标字体,实际上就是根据最基础的字库技术生成出来的。
字体的显示流程
介绍到这里,大家应该对整个字体的绘制过程有个整体的认识了,首先是经过字符编解码,将硬盘中存储的有对应编码的文本文件进行加载,例如Java的文件IO,变成内存中的字符串对象,也就是符合本语言字符串存储特性的数据,(当然如果你愿意,也可以当做二进制读入,然后再手段转换编码,也是可以的),绘制时则首先调用对应的CodePage进行编码的索引查找,找到对应的字体字形索引,然后根据字形索引获取到字库中的数据,根据系统提供的绘制曲线绘制样条线的方法进行图形渲染,这样就能得到显示器上的图像了。而更高级的就是对字体进行反走样,锐化,等更为细节的操作了。
本文好多内容是基于我自己的理解写成,如有疏漏错误,还请指正。