Talk is cheap

Show me the code

制作极限压缩的 Material Icons 图标字体

UPDATE: 写了一个傻傻的 Python 脚本 来自动处理字体,只要将所需要的图标名称每行一个写入 icon-names.txt ,就能一键生成压缩好的字体文件。(2018/04/26)

以下是原文。


这几天挖了 新坑 MelodyPlayer ,其中有用到 Google 的 Material Icons 。虽然它的 woff2 格式只有 42KB ,但我只用了其中的 10 个图标,加载整个字体文件显得很不划算。虽然 Material Icons 提供了制作 Sprite 的方案,但只有 SVG 或 CSS Sprite ,我的用法是 <button> 里写图标的名字,不能兼容。

我不愿意改代码,于是想寻找一种提取字体中特定字符的方法。

经过一番搜索,找到了 fonttools 这个强大的工具,可以精确控制字体文件的每一个 Glyph ,最大限度压缩字体。

fonttool is "A library to manipulate font files from Python." 。使用 pip 进行安装。顺便说一句,接下来的操作基于 Arch Linux , Python 3.6.5 和 pip 9.0.1 .

pip install --user fonttools # 字体工具
pip install --user brotil    # 制作 woff2 格式所需

然后,将 material-icons.woff 下载到当前目录备用。

curl -L https://fonts.gstatic.com/s/materialicons/v36/flUhRq6tzZclQEJ-Vdg-IuiaDsNc.woff2 -o material-icons.woff

接下来就是喜闻乐见的教程环节了。步骤有些多,还请耐心观看。

1

解包 material-icons.woff2

ttx material-icons.woff2

输出:

Dumping "material-icons.woff2" to "material-icons.ttx"...
Dumping 'GlyphOrder' table...
Dumping 'head' table...
Dumping 'hhea' table...
Dumping 'maxp' table...
Dumping 'OS/2' table...
Dumping 'hmtx' table...
Dumping 'cmap' table...
Dumping 'cvt ' table...
Dumping 'loca' table...
Dumping 'glyf' table...
Dumping 'name' table...
Dumping 'post' table...
Dumping 'gasp' table...
Dumping 'GDEF' table...
Dumping 'GPOS' table...
Dumping 'GSUB' table...

得到 material-icons.ttx ,保留备用。

2

用文本编辑器打开 material-icons.ttx ,寻找需要的 Glyph Name ,这里最好按照字母序排列,方便后续的寻找。

Material Icons 通过 OpenType Ligature 进行字形替换,这里要找的就是它的 Ligature 部分。找到文件中的

/GSUB/LookupList/Lookup/LigatureSubst

部分,使用文本编辑器的查找功能来寻找想要的字符。要查找的内容是:

iconName.slice(1).split('').join(',').replace(/_/g, 'underscore')

比如,我要找 play_arrow 这个图标,要搜索 l,a,y,underscore,a,r,r,o,w

<Ligature components="l,a,y,underscore,a,r,r,o,w" glyph="uniE037"/>

然后记下它的 glyph 值,也就是字体中的 Glyph Name ,这里是 uniE037

我只用了 10 个图标,这个工作量还可以承受。

然后这是结果了:

Glyph Name Icon Name
uniE318 keyboard_capslock
uniE037 play_arrow
uniE034 pause
uniE041 repeat_one
uniE040 repeat
uniE5DA subdirectory_arrow_right
uniE045 skip_previous
uniE044 skip_next
uniE043 shuffle
uniE047 stop

存储为 needed-glyph.txt 备用。

3

这一步要把所需的 Glyph 提取出来,制作一个 Font Subset 。

pyftsubset material-icons.woff2 \
--gids=13-39 \
--glyphs=uniE045,uniE044,uniE037,uniE034,uniE047,uniE5DA,uniE041,uniE040,uniE043,uniE318 \
--layout-features-=liga \
--output-file=needed-glyph.ttf

下面对参数做一点说明:

--gids=13-39 包含 _az 这 27 个字形的 Glyph ID 范围。上一步提到过, Material Icons 通过 Ligature 进行字形替换,没有这些基本字形, Ligature 不能正常工作。

--glyphs=uniEooo,uniExxx 第二步中找到的所有所需 Glyph Name ,逗号分隔。

--layout-features-=liga 在输出的字体文件中禁用 Font Ligature ,否则所有的 Glyph 都会被包含在输出文件中。但我们要在后面的步骤重新“启用”这个特性。

--output-file=needed-glyph.ttf 输出到文件 needed-glyph.ttf

更具体的参数说明,可以参见 pyfontsubset --help

4

needed-glyph.ttf 转换为 ttx 以便编辑。

ttx needed-glyph.ttf

输出:

Dumping "needed-glyph.ttf" to "needed-glyph.ttx"...
Dumping 'GlyphOrder' table...
Dumping 'head' table...
Dumping 'hhea' table...
Dumping 'maxp' table...
Dumping 'OS/2' table...
Dumping 'hmtx' table...
Dumping 'cmap' table...
Dumping 'cvt ' table...
Dumping 'loca' table...
Dumping 'glyf' table...
Dumping 'name' table...
Dumping 'post' table...
Dumping 'gasp' table...
Dumping 'GDEF' table...
Dumping 'GPOS' table...
Dumping 'GSUB' table...

得到 needed-glyph.ttx 。没什么好说的。

5

