前端如何获取本地字体列表

3,543 阅读2分钟

  字体枚举是一项缺失的功能,它阻碍了现代 Web 应用程序充分利用 Web 平台。想要使用本地字体的 Web 应用程序需要用户先将它们上传到服务器,然后再通过字节下发到客户端。

   相信很多做过富文本的小伙伴都遇到过这个问题,就是需要在字体列表展示相关的字体供用户使用,对于开发也不清楚具体本地都有哪些字体,只能每一种字体都加载一遍,然后再去结合FontFacSet API去做一些性能加载优化,比如:

FontFaceSet.onloading
FontFaceSet.onloadingdone
FontFaceSet.onloadingerror

FontFaceSet.check()
FontFaceSet.load()

....具体还有哪些方法可以参考MDN
https://developer.mozilla.org/en-US/docs/Web/API/FontFaceSet/load

以此来满足一些业务迭代的需求。

   那么现在有一个好消息就是,chrome103可以支持我们去获取本地用户的字体列表。从而我们可以避免一些加载以及找到一些相近的字体来进行替换代替。下面我们来看看如何使用它?

首先要检测一下是否支持本地字体访问API:

if ('fonts' in navigator) {  // 支持本地字体访问 API}

请求权限,对用户本地字体的访问受“local-font”权限的限制,我们可以通过使用navigator.permissions.request()来请求该权限

// 请求使用 API 的权限
try {  
  const status = await navigator.permissions.request({    name: 'local-fonts',  }); 
   if (status.state !== 'granted') {    
     throw new Error('Permission to access local fonts not granted.');  
   }} catch (err) {  
    // `TypeError` 指示 'local-fonts'  
    // 权限尚未实现,因此,  
    // 只有这不是问题时,才会 `throw`。  
     if (err.name !== 'TypeError') {    throw err;  }
}

授予权限后,可以从navigator.fonts上公开的FontManager接口调用query()向浏览器询问本地安装的字体,会得到一个数组,每个字体都可以标识为一个FontMetadata对象,具有属性family、fullName和postscriptName

// 查询所有可用字体和日志元数据。
try { 
   const pickedFonts = await navigator.fonts.query();  
   for (const metadata of pickedFonts) {    
       console.log(metadata.postscriptName);    
       console.log(metadata.fullName);    
       console.log(metadata.family);  
}} catch (err) {  console.error(err.name, err.message);}

如果想要访问SFNT数据,那我们可以通过FontMetadata对象的blob()方法可以获得完整的SFNT。SNFT是一种字体文件格式,可以包含其他字体类型,例如 PostScript、TrueType、OpenType、Web Open Font Format (WOFF) 字体等。

try {  
  const pickedFonts = await navigator.fonts.query();  
  for (const metadata of pickedFonts) {    
      // 我们只对特定字体感兴趣。    
      if (metadata.family !== 'Comic Sans MS') {      
          continue;    
       }    
      // `blob()` 返回一个包含有效且完整的   
      // SFNT 包装字体数据的 Blob。    
      const sfnt = await metadata.blob();    
      const sfntVersion = new TextDecoder().decode(      
        // 只选取我们需要的字节:前 4 个字节是 SFNT      
        // 版本信息。      
       // 规范:https://docs.microsoft.com/en-us/typography/opentype/spec/otff#organization-of-an-opentype-font      
       await sfnt.slice(0, 4).arrayBuffer(),    
      );    
      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);}

想要看完整的demo。可以参考chrome提供的官方事例:font-access-api.glitch.me/

下面我粘贴一下此demo的完整代码。

<!DOCTYPE html>
<html lang="en">
<head>  
   <meta charset="UTF-8">  
   <meta http-equiv="X-UA-Compatible" content="IE=edge">  
   <meta name="viewport" content="width=device-width, initial-scale=1.0">  
   <title>Document</title>

<style>    
body {  
  font-family: "Helvetica Neue", helvetica, arial, sans-serif;  
  margin: 16px;
}
.title {  
  font-size: 26px;
}
.content {  
  font-size: 16px;  
  margin: 36px 0;
}
.status {  
  font-size: 14px;  
  font-style: italic;  
  margin: 12px 0;
}
.button {  
  background-color: #eeeeee;  
  border: 1px;  
  border-radius: 8px;  
  cursor: pointer;  
  font-size: 16px;  
  margin: 8px 4px 8px -2px;  
  padding: 8px 16px;
}
.button:hover {  
  background-color: #dddddd;
}
.input {  
  font-size: 16px;  
  margin-left: 8px;  
  min-width: 360px;  
  padding: 4px;
}
.result {  
  display: none;
}
.select {  
  border-radius: 8px;  
  font-size: 16px;  
  margin: 8px 0;  
  min-width: 300px;  
  padding: 4px;
}
.error {  
  color: red;  
  font-size: 16px;  
  font-style: italic;
}
.font-info {  
  background-color: #b5e0f1;  
  border-radius: 16px;  
  display: none;  
  min-width: 400px;  
   padding: 16px;
}
.font-info-item {  padding: 4px;}

</style>
</head>
<body onload="showStatus()">  
  <div>    
    <div class="title">      Local Font Access API    </div>   
    <div class="content">      
      <div>       
       <div class="status">          
         Status: <span id="status"></span>       
       </div>        
       <button class="button" onclick="loadFonts()">          Query Fonts        </button>        
       <span>with optional filter</span>        
       <input class="input" type="text" id="filter-input" placeholder="Type in font names separated by comma(s)...">                
      </div>      
      <div id="error-message" class="error"></div>      
      <div id="result" class="result">        
      <br>       
      <hr>        
      <span>Local Fonts:</span>        
      <select class="select" id="fonts-select" onchange="onFontSelected()"> </select>       
      <br>       
      <div id="font-info" class="font-info">          
         <div id="font-info-postscript-name" class="font-info-item"></div>         
         <div id="font-info-full-name" class="font-info-item"></div>          
         <div id="font-info-style" class="font-info-item"></div>          
         <div id="font-info-family" class="font-info-item"></div>          
         <div id="font-info-outline-format" class="font-info-item"></div>        
      </div>      
    </div>    
  </div>  
</div>  
<style id="font-style"></style>
</body>
<script>  
const elemStatus = document.getElementById('status');
const elemFilterInput = document.getElementById('filter-input');
const elemResult = document.getElementById('result');
const elemFontsSelect = document.getElementById('fonts-select');
const elemErrorMessage = document.getElementById('error-message');
const elemFontInfo = document.getElementById('font-info');
const elemFontInfoPostscriptName = document.getElementById('font-info-postscript-name');
const elemFontInfoFullName = document.getElementById('font-info-full-name');
const elemFontInfoStyle = document.getElementById('font-info-style');
const elemFontInfoFamily = document.getElementById('font-info-family');
const elemFontInfoOutlineFormat = document.getElementById('font-info-outline-format');
const elemFontStyle = document.getElementById('font-style');
const fontMap = new Map();
let enabled = true;
async function showStatus() {  
    if (!self.queryLocalFonts) {    
       elemStatus.innerText = 'API is not available on this platform.';    
       enabled = false;    
       return;  
     }    
     const status = await navigator.permissions.query({ name: "local-fonts" });  
     let statusMessage;  
     if (status.state === "granted") {   
         statusMessage = 'Permission was granted 👍';  
     }else if (status.state === "prompt")    {
         statusMessage = 'Permission will be requested';  
     }else{
         statusMessage = 'Permission was denied 👎';  
        elemStatus.innerText = statusMessage;
    }
 async function loadFonts() {  
       if (!enabled)    return;    
       try {    
         reset();        // Query fonts, with optional postsscript name filter.    
         const optionalFilterArr = elemFilterInput.value.split(',').map(str => str.trim()).filter(str => str !== '');    
         let fonts;    
         if (optionalFilterArr.length > 0) {     
           fonts = await self.queryLocalFonts({postscriptNames: optionalFilterArr});   
         } else {      fonts = await self.queryLocalFonts();    }        // Processed response.         
if (fonts.length === 0) {      elemErrorMessage.innerText = 'No fonts returned.';      
return;    }    
elemResult.style.display = 'block';    
elemFontsSelect[0] = new Option('-- select a font --', '');    
fonts.forEach((font, index) => {      fontMap.set(font.postscriptName, font);      elemFontsSelect[index + 1] = new Option(font.fullName, font.postscriptName);    });  } catch(e) {    elemErrorMessage.innerText = `Cannot query fonts: ${e.message}`;  } finally {    showStatus();  }}function reset() {  fontMap.clear();    while (elemFontsSelect.options.length > 0) {                    elemFontsSelect.remove(0);  }     elemFontInfo.style.display = 'none';  elemFontInfoPostscriptName.innerText = '';  elemFontInfoFullName.innerText = '';  elemFontInfoStyle.innerText = '';  elemFontInfoFamily.innerText = '';  elemErrorMessage.innerText = '';}async function onFontSelected() {  if (elemFontsSelect.value === '') {    // The default option selected.    return;  }  const selectedFontData = fontMap.get(elemFontsSelect.value);  if (selectedFontData /*&& selectedFontData instanceof FontDta*/) {    elemFontStyle.textContent = `        @font-face {          font-family: "dynamic-font";          src: local("${selectedFontData.postscriptName}");        }`;    elemFontInfoPostscriptName.innerText = `Postscript Name: ${selectedFontData.postscriptName}`;    elemFontInfoFullName.innerText = `Full Name: ${selectedFontData.fullName}`;    elemFontInfoStyle.innerText = `Style: ${selectedFontData.style}`;    elemFontInfoFamily.innerText = `Family: ${selectedFontData.family}`;    elemFontInfoOutlineFormat.innerText = `Outline Format: ${await getFormat(selectedFontData)}`;    elemFontInfo.style.fontFamily = "dynamic-font";        elemFontInfo.style.display = 'inline-block';      } else {    elemErrorMessage.innerText = 'Unable to load font data';  }}async function getFormat(fontdata) {  const bytes = await fontdata.blob();  // Inspect the first four bytes, which for SFNT define the format.  // Spec: https://docs.microsoft.com/en-us/typography/opentype/spec/otff#organization-of-an-opentype-font  const sfntVersion = await bytes.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;  }  return outlineFormat;}

</script>
</html>