前端:如何校验某个字符是否存在于字体文件?

1,412 阅读6分钟

起因

C端活动页项目需要用到 UI 同学给的一套艺术字体文件。为了减少包体积,该字体文件(TTF格式)已经经过一次字符的筛选(大约53kb),筛选出了常用的数字、字母、营销常用汉字大约280个字符左右。

后台需要在活动页配置时艺术字的校验,若配置的字符不在该字体文件内,则不允许用户进行配置。

通过这个需求的开发,学习一些字体的相关知识。

字体基础

字体标准

字体就像 JavaScript 语言有自己的 ECMAScript 标准一样有着自己的行业标准。Apple 和 Microsoft 在1991年共同推出了 TrueType 标准,在此标准之前,还有 Adobe 公司于 1984 年发布了 PostScript 语言。1996 年微软联合 Adobe 在 TrueType 的基础上推出了 OpenType 字体标准,该标准整合了 PostScript 字体和 TrueType 字体的特点,并新增了许多新的特性。OpenType 陆续得到苹果和谷歌等公司的支持,在主流计算机平台应用广泛,目前已经成为字体设计的主要发展趋势。

对于我们普通开发者而言,TrueType (即 ttf 后缀的字体文件)和 OpenType (可以采用 otf,ttf,otc,ttc 等多种格式) 比较常见。此外,还有专用于网页的网页开放字体(Web Open Font Format)格式,其设计标准通常为 OpenType 或 TureType,但在文件打包和压缩中专门为网络传输做了优化。WOFF 字体在主流浏览器中都得到很好的支持,文件后缀名为 .woff 以及 .woff2

字体显示方式及加载

字体显示,即使用 CSS 属性 font-family,这个属性前端开发者们相对都比较熟悉了。MDN 上是这么描述的:允许你通过给定一个有**先后顺序**的,由**字体名或者字体族名**组成的列表来为选定的元素设置字体。

先后顺序指的是,当我们对一个字符进行字体的选择时,我们会从 font-family 中的从前到后选定能够使用的字体,这里字体的选择的单位是逐字符的,有可能一个字符周围都是另外一个 A 字体,而此字符由于 A 字体无法正常显示(可能由于 A 字体 仅仅某些特定的 样式变体大小下有效时),使用了 B 字体。

我们应当至少在使用的 font-family 列表中添加一个通用的字体族名。通用的字体族名包含一些常见内置在电脑中的字体。比如 带衬线字体 serif,无衬线字体 sans-serif 等等。

/* 使用通用字体族sans-serif中的Gill Sans Extrabold和Goudy Bookletter 1911 */
font-family: "Gill Sans Extrabold", sans-serif;
font-family: "Goudy Bookletter 1911", sans-serif;

/* 使用一个通用字体族名 */
font-family: serif;
font-family: sans-serif;

接下来我们来说说自定义字体。自定义字体加载最常用也是我们最熟悉的方式就是利用 CSS 的 @font-face 属性。字体通过这个属性能用户本地安装的字体加载(src 属性中的 local 函数)或者从远程服务器加载(url 函数)。

@font-face {
  font-family: "Roboto";
  src: local("Roboto"), 
    url("/fonts/Roboto/Roboto-Regular.woff2") format("woff2"), 
    url("/fonts/Roboto/Roboto-Regular.woff") format("woff");
  font-weight: 400;
  font-style: normal;
  font-display: auto; 
  unicode-range: U+000-5FF;
}

此 @font-face 指定了一个名为 Roboto 的自定义字体。注册和加载完相关的自定义字体后,即可以通过 font-family 属性加载此字体。

注意,unicode-range 其作用是告知浏览器,通过 @font-face 引入的字体覆盖了 unicode 字符体系的哪些部分,以便浏览器仅在该范围内使用该字体。这个 unicode-range 算是个生僻的属性,有兴趣的可以查看更多相关解析。

解决方案

设计给了我一个 ttf 格式的字体文件,我该如何提取出文件中对应的字符呢?毕竟我需要在后台做一些用户输入校验。此时一个专用于处理字体的 JS 库就派上用途了。即 opentype.js「https://github.com/opentypejs/opentype.js」 库,该库提供了从浏览器或者 Node.js 两种访问方式,提供很多实用方便的方法让我们对字体文件进行解析和处理、剪裁,重绘等等。

