您好,登錄后才能下訂單哦!
本篇內容介紹了“Web中文字體處理的方法有哪些”的有關知識,在實際案例的操作過程中,不少人都會遇到這樣的困境,接下來就讓小編帶領大家學習一下如何處理這些情況吧!希望大家仔細閱讀,能夠學有所成!
Web 項目中,使用一個合適的字體能給用戶帶來良好的體驗。但是字體文件太多,如果想要查看字體效果,只能一個個打開,非常影響工作效率。因此,需要實現(xiàn)一個功能,能夠根據固定文字以及用戶輸入預覽字體。在實現(xiàn)這一功能的過程中主要解決兩個問題:
中文字體體積太大導致加載時間過長
字體加載完成前不展示預覽內容
現(xiàn)在將問題的解決以及我的思考總結成文。
在聊這兩個問題之前,我們先簡述怎樣使用一個 Web 自定義字體。要想使用一個自定義字體,可以依賴 CSS Fonts Module Level 3 定義的 @font-face
規(guī)則。一種基本能夠兼容所有瀏覽器的使用方法如下:
@font-face { font-family: "webfontFamily"; /* 名字任意取 */ src: url('webfont.eot'); url('web.eot?#iefix') format("embedded-opentype"), url("webfont.woff2") format("woff2"), url("webfont.woff") format("woff"), url("webfont.ttf") format("truetype"); font-style:normal; font-weight:normal; } .webfont { font-family: webfontFamily; /* @font-face里定義的名字 */ }
由于 woff2
、woff
、ttf
格式在大多數瀏覽器支持已經較好,因此上面的代碼也可以寫成:
@font-face { font-family: "webfontFamily"; /* 名字任意取 */ src: url("webfont.woff2") format("woff2"), url("webfont.woff") format("woff"), url("webfont.ttf") format("truetype"); font-style:normal; font-weight:normal; }
有了@font-face
規(guī)則,我們只需要將字體源文件上傳至 cdn,讓 @font-face
規(guī)則的 url
值為該字體的地址,最后將這個規(guī)則應用在 Web 文字上,就可以實現(xiàn)字體的預覽效果。
但這么做我們可以明顯發(fā)現(xiàn)一個問題,字體體積太大導致的加載時間過長。我們打開瀏覽器的 Network 面板查看:
可以看到字體的體積為5.5 MB,加載時間為5.13 s。而夸克平臺很多的中文字體大小在20~40 MB 之間,可以預想到加載時間會進一步增長。如果用戶還處于弱網環(huán)境下,這個等待時間是不能接受的。
那么中文字體相較于英文字體體積為什么這么大,這主要是兩個方面的原因:
中文字體包含的字形數量很多,而英文字體僅包含26個字母以及一些其他符號。
中文字形的線條遠比英文字形的線條復雜,用于控制中文字形線條的位置點比英文字形更多,因此數據量更大。
我們可以借助于 opentype.js
,統(tǒng)計一個中文字體和一個英文字體在字形數量以及字形所占字節(jié)數的差異:
字體名稱 | 字形數 | 字形所占字節(jié)數 |
---|---|---|
FZQingFSJW_Cu.ttf | 8731 | 4762272 |
JDZhengHT-Bold.ttf | 122 | 18328 |
夸克平臺字體預覽需要滿足兩種方式,一種是固定字符預覽, 另一種是根據用戶輸入的字符進行預覽。但無論哪種預覽方式,也僅僅會使用到該字體的少量字符,因此全量加載字體是沒有必要的,所以我們需要對字體文件做精簡。
unicode-range 屬性一般配合 @font-face
規(guī)則使用,它用于控制特定字符使用特定字體。但是它并不能減小字體文件的大小,感興趣的讀者可以試試。
CSS unicode-range特定字符使用font-face自定義字體
fontmin
是一個純 JavaScript
實現(xiàn)的字體子集化方案。前文談到,中文字體體積相較于英文字體更大的原因是其字形數量更多,那么精簡一個字體文件的思路就是將無用的字形移除:
// 偽代碼 const text = '字體預覽' const unicodes = text.split('').map(str => str.charCodeAt(0)) const font = loadFont(fontPath) font.glyf = font.glyf.map(g => { // 根據unicodes獲取對應的字形 })
實際上的精簡并沒有這么簡單,因為一個字體文件由許多
表(table)
構成,這些表之間是存在關聯(lián)的,例如maxp
表記錄了字形數量,loca
表中存儲了字形位置的偏移量。同時字體文件以offset table(偏移表)
開頭,offset table
記錄了字體所有表的信息,因此如果我們更改了glyf
表,就要同時去更新其他表。
在討論 fontmin
如何進行字體截取之前,我們先來了解一下字體文件的結構:
上面的結構限于字體文件只包含一種字體,且字形輪廓是基于 TrueType
格式(決定 sfntVersion
的取值)的情況,因此偏移表會從字體文件的0字節(jié)
開始。如果字體文件包含多個字體,則每種字體的偏移表會在 TTCHeader 中指定,這種文件不在文章的討論范圍內。
偏移表(offset table):
Type | Name | Description |
---|---|---|
uint32 | sfntVersion | 0x00010000 |
uint16 | numTables | Number of tables |
uint16 | searchRange | (Maximum power of 2 <= numTables) x 16. |
uint16 | entrySelector | Log2(maximum power of 2 <= numTables). |
uint16 | rangeShift | NumTables x 16-searchRange. |
表記錄(table record):
Type | Name | Description |
---|---|---|
uint32 | tableTag | Table identifier |
uint32 | checkSum | CheckSum for this table |
uint32 | offset | Offset from beginning of TrueType font file |
uint32 | length | Length of this table |
對于一個字體文件,無論其字形輪廓是 TrueType 格式還是基于 PostScript 語言的 CFF 格式,其必須包含的表有 cmap
、head
、hhea
、htmx
、maxp
、name
、OS/2
、post
。如果其字形輪廓是 TrueType 格式,還有cvt
、fpgm
、glyf
、loca
、prep
、gasp
六張表會被用到。這六張表除了 glyf
和 loca
必選外,其它四個為可選表。
fontmin
內部使用了 fonteditor-core
,核心的字體處理交給這個依賴完成,fonteditor-core
的主要流程如下:
將字體文件轉為 ArrayBuffer
用于后續(xù)讀取數據。
前文我們說到緊跟在 offset table(偏移表)
之后的結構就是 table record(表記錄)
,而多個 table record
叫做 Table Directory
。fonteditor-core
會先讀取原字體的 Table Directory
,由上文表記錄的結構我們知道,每一個 table record
有四個字段,每個字段占4個字節(jié),因此可以很方便的利用 DataView
進行讀取,最終得到一個字體文件的所有表信息如下:
在這一步會根據 Table Directory
記錄的偏移和長度信息讀取表數據。對于精簡字體來說,glyf
表的內容是最重要的,但是 glyf
的 table record
僅僅告訴了我們 glyf
表的長度以及 glyf
表相對于整個字體文件的偏移量,那么我們如何得知 glyf
表中字形的數量、位置以及大小信息呢?這需要借助字體中的 maxp
表和 loca(glyphs location)
表,maxp
表的 numGlyphs
字段值指定了字形數量,而 loca
表記錄了字體中所有字形相對于 glyf
表的偏移量,它的結構如下:
Glyph Index | Offset | Glyph Length |
---|---|---|
0 | 0 | 100 |
1 | 100 | 150 |
2 | 250 | 0 |
… | … | … |
n-1 | 1170 | 120 |
extra | 1290 | 0 |
根據規(guī)范,索引0
指向缺失字符(missing character)
,也就是字體中找不到某個字符時出現(xiàn)的字符,這個字符通常用空白框或者空格表示,當這個缺失字符不存在輪廓時,根據 loca
表的定義可以得到 loca[n] = loca[n+1]
。我們可以發(fā)現(xiàn)上文表格中多出了 extra
一項,這是為了計算最后一個字形 loca[n-1]
的長度。
上述表格中 Offset 字段值的單位是字節(jié),但是具體的字節(jié)數取決于字體
head
表的indexToLocFormat
字段取值,當此值為0
時,Offset 100 等于 200 個字節(jié),當此值為1
時,Offset 100 等于 100 個字節(jié),這兩種不同的情況對應于字體中的Short version
和Long version
。
但是僅僅知道所有字形的偏移量還不夠,我們沒辦法認出哪個字形才是我們需要的。假設我需要字體預覽
這四個字形,而字體文件有一萬個字形,同時我們通過 loca
表得知了所有字形的偏移量,但這一萬里面哪四個數據塊代表了字體預覽
四個字符呢?因此我們還需要借助 cmap
表來確定具體的字形位置,cmap
表里記錄了字符代碼(unicode)
到字形索引的映射,我們拿到對應的字形索引后,就可以根據索引獲得該字形在 glyf
表中的偏移量。
而一個字形的數據結構以 Glyph Headers
開頭:
Type | Name | Description |
---|---|---|
int16 | numberOfContours | the number of contours |
int16 | xMin | Minimum x for coordinate data |
int16 | yMin | Maximum y for coordinate data |
int16 | xMax | Minimum x for coordinate data |
int16 | yMax | Maximum x for coordinate data |
numberOfContours
字段指定了這個字形的輪廓數量,緊跟在 Glyph Headers
后面的數據結構為 Glyph Table
。
在字體的定義中,輪廓是由一個個位置點構成的,并且每個位置點具有編號,這些編號從0
開始按升序排列。因此我們讀取指定的字形就是讀取 Glyph Headers
中的各項值以及輪廓的位置點坐標。
在 Glyph Table
中,存放了每個輪廓的最后一個位置點編號構成的數組,從這個數組中就可以求得這個字形一共存在幾個位置點。例如這個數組的值為[3, 6, 9, 15]
,可以得知第四個輪廓上最后一個位置點的編號是15,那么這個字形一共有16個位置點,所以我們只需要以16
為循環(huán)次數進行遍歷訪問 ArrayBuffer 就可以得到每個位置點的坐標信息,從而提取出了我們想要的字形,這也就是 fontmin
在截取字形時的原理。
另外,在提取坐標信息時,除了第一個位置點,其他位置點的坐標值并不是絕對值,例如第一個點的坐標為[100, 100]
,第二個讀取到的值為[200, 200]
,那么該點位置坐標并不是[200, 200]
,而是基于第一個點的坐標進行增量,因此第二點的實際坐標為[300, 300]
因為一個字體涉及的表實在太多,并且每個表的數據結構也不一樣。這里無法一一列舉
fonteditor-core
是如何處理每個表的。
在使用了 TrueType 輪廓的字體中,每個字形都提供了 xMin
、xMax
、yMin
和 yMax
的值,這四個值也就是下圖的Bounding Box
。除了這四個值,還需要 advanceWidth
和 leftSideBearing
兩個字段,這兩個字段并不在 glyf
表中,因此在截取字形信息的時候無法獲取。在這個步驟,fonteditor-core
會讀取字體的 hmtx
表獲取這兩個字段。
在這一步會重新計算字體文件的大小,并且更新偏移表(Offset table)
和表記錄(Table record)
有關的值, 然后依次將偏移表
、表記錄
、表數據
寫入文件中。有一點需要注意的是,在寫入表記錄
時,必須按照表名排序進行寫入。例如有四張表分別是 prep
、hmtx
、glyf
、head
、則寫入的順序應為 glyf -> head -> hmtx -> prep
,而表數據
沒有這個要求。
fonteditor-core
在截取字體的過程中只會對前文提到的十四張表進行處理,其余表丟棄。每個字體通常還會包含 vhea
和 vmtx
兩張表,它們用于控制字體在垂直布局時的間距等信息,如果用 fontmin
進行字體截取后,會丟失這部分信息,可以在文本垂直顯示時看出差異(右邊為截取后):
在了解了 fontmin
的原理后,我們就可以愉快的使用它啦。服務器接受到客戶端發(fā)來的請求后,通過 fontmin
截取字體,fontmin
會返回截取后的字體文件對應的 Buffer,別忘了 @font-face
規(guī)則中字體路徑是支持 base64
格式的,因此我們只需要將 Buffer 轉為 base64
格式嵌入在 @font-face
中返回給客戶端,然后客戶端將該 @font-face
以 CSS 形式插入 <head></head>
標簽中即可。
對于固定的預覽內容,我們也可以先生成字體文件保存在 CDN 上,但是這個方式的缺點在于如果 CDN 不穩(wěn)定就會造成字體加載失敗。如果用上面的方法,每一個截取后的字體以 base64
字符串形式存在,則可以在服務端做一個緩存,就沒有這個問題。利用 fontmin
生成字體子集代碼如下:
const Fontmin = require('fontmin') const Promise = require('bluebird') async function extractFontData (fontPath) { const fontmin = new Fontmin() .src('./font/senty.ttf') .use(Fontmin.glyph({ text: '字體預覽' })) .use(Fontmin.ttf2woff2()) .dest('./dist') await Promise.promisify(fontmin.run, { context: fontmin })() } extractFontData()
對于固定預覽內容我們可以預先生成好分割后的字體,對于用戶輸入的動態(tài)預覽內容,我們當然也可以按照這個流程:
獲取輸入 -> 截取字形 -> 上傳 CDN -> 生成 @font-face -> 插入頁面
按照這個流程來客戶端需要請求兩次才能獲取字體資源(別忘了在 @font-face
插入頁面后才會去真正請求字體),并且截取字形
和上傳 CDN
這兩步時間消耗也比較長,有沒有更好的辦法呢?我們知道字形的輪廓是由一系列位置點確定的,因此我們可以獲取 glyf
表中的位置點坐標,通過 SVG
圖像將特定字形直接繪制出來。
SVG
是一種強大的圖像格式,可以使用CSS
和JavaScript
與它們進行交互,在這里主要應用了path
元素
獲取位置信息以及生成 path
標簽我們可以借助 opentype.js
完成,客戶端得到輸入字形的 path
元素后,只需要遍歷生成 SVG
標簽即可。
下面附上字體截取后文件大小和加載速度對比表格。可以看出,相較于全量加載,對字體進行截取后加載速度快了145
倍。
fontmin
是支持生成woff2
文件的,但是官方文檔并沒有更新,最開始我使用的woff
文件,但是woff2
格式文件體積更小并且瀏覽器支持不錯
字體名稱 | 大小 | 時間 |
---|---|---|
HanyiSentyWoodcut.ttf | 48.2MB | 17.41s |
HanyiSentyWoodcut.woff | 21.7KB | 0.19s |
HanyiSentyWoodcut.woff2 | 12.2KB | 0.12s |
這是在實現(xiàn)預覽功能過程中的第二個問題。
在瀏覽器的字體顯示行為中存在阻塞期
和交換期
兩個概念,以 Chrome
為例,在字體加載完成前,會有一段時間顯示空白,這段時間被稱為阻塞期
。如果在阻塞期
內仍然沒有加載完成,就會先顯示后備字體,進入交換期
,等待字體加載完成后替換。這就會導致頁面字體出現(xiàn)閃爍,與我想要的效果不符。而 font-display
屬性控制瀏覽器的這個行為,是否可以更換 font-display
屬性的取值來達到我們的目的呢?
Block Period | Swap Period | |
---|---|---|
block | Short | Infinite |
swap | None | Infinite |
fallback | Extremely Short | Short |
optional | Extremely Short | None |
字體的顯示策略和 font-display
的取值有關,瀏覽器默認的 font-display
值為 auto
,它的行為和取值 block
較為接近。
第一種策略是
FOIT(Flash of Invisible Text)
,FOIT
是瀏覽器在加載字體的時候的默認表現(xiàn)形式,其規(guī)則如前文所說。
第二種策略是
FOUT(Flash of Unstyled Text)
,FOUT
會指示瀏覽器使用后備字體直至自定義字體加載完成,對應的取值為swap
。
兩種不同策略的應用:Google Fonts FOIT ?漢儀字庫 FOUT
在夸克項目中,我希望的效果是字體加載完成前不展示預覽內容,FOIT
策略最為接近。但是 FOIT
文本內容不可見的最長時間大約是3s
, 如果用戶網絡狀況不太好,那么3s
過后還是會先顯示后備字體,導致頁面字體閃爍,因此 font-display
屬性不滿足要求。
查閱資料得知,CSS Font Loading API 在 JavaScript
層面上也提供了解決方案:
先看看它們的兼容性:
又是 IE,IE 沒有用戶不用管
我們可以通過 FontFace
構造函數構造出一個 FontFace
對象:
const fontFace = new FontFace(family, source, descriptors)
family
字體名稱,指定一個名稱作為 CSS
屬性 font-family
的值,
source
字體來源,可以是一個 url
或者 ArrayBuffer
descriptors optional
style:font-style
weight:font-weight
stretch:font-stretch
display: font-display
(這個值可以設置,但不會生效)
unicodeRange:@font-face
規(guī)則的 unicode-ranges
variant:font-variant
featureSettings:font-feature-settings
構造出一個 fontFace
后并不會加載字體,必須執(zhí)行 fontFace
的 load
方法。load
方法返回一個 promise
,promise
的 resolve
值就是加載成功后的字體。但是僅僅加載成功還不會使這個字體生效,還需要將返回的 fontFace
添加到 fontFaceSet
。
使用方法如下:
/** * @param {string} path 字體文件路徑 */ async function loadFont(path) { const fontFaceSet = document.fonts const fontFace = await new FontFace('fontFamily', `url('${path}') format('woff2')`).load() fontFaceSet.add(fontFace) }
因此,在客戶端我們可以先設置文字內容的 CSS 為 opacity: 0
,
等待 await loadFont(path)
執(zhí)行完畢后,再將 CSS 設置為 opacity: 1
, 這樣就可以控制在自定義字體加載未完成前不顯示內容。
“Web中文字體處理的方法有哪些”的內容就介紹到這里了,感謝大家的閱讀。如果想了解更多行業(yè)相關的知識可以關注億速云網站,小編將為大家輸出更多高質量的實用文章!
免責聲明:本站發(fā)布的內容(圖片、視頻和文字)以原創(chuàng)、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。