Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

47. 可变字体探索与 require 扫盲记 #47

Open
funfish opened this issue Oct 26, 2020 · 1 comment
Open

47. 可变字体探索与 require 扫盲记 #47

funfish opened this issue Oct 26, 2020 · 1 comment

Comments

@funfish
Copy link
Owner

funfish commented Oct 26, 2020

国庆后一场秋雨一场寒,属于东南季风的台风带来了明显的降温,又到了一个尴尬的温度,长袖短裤都有人穿。这个温度,感觉很舒服,尤其是在海边骑单车的时候,沿着沙河路的时候,城市灯光的点缀,观景台边的海涛声、阵阵袭人的秋意就来了。

本篇是介绍两个琐事,都是工作中遇到的,一个是可变字体探索,一个是 require 扫盲记。

Variable Fonts

好像从前几个月开始,就接触了可变字体,以前设计推荐的是使用 5 种字体,你没有看错,在项目里面用到了 5 、种字体,不同的粗细,不同的高瘦,每个字体都基本在 8M 左右,通过不同的字体来展示设计的风格,真是。。。挺好的。今年开始有新的字体,没有以前的 5 种,只用一个可变字体了,通过一个字体来展示之前 5 个的字体,可以说很是优秀,当然对开发而言,统一的字体最是简单,而且一个字体意味着只要加载一种就好了,之前的要加载 5 种字体,虽然一个可变字体的体积是 20 M。

可以通过这个网站 玩一下可变字体。

字体,对于开发者而言,默认基本都是采用系统的字体,比如系统差别、中西文差别,还有最后的衬线字体,比如我们公司就喜欢用 androidRoboto 默认字体来显示数字。如果采用自己的字体的话,会把其放在最前面,所以最前面是 OPPOSANS, Roboto, Noto Sans CJK SC, Source Han Sans CN 后面两个是思源字体,毕竟 OPPOSANS 是和思源字体结合的。。。。

通过设置 font-variation-settings: "wght" 550 可以调整字体的粗细,比如 OPPOSANS 字重可以调整到 1000-1000 区间,实现无极调整,不像以前的字体,只有一百倍数的 font-weight,而且要一个字体文件就够了。还有其他的比如 wdth ital 这些都可以设置。
比如下图

还能在这个基础上用上 font-weight,当然这个就不规范了。目前 font-variation-settings 的兼容性还是比较好的,除了 ie 和部分比较老的浏览器不支持外,其他都没有问题的。

字体的普通处理

如果是采用系统的字体那一切都挺好的,但是作为设计,作为一家最求美感的公司,就是要有自己的字体,于是普通字体的 10M 的体积,加载速度就可以劝退大部分人了。为了平滑顺利过渡字体,一般采用的是如下几个方案:

  1. **font-face 定义的时,采用 swap 来显示,系统会优先采用已有的字体,避免字体加载导致的阻塞,使得文字无法显示;**当然这种方案会导致字体加载成功时,页面切换会从当前字体切换到自定义字体,导致用户体验稍差。如果自定字体体积小,可以不采用 swap 方式。
  2. 字体文件预加载,就是在 link 标签里面采用 preload 的方式,字体资源在浏览器里,属于优先级较低的资源,通过 link 的预加载可以显著的提高优先级,避免字体加载时间过长,导致切换时候带来的不好体验
  3. **字体体积,上面更多的是辅助优化,对于中文字体而言,最重要的是体积。中文不同于其他字母语言,有非常多的字,一个字体 10 M 的体积要如何处理呢,正常会对字体文件做提取,只保留可能要用的字,也就是 glyphs。**比如只用到 这个字,那就提取字体包里的 ,这样字体文件就可以压缩的非常小了
  4. 最后是 woff、woff2 这些新格式带来的优化,以及更好的压缩算法带来的帮助。

字体的提取历程

这里要介绍是可变字体的提取问题,先看看普通字体提取,之前用的是 font-spider,使用下来可以满足字体的压缩,提取需要的子集,用法很方便,如下:

