爲何要截取字體?
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規則。這個規則後來進入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碼點範圍,語法以下:
// 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+26(26是英文&符號的十六進制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+ff0c和u+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-7F,U+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是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的狀況下,生成了.ttf、woff和woff2三種格式的字體子集,這是爲了提升對瀏覽器的兼容性。最後,除了例行生成CSS文件,--css選項還讓glyphhanger把CSS文件的內容輸出到了控制檯,便於複製。
可是要注意,CSS文件和輸出都沒有包含font-family屬性,也就是沒有自定義字體的名字(custom),使用時必須本身手工加上。好,結果以下:
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.
[全文完]