再談中文字體的子集化與動態建立字體

其實在項目中用中文字體子集化已經好久了,在剛接受到項目時真的讓用戶去下載全量字體的方式也早已被廢除。現在終於有時間將它整理成文。算是對這件事情的一個基本告終吧。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/…

相關文章
相關標籤/搜索