<!-- test.html -->
<style>
  @font-face {
    font-family: "source";
    src: url("../font/source.eot");
    font-style: normal;
  }
  body {
    font-family: "source";
  }
</style>
<body></body>

再通过指令 font-spider ./test.html 就可以从 source.eot 字体包里面压缩出仅仅包含 一个字的字体,当然会有一点小问题,比如垂直方向的行间距变小了,但是总体问题不大,10M 的字体包,最后只剩下几 kb。这个时候如果用软件 FontForge 查看的话,可以看到 保留下来了,其他被移除了。

上面左边是正常的字形,右边则是压缩之后的效果,可以看到压缩后周围的小伙伴都被吓跑了。

只是到了可变字体,压缩就不是这样了,简简单单的 font-spider 打包出来的字体就不能用,会出现字体镂空的情况,而且关键是不能调整可变字体的 wght,设置了也不起作用,简直就是和普通字体差不多,不再是可变字体了。

于是翻箱倒柜的,在 font-spider 里面转了一圈,结果发现里面处理字体的内容不多,更多的是对输入文件和样式处理,通过模拟的浏览器环境,自研的 browser-x(大佬自己写的 Node.js 实现的虚拟浏览器) 来获取样式,保证不同的的 font-family 打包出不同的字体,分析输入的参数,文字最后输出四种格式的字体,woff woff2 svg ttf 这些,当然在 WebFont 里面看到了不少冗余的代码,一度让我误解了,比如 weight stretch 这些属性,就不能使用。。。。可能也是大佬弃坑了吧,最后落实到压缩的还是 fontmin 这个库,也有三方压缩的工具都是基于 fontmin 的。

fontmin 是一款中间件机制的字体处理工具,比如 glyph 可以用来压缩字体,比如 ttf2woff 可以转换字体。比如下面的官方例子:

var Fontmin = require("fontmin");

var fontmin = new Fontmin().use(
  Fontmin.glyph({
    text: "天地玄黄 宇宙洪荒",
    hinting: false,
  })
);

fontmin 代码里面的 glyph 插件代码中可以看到如下形式:

var TTF = require("fonteditor-core").TTF;
var TTFReader = require("fonteditor-core").TTFReader;
var TTFWriter = require("fonteditor-core").TTFWriter;
function minifyTtf(contents, opts) {
  opts = opts || {};
  var ttfobj = contents;
  if (Buffer.isBuffer(contents)) {
    ttfobj = new TTFReader(opts).read(b2ab(contents));
  }
  var miniObj = minifyFontObject(ttfobj, opts.subset, opts.use);
  var ttfBuffer = ab2b(new TTFWriter(opts).write(miniObj));
  return {
    object: miniObj,
    buffer: ttfBuffer,
  };
}

function minifyFontObject(ttfObject, subset, plugin) {
  if (subset.length === 0) {
    return ttfObject;
  }
  var ttf = new TTF(ttfObject);
  ttf.setGlyf(getSubsetGlyfs(ttf, subset));
  if (_.isFunction(plugin)) {
    plugin(ttf);
  }
  return ttf.get();
}

敢情 fontmin 也是套娃的。。。最后核心的字体处理还是要跑到 fonteditor-core 里面,怎么说呢, fontmin 是一个优秀的集成商,有字体压缩,还有字体格式转换这些功能,虽然大部分是基于第三方的。而且不管是 fontmin 还是 font-spider 也有四五年没有更新主要内容了,作者也都弃坑了。那对于 16 年底才发布的可变字体,好像不支持也是可以理解的。

table

介绍到 fonteditor-core 就要提一下 table 的概念,这个是布局信息表,其包含了字形的位置、对齐、基线等等信息,字体文件则是由这一系列的表构成的,其中有部分表是可选的。

