关于在 Fabric.js 中注册字体覆盖的问题

今天发现生成定制图片中的字体变成了默认字体。因为特殊原因,定制中的字体使用的后端生成的方式。生成字体图片的服务是 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),这里看不到源码。根据豆包回答,会比较这些属性:familyweightstylestretchvariantsize

本次的问题就是由于这个机制导致的。 注册的两个字体的 fontFamily 对应的字体文件的文件名虽然不一样,但其实是同一个字体。 后注册字体的 user_desc 会替换之前的,导致之前的 fontFamily 就失效。

没找到通过代码自动更改字体的 sys_desc 信息的方法,暂时的解决方案是:使用 FontForge,打开字体文件,点击「Element」→「Font Info」→「PS Names」,修改「Fontname」「Family Name」「Name For Humans」,最后导出为一个新的字体文件。

相关推荐
GoGeekBaird2 小时前
通过ChatGPT+Nano Banana定制一个 PPT 生成的工作流
后端
用户21411832636022 小时前
手把手教你部署AI视频复刻神器!一键生成Sora2级别视频
后端
计算机学姐3 小时前
基于SpringBoot的高校论坛系统【2026最新】
java·vue.js·spring boot·后端·spring·java-ee·tomcat
Victor3564 小时前
Hibernate(13) Hibernate的一级缓存是什么?
后端
毕设源码-赖学姐4 小时前
【开题答辩全过程】以 基于SpringBoot的健身房管理系统的设计与实现为例,包含答辩的问题和答案
java·spring boot·后端
Victor3564 小时前
Hibernate(14)什么是Hibernate的二级缓存?
后端
czlczl200209254 小时前
SpringBoot自动配置AutoConfiguration原理与实践
开发语言·spring boot·后端
heartbeat..5 小时前
Servlet 全面解析(JavaWeb 核心)
java·网络·后端·servlet
vx_bisheyuange5 小时前
基于SpringBoot的疗养院管理系统
java·spring boot·后端