随机碎碎念:博客字体和 OG Image

昨天晚上在糊用 CanvasKit 给每篇文章自动生成 OpenGraph Image 图像的脚本,我准备用和网页使用同样的字体渲染到图片上。

如果你仔细Devtools观察这个页面会发现博文使用了两种不同的字体。中文是落霞孤鹜文楷,但我总觉得中英文混排时它的英文字型和中文实在不太搭配。楷体本就处在一个有衬线特征但不是很多的模糊地带,放到同类型的英文书法字体里混排就更加诡异,于是在最开始选择了 Palatino 作为英文字体。

这个选择仍然很别扭,点开之前的版本1,可以看到 FM 那篇文章标题的英文衬线要素过多,

明明两个都是好看的字体,为什么放在一起就变成这样了呢?

后来某一天醒悟了,简体文楷的衬线特征并不是特别明显,于是我把搜索范围扩大到非衬线体,找到了一款吸收了 Garamond 精气神的字体 Ysabeau,和落霞孤鹜文楷整体都很搭配,唯一的缺点就是...

Garamond (WikiMedia)
Garamond (WikiMedia)

它也和 Garamond 一样数字是不齐线的2,"012" 和小写 "x" 一样高,"34579" 则有降部,而 "68" 有升部。单独放在纯英文的上下文里当然没有什么问题,和中文混排的时候就显得有些突兀了。

当然这难不倒 精通 CSS 3的我,这个字体其实是提供了替换字形的,通过 CSS 设置 font-variant: lining-nums 即可强制使用齐线数字,让所有数字和大写字母一样高。

但是这跟 OG Image 有什么关系呢?

前面提到我准备用跟网页一样的字体来渲染 OG Image,这样也会碰到 oldstyle figure 的问题。网页上通过 CSS 改了,而 CanvasKit 是 skia 的 WASM 封装,它的文档不能说是没有只能说是几乎没有,我只能从 TS 的类型定义里猜

在 CanvasKit 的 TextStyle 上有两个长得像 CSS font-variant 的属性:

canvaskit-wasm/types/index.d.ts
export interface TextStyle {
  backgroundColor?: InputColor
  color?: InputColor
  decoration?: number
  decorationColor?: InputColor
  decorationThickness?: number
  decorationStyle?: DecorationStyle
  fontFamilies?: string[]
  fontFeatures?: TextFontFeatures[]
  fontSize?: number
  fontStyle?: FontStyle
  fontVariations?: TextFontVariations[]
  foregroundColor?: InputColor
  heightMultiplier?: number
  halfLeading?: boolean
  letterSpacing?: number
  locale?: string
  shadows?: TextShadow[]
  textBaseline?: TextBaseline
  wordSpacing?: number
}
 
export interface TextFontFeatures {
  name: string
  value: number
}
 
export interface TextFontVariations {
  axis: string
  value: number
}
canvaskit-wasm/types/index.d.ts
export interface TextStyle {
  backgroundColor?: InputColor
  color?: InputColor
  decoration?: number
  decorationColor?: InputColor
  decorationThickness?: number
  decorationStyle?: DecorationStyle
  fontFamilies?: string[]
  fontFeatures?: TextFontFeatures[]
  fontSize?: number
  fontStyle?: FontStyle
  fontVariations?: TextFontVariations[]
  foregroundColor?: InputColor
  heightMultiplier?: number
  halfLeading?: boolean
  letterSpacing?: number
  locale?: string
  shadows?: TextShadow[]
  textBaseline?: TextBaseline
  wordSpacing?: number
}
 
export interface TextFontFeatures {
  name: string
  value: number
}
 
export interface TextFontVariations {
  axis: string
  value: number
}

TextFontVariations 非彼 font-variant,应该是 Variable Font 的配置,那么剩下的就是 TextFontFeatures 了。但是 name 和 value 应该传什么值,skia 的文档里没有注释,于是我只能在 skia 的仓库里挖呀挖呀挖

一个 SkFontFeature 是一个 SkFourByteTag

modules/skparagraph/src/OneLineShaper.cpp
  SkShaper::Feature feature = {
    SkSetFourByteTag(ff.fName[0], ff.fName[1], ff.fName[2], ff.fName[3]),
    SkToU32(ff.fValue),
    block.fRange.start,
    block.fRange.end
  };
modules/skparagraph/src/OneLineShaper.cpp
  SkShaper::Feature feature = {
    SkSetFourByteTag(ff.fName[0], ff.fName[1], ff.fName[2], ff.fName[3]),
    SkToU32(ff.fValue),
    block.fRange.start,
    block.fRange.end
  };

但并没有地方解释这个 SkFourByteTag 是什么,并且 skia 项目里面其他地方也在用,在源码里搜索到的用法就有很多种,但唯独没有找到 lining-num 对应的四个字节,我隐约感觉 tag 的定义似乎不在 skia 这边。