字体目录是字体文件的指南,提供访问其他表所需的信息,包含两部分:偏移子表(offset subtable)和表目录(table directory)。偏移子表记录了字体文件中 table 的数量,并提供了快速访问表目录的方法。偏移子表后面就是表目录,表目录主要包含了表的 tag、校验、偏移、长度等信息,字体文件中的所有表都在表目录里面有入口。

看了不少文档,每个文档对必选的 table 都有自己的解释,综合一下,下面是其中有几个是非常必要的 table:

  1. cmap:字符代码到字形索引之间的映射关系,字符代码也就是字符的 Unicode,获得索引也就可以根据索引从字体中加载这个字形。
  2. head:字体的各种基本信息,如版本、创建、修改时间,还包括基本字体数据,如 unitsPerEm、xMin, yMin 等。
  3. hhea:水平排列信息,如 ascender、descender、lineGap 等水平排列时候的布局信息。
  4. hmtx:水平参数,如间距,如果是字形之间是等距的,那只需要一个间距就可以了。
  5. maxp:最大需求表,包含字形数量,表示字形的内存需求情况。
  6. name:命名内容,如字体名,授权信息等等。
  7. post:PostScript 表,用于打印。
  8. glyf:字形数据,也是最重要的一个表了。
  9. loca:偏移和字符索引映射关系表。

上面几个表的介绍可能理解不太到位地方,因该差不多大致如此吧。另外,还有一些比如 OS/2: 用于 windows 系统的配置,所以对跨平台的字体就非常需要了,但是若是针对 Mac 这些就不必了。

具体的字形什么的,用 FontForge 软件打开任意一个字体就可以看到了,比如下面的:

你甚至都可以修改字形。。。。

至于字体从加载到渲染出来的流程可以参考一下知乎上的介绍

加载字体文件
确定要输出的字体大小
输入这个字符的编码值
根据字体文件里面的 Charmap,把编码值转换成字形索引(就是这个字符对应字体文件中的第几个形状)
根据索引从字体中加载这个字形
将这个字形渲染成位图,有可能进行加粗,倾斜等变换。注意这里的倾斜和倾斜字体不同,它只是从算法上对位图进行变换,与专门制作的加粗字体是不一样的。

上面介绍的是 cmap 根据字符代码拿到字形索引,再从 loca 拿到索引对应的字形偏移,最后到 glyf 加载字形的过程。loca 表可以参考以下图:

每个字形都有自己长度,从而形成相对于 0 位置的偏移,而 loca 表则是记录字形索引到字形偏移的映射表。

当然这里面还有很多的表的内容没有谈到,比如和 TrueTypeCCFSVG 以及 BitMap 相关的表,还有一个是高级表,比如 GSUBglyf 的替换表,之前提到的一个字符代码最后可以映射到字形,但是如果是连字的时候,就不一定是简单的字形叠加,例如下面的:

可以看到单独一个字的时候都是好好的,但是一旦结合在一起,就是不是 f + i = fi 了,而是有新的字形。这一点在阿拉伯语中也是的,字形在不同的位置有不同的显示。。。。。(原来阿拉伯语这么神奇,简直就是蝌蚪文)。

除了高级表,还有色彩相关的,其他比较杂的,最后还有一个是 OpenType Font Variations 可变字体,也是 OpenType 规范中,里面有 avarcvarfvargvarHVARMVARSTATVVAR 这几个。

可变字体的 table

可变字体,如前面提到的,可以让设计者将多个字体合并为一个字体,下面的示意图很好的介绍了字重和字宽度变化导致字形的变化:

这里面看到的 widthweight 都是 fvar 表所描述的,用来存储轴的信息,以及命名实例,其中命名实例是可选的字段。轴的信息,比如 wght(100-1000)width(10-200),包含了轴名称、最小最大值和默认值等,命名实例则是由轴与轴之间定下的命名的特定坐标,如下面几个:

