关于在 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」,最后导出为一个新的字体文件。

相关推荐
橙子家4 小时前
WebAPI 项目通过 CI/CD 自动化部署到 Linux 服务器(docker-compose)
后端
钟离墨笺5 小时前
Go语言--2go基础-->基本数据类型
开发语言·前端·后端·golang
飞Link7 小时前
【Django】Django的静态文件相关配置与操作
后端·python·django
钟离墨笺7 小时前
Go语言--2go基础-->map
开发语言·后端·golang
Tony Bai8 小时前
Go 语言的“魔法”时刻:如何用 -toolexec 实现零侵入式自动插桩?
开发语言·后端·golang
qq_124987075310 小时前
基于小程序中医食谱推荐系统的设计(源码+论文+部署+安装)
java·spring boot·后端·微信小程序·小程序·毕业设计·计算机毕业设计
Marktowin11 小时前
SpringBoot项目的国际化流程
java·后端·springboot
程序员泠零澪回家种桔子11 小时前
RAG中的Embedding技术
人工智能·后端·ai·embedding
汤姆yu11 小时前
基于springboot的直播管理系统
java·spring boot·后端
a努力。11 小时前
虾皮Java面试被问:分布式Top K问题的解决方案
java·后端·云原生·面试·rpc·架构