不过我搜到了一个 flutter 的 issue,其中提到了一个叫 tnum 的 font feature。我知道这个 feature 应该全名 tabular number,对应 CSS 的 font-variant: tabular-nums4,那么 lining-nums 很可能对应 lnum 这个 feature,我把这个值填到代码里

它 work 了,但我的脑子不 work 了

晚上回到家我请来 ChatGPTDeus Polymathiae,问祂有哪些 SkFourByteTag 组合:

Sure! Here are some common SkFourByteTag values that are used to control font features:

  1. aalt: Access All Alternates

    This feature, when present in a font, replaces the default glyphs with alternate forms that provide better joining behavior. It is often used in scripts such as Arabic, where letterforms must be joined together in different ways depending on their context.

  2. calt: Contextual Alternates

    This feature, when present in a font, replaces certain character combinations with alternate glyphs that improve the overall appearance of the text. It is often used in scripts such as Arabic or Devanagari, where certain letter combinations require special handling.

  3. liga: Standard Ligatures

    This feature, when present in a font, replaces certain letter combinations with ligature glyphs that improve the overall appearance of the text. It is often used in scripts such as Latin or Cyrillic, where certain letter combinations can be difficult to read in their default form.

  4. dlig: Discretionary Ligatures

    This feature, when present in a font, replaces certain letter combinations with ligature glyphs that are more decorative in nature. It is often used in display or decorative fonts.

  5. smcp: Small Capitals

    This feature, when present in a font, replaces lowercase characters with smaller versions of the corresponding uppercase characters. It is often used in titles or headings to add emphasis.

  6. frac: Fractions

    This feature, when present in a font, replaces certain character combinations with glyphs that represent fractions. It is often used in typesetting mathematical or scientific text.

These are just a few examples of the many SkFourByteTag values that are used to control font features. Different fonts may support different sets of features, so it's important to consult the font's documentation to determine which features are available.

于是我又问祂,你知道 tnum 和 lnum 吗?

The tnum and lnum tags in SkFourByteTag refer to different types of numerals in a font.

tnum stands for "tabular figures" and lnum stands for "lining figures". These are two different styles of numerals that are often included in a font.

Tabular figures are designed to be the same width, so that they can be aligned in columns. They are often used in tables, charts, and other situations where numerical data needs to be presented in a structured format.

Lining figures, on the other hand, are designed to be the same height as the uppercase letters in the font. They are often used in running text, where they help to maintain a consistent visual rhythm.

By using the tnum and lnum tags in Skia, you can specify which style of numerals should be used for a particular block of text. This can be useful in situations where you need to present numerical data in a consistent and easily readable format.

For example, to use tabular figures in Skia, you could set the paint's text encoding mode to kGlyphID and then apply the tnum tag to the text:

SkPaint paint;
paint.setTextEncoding(SkPaint::kGlyphID_TextEncoding);
paint.setTypeface(myTypeface);
paint.setFourByteTag(SkFourByteTag('t', 'n', 'u', 'm'));
canvas->drawString("1234567890", x, y, paint);
SkPaint paint;
paint.setTextEncoding(SkPaint::kGlyphID_TextEncoding);
paint.setTypeface(myTypeface);
paint.setFourByteTag(SkFourByteTag('t', 'n', 'u', 'm'));
canvas->drawString("1234567890", x, y, paint);

This would cause Skia to use the tabular figures from the specified typeface for the text "1234567890". To use lining figures, you would set the tag to lnum instead.

不愧是博物之神,尽管文档和源码里都没提到这些 tag,祂仍然给出了正确的答案。这些四字标签一定来自某个比 skia 更底层的地方,只是我还不知道在哪里。

我本来准备先咕到这里,但是在给这文章贴链接的时候突然注意到 MDN 在 font-variant-numeric 的文档里提到了:

lining-nums activating the set of figures where numbers are all lying on the baseline. It corresponds to the OpenType values lnum.

得来全不费功夫,真相一下就浮出了浮出水面。

FontFeature 是 OpenType 标准里定义的字体特性,由于是字体渲染的参数所以 skia 并不会直接处理这些值。

可用的 FontFeature 会注册到 OpenType Layout Tag Registry 中,但私有的 FontFeature 恐怕也是存在的,所以 CanvasKit 的 TS 定义上也没有枚举 FontFeature 可能出现的值(

我在 MDN 上也找到了一篇介绍 Font Feature Settings (zh-CN) 的文章,详细介绍了许多常用的 font feature。

啊,没用的知识又增加了。


  1. 坏了,不知为什么被铸进互联网历史博物馆了

  2. 英文叫 Old Style,而平常见到的现代字体中的数字都是齐线的 (Lining Figures),高度和大写字母的高度一样

  3. 指花了 0.13 秒对着所有 font- 开头的 CSS 属性进行了一个遍历看看哪个会有效果

  4. 这是一个相当方便的特性,在展示包含数字的表格/列表的时候可以让数字等宽便于阅读

Loading New Comments...