可以看到轴 wght = 400 以及 wdth = 100 形成的坐标 Regular,也就是命名实例。Regular 是给特定坐标提供的预设名称,也是该子字体的名称,可以让使用者直接使用。使用可变字体的时候,如果没有指定子字体,其采用默认轴值。(css 里面修改 font-variation-settings,也就是实例了)。

对于可变字体,有两个表是必须的:fvarSTAT(style attributes),后者是样式属性,每个在 fvar 里面的每一条轴和子字体都需要在 STAT 里面有对应的信息。STAT 用来区分字体族下面的不同的字体,支持动态属性,比如 fvar 里面的 wght(无极),也支持静态属性,比如 italic 是否为斜体这些,展示 Variable Font 下的样式名称,比如 Medium 这样的字体。

其他的表则是描述 fvar 里面字体轴变化时字形的变化情况,例如 avar,是非线性的轴变化数据,例如字体的 width 轴,若变化区间是 100-200,线性的时候,则 150 表示字体的字形宽度是两个极值的正中间,但是非线性变化,就使得值不是均匀的变化的,150 可能不是字形宽度上的正中间状态。这种非线性变化也符合用户习惯。还有 gvar,存储字形在轴上的变化信息,描述 glyp 中各个点的变化情况,可以说是非常重要的。

字体提取工具

看了上面的 table 介绍,字体的处理,其实就是对 table 的处理,fonteditor-core 对可变字体的处理,看了一下源码的结构,well,根本就没有可变字体的表处理,连 fvar 的踪迹都没有。

于是开启大海捞针的方式,在 github 里面找,最后发现一个 opentypejs/opentype.js 仓库,卧槽,难道是官方的嫡系部队?只是打开到结构目录还是很失望,都是三四年前的代码了,和 fonteditor-core 差不多,虽然有 fvar 表,但是其他可变字体的表一个都没有。抱着试一试的想法,用一下,最后的打包出来的字体,虽然比 fonteditor-core 好不少,但是压根就不可变。。。。毕竟连 gvar 也没有。最后看到了这个roadMap,里面介绍到:

本来决定要放弃了,毕竟官方也不支持系列,但是总觉得有问题,难道可变字体没有工具?都好几年历史了,没有人造轮子吗。。。。

最后找到了字体处理的重量级库 fonttools,一个 python 库,打开一看密密麻麻的的 table 处理,有 50 个以上的处理,对比一下 fonteditor-core 的 18 个表处理,简直是。。。。。。在 fonttools 里面也找到了各种各样的可变字体处理表,比如 gvar,只是对 python 不是很熟悉,而且一上来就看源码,有点吃力,所以就放弃了(想起了看 esbuild 源码的经历)。

fonttools 里面有很多工具,提取字体用的是 pyftsubset,通过指定文件字符来确定要输出的字形,基本上一顿操作下来,从 22M 的字体包,压缩到 300kb。正常来做这就可以了,但是 原本字体包还包含了斜、高度轴,这些轴,项目用不上,而且 wght 也就用到了 550-1000 的范围,能不能去掉剩下的部分呢? 这样不就可以完美压缩字体了,甚至 wght 就用了 5501000 两个值,其他的能不能抛弃掉呢?可能这个就要用专业的设计工具了(比如 Adobe Illustrator?),目前在 fonttools 没有看到更多可操作空间,如果有大佬晓得一定要告知。

一般可变字体的体积是要大于单个字体的(字体族里面的单个字体),只有当需要用到同一字体族的多个字体的时候,可变字体收益才很大。当然如果需要艺术字那就另当别论了。另外 font-variation-settings 是属于比较基础的 API 了,如果要设置字重的话,可以使用 font-weight: 550 是不是很熟悉?这个和以前的 CSS 是一致的,只是 CSS Fonts Level 4 做了扩展,当然还有其他几个轴的,比如 font-stretch

require

相比于字体,require 可以说是以前的一个知识盲区。在 Vue 里面,如果需要加载资源可以采用 require 方式引入,但是时不时的总会遇到无法加载资源的问题。直到一次想要把资源路径作为 props 传入组件,再通过 require 来获取,结果是获取到图片了,但是还引发了另外一个严重的问题,页面的样式错乱?

