前端字体截取:实战篇

为何要截取字体?
css

众所周知,相对于英文字体,中文字体天生是“庞然大物”。英文字体两三百KB已经很大了,而中文字体几MB十几MB都算小的。一方面,中文字体包含的字形数量极多,动辄数以千计甚至万计,而英文字体则只需包含几十个基本字符和符号,哪怕支持多种语言及字符变体,容量达到三千多个字形已经算很是庞大的了。另外一方面,中文字形的曲折变化复杂度高,在基于轮廓的矢量字体设计中,用于控制中文字形曲线的控制点广泛比英文更多,于是须要的数据量更大,也会致使字体文件膨胀。html

前端开发实践中,为了实现一些特殊视觉效果,常常须要使用某些特殊字体,而用户电脑上几乎不太可能安装这些字体,这时候一般须要使用Web字体技术,让浏览器动态下载咱们的自定义字体。但是中文字体很是庞大,不少时候“全量”加载某个字体文件是不现实的。特别是对于一些动态页面且每一个页面只有少许字符用到该字体的状况下。固然,也不是每一个页面都会用到一个字体文件中的全部字符,全量加载自己也极其浪费。前端

研究代表,3500经常使用中文汉字(中国义务教育9年级须要掌握的汉字数量)便可覆盖平常使用汉字的99.8%:python

  • 500 字(78.53202%)git

  • 1000字(91.91527%)github

  • 1500字(96.47563%)web

  • 2000字(98.38765%)npm

  • 2500字(99.24388%)json

  • 3000字(99.63322%)api

  • 3500字(99.82015%)

可见,最经常使用的前500个汉字的覆盖率已经达到78%。所以,“全量”加载某个字体,特别是中文字体,在当前网络环境下不只浪费流量和时间,并且也是彻底没有必要的。这时候,咱们能够根据网页用到的字符来截取字体的片断,这个技术英文叫subset,也就是“取子集”。

本文首先简单回顾Web自定义字体的技术规范,而后经过实例介绍两种前端经常使用的截取字体的技术。首先是CSS中的unicode-range属性,咱们称之为“软截取技术”,由于它只是在本地既有字体或者浏览器已经下载的字体基础上作一个指向子集的“软连接”,并不能真正减少浏览器下载文件的大小。其次是Node命令行工具glyphhanger,咱们称之为“硬截取技术”,即在服务端从“全量”字体中分离出一个体积相对极小的字体子集,作成Web字体经过Web服务器或CDN下发给浏览器。

不管是“软截取”,仍是“硬截取”,都会用到Web字体和@font-face规则。所以,咱们须要先来了解一下这个基础的Web标准语法。

Web字体与@font-face

为了超越“Web安全字体”的局限,在网页上使用一些用户电脑上不太可能会安装的字体,微软曾率先提出了@font-face规则。这个规则后来进入W3C的CSS Fonts Module Level 31模块,因而就有了前端经常使用的Web自定义字体技术:

