今天发现生成定制图片中的字体变成了默认字体。因为特殊原因,定制中的字体使用的后端生成的方式。生成字体图片的服务是 Node.js 写的,通过 fabric.js 生成对应的图片。字体的注册是通过 fabric.nodeCanvas.registerFont 方法实现的。
尝试重启 Node.js 服务后,短时间内字体是对的,但是很快就再次失效,变成了默认字体。
因为最近的改动就是增加了几种字体,代码并没有改动,于是排查的重点就在这几个新增的字体上。
最终发现有两个字体文件是相同的(前端网页中注册字体时使用了不同的 fontFamily)。依次测试这两个字体(fontFamily)时,第一次这两个字体都是正确的,但是第二次时先注册的字体就会变成默认字体。
注册字体的代码:
js
fabric.nodeCanvas.registerFont(fontPath, {
family: fontFamily,
weight: 'normal',
style: 'normal'
})
其中 fabric.nodeCanvas 实际上来自 node-canvas 包。其注册字体的代码如下:
js
function registerFont (src, fontFace) {
// TODO this doesn't need to be on Canvas; it should just be a static method
// of `bindings`.
return Canvas._registerFont(fs.realpathSync(src), fontFace)
}
上面 _registerFont 方法绑定到的是 C++ 代码 Canvas::RegisterFont。
cpp
StaticMethod<&Canvas::RegisterFont>("_registerFont", napi_default_method)
Canvas::RegisterFont 方法的实现如下:
cpp
void
Canvas::RegisterFont(const Napi::CallbackInfo& info) {
Napi::Env env = info.Env();
if (!info[0].IsString()) {
Napi::Error::New(env, "Wrong argument type").ThrowAsJavaScriptException();
return;
} else if (!info[1].IsObject()) {
Napi::Error::New(env, GENERIC_FACE_ERROR).ThrowAsJavaScriptException();
return;
}
std::string filePath = info[0].As<Napi::String>();
PangoFontDescription *sys_desc = get_pango_font_description((unsigned char *)(filePath.c_str()));
if (!sys_desc) {
Napi::Error::New(env, "Could not parse font file").ThrowAsJavaScriptException();
return;
}
PangoFontDescription *user_desc = pango_font_description_new();
// now check the attrs, there are many ways to be wrong
Napi::Object js_user_desc = info[1].As<Napi::Object>();
// TODO: use FontParser on these values just like the FontFace API works
char *family = str_value(js_user_desc.Get("family"), NULL, false);
char *weight = str_value(js_user_desc.Get("weight"), "normal", true);
char *style = str_value(js_user_desc.Get("style"), "normal", false);
if (family && weight && style) {
pango_font_description_set_weight(user_desc, Canvas::GetWeightFromCSSString(weight));
pango_font_description_set_style(user_desc, Canvas::GetStyleFromCSSString(style));
pango_font_description_set_family(user_desc, family);
auto found = std::find_if(font_face_list.begin(), font_face_list.end(), [&](FontFace& f) {
return pango_font_description_equal(f.sys_desc, sys_desc);
});
if (found != font_face_list.end()) {
pango_font_description_free(found->user_desc);
found->user_desc = user_desc;
} else if (register_font((unsigned char *) filePath.c_str())) {
FontFace face;
face.user_desc = user_desc;
face.sys_desc = sys_desc;
strncpy((char *)face.file_path, (char *) filePath.c_str(), 1023);
font_face_list.push_back(face);
} else {
pango_font_description_free(user_desc);
Napi::Error::New(env, "Could not load font to the system's font host").ThrowAsJavaScriptException();
}
} else {
pango_font_description_free(user_desc);
if (!env.IsExceptionPending()) {
Napi::Error::New(env, GENERIC_FACE_ERROR).ThrowAsJavaScriptException();
}
}
free(family);
free(weight);
free(style);
fontSerial++;
}
从上面的可以看到,注册时会查找是否已经有了相同的字体描述 sys_desc 的字体。如果有,则会释放用户字体描述 user_desc,替换为新的。
如何判定字体描述相同(
pango_font_description_equal),这里看不到源码。根据豆包回答,会比较这些属性:family、weight、style、stretch、variant、size。
本次的问题就是由于这个机制导致的。 注册的两个字体的 fontFamily 对应的字体文件的文件名虽然不一样,但其实是同一个字体。 后注册字体的 user_desc 会替换之前的,导致之前的 fontFamily 就失效。
没找到通过代码自动更改字体的 sys_desc 信息的方法,暂时的解决方案是:使用 FontForge,打开字体文件,点击「Element」→「Font Info」→「PS Names」,修改「Fontname」「Family Name」「Name For Humans」,最后导出为一个新的字体文件。