2

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

正如您将在代码中注意到的那样,我正在加载Anton字体,这是我从这里registerFont获得的,带有node-canvas 提供的文档化函数

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

在本地,一切都像魅力一样,但是当我部署到Vercel(以前称为 zeit)时,该行会引发ENOENT错误:

没有这样的文件或目录,lstat '/var/task/fonts'

  • 有没有我可以在这里使用的路径,可以从 Vercel 函数中成功加载字体?
  • 我能否找到一条既能在本地工作又能在部署后工作的单一路径?
4

4 回答 4

3

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

由于 Vercel 运行其无服务器函数的方式,函数并不真正了解项目的其余部分或公用文件夹。这是有道理的(因为安全性),但是当您需要文件的实际路径时确实会变得很棘手。你可以导入字体文件没有问题,构建过程会给它一个新的名字并把它放在磁盘上(in /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可读和可写)然后返回文件路径的函数,/tmp/Roboto-Regular.ttf.

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

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

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

在我的 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 上,所以它们就在那里。

fonttrick 文件夹还包含我的字体文件(在本例中为“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 中,然后返回该文件的路径;然后将字体的路径传递给画布绘图函数(以及其他一些东西;绘制什么,绘制多大等)

我的绘图功能本身在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 路径在我的游戏的标题中使用,在元标记中用于在 facebook 或 twitter 或任何地方共享页面时按需创建图像:

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

反正。丑陋而骇人听闻,但它对我有用。

于 2020-05-03T22:36:39.720 回答
1

我认为你与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/我在called中添加了一个新文件夹fonts/,并添加了从 Google Fonts 下载的 Anton 文件夹。点击“Download Family”从这里获取字体文件: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客户端之类的东西,以确保在第一次渲染画布之前已加载字体。)

然后,writeText.js您还将更改fontFamily为 Anton:

const fontFamily = 'Anton'

这将使 Anton 通过托管的 Google 字体在客户端可用,并且它应该作为服务器上的文件提供给您,以便使用服务器端画布包进行渲染。

提供代码的“顶部”“底部”示例图像输出

希望这会有所帮助!

于 2020-04-16T23:23:06.467 回答
0

我终于得到了这个工作,使用官方记录的配置,而不是骇人听闻的最佳答案!

首先,我假设您的无服务器功能位于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' })

这是基于这个 Vercel 帮助页面,除了我必须弄清楚该_files/文件夹在您的项目目录结构中的位置,因为他们忘了提及这一点。

于 2021-05-02T21:15:41.023 回答
0

解决方案最终是

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

path.resolve

于 2020-08-27T19:28:06.200 回答