溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

Web中文字體處理的方法有哪些

發(fā)布時間:2022-02-25 10:17:42 來源:億速云 閱讀:139 作者:iii 欄目:開發(fā)技術

本篇內容介紹了“Web中文字體處理的方法有哪些”的有關知識,在實際案例的操作過程中,不少人都會遇到這樣的困境,接下來就讓小編帶領大家學習一下如何處理這些情況吧!希望大家仔細閱讀,能夠學有所成!

背景介紹

Web 項目中,使用一個合適的字體能給用戶帶來良好的體驗。但是字體文件太多,如果想要查看字體效果,只能一個個打開,非常影響工作效率。因此,需要實現(xiàn)一個功能,能夠根據固定文字以及用戶輸入預覽字體。在實現(xiàn)這一功能的過程中主要解決兩個問題:

  • 中文字體體積太大導致加載時間過長

  • 字體加載完成前不展示預覽內容

現(xiàn)在將問題的解決以及我的思考總結成文。

Web中文字體處理的方法有哪些

使用 web 自定義字體

在聊這兩個問題之前,我們先簡述怎樣使用一個 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、woffttf 格式在大多數瀏覽器支持已經較好,因此上面的代碼也可以寫成:

@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 面板查看:

Web中文字體處理的方法有哪些

可以看到字體的體積為5.5 MB,加載時間為5.13 s。而夸克平臺很多的中文字體大小在20~40 MB 之間,可以預想到加載時間會進一步增長。如果用戶還處于弱網環(huán)境下,這個等待時間是不能接受的。

一、中文字體體積太大導致加載時間過長

1. 分析原因

那么中文字體相較于英文字體體積為什么這么大,這主要是兩個方面的原因:

  1. 中文字體包含的字形數量很多,而英文字體僅包含26個字母以及一些其他符號。

  2. 中文字形的線條遠比英文字形的線條復雜,用于控制中文字形線條的位置點比英文字形更多,因此數據量更大。

我們可以借助于 opentype.js,統(tǒng)計一個中文字體和一個英文字體在字形數量以及字形所占字節(jié)數的差異:

字體名稱字形數字形所占字節(jié)數
FZQingFSJW_Cu.ttf87314762272
JDZhengHT-Bold.ttf12218328

夸克平臺字體預覽需要滿足兩種方式,一種是固定字符預覽, 另一種是根據用戶輸入的字符進行預覽。但無論哪種預覽方式,也僅僅會使用到該字體的少量字符,因此全量加載字體是沒有必要的,所以我們需要對字體文件做精簡。

2. 如何減小字體文件體積

unicode-range

unicode-range 屬性一般配合 @font-face 規(guī)則使用,它用于控制特定字符使用特定字體。但是它并不能減小字體文件的大小,感興趣的讀者可以試試。

  • CSS unicode-range特定字符使用font-face自定義字體

fontmin

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 如何進行字體截取之前,我們先來了解一下字體文件的結構:

Web中文字體處理的方法有哪些

上面的結構限于字體文件只包含一種字體,且字形輪廓是基于 TrueType 格式(決定 sfntVersion 的取值)的情況,因此偏移表會從字體文件的0字節(jié)開始。如果字體文件包含多個字體,則每種字體的偏移表會在 TTCHeader 中指定,這種文件不在文章的討論范圍內。

偏移表(offset table):

TypeNameDescription
uint32sfntVersion0x00010000
uint16numTablesNumber of tables
uint16searchRange(Maximum power of 2 <= numTables) x 16.
uint16entrySelectorLog2(maximum power of 2 <= numTables).
uint16rangeShiftNumTables x 16-searchRange.

表記錄(table record):

TypeNameDescription
uint32tableTagTable identifier
uint32checkSumCheckSum for this table
uint32offsetOffset from beginning of TrueType font file
uint32lengthLength of this table

對于一個字體文件,無論其字形輪廓是 TrueType 格式還是基于 PostScript 語言的 CFF 格式,其必須包含的表有 cmap、head、hheahtmx、maxpname、OS/2post。如果其字形輪廓是 TrueType 格式,還有cvt、fpgmglyf、loca、prep、gasp 六張表會被用到。這六張表除了 glyfloca 必選外,其它四個為可選表。

fontmin 截取字形原理

fontmin 內部使用了 fonteditor-core,核心的字體處理交給這個依賴完成,fonteditor-core 的主要流程如下:

Web中文字體處理的方法有哪些

1. 初始化 Reader

將字體文件轉為 ArrayBuffer 用于后續(xù)讀取數據。

2. 提取 Table Directory

前文我們說到緊跟在 offset table(偏移表) 之后的結構就是 table record(表記錄),而多個 table record 叫做 Table Directoryfonteditor-core 會先讀取原字體的 Table Directory,由上文表記錄的結構我們知道,每一個 table record 有四個字段,每個字段占4個字節(jié),因此可以很方便的利用 DataView 進行讀取,最終得到一個字體文件的所有表信息如下:

Web中文字體處理的方法有哪些

3. 讀取表數據

