搭配本機字型使用進階字體排版

瞭解如何使用 Local Font Access API 存取使用者在本機安裝的字型,並取得這些字型的低階詳細資料

網頁安全字型

如果您從事網頁開發工作已有一段時間,可能還記得所謂的「網頁安全字型」。這些字型幾乎適用於所有最常用的作業系統 (即 Windows、macOS、最常見的 Linux 發行版本、Android 和 iOS)。2000 年代初期,Microsoft 甚至主導了一項名為「網頁專用 TrueType 核心字型」的計畫,提供這些字型免費下載,目標是「只要您造訪指定這些字型的網站,就能看到網站設計師預期的頁面」。可以,包括以 Comic Sans MS 設定的網站。以下是經典的網頁安全字型堆疊 (最終備用字型為任何字型)sans-serif

body {
  font-family: Helvetica, Arial, sans-serif;
}

網頁字型

網頁安全字型的重要性已大不如前。如今,我們有網頁字型,其中有些甚至是可變字型,可透過變更各種公開軸的值進一步調整。您可以在 CSS 開頭宣告 @font-face 區塊,指定要下載的字型檔案,即可使用網路字型:

@font-face {
  font-family: 'FlamboyantSansSerif';
  src: url('flamboyant.woff2');
}

完成後,您就可以照常指定 font-family,使用自訂網頁字型:

body {
  font-family: 'FlamboyantSansSerif';
}

本機字型做為指紋向量

大多數網路字型都來自網路。不過,有趣的是,除了 url() 函式之外,@font-face 宣告中的 src 屬性也會接受 local() 函式。這樣就能在本機載入自訂字型 (沒錯!)。如果使用者在作業系統上安裝了 FlamboyantSansSerif,系統會使用本機副本,而不是下載字型:

@font-face {
  font-family: 'FlamboyantSansSerif';
  src: local('FlamboyantSansSerif'), url('flamboyant.woff2');
}

這種做法可提供良好的備援機制,有助於節省頻寬。很遺憾,網路上無法有美好的事物。local() 函式的問題在於可能會遭到濫用,用於瀏覽器指紋辨識。結果發現,使用者安裝的字型清單可能相當具有識別力。許多公司都會在員工的筆電上安裝自家企業字型。舉例來說,Google 的企業字型是 Google Sans

macOS 的「字體簿」應用程式,顯示 Google Sans 字型的預覽畫面。
Google 員工筆電上安裝的 Google Sans 字型。

攻擊者可以測試是否存在大量已知的公司字型 (例如 Google Sans),藉此判斷使用者所屬的公司。攻擊者會嘗試在畫布上算繪這些字型中設定的文字,並測量字元。如果字形與公司字型的已知形狀相符,表示攻擊者得手。如果字形不相符,攻擊者就會知道系統使用了預設的替代字型,因為公司字型未安裝。如要進一步瞭解這類及其他瀏覽器指紋攻擊,請參閱 Laperdix 等人調查論文

除了公司字型,光是已安裝字型的清單就可能洩漏身分。這個攻擊媒介的情況日益嚴重,因此 WebKit 團隊最近決定「只納入 [在可用字型清單中] 網頁字型和作業系統隨附的字型,但不納入使用者在本機安裝的字型」。(我這裡有一篇文章,說明如何授予本機字型存取權。)

Local Font Access API

本文開頭的內容可能讓你感到負面。我們真的不能擁有美好的事物嗎?別擔心,我們認為可以,或許一切都還有希望。不過,請先容我回答您可能有的疑問。

既然有網路字型,為什麼還需要本機字型存取權 API?

過去,在網路上提供專業品質的設計和圖像工具並不容易。其中一個障礙是無法存取及使用設計師在本機安裝的各種專業建構和提示字型。網路字型可支援部分發布用途,但無法透過程式輔助方式存取點陣化工具用來算繪字元外框的向量字元形狀和字型表。同樣地,您也無法存取網路字型的二進位資料。

  • 設計工具需要存取字型位元組,才能自行實作 OpenType 版面配置,並允許設計工具在較低層級掛鉤,以執行字元形狀的向量篩選器或轉換等動作。
  • 開發人員可能為要帶到網路上的應用程式使用舊版字型堆疊。如要使用這些堆疊,通常需要直接存取字型資料,但網頁字型不會提供這類資料。
  • 部分字型可能未取得授權,無法透過網路傳送。舉例來說,Linotype 某些字型的授權僅包含桌面用途

為解決這些難題,我們推出了 Local Font Access API。這項功能包含兩個部分:

  • 字型列舉 API,可讓使用者授予存取權,存取所有可用的系統字型。
  • 從每個列舉結果中,要求低層級 (以位元組為導向) 的 SFNT 容器存取權,包括完整字型資料。

瀏覽器支援

Browser Support

  • Chrome: 103.
  • Edge: 103.
  • Firefox: not supported.
  • Safari: not supported.

Source

如何使用 Local Font Access API

特徵偵測

如要檢查是否支援 Local Font Access API,請使用:

if ('queryLocalFonts' in window) {
  // The Local Font Access API is supported
}

列舉本機字型