打开 needed-glyph.ttx ,找到它的 /ttFont/GSUB 元素,应该长这样:

<GSUB>
  <Version value="0x00010000"/>
  <ScriptList>
    <!-- ScriptCount=1 -->
    <ScriptRecord index="0">
      <ScriptTag value="latn"/>
      <Script>
        <!-- LangSysCount=0 -->
      </Script>
    </ScriptRecord>
  </ScriptList>
  <FeatureList>
    <!-- FeatureCount=0 -->
  </FeatureList>
  <LookupList>
    <!-- LookupCount=0 -->
  </LookupList>
</GSUB>

然后打开 material-icons.ttx 文件,也找到它的 /ttFont/GSUB 元素。复制其中的 ScriptListFeatureList (下面的代码块部分) ,覆盖 needed-glyph.ttx 中对应的元素。

<ScriptList>
  <!-- ScriptCount=1 -->
  <ScriptRecord index="0">
  <ScriptTag value="latn"/>
  <Script>
    <DefaultLangSys>
    <ReqFeatureIndex value="65535"/>
    <!-- FeatureCount=1 -->
    <FeatureIndex index="0" value="0"/>
    </DefaultLangSys>
    <!-- LangSysCount=0 -->
  </Script>
  </ScriptRecord>
</ScriptList>
<FeatureList>
  <!-- FeatureCount=1 -->
  <FeatureRecord index="0">
  <FeatureTag value="liga"/>
  <Feature>
    <!-- LookupCount=1 -->
    <LookupListIndex index="0" value="0"/>
  </Feature>
  </FeatureRecord>
</FeatureList>

6

关键步骤 :将所需的 Font Ligature 信息写入。

material-icons.ttx 找到

/ttFont/GSUB/LookupList/Lookup/LigatureSubst

它包含很多子元素,大概长这样:

<GSUB>
  <Version value="0x00010000"/>
  <!-- ... -->
  <LookupList>
    <!-- LookupCount=1 -->
    <Lookup index="0">
      <LookupType value="4"/>
      <LookupFlag value="0"/>
      <!-- SubTableCount=1 -->
      <LigatureSubst index="0" Format="1">
        <LigatureSet glyph="a">
          <!-- ... -->
          <Ligature components="d,b" glyph="uniE60E"/>

最内层的 Ligature 就是我们需要的。还记得第二步的 needed-glyph.txt 吗?找到所有对应的 Ligature 元素,复制到 needed-glyph.ttx 对应的位置,特别要注意保留 XML 的层级关系。

<LookupList>
  <!-- LookupCount=1 -->
  <Lookup index="0">
  <LookupType value="4"/>
  <LookupFlag value="0"/>
  <!-- SubTableCount=1 -->
  <LigatureSubst index="0" Format="1">
    <LigatureSet glyph="k">
      <Ligature components="e,y,b,o,a,r,d,underscore,c,a,p,s,l,o,c,k" glyph="uniE318"/>
    </LigatureSet>
    <LigatureSet glyph="p">
      <Ligature components="l,a,y,underscore,a,r,r,o,w" glyph="uniE037"/>
      <Ligature components="a,u,s,e" glyph="uniE034"/>
    </LigatureSet>
      <LigatureSet glyph="r">
      <Ligature components="e,p,e,a,t,underscore,o,n,e" glyph="uniE041"/>
      <Ligature components="e,p,e,a,t" glyph="uniE040"/>
    </LigatureSet>
      <LigatureSet glyph="s">
      <Ligature components="u,b,d,i,r,e,c,t,o,r,y,underscore,a,r,r,o,w,underscore,r,i,g,h,t" glyph="uniE5DA"/>
      <Ligature components="k,i,p,underscore,p,r,e,v,i,o,u,s" glyph="uniE045"/>
      <Ligature components="k,i,p,underscore,n,e,x,t" glyph="uniE044"/>
      <Ligature components="h,u,f,f,l,e" glyph="uniE043"/>
      <Ligature components="t,o,p" glyph="uniE047"/>
    </LigatureSet>
  </LigatureSubst>
  </Lookup>
</LookupList>

7

终于到了最激动人心的一步了,编译字体:

ttx --flavor woff2 -o ic.min.woff2 needed-glyph.ttx

一切正常的话,将会输出输出:

Compiling "needed-glyph.ttx" to "ic.min.woff2"...
Parsing 'GlyphOrder' table...
Parsing 'head' table...
Parsing 'hhea' table...
Parsing 'maxp' table...
Parsing 'OS/2' table...
Parsing 'hmtx' table...
Parsing 'cmap' table...
Parsing 'cvt ' table...
Parsing 'loca' table...
Parsing 'glyf' table...
Parsing 'name' table...
Parsing 'post' table...
Parsing 'gasp' table...
Parsing 'GDEF' table...
Parsing 'GPOS' table...
Parsing 'GSUB' table...

成功。来看一下文件大小

$ ls -lh ic.min.woff2
-rw-r--r-- 1 rocka rocka 968 Apr  24 14:42 ic.min.woff2

只有 968B 。然后转换成 Data URL :

echo "data:font/woff2;charset=utf-8;base64,"$(base64 -w 0 ic.min.woff2)

测试一番吧:

skip_previousskip_nextplay_arrowpausestopsubdirectory_arrow_rightrepeat_onerepeatshufflekeyboard_capslock

嗯,效果很不错。这次的教程就到这里,感谢观看!