@font-face {  font-family: 'MyWebFont';  src: url('webfont.eot'); /* 兼容IE9 */  src: url('webfont.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */       url('webfont.woff2') format('woff2'), /* 最新浏览器 */       url('webfont.woff') format('woff'), /* 较新浏览器 */       url('webfont.ttf') format('truetype'), /* Safari、Android、iOS */       url('webfont.svg#svgFontName') format('svg'); /* 早期iOS */}

示例代码出处:https://css-tricks.com/snippets/css/using-font-face/

固然,上面的代码是几乎能够兼容全部浏览器的方案。大约在两年前,也就是2016年,因为浏览器版本的快速更迭,写成下面这样已是比较现实的了:

@font-face {  font-family: 'MyWebFont';  src: url('myfont.woff2') format('woff2'),        url('myfont.woff') format('woff');}

若是要兼容更多浏览器,那再加上一种几乎全部浏览器都支持的ttf格式则彷佛更稳妥:

@font-face {  font-family: 'MyWebFont';  src: url('myfont.woff2') format('woff2'),       url('myfont.woff') format('woff'),       url('myfont.ttf') format('truetype');}

不过,咱们的最终目标仍是写成这样,即只使用woff2这种自带压缩的格式:

@font-face {  font-family: 'MyWebFont';  src: url('myfont.woff2') format('woff2');}

从技术角度讲,除了直接使用@font-face,还可使用@import规则或link元素导入或加载包含@font-face声明的外部文件:

// 导入@import url(//fonts.googleapis.com/css?family=Open+Sans);// 或者引用<link href='//fonts.googleapis.com/css?family=Open+Sans' rel='stylesheet' type='text/css'>// 实际使用body {  font-family: 'Open Sans', sans-serif;}

打开Google Fonts看一看:https://fonts.googleapis.com/css?family=Open+Sans

以上都是技术规范,至于何时能够过渡到只使用专门针对Web字体优化的压缩格式woff2,应该只是一个时间问题。

回顾完了基础的技术规范和语法,明确了将来的方向,接下来咱们进入实战。先看一下CSS Fonts Module Level 3定义的与@font-face规则配合使用的unicode-range属性,而后再给你们介绍一家有名的国外Web开发公司Filament Group, Inc.2推出的字体截取工具glyphhanger3

unicode-range

unicode-range属性虽然能够算做“字体截取”技术,但它是“软截取”,不是“硬截取”。它相似于一种快捷方式,而不能真正减小浏览器须要下载的字体文件大小。

顾名思义,unicode-range用于指定自定义字体中包含的字符的Unicode码点范围,语法以下:

// CSS@font-face {  font-family: 'Ampersand';  src: local('Times New Roman');  unicode-range: U+26;}div {  font-size: 4em;  font-family: Ampersand, Helvetica, sans-serif;}// HTML<div>Me & You = Us</div>

示例代码出处:https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/unicode-range

以上@font-face规则自定义了一个名为“Ampersand”(英文&符号)的字体,这个字体“截取”自本地字体Times New Roman,而这个字体只包含一个字符:U+2626是英文&符号的十六进制Unicode码点,对应的十进制值是38)。

HTML中div元素根据font-family的指令,依次会应用自定义字体Ampersand(Times New Roman,衬线字体)、Helvetica(无衬线字体)和sans-serif(无衬线)字体族。实际应用效果以下:

Unicode编码扩容到了17个编码平面,每一个平面的容量为65,536,总容量为1,114,112个码点,其中实际分配使用的只有128,237个,约占12%。所以在能够预见的将来,Unicode有足够的空间包含地球上全部文明的字符。

看一个中文字体的例子。假设咱们要用特殊字体突出显示“初唐四杰”之一王勃的千古名篇《滕王阁序》中最有名的那句:“落霞与孤鹜齐飞,秋水共长天一色。”

能够先把这句名句(包括标点)转换成Unicode码点:

字符串转码点可使用如下JavaScript函数:

而后以“隶变”(Libian SC)做为源字体,自定义一个名叫custom的字体,把它应用到.emphasis元素:

// CSS@font-face {  font-family: custom;  src: local(Libian SC);  unicode-range: u+843d,u+971e,u+4e0e,u+5b64,u+9e5c,u+9f50,u+98de,u+ff0c,                  u+79cb,u+6c34,u+5171,u+957f,u+5929,u+4e00,u+8272,u+3002;  font-weight: 500;}.emphasis {  font-family: custom;}// HTML<!--其余句子--><span class="emphasis">落霞与孤鹜齐飞,秋水共长天一色。</span><!--其余句子-->

注意,上面代码中的码点列表为排版阅读方便而人为换了行,实际使用中不要人为换行,以避免形成语法错误。下面的代码示例也同样。

结果以下:

此时,咱们发现标点(逗号和句号)的样式与其余文字不统一,而其余文字使用的是“苹方”(PingFang SC)字体(在Mac上)。是否是能够简单地从前面的码点列表中删除逗号和句号的码点u+ff0cu+3002?这个方案在Safari 十二、Firefox 62中可行,删除码点以后的逗号和句号会继承使用“苹方”字体,可是在Chrome 69中并不奏效。

此外,Chrome彷佛还有一个bug。假设不删除上述码点,而直接在标点左侧输入一个自定义字体中并不包含的字符,Chrome会强制把这个字符显示成自定义字体。看来浏览器的实现仍是有不一致的地方。时间关系,Windows平台下的IE和Edge没有测试,读者能够自行测试一下。

不管如何,咱们能够再定义一个只包含逗号和句号两个字符的自定义字体来解决这个问题:

@font-face {  font-family: punc;  src: local(PingFang SC);  unicode-range: u+ff0c,u+3002;}.emphasis {  font-family:punc, custom;}

这样,即便不删除custom声明中的码点,Chrome、Safari和Firefox也均可以将逗号和句号显示为“苹方”字体了:

注意,不要试图基于英文字体自定义punc字体,由于英文字体中不包含对中文标点符号对应码点的映射。

虽然这个例子明显是自造的,“对中文内容中的某部分中文字符作特殊字体处理,或者是英文字体中部分字符作特殊字体处理”正是unicode-range这种“软截取技术”最适合的应用场景。更多unicode-range的内容,推荐你们看一看张鑫旭老师的文章“CSS unicode-range特定字符使用font-face自定义字体”:(https://www.zhangxinxu.com/wordpress/2016/11/css-unicode-range-character-font-face/)。

使用unicode-range的注意事项:

  • unicode-range能够接收


    • 单个码点:U+26(或u+26)

    • 码点范围:U+0-7FU+0025-00FF

    • 通配符范围:U+4??至关于U+400-U+4FF

    • 逗号分隔的多个值:U+0025-00FF, U+4??

  • unicode-range默认值为:U+0-10FFFF,即所有Unicode字符编码

  • unicode-range的值是码点的字面值或字面值列表,不是字符串


    • 正确:unicode-range: u+ff0c,u+3002;

    • 错误:unicode-range: "u+ff0c,u+3002";

  • unicode-range的值不能有语法错误,好比上面说的不是字符串,以及不能出现多余的逗号:u+ff0c,u+3002,;末尾多了一个逗号)等,出现语法错误的后果是自定义字体会变成源字体的别名,而非基于源字体截取的子集。(固然,经过@font-face定义已有字体全集的别名,也是一种实用的CSS技术,能够参考前面张老师的文章。

  • 转换为码点时确保使用正确的字符,好比前面例子中的“鹜”(u+9e5c)不要错误地使用“骛”(u+9a9b)。

关于unicode-range这种“软截取技术”的使用就介绍这些。接下来咱们介绍“硬截取工具”:glyphhanger。

glyphhanger

glyphhanger是Zach Leatherman(https://www.zachleat.com/web/)为Filament Group(https://www.filamentgroup.com)写的一个.ttf转WOFF/WOFF2等Web字体格式的命令行工具,能够:

  • 抓取远程或本地文件并分析其中包含的文字

  • 将分析结果去重排序并转换为Unicode码点

  • 根据指定的源字体生成对应格式的子集(须要安装另外一个工具,稍后介绍)

  • 同时也生成包含@font-face规则的CSS文件

这个工具很是实用方便,下面咱们就来演示在制做Web字体过程当中glyphhanger的几个典型用法。

首先,全局安装:

npm install -g glyphhanger

用法一:把王勃的千古名句改为“思源宋体”

进入包含例子页面的目录fontSubsetInAction,运行以下命令:

 
  

➜ fontSubsetInAction glyphhanger http://127.0.0.1:8080/index.html --family='custom' --subset=SourceHanSerifCN-Light.ttf --formats=woff2


U+3002,U+4E00,U+4E0E,U+5171,U+5929,U+5B64,U+6C34,U+79CB,U+8272,U+843D,U+957F,U+971E,U+98DE,U+9E5C,U+9F50,U+FF0C

Writing CSS file: SourceHanSerifCN-Light.css

Subsetting SourceHanSerifCN-Light.ttf to SourceHanSerifCN-Light-subset.woff2 (was 12.44 MB, now 3.57 KB)

这里给glyphhanger传入了4个参数。

  • 要分析的远程文件(这里是一个本地Web服务):http://127.0.0.1:8080/index.html

  • --family='custom'指定只分析以上页面中应用了font-family: custom;规则的元素

  • --subset=SourceHanSerifCN-Light.ttf指定使用的源字体,这里为“思源宋体”(Source Han Serif)

  • --formats=woff2指定想要生成的字体子集的目标格式,这里是WOFF2

glyphhanger首先输出了“落霞与孤鹜齐飞,秋水共长天一色。”对应的Unicode码点(包含逗号和句号)。紧接着在当前目录建立了一个名为“SourceHanSerifCN-Light.css”的文件。以后的输出显示,截取的字体叫“SourceHanSerifCN-Light-subset.woff2”,且源字体文件有12.44 MB,子集文件3.57 KB。16个汉字字符就用了3.57 KB,平均每一个字符占228字节,吓人吧?!

不过,比起12.44 MB,3.57 KB已经算极小了。下面,看看glyphhanger帮咱们生成的CSS文件:

/* This file was automatically generated by GlyphHanger 3.0.3 */@font-face {  font-family: custom;  src: url(SourceHanSerifCN-Light-subset.woff2) format("woff2");  unicode-range: U+3002,U+4E00,U+4E0E,U+5171,U+5929,U+5B64,U+6C34,U+79CB,                  U+8272,U+843D,U+957F,U+971E,U+98DE,U+9E5C,U+9F50,U+FF0C;}

直接使用便可,相比以前手工生成码点,这样省事多了。结果以下:

图片

用法二:分析网页用到汉字子集

可能有读者没有注意到,上面例子中glyphhanger输出的码点是按照每一个字符在Unicode编码中的顺序从小到大排序过的。并且,这些码点是在自动去重以后排的序。

“落霞与孤鹜齐飞,秋水共长天一色。”没有重复的字,咱们再看下面这个例子:

 
  

➜ fontSubsetInAction glyphhanger https://lisongfeng.cn/post/dive-into-async-function.html --string


 "#$&'()*+,-./0123456789:;<=>?ACDEFGHIJKLMNOPQRSTUVWXY[]`abcdefghijklmnopqrstuvwxy{|} ©«»—“”…、。一丁三上下不与且两个中串为主么义之乎乘也书了事二于互些交人什仅今介从代以们件任会伟传似但位低住体何做你使例供依便信修倍候值假作停催像儿充先兑入全兮关其典内册再写决况准出函分切列则刚创初利别到制刻前力办功加务动助努包化区升半协单占即却原去参又及反发取受变口句另只叫可各合同名后向吗吧含启呀员呢周味命和品哈哉响哎哥哦啊啥啦喽嘛器回因困围图在地坑块型基塞境增处备复外多大夫失头奇套好如始姐媒媲子字存它完定实家容对导封将小少尔尝就尾层屈展属山崩嵌工己已布带帮常干年并序库应底度建开异式引张强当彻往征待很得循微心必快念态怎思急性总恢息悉悖悲情想意感成我或户所手才打执扩扰找承把抛抢护抽拒括拼拿持按挠捕换据探接控推描提摸操擎支收改效救数文料断新方无既早时明易是显普景智暂更替最月有未末本机杂束条来构析果某查标样核根格案梦检概模次止正此步段每比毕毫永求无法注洞活流浏消涩深添溃满漏演漫点烦烫然照熟版特状独狱环现理甚甜生用由界疼疾白的目直相省看真眼着知码础确示神种秒积称程稍穷立竟端笔符第笼等答简算管箭类糖系索级纯线组细终绍经结给绝绞统继续维综编网置美翻考者而聪肉背能脚自至致节芋苦荒获虽蛮行补表被装要见规览解触计认让议记讲论设访证评译试话该详语误说请诺读调谋象负责败质费资赋越足踩身转载较辑达迅过迈运返还这进远迭述退送适逆逐递通速造逻遍道那部都释里重量鉴针链错键长问闲间队阻际限除随隐集需非靠面页顺须题风饰饱首香驾验高麻默!(),:;?~

第一个参数是一个“真正的”远程网页:https://lisongfeng.cn/post/dive-into-async-function.html,是我以前写的文章“小哥哥小姐姐,来尝尝Async函数这块语法糖”的网址。那篇文章全文接近5000字,但能够看到,通过分析去重以后,实际用到的只有604个汉字。另外,这里也使用另外一个参数--string让glyphhanger把Unicode码点转换为字符串输出,是按照码点从小到大排序的。

字符串去重其实很简单,下面这个简单的JavaScript函数就能够搞定:

function textEliminateDuplicationAndSorting(text) {    return text.split('').filter((value, index, self) => {      return self.indexOf(value) === index;    }).sort().join('')}

用法三:指定文本或码点生成字体子集

固然,若是你有现成的文本或码点,也能够只让glyphhanger帮你生成相应源字体的子集和CSS文件。好比我想把“渔舟唱晚,响穷彭蠡之滨,雁阵惊寒,声断衡阳之浦。”也显示为“思源宋体”:

 
  

➜ fontSubsetInAction glyphhanger --whitelist="落霞与孤鹜齐飞,秋水共长天一色。渔舟唱晚,响穷彭蠡之滨,雁阵惊寒,声断衡阳之浦。" --subset=SourceHanSerifCN-Light.ttf --css


U+3002,U+4E00,U+4E0E,U+4E4B,U+5171,U+54CD,U+5531,U+58F0,U+5929,U+5B64,U+5BD2,U+5F6D,U+60CA,U+65AD,U+665A,U+6C34,U+6D66,U+6E14,U+6EE8,U+79CB,U+7A77,U+821F,U+8272,U+843D,U+8821,U+8861,U+957F,U+9633,U+9635,U+96C1,U+971E,U+98DE,U+9E5C,U+9F50,U+FF0C

Subsetting SourceHanSerifCN-Light.ttf to SourceHanSerifCN-Light-subset.ttf (was 12.44 MB, now 13.02 KB)

Subsetting SourceHanSerifCN-Light.ttf to SourceHanSerifCN-Light-subset.zopfli.woff (was 12.44 MB, now 9.13 KB)

Subsetting SourceHanSerifCN-Light.ttf to SourceHanSerifCN-Light-subset.woff2 (was 12.44 MB, now 7.45 KB)

Writing CSS file: SourceHanSerifCN-Light.css


@font-face {

  src: url(SourceHanSerifCN-Light-subset.woff2) format("woff2"),

          url(SourceHanSerifCN-Light-subset.zopfli.woff) format("woff"),

          url(SourceHanSerifCN-Light-subset.ttf) format("truetype");

  unicode-range: U+3002,U+4E00,U+4E0E,U+4E4B,U+5171,U+54CD,U+5531,U+58F0,U+5929,

              U+5B64,U+5BD2,U+5F6D,U+60CA,U+65AD,U+665A,U+6C34,U+6D66,U+6E14,

            U+6EE8,U+79CB,U+7A77,U+821F,U+8272,U+843D,U+8821,U+8861,U+957F,

            U+9633,U+9635,U+96C1,U+971E,U+98DE,U+9E5C,U+9F50,U+FF0C;

}

这一次使用--whitelist参数传入了要截取的汉字,省略了--formmats,增长了--css参数。

从结果能够看到,glyphhanger仍是对文字进行了去重、转码点和排序。并且,在没有指定--formats的状况下,生成了.ttfwoffwoff2三种格式的字体子集,这是为了提升对浏览器的兼容性。最后,除了例行生成CSS文件,--css选项还让glyphhanger把CSS文件的内容输出到了控制台,便于复制。

可是要注意,CSS文件和输出都没有包含font-family属性,也就是没有自定义字体的名字(custom),使用时必须本身手工加上。好,结果以下:

图片

安装pyftsubset

glyphhanger自己只作了网页抓取和分析,实际的字体截取使用的是一个著名的Python包fonttools:https://github.com/fonttools/fonttools。安装方法以下:

 
  

pip install fonttools


# Additional installation for --flavor=woff2

git clone https://github.com/google/brotli

cd brotli

python setup.py install


# Additional installation for --flavor=woff --with-zopfli

git clone https://github.com/anthrotype/py-zopfli

cd py-zopfli

git submodule update --init --recursive

python setup.py install

文章最后,为了便于你们参考,咱们给出glyphhanger的帮助信息,你们能够本身去探索更多好玩的用法:

 
  

➜ fontSubsetInAction glyphhanger -h

glyphhanger error: requires at least one URL or whitelist.


usage: glyphhanger ./test.html

       glyphhanger http://example.com

       glyphhanger https://google.com https://www.filamentgroup.com

       glyphhanger http://example.com --subset=*.ttf

       glyphhanger --whitelist=abcdef --subset=*.ttf


arguments:

  --version

  --whitelist=abcdef

       A list of whitelist characters (optionally also --US_ASCII).

  --string

       Output the actual characters instead of Unicode code point values.

  --family='Lato,monospace'

       Show only results matching one or more font-family names (comma separated, case insensitive).

  --json

       Show detailed JSON results (including per font-family glyphs for results).

  --css

       Output a @font-face block for the current data.

  --subset=*.ttf

       Automatically subsets one or more font files using fonttools `pyftsubset`.

  --formats=ttf,woff,woff2,woff-zopfli

       woff2 requires brotli, woff-zopfli requires zopfli, installation instructions: https://github.com/filamentgroup/glyphhanger#installing-pyftsubset


  --spider

       Gather local URLs from the main page and navigate those URLs.

  --spider-limit=10

       Maximum number of URLs gathered from the spider (default: 10, use 0 to ignore).

  --timeout

       Maximum navigation time for a single URL.

[全文完]

相关文章
相关标签/搜索