Skip to content

Latest commit

 

History

History
169 lines (102 loc) · 7.8 KB

matplotlibWithCJKChars.md

File metadata and controls

169 lines (102 loc) · 7.8 KB

【小记】 Matplotlib 中设置自定义中文字体的正确姿势

最近做实验涉及到用 Matplotlib 绘制图表,我希望相同的代码能不仅在本地运行,且还能在 Google Colab, Binder 这些线上平台运行。

问题就来了,为了在各个平台上都能绘制中文字符,该如何在 Matplotlib 中使用自定义的字体?(毕竟国外的一些平台不会预装支持中文字符的字体)

网上查了一下,很多文章都是复制粘贴来的,十分误导人,遂决定写下此笔记。

studyAttitude-2024-11-23

1. 开门见山

import matplotlib.pyplot as plt
import matplotlib.font_manager as fm

# 自定义字体(OTF, TTF)的路径
FONT_PATH = './SourceHanSerifCN-Regular.otf'

# 💡 利用 fontManager 的方法添加字体到内部的字体列表中
fm.fontManager.addfont(FONT_PATH)
# 获得字体属性对象
font_props=fm.FontProperties(fname=FONT_PATH)

# 获得字体名
font_name=font_props.get_name()

# 优先使用自定义的字体,不满足的则 fallback 到 sans-serif
plt.rcParams['font.family']=[font_name, 'sans-serif']
# (可选)还可以单独设置数学公式字体,这里用 matplotlib 默认的字体
plt.rcParams["mathtext.fontset"]='cm'
# unicode_minus 即采用 Unicode 中的 '−' 字符(U+2212),而不是 ASCII 中的 '-' 字符(U+002D)
# 如果你用的字体没有 U+2212 对应的字形,就需要把这一项设定为 False,让减号用 ASCII 编码。
# plt.rcParams['axes.unicode_minus'] = False

这样配置后,Matplotlib 就可以用自定义字体来渲染文本了。

2. 顺藤摸瓜

2.1. 让 Matplotlib 能找到字体

在渲染图像时,如果涉及到文字部分,Matplotlib 会在内部调用字体管理模块 FontManager 的方法 matplotlib.font_manager.FontManager.findfont 来找到合适的字体对应的路径

调用 findfont 时实际上是调用了同模块的 _findfont_cached 方法,从这个方法的源码中可以看到,查找 TTF/OTF 字体时,依赖于 FontManager 对象本身的一个列表 ttflist#L1417)。

FontManager 对象初始化的时候,程序实际上是扫描了所有的系统字体,把它们添加到 ttflist 中,采用了 FontManager.addfont 方法(#L1048)。

💡 因此为了让 Matplotlib 能找到我们自定义的中文字体,要做的事就是调用 FontManager.addfont 这个方法,其把自定义 TTF/OTF 字体路径包装为 FontEntry 对象后添加到 ttflist 这个列表中#L1057)。

fm.fontManager.addfont(FONT_PATH)

2.2. 获得字体族名

在用字体属性对象 FontProperties 包装了自定义字体后,可以用其 get_name 方法来获得字体名:

# 获得字体属性对象
font_props=fm.FontProperties(fname=FONT_PATH)
# 获得字体名
font_name=font_props.get_name()

get_name#L672)的方法调用链如下:

FontProperties.get_name -> FontManager.get_font -> FontManager.find_font -> FontManager._findfont_cached

因为已经通过 FontPropertiesfname 参数指定了字体路径,FontManager.find_font 会直接返回这个路径给 get_font#L1531)方法,get_font 则将字体载入后取得字体的 family_name

因此上面代码片段中 font_name 存储的是自定义字体的 family name(这里是 Source Han Serif CN)。

2.3. 修改 Matplotlib 的字体配置

# 设定字体 family name 
plt.rcParams['font.family']=[font_name, 'sans-serif']
# (可选)单独设定数学字体
plt.rcParams["mathtext.fontset"]='cm'

注:plt.rcParamsmatplotlib.rcParams 是一样的,前者只不过是在 pyplot 模块内导入了 rcParams

这里修改了 Matplotlib 的运行时配置(Runtime Configuration,即 rc)中的相关配置,相关文档已有说明:

3. 拓展:字体回退(Fallback)

Matplotlib 支持字体回退,借此我可以让中文和英文字符在被渲染时分别基于不同的字体:

# 英文字符用 Monospace,中文字符用自定义字体
plt.rcParams['font.family']=['monospace', font_name, 'sans-serif']

fig, axe = plt.subplots(figsize=(1, 1))
axe.axis("off")
axe.text(0, 0.5, "I 有 some 水 in that 瓶子.")

遇到 monospace 不支持的 CJK 字符时,会回退(fallback)到第二个字体,即我们自定义的中文字体。字体渲染效果如下:

font_fallback_render_output-2024-11-23

3.1. 存在的问题

写这篇笔记的过程中我意外发现,文本中包含有数学公式时字体无法正常回退:

axe.text(0, 0.5, "I 有 some 水 in that $bottle$. 哦看哪,这里有一个数学公式:$sin(x)$")

font_cannot_fallback-2024-11-23

注意,这里英文字符仍然是用 monospace 字体渲染的,但是遇到中文字符时却没有回退,因而找不到对应的字形。

火速去 Github 提了个 issue,目前已经被确认为 BUG。期待维护者们能在后续版本中修复,加油!

3.2. 权宜之计

既然没法回退,那么只好把我们自定义的字体放在首个位置上了:

# 中文和英文字符都基于自定义字体来渲染
plt.rcParams['font.family']=[font_name, 'sans-serif']

output_with_math_formulas-2024-11-23

4. 写在最后

以上的方式可以直接让当前运行环境中的 Matplotlib 支持中文字符的渲染。

如果你要为少数几条文本单独配置字体,可以在相关的绘制语句上配置相应参数:

font_props=fm.FontProperties(fname=FONT_PATH)
# fontproperties 配置后会覆盖默认的配置
plt.text(0, 0.5, "为什么是 SomeBottle 而不是 SomeBottles ?", fontproperties=font_props)  

那么这篇笔记就是这样,咱们下次再会~ (∠・ω< )⌒★