通过审查打包出现的代码,发现原本完全没有引入的 scss 文件都被打包到样式文件里面,如果去掉 require(urlProp) 则一切正常,这个就很神奇了,而且前者的打出的包还很大。有种奇奇怪怪的感觉。

后面耐心的看 webpack 文档才晓得:

A context module is generated. It contains references to all modules in that directory that can be required with a request matching the regular expression. The context module contains a map which translates requests to module ids.

如果采用 require('./template/' + name + '.ejs') 的方式,那 template 文件下面的所有 ejs 文件都会被引用,形成一个上下文的 map 对象,导致该目录下原本不会被使用的文件,也被打包使用上了,这也就是为什么使用了 require(urlProp) 会加载上错误的资源,可想而知,若 require 里面完全采用传参的方式,会使其无法分析正确的 Directory, 于是从根文件 src 开始查询文件。。。。。

至于要如何破局呢,urlProp 为了可扩展性,是要从外部传入的,而里面要读取资源只能用 require 了,直到看到了下面的 require.context 的方式,表达式如下:

require.context(
  directory,
  (useSubdirectories = true),
  (regExp = /^\.\/.*$/),
  (mode = "sync")
);

通过在外部指定目录,和正则就能获得正确的资源路径,再传给 urlProp 就完美了。

上面的 mode 配置呢,其实是和 webpackMode 类似的,有 synceagerlazylazy-onceweakasync-weak 一共六种。其中sync 是默认的,会直接打包到文件里面,而 lazy 则会生成可延迟加载单独的 chunk

这里我用到的是 lazy-once。为什么呢,因为我需要从 require.context 里面引入的资源非常多,肯定是要拆包的,而 lazy 虽然是懒加载了,但是所有文件都单独形成 chunk,导致增加了很多文件,lazy-once 就很舒服,将所有文件合成一个 chunk,只需要通过 promise 的方式获取正确的路径就可以了,比如 urlProp(oneFileName).then(src => list.push(src)) 这样的方式。

总结

require 部分算是一个小知识点,至于深入的理解,比如 Directory 目录的获取和分析,感觉有点类似,可能是 @babel/parser 的形式,通过 ast 分析表达式来获取目录?后面的理解就没有去研究了,倒是解决了一直以来使用 require 的困惑(指不定以前有好多写的不太正常的 bug,采用 require 多加载了多余文件。。。。。呵呵呵)。

字体部分,更像是一个新领域的探索,想要不断的优化页面,而新版本的字体就是重中之重了,从开始的通过 node.jsdebug,到最后定位到 fonteditor-core 再到 fonttools,可以看到前端的字体轮子还是少了(比如参照 fonttools 代码,更新 fontedior-core ?)。更多的是学习字体的结构,看各个 table 的作用,对字体的展示也有初步的理解,但是没有去研究代码层面的实现,没有深入去,更多的是浅尝辄止,可能兴趣就到这里吧,没有更多的想法了,想要深入探索更多的东西,更有价值的吧。

写完的时候又一个台风飞过,今年的台风真是奇怪。

参考

技术文档,当然微软的是 Opentype,苹果的是 TrueType 与 AAT。

  1. microsoft OpenType 1.8.3 specification
  2. apple Font Tables
  3. 参数化设计与字体战争:从 OpenType 1.8 说起 很有趣的一篇历史介绍,想不到可变字体,出道快 30 年了,结果 16 年才成为统一标准。。。。。。
@yisibl
Copy link

yisibl commented Jun 14, 2023

甚至 wght 就用了 550 和 1000 两个值,其他的能不能抛弃掉呢?

这是是可变字体的实例化,现在 fonttools 基本都支持了。

对于字体子集化(裁剪)基本上 JS 的库都不够成熟。我选择了另外的路线,可以看看 iconfont 的效果,支持可变字体

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants