再谈中文字体的子集化与动态建立字体

其实在项目中用中文字体子集化已经好久了,在刚接受到项目时真的让用户去下载全量字体的方式也早已被废除。现在终于有时间将它整理成文。算是对这件事情的一个基本告终吧。javascript

为何要截取字体?

众所周知,相对于英文字体,中文字体就是一个“庞然大物”。英文字体 200~300KB 已经很大了,而中文字体 动戈 10~30MB
这主要是两个方面的缘由:css

  1. 中文字体包含的字形数量极多 英文字体则只需包含几十个基本字符和符号。有些中文字体还要包括韩语和日语的字形。
  2. 中文字形的曲折变化复杂度高,用于控制中文字形曲线的控制点广泛比英文更多,因为数据量不同,字体大小也天然就有这样的膨胀了

可是需求老是有的,在一些特殊的视觉效果,或者是在一些富文本(如海报设计类)的编辑场景下,特殊字体的支持更是必不可少的。 可是一个中文字体 10~20Mb 我网站可能支持100种字体,你让用户都全量下载显然是不可能的!而且也不是每一个页面都会用到一个字体文件中的全部字符,全量加载自己也极其浪费。html

在《通用汉字表》中一级表肯定 3500 经常使用中文汉字(中国义务教育9年级须要掌握的汉字数量)便可覆盖平常使用汉字的99.8%前端

如何使用自定义字体。

在真正开始以前,咱们先来回顾一下,如何去让一个文本使用自定义字体。这里咱们会聊到 @font-face,这就是咱们目前前端最经常使用的Web自定义字体技术。java

示例代码:https://css-tricks.com/snippets/css/using-font-face/
这里取了其中一个最全的方案,基本上可以兼容到全部的浏览器。git

@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 */
}
<!--使用-->
.newfont {
    font-family: 'MyWebFont';
}
复制代码

固然除了直接使用 @font-face ,还可使用 @import 规则或 link 元素导入或加载包含 @font-face 声明的外部文件:github

使用 google open font (360 奇舞 cdn 的 google font 镜像web

// 导入
@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;
}
复制代码

关于字体如何使用就简单介绍到这,网上也已经有不少各类各样的教程。再也不过多赘述。 其实目前 iconfont.cn 这类字体图标的网站就是这样的技术。redis

字体如何截取?

1. unicode-range

unicode-range 是一个 CSS 属性,通常和 @font-face 规则一块儿使用。它只是在本地既有字体或者浏览器已经下载的字体基础上作一个指向子集的“软连接”,并不能真正减少浏览器下载文件的大小。chrome

对于这种技术因为并不能真正的减小字体大小,因此也不在这我篇文章的范围内。给两个参考连接给你们观看了解。

2. 全量字体精简

即在服务端从“全量”字体中分离出一个体积相对极小的字体子集,作成 webfont 经过 Web 服务器或 CDN 下发给浏览器。

这里须要介绍笔者 fork 以后修改的一个库: font-carrier2
项目 fork 自 font-carrier。 因为 font-carrier 有很长时间无人维护,可是我又有需求。而后就特此开一个新分支。作一些特性的更新与 bug 的修复。

下面给出一种精简中文字体的方式。

var fontCarrier2 = require('font-carrier2')
var transFont = fontCarrier2.transfer('./test/test.ttf')
// 会自动根据当前的输入的文字过滤精简字体
transFont.min('我是精简后的字体,我能够重复')
// 产生一个新字体
transFont.output({
  path: './test/minFont'
})
复制代码

使用新字体:(这样这个新字体中只有《我是精简后的字体,我能够重复》这几个字)

@font-face {
    font-family: 'minFont';
    src:url('./test/minFont.eot'); /* IE9 */
    src: url('./test/minFont.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */
    url('./test/minFont.woff2') format('woff2'),
    url('./test/minFont.woff') format('woff'),
    url('./test/minFont.ttf') format('truetype'), /* chrome、firefox、opera、Safari, Android, iOS 4.2+*/
    url('./test/minFont.svg#iconfont'); /* iOS 4.1- */
  }
复制代码

能够看到咱们这里很简单的就将一个中文字体给子集化了,那么关于 font-carrier2 如何去子集化一个字体咱们也简单介绍到这。下面咱们来进入重头戏:究竟是如何作到精简的

字体解析。(font-carrier2 基本思路剖析)

关于如何解析一个字体的话,其实都是有对应规范的:这个是其中一个规范的描述。microsoft-The OpenType Font File
其实也就是咱们如何从一个二进制的流(固然会转化成 buffer )中,转化成一我的类可读的对象。(psd.js(一个解析psd为json的库,其实也是在作一个相似的事情。))
这一步 opentype.js 已经帮咱们作得很好了。 他可以解析 ttf otf woff 三种文件格式解析为一个 font 类。那么咱们拿到这个 font 类 以后就能够去作咱们任何想作的事情了。那么对于一个 webfont 来讲有哪些是最关键的呢?

1.解读字体内容

// 其实咱们就用这些东西足够去建立一个字体了
// 首先咱们使用 opentype 解析一个字体文件读取以后的 buffer 。
var font = opentype.parse(toArrayBuffer(fs.readFileSync('font.tff'))) 
// 这些内容能够在 opentype.js 官网中看到详细信息
var hhea = font.tables.hhea // Horizontal Header table
var head = font.tables.head // Font Header table
var name = font.tables.name  // 存储了原字体 名称相关信息。处理 fontFamily 
var glyphs = font.glyphs.glyphs // 重点(存储了全部的 字形的列表。
复制代码

2.生成一个简单的 fontObjs 数据对象

var _ = require('lodash')
 var fontObjs = {
      options: {
        id: name.postScriptName.en || 'iconfont',
        horizAdvX: hhea.advanceWidthMax || 1024,
        vertAdvY: head.unitsPerEm || 1024
      },
      fontface: {
        fontFamily: name.fontFamily.en || 'iconfont',
        ascent: hhea.ascender,
        descent: hhea.descender,
        unitsPerEm: head.unitsPerEm
      },
      glyphs: {}
    }
    var path, unicode
    _.each(font.glyphs.glyphs, function(g) {
      try {
        path = g.path.toPathData()
        if (_.isArray(g.unicodes)) {
          _.each(g.unicodes,function(_unicode){
            unicode = '&#x' + (_unicode).toString(16) + ';'
            if(unicode === '&#x20;' || unicode === '&#x2005;' || path){
                fontObjs.glyphs[unicode] = {
                    d: path,
                    unicode: unicode,
                    name: g.name || 'uni' + _unicode,
                    horizAdvX: g.advanceWidth,
                    vertAdvY: fontObjs.options.vertAdvY
                }
            }
          })
        }
      } catch (e) {}
    })
复制代码

3.glyphs 精简。 glyphs 这个时候已是一个对象了。 key 为 文字对应的 unicodevalue 其实是一个 svg 字体中对应 glyphs 的信息。具体能够查看:MDN - SVG 字体 里面 glyphs 对应的部分。若是须要精简的话 那么咱们其实只要从这个 glyphs 对象里面 提取所须要文字对应的 unicode 就好了。
4.转化成 svg 字体。这个其实就是将 上面 提到的 fontObjs,和须要提取的文字精简事后的 glyphs 转化成 MDN - SVG 字体。这个其实也是 fontCarrier2 中比较重要的部分。
5.生成各类字体。fontCarrier2 就是直接先生成一个 svg 的字符串,而后经过 svg2ttf 转化成 ttf buffer 。(本着很少次重复造轮子的原则。在网上能够找到各类字体转化的库 好比 svg2ttf ttf2woff.. 等。而后再经过 ttf2woff/ttf2woff2 等.. 转化成其余的字体文件。(这样固然性能不是最高的。不过实现会快不少。)
6.在前端使用 font-face + font-family 引用新的字体。

那么 font-carrier2 的基本思路剖析 咱们就到这了。经过上面这些步骤咱们就实现了一个中文字体的子集化。下面咱们再聊聊动态建立字体思路。

动态建立字体

先来看一下 在 font-carrier2 中如何经过空白字体去建立文字。具体效果能够在库中 test/index.html 看到。

其实这个图看完。结合咱们以前咱们看的 font-carrier2 处理流程。 咱们动态建立字体的思路就很明确了。

  1. 解析字体 获得 fontObjs ,(options & fontface & glyphs)。
  2. fontObjs 存下来(各类存储方式任选:内存/文件/redis/数据库...)
  3. 前端发送请求。( font-family和对应的文字("simplified":"纯空白 迷你简硬笔楷书 字体测试1,2,3"
  4. 服务端接受到请求。经过 将接受到的文字转化成 unicode, 而后再经过 font-family,取到 options & fontface & glyphs 对应的值。建立一个新字体。返回给前端。
  5. 前端接受到返回。建立 font-face 插入到 style 插入 html
  6. 你还能经过 fontfaceobserver 这个库来监听字体是否生效。( canvas 的 fillText 不会在字体更新后自动刷新
  7. 而后就是正常使用了。

在笔者目前的项目中使用的是上述的流程。

不过也非固定,第 4 步以后 是一个分支流程。
经过后端去建立字体可能对服务端形成较大压力。因为咱们去建立一个字体的基本信息都存下来了。
那么其实也能够后端只作存储相关的工做。 经过在浏览器直接操做 ArrayBufferblob (其实 opentype.js把这个也实现了)利用客户的浏览器去生成字体(目前市面上调研到几个作子集化公司付费解决方法。

文章到这就基本结束了。相信看下来应该对中文字体的子集化应该会有一个基本上的了解。
font-carrier2opentype.js 还有不少特色没介绍到。剩下的就交给各位本身去想象了。

本文首发在博客中 blog.guowenfh.com/2019/06/04/…

相关文章
相关标签/搜索