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

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

網頁安全字型

如果您從事網頁開發已久,可能會記得所謂的網頁安全字體。這些字型已知可在大多數最常用的作業系統 (例如 Windows、macOS、最常見的 Linux 發行版、Android 和 iOS) 上使用。在 2000 年代初期,微軟甚至還推出了名為「網頁專用 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';
}

本機字型做為指紋向量

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

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

這種做法提供不錯的備用機制,可能可節省頻寬。很抱歉,我們無法在網際網路上提供優質內容。local() 函式的缺點是,可能會遭到濫用,用於瀏覽器指紋記錄。事實證明,使用者安裝的字型清單可以提供相當明確的資訊。許多公司都會在員工的筆電上安裝自家的企業字型。舉例來說,Google 有一個名為 Google Sans 的企業字型。

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

攻擊者可以嘗試透過測試是否存在大量已知的企業字型 (例如 Google Sans),來判斷使用者所屬的公司。攻擊者會嘗試在畫布上算繪這些字型中的文字,並測量字形。如果圖形與公司字型的已知形狀相符,攻擊者就會成功。如果字形不相符,攻擊者就會知道系統使用了預設替換字型,因為公司字型並未安裝。如要進一步瞭解這類攻擊和其他瀏覽器指紋攻擊,請參閱 Laperdix 等人調查報告

除了公司字型,即使只是已安裝的字型清單,也可能會洩漏資訊。這個攻擊向量的問題日益嚴重,因此 WebKit 團隊最近決定「只在可用字型清單中納入網路字型和作業系統附帶的字型,但不納入使用者在本機安裝的字型」。(我正在撰寫一篇關於授予本機字型存取權的文章。)

Local Font Access API

這篇文章開頭的內容可能會讓你感到沮喪,我們真的不能擁有美好的事物嗎?別擔心,我們認為我們可以,也許一切都不會毫無希望。不過,請先讓我回答你可能會問的問題。

為什麼在有網路字型的情況下,我們還需要本機字型存取 API?

過去,要透過網路提供專業品質的設計和圖形工具一直不容易。其中一個障礙是無法存取及使用設計人員在本機安裝的各種專業構建和提示字型。網路字型可用於某些發布用途,但無法啟用程式輔助存取向量字形形狀和字型表,因為這些項目會用於轉譯字形輪廓。同樣地,也無法存取網頁字型的二進位資料。

  • 設計工具需要存取字型位元組,才能實作自己的 OpenType 版面配置,並允許設計工具在較低層級掛鉤,執行對字元圖形形狀執行向量篩選或轉換等動作。
  • 開發人員可能會為應用程式提供舊版字體堆疊,以便在網路上使用。如要使用這些堆疊,通常需要直接存取字型資料,而這正是網路字型無法提供的功能。
  • 部分字型可能未獲得授權,無法透過網路提供。舉例來說,Linotype 擁有部分字型的授權,但僅限於電腦使用

本機字型存取 API 就是為瞭解決這些問題。這個公式包含兩個部分:

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

瀏覽器支援

瀏覽器支援

  • Chrome:103。
  • Edge:103。
  • Firefox:不支援。
  • Safari:不支援。

資料來源

如何使用 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 團隊根據「控制強大網路平台功能的存取權」一文中定義的核心原則設計並實作本機字型存取 API,包括使用者控制、透明度和人體工學。

使用者控制項

使用者可以完全控制字型存取權,除非授予 權限註冊表中列出的 "local-fonts" 權限,否則系統不會允許存取。

透明度

網站資訊單會顯示網站是否已獲准存取使用者的本機字型。

權限持續性

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

意見回饋

Chrome 團隊希望瞭解你使用 Local Font Access API 的體驗。

請告訴我們 API 設計

API 是否有任何功能無法正常運作?或者,您是否缺少實作想法所需的方法或屬性?對於安全性模型有任何問題或意見嗎?在對應的 GitHub 存放區中提出規格問題,或在現有問題中加入您的想法。

回報實作問題

你是否發現 Chrome 實作項目有錯誤?或者實作方式與規格不同?請前往 new.crbug.com 回報錯誤。請務必盡可能提供詳細資訊,並在「Components」方塊中輸入 Blink>Storage>FontAccess,以便重現問題。Glitch 可讓您輕鬆快速地分享重現內容。

顯示對 API 的支援

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

使用主題標記 #LocalFontAccess@ChromiumDev 發送推文,告訴我們你在何處使用這項功能,以及使用方式。

特別銘謝

本機字型存取權 API 規格由 Emil A. EklundAlex RussellJoshua BellOlivier Yiptong。本文由 Joe MedleyDominik RöttschesOlivier Yiptong 審查。主頁橫幅圖片由 Brett Jordan 提供,取自 Unsplash