将字体文件上传至 CDN 地址后,在 C 端通过 @font-face 引入远程字体,使用无问题。后台可以通过 fetch 方法可以拿到该字体文件,然后通过 opentype 库对字体文件进行解析。

通过 opentype 的 parse 方法解析出字体库,得到 font 对象。

在此对象中,我们能拿到所有字符的字形表,即由所有字符组成的 Glyph 对象集合(GlyphSet)。

其中,glyphs 对象下的 glyphs 属性保存了所有字形的集合,我们可以将它们进行收集。

Glyph 对象是字形的一组属性集合,glyph 为单个需要渲染的字形,是渲染的最小单位。字形是通常与字符相对应的单个标记。一个抽象字符可能有多种形状(字形),但由于都具有相同的含义,在 Unicode 中被视为同一个字符,只会分配一个码点,比如汉字有楷、行、草、隶等写法,这些属于同一个字符的不同字形。所以我们拿到 Unicode 唯一编码即可,然后和我们需要校验的字符的 Unicode 编码进行比较。

除此之外,这个库还提供了许多其他关于字体的方法,只是我的需求中暂时用不到。

附上代码:

import opentype from 'opentype.js'
// 读取后台配置化接口拿到字体CDN并转化为二进制buffer形式
getFontData() {
  return new Promise((resolve, reject) => {
    this.$tHttp
    .post('xxx后台配置化接口')
    .then((res) => {
        const { code, data } = res;

        if (code === 10000) {
        // 拿到配置化接口中配置的字体CDN地址
        const { fontCDNUrl } = data;
        // fetch 获取字体 CDN 并解析
        fetch(fontCDNUrl)
          .then(async response => {
            // 检查响应是否成功
            if (!response.ok) {
                this.$Message.error('字体接口获取失败,请联系管理员!');
                throw new Error('网络响应不是OK');
            }
            const buffer = response.arrayBuffer();
            // 转化为二进制 buffer
            const fontBuffer = await buffer;
            // 解析为JSON格式
            resolve(fontBuffer);
            })
          .catch(error => {
            // 处理错误
            this.$Message.error('字体接口获取失败,请联系管理员!');
            console.error('获取数据时出现问题:', error);
          });
        }
    }).catch((err) => {
        this.$Message.error('字体接口获取失败,请联系管理员!');
        reject(err);
    });
  });
},
const StaticCharSet = new Set();
/**
 * 校验函数,校验字符串是否合法
 * @param titleNeedValid 需要校验的字符串
 */
async validCorrectFontFile(titleNeedValid) {
  if (!StaticCharSet.size) {
    // 读取文件转为 buffer
    const fontFileBuffer = await this.getFontData();

    const font = opentype.parse(fontFileBuffer);

    // 获取 cmap(字符映射)表
    const glyphs = font.glyphs.glyphs;

    for (const key in glyphs) {
    const unicode = glyphs[key]['unicode'];

    if (unicode) {
        StaticCharSet.add(unicode);
    }
    }

    const SPACEUNICODE = ' '.codePointAt(0); // 空格 - 值为 32

    if (!StaticCharSet.has(SPACEUNICODE)) {
    // Unicode 32 为空格,UI给的字体文件中空格 Unicode 不合法,这里需要手动塞入,且空格在C端不同字体不会影响到样式
    StaticCharSet.add(SPACEUNICODE);
    }
  }

  const unValidUnicodeIndex = [];

  // 检查字符串中的字符是否在 Unicode 集中
  for (let index = 0; index < titleNeedValid.length; index++) {
    const code = titleNeedValid[index].codePointAt(0); // 获取字符的 Unicode 编码

    if (!StaticCharSet.has(code)) {
    unValidUnicodeIndex.push(index); // 如果有任何字符不在 Unicode 集中,返回 false
    }
  }
  return unValidUnicodeIndex; // 字符串中的所有字符都在 Unicode 集中,返回 true
}