node-canvas registerFont 部署后找不到字体文件(在本地工作)

node-canvas registerFont can't find font file once deployed (works locally)

我有一个 Node.js 服务器,它使用 node-canvas 在服务器端的图像上呈现文本。这是回购协议:https://github.com/shawninder/meme-generator(仅 git clonenpm inpm run dev 到 运行 本地)。

正如您在代码中注意到的那样,我正在加载 Anton 字体,这是我从 here 获得的,其中提供了记录的 registerFont 函数通过节点-canvas

registerFont('./fonts/Anton-Regular.ttf', { family: 'Anton' })

一切都在本地运行得很好,但是当我部署到 Vercel(以前称为 zeit)时,该行抛出一个 ENOENT 错误:

no such file or directory, lstat '/var/task/fonts'

我认为您与 registerFont 非常亲密。这是我使用您的存储库所做的工作:

img.js中:

import { registerFont, createCanvas, loadImage } from 'canvas'

// …

// Where 'Anton' is the same font-family name you want to use within
// your canvas code, ie. in writeText.js.
registerFont('./pages/fonts/Anton/Anton-Regular.ttf', { family: 'Anton' })

// Make sure this goes after registerFont()
const canvas = createCanvas()

//…

我在 pages/ 中添加了一个名为 fonts/ 的新文件夹,并添加了从 Google Fonts 下载的 Anton 文件夹。单击“下载系列”从此处获取字体文件:https://fonts.google.com/specimen/Anton?query=Anton&selection.family=Anton&sidebar.open

