Problem with canvas using vega with nodejs (server side only) - node.js

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;
}

Related

Canvas Image manipulation in Firebase Functions

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.

Can't change leaflet setView params when using leaflet-headless

I need to generate a map on the server side using Nodejs and then create an image of that map. I'm using leaflet-headless to create the map and generate the image.
This is the code:
const L = require('leaflet-headless');
const document = global.document;
let createMap = (lanLat) => {
const element = document.createElement('div');
element.id = 'map-leaflet-image';
document.body.appendChild(element);
const filename = path.join(__dirname, '/leaflet-image.png');
const map = L.map(element.id).setView([0, 0], 3);
L.tileLayer('http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '© OpenStreetMap contributors'
}).addTo(map);
map.saveImage(filename, () => {
console.log('done');
})
};
This works and the image is saved but when I change the setView() parameters to setView([0,0], 1)(zoom out) I receive an error message:
return prev.apply(ctx, arguments);
Error: Image given has not completed loading
at Error (native)
at CanvasRenderingContext2D.ctx.(anonymous function) [as drawImage]
Any thoughts?
If this might interest someone, the problem was in the map.save() function which uses the leaflet-image lib.
This happened due to a weirdly specific scenario where a marker with certain coordinates, when added to the map with any other marker(?!), caused the error. I removed that marker and it worked.

Unable to use getElementsByTagName("body")

Here's the code that results in an error each time I run it. My goal is to scrap the content from the URL, remove all HTML, and return it:
console.log("Fetching: " + inputData.tweeturl);
fetch(inputData.tweeturl)
.then(function(res) {
return res.text();
}).then(function(body) {
var rawText = body.getElementsByTagName("body")[0].innerHTML;
var output = { id: 100, rawHTML: body, rawText: rawText };
callback(null, output);
})
.catch(callback);
The problem is with var rawText = body.getElementsByTagName("body")[0].innerHTML;
The error I receive is:
Bargle. We hit an error creating a run javascript. :-( Error:
TypeError: body.getElementsByTagName is not a function eval (eval at (/var/task/index.js:52:23), :16:24) process._tickDomainCallback (node.js:407:9)
Unfortunately - there is no JS DOM API in the Code by Zapier triggers or actions (that is because it isn't run in a browser and doesn't have the necessary libraries installed to fake it).
You might look at Python and instead, and https://docs.python.org/2/library/xml.etree.elementtree.html. Decent question and answer is available here Python Requests package: Handling xml response. Good luck!
Any function not supported by Zapier will result in a TypeError. I needed to use a regular expression to achieve this.

Typescript : Lost scope of 'this'

I'm trying to assign a value to myImage. When I look at the js target file myImage doesn't even exist. Obviously this throws an error. How do I preserve the scope of this within typescript classes?
What I'm trying to do here is load an image using the Jimp library. Then have a reference to that loaded image to perform operations on, for example resize is a method of a loaded image inside Jimp, so I'd like to be able to call i.myImage.resize(100,100).
"use strict";
var Promise = require('bluebird');
var Jimp = require("jimp/jimp");
class Image{
public myImage:any;
constructor(newImage:string){
Promise.all([new Jimp(newImage)]).then(function(img){
this.myImage = img;
}).catch(function(e){
console.log(e);
})
}
}
var i = new Image('./jd.jpg');
console.log(i.myImage)
The output:
undefined
[TypeError: Cannot set property 'myImage' of undefined]
With Callbacks :
var Jimp = require("jimp/jimp");
class Image {
public myImage: any;
constructor(newImage: string, typeOfImage: string) {
var self = this;
new Jimp(newImage, function(e,img) {
self.myImage = img;
});
}
}
var i = new Image('./jd.jpg');
console.log(i.myImage) // outputs undefined
The Jimp constructor does not return a promise so your use of Promise.all() here is likely not doing what you want it to do. As some people get confused by this, promises do not have any magic powers to somehow know when the Jimp object has finished loading it's image so if that's what you're trying to do here, it will not work that way. The Jimp constructor returns a Jimp object, not a promise.
I don't see any particular reason to not just use the Jimp callback that you pass into the constructor and make your code work like this:
var Jimp = require("jimp/jimp");
class Image {
private myImage: any;
constructor(newImage: string, typeOfImage: string) {
var self = this;
new Jimp(newImage, function(img) {
self.myImage = img;
// you can use the img here
});
// you cannot reliably use the img here
}
}
But, doing it this way looks like it still leaves all sorts of loose ends because the outside world has no way of knowing when the img has finished loading and you aren't saving the Jimp object reference itself so you can't use any of the Jimp methods on this image. As I said in my comments, if you share the bigger picture for what you're trying to do here, then we can more likely help you with a more complete solution to your problem.

Fabricjs+Node.js: fabric's canvas toDataURL calling toBuffer() not a method

I'm using the following code to try to see if I can load a canvas from a json string then generate a dataURL png for it:
var fabric=require('fabric');
var canvas = new fabric.fabric.Canvas();
var jsonStr='{"objects":[],"background":"rgba(0, 0, 0,0)","backgroundImage":"http://entropy.tmok.com/~gauze/canvas/any.gif","backgroundImageOpacity":1,"backgroundImageStretch":true,"overlayImage":"http://entropy.tmok.com/~gauze/canvas/frame.png","overlayImageLeft":0,"overlayImageTop":0}';
canvas.loadFromJSON(jsonStr);
img=canvas.toDataURL('png');
it errors on the toDataURL() line with:
/root/node-v0.8.16-linux-x86/node_modules/canvas/lib/canvas.js:190
return prefix + this.toBuffer().toString('base64');
^
which tells me 'this' (which is a Canvas according to console.log) doesn't have a .toBuffer() method. am I doing something wrong or is this a bug in fabric's node module?
thanks.
never mind I missed : .createCanvasForNode()
cant wait for documentation of this :P

Resources