如要取得本機安裝的字型清單,請呼叫 window.queryLocalFonts()。第一次執行時,系統會觸發權限提示,使用者可以核准或拒絕。如果使用者允許查詢本機字型,瀏覽器會傳回含有字型資料的陣列,供您進行迴圈作業。每個字型都以 FontData 物件表示,並具有 family (例如 "Comic Sans MS")、fullName (例如 "Comic Sans MS")、postscriptName (例如 "ComicSansMS") 和 style (例如 "Regular") 屬性。

// Query for all available fonts and log metadata.
try {
  const availableFonts = await window.queryLocalFonts();
  for (const fontData of availableFonts) {
    console.log(fontData.postscriptName);
    console.log(fontData.fullName);
    console.log(fontData.family);
    console.log(fontData.style);
  }
} catch (err) {
  console.error(err.name, err.message);
}

如果只對部分字型感興趣,也可以新增 postscriptNames 參數,根據 PostScript 名稱篩選字型。

const availableFonts = await window.queryLocalFonts({
  postscriptNames: ['Verdana', 'Verdana-Bold', 'Verdana-Italic'],
});

存取 SFNT 資料

您可以使用 FontData 物件的 blob() 方法,取得完整的 SFNT 存取權。SFNT 是一種字型檔案格式,可包含其他字型,例如 PostScript、TrueType、OpenType、Web Open Font Format (WOFF) 字型等。

try {
  const availableFonts = await window.queryLocalFonts({
    postscriptNames: ['ComicSansMS'],
  });
  for (const fontData of availableFonts) {
    // `blob()` returns a Blob containing valid and complete
    // SFNT-wrapped font data.
    const sfnt = await fontData.blob();
    // Slice out only the bytes we need: the first 4 bytes are the SFNT
    // version info.
    // Spec: https://docs.microsoft.com/en-us/typography/opentype/spec/otff#organization-of-an-opentype-font
    const sfntVersion = await sfnt.slice(0, 4).text();

    let outlineFormat = 'UNKNOWN';
    switch (sfntVersion) {
      case '\x00\x01\x00\x00':
      case 'true':
      case 'typ1':
        outlineFormat = 'truetype';
        break;
      case 'OTTO':
        outlineFormat = 'cff';
        break;
    }
    console.log('Outline format:', outlineFormat);
  }
} catch (err) {
  console.error(err.name, err.message);
}

示範

如要查看 Local Font Access API 的實際應用情形,請參閱示範。請務必查看原始碼。這個範例展示名為 <font-select> 的自訂元素,可實作本機字型挑選器。

隱私權注意事項

"local-fonts" 權限會顯示,提供高度可指紋辨識的介面。不過,瀏覽器可以自由傳回任何內容。舉例來說,注重匿名性的瀏覽器可能會選擇只提供瀏覽器內建的一組預設字型。同樣地,瀏覽器也不必提供與磁碟上完全相同的表格資料。

在可行範圍內,Local Font Access API 的設計宗旨是只公開啟用上述用途所需的確切資訊。系統 API 可能會產生已安裝字型的清單,但不會隨機或依序排列,而是按照字型安裝順序排列。如果傳回的已安裝字型清單與這類系統 API 提供的清單完全相同,可能會洩漏可用於指紋辨識的其他資料,而保留這項排序方式也無法協助我們啟用想啟用的用途。因此,這個 API 要求傳回的資料必須先經過排序,才能傳回。

安全性和權限

Chrome 團隊在設計及實作 Local Font Access API 時,採用了「控管強大的網頁平台功能存取權」中定義的核心原則,包括使用者控制權、透明度和人體工學。

使用者控制項

使用者可完全控管字型存取權,除非授予 "local-fonts"權限 (如 權限登錄所列),否則系統不會允許存取。

透明度

網站是否已獲准存取使用者的本機字型,會顯示在網站資訊頁面中。

權限保留

"local-fonts" 權限會在網頁重新載入時保留。您可以透過「網站資訊」工作表撤銷授權。

意見回饋

Chrome 團隊很想瞭解您使用 Local Font Access API 的體驗。

介紹 API 設計

API 是否有任何不符合預期的運作方式?或者,是否有缺少的屬性或方法需要實作,才能實現您的想法?對安全模型有任何問題或意見嗎?在對應的 GitHub 存放區中提出規格問題,或在現有問題中新增想法。

回報導入問題

您是否發現 Chrome 實作方式有錯誤?還是實作方式與規格不同? 在 new.crbug.com 提出錯誤回報。請務必盡可能提供詳細資料、重現問題的簡單操作說明,並在「Components」(元件) 方塊中輸入 Blink>Storage>FontAccess

支援 API

您是否打算使用 Local Font Access API?您的公開支持有助於 Chrome 團隊優先處理功能,並向其他瀏覽器供應商展現支援這些功能的重要性。

使用主題標記 #LocalFontAccess 發送推文給 @ChromiumDev,告訴我們您使用這項功能的地點和方式。

特別銘謝

Local Font Access API 規格是由 Emil A. EklundAlex RussellJoshua BellOlivier Yiptong。本文由 Joe MedleyDominik RöttschesOlivier Yiptong 審查。主頁橫幅圖片由 Brett JordanUnsplash 上提供。