您下载的另一个文件 (https://fonts.googleapis.com/css?family=Anton&display=swap) 实际上是 CSS 您要在浏览器中使用客户端字体预览器的文件。

起初,我会继续使用 Google Fonts 提供的托管版本。您可以将其添加到 PreviewMeme.js 组件:

<link href="https://fonts.googleapis.com/css2?family=Anton" rel="stylesheet" />
<canvas id='meme' ref={canvas}></canvas>

(您可能还想使用 FontFaceObserver 客户端之类的东西来确保在第一次呈现您的 canvas 之前字体已经加载。)

writeText.js 中,您还要将 fontFamily 更改为 Anton:

const fontFamily = 'Anton'

这将使 Anton 通过托管的 Google 字体在客户端可用,并且它应该作为服务器上的文件供您使用,以便与服务器端 canvas 包一起呈现。

希望对您有所帮助!

我最近遇到了同样的问题,终于找到了解决办法。我不是大师,所以有人可能会提出更好的方法,但这是对我有用的方法。

由于 Vercel 运行 的无服务器函数的方式,函数实际上对项目的其余部分或 public 文件夹一无所知。这是有道理的(因为安全性),但是当您需要文件的实际路径时,它确实会变得棘手。您可以毫无问题地导入字体文件,构建过程会给它一个新名称并将其放在磁盘上(在 /var/task 中),但您无法访问它。 path.resolve(_font_name_)可以看到,但是不能访问。

我最终写了一个非常糟糕的单独 api 页面,该页面使用 path.joinfs.readdirSync 来查看 api 页面实际上可以看到哪些文件。可以看到的一件事是 node_modules 文件夹,其中包含 api 页面上使用的模块文件。

fs.readdirSync(path.join(process.cwd(), 'node_modules/')

所以我所做的是编写一个本地模块,将其安装到我的项目中,然后将其导入到我的 api 页面中。在本地模块的 package.json 中,我有一行 "files": ["*"] 因此它将所有模块文件捆绑到它的 node_modules 文件夹中(而不仅仅是 .js 文件)。在我的模块中,我有我的字体文件和一个将它复制到 /tmp 的函数(/tmp 是可读和可写的),然后是 returns 文件的路径,/tmp/Roboto-Regular.ttf.

在我的 api 页面上,我包含了这个模块,然后 运行 它,并将结果路径传递给 registerfont

有效。我会分享我的代码,但它现在很草率,我想先清理它并尝试一些事情(比如我不确定是否需要将它复制到 /tmp,但我没有'没有那个步骤就测试了它)。当我把它理顺时,我会编辑这个答案。

-- 编辑 由于我无法改进我原来的解决方案,让我提供一些关于我所做的更多细节。

在我的 package.json 中,我添加了一行以包含本地模块:

"dependencies": {
 "canvas": "^2.6.1",
 "fonttrick": "file:fonttrick",

在我的项目根目录中,我有一个文件夹“fonttrick”。文件夹里面还有一个 package.json:

{
    "name": "fonttrick",
    "version": "1.0.6",
    "description": "a trick to get canvas registerfont to work in a Vercel serverless function",
    "license": "MIT",
    "homepage": "https://grumbly.games",
    "main": "index.js",
    "files": [
        "*"
    ],
    "keywords": [
        "registerfont",
        "canvas",
        "vercel",
        "zeit",
        "nextjs"
    ]
}

这是我不得不编写的唯一本地模块;关键字没有任何作用,但起初我想把它放在 NPM 上,所以它们就在那里。

fontrick 文件夹还包含我的字体文件(在本例中为“Roboto-Regular.ttf”)和一个主文件 index.js

module.exports = function fonttrick() {
  const fs = require('fs')
  const path = require('path')
  const RobotoR = require.resolve('./Roboto-Regular.ttf')
  const { COPYFILE_EXCL } = fs.constants;
  const { COPYFILE_FICLONE } = fs.constants;

  //const pathToRoboto = path.join(process.cwd(), 'node_modules/fonttrick/Roboto-Regular.ttf')

  try {
    if (fs.existsSync('/tmp/Roboto-Regular.ttf')) {
      console.log("Roboto lives in tmp!!!!")
    } else {
      fs.copyFileSync(RobotoR, '/tmp/Roboto-Regular.ttf', COPYFILE_FICLONE | COPYFILE_EXCL)
    }
  } catch (err) {
    console.error(err)
  }

  return '/tmp/Roboto-Regular.ttf'
};

我 运行 npm install 在这个文件夹中,然后 fonttrick 在我的主项目中作为一个模块可用(不要忘记 运行 npm install 还有)。

因为我只需要将它用于 API 调用,该模块仅在一个文件中使用,/pages/api/[img].js

import { drawCanvas } from "../../components/drawCanvas"
import { stringIsValid, strToGameState } from '../../components/gameStatePack'
import fonttrick from 'fonttrick'


export default (req, res) => {      // { query: { img } }
  // some constants
  const fallbackString = "1xThe~2ysent~3zlink~4yis~5wnot~6xa~7xvalid~8zsentence~9f~~"
  // const fbs64 = Buffer.from(fallbackString,'utf8').toString('base64')

  // some variables
  let imageWidth = 1200     // standard for fb ogimage
  let imageHeight = 628     // standard for fb ogimage

  // we need to remove the initial "/api/" before we can use the req string
  const reqString64 = req.url.split('/')[2]
  // and also it's base64 encoded, so convert to utf8
  const reqString = Buffer.from(reqString64, 'base64').toString('utf8')

  //const pathToRoboto = path.join(process.cwd(), 'node_modules/fonttrick/Roboto-Regular.ttf')
  let output = null

  if (stringIsValid({ sentenceString: reqString })) {
    let data = JSON.parse(strToGameState({ canvasURLstring: reqString }))
    output = drawCanvas({
      sentence: data.sentence,
      cards: data.cards,
      width: imageWidth,
      height: imageHeight,
      fontPath: fonttrick()
    })
  } else {
    let data = JSON.parse(strToGameState({ canvasURLstring: fallbackString }))
    output = drawCanvas({
      sentence: data.sentence,
      cards: data.cards,
      width: imageWidth,
      height: imageHeight,
      fontPath: fonttrick()
    })
  }

  const buffy = Buffer.from(output.split(',')[1], 'base64')
  res.statusCode = 200
  res.setHeader('Content-Type', 'image/png')
  res.end(buffy)
}

此操作的重要部分是 import fonttrick 将字体的副本放入 tmp,然后 returns 该文件的路径; 然后将字体的路径传递给 canvas 绘图函数(连同其他一些东西;要画什么,画多大等等)

我的绘图功能本身在components/drawCanvas.js;这是开头的重要内容(TLDR 版本:如果从 API 页面调用它,它会获得字体路径;如果是,则使用该路径,否则常规系统字体可用):

import { registerFont, createCanvas } from 'canvas';
import path from 'path'


// width and height are optional
export const drawCanvas = ({ sentence, cards, width, height, fontPath }) => {
  // default canvas size
  let cw = 1200 // canvas width
  let ch = 628 // canvas height
  // if given different canvas size, update
  if (width && !height) {
    cw = width
    ch = Math.floor(width / 1.91)
  }
  if (height && width) {
    cw = width
    ch = height
  }
  if (height && !width) {
    ch = height
    cw = Math.floor(height * 1.91)
  }

  // this path is only used for api calls in development mode
  let theFontPath = path.join(process.cwd(), 'public/fonts/Roboto-Regular.ttf')
  // when run in browser, registerfont isn't available,
  // but we don't need it; when run from an API call,
  // there is no css loaded, so we can't get fonts from @fontface
  // and the canvas element has no fonts installed by default;
  // in dev mode we can load them from local, but when run serverless
  // it gets complicated: basically, we have a local module whose only
  // job is to get loaded and piggyback the font file into the serverless
  // function (thread); the module default function copies the font to
  // /tmp then returns its absolute path; the function in the api 
  // then passes that path here so we can load the font from it  
  if (registerFont !== undefined) {
    if (process.env.NODE_ENV === "production") {
      theFontPath = fontPath
    }
    registerFont(theFontPath, { family: 'Roboto' })
  }
  const canvas = createCanvas(cw, ch)
  const ctx = canvas.getContext('2d')

此 API 路径在我的游戏的 header 中使用,在元标记中用于在页面在 facebook 或 twitter 或任何地方共享时按需创建图像:

<meta property="og:image" content={`https://grumbly.games/api/${returnString}`} />

无论如何。又丑又黑,但对我有用。

最终的解决方案是

import path from 'path'
registerFont(path.resolve('./fonts/Anton-Regular.ttf'), { family: 'Anton' })`

path.resolve

我终于成功了,使用的是官方记录的配置,而不是乱七八糟的最佳答案!

首先,我假设您的无服务器函数位于 api/some_function.js,其中 api/ 文件夹位于项目根目录。

  1. api/中创建一个文件夹来放置静态文件,例如api/_files/。对我来说,我放了字体和图像文件。

  2. 把这个放在vercel.json:

{
  "functions": {
    "api/some_function.js": {
      "includeFiles": "_files/**"
    }
  }
}
  1. 现在 api/some_function.js,您可以使用 __dirname 来引用文件:
const { join } = require('path')
registerFont(join(__dirname, '_files/fonts/Anton-Regular.ttf'), { family: 'Anton' })

这是基于 this Vercel help page,只是我不得不弄清楚 _files/ 文件夹在您的项目目录结构中的位置,因为他们忘了提到它。