浏览器环境下,网页从加载到其中文本渲染,可分为 HTML 解析->字体匹配->字体渲染3个部分。
一、HTML 解析
HTML 解析分为字节流解码(Byte Stream Decoder)、输入流预处理(Input Stream Preprocessor)、标记分析(Tokenizer)、DOM 树构建(Tree Constructon),详细过程见 W3C HTML 解析 或 WHATWG HTML 解析。
字节流解码
因历史原因,不同区域和语言的页面可能使用不同的编码方式,导致浏览器在解析前需先确定编码,W3C 推荐的编码是 UTF-8。
现代浏览器是依据 W3C 规范的一套编码嗅探算法(Encoding Sniffing Algorithm),以确定首次解码文档时要使用的字符编码。当 HTML 解析器解码输入字节流时,它会使用字符编码和置信度。置信度分为暂定、 确定和无关紧要三种。解析过程中会根据所使用的编码以及该编码的置信度(暂定或确定)来决定是否需要更改编码。如果不需要编码,则置信度无关紧要。
输入流预处理
即规范化换行。
标记分析和 DOM 树构建
标记分析即输出各种 DOCTYPE 标记、HTML 5 元素的起始和结束标记、注释、文件结束符。每个标记生成后,立即由 DOM 树构建调度器处理,以生成 DOM 树。如果遇到阻塞脚本样式表或正执行脚本,则标记分析中止,待解除阻塞或脚本执行完后再继续。
DOM 中的文本由字符构成的。相同特定用途的字符组成一个字符集(character set,也称为字符库 repertoire)中。为了明确地指代字符,每个字符都与一个数字相关联,称为代码点(code point)。字符在计算机中以一个或多个字节(bytes)的形式存储。
DOM 中的文本可能是中文、英文,或阿拉伯文等多种语言混和。lang 属性不仅可在 HTTP 头、html 或者 meta ,用于标记整个文档的全局语言,还可能在页面内元素上,比如
简体普通话
为了统一处理这些复杂的情况,需要将文本分为由不同语言组成的小段,在有的文本布局引擎里,这个步骤称为 itemize,分解后的文本段常被称作 text run,但是具体划分的规则可能根据不同的引擎有所区别,比如 HarfBuzz 和 ICU 一般是根据要使用的不同排版类来划分 (常称作 shaper),比如英语和法语可能使用同一个 shaper 排版,那么相邻的英语和法语文本就会划分到同一个 run 里,而希伯来文需要另一个 shaper,就划分到它自己的 run 里。
不少浏览器还会在这个划分下面,在确定具体使用的字体之后,根据使用字体的不同划分更细的 run,比如 SimpleTextRun,最后把它们逐一交给 shaper 进行排版得到要绘制的字形。
开发建议
声明内容应完全位于文件开头的前1024个字节内,因此最好将其放在 head 元素开始标记之后。
<head>
<meta charset="utf-8">
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
二、字体匹配
字体是字形(glyph)定义的集合,即用于显示字符的形状的定义。
浏览器识别出要处理的字符后,就会在字体中查找可用于显示或打印这些字符的字形。 给定的字体通常只包含一个字符集,如果字体中缺少某个特定字符的字形,一些浏览器会在系统的其他字体中查找缺失的字形(这意味着该字形看起来会与周围的文本不同)。否则,通常会看到一个方框、问号或其他字符来代替。
根据 CSS 确定字体(CSS3 字体匹配算法 ),例如
<p>A <strong>XX</strong> ruler.</p>
p { font-family: Helvetica, Arial, sans-serif; }
p strong { font-weight: bold; }
表示这个段落里优先使用 Helvetica 这个 family 的字体,如果找不到,就找 Arial,如果还是找不到,就用浏览器设置的默认非衬线字体。稍微复杂的“XX”,它应该继承父元素的 font-family, 也用 Helvetica,但不用默认的 Regular,而用 Bold 版本,假如找不到 Helvetica Bold,就找 Arial Bold,否则就找浏览器默认设置的 Bold 版本,假如都没有呢?就要考虑用人工伪造的方式来显示粗体了。
中文字体按照英文字体一样的规则来判断:逐个字符尝试当前的字体是否提供了针对该字符的字形,如果没有则尝试下一个,要是到了最后都没找到匹配的字体呢?CSS 规范里只简单的说执行“system font fallback”,但这个过程在不同的浏览器下可能很不一样。
具体的字体选择还有一些不太容易注意的细节,也是各个浏览器差异比较大的一点。
- 是否支持用 PostScript name(格式通常为“家族名-样式名”)选择:如 FZHei-B01S;
- 是否支持用 Full font name(格式通常为“家族名 样式名”)选择:如何将 CSS 里对字体的 font-weight 或者 font-style 等要求映射到特定的字体上,尤其是在字体使用了非标准的 style 命名的情况下,考虑到很多厂商有自己的字体命名规则,像 Helvetica Neue 的 UltraLight, Light, Regular, Medium, Bold 这些不同的 weight,是怎么对应到 CSS font-weight 的 100 到 900 数值上的。
- 是否支持按 localized name 选择:如用宋体来代表 SimSun。
开发建议
- 首先确定元素使用的字体风格,比如是衬线字体、非衬线字体还是 cursive、fantasy 之类的;
- 确定了风格之后,先选择西文字体,多个平台共有的字体应该尽量放在后边,独有的字体放在前面,比如 Mac OS X 下有 Helvetica 也有 Arial,但 Helvetica (可能) 效果更好,Windows 下则一般只有 Arial,那么写 Helvetica, Arial 就比 Arial, Helvetica 或者只有 Arial 更好;
- 然后列出中文字体,原则相同。
- 最后还是应该放上对应的 generic family,比如 sans-serif 或者 serif。
- 尽量用字体的基本名称,而不要用本地化过的名称。Mac OS X 下字体名称可以用 Font Book 查到 (菜单 Preview -> Show Font Info),Windows 下字体信息在微软的网站可以得到,Linux/X11 下可以使用 fc-list 命令查到。
- 虽然可随意调整字体大小,但字体的设计都考虑到了特定的(且有限的)尺寸和使用场景。
三、字体渲染
当确定了字体以后,就可以将文本、字体等等参数一起交给具体的排版引擎,生成字形和位置,然后根据不同的平台调用不同的字体 rasterizer 将字形转换成最后显示在屏幕上的图像。每个操作系统都包含一个或多个文本渲染引擎,每个浏览器则控制着使用哪个渲染引擎。比如iOS、Mac OS 下用 Core Text(工作在 Quartz 2D 之上),Linux/X11 下用 FreeType,Windows 下用 GDI/DirectWrite。
不同版本的操作系统和浏览器所使用的渲染引擎可能也存在差异,因此我们不能期望所有浏览器(即使在同一系统)的渲染效果完全相同。
在 Windows 系统中,字体格式对渲染效果有着显著的影响。字体文件包含字体的矢量轮廓,有两种格式:PostScript (轮廓由三次曲线构成) 或 TrueType (轮廓由二次曲线构成)。EOT 和 .ttf 文件总是包含 TrueType 字体,而 .otf 字体通常基于 PostScript。WOFF 可以包含两种字体格式。
字体微调(hinting)可以帮助字体的矢量轮廓与像素网格保持一致,其操作范围很广,从细微的调整到针对特定尺寸、像素级的精确指令都可以实现。