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 clone
、npm i
和 npm 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'
- 这里有我可以使用的路径,可以从 Vercel 函数中成功加载字体吗?
- 我能否找到既可在本地工作又可在部署后使用的单一路径?
我认为您与 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.join
和 fs.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' })`
我终于成功了,使用的是官方记录的配置,而不是乱七八糟的最佳答案!
首先,我假设您的无服务器函数位于 api/some_function.js
,其中 api/
文件夹位于项目根目录。
在api/
中创建一个文件夹来放置静态文件,例如api/_files/
。对我来说,我放了字体和图像文件。
把这个放在vercel.json
:
{
"functions": {
"api/some_function.js": {
"includeFiles": "_files/**"
}
}
}
- 现在
api/some_function.js
,您可以使用 __dirname
来引用文件:
const { join } = require('path')
registerFont(join(__dirname, '_files/fonts/Anton-Regular.ttf'), { family: 'Anton' })
这是基于 this Vercel help page,只是我不得不弄清楚 _files/
文件夹在您的项目目录结构中的位置,因为他们忘了提到它。
我有一个 Node.js 服务器,它使用 node-canvas 在服务器端的图像上呈现文本。这是回购协议:https://github.com/shawninder/meme-generator(仅 git clone
、npm i
和 npm 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'
- 这里有我可以使用的路径,可以从 Vercel 函数中成功加载字体吗?
- 我能否找到既可在本地工作又可在部署后使用的单一路径?
我认为您与 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.join
和 fs.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' })`
我终于成功了,使用的是官方记录的配置,而不是乱七八糟的最佳答案!
首先,我假设您的无服务器函数位于 api/some_function.js
,其中 api/
文件夹位于项目根目录。
在
api/
中创建一个文件夹来放置静态文件,例如api/_files/
。对我来说,我放了字体和图像文件。把这个放在
vercel.json
:
{
"functions": {
"api/some_function.js": {
"includeFiles": "_files/**"
}
}
}
- 现在
api/some_function.js
,您可以使用__dirname
来引用文件:
const { join } = require('path')
registerFont(join(__dirname, '_files/fonts/Anton-Regular.ttf'), { family: 'Anton' })
这是基于 this Vercel help page,只是我不得不弄清楚 _files/
文件夹在您的项目目录结构中的位置,因为他们忘了提到它。