在這一步會根據 Table Directory 記錄的偏移和長度信息讀取表數據。對于精簡字體來說,glyf 表的內容是最重要的,但是 glyftable record 僅僅告訴了我們 glyf 表的長度以及 glyf 表相對于整個字體文件的偏移量,那么我們如何得知 glyf 表中字形的數量、位置以及大小信息呢?這需要借助字體中的 maxp 表和 loca(glyphs location) 表,maxp 表的 numGlyphs 字段值指定了字形數量,而 loca 表記錄了字體中所有字形相對于 glyf 表的偏移量,它的結構如下:

Glyph IndexOffsetGlyph Length
00100
1100150
22500
n-11170120
extra12900

根據規(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 versionLong version。

但是僅僅知道所有字形的偏移量還不夠,我們沒辦法認出哪個字形才是我們需要的。假設我需要字體預覽這四個字形,而字體文件有一萬個字形,同時我們通過 loca 表得知了所有字形的偏移量,但這一萬里面哪四個數據塊代表了字體預覽四個字符呢?因此我們還需要借助 cmap 表來確定具體的字形位置,cmap 表里記錄了字符代碼(unicode)到字形索引的映射,我們拿到對應的字形索引后,就可以根據索引獲得該字形在 glyf 表中的偏移量。

Web中文字體處理的方法有哪些

而一個字形的數據結構以 Glyph Headers 開頭:

TypeNameDescription
int16numberOfContoursthe number of contours
int16xMinMinimum x for coordinate data
int16yMinMaximum y for coordinate data
int16xMaxMinimum x for coordinate data
int16yMaxMaximum 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 是如何處理每個表的。

4. 關聯(lián)glyf信息

在使用了 TrueType 輪廓的字體中,每個字形都提供了 xMin、xMaxyMinyMax 的值,這四個值也就是下圖的Bounding Box。除了這四個值,還需要 advanceWidthleftSideBearing 兩個字段,這兩個字段并不在 glyf 表中,因此在截取字形信息的時候無法獲取。在這個步驟,fonteditor-core 會讀取字體的 hmtx 表獲取這兩個字段。

Web中文字體處理的方法有哪些

5. 寫入字體

在這一步會重新計算字體文件的大小,并且更新偏移表(Offset table)表記錄(Table record)有關的值, 然后依次將偏移表、表記錄、表數據寫入文件中。有一點需要注意的是,在寫入表記錄時,必須按照表名排序進行寫入。例如有四張表分別是 prep、hmtx、glyfhead、則寫入的順序應為 glyf -> head -> hmtx -> prep,而表數據沒有這個要求。

fontmin 不足之處

fonteditor-core 在截取字體的過程中只會對前文提到的十四張表進行處理,其余表丟棄。每個字體通常還會包含 vheavmtx 兩張表,它們用于控制字體在垂直布局時的間距等信息,如果用 fontmin 進行字體截取后,會丟失這部分信息,可以在文本垂直顯示時看出差異(右邊為截取后):

Web中文字體處理的方法有哪些

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 是一種強大的圖像格式,可以使用 CSSJavaScript 與它們進行交互,在這里主要應用了 path 元素

獲取位置信息以及生成 path 標簽我們可以借助 opentype.js 完成,客戶端得到輸入字形的 path 元素后,只需要遍歷生成 SVG 標簽即可。

3. 減小字體文件體積的優(yōu)勢

下面附上字體截取后文件大小和加載速度對比表格。可以看出,相較于全量加載,對字體進行截取后加載速度快了145 倍。

fontmin 是支持生成 woff2 文件的,但是官方文檔并沒有更新,最開始我使用的 woff 文件,但是 woff2 格式文件體積更小并且瀏覽器支持不錯

字體名稱大小時間
HanyiSentyWoodcut.ttf48.2MB17.41s
HanyiSentyWoodcut.woff21.7KB0.19s
HanyiSentyWoodcut.woff212.2KB0.12s

二、字體加載完成前不展示預覽內容

這是在實現(xiàn)預覽功能過程中的第二個問題。

在瀏覽器的字體顯示行為中存在阻塞期交換期兩個概念,以 Chrome 為例,在字體加載完成前,會有一段時間顯示空白,這段時間被稱為阻塞期。如果在阻塞期內仍然沒有加載完成,就會先顯示后備字體,進入交換期,等待字體加載完成后替換。這就會導致頁面字體出現(xiàn)閃爍,與我想要的效果不符。而 font-display 屬性控制瀏覽器的這個行為,是否可以更換 font-display 屬性的取值來達到我們的目的呢?

font-display


Block PeriodSwap Period
blockShortInfinite
swapNoneInfinite
fallbackExtremely ShortShort
optionalExtremely ShortNone

字體的顯示策略和 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 層面上也提供了解決方案:

FontFace、FontFaceSet

先看看它們的兼容性:

Web中文字體處理的方法有哪些

Web中文字體處理的方法有哪些

又是 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í)行 fontFaceload 方法。load 方法返回一個 promise,promiseresolve 值就是加載成功后的字體。但是僅僅加載成功還不會使這個字體生效,還需要將返回的 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è)相關的知識可以關注億速云網站,小編將為大家輸出更多高質量的實用文章!

向AI問一下細節(jié)

免責聲明:本站發(fā)布的內容(圖片、視頻和文字)以原創(chuàng)、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。

web
AI