Canvas Image manipulation in Firebase Functions - node.js

I am trying to replicate some client-side code, using firebase functions. Basically, I am just trying to take a user image, create a circular version of it with a color around it.
Here is the code that I am currently using, which works on the client side of things, but not in Firebase Functions.
export async function createCanvasImage(userImage) {
console.log('createCanvasImage started...');
const canvas = new Canvas(60, 60);
const context = canvas.getContext('2d');
// Creates the blue user image.
async function blueCanvas() {
return new Promise((resolve, reject) => {
context.strokeStyle = '#488aff';
context.beginPath();
context.arc(30, 30, 28, 0, 2 * Math.PI);
context.imageSmoothingEnabled = false;
context.lineWidth = 3;
context.stroke();
context.imageSmoothingEnabled = false;
console.log('Canvas Image loaded!');
context.save();
context.beginPath();
context.arc(30, 30, 28, 0, Math.PI * 2, true);
context.closePath();
context.clip();
context.drawImage(userImage, 0, 0, 60, 60);
context.beginPath();
context.arc(0, 0, 28, 0, Math.PI * 2, true);
context.clip();
context.closePath();
context.restore();
const dataURL = canvas.toDataURL();
console.log(dataURL);
resolve(dataURL);
});
}
But it is, unfortunately, returning the following error:
Error creating one of the canvas images... TypeError: Image or Canvas expected
I suspect it's because the image isn't loading properly, but I'm not exactly sure how to get it to load on the server properly.
I was trying to use some code that is provided in the firebase function samples:
return mkdirp(tempLocalDir)
.then(() => {
// Download file from bucket.
return bucket.file(filePath)
.download({ destination: tempLocalFile });
})
.then(() => {
console.log('The file has been downloaded to', tempLocalFile);
createCanvasImage(tempLocalFile)
.catch(err => {console.log(err); });
})
.catch(err => { console.log(err); });

I think you'll be missing the DOM, unless you're importing libraries that aren't shown here. This is the same reason Nodejs doesn't have a window or document object. There is a good Q&A here Why doesn't node.js have a native DOM?
There is a node canvas implementation node-canvas which can include in your package.

If you weren't missing the DOM as Lex suggested, I found this and this post where the issue was the node-canvas version.
I only see Canvas in one module but if you were using more, read the explanation below I found here:
I can't find the other issue that discusses this more, but there is no
way to fix this in node-canvas itself because it's a limitation of
native addons (two different builds of node-canvas don't produce
compatible objects). At some level, your JS code needs to make sure
that it's using the same node-canvas everywhere. Without knowing
anything about your application, this may mean using npm v3 or later,
which has a flat(ter) node_modules directory; ensuring that everything
that depends on node-canvas is allowed to use the same version; and/or
providing a central wrapper for node-canvas that you require instead
of requiring node-canvas directly. The first two should hopefully Just
Work and not require code changes, but may be brittle.

Related

Problem with canvas using vega with nodejs (server side only)

I've been working for a few weeks now on a Discord bot that basically compiles stats on the server and deduces patterns. In order to improve it, I wanted to make it generate graphs as PNGs in order to send them back to the user - in short, no DOM.
In order to achieve this, I'm currenlty using vega (version 5.10.1 - latest) and node-canvas (version 2.6.1 - latest), with nodejs v12.16.1.
I've been scouring the web for help on vega usage, and found a couple contradicting sources. I've been using the example code provided here :
https://vega.github.io/vega/usage/
The thing is that I keep getting this error :
TypeError: Cannot read property 'getContext' of null
message:"Cannot read property 'getContext' of null"
stack:"TypeError: Cannot read property 'getContext' of null
at resize (e:\DEV\GIT REPOS\GITHUB\PERSO\JS\test-StatBot\node_modules\vega-scenegraph\build\vega-scenegraph.js:3665:28)
at CanvasRenderer.prototype$6.resize (e:\DEV\GIT REPOS\GITHUB\PERSO\JS\test-StatBot\node_modules\vega-scenegraph\build\vega-scenegraph.js:3714:5)
at CanvasRenderer.prototype$4.initialize (e:\DEV\GIT REPOS\GITHUB\PERSO\JS\test-StatBot\node_modules\vega-scenegraph\build\vega-scenegraph.js:3294:17)
at CanvasRenderer.prototype$6.initialize (e:\DEV\GIT REPOS\GITHUB\PERSO\JS\test-StatBot\node_modules\vega-scenegraph\build\vega-scenegraph.js:3709:28)
at initializeRenderer (e:\DEV\GIT REPOS\GITHUB\PERSO\JS\test-StatBot\node_modules\vega-view\build\vega-view.js:657:8)
at renderHeadless (e:\DEV\GIT REPOS\GITHUB\PERSO\JS\test-StatBot\node_modules\vega-view\build\vega-view.js:780:12)
at processTicksAndRejections (internal/process/task_queues.js:97:5)
at async View.renderToCanvas [as toCanvas] (e:\DEV\GIT REPOS\GITHUB\P...
Here is the code which is giving me trouble :
// Imports
const vega = require('vega');
// Render image from given graph spec (statsObject)
async function graphToImage (statsObject) {
graphObject = new vega.View(vega.parse(statsObject), { renderer: 'none'});
const pngName = generateFileName(10);
removeExistingFile(pngName);
graphObject.toCanvas().then(canvas => {
console.log('Writing PNG to file...');
writeFile(`../../../../generated/${pngName}.png`, canvas.toBuffer());
}).catch(err => {
console.log("Error writing PNG to file:");
console.error(err);
});
return pngName;
}
I don't really know how canvas or vega work, and so I have no idea what could be causing this issue and how to fix it... However, the problem seems to be located inside of the toCanvas() method. Any help is much appreciated !
Thanks in advance !
// Using view.toSVG() along with the npm package sharp worked well for me
const view = new vega.View(vega.parse(templateObject), {renderer: 'none'});
view.toSVG().then(async function (svg) {
await sharp(Buffer.from(svg))
.toFormat('png')
.toFile('fileName.png')
}).catch(function(err) {
console.error(err);
});
Edit : I managed to fix my issue, and I am posting the anwser here for future notice :
I succeeded to actually generate a graph picture by rendering the View object straight to an SVG string, by using view.toSVG() instead of the buggy view.toCanvas(), which worked great.
Then, all that was left to do was to convert the obtained SVG string into a PNG file, and that was it.
Here is the updated, working code :
// Imports
const vega = require('vega');
// Render image from given graph object
async function graphToImage (statsObject) {
// Generate a new 10-char hex string
const pngName = generateHexStringName(10);
// Remove any existing file with the same name in order to prevent confusion
removeExistingFile(pngName);
var view = new vega.View(vega.parse(statsObject), {renderer: 'none'});
// Generate an SVG string
view.toSVG().then(async function (svg) {
// Working SVG string
console.log(svg);
// Process obtained SVG string, e. g. write it to PNG file
}).catch(function(err) {
console.error(err);
});
// Return the name of the generated PNG file
return pngName;
}

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

I have a Node.js server that uses node-canvas to render text on an image on the server-side. Here is the repo: https://github.com/shawninder/meme-generator (just git clone, npm i and npm run dev to run locally).
As you'll notice in the code, I am loading the Anton font, which I got from here with the documented registerFont function provided by node-canvas
registerFont('./fonts/Anton-Regular.ttf', { family: 'Anton' })
Everything works like a charm locally, but when I deploy to Vercel (formerly known as zeit), that line throws an ENOENT error:
no such file or directory, lstat '/var/task/fonts'
Is there a path I can use here that will successfully load the font from within a Vercel function?
Can I find a single path that will work both locally and once deployed?
I had the same problem recently and I finally found a solution. I'm no guru, so someone will probably be able to suggest a better way, but here's what worked for me.
Because of how Vercel runs their serverless functions, a function doesn't really know anything about the rest of the project, or the public folder. This makes sense (because security), but it does make it tricky when you need the actual path to a file. You can import the font file no problem, the build process will give it a new name and put it on the disk (in /var/task ), but you can't access it. path.resolve(_font_name_) can see it, but you can't access it.
I ended up writing a very bad, separate api page that used path.join and fs.readdirSync to see what files are actually visible from the api page. One thing that is visible is a node_modules folder that contains the files for modules used on that api page.
fs.readdirSync(path.join(process.cwd(), 'node_modules/')
So what I did was write a local module, install it in my project, then import it into my api page. In the local module's package.json, I have a line "files": ["*"] so it will bundle all the module files into its node_modules folder (instead of just the .js files). In my module I have my font file and a function that copies it to /tmp (/tmp is readable and writable) then returns the path to the file, /tmp/Roboto-Regular.ttf.
On my api page, I include this module, then run it, and I pass the resultant path to registerfont.
It works. I'd share my code, but it's pretty sloppy right now, and I'd like to clean it up and try a couple things first (like I'm not sure if I need to copy it to /tmp, but I haven't tested it without that step). When I get it straightened out I'll edit this answer.
-- EDIT
Since I haven't been able to improve on my original solution, let me give some more details about what I did.
In my package.json I added a line to include a local module:
"dependencies": {
"canvas": "^2.6.1",
"fonttrick": "file:fonttrick",
In my project root, I have a folder "fonttrick". Inside the folder is another 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"
]
}
This is the only local module I've ever had to write; the keywords don't do anything, but at first I'd thought about putting it on NPM, so they're there.
The fonttrick folder also contains my font file (in this case "Roboto-Regular.ttf"), and a the main file, 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'
};
I ran npm install in this folder, and then fonttrick was available as a module in my main project (don't forget to run npm install there, too).
Since I only need to use this for API calls, the module is only used in one file, /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)
}
The important part of what this does is import fonttrick which puts a copy of the font in tmp, then returns the path to that file; the path to the font is then passed to the canvas drawing function (along with some other stuff; what to draw, how big to draw it, etc.)
My drawing function itself is in components/drawCanvas.js; here's the important stuff at the beginning (TLDR version: if it gets called from the API page, it gets a path to the font; if so, it uses that, otherwise the regular system fonts are available):
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')
This API path gets used in the header for my game, in the meta tags to create the image on demand when a page gets shared on facebook or twitter or wherever:
<meta property="og:image" content={`https://grumbly.games/api/${returnString}`} />
Anyway. Ugly and hacky, but it works for me.
I think you were very close with registerFont. Here’s what I got to work using your repo:
In 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()
//…
I added a new folder in pages/ called fonts/, and added the Anton folder downloaded from Google Fonts. Click “Download Family” to get the font file from here: https://fonts.google.com/specimen/Anton?query=Anton&selection.family=Anton&sidebar.open
The other file you downloaded (https://fonts.googleapis.com/css?family=Anton&display=swap) is actually the CSS file you’ll want to use the fonts client side in the browser, for your previewer.
At first, I would keep using the hosted version provided by Google Fonts. You can add that to the PreviewMeme.js component:
<link href="https://fonts.googleapis.com/css2?family=Anton" rel="stylesheet" />
<canvas id='meme' ref={canvas}></canvas>
(You might also want to use something like FontFaceObserver client side to make sure the font has loaded before rendering your canvas the first time.)
In writeText.js you’ll also then change the fontFamily to Anton:
const fontFamily = 'Anton'
That will make Anton available client side via the hosted Google Fonts, and it should be available to you as a file on the server for rendering with the server-side canvas package.
Hope that’s helpful!
The solution ended up being
import path from 'path'
registerFont(path.resolve('./fonts/Anton-Regular.ttf'), { family: 'Anton' })`
See path.resolve
I finally got this working, using officially-documented configurations rather than the hacky top answer!
First of all, I'm assuming your serverless function is at api/some_function.js, where the api/ folder is at the project root.
Create a folder in api/ to put static files into, such as api/_files/. For me, I put font and image files.
Put this in vercel.json:
{
"functions": {
"api/some_function.js": {
"includeFiles": "_files/**"
}
}
}
Now in api/some_function.js, you can use __dirname to reference the files:
const { join } = require('path')
registerFont(join(__dirname, '_files/fonts/Anton-Regular.ttf'), { family: 'Anton' })
This is based on this Vercel help page, except I had to figure out where the _files/ folder goes in your project directory structure because they forgot to mention that.

Present canvas interface in shared code on both server and client

I am trying to write shared code (that runs on both server and client) that uses an HTML canvas.
On the client, this should work perfectly fine. On the server, Node doesn't have a canvas (or a DOM), so I'd like to use the node-canvas plugin: https://github.com/Automattic/node-canvas.
However, I can't work out a way to access it that doesn't make webpack try to bundle node-canvas into my client-side code (which crashes webpack). Is there any way of loading node-canvas in such a way that I can reference it with the same code I'll use in the browser and without making webpack crash horribly?
My current effort, which did not work:
canvas.server.js
import Canvas from 'canvas';
const createCanvas = (width, height) => new Canvas(width, height);
export default createCanvas;
canvas.client.js
const createCanvas = (width, height) => {
const canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
return canvas;
};
export default createCanvas;
canvas.js
let createCanvas;
if (typeof document === 'undefined') {
// SERVER/node
createCanvas = require('./canvas.server.js');
} else {
// BROWSER
createCanvas = require('./canvas.client.js');
}
export default createCanvas;
in use:
import createCanvas from './canvas';
const canvasElement = createCanvas(width, height);
const ctx = canvasElement.getContext('2d');
Unfortunately, webpack still bundles in node-canvas.
Did you try requiring node-canvas only when the code is running in node?
If you do not actually call the require in front-end code, webpack will not bundle it. This means calling the actual require inside aforementioned conditional statement and not at the top of your file. This is important. Also, verify that you did not put node-canvas as an entry point in webpack.
Example:
// let's assume there is `isNode` variable as described in linked answer
let canvas;
if (isNode) {
const Canvas = require('canvas');
canvas = new Canvas(x, y);
else {
canvas = document.getElementById('canvas');
}
// get canvas context and draw on it
Edit after OP provided code example:
I've reproduced the exact structure in this WebpackBin to prove this should be working as explained above. After all, this is a feature of common.js (and other module loaders) and is therefore supported in webpack.
My changes to the code:
Replaced the code in canvas.client.js and canvas.server.js with console.log of what would be called at that line
Fixed wrong use of require with export default (should be require(...).default). Irrelevant, but had to do it to make the code work.
Removed the canvasElement.getContex call. Would not work in the example code and is irrelevant anyway, so there is no point of mocking it.

Watermarking an image on AWS Lambda with node.js and gm

I'm trying to resize and watermark an image (downloaded from S3) in an AWS Lambda function.
The resizing part is working well, based on the sample code from the "getting started" project of AWS Lambda.
Now, I have a problem adding a watermark to my files.
On my local system, I can do this:
gm('martinrose.jpg')
.draw(['image Over 0,0 0,0 wm-bas.png'])
.write('brol.jpg', function(e){
console.log(e||'done');
});
And it works without problem.
In the Lambda environment, I added the wm-bas.png file to the zip file uploaded to Amazon, and it seems to be found by my js code (I tested using lstatSync), but the real watermarking does not work.
Here is the relevant part of what I do:
gm(response.Body).size(function(err, size) {
var scalingFactor = Math.min(
newSize / size.width,
newSize / size.height
);
var width = scalingFactor * size.width;
var height = scalingFactor * size.height;
var fs = require('fs');
var stats = fs.lstatSync('wm-bas.png');
console.log(stats); // this outputs meaningful info, so, the file exists
var ctx = this.resize(width, height);//this works
if (shouldWatermark)
{
console.log("trying to watermark");
ctx = ctx.draw(['image Over 0,0 0,0 wm-bas.png']) //this doesn't work, although the previous log is written
}
ctx.toBuffer(imageType, function(err, buffer)
{
if (err) {
next(err);
} else {
next(null, response.ContentType, buffer);
}
}
);
});
What am I missing? Why doesn't this work? Is it related to the fact that I save in a buffer and not in a file?
I import gm with this code, BTW:
var gm = require('gm')
.subClass({ imageMagick: true });
You need to package all of your node_modules with your Lambda deployment. Install your modules locally in your project and package them with your Lambda code. Another very important fact is Amazon Lambda still relies on installed system libraries. Your Node.js module may be using a library that may not be installed on the system where Lambda is executed, you need to package everything with your Lambda deployment.
See this official post about modules and
Node.js packages in Lambda
I have used "sharp" library before to add a text watermark with a custom font using a Nodejs lambda function. I wrote a story on Medium you can read Watermark with an AWS lambda
//...
const textedSVG = Buffer.from(`<svg xmlns="http://www.w3.org/2000/svg"
xml:lang="en"
height="40"
width="200">
<text
font-family="MyFont"
font-style="italic"
x="0" y="20" font-size="16" fill="#fff">
${process.env.WATERMARK_TEXT}
</text></svg>`);
let imgDst = sharp(origimage.Body);
var buffer = await imgDst
.composite([
{
input: textedSVG,
gravity: "southeast",
},
])
// Use the Sharp module to resize the image and save in a buffer.
.resize(width)
.jpeg({ quality: 70 }) //decrease the image quality
.toBuffer();
//...

Write text on existing PNG with Node.js

I'm trying to create a simple dynamic-badge (png) to embed in static pages to let know the status of my application.
I'd like so to use an existing PNG image and write on it some text with Node.js.
I've found lot of libraries but all of them use Imagemagick or Cairo as native dependencies, I'd like to avoid to install anything else on the server.
I've then found lwip, but I can't really understand how to write text on an image with it. How can I do?
You could use Jimp. It has a print method:
var Jimp = require("jimp");
var fileName = 'test.png';
var imageCaption = 'Image caption';
var loadedImage;
Jimp.read(fileName)
.then(function (image) {
loadedImage = image;
return Jimp.loadFont(Jimp.FONT_SANS_16_BLACK);
})
.then(function (font) {
loadedImage.print(font, 10, 10, imageCaption)
.write(fileName);
})
.catch(function (err) {
console.error(err);
});
If you want to use a ttf file you can use gm
This will also align the text automatically so you don't have to keep track of your letter sizing / location.
const gm = require('gm').subClass({imageMagick: true});
gm('path/to/image.png')
.fill("#FFFFFF")
.font("./font.ttf", 20)
.drawText(15, 10, "your text", 'southeast') //can be any location
.write("./output.png", function (err) {
if (!err) console.log